diff --git a/agent/.dockerignore b/agent/.dockerignore new file mode 100644 index 0000000..0713fe3 --- /dev/null +++ b/agent/.dockerignore @@ -0,0 +1,7 @@ +** +!Dockerfile +!package.json +!package-lock.json +!tsconfig.json +!*.ts +!supervisor diff --git a/agent/.gitignore b/agent/.gitignore new file mode 100644 index 0000000..78eb301 --- /dev/null +++ b/agent/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +__pycache__/ diff --git a/agent/Dockerfile b/agent/Dockerfile new file mode 100644 index 0000000..4b11695 --- /dev/null +++ b/agent/Dockerfile @@ -0,0 +1,24 @@ +FROM node:22-bookworm-slim@sha256:83f487e0a63425e5b4d146fb5e5be574bcbe1b7b843d3ebafdd95eaf7767a7e5 AS build +WORKDIR /opt/agent +COPY package.json package-lock.json ./ +RUN npm ci --ignore-scripts +COPY tsconfig.json *.ts ./ +RUN npm run build && npm prune --omit=dev --ignore-scripts + +FROM node:22-bookworm-slim@sha256:83f487e0a63425e5b4d146fb5e5be574bcbe1b7b843d3ebafdd95eaf7767a7e5 +# Snapshot fixes apt dependency selection; no runtime installations or downloads. +RUN printf 'deb [check-valid-until=no] http://snapshot.debian.org/archive/debian/20260901T000000Z bookworm main\n' > /etc/apt/sources.list \ + && rm -f /etc/apt/sources.list.d/debian.sources \ + && apt-get -o Acquire::Check-Valid-Until=false update \ + && apt-get install -y --no-install-recommends python3 bash coreutils findutils grep sed gawk procps util-linux \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd -g 10001 agent && useradd -u 10001 -g 10001 -d /home/agent -M agent \ + && mkdir -p /work /home/agent /opt/agent && chown 10001:10001 /work /home/agent +COPY --from=build /opt/agent/node_modules /opt/agent/node_modules +COPY --from=build /opt/agent/dist /opt/agent/dist +COPY supervisor /opt/agent/supervisor +RUN chmod 0555 /opt/agent/supervisor +ENV HOME=/home/agent LANG=C.UTF-8 +USER 10001:10001 +WORKDIR /work +ENTRYPOINT ["/usr/bin/python3", "/opt/agent/supervisor"] diff --git a/agent/README.md b/agent/README.md new file mode 100644 index 0000000..635356b --- /dev/null +++ b/agent/README.md @@ -0,0 +1,81 @@ +# Confluence pi runtime + +This package wraps the unmodified pi SDK. It registers native Confluence tools +and the SDK's shell/read/write/edit tools, and supplies a custom model stream +function over revision-1 NDJSON. There are no provider credentials, remote +Confluence clients, CLI stdout scraping, or modifications to pi dependencies. + +From the repository root: + +```sh +npm --prefix agent ci --ignore-scripts +npm --prefix agent run build +npm --prefix agent test +npm --prefix agent run proof +``` + +The host proof uses an explicit development supervisor, fresh temporary workspace +and home, and a deterministic model/backend substitute. It runs real local +commands. The supervisor signals only its own descendants and adopts orphaned +children with Linux `PR_SET_CHILD_SUBREAPER`. Ordinary host `npm start` refuses +to launch: production supervision requires PID 1 in a private container PID +namespace. Do not add Docker `--init` or share the host PID namespace. + +Build and exercise the image against a rootless Docker daemon: + +```sh +docker build -t confluence-pi-agent:rev1 agent +node agent/dist/dev/fake-backend.js --image confluence-pi-agent:rev1 +node agent/dist/dev/image-checks.js +node agent/dist/dev/boundary-checks.js +``` + +The image-check suite includes a real, unchanged 180-second missing-start test. +It prints progress every 30 seconds. Boundary checks generate large payloads on +the host and send them to a runtime constrained to 1 GiB and one CPU. These test +commands need access to Docker and local supervisor sockets; restrictive execution +sandboxes can prevent them from running. + +`dev/fake-backend.ts` is an independent host peer that uses the public wire +contract. Add `--mode=...` for `no-artifacts`, `empty-file`, `skip`, `delayed`, +`background`, `spaces`, `confluence-error`, `upstream-failure`, `bad-reply`, `duplicate`, `eof-model`, +`eof-collection`, `eof-begin`, `eof-end`, `context-limit`, `output-limit`, +`zero-text`, `failure-during-collection`, `stalled-model`, or `stalled-collection`. `isolation` and +`stop-bridge` require `--image`. `--transcript` records dummy wire traffic. + +The default entrypoint is `/usr/bin/python3 /opt/agent/supervisor`; user/group +10001:10001, cwd `/work`, HOME `/home/agent`. Mount fresh owned tmpfs filesystems +at `/work` (256 MiB), `/tmp` (64 MiB), and `/home/agent` (32 MiB). Supply no bind +mounts or secret environment variables. `dev/fake-backend.ts` contains the complete +launch flags: read-only root, no network, no privileges/capabilities, default +seccomp, private namespaces, 1 GiB memory/no extra swap, 128 PIDs, one CPU, no TTY, +and no Docker logging. The backend owns launch, attachment, kill/removal, host +artifact storage, provider configuration, and upstream HTTP. + +Shell tools use `/bin/bash --noprofile --norc`, a 30-second maximum per command, +and 1 MiB captured output per invocation. Excess output terminates the command +and becomes a model-visible tool failure; pi retains its normal 50 KiB/2000-line +presentation truncation and temporary output file. Native read/edit operations +require regular files up to 16 MiB; use shell ranges for larger files. The shared +model boundary supports text and tools, so read never creates image attachments. +Tool results and input arguments also obey shared serialized protocol limits. + +Artifacts are collected only after the supervisor has killed and reaped local +descendants. Directory traversal uses verified directory descriptors through +Linux `/proc/self/fd`, with no-follow opens at each component. File descriptors +remain open through transfer; link/type/inode/size/time checks precede and follow +bounded reads. Names and Unicode-normalized duplicates are validated, file/count/ +aggregate limits produce bounded warnings, and a traversal budget prevents +unbounded directory enumeration. The backend still receives untrusted bytes and +must independently validate paths, sizes, storage and attachment delivery. + +See [HANDOFF.md](HANDOFF.md) for evidence, exact pins and outstanding integration +checks, and [REVIEW.md](REVIEW.md) for problems corrected in the initial code. + +Completion is committed only after the complete frame finishes writing. The bridge +timer is then cleared and no further terminal error is emitted; the supervisor's +independent lifetime remains armed. Failure exit allows up to one second for an +active NDJSON frame and the terminal error to drain. Not-yet-started frames are +cancelled, preserving complete lines rather than interleaving error bytes. If the +stream breaks or remains blocked, nonzero exit/EOF conveys failure. Repeated +warnings include occurrence counts; overflow is summarized within 100 entries. diff --git a/agent/artifacts.ts b/agent/artifacts.ts new file mode 100644 index 0000000..bfebdf7 --- /dev/null +++ b/agent/artifacts.ts @@ -0,0 +1,113 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { LIMITS, type Warning } from './types.js'; +import { WarningCollector } from './warnings.js'; + +export interface ArtifactFileCandidate { relPath: string; size: number; fd: number; stat: fs.Stats; } +export interface ArtifactScanResult { candidates: ArtifactFileCandidate[]; warnings: Warning[]; } +export function validateRelativeArtifactPath(name: string): { valid: boolean; reason?: string } { + const valid = typeof name === 'string' && !!name && !name.startsWith('/') && + !/[\\\x00-\x1f\x7f-\x9f]/.test(name) && Buffer.byteLength(name) <= LIMITS.MAX_PATH_BYTES && + name.split('/').every(c => !!c && c !== '.' && c !== '..'); + return valid ? { valid } : { valid: false, reason: 'Invalid relative artifact name' }; +} +const directoryFlags = fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW; +// Linux descriptor-relative traversal. /proc/self/fd is kernel-owned: following +// that magic link uses an already-open directory, never an agent symlink. +function at(fd: number, name: string): string { return `/proc/self/fd/${fd}/${name}`; } +function openRoot(root: string): number { + let fd = fs.openSync('/', directoryFlags); + try { + for (const component of path.resolve(root).split('/').filter(Boolean)) { + const next = fs.openSync(at(fd, component), directoryFlags); + fs.closeSync(fd); fd = next; + } + return fd; + } catch (err) { fs.closeSync(fd); throw err; } +} +export function closeArtifacts(candidates: ArtifactFileCandidate[]): void { + for (const c of candidates) { try { fs.closeSync(c.fd); } catch {} } +} +export function scanArtifactsDirectory(root: string): ArtifactScanResult { + const candidates: ArtifactFileCandidate[] = []; + const warnings = new WarningCollector(); + const warn = (code: string) => { + warnings.add({ code, message: 'An artifact was omitted during filesystem validation.' }); + }; + const seen = new Set(); + let total = 0, visited = 0; + function walk(fd: number, prefix: string): void { + const dir = fs.opendirSync(`/proc/self/fd/${fd}`); + try { + let item: fs.Dirent | null; + while ((item = dir.readSync())) { + if (++visited > 10000) { warn('artifact_scan_limit'); return; } + const name = prefix + item.name; + if (!validateRelativeArtifactPath(name).valid) { warn('artifact_invalid_path'); continue; } + const location = at(fd, item.name); + const stat = fs.lstatSync(location); + if (stat.isDirectory()) { + let next: number; + try { next = fs.openSync(location, directoryFlags); } catch { warn('artifact_open_failed'); continue; } + try { walk(next, name + '/'); } finally { fs.closeSync(next); } + if (visited > 10000) return; + continue; + } + if (!stat.isFile() || stat.nlink !== 1) { warn('artifact_not_regular'); continue; } + const normalized = name.normalize('NFC'); + if (seen.has(normalized)) { warn('artifact_duplicate_name'); continue; } + seen.add(normalized); + if (stat.size > LIMITS.ARTIFACT_MAX_FILE_BYTES) { warn('artifact_oversized'); continue; } + if (candidates.length >= LIMITS.ARTIFACT_MAX_FILES) { warn('artifact_count_exceeded'); continue; } + if (total + stat.size > LIMITS.ARTIFACT_MAX_TOTAL_BYTES) { warn('artifact_total_limit_exceeded'); continue; } + let file: number; + // O_NONBLOCK prevents a raced FIFO open from blocking the exporter. + try { file = fs.openSync(location, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK); } + catch { warn('artifact_open_failed'); continue; } + const opened = fs.fstatSync(file); + if (!opened.isFile() || opened.nlink !== 1 || opened.ino !== stat.ino || opened.dev !== stat.dev || opened.size !== stat.size) { + fs.closeSync(file); warn('artifact_descriptor_mismatch'); continue; + } + total += opened.size; + // Preserve the verified descriptor across backend acknowledgements. + candidates.push({ relPath: name, fd: file, size: opened.size, stat: opened }); + } + } finally { dir.closeSync(); } + } + let fd: number; + try { fd = openRoot(root); } catch { warn('artifact_root_rejected'); return { candidates, warnings: warnings.snapshot() }; } + try { walk(fd, ''); } catch { warn('artifact_scan_failed'); } finally { fs.closeSync(fd); } + return { candidates, warnings: warnings.snapshot() }; +} +export interface ArtifactTransferCallbacks { + sendBegin: (id: string, name: string, size: number) => Promise<'accept' | 'skip'>; + sendChunk: (id: string, index: number, data: string) => Promise; + sendEnd: (id: string, size: number, chunks: number) => Promise; +} +function unchanged(c: ArtifactFileCandidate): void { + const s = fs.fstatSync(c.fd); + if (!s.isFile() || s.nlink !== 1 || s.ino !== c.stat.ino || s.dev !== c.stat.dev || s.size !== c.size || + s.mtimeMs !== c.stat.mtimeMs || s.ctimeMs !== c.stat.ctimeMs) throw new Error('Artifact changed after validation'); +} +export async function exportArtifacts(candidates: ArtifactFileCandidate[], callbacks: ArtifactTransferCallbacks): Promise { + let stored = 0, seq = 0; + const buffer = Buffer.alloc(LIMITS.ARTIFACT_CHUNK_MAX_DECODED_BYTES); + for (const c of candidates) { + unchanged(c); + const id = `a_t_${++seq}`; + if (await callbacks.sendBegin(id, c.relPath, c.size) === 'skip') continue; + unchanged(c); + let offset = 0, chunks = 0; + while (offset < c.size) { + const n = fs.readSync(c.fd, buffer, 0, Math.min(buffer.length, c.size - offset), offset); + if (!n) throw new Error('Artifact truncated'); + offset += n; + await callbacks.sendChunk(id, chunks++, buffer.subarray(0, n).toString('base64')); + } + if (fs.readSync(c.fd, buffer, 0, 1, offset) !== 0) throw new Error('Artifact grew'); + unchanged(c); + await callbacks.sendEnd(id, offset, chunks); + stored++; + } + return stored; +} diff --git a/agent/bridge.ts b/agent/bridge.ts new file mode 100644 index 0000000..610f02b --- /dev/null +++ b/agent/bridge.ts @@ -0,0 +1,209 @@ +import { jsonBytes } from './json.js'; +import fs from 'node:fs'; +import { Agent } from '@earendil-works/pi-agent-core'; +import { localTools } from './local-tools.js'; +import { LIMITS, type Warning, type BridgeBaseMessage, type ModelRequest, type ModelResponse } from './types.js'; +import { NDJsonFrameParser, StreamMessageWriter, sanitizeError } from './framing.js'; +import { envelope, confluenceResult } from './validation.js'; +import { createModelProvider } from './model-provider.js'; +import { createConfluenceTools } from './confluence-tools.js'; +import { scanArtifactsDirectory, exportArtifacts, closeArtifacts } from './artifacts.js'; +import { supervisorChannel } from './supervision.js'; +import { WarningCollector } from './warnings.js'; + +export type BridgeState = 'INIT' | 'RUNNING' | 'COLLECTING' | 'COMPLETE' | 'FAILED'; +export interface BridgeOptions { + stdin?: NodeJS.ReadableStream; stdout?: NodeJS.WritableStream; + workDir?: string; artifactsDir?: string; + onChildReap?: () => Promise; onSetDeadline?: (ms: number) => void; +} +type Pending = { type: string; tool?: string; transfer?: string; decisions?: string[]; resolve: (x: any) => void; reject: (e: Error) => void }; +export class Bridge { + private state: BridgeState = 'INIT'; + private seq = 0; + private seen = new Set(); + private pending = new Map(); + private writer: StreamMessageWriter; + private parser = new NDJsonFrameParser(); + private input: NodeJS.ReadableStream; + private work: string; + private artifacts: string; + private agent?: Agent; + private warnings = new WarningCollector(); + private completing = false; + private inputEnded = false; + private timer?: NodeJS.Timeout; + private expires = Date.now() + LIMITS.MAX_DEADLINE_MS; + private modelCalls = 0; + private toolCalls = 0; + private settled = false; + private resolve!: () => void; + private reject!: (e: Error) => void; + constructor(private options: BridgeOptions = {}) { + this.input = options.stdin || process.stdin; + this.writer = new StreamMessageWriter(options.stdout || process.stdout); + this.work = options.workDir || '/work'; + this.artifacts = options.artifactsDir || `${this.work}/artifacts`; + } + nextId(): string { return `a_${++this.seq}`; } + getState(): BridgeState { return this.state; } + addWarning(w: Warning): void { this.warnings.add(w); } + private arm(ms: number): void { + this.expires = Math.min(this.expires, Date.now() + ms); + clearTimeout(this.timer); + this.timer = setTimeout(() => this.failRun('query_timeout'), Math.max(1, this.expires - Date.now())); + } + start(): Promise { + const done = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); + this.arm(LIMITS.MAX_DEADLINE_MS); + this.parser.on('data', frame => { + try { this.inbound(frame); } catch { this.failRun('invalid_input'); } + }); + this.parser.on('error', () => this.failRun('invalid_input')); + this.input.on('error', () => this.failRun('connectivity_failed')); + this.input.on('close', () => { if (this.state !== 'COMPLETE' && this.state !== 'FAILED' && !this.inputEnded) this.failRun('connectivity_failed'); }); + this.parser.on('end', () => { + this.inputEnded = true; + if (this.state === 'COMPLETE') this.finishEOF(); + else if (!this.completing) this.failRun('connectivity_failed'); + // A peer can close stdin synchronously after observing complete, before + // the write callback. Wait for that write to succeed before resolving. + }); + this.input.pipe(this.parser); + return done; + } + private finishEOF(): void { + if (this.settled) return; + this.settled = true; this.resolve(); + } + private inbound(frame: BridgeBaseMessage): void { + if (this.state === 'FAILED') return; + envelope(frame); + if (this.seen.has(frame.id) || this.seen.size >= 512) throw new Error('Duplicate or excess IDs'); + this.seen.add(frame.id); + if (frame.type === 'start') { + if (this.state !== 'INIT') throw new Error('Unexpected start'); + this.state = 'RUNNING'; + this.arm(frame.payload.remaining_ms); + this.options.onSetDeadline?.(frame.payload.remaining_ms); + this.run(frame.payload).catch(() => this.failRun('execution_failed')); + return; + } + const p = this.pending.get(frame.reply_to!); + if (!p || p.type !== frame.type) throw new Error('Unknown or wrong response'); + if (frame.type === 'artifact_ack' && (p.transfer !== frame.payload.transfer_id || !p.decisions!.includes(frame.payload.decision))) throw new Error('Wrong artifact acknowledgement'); + if (frame.type === 'tool_response' && frame.payload.error === null) confluenceResult(frame.payload.result, p.tool!); + this.pending.delete(frame.reply_to!); + if (frame.payload.error) { + const error = sanitizeError(frame.payload.error.code); + // Model errors are terminal, while recoverable Confluence failures are + // returned to the SDK as ordinary failed tool calls. + p.reject(new Error(error.message)); + if (frame.type === 'model_response') this.failRun(error.code); + } else p.resolve(frame.payload); + } + private async send(type: string, payload: any): Promise { + if (this.state === 'FAILED') throw new Error('Failed run'); + await this.writer.write({ v: 1, type, id: this.nextId(), payload }); + } + private request(type: string, payload: any, response: string, transfer?: string, decisions?: string[]): Promise { + if (this.state === 'FAILED') return Promise.reject(new Error('Failed run')); + const id = this.nextId(); + return new Promise((resolve, reject) => { + this.pending.set(id, { type: response, tool: payload.tool, transfer, decisions, resolve, reject }); + this.writer.write({ v: 1, type, id, payload }).catch(() => this.failRun('connectivity_failed')); + }); + } + async sendToolRequest(tool: string, parameters: Record): Promise { + if (this.state !== 'RUNNING' || this.pending.size >= 4 || ++this.toolCalls > 100) throw new Error('Tool call limit'); + const payload = { tool, parameters }; + if (jsonBytes(payload, LIMITS.TOOL_REQUEST_PAYLOAD_MAX_BYTES) > LIMITS.TOOL_REQUEST_PAYLOAD_MAX_BYTES) throw new Error('Tool payload limit'); + return (await this.request('tool_request', payload, 'tool_response')).result; + } + async sendModelRequest(request: ModelRequest): Promise { + if (this.state !== 'RUNNING' || this.pending.size >= 4 || ++this.modelCalls > 50) { this.failRun('execution_failed'); throw new Error('Model call limit'); } + if (jsonBytes(request, LIMITS.MODEL_REQUEST_PAYLOAD_MAX_BYTES) > LIMITS.MODEL_REQUEST_PAYLOAD_MAX_BYTES) { this.failRun('model_context_exceeded'); throw new Error('Model payload limit'); } + return (await this.request('model_request', request, 'model_response')).result; + } + private async run(start: any): Promise { + fs.mkdirSync(this.artifacts, { recursive: true }); + const { model, streamFn } = createModelProvider(start.model, async request => { + const response = await this.sendModelRequest(request); + if (response.usage.input_tokens > start.model.context_window_tokens) { this.failRun('model_context_exceeded'); throw new Error('Context limit'); } + if (response.stop_reason === 'length' || response.usage.output_tokens > start.model.max_output_tokens) { this.failRun('model_output_limit'); throw new Error('Output limit'); } + return response; + }); + this.agent = new Agent({ initialState: { + systemPrompt: start.system_instruction, model, + tools: [...createConfluenceTools((name, args) => this.sendToolRequest(name, args)), ...localTools(this.work)], + }, streamFn, toolExecution: 'sequential' }); + await this.agent.prompt(start.prompt); + await this.agent.waitForIdle(); + if (this.state !== 'RUNNING') return; + if (this.pending.size) throw new Error('Unanswered calls'); + const final = this.agent.state.messages.at(-1); + if (!final || final.role !== 'assistant' || final.stopReason !== 'stop') throw new Error('Invalid final state'); + const markdown = final.content.filter(x => x.type === 'text').map(x => x.text).join(''); + if (!markdown.trim()) throw new Error('Empty final answer'); + if (Buffer.byteLength(markdown) > LIMITS.FINAL_MARKDOWN_MAX_BYTES) throw new Error('Answer too large'); + this.agent.clearAllQueues(); this.agent.abort(); + // No host /proc fallback: collection requires a supervisor or an explicit + // test hook. Production runs always use the immutable PID-1 supervisor. + if (!this.options.onChildReap) throw new Error('Missing process supervisor'); + await this.options.onChildReap(); + if (this.state !== 'RUNNING') return; + const scan = scanArtifactsDirectory(this.artifacts); + try { + scan.warnings.forEach(w => this.addWarning(w)); + this.state = 'COLLECTING'; + await this.request('collection_start', { markdown, warnings: this.warnings.snapshot() }, 'collection_ready'); + const count = await exportArtifacts(scan.candidates, { + sendBegin: async (transfer_id, name, size_bytes) => { + const ack = await this.request('artifact_begin', { transfer_id, name, size_bytes }, 'artifact_ack', transfer_id, ['accept', 'skip']); + return ack.decision; + }, + sendChunk: async (transfer_id, index, data_base64) => this.send('artifact_chunk', { transfer_id, index, data_base64 }), + sendEnd: async (transfer_id, size_bytes, chunks) => { + await this.request('artifact_end', { transfer_id, size_bytes, chunks }, 'artifact_ack', transfer_id, ['stored']); + }, + }); + if (this.getState() === 'FAILED') return; + await this.writer.write({ v: 1, type: 'complete', id: this.nextId(), payload: { accepted_transfer_count: count } }, () => { this.completing = true; }); + if (this.getState() === 'FAILED') return; + this.state = 'COMPLETE'; + clearTimeout(this.timer); + if (this.inputEnded) this.finishEOF(); + } finally { closeArtifacts(scan.candidates); } + } + async flushOutput(timeoutMs = 1000): Promise { + let timer: NodeJS.Timeout | undefined; + try { + return await Promise.race([ + this.writer.close().then(() => true, () => false), + new Promise(resolve => { timer = setTimeout(() => resolve(false), timeoutMs); }), + ]); + } finally { clearTimeout(timer); } + } + failRun(code: string, _raw?: unknown): void { + if (this.state === 'FAILED' || this.state === 'COMPLETE' || this.settled) return; + this.state = 'FAILED'; clearTimeout(this.timer); + this.agent?.clearAllQueues(); this.agent?.abort(); + const error = sanitizeError(code); + for (const p of this.pending.values()) p.reject(new Error(error.message)); + this.pending.clear(); + // Failure settlement does not depend on a peer continuing to drain stdout. + this.writer.writeTerminal({ v: 1, type: 'error', id: this.nextId(), payload: error }).catch(() => {}); + this.settled = true; this.reject(new Error(error.message)); + } +} +if (/\bbridge\.(ts|js)$/.test(process.argv[1] || '')) { + const channel = supervisorChannel(); + const bridge = new Bridge({ workDir: process.env.AGENT_WORK_DIR || '/work', + onChildReap: channel?.reap, onSetDeadline: channel?.deadline }); + bridge.start().then(() => process.exit(0)).catch(async () => { + // Flush complete NDJSON lines within a bounded budget. The supervisor's + // independent deadline still owns the maximum container lifetime. + await bridge.flushOutput(); + process.exit(1); + }); +} diff --git a/agent/confluence-tools.ts b/agent/confluence-tools.ts new file mode 100644 index 0000000..5833038 --- /dev/null +++ b/agent/confluence-tools.ts @@ -0,0 +1,104 @@ +import { Type } from "typebox"; +import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core"; + +export type SendToolRequestFn = ( + toolName: string, + parameters: Record +) => Promise; + +/** + * Creates the three native Confluence tools for the pi runtime. + * They execute purely via bridge requests; no remote HTTP clients or credentials exist here. + */ +export function createConfluenceTools(sendToolRequest: SendToolRequestFn): AgentTool[] { + const searchTool: AgentTool = { + name: "confluence_search", + label: "confluence_search", + description: + "Search Confluence documentation using plain keywords. Returns matching pages, space keys, canonical URLs, and snippets.", + parameters: Type.Object({ + query: Type.String({ + description: "Search keywords (plain text, escaped into CQL by backend)" + }), + space: Type.Optional( + Type.String({ + maxLength: 256, + description: "Optional space key filter (up to 256 UTF-8 bytes)" + }) + ), + limit: Type.Optional( + Type.Integer({ + minimum: 1, + maximum: 50, + description: "Maximum number of pages to return (1–50, default 10)" + }) + ), + offset: Type.Optional( + Type.Integer({ + minimum: 0, + maximum: 10000, + description: "Pagination offset (default 0, maximum 10,000)" + }) + ) + }, { additionalProperties: false }), + execute: async (_toolCallId, params): Promise> => { + const result = await sendToolRequest("confluence_search", params as Record); + return { + content: [{ type: "text", text: JSON.stringify(result) }], + details: result + }; + } + }; + + const viewTool: AgentTool = { + name: "confluence_view", + label: "confluence_view", + description: + "Read a Confluence page by numeric page ID. Returns page title, space, canonical URL, and Markdown content.", + parameters: Type.Object({ + page_id: Type.String({ + pattern: "^[0-9]+$", + description: "Numeric page ID string (e.g. '847291')" + }) + }, { additionalProperties: false }), + execute: async (_toolCallId, params): Promise> => { + const result = await sendToolRequest("confluence_view", params as Record); + return { + content: [{ type: "text", text: JSON.stringify(result) }], + details: result + }; + } + }; + + const listSpacesTool: AgentTool = { + name: "confluence_list_spaces", + label: "confluence_list_spaces", + description: + "List available Confluence spaces. Returns space keys and display names.", + parameters: Type.Object({ + limit: Type.Optional( + Type.Integer({ + minimum: 1, + maximum: 50, + description: "Maximum number of spaces to return (1–50, default 25)" + }) + ), + offset: Type.Optional( + Type.Integer({ + minimum: 0, + maximum: 10000, + description: "Pagination offset (default 0, maximum 10,000)" + }) + ) + }, { additionalProperties: false }), + execute: async (_toolCallId, params): Promise> => { + const result = await sendToolRequest("confluence_list_spaces", params as Record); + return { + content: [{ type: "text", text: JSON.stringify(result) }], + details: result + }; + } + }; + + return [searchTool, viewTool, listSpacesTool]; +} diff --git a/agent/dev/boundary-checks.ts b/agent/dev/boundary-checks.ts new file mode 100644 index 0000000..52e947b --- /dev/null +++ b/agent/dev/boundary-checks.ts @@ -0,0 +1,52 @@ +/** Host-side generated boundary frames; no runtime-private function calls. */ +import { spawn } from 'node:child_process'; +import { createInterface } from 'node:readline'; +import assert from 'node:assert/strict'; +import { dockerFlags } from './fake-backend.js'; +const image = process.argv[2] || 'confluence-pi-agent:rev1'; +const MiB = 1048576; +async function check(mode: string) { + const name = `pi-boundary-${process.pid}-${mode}`; + const child = spawn('docker', ['run', '--name', name, ...dockerFlags, image], { stdio: ['pipe', 'pipe', 'pipe'] }); + const exited = new Promise((resolve, reject) => { child.on('exit', resolve); child.on('error', reject); }); + let seq = 0, complete = false, error: any, peerError: Error | undefined; + let stderr = ''; + child.stderr.on('data', c => { stderr += c.toString().slice(0, Math.max(0, 2048 - stderr.length)); }); + const send = (type: string, payload: any, reply_to?: string) => child.stdin.write(JSON.stringify({ v: 1, type, id: `b_${++seq}`, ...(reply_to ? { reply_to } : {}), payload }) + '\n'); + let markdownBytes = 0; + const lines = createInterface({ input: child.stdout }); + lines.on('line', line => { + try { + const f = JSON.parse(line); + if (f.type === 'model_request') { + assert.equal(Buffer.byteLength(f.payload.messages[0].content[0].text), 16 * MiB); + const result = { content: [{ type: 'text', text: '' }], stop_reason: 'stop', usage: { input_tokens: 1, output_tokens: 1 } }; + const budget = 128 * MiB - Buffer.byteLength(JSON.stringify({ result, error: null })); + result.content[0].text = mode === 'response-exact' || mode === 'response-over' ? 'x'.repeat(budget + (mode === 'response-over' ? 1 : 0)) : 'Small answer.'; + markdownBytes = Buffer.byteLength(result.content[0].text); + send('model_response', { result, error: null }, f.id); + } else if (f.type === 'collection_start') { + assert.equal(Buffer.byteLength(f.payload.markdown), markdownBytes); + send('collection_ready', {}, f.id); + } else if (f.type === 'complete') { + assert.equal(f.payload.accepted_transfer_count, 0); complete = true; child.stdin.end(); + } else if (f.type === 'error') { error = f.payload; child.stdin.end(); } + else throw new Error('Unexpected boundary frame'); + } catch (err) { peerError = err as Error; child.stdin.end(); } + }); + const timer = setTimeout(() => { peerError = new Error('Boundary peer timeout'); child.stdin.end(); }, 125000); + // Control characters exercise sixfold JSON escaping at the decoded prompt limit. + send('start', { prompt: '\u0001'.repeat(16 * MiB + (mode === 'prompt-over' ? 1 : 0)), system_instruction: 'Dummy boundary test.', remaining_ms: 120000, + model: { id: 'scripted-boundary', context_window_tokens: 100000000, max_output_tokens: 1000000 } }); + try { + const code = await exited; + if (peerError) throw peerError; + if (mode.endsWith('-over')) { assert(!complete); assert.notEqual(code, 0); assert(error, stderr || 'Runtime exited without a controlled boundary error'); } + else { assert(complete, stderr || JSON.stringify(error) || `Runtime exit ${code}; possible resource failure`); assert.equal(code, 0); } + console.log(`PASS image ${mode}; markdown_bytes=${markdownBytes}`); + } finally { + clearTimeout(timer); child.stdin.destroy(); + await new Promise(resolve => { const cleanup = spawn('docker', ['rm', '-f', name], { stdio: 'ignore' }); cleanup.on('exit', resolve); }); + } +} +for (const mode of ['prompt-exact', 'prompt-over', 'response-exact', 'response-over']) await check(mode); diff --git a/agent/dev/fake-backend.ts b/agent/dev/fake-backend.ts new file mode 100644 index 0000000..2becf60 --- /dev/null +++ b/agent/dev/fake-backend.ts @@ -0,0 +1,200 @@ +/** Independent trusted-peer substitute: only the public NDJSON contract. */ +import { spawn } from 'node:child_process'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createInterface } from 'node:readline'; +import { createHash } from 'node:crypto'; +import assert from 'node:assert/strict'; + +const parentDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const root = path.basename(parentDir) === 'dist' ? path.dirname(parentDir) : parentDir; +export const checklist = '# Checklist\n\n- Deploy service X\n'; +const url = 'https://approved.example.com/pages/viewpage.action?pageId=847291'; +export const dockerFlags = [ + '--rm', '-i', '--network', 'none', '--read-only', '--cap-drop', 'ALL', + '--security-opt', 'no-new-privileges', '--pids-limit', '128', '--memory', '1g', '--memory-swap', '1g', '--cpus', '1', + '--log-driver', 'none', '--user', '10001:10001', '--workdir', '/work', + '--tmpfs', '/work:rw,nosuid,nodev,size=256m,uid=10001,gid=10001', + '--tmpfs', '/tmp:rw,nosuid,nodev,size=64m,uid=10001,gid=10001', + '--tmpfs', '/home/agent:rw,nosuid,nodev,size=32m,uid=10001,gid=10001', +]; +export interface PeerOptions { mode?: string; image?: string; timeoutMs?: number; record?: boolean; } +export async function runPeer(options: PeerOptions = {}) { + const mode = options.mode || 'happy'; + if (['isolation', 'stop-bridge'].includes(mode) && !options.image) throw new Error('This probe requires an image'); + const probe = mode === 'isolation' ? await fs.readFile(path.join(root, 'dev/isolation-probe.py'), 'utf8') : ''; + const scratch = await fs.mkdtemp(path.join(os.tmpdir(), 'pi-peer-')); + const work = options.image ? '/work' : path.join(scratch, 'work'); + const home = path.join(scratch, 'home'); + await fs.mkdir(home); await fs.mkdir(path.join(scratch, 'work')); + const name = `pi-proof-${process.pid}-${Date.now()}`; + const child = options.image ? spawn('docker', ['run', '--name', name, ...dockerFlags, options.image], { stdio: ['pipe', 'pipe', 'pipe'] }) : + spawn('python3', [path.join(root, 'supervisor'), '--dev'], { env: { + PATH: process.env.PATH, HOME: home, LANG: 'C.UTF-8', AGENT_WORK_DIR: work, + }, stdio: ['pipe', 'pipe', 'pipe'] }); + let stderr = ''; + child.stderr.on('data', data => { if (stderr.length < 4096) stderr += data.toString().slice(0, 4096 - stderr.length); }); + const transcript: any[] = []; + const seen = new Set(); + const artifacts = new Map(); + let inboundSeq = 0, turns = 0, stored = 0, collecting = false, complete = false; + let active: { id: string; name: string; size: number; chunks: Buffer[] } | undefined; + let failure: any; + const send = (type: string, payload: any, reply_to?: string) => { + const frame = { v: 1, type, id: `b_${++inboundSeq}`, ...(reply_to ? { reply_to } : {}), payload }; + transcript.push(frame); + child.stdin.write(JSON.stringify(frame) + '\n'); + }; + let injectedFailure = false; + child.stdout.on('data', chunk => { + if (mode === 'failure-during-collection' && !injectedFailure && chunk.includes('"type":"collection_start"')) { + injectedFailure = true; + child.stdout.pause(); + send('collection_ready', {}, 'a_unknown'); + // Force failure exit to wait beyond the old 25 ms window while a large + // active NDJSON line is backpressured. Then let it finish and read error. + setTimeout(() => child.stdout.resume(), 120); + } + }); + const tools: Array<[string, Record]> = [ + ['confluence_search', { query: 'deploy service X' }], + ['confluence_view', { page_id: '847291' }], + ['write', { path: `${work}/scratch.md`, content: checklist.replace('Deploy', 'Stage') }], + ['read', { path: `${work}/scratch.md` }], + ['edit', { path: `${work}/scratch.md`, edits: [{ oldText: 'Stage', newText: 'Deploy' }] }], + ['bash', { command: mode === 'isolation' ? `set -e\npython3 - <<'PYPROBE'\n${probe}\nPYPROBE\ncp '${work}/scratch.md' '${work}/artifacts/checklist.md'` : mode === 'stop-bridge' ? 'kill -STOP "$PPID"; sleep 10' : mode === 'background' ? + `cp '${work}/scratch.md' '${work}/artifacts/checklist.md'; python3 -c 'import os,time; os.setsid(); pid=os.fork(); os._exit(0) if pid else None; f=open("${work}/artifacts/writer.log","a"); exec("while True:\\n f.write(\\\"tick\\\\n\\\"); f.flush(); time.sleep(0.01)")' >/dev/null 2>&1 &` : + mode === 'no-artifacts' ? 'true' : mode === 'empty-file' ? `touch '${work}/artifacts/empty.txt'` : + `cp '${work}/scratch.md' '${work}/artifacts/checklist.md'; printf 'shell output stays local\\n'` }], + ['read', { path: `${work}/scratch.md` }], + ]; + if (mode === 'spaces') tools.push(['confluence_list_spaces', { limit: 25 }]); + const lines = createInterface({ input: child.stdout }); + const exited = new Promise((resolve, reject) => { + child.on('exit', resolve); child.on('error', reject); + }); + let serial = Promise.resolve(); + let peerError: Error | undefined; + lines.on('line', line => { + serial = serial.then(async () => { + const frame = JSON.parse(line); + transcript.push(frame); + assert.equal(frame.v, 1); assert.match(frame.id, /^a_[A-Za-z0-9_-]+$/); + assert(!seen.has(frame.id)); seen.add(frame.id); + if (frame.type === 'model_request') { + assert(!collecting); + assert(!frame.payload.messages.some((m: any) => m.role === 'system')); + assert.equal(frame.payload.messages[0].content[0].text, 'Research deploy service X and create a checklist.'); + for (const tool of tools) assert(frame.payload.tools.some((t: any) => t.name === tool[0])); + assert(frame.payload.tools.find((t: any) => t.name === 'edit').input_schema.properties.edits); + if (turns) { + const last = frame.payload.messages.at(-1); + assert.equal(last.role, 'tool'); assert.equal(last.tool_call_id, `sdk_${turns}`); + assert.equal(last.is_error, mode === 'confluence-error' && turns === 2, last.content); + const assistant = frame.payload.messages.filter((m: any) => m.role === 'assistant').at(-1); + assert.equal(assistant.provider_state, `state_${turns}`); + if (turns === 4 || turns === 7) assert(last.content.includes(turns === 4 ? 'Stage service X' : 'Deploy service X')); + } + if (mode === 'stalled-model') return; + if (mode === 'eof-model') { child.stdin.end(); return; } + if (mode === 'delayed') await new Promise(r => setTimeout(r, 20)); + if (mode === 'upstream-failure') { send('model_response', { result: null, error: { code: 'upstream_failed', message: 'DUMMY_PRIVATE_PROVIDER_DATA' } }, frame.id); return; } + const result = { + content: turns < tools.length ? [{ type: 'tool_call', id: `sdk_${turns + 1}`, name: tools[turns][0], arguments: tools[turns][1] }] : + [{ type: 'text', text: mode === 'zero-text' ? '' : `[Deployment Guide](${url}). See checklist.md.` + (mode === 'failure-during-collection' ? 'x'.repeat(1024 * 1024) : '') }], + stop_reason: mode === 'output-limit' ? 'length' : turns < tools.length ? 'tool_calls' : 'stop', + usage: { input_tokens: mode === 'context-limit' ? 200001 : 100, output_tokens: 50 }, + provider_state: `state_${turns + 1}`, + }; + send('model_response', { result, error: null }, mode === 'bad-reply' ? 'a_unknown' : frame.id); + if (mode === 'duplicate') send('model_response', { result, error: null }, frame.id); + turns++; + } else if (frame.type === 'tool_request') { + assert(!collecting); assert.notEqual(frame.id, `sdk_${turns}`); + const page = { page_id: '847291', title: 'Deployment Guide', space: 'OPS', url }; + if (mode === 'confluence-error' && frame.payload.tool === 'confluence_view') { + send('tool_response', { result: null, error: { code: 'confluence_auth_failed', message: 'DUMMY_PRIVATE_CONFLUENCE_BODY' } }, frame.id); + return; + } + const result = frame.payload.tool === 'confluence_list_spaces' ? { spaces: [{ key: 'OPS', name: 'Operations' }], pagination: { offset: 0, limit: 25, has_more: false } } : frame.payload.tool === 'confluence_search' ? + { pages: [{ ...page, snippet: 'Deployment steps.' }], pagination: { offset: 0, limit: 10, has_more: false } } : + { ...page, markdown: 'Deploy service X using the release checklist.', truncated: false }; + send('tool_response', { result, error: null }, frame.id); + } else if (frame.type === 'collection_start') { + assert(!collecting); collecting = true; + assert(frame.payload.markdown.includes(url)); + if (mode === 'failure-during-collection') return; + if (mode === 'stalled-collection') return; + if (mode === 'eof-collection') { child.stdin.end(); return; } + if (mode === 'background' && options.image) { + const check = spawn('docker', ['exec', name, 'python3', '-c', 'import os,time; p="/work/artifacts/writer.log"; before=os.stat(p).st_size if os.path.exists(p) else 0; time.sleep(0.1); after=os.stat(p).st_size if os.path.exists(p) else 0; assert before == after']); + assert.equal(await new Promise(resolve => check.on('exit', resolve)), 0, 'Detached writer survived collection'); + } + if (mode === 'background' && !options.image) { + const log = `${work}/artifacts/writer.log`; + const before = await fs.stat(log).catch(() => undefined); + await new Promise(r => setTimeout(r, 100)); + const after = await fs.stat(log).catch(() => undefined); + assert.equal(after?.size, before?.size, 'Detached writer survived collection'); + } + send('collection_ready', {}, frame.id); + } else if (frame.type === 'artifact_begin') { + assert(collecting && !active); + if (mode === 'eof-begin') { child.stdin.end(); return; } + if (mode === 'skip') send('artifact_ack', { transfer_id: frame.payload.transfer_id, decision: 'skip', warning: null }, frame.id); + else { + active = { id: frame.payload.transfer_id, name: frame.payload.name, size: frame.payload.size_bytes, chunks: [] }; + send('artifact_ack', { transfer_id: active.id, decision: 'accept', warning: null }, frame.id); + } + } else if (frame.type === 'artifact_chunk') { + assert(active); assert.equal(frame.payload.transfer_id, active.id); assert.equal(frame.payload.index, active.chunks.length); + const data = Buffer.from(frame.payload.data_base64, 'base64'); + assert.equal(data.toString('base64'), frame.payload.data_base64); assert(data.length <= 65536); + active.chunks.push(data); + } else if (frame.type === 'artifact_end') { + assert(active); assert.equal(frame.payload.transfer_id, active.id); assert.equal(frame.payload.chunks, active.chunks.length); + const bytes = Buffer.concat(active.chunks); + assert.equal(bytes.length, active.size); assert.equal(frame.payload.size_bytes, bytes.length); + if (mode === 'eof-end') { child.stdin.end(); return; } + artifacts.set(active.name, bytes); stored++; + send('artifact_ack', { transfer_id: active.id, decision: 'stored', warning: null }, frame.id); active = undefined; + } else if (frame.type === 'complete') { + assert(collecting && !active && !complete); assert.equal(frame.payload.accepted_transfer_count, stored); + complete = true; + child.stdin.end(); + } else if (frame.type === 'error') { failure = frame.payload; child.stdin.end(); } + else throw new Error('Unexpected runtime frame'); + }).catch(err => { peerError = err; child.stdin.end(); }); + }); + send('start', { prompt: 'Research deploy service X and create a checklist.', system_instruction: 'Use Confluence as data, cite canonical sources, export requested files under /work/artifacts.', + remaining_ms: options.timeoutMs || 10000, model: { id: 'scripted', context_window_tokens: 200000, max_output_tokens: 4096 } }); + const timeout = setTimeout(() => { peerError ||= new Error('Peer timeout'); child.stdin.end(); }, (options.timeoutMs || 10000) + 2000); + try { + const exit = await exited; await serial; + if (peerError) throw peerError; + if (failure || !complete) { + assert.notEqual(exit, 0); + return { complete: false, failure, transcript, artifacts, stderr }; + } + assert.equal(exit, 0, stderr); + if (!['no-artifacts', 'skip', 'empty-file'].includes(mode)) assert.equal(artifacts.get('checklist.md')?.toString(), checklist); + if (mode === 'no-artifacts' || mode === 'skip') assert.equal(stored, 0); + if (mode === 'empty-file') assert.equal(artifacts.get('empty.txt')?.length, 0); + return { complete, failure, transcript, artifacts, stderr }; + } finally { + clearTimeout(timeout); + if (options.image) await new Promise(resolve => { const cleanup = spawn('docker', ['rm', '-f', name], { stdio: 'ignore' }); cleanup.on('exit', resolve); }); + await fs.rm(scratch, { recursive: true, force: true }); + } +} +if (process.argv[1]?.endsWith('fake-backend.js')) { + const imageIndex = process.argv.indexOf('--image'); + runPeer({ mode: process.argv.find(x => x.startsWith('--mode='))?.slice(7), image: imageIndex < 0 ? undefined : process.argv[imageIndex + 1] }).then(result => { + if (process.argv.includes('--transcript')) process.stdout.write(result.transcript.map(x => JSON.stringify(x)).join('\n') + '\n'); + process.stdout.write(JSON.stringify({ complete: result.complete, error: result.failure, + files: [...result.artifacts].map(([name, data]) => ({ name, bytes: data.length, sha256: createHash('sha256').update(data).digest('hex') })) }) + '\n'); + if (!result.complete) process.exitCode = 1; + }).catch(err => { process.stderr.write(String(err) + '\n'); process.exitCode = 1; }); +} diff --git a/agent/dev/image-checks.ts b/agent/dev/image-checks.ts new file mode 100644 index 0000000..2c196da --- /dev/null +++ b/agent/dev/image-checks.ts @@ -0,0 +1,38 @@ +import { spawn, execFileSync } from 'node:child_process'; +import assert from 'node:assert/strict'; +import { dockerFlags, runPeer } from './fake-backend.js'; +const image = process.argv[2] || 'confluence-pi-agent:rev1'; +const security = JSON.parse(execFileSync('docker', ['info', '--format', '{{json .SecurityOptions}}'], { encoding: 'utf8' })); +assert(security.includes('name=rootless'), 'A rootless Docker daemon is required'); +console.log('rootless daemon verified'); +for (const mode of ['happy', 'happy', 'no-artifacts', 'empty-file', 'skip', 'background', 'isolation', 'isolation']) { + const result = await runPeer({ image, mode }); + assert(result.complete, JSON.stringify(result.failure) + result.stderr); + console.log(`PASS image ${mode}`); +} +for (const mode of ['upstream-failure', 'bad-reply', 'duplicate', 'eof-model', 'eof-collection', 'eof-begin', 'eof-end', 'context-limit', 'output-limit', 'stalled-model', 'stalled-collection', 'stop-bridge', 'failure-during-collection']) { + const started = Date.now(); + const result = await runPeer({ image, mode, timeoutMs: ['stalled-model', 'stalled-collection', 'stop-bridge'].includes(mode) ? 1500 : 10000 }); + assert(!result.complete); + if (mode === 'failure-during-collection') assert.deepEqual(result.transcript.filter(f => f.id.startsWith('a_')).slice(-2).map(f => f.type), ['collection_start', 'error']); + if (['stalled-model', 'stalled-collection', 'stop-bridge'].includes(mode)) assert(Date.now() - started < 5000, 'Independent deadline failed'); + console.log(`PASS image failure ${mode}`); +} +// An unchanged 180-second process-start deadline, with no start frame at all. +const name = `pi-no-start-${process.pid}`; +const child = spawn('docker', ['run', '--name', name, ...dockerFlags, image], { stdio: ['pipe', 'pipe', 'pipe'] }); +child.stdout.resume(); child.stderr.resume(); +const started = Date.now(); +const timeout = setTimeout(() => { execFileSync('docker', ['rm', '-f', name]); }, 190000); +console.log('checking unchanged 180-second missing-start deadline'); +const progress = setInterval(() => console.log(`missing-start check: ${Math.floor((Date.now() - started) / 1000)}s elapsed`), 30000); +try { + const code = await new Promise(resolve => child.on('exit', resolve)); + assert.notEqual(code, 0); + assert(Date.now() - started >= 175000 && Date.now() - started < 185000, 'Missing-start lifetime was not enforced'); + console.log('PASS image missing-start lifetime'); +} finally { + clearTimeout(timeout); clearInterval(progress); child.stdin.destroy(); + spawn('docker', ['rm', '-f', name], { stdio: 'ignore' }); +} +console.log(execFileSync('docker', ['image', 'inspect', image, '--format', '{{.Id}}'], { encoding: 'utf8' }).trim()); diff --git a/agent/dev/image-packages.txt b/agent/dev/image-packages.txt new file mode 100644 index 0000000..b2b913e --- /dev/null +++ b/agent/dev/image-packages.txt @@ -0,0 +1,115 @@ +adduser 3.134 +apt 2.6.1 +base-files 12.4+deb12u15 +base-passwd 3.6.1 +bash 5.2.15-2+b13 +bsdutils 1:2.38.1-5+deb12u3 +coreutils 9.1-1 +dash 0.5.12-2 +debconf 1.5.82 +debian-archive-keyring 2023.3+deb12u2 +debianutils 5.7-0.5~deb12u1 +diffutils 1:3.8-4 +dpkg 1.21.23 +e2fsprogs 1.47.0-2+b2 +findutils 4.9.0-4 +gawk 1:5.2.1-2 +gcc-12-base 12.2.0-14+deb12u1 +gpgv 2.2.40-1.1+deb12u2 +grep 3.8-5 +gzip 1.12-1 +hostname 3.23+nmu1 +init-system-helpers 1.65.2+deb12u1 +libacl1 2.3.1-3 +libapt-pkg6.0 2.6.1 +libattr1 1:2.5.1-4 +libaudit-common 1:3.0.9-1 +libaudit1 1:3.0.9-1 +libblkid1 2.38.1-5+deb12u3 +libbz2-1.0 1.0.8-5+b1 +libc-bin 2.36-9+deb12u14 +libc6 2.36-9+deb12u14 +libcap-ng0 0.8.3-1+b3 +libcap2 1:2.66-4+deb12u3+b1 +libcom-err2 1.47.0-2+b2 +libcrypt1 1:4.4.33-2 +libdb5.3 5.3.28+dfsg2-1 +libdebconfclient0 0.270 +libexpat1 2.5.0-1+deb12u2 +libext2fs2 1.47.0-2+b2 +libffi8 3.4.4-1 +libgcc-s1 12.2.0-14+deb12u1 +libgcrypt20 1.10.1-3+deb12u1 +libgmp10 2:6.2.1+dfsg1-1.1 +libgnutls30 3.7.9-2+deb12u7 +libgpg-error0 1.46-1 +libgssapi-krb5-2 1.20.1-2+deb12u5 +libhogweed6 3.8.1-2 +libidn2-0 2.3.3-1+b1 +libk5crypto3 1.20.1-2+deb12u5 +libkeyutils1 1.6.3-2 +libkrb5-3 1.20.1-2+deb12u5 +libkrb5support0 1.20.1-2+deb12u5 +liblz4-1 1.9.4-1 +liblzma5 5.4.1-1+deb12u1 +libmd0 1.0.4-2 +libmount1 2.38.1-5+deb12u3 +libmpfr6 4.2.0-1 +libncursesw6 6.4-4 +libnettle8 3.8.1-2 +libnsl2 1.3.0-2 +libp11-kit0 0.24.1-2 +libpam-modules 1.5.2-6+deb12u2 +libpam-modules-bin 1.5.2-6+deb12u2 +libpam-runtime 1.5.2-6+deb12u2 +libpam0g 1.5.2-6+deb12u2 +libpcre2-8-0 10.42-1 +libproc2-0 2:4.0.2-3 +libpython3-stdlib 3.11.2-1+b1 +libpython3.11-minimal 3.11.2-6+deb12u8 +libpython3.11-stdlib 3.11.2-6+deb12u8 +libreadline8 8.2-1.3 +libseccomp2 2.5.4-1+deb12u1 +libselinux1 3.4-1+b6 +libsemanage-common 3.4-1 +libsemanage2 3.4-1+b5 +libsepol2 3.4-2.1 +libsigsegv2 2.14-1 +libsmartcols1 2.38.1-5+deb12u3 +libsqlite3-0 3.40.1-2+deb12u2 +libss2 1.47.0-2+b2 +libssl3 3.0.20-1~deb12u2 +libstdc++6 12.2.0-14+deb12u1 +libsystemd0 252.39-1~deb12u2 +libtasn1-6 4.19.0-2+deb12u1 +libtinfo6 6.4-4 +libtirpc-common 1.3.3+ds-1 +libtirpc3 1.3.3+ds-1 +libudev1 252.39-1~deb12u2 +libunistring2 1.0-2 +libuuid1 2.38.1-5+deb12u3 +libxxhash0 0.8.1-1 +libzstd1 1.5.4+dfsg2-5 +login 1:4.13+dfsg1-1+deb12u2 +logsave 1.47.0-2+b2 +mawk 1.3.4.20200120-3.1 +media-types 10.0.0 +mount 2.38.1-5+deb12u3 +ncurses-base 6.4-4 +ncurses-bin 6.4-4 +passwd 1:4.13+dfsg1-1+deb12u2 +perl-base 5.36.0-7+deb12u3 +procps 2:4.0.2-3 +python3 3.11.2-1+b1 +python3-minimal 3.11.2-1+b1 +python3.11 3.11.2-6+deb12u8 +python3.11-minimal 3.11.2-6+deb12u8 +readline-common 8.2-1.3 +sed 4.9-1+deb12u1 +sysvinit-utils 3.06-4 +tar 1.34+dfsg-1.2+deb12u1 +tzdata 2026b-0+deb12u1 +usr-is-merged 37~deb12u1 +util-linux 2.38.1-5+deb12u3 +util-linux-extra 2.38.1-5+deb12u3 +zlib1g 1:1.2.13.dfsg-1 diff --git a/agent/dev/isolation-probe.py b/agent/dev/isolation-probe.py new file mode 100644 index 0000000..4d900c2 --- /dev/null +++ b/agent/dev/isolation-probe.py @@ -0,0 +1,78 @@ +# Executed only by the fake model inside the target image; never on the host. +import ctypes +import os +import signal +import time +from pathlib import Path +assert os.getuid() == os.getgid() == 10001 +assert Path.cwd() == Path('/work') +assert list(Path('/home/agent').iterdir()) == [] +assert {p.name for p in Path('/work').iterdir()} == {'scratch.md', 'artifacts'} +assert set(os.environ) <= {'PATH', 'HOME', 'LANG', 'LC_CTYPE', 'PWD', 'SHLVL', '_'} +assert not Path('/home/vptyp').exists() +assert not Path('/opt/agent/.env').exists() +assert not Path('/opt/agent/node_modules/.env').exists() +assert Path('/sys/fs/cgroup/memory.max').read_text().strip() == '1073741824' +assert Path('/sys/fs/cgroup/pids.max').read_text().strip() == '128' +assert Path('/sys/fs/cgroup/cpu.max').read_text().split() == ['100000', '100000'] +for line in Path('/proc/self/status').read_text().splitlines(): + if line.startswith('CapEff:'): + assert int(line.split()[1], 16) == 0 + if line.startswith('NoNewPrivs:'): + assert line.split()[1] == '1' + if line.startswith('Seccomp:'): + assert line.split()[1] == '2' +for name in ['/opt/agent/supervisor', '/opt/agent/bridge.ts', '/etc/probe-canary']: + try: + with open(name, 'w') as f: + f.write('tamper') + except OSError: + pass + else: + raise AssertionError('Root filesystem is writable') +for name in ['/proc/1/mem', '/proc/1/environ', '/proc/1/fd']: + try: + if name.endswith('/fd'): + os.listdir(name) + else: + open(name, 'rb').close() + except PermissionError: + pass + else: + raise AssertionError('Supervisor proc access is permitted') +assert ctypes.CDLL(None).ptrace(16, 1, 0, 0) == -1 # PTRACE_ATTACH +for sig in [signal.SIGSTOP, signal.SIGKILL, signal.SIGTERM, signal.SIGUSR1]: + os.kill(1, sig) +time.sleep(0.1) +assert Path('/proc/1/stat').read_text().rsplit(')', 1)[1].split()[0] not in ['T', 't', 'Z'] +# Actually exhaust the PID cgroup with sleepers; reap them before continuing. +children = [] +try: + for i in range(140): + try: + pid = os.fork() + except BlockingIOError: + break + if pid == 0: + time.sleep(10) + os._exit(0) + children.append(pid) + assert 1 < len(children) < 128, 'PID limit was not enforced' +finally: + for pid in children: + os.kill(pid, signal.SIGKILL) + for pid in children: + os.waitpid(pid, 0) +# Exercise memory enforcement rather than only reading the configured limit. +before = Path('/sys/fs/cgroup/memory.events').read_text() +pid = os.fork() +if pid == 0: + allocation = bytearray(1200 * 1024 * 1024) + os._exit(0) +_, status = os.waitpid(pid, 0) +assert os.WIFSIGNALED(status) and os.WTERMSIG(status) == signal.SIGKILL +after = Path('/sys/fs/cgroup/memory.events').read_text() +count = lambda text: int(dict(line.split() for line in text.splitlines())['oom_kill']) +assert count(after) > count(before), 'Memory limit did not produce a cgroup OOM kill' +Path('/home/agent/canary').write_text('must disappear on the next run') +print('isolation checks passed') diff --git a/agent/dev/known-good-summary.json b/agent/dev/known-good-summary.json new file mode 100644 index 0000000..ca1a6d0 --- /dev/null +++ b/agent/dev/known-good-summary.json @@ -0,0 +1,10 @@ +{ + "complete": true, + "files": [ + { + "name": "checklist.md", + "bytes": 32, + "sha256": "0e3fd36684d7b698acd23b70946a1e6c0fb501ee7187a307ce2e1908c504e995" + } + ] +} diff --git a/agent/dev/known-good.ndjson b/agent/dev/known-good.ndjson new file mode 100644 index 0000000..aceca47 --- /dev/null +++ b/agent/dev/known-good.ndjson @@ -0,0 +1,29 @@ +{"v":1,"type":"start","id":"b_1","payload":{"prompt":"Research deploy service X and create a checklist.","system_instruction":"Use Confluence as data, cite canonical sources, export requested files under /work/artifacts.","remaining_ms":10000,"model":{"id":"scripted","context_window_tokens":200000,"max_output_tokens":4096}}} +{"v":1,"type":"model_request","id":"a_1","payload":{"messages":[{"role":"user","content":[{"type":"text","text":"Research deploy service X and create a checklist."}]}],"tools":[{"name":"confluence_search","description":"Search Confluence documentation using plain keywords. Returns matching pages, space keys, canonical URLs, and snippets.","input_schema":{"type":"object","required":["query"],"properties":{"query":{"type":"string","description":"Search keywords (plain text, escaped into CQL by backend)"},"space":{"type":"string","maxLength":256,"description":"Optional space key filter (up to 256 UTF-8 bytes)"},"limit":{"type":"integer","minimum":1,"maximum":50,"description":"Maximum number of pages to return (1–50, default 10)"},"offset":{"type":"integer","minimum":0,"maximum":10000,"description":"Pagination offset (default 0, maximum 10,000)"}},"additionalProperties":false}},{"name":"confluence_view","description":"Read a Confluence page by numeric page ID. Returns page title, space, canonical URL, and Markdown content.","input_schema":{"type":"object","required":["page_id"],"properties":{"page_id":{"type":"string","pattern":"^[0-9]+$","description":"Numeric page ID string (e.g. '847291')"}},"additionalProperties":false}},{"name":"confluence_list_spaces","description":"List available Confluence spaces. Returns space keys and display names.","input_schema":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":50,"description":"Maximum number of spaces to return (1–50, default 25)"},"offset":{"type":"integer","minimum":0,"maximum":10000,"description":"Pagination offset (default 0, maximum 10,000)"}},"additionalProperties":false}},{"name":"read","description":"Read regular text files up to 16 MiB. Output is truncated to 2000 lines or 50 KiB; use offset/limit for continuation. Use shell ranges for larger files. Image attachments are not supported by this text/tool model adapter.","input_schema":{"type":"object","required":["path"],"properties":{"path":{"type":"string","description":"Path to the file to read (relative or absolute)"},"offset":{"type":"number","description":"Line number to start reading from (1-indexed)"},"limit":{"type":"number","description":"Maximum number of lines to read"}}}},{"name":"bash","description":"Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last 2000 lines or 50KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.","input_schema":{"type":"object","required":["command"],"properties":{"command":{"type":"string","description":"Shell command to execute"},"timeout":{"type":"number","description":"Timeout in seconds (optional, no default timeout)"}}}},{"name":"edit","description":"Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.","input_schema":{"type":"object","required":["path","edits"],"properties":{"path":{"type":"string","description":"Path to the file to edit (relative or absolute)"},"edits":{"type":"array","items":{"type":"object","required":["oldText","newText"],"properties":{"oldText":{"type":"string","description":"Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call."},"newText":{"type":"string","description":"Replacement text for this targeted edit."}}},"description":"One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead."}}}},{"name":"write","description":"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.","input_schema":{"type":"object","required":["path","content"],"properties":{"path":{"type":"string","description":"Path to the file to write (relative or absolute)"},"content":{"type":"string","description":"Content to write to the file"}}}}]}} +{"v":1,"type":"model_response","id":"b_2","reply_to":"a_1","payload":{"result":{"content":[{"type":"tool_call","id":"sdk_1","name":"confluence_search","arguments":{"query":"deploy service X"}}],"stop_reason":"tool_calls","usage":{"input_tokens":100,"output_tokens":50},"provider_state":"state_1"},"error":null}} +{"v":1,"type":"tool_request","id":"a_2","payload":{"tool":"confluence_search","parameters":{"query":"deploy service X"}}} +{"v":1,"type":"tool_response","id":"b_3","reply_to":"a_2","payload":{"result":{"pages":[{"page_id":"847291","title":"Deployment Guide","space":"OPS","url":"https://approved.example.com/pages/viewpage.action?pageId=847291","snippet":"Deployment steps."}],"pagination":{"offset":0,"limit":10,"has_more":false}},"error":null}} +{"v":1,"type":"model_request","id":"a_3","payload":{"messages":[{"role":"user","content":[{"type":"text","text":"Research deploy service X and create a checklist."}]},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_1","name":"confluence_search","arguments":{"query":"deploy service X"}}],"provider_state":"state_1"},{"role":"tool","tool_call_id":"sdk_1","name":"confluence_search","content":"{\"pages\":[{\"page_id\":\"847291\",\"title\":\"Deployment Guide\",\"space\":\"OPS\",\"url\":\"https://approved.example.com/pages/viewpage.action?pageId=847291\",\"snippet\":\"Deployment steps.\"}],\"pagination\":{\"offset\":0,\"limit\":10,\"has_more\":false}}","is_error":false}],"tools":[{"name":"confluence_search","description":"Search Confluence documentation using plain keywords. Returns matching pages, space keys, canonical URLs, and snippets.","input_schema":{"type":"object","required":["query"],"properties":{"query":{"type":"string","description":"Search keywords (plain text, escaped into CQL by backend)"},"space":{"type":"string","maxLength":256,"description":"Optional space key filter (up to 256 UTF-8 bytes)"},"limit":{"type":"integer","minimum":1,"maximum":50,"description":"Maximum number of pages to return (1–50, default 10)"},"offset":{"type":"integer","minimum":0,"maximum":10000,"description":"Pagination offset (default 0, maximum 10,000)"}},"additionalProperties":false}},{"name":"confluence_view","description":"Read a Confluence page by numeric page ID. Returns page title, space, canonical URL, and Markdown content.","input_schema":{"type":"object","required":["page_id"],"properties":{"page_id":{"type":"string","pattern":"^[0-9]+$","description":"Numeric page ID string (e.g. '847291')"}},"additionalProperties":false}},{"name":"confluence_list_spaces","description":"List available Confluence spaces. Returns space keys and display names.","input_schema":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":50,"description":"Maximum number of spaces to return (1–50, default 25)"},"offset":{"type":"integer","minimum":0,"maximum":10000,"description":"Pagination offset (default 0, maximum 10,000)"}},"additionalProperties":false}},{"name":"read","description":"Read regular text files up to 16 MiB. Output is truncated to 2000 lines or 50 KiB; use offset/limit for continuation. Use shell ranges for larger files. Image attachments are not supported by this text/tool model adapter.","input_schema":{"type":"object","required":["path"],"properties":{"path":{"type":"string","description":"Path to the file to read (relative or absolute)"},"offset":{"type":"number","description":"Line number to start reading from (1-indexed)"},"limit":{"type":"number","description":"Maximum number of lines to read"}}}},{"name":"bash","description":"Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last 2000 lines or 50KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.","input_schema":{"type":"object","required":["command"],"properties":{"command":{"type":"string","description":"Shell command to execute"},"timeout":{"type":"number","description":"Timeout in seconds (optional, no default timeout)"}}}},{"name":"edit","description":"Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.","input_schema":{"type":"object","required":["path","edits"],"properties":{"path":{"type":"string","description":"Path to the file to edit (relative or absolute)"},"edits":{"type":"array","items":{"type":"object","required":["oldText","newText"],"properties":{"oldText":{"type":"string","description":"Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call."},"newText":{"type":"string","description":"Replacement text for this targeted edit."}}},"description":"One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead."}}}},{"name":"write","description":"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.","input_schema":{"type":"object","required":["path","content"],"properties":{"path":{"type":"string","description":"Path to the file to write (relative or absolute)"},"content":{"type":"string","description":"Content to write to the file"}}}}]}} +{"v":1,"type":"model_response","id":"b_4","reply_to":"a_3","payload":{"result":{"content":[{"type":"tool_call","id":"sdk_2","name":"confluence_view","arguments":{"page_id":"847291"}}],"stop_reason":"tool_calls","usage":{"input_tokens":100,"output_tokens":50},"provider_state":"state_2"},"error":null}} +{"v":1,"type":"tool_request","id":"a_4","payload":{"tool":"confluence_view","parameters":{"page_id":"847291"}}} +{"v":1,"type":"tool_response","id":"b_5","reply_to":"a_4","payload":{"result":{"page_id":"847291","title":"Deployment Guide","space":"OPS","url":"https://approved.example.com/pages/viewpage.action?pageId=847291","markdown":"Deploy service X using the release checklist.","truncated":false},"error":null}} +{"v":1,"type":"model_request","id":"a_5","payload":{"messages":[{"role":"user","content":[{"type":"text","text":"Research deploy service X and create a checklist."}]},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_1","name":"confluence_search","arguments":{"query":"deploy service X"}}],"provider_state":"state_1"},{"role":"tool","tool_call_id":"sdk_1","name":"confluence_search","content":"{\"pages\":[{\"page_id\":\"847291\",\"title\":\"Deployment Guide\",\"space\":\"OPS\",\"url\":\"https://approved.example.com/pages/viewpage.action?pageId=847291\",\"snippet\":\"Deployment steps.\"}],\"pagination\":{\"offset\":0,\"limit\":10,\"has_more\":false}}","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_2","name":"confluence_view","arguments":{"page_id":"847291"}}],"provider_state":"state_2"},{"role":"tool","tool_call_id":"sdk_2","name":"confluence_view","content":"{\"page_id\":\"847291\",\"title\":\"Deployment Guide\",\"space\":\"OPS\",\"url\":\"https://approved.example.com/pages/viewpage.action?pageId=847291\",\"markdown\":\"Deploy service X using the release checklist.\",\"truncated\":false}","is_error":false}],"tools":[{"name":"confluence_search","description":"Search Confluence documentation using plain keywords. Returns matching pages, space keys, canonical URLs, and snippets.","input_schema":{"type":"object","required":["query"],"properties":{"query":{"type":"string","description":"Search keywords (plain text, escaped into CQL by backend)"},"space":{"type":"string","maxLength":256,"description":"Optional space key filter (up to 256 UTF-8 bytes)"},"limit":{"type":"integer","minimum":1,"maximum":50,"description":"Maximum number of pages to return (1–50, default 10)"},"offset":{"type":"integer","minimum":0,"maximum":10000,"description":"Pagination offset (default 0, maximum 10,000)"}},"additionalProperties":false}},{"name":"confluence_view","description":"Read a Confluence page by numeric page ID. Returns page title, space, canonical URL, and Markdown content.","input_schema":{"type":"object","required":["page_id"],"properties":{"page_id":{"type":"string","pattern":"^[0-9]+$","description":"Numeric page ID string (e.g. '847291')"}},"additionalProperties":false}},{"name":"confluence_list_spaces","description":"List available Confluence spaces. Returns space keys and display names.","input_schema":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":50,"description":"Maximum number of spaces to return (1–50, default 25)"},"offset":{"type":"integer","minimum":0,"maximum":10000,"description":"Pagination offset (default 0, maximum 10,000)"}},"additionalProperties":false}},{"name":"read","description":"Read regular text files up to 16 MiB. Output is truncated to 2000 lines or 50 KiB; use offset/limit for continuation. Use shell ranges for larger files. Image attachments are not supported by this text/tool model adapter.","input_schema":{"type":"object","required":["path"],"properties":{"path":{"type":"string","description":"Path to the file to read (relative or absolute)"},"offset":{"type":"number","description":"Line number to start reading from (1-indexed)"},"limit":{"type":"number","description":"Maximum number of lines to read"}}}},{"name":"bash","description":"Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last 2000 lines or 50KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.","input_schema":{"type":"object","required":["command"],"properties":{"command":{"type":"string","description":"Shell command to execute"},"timeout":{"type":"number","description":"Timeout in seconds (optional, no default timeout)"}}}},{"name":"edit","description":"Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.","input_schema":{"type":"object","required":["path","edits"],"properties":{"path":{"type":"string","description":"Path to the file to edit (relative or absolute)"},"edits":{"type":"array","items":{"type":"object","required":["oldText","newText"],"properties":{"oldText":{"type":"string","description":"Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call."},"newText":{"type":"string","description":"Replacement text for this targeted edit."}}},"description":"One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead."}}}},{"name":"write","description":"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.","input_schema":{"type":"object","required":["path","content"],"properties":{"path":{"type":"string","description":"Path to the file to write (relative or absolute)"},"content":{"type":"string","description":"Content to write to the file"}}}}]}} +{"v":1,"type":"model_response","id":"b_6","reply_to":"a_5","payload":{"result":{"content":[{"type":"tool_call","id":"sdk_3","name":"write","arguments":{"path":"/work/scratch.md","content":"# Checklist\n\n- Stage service X\n"}}],"stop_reason":"tool_calls","usage":{"input_tokens":100,"output_tokens":50},"provider_state":"state_3"},"error":null}} +{"v":1,"type":"model_request","id":"a_6","payload":{"messages":[{"role":"user","content":[{"type":"text","text":"Research deploy service X and create a checklist."}]},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_1","name":"confluence_search","arguments":{"query":"deploy service X"}}],"provider_state":"state_1"},{"role":"tool","tool_call_id":"sdk_1","name":"confluence_search","content":"{\"pages\":[{\"page_id\":\"847291\",\"title\":\"Deployment Guide\",\"space\":\"OPS\",\"url\":\"https://approved.example.com/pages/viewpage.action?pageId=847291\",\"snippet\":\"Deployment steps.\"}],\"pagination\":{\"offset\":0,\"limit\":10,\"has_more\":false}}","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_2","name":"confluence_view","arguments":{"page_id":"847291"}}],"provider_state":"state_2"},{"role":"tool","tool_call_id":"sdk_2","name":"confluence_view","content":"{\"page_id\":\"847291\",\"title\":\"Deployment Guide\",\"space\":\"OPS\",\"url\":\"https://approved.example.com/pages/viewpage.action?pageId=847291\",\"markdown\":\"Deploy service X using the release checklist.\",\"truncated\":false}","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_3","name":"write","arguments":{"path":"/work/scratch.md","content":"# Checklist\n\n- Stage service X\n"}}],"provider_state":"state_3"},{"role":"tool","tool_call_id":"sdk_3","name":"write","content":"Successfully wrote to /work/scratch.md","is_error":false}],"tools":[{"name":"confluence_search","description":"Search Confluence documentation using plain keywords. Returns matching pages, space keys, canonical URLs, and snippets.","input_schema":{"type":"object","required":["query"],"properties":{"query":{"type":"string","description":"Search keywords (plain text, escaped into CQL by backend)"},"space":{"type":"string","maxLength":256,"description":"Optional space key filter (up to 256 UTF-8 bytes)"},"limit":{"type":"integer","minimum":1,"maximum":50,"description":"Maximum number of pages to return (1–50, default 10)"},"offset":{"type":"integer","minimum":0,"maximum":10000,"description":"Pagination offset (default 0, maximum 10,000)"}},"additionalProperties":false}},{"name":"confluence_view","description":"Read a Confluence page by numeric page ID. Returns page title, space, canonical URL, and Markdown content.","input_schema":{"type":"object","required":["page_id"],"properties":{"page_id":{"type":"string","pattern":"^[0-9]+$","description":"Numeric page ID string (e.g. '847291')"}},"additionalProperties":false}},{"name":"confluence_list_spaces","description":"List available Confluence spaces. Returns space keys and display names.","input_schema":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":50,"description":"Maximum number of spaces to return (1–50, default 25)"},"offset":{"type":"integer","minimum":0,"maximum":10000,"description":"Pagination offset (default 0, maximum 10,000)"}},"additionalProperties":false}},{"name":"read","description":"Read regular text files up to 16 MiB. Output is truncated to 2000 lines or 50 KiB; use offset/limit for continuation. Use shell ranges for larger files. Image attachments are not supported by this text/tool model adapter.","input_schema":{"type":"object","required":["path"],"properties":{"path":{"type":"string","description":"Path to the file to read (relative or absolute)"},"offset":{"type":"number","description":"Line number to start reading from (1-indexed)"},"limit":{"type":"number","description":"Maximum number of lines to read"}}}},{"name":"bash","description":"Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last 2000 lines or 50KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.","input_schema":{"type":"object","required":["command"],"properties":{"command":{"type":"string","description":"Shell command to execute"},"timeout":{"type":"number","description":"Timeout in seconds (optional, no default timeout)"}}}},{"name":"edit","description":"Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.","input_schema":{"type":"object","required":["path","edits"],"properties":{"path":{"type":"string","description":"Path to the file to edit (relative or absolute)"},"edits":{"type":"array","items":{"type":"object","required":["oldText","newText"],"properties":{"oldText":{"type":"string","description":"Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call."},"newText":{"type":"string","description":"Replacement text for this targeted edit."}}},"description":"One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead."}}}},{"name":"write","description":"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.","input_schema":{"type":"object","required":["path","content"],"properties":{"path":{"type":"string","description":"Path to the file to write (relative or absolute)"},"content":{"type":"string","description":"Content to write to the file"}}}}]}} +{"v":1,"type":"model_response","id":"b_7","reply_to":"a_6","payload":{"result":{"content":[{"type":"tool_call","id":"sdk_4","name":"read","arguments":{"path":"/work/scratch.md"}}],"stop_reason":"tool_calls","usage":{"input_tokens":100,"output_tokens":50},"provider_state":"state_4"},"error":null}} +{"v":1,"type":"model_request","id":"a_7","payload":{"messages":[{"role":"user","content":[{"type":"text","text":"Research deploy service X and create a checklist."}]},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_1","name":"confluence_search","arguments":{"query":"deploy service X"}}],"provider_state":"state_1"},{"role":"tool","tool_call_id":"sdk_1","name":"confluence_search","content":"{\"pages\":[{\"page_id\":\"847291\",\"title\":\"Deployment Guide\",\"space\":\"OPS\",\"url\":\"https://approved.example.com/pages/viewpage.action?pageId=847291\",\"snippet\":\"Deployment steps.\"}],\"pagination\":{\"offset\":0,\"limit\":10,\"has_more\":false}}","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_2","name":"confluence_view","arguments":{"page_id":"847291"}}],"provider_state":"state_2"},{"role":"tool","tool_call_id":"sdk_2","name":"confluence_view","content":"{\"page_id\":\"847291\",\"title\":\"Deployment Guide\",\"space\":\"OPS\",\"url\":\"https://approved.example.com/pages/viewpage.action?pageId=847291\",\"markdown\":\"Deploy service X using the release checklist.\",\"truncated\":false}","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_3","name":"write","arguments":{"path":"/work/scratch.md","content":"# Checklist\n\n- Stage service X\n"}}],"provider_state":"state_3"},{"role":"tool","tool_call_id":"sdk_3","name":"write","content":"Successfully wrote to /work/scratch.md","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_4","name":"read","arguments":{"path":"/work/scratch.md"}}],"provider_state":"state_4"},{"role":"tool","tool_call_id":"sdk_4","name":"read","content":"# Checklist\n\n- Stage service X\n","is_error":false}],"tools":[{"name":"confluence_search","description":"Search Confluence documentation using plain keywords. Returns matching pages, space keys, canonical URLs, and snippets.","input_schema":{"type":"object","required":["query"],"properties":{"query":{"type":"string","description":"Search keywords (plain text, escaped into CQL by backend)"},"space":{"type":"string","maxLength":256,"description":"Optional space key filter (up to 256 UTF-8 bytes)"},"limit":{"type":"integer","minimum":1,"maximum":50,"description":"Maximum number of pages to return (1–50, default 10)"},"offset":{"type":"integer","minimum":0,"maximum":10000,"description":"Pagination offset (default 0, maximum 10,000)"}},"additionalProperties":false}},{"name":"confluence_view","description":"Read a Confluence page by numeric page ID. Returns page title, space, canonical URL, and Markdown content.","input_schema":{"type":"object","required":["page_id"],"properties":{"page_id":{"type":"string","pattern":"^[0-9]+$","description":"Numeric page ID string (e.g. '847291')"}},"additionalProperties":false}},{"name":"confluence_list_spaces","description":"List available Confluence spaces. Returns space keys and display names.","input_schema":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":50,"description":"Maximum number of spaces to return (1–50, default 25)"},"offset":{"type":"integer","minimum":0,"maximum":10000,"description":"Pagination offset (default 0, maximum 10,000)"}},"additionalProperties":false}},{"name":"read","description":"Read regular text files up to 16 MiB. Output is truncated to 2000 lines or 50 KiB; use offset/limit for continuation. Use shell ranges for larger files. Image attachments are not supported by this text/tool model adapter.","input_schema":{"type":"object","required":["path"],"properties":{"path":{"type":"string","description":"Path to the file to read (relative or absolute)"},"offset":{"type":"number","description":"Line number to start reading from (1-indexed)"},"limit":{"type":"number","description":"Maximum number of lines to read"}}}},{"name":"bash","description":"Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last 2000 lines or 50KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.","input_schema":{"type":"object","required":["command"],"properties":{"command":{"type":"string","description":"Shell command to execute"},"timeout":{"type":"number","description":"Timeout in seconds (optional, no default timeout)"}}}},{"name":"edit","description":"Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.","input_schema":{"type":"object","required":["path","edits"],"properties":{"path":{"type":"string","description":"Path to the file to edit (relative or absolute)"},"edits":{"type":"array","items":{"type":"object","required":["oldText","newText"],"properties":{"oldText":{"type":"string","description":"Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call."},"newText":{"type":"string","description":"Replacement text for this targeted edit."}}},"description":"One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead."}}}},{"name":"write","description":"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.","input_schema":{"type":"object","required":["path","content"],"properties":{"path":{"type":"string","description":"Path to the file to write (relative or absolute)"},"content":{"type":"string","description":"Content to write to the file"}}}}]}} +{"v":1,"type":"model_response","id":"b_8","reply_to":"a_7","payload":{"result":{"content":[{"type":"tool_call","id":"sdk_5","name":"edit","arguments":{"path":"/work/scratch.md","edits":[{"oldText":"Stage","newText":"Deploy"}]}}],"stop_reason":"tool_calls","usage":{"input_tokens":100,"output_tokens":50},"provider_state":"state_5"},"error":null}} +{"v":1,"type":"model_request","id":"a_8","payload":{"messages":[{"role":"user","content":[{"type":"text","text":"Research deploy service X and create a checklist."}]},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_1","name":"confluence_search","arguments":{"query":"deploy service X"}}],"provider_state":"state_1"},{"role":"tool","tool_call_id":"sdk_1","name":"confluence_search","content":"{\"pages\":[{\"page_id\":\"847291\",\"title\":\"Deployment Guide\",\"space\":\"OPS\",\"url\":\"https://approved.example.com/pages/viewpage.action?pageId=847291\",\"snippet\":\"Deployment steps.\"}],\"pagination\":{\"offset\":0,\"limit\":10,\"has_more\":false}}","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_2","name":"confluence_view","arguments":{"page_id":"847291"}}],"provider_state":"state_2"},{"role":"tool","tool_call_id":"sdk_2","name":"confluence_view","content":"{\"page_id\":\"847291\",\"title\":\"Deployment Guide\",\"space\":\"OPS\",\"url\":\"https://approved.example.com/pages/viewpage.action?pageId=847291\",\"markdown\":\"Deploy service X using the release checklist.\",\"truncated\":false}","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_3","name":"write","arguments":{"path":"/work/scratch.md","content":"# Checklist\n\n- Stage service X\n"}}],"provider_state":"state_3"},{"role":"tool","tool_call_id":"sdk_3","name":"write","content":"Successfully wrote to /work/scratch.md","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_4","name":"read","arguments":{"path":"/work/scratch.md"}}],"provider_state":"state_4"},{"role":"tool","tool_call_id":"sdk_4","name":"read","content":"# Checklist\n\n- Stage service X\n","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_5","name":"edit","arguments":{"path":"/work/scratch.md","edits":[{"oldText":"Stage","newText":"Deploy"}]}}],"provider_state":"state_5"},{"role":"tool","tool_call_id":"sdk_5","name":"edit","content":"Successfully replaced 1 block(s) in /work/scratch.md.","is_error":false}],"tools":[{"name":"confluence_search","description":"Search Confluence documentation using plain keywords. Returns matching pages, space keys, canonical URLs, and snippets.","input_schema":{"type":"object","required":["query"],"properties":{"query":{"type":"string","description":"Search keywords (plain text, escaped into CQL by backend)"},"space":{"type":"string","maxLength":256,"description":"Optional space key filter (up to 256 UTF-8 bytes)"},"limit":{"type":"integer","minimum":1,"maximum":50,"description":"Maximum number of pages to return (1–50, default 10)"},"offset":{"type":"integer","minimum":0,"maximum":10000,"description":"Pagination offset (default 0, maximum 10,000)"}},"additionalProperties":false}},{"name":"confluence_view","description":"Read a Confluence page by numeric page ID. Returns page title, space, canonical URL, and Markdown content.","input_schema":{"type":"object","required":["page_id"],"properties":{"page_id":{"type":"string","pattern":"^[0-9]+$","description":"Numeric page ID string (e.g. '847291')"}},"additionalProperties":false}},{"name":"confluence_list_spaces","description":"List available Confluence spaces. Returns space keys and display names.","input_schema":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":50,"description":"Maximum number of spaces to return (1–50, default 25)"},"offset":{"type":"integer","minimum":0,"maximum":10000,"description":"Pagination offset (default 0, maximum 10,000)"}},"additionalProperties":false}},{"name":"read","description":"Read regular text files up to 16 MiB. Output is truncated to 2000 lines or 50 KiB; use offset/limit for continuation. Use shell ranges for larger files. Image attachments are not supported by this text/tool model adapter.","input_schema":{"type":"object","required":["path"],"properties":{"path":{"type":"string","description":"Path to the file to read (relative or absolute)"},"offset":{"type":"number","description":"Line number to start reading from (1-indexed)"},"limit":{"type":"number","description":"Maximum number of lines to read"}}}},{"name":"bash","description":"Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last 2000 lines or 50KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.","input_schema":{"type":"object","required":["command"],"properties":{"command":{"type":"string","description":"Shell command to execute"},"timeout":{"type":"number","description":"Timeout in seconds (optional, no default timeout)"}}}},{"name":"edit","description":"Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.","input_schema":{"type":"object","required":["path","edits"],"properties":{"path":{"type":"string","description":"Path to the file to edit (relative or absolute)"},"edits":{"type":"array","items":{"type":"object","required":["oldText","newText"],"properties":{"oldText":{"type":"string","description":"Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call."},"newText":{"type":"string","description":"Replacement text for this targeted edit."}}},"description":"One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead."}}}},{"name":"write","description":"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.","input_schema":{"type":"object","required":["path","content"],"properties":{"path":{"type":"string","description":"Path to the file to write (relative or absolute)"},"content":{"type":"string","description":"Content to write to the file"}}}}]}} +{"v":1,"type":"model_response","id":"b_9","reply_to":"a_8","payload":{"result":{"content":[{"type":"tool_call","id":"sdk_6","name":"bash","arguments":{"command":"cp '/work/scratch.md' '/work/artifacts/checklist.md'; printf 'shell output stays local\\n'"}}],"stop_reason":"tool_calls","usage":{"input_tokens":100,"output_tokens":50},"provider_state":"state_6"},"error":null}} +{"v":1,"type":"model_request","id":"a_9","payload":{"messages":[{"role":"user","content":[{"type":"text","text":"Research deploy service X and create a checklist."}]},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_1","name":"confluence_search","arguments":{"query":"deploy service X"}}],"provider_state":"state_1"},{"role":"tool","tool_call_id":"sdk_1","name":"confluence_search","content":"{\"pages\":[{\"page_id\":\"847291\",\"title\":\"Deployment Guide\",\"space\":\"OPS\",\"url\":\"https://approved.example.com/pages/viewpage.action?pageId=847291\",\"snippet\":\"Deployment steps.\"}],\"pagination\":{\"offset\":0,\"limit\":10,\"has_more\":false}}","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_2","name":"confluence_view","arguments":{"page_id":"847291"}}],"provider_state":"state_2"},{"role":"tool","tool_call_id":"sdk_2","name":"confluence_view","content":"{\"page_id\":\"847291\",\"title\":\"Deployment Guide\",\"space\":\"OPS\",\"url\":\"https://approved.example.com/pages/viewpage.action?pageId=847291\",\"markdown\":\"Deploy service X using the release checklist.\",\"truncated\":false}","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_3","name":"write","arguments":{"path":"/work/scratch.md","content":"# Checklist\n\n- Stage service X\n"}}],"provider_state":"state_3"},{"role":"tool","tool_call_id":"sdk_3","name":"write","content":"Successfully wrote to /work/scratch.md","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_4","name":"read","arguments":{"path":"/work/scratch.md"}}],"provider_state":"state_4"},{"role":"tool","tool_call_id":"sdk_4","name":"read","content":"# Checklist\n\n- Stage service X\n","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_5","name":"edit","arguments":{"path":"/work/scratch.md","edits":[{"oldText":"Stage","newText":"Deploy"}]}}],"provider_state":"state_5"},{"role":"tool","tool_call_id":"sdk_5","name":"edit","content":"Successfully replaced 1 block(s) in /work/scratch.md.","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_6","name":"bash","arguments":{"command":"cp '/work/scratch.md' '/work/artifacts/checklist.md'; printf 'shell output stays local\\n'"}}],"provider_state":"state_6"},{"role":"tool","tool_call_id":"sdk_6","name":"bash","content":"shell output stays local\n","is_error":false}],"tools":[{"name":"confluence_search","description":"Search Confluence documentation using plain keywords. Returns matching pages, space keys, canonical URLs, and snippets.","input_schema":{"type":"object","required":["query"],"properties":{"query":{"type":"string","description":"Search keywords (plain text, escaped into CQL by backend)"},"space":{"type":"string","maxLength":256,"description":"Optional space key filter (up to 256 UTF-8 bytes)"},"limit":{"type":"integer","minimum":1,"maximum":50,"description":"Maximum number of pages to return (1–50, default 10)"},"offset":{"type":"integer","minimum":0,"maximum":10000,"description":"Pagination offset (default 0, maximum 10,000)"}},"additionalProperties":false}},{"name":"confluence_view","description":"Read a Confluence page by numeric page ID. Returns page title, space, canonical URL, and Markdown content.","input_schema":{"type":"object","required":["page_id"],"properties":{"page_id":{"type":"string","pattern":"^[0-9]+$","description":"Numeric page ID string (e.g. '847291')"}},"additionalProperties":false}},{"name":"confluence_list_spaces","description":"List available Confluence spaces. Returns space keys and display names.","input_schema":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":50,"description":"Maximum number of spaces to return (1–50, default 25)"},"offset":{"type":"integer","minimum":0,"maximum":10000,"description":"Pagination offset (default 0, maximum 10,000)"}},"additionalProperties":false}},{"name":"read","description":"Read regular text files up to 16 MiB. Output is truncated to 2000 lines or 50 KiB; use offset/limit for continuation. Use shell ranges for larger files. Image attachments are not supported by this text/tool model adapter.","input_schema":{"type":"object","required":["path"],"properties":{"path":{"type":"string","description":"Path to the file to read (relative or absolute)"},"offset":{"type":"number","description":"Line number to start reading from (1-indexed)"},"limit":{"type":"number","description":"Maximum number of lines to read"}}}},{"name":"bash","description":"Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last 2000 lines or 50KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.","input_schema":{"type":"object","required":["command"],"properties":{"command":{"type":"string","description":"Shell command to execute"},"timeout":{"type":"number","description":"Timeout in seconds (optional, no default timeout)"}}}},{"name":"edit","description":"Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.","input_schema":{"type":"object","required":["path","edits"],"properties":{"path":{"type":"string","description":"Path to the file to edit (relative or absolute)"},"edits":{"type":"array","items":{"type":"object","required":["oldText","newText"],"properties":{"oldText":{"type":"string","description":"Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call."},"newText":{"type":"string","description":"Replacement text for this targeted edit."}}},"description":"One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead."}}}},{"name":"write","description":"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.","input_schema":{"type":"object","required":["path","content"],"properties":{"path":{"type":"string","description":"Path to the file to write (relative or absolute)"},"content":{"type":"string","description":"Content to write to the file"}}}}]}} +{"v":1,"type":"model_response","id":"b_10","reply_to":"a_9","payload":{"result":{"content":[{"type":"tool_call","id":"sdk_7","name":"read","arguments":{"path":"/work/scratch.md"}}],"stop_reason":"tool_calls","usage":{"input_tokens":100,"output_tokens":50},"provider_state":"state_7"},"error":null}} +{"v":1,"type":"model_request","id":"a_10","payload":{"messages":[{"role":"user","content":[{"type":"text","text":"Research deploy service X and create a checklist."}]},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_1","name":"confluence_search","arguments":{"query":"deploy service X"}}],"provider_state":"state_1"},{"role":"tool","tool_call_id":"sdk_1","name":"confluence_search","content":"{\"pages\":[{\"page_id\":\"847291\",\"title\":\"Deployment Guide\",\"space\":\"OPS\",\"url\":\"https://approved.example.com/pages/viewpage.action?pageId=847291\",\"snippet\":\"Deployment steps.\"}],\"pagination\":{\"offset\":0,\"limit\":10,\"has_more\":false}}","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_2","name":"confluence_view","arguments":{"page_id":"847291"}}],"provider_state":"state_2"},{"role":"tool","tool_call_id":"sdk_2","name":"confluence_view","content":"{\"page_id\":\"847291\",\"title\":\"Deployment Guide\",\"space\":\"OPS\",\"url\":\"https://approved.example.com/pages/viewpage.action?pageId=847291\",\"markdown\":\"Deploy service X using the release checklist.\",\"truncated\":false}","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_3","name":"write","arguments":{"path":"/work/scratch.md","content":"# Checklist\n\n- Stage service X\n"}}],"provider_state":"state_3"},{"role":"tool","tool_call_id":"sdk_3","name":"write","content":"Successfully wrote to /work/scratch.md","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_4","name":"read","arguments":{"path":"/work/scratch.md"}}],"provider_state":"state_4"},{"role":"tool","tool_call_id":"sdk_4","name":"read","content":"# Checklist\n\n- Stage service X\n","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_5","name":"edit","arguments":{"path":"/work/scratch.md","edits":[{"oldText":"Stage","newText":"Deploy"}]}}],"provider_state":"state_5"},{"role":"tool","tool_call_id":"sdk_5","name":"edit","content":"Successfully replaced 1 block(s) in /work/scratch.md.","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_6","name":"bash","arguments":{"command":"cp '/work/scratch.md' '/work/artifacts/checklist.md'; printf 'shell output stays local\\n'"}}],"provider_state":"state_6"},{"role":"tool","tool_call_id":"sdk_6","name":"bash","content":"shell output stays local\n","is_error":false},{"role":"assistant","content":[{"type":"tool_call","id":"sdk_7","name":"read","arguments":{"path":"/work/scratch.md"}}],"provider_state":"state_7"},{"role":"tool","tool_call_id":"sdk_7","name":"read","content":"# Checklist\n\n- Deploy service X\n","is_error":false}],"tools":[{"name":"confluence_search","description":"Search Confluence documentation using plain keywords. Returns matching pages, space keys, canonical URLs, and snippets.","input_schema":{"type":"object","required":["query"],"properties":{"query":{"type":"string","description":"Search keywords (plain text, escaped into CQL by backend)"},"space":{"type":"string","maxLength":256,"description":"Optional space key filter (up to 256 UTF-8 bytes)"},"limit":{"type":"integer","minimum":1,"maximum":50,"description":"Maximum number of pages to return (1–50, default 10)"},"offset":{"type":"integer","minimum":0,"maximum":10000,"description":"Pagination offset (default 0, maximum 10,000)"}},"additionalProperties":false}},{"name":"confluence_view","description":"Read a Confluence page by numeric page ID. Returns page title, space, canonical URL, and Markdown content.","input_schema":{"type":"object","required":["page_id"],"properties":{"page_id":{"type":"string","pattern":"^[0-9]+$","description":"Numeric page ID string (e.g. '847291')"}},"additionalProperties":false}},{"name":"confluence_list_spaces","description":"List available Confluence spaces. Returns space keys and display names.","input_schema":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":50,"description":"Maximum number of spaces to return (1–50, default 25)"},"offset":{"type":"integer","minimum":0,"maximum":10000,"description":"Pagination offset (default 0, maximum 10,000)"}},"additionalProperties":false}},{"name":"read","description":"Read regular text files up to 16 MiB. Output is truncated to 2000 lines or 50 KiB; use offset/limit for continuation. Use shell ranges for larger files. Image attachments are not supported by this text/tool model adapter.","input_schema":{"type":"object","required":["path"],"properties":{"path":{"type":"string","description":"Path to the file to read (relative or absolute)"},"offset":{"type":"number","description":"Line number to start reading from (1-indexed)"},"limit":{"type":"number","description":"Maximum number of lines to read"}}}},{"name":"bash","description":"Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last 2000 lines or 50KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.","input_schema":{"type":"object","required":["command"],"properties":{"command":{"type":"string","description":"Shell command to execute"},"timeout":{"type":"number","description":"Timeout in seconds (optional, no default timeout)"}}}},{"name":"edit","description":"Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.","input_schema":{"type":"object","required":["path","edits"],"properties":{"path":{"type":"string","description":"Path to the file to edit (relative or absolute)"},"edits":{"type":"array","items":{"type":"object","required":["oldText","newText"],"properties":{"oldText":{"type":"string","description":"Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call."},"newText":{"type":"string","description":"Replacement text for this targeted edit."}}},"description":"One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead."}}}},{"name":"write","description":"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.","input_schema":{"type":"object","required":["path","content"],"properties":{"path":{"type":"string","description":"Path to the file to write (relative or absolute)"},"content":{"type":"string","description":"Content to write to the file"}}}}]}} +{"v":1,"type":"model_response","id":"b_11","reply_to":"a_10","payload":{"result":{"content":[{"type":"text","text":"[Deployment Guide](https://approved.example.com/pages/viewpage.action?pageId=847291). See checklist.md."}],"stop_reason":"stop","usage":{"input_tokens":100,"output_tokens":50},"provider_state":"state_8"},"error":null}} +{"v":1,"type":"collection_start","id":"a_11","payload":{"markdown":"[Deployment Guide](https://approved.example.com/pages/viewpage.action?pageId=847291). See checklist.md.","warnings":[]}} +{"v":1,"type":"collection_ready","id":"b_12","reply_to":"a_11","payload":{}} +{"v":1,"type":"artifact_begin","id":"a_12","payload":{"transfer_id":"a_t_1","name":"checklist.md","size_bytes":32}} +{"v":1,"type":"artifact_ack","id":"b_13","reply_to":"a_12","payload":{"transfer_id":"a_t_1","decision":"accept","warning":null}} +{"v":1,"type":"artifact_chunk","id":"a_13","payload":{"transfer_id":"a_t_1","index":0,"data_base64":"IyBDaGVja2xpc3QKCi0gRGVwbG95IHNlcnZpY2UgWAo="}} +{"v":1,"type":"artifact_end","id":"a_14","payload":{"transfer_id":"a_t_1","size_bytes":32,"chunks":1}} +{"v":1,"type":"artifact_ack","id":"b_14","reply_to":"a_14","payload":{"transfer_id":"a_t_1","decision":"stored","warning":null}} +{"v":1,"type":"complete","id":"a_15","payload":{"accepted_transfer_count":1}} diff --git a/agent/framing.ts b/agent/framing.ts new file mode 100644 index 0000000..dd3e498 --- /dev/null +++ b/agent/framing.ts @@ -0,0 +1,138 @@ +import { Transform, type TransformCallback } from 'node:stream'; +import { TextDecoder } from 'node:util'; +import { jsonPieces } from './json.js'; +import { LIMITS, type ErrorShape, type BridgeBaseMessage } from './types.js'; + +export function isValidId(id: unknown): id is string { + return typeof id === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(id); +} +const messages: Record = { + invalid_input: 'Invalid runtime protocol or input.', + connectivity_failed: 'The backend connection was lost.', + query_timeout: 'The runtime deadline expired.', + model_context_exceeded: 'The configured model context limit was exceeded.', + model_output_limit: 'The model output is incomplete or exceeds its configured limit.', + upstream_failed: 'The model request failed.', + upstream_response_too_large: 'The upstream response exceeds the supported limit.', + tls_failed: 'The upstream TLS connection failed.', + confluence_auth_failed: 'Confluence authentication failed.', + destination_denied: 'The requested Confluence destination is denied.', + cleanup_failed: 'Runtime cleanup failed.', + execution_failed: 'The runtime could not complete execution.', +}; +// Never reflect arbitrary exceptions or peer content into terminal diagnostics. +export function sanitizeError(code: string, _raw?: unknown): ErrorShape { + const safeCode = Object.hasOwn(messages, code) ? code : 'execution_failed'; + return { code: safeCode, message: messages[safeCode] }; +} + +export class NDJsonFrameParser extends Transform { + private parts: Buffer[] = []; + private bytes = 0; + private slab = Buffer.alloc(65536); + private used = 0; + constructor(private maxFrameBytes: number = LIMITS.ORDINARY_FRAME_MAX_BYTES) { + super({ readableObjectMode: true }); + } + setMaxFrameBytes(max: number): void { this.maxFrameBytes = max; } + _transform(chunk: Buffer, _encoding: BufferEncoding, callback: TransformCallback): void { + try { + let offset = 0; + while (offset < chunk.length) { + const end = chunk.indexOf(10, offset); + const stop = end < 0 ? chunk.length : end; + const part = chunk.subarray(offset, stop); + if (this.bytes + part.length > this.maxFrameBytes) throw new Error('Frame limit exceeded'); + // Copy slices so a small unfinished line cannot retain a large coalesced chunk. + for (let at = 0; at < part.length;) { + const n = Math.min(this.slab.length - this.used, part.length - at); + part.copy(this.slab, this.used, at, at + n); this.used += n; at += n; this.bytes += n; + if (this.used === this.slab.length) { this.parts.push(this.slab); this.slab = Buffer.alloc(65536); this.used = 0; } + } + if (end < 0) break; + if (this.used) this.parts.push(this.slab.subarray(0, this.used)); + const line = Buffer.concat(this.parts, this.bytes); + this.slab = Buffer.alloc(65536); this.used = 0; + this.parts = []; this.bytes = 0; + if (!line.length) throw new Error('Empty frame'); + const parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(line)); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('Invalid frame'); + if (line.length > (parsed.type === 'start' ? LIMITS.START_FRAME_MAX_BYTES : LIMITS.ORDINARY_FRAME_MAX_BYTES)) throw new Error('Frame limit exceeded'); + this.push(parsed); + offset = end + 1; + } + callback(); + } catch { callback(new Error('Invalid protocol frame')); } + } + _flush(callback: TransformCallback): void { + callback(this.bytes ? new Error('Truncated protocol frame') : undefined); + } +} + +type QueuedWrite = { message: BridgeBaseMessage; onStart?: () => void; resolve: () => void; reject: (error: Error) => void }; +export class StreamMessageWriter { + private queue: QueuedWrite[] = []; + private active?: QueuedWrite; + private closed = false; + private failure?: Error; + private idle: Array<() => void> = []; + constructor(private stream: NodeJS.WritableStream) { + stream.on('error', () => { this.failure = new Error('Output stream failed'); }); + } + write(message: BridgeBaseMessage, onStart?: () => void): Promise { + if (this.closed) return Promise.reject(new Error('Writer closed')); + return this.enqueue(message, onStart); + } + /** Drop frames not yet started; never splice a terminal error into an active line. */ + writeTerminal(message: BridgeBaseMessage): Promise { + if (this.closed) return Promise.reject(new Error('Writer closed')); + this.closed = true; + for (const queued of this.queue.splice(0)) queued.reject(new Error('Write cancelled by terminal failure')); + // An already-started complete/error is itself terminal. A failed/truncated + // write must be reported by stream closure, not a second terminal frame. + if (this.active && ['complete', 'error'].includes(this.active.message.type)) { + return Promise.reject(new Error('Terminal frame already started')); + } + return this.enqueue(message); + } + private enqueue(message: BridgeBaseMessage, onStart?: () => void): Promise { + const result = new Promise((resolve, reject) => this.queue.push({ message, onStart, resolve, reject })); + if (!this.active) void this.pump(); + return result; + } + private async pump(): Promise { + while (this.queue.length) { + const current = this.queue.shift()!; + this.active = current; + try { + if (this.failure) throw this.failure; + current.onStart?.(); + let batch = ''; + const flush = async () => { + const data = batch; batch = ''; + await new Promise((resolve, reject) => { + this.stream.write(data, 'utf8', (error?: Error | null) => error ? reject(new Error('Output stream failed')) : resolve()); + }); + }; + for (const part of jsonPieces(current.message)) { batch += part; if (batch.length >= 65536) await flush(); } + batch += '\n'; await flush(); + current.resolve(); + } catch { + this.failure = new Error('Output stream failed'); + current.reject(this.failure); + for (const queued of this.queue.splice(0)) queued.reject(this.failure); + this.closed = true; + } + this.active = undefined; + } + for (const resolve of this.idle.splice(0)) resolve(); + } + async close(): Promise { + this.closed = true; + if (this.active || this.queue.length) await new Promise(resolve => this.idle.push(resolve)); + if (this.failure) throw this.failure; + } +} +export function logDiagnostic(message: string): void { + process.stderr.write(message.slice(0, 1024) + '\n'); +} diff --git a/agent/json.ts b/agent/json.ts new file mode 100644 index 0000000..14cc335 --- /dev/null +++ b/agent/json.ts @@ -0,0 +1,30 @@ +/** Bounded serialization fragments: large escaped strings never form one copy. */ +export function* jsonPieces(value: any, depth = 0): Generator { + if (depth > 64) throw new Error('JSON nesting limit'); + if (typeof value === 'string') { + yield '"'; + for (let offset = 0; offset < value.length;) { + let end = Math.min(value.length, offset + 16384); + if (end < value.length && /[\uD800-\uDBFF]/.test(value[end - 1])) end--; + yield JSON.stringify(value.slice(offset, end)).slice(1, -1); offset = end; + } + yield '"'; + } else if (Array.isArray(value)) { + yield '['; + for (let i = 0; i < value.length; i++) { if (i) yield ','; yield* jsonPieces(value[i] ?? null, depth + 1); } + yield ']'; + } else if (value && typeof value === 'object') { + yield '{'; let first = true; + for (const key of Object.keys(value)) { + if (value[key] === undefined || typeof value[key] === 'function' || typeof value[key] === 'symbol') continue; + if (!first) yield ','; first = false; + yield JSON.stringify(key); yield ':'; yield* jsonPieces(value[key], depth + 1); + } + yield '}'; + } else { yield JSON.stringify(value) ?? 'null'; } +} +export function jsonBytes(value: any, limit = Number.MAX_SAFE_INTEGER): number { + let bytes = 0; + for (const part of jsonPieces(value)) { bytes += Buffer.byteLength(part); if (bytes > limit) return bytes; } + return bytes; +} diff --git a/agent/local-tools.ts b/agent/local-tools.ts new file mode 100644 index 0000000..d6062a4 --- /dev/null +++ b/agent/local-tools.ts @@ -0,0 +1,56 @@ +import fs from 'node:fs/promises'; +import type { AgentTool } from '@earendil-works/pi-agent-core'; +import { constants } from 'node:fs'; +import { spawn } from 'node:child_process'; +import { createCodingTools, type BashOperations } from '@earendil-works/pi-coding-agent'; +const maxFile = 16 * 1024 * 1024; +async function readFile(name: string): Promise { + const file = await fs.open(name, constants.O_RDONLY | constants.O_NONBLOCK); + try { + const stat = await file.stat(); + if (!stat.isFile() || stat.size > maxFile) throw new Error('Local read requires a regular file of at most 16 MiB; use shell ranges for large files.'); + // Bound reads even if another local process grows the file concurrently. + const buffer = Buffer.alloc(stat.size + 1); + let offset = 0; + while (offset < buffer.length) { const { bytesRead } = await file.read(buffer, offset, buffer.length - offset, offset); if (!bytesRead) break; offset += bytesRead; } + if (offset > stat.size) throw new Error('Local file changed while reading.'); + return buffer.subarray(0, offset); + } finally { await file.close(); } +} +const operations: BashOperations = { + exec: (command, cwd, options) => new Promise((resolve, reject) => { + const child = spawn('/bin/bash', ['--noprofile', '--norc', '-c', command], { cwd, detached: true, env: { PATH: process.env.PATH, HOME: process.env.HOME, LANG: 'C.UTF-8' }, stdio: ['ignore', 'pipe', 'pipe'] }); + let bytes = 0, capped = false, timedOut = false; + const kill = () => { if (child.pid) { try { process.kill(-child.pid, 'SIGKILL'); } catch {} } }; + const output = (data: Buffer) => { + if (capped) return; + const remaining = 1024 * 1024 - bytes; + options.onData(data.subarray(0, remaining)); bytes += Math.min(data.length, remaining); + if (data.length > remaining) { capped = true; options.onData(Buffer.from('\n[Local shell output capped at 1 MiB.]\n')); kill(); } + }; + child.stdout.on('data', output); child.stderr.on('data', output); + options.signal?.addEventListener('abort', kill, { once: true }); + if (options.signal?.aborted) kill(); + const timer = setTimeout(() => { timedOut = true; kill(); }, Math.min(30000, (options.timeout ?? 30) * 1000)); + child.on('error', () => reject(new Error('Local shell could not start.'))); + child.on('close', code => { + clearTimeout(timer); options.signal?.removeEventListener('abort', kill); + if (options.signal?.aborted) reject(new Error('aborted')); + else if (timedOut) reject(new Error(`timeout:${Math.min(30, options.timeout ?? 30)}`)); + else resolve({ exitCode: capped ? 1 : code }); + }); + }), +}; +export function localTools(work: string): AgentTool[] { + const tools = createCodingTools(work, { + bash: { operations, exposeSessionEnvironment: false, shellPath: '/bin/bash' }, + read: { operations: { readFile, access: p => fs.access(p, constants.R_OK), detectImageMimeType: async () => null } }, + edit: { operations: { readFile, access: p => fs.access(p, constants.R_OK | constants.W_OK), writeFile: async (p, content) => { + if (Buffer.byteLength(content) > maxFile) throw new Error('Local edit result exceeds 16 MiB.'); + await fs.writeFile(p, content); + } } }, + }); + const read = tools.find(t => t.name === 'read')!; + read.description = 'Read regular text files up to 16 MiB. Output is truncated to 2000 lines or 50 KiB; use offset/limit for continuation. Use shell ranges for larger files. Image attachments are not supported by this text/tool model adapter.'; + return tools; +} diff --git a/agent/model-provider.ts b/agent/model-provider.ts new file mode 100644 index 0000000..001a811 --- /dev/null +++ b/agent/model-provider.ts @@ -0,0 +1,277 @@ +import type { + Context, + Model, + AssistantMessage, + SimpleStreamOptions, + TextContent, + ToolCall +} from "@earendil-works/pi-ai"; +import { createAssistantMessageEventStream } from "@earendil-works/pi-ai"; +import type { StreamFn } from "@earendil-works/pi-agent-core"; +import type { + ModelDescriptor, + ModelRequest, + ModelResponse, + MessageContract, + ContentBlock, + ToolDeclaration +} from "./types.js"; + +/** + * Converts pi-ai Context into CONTRACTS ModelRequest format. + * Invariants: + * - System instruction is NOT duplicated into messages. + * - Tool calls use type: "tool_call". + * - Tool results use role: "tool", tool_call_id, is_error. + * - Provider state is round-tripped on assistant messages. + */ +export function contextToModelRequest(context: Context): ModelRequest { + const messages: MessageContract[] = []; + + for (const m of context.messages) { + if (m.role === "user") { + let textContent: string = ""; + if (typeof m.content === "string") { + textContent = m.content; + } else if (Array.isArray(m.content)) { + textContent = m.content + .filter((c: any) => c.type === "text") + .map((c: any) => c.text) + .join("\n"); + } + messages.push({ + role: "user", + content: [{ type: "text", text: textContent }] + }); + } else if (m.role === "assistant") { + const contentBlocks: ContentBlock[] = []; + for (const block of m.content) { + if (block.type === "text") { + contentBlocks.push({ + type: "text", + text: block.text + }); + } else if (block.type === "toolCall") { + contentBlocks.push({ + type: "tool_call", + id: block.id, + name: block.name, + arguments: block.arguments || {} + }); + } + // Thinking blocks are not part of shared model boundary + } + + const assistantMsg: MessageContract = { + role: "assistant", + content: contentBlocks, + ...((m as any).provider_state !== undefined ? { provider_state: (m as any).provider_state } : {}) + }; + messages.push(assistantMsg); + } else if (m.role === "toolResult") { + let text = ""; + if (typeof m.content === "string") { + text = m.content; + } else if (Array.isArray(m.content)) { + text = m.content + .filter((c: any) => c.type === "text") + .map((c: any) => c.text) + .join("\n"); + } + + messages.push({ + role: "tool", + tool_call_id: m.toolCallId, + name: m.toolName, + content: text, + is_error: Boolean(m.isError) + }); + } + } + + const tools: ToolDeclaration[] = (context.tools || []).map((t) => ({ + name: t.name, + description: t.description || "", + input_schema: (t.parameters as any) || {} + })); + + return { + messages, + tools + }; +} + +/** + * Converts CONTRACTS ModelResponse to pi-ai AssistantMessage. + */ +export function modelResponseToAssistantMessage( + response: ModelResponse, + model: Model +): AssistantMessage { + const content: (TextContent | ToolCall)[] = []; + + for (const block of response.content) { + if (block.type === "text") { + content.push({ + type: "text", + text: block.text + }); + } else if (block.type === "tool_call") { + content.push({ + type: "toolCall", + id: block.id, + name: block.name, + arguments: block.arguments || {} + }); + } + } + + let stopReason: AssistantMessage["stopReason"] = "stop"; + if (response.stop_reason === "tool_calls") { + stopReason = "toolUse"; + } else if (response.stop_reason === "length") { + stopReason = "length"; + } else if (response.stop_reason === "stop") { + stopReason = "stop"; + } + + const assistantMessage: AssistantMessage = { + role: "assistant", + content, + api: model.api, + provider: model.provider, + model: model.id, + usage: { + input: response.usage?.input_tokens ?? 0, + output: response.usage?.output_tokens ?? 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: (response.usage?.input_tokens ?? 0) + (response.usage?.output_tokens ?? 0), + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } + }, + stopReason, + timestamp: Date.now() + }; + + if (response.provider_state !== undefined) { + (assistantMessage as any).provider_state = response.provider_state; + } + + return assistantMessage; +} + +export type SendModelRequestFn = (request: ModelRequest) => Promise; + +/** + * Creates a Model definition and StreamFn that routes model calls across the bridge. + */ +export function createModelProvider( + descriptor: ModelDescriptor, + sendModelRequest: SendModelRequestFn +): { model: Model; streamFn: StreamFn } { + const model: Model = { + id: descriptor.id, + name: descriptor.id, + api: "custom" as any, + provider: "backend", + baseUrl: "", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: descriptor.context_window_tokens, + maxTokens: descriptor.max_output_tokens + }; + + const streamFn: StreamFn = async (_model, context, options) => { + const stream = createAssistantMessageEventStream(); + + (async () => { + try { + if (options?.signal?.aborted) { + const abortedMsg: AssistantMessage = { + role: "assistant", + content: [], + api: model.api, + provider: model.provider, + model: model.id, + usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }, + stopReason: "aborted", + errorMessage: "Request aborted", + timestamp: Date.now() + }; + stream.push({ type: "error", reason: "aborted", error: abortedMsg }); + stream.end(abortedMsg); + return; + } + + const modelRequest = contextToModelRequest(context); + const response = await sendModelRequest(modelRequest); + const assistantMessage = modelResponseToAssistantMessage(response, model); + + if (options?.signal?.aborted) { + const abortedMsg: AssistantMessage = { + role: "assistant", + content: [], + api: model.api, + provider: model.provider, + model: model.id, + usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }, + stopReason: "aborted", + errorMessage: "Request aborted", + timestamp: Date.now() + }; + stream.push({ type: "error", reason: "aborted", error: abortedMsg }); + stream.end(abortedMsg); + return; + } + + // Emit streaming events + const partial: AssistantMessage = { + ...assistantMessage, + content: [], + stopReason: "pending" as any + }; + stream.push({ type: "start", partial: { ...partial } }); + + for (let i = 0; i < assistantMessage.content.length; i++) { + const block = assistantMessage.content[i]; + if (block.type === "text") { + partial.content = [...partial.content, { type: "text", text: "" }]; + stream.push({ type: "text_start", contentIndex: i, partial: { ...partial } }); + partial.content[i] = { type: "text", text: block.text }; + stream.push({ type: "text_delta", contentIndex: i, delta: block.text, partial: { ...partial } }); + stream.push({ type: "text_end", contentIndex: i, content: block.text, partial: { ...partial } }); + } else if (block.type === "toolCall") { + partial.content = [...partial.content, { type: "toolCall", id: block.id, name: block.name, arguments: {} }]; + stream.push({ type: "toolcall_start", contentIndex: i, partial: { ...partial } }); + partial.content[i] = block; + stream.push({ type: "toolcall_delta", contentIndex: i, delta: JSON.stringify(block.arguments), partial: { ...partial } }); + stream.push({ type: "toolcall_end", contentIndex: i, toolCall: block, partial: { ...partial } }); + } + } + + const validDoneReason = assistantMessage.stopReason as "stop" | "toolUse" | "length"; + stream.push({ type: "done", reason: validDoneReason, message: assistantMessage }); + stream.end(assistantMessage); + } catch (err: any) { + const errorMsg: AssistantMessage = { + role: "assistant", + content: [], + api: model.api, + provider: model.provider, + model: model.id, + usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }, + stopReason: "error", + errorMessage: "Model request failed.", + timestamp: Date.now() + }; + stream.push({ type: "error", reason: "error", error: errorMsg }); + stream.end(errorMsg); + } + })(); + + return stream; + }; + + return { model, streamFn }; +} diff --git a/agent/package-lock.json b/agent/package-lock.json new file mode 100644 index 0000000..7b95991 --- /dev/null +++ b/agent/package-lock.json @@ -0,0 +1,3993 @@ +{ + "name": "confluence-agent-runtime", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "confluence-agent-runtime", + "version": "1.0.0", + "dependencies": { + "@earendil-works/pi-agent-core": "0.85.1", + "@earendil-works/pi-ai": "0.85.1", + "@earendil-works/pi-coding-agent": "0.85.1", + "typebox": "1.3.7" + }, + "devDependencies": { + "@types/node": "22.20.2", + "tsx": "4.23.13", + "typescript": "5.9.3" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.123.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.123.0.tgz", + "integrity": "sha512-Y9oX9mPNGZClHQOFqrWRk43Srcu/UHuPq3rfxxOq7JgW0gi+lJA2MAOK4Ul3k/+AUrwRWFJvd0tK3oC0Pw25dw==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-node": "^3.972.42", + "@aws-sdk/eventstream-handler-node": "^3.972.16", + "@aws-sdk/middleware-eventstream": "^3.972.12", + "@aws-sdk/middleware-websocket": "^3.972.19", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.978.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.0.tgz", + "integrity": "sha512-2yX9LUmxPklVjSGTb8dfnWRJSiFQ3TeH2nn7G1mdKHTfnabzF0+gfrS8rYfLWmZrQ8A3mEcxMJjRc51dL5KWaA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.5", + "@aws-sdk/xml-builder": "^3.972.40", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.33.3", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.17.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.71", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.71.tgz", + "integrity": "sha512-JN+JHruYZw3GUZB8YGAlDk4wTDPOEAEEdEzj5nS0xodWR4smzHsN7PnK2j6IeOsDIj2aqua5DSbhXl9Gtf90FQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.73", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.73.tgz", + "integrity": "sha512-uyYYnJOnlis8uQzaYGPd7N1JoioCoNpXgnkXYixsWJXHXgXyYi8WXJSDfofxJeWfQIGWLe2Nwyq60Uc7MZdVOg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/fetch-http-handler": "^5.7.2", + "@smithy/node-http-handler": "^4.11.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-http-handler": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz", + "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.16.tgz", + "integrity": "sha512-i++ly+0Uxa+u3ebSSyr0S/3CFhFJDxCXT3+Zj+mW2bXenEx5bKGCdTIKFu39SgXBNhWDjex/8cXUx9MUTMCrTw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/credential-provider-env": "^3.972.71", + "@aws-sdk/credential-provider-http": "^3.972.73", + "@aws-sdk/credential-provider-login": "^3.972.78", + "@aws-sdk/credential-provider-process": "^3.972.71", + "@aws-sdk/credential-provider-sso": "^3.973.15", + "@aws-sdk/credential-provider-web-identity": "^3.972.77", + "@aws-sdk/nested-clients": "^3.997.45", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.78", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.78.tgz", + "integrity": "sha512-eUtswnXu0+Ii9ieRK+0L7aPFV3Z/dnW2VntJzjBP9xs8s+8p5nBNuymIXtXwZ+5r5+XJP3e32nMkuZ/r0HozEA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/nested-clients": "^3.997.45", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.83", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.83.tgz", + "integrity": "sha512-jdso7ejzfRnatxMUZK4S/U6KbaDPCvfIV4XL+IQAPFDBt5rj5Fq595euqlK8Le4lNCMFR9oUpt+1l0aMgaayOQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.71", + "@aws-sdk/credential-provider-http": "^3.972.73", + "@aws-sdk/credential-provider-ini": "^3.973.16", + "@aws-sdk/credential-provider-process": "^3.972.71", + "@aws-sdk/credential-provider-sso": "^3.973.15", + "@aws-sdk/credential-provider-web-identity": "^3.972.77", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.71", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.71.tgz", + "integrity": "sha512-lYmXJa4gvq4xN1lrT5NiP5vIYYKcGWAdj8y+8o6dlcateB5eF3Dn8DtmjjHKfMBrTPAMr2pebIiX/UOj8c1/UA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.15.tgz", + "integrity": "sha512-6Jhcf4v0pSFdjk1EW2kvzuEBKD+UZ2uNcHUIglKKLndD20YhvkL2kdmDOV5/j4mYuWWwe/a1FQ1aomU86/Cg5Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/nested-clients": "^3.997.45", + "@aws-sdk/token-providers": "3.1129.0", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.1129.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1129.0.tgz", + "integrity": "sha512-Sbl3rpzQdsG4ZK2zh0JWUYyZPKKorJlVOddA2T0DVbKJFrsW8J6wgnslxxUH04+WaBMr4A1HzJZvZX0xUvkniA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/nested-clients": "^3.997.45", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.77", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.77.tgz", + "integrity": "sha512-uylIQSUWpfLuH2LovxEEfwzJGM/SabLOfLMg6YXu/E8jJEKUdpdILCVCQCdFvHyu/7dLJOHPMfrSwduxO56NkQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/nested-clients": "^3.997.45", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.34.tgz", + "integrity": "sha512-cTeVzpu1xEAkryTZBYhGwnQ6gOGyp8ZYZvmn0Sg/nI/ABmy/CRHHxPDJDUi9PxwxUtGGaatvfRUB3FCgT/rSWw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.29.tgz", + "integrity": "sha512-dlRzHCgyB8W6hLuDC5pcT5q+ziPt00n4QGgGBE17ucLVU4zMa6lsbuUdQ2Pm75Z5VA8GF+R/+SgrRcaTdIzSIQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.53", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.53.tgz", + "integrity": "sha512-bIrDaMENQmYRBHntOiOheqkiw5+fhKW4Lqb+mS1uqF0VwvdWI22fW2HFgWrng66CmYd+4k8ePlpj38sEfTuMLQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/fetch-http-handler": "^5.7.2", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.45", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.45.tgz", + "integrity": "sha512-mooq9Q+jLa18VoM7HouczmslZU60iiB0aKc/Ztnq/luIL1ud0z4DnYprLR/ZO1gp331S9tJctM1HZr7u6YKBXQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/signature-v4-multi-region": "^3.996.46", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/fetch-http-handler": "^5.7.2", + "@smithy/node-http-handler": "^4.11.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz", + "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.46", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.46.tgz", + "integrity": "sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.5", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.974.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.5.tgz", + "integrity": "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.10.tgz", + "integrity": "sha512-ycwH6Zd2GhuSqdXX9ihbCjeGTB6xOJs+O3+Jb8/zDG9978XU80qs75dfkPJRMNKe5MvBZPuNeFpd4JZKPoUF4g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.40", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.40.tgz", + "integrity": "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@earendil-works/chord": { + "version": "0.85.1", + "resolved": "https://registry.npmjs.org/@earendil-works/chord/-/chord-0.85.1.tgz", + "integrity": "sha512-VDlkEC3dhCzQ5fcyH1OhG19dq+6jCn+rqc/iXFivwDYGR5anwo2RCiXij9PpHhqNR5GuhhE+Er69Zi1Sn4eY6w==", + "license": "MIT", + "dependencies": { + "esbuild": "0.28.1" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-agent-core": { + "version": "0.85.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.85.1.tgz", + "integrity": "sha512-hIXIP3eAWueAYiAl8aMvWCvvZ8Q5gT3Dip5bE5uJyIGh4+YlWRjtMLI4BaeoXoSs93zndjue61u1B/vhefLnuA==", + "license": "MIT", + "dependencies": { + "@earendil-works/chord": "^0.85.1", + "@earendil-works/pi-ai": "^0.85.1", + "@earendil-works/pi-telemetry": "^0.85.1", + "diff": "8.0.4", + "ignore": "7.0.5", + "typebox": "1.3.7", + "yaml": "2.9.0" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-ai": { + "version": "0.85.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.85.1.tgz", + "integrity": "sha512-+VgVIJDkDO2efYJKEEqvPTH4zmnIaXdAppGbO+vKFA9qy5PdhFiAenuFAkU+oiCSfOC4dMHDyrjdQeL4ZoC5CQ==", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.123.0", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@earendil-works/pi-telemetry": "^0.85.1", + "@google/genai": "1.52.0", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.40.0", + "partial-json": "0.1.7", + "typebox": "1.3.7" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent": { + "version": "0.85.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.85.1.tgz", + "integrity": "sha512-FGRN+OHbWaefBPGaTggAdLjrIHW+s2PzLyglz/5dfLzb9of7uuXMXYC0fJIeZTw+shS32o2cuQ9jF7YSDuL/oQ==", + "hasShrinkwrap": true, + "license": "MIT", + "dependencies": { + "@earendil-works/chord": "^0.85.1", + "@earendil-works/pi-agent-core": "^0.85.1", + "@earendil-works/pi-ai": "^0.85.1", + "@earendil-works/pi-tui": "^0.85.1", + "@silvia-odwyer/photon-node": "0.3.4", + "chalk": "5.6.2", + "cross-spawn": "7.0.6", + "diff": "8.0.4", + "grok-mermaid": "0.2.2", + "highlight.js": "10.7.3", + "hosted-git-info": "9.0.3", + "ignore": "7.0.5", + "jiti": "2.7.0", + "minimatch": "10.2.5", + "proper-lockfile": "4.1.2", + "semver": "7.8.0", + "typebox": "1.3.7", + "undici": "8.9.0", + "yaml": "2.9.0" + }, + "bin": { + "pi": "dist/bundle/cli.js" + }, + "engines": { + "node": ">=22.19.0" + }, + "optionalDependencies": { + "@mariozechner/clipboard": "0.3.9" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@anthropic-ai/sdk": { + "version": "0.123.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.123.0.tgz", + "integrity": "sha512-Y9oX9mPNGZClHQOFqrWRk43Srcu/UHuPq3rfxxOq7JgW0gi+lJA2MAOK4Ul3k/+AUrwRWFJvd0tK3oC0Pw25dw==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-node": "^3.972.42", + "@aws-sdk/eventstream-handler-node": "^3.972.16", + "@aws-sdk/middleware-eventstream": "^3.972.12", + "@aws-sdk/middleware-websocket": "^3.972.19", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/core": { + "version": "3.974.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.11.tgz", + "integrity": "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.24", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.37.tgz", + "integrity": "sha512-/jpPvEh6f7ntmIzf7dNxoNX6Q8vt8UpesCjbW6mFfk4V1NW6bIy9qxcQ6WbA8As5yQhsZOe+xeNd4xHX8kdY2Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.39.tgz", + "integrity": "sha512-pIgTpisWyWg7X1bUbzSjuUYosYTD0Ghz2M0hkSTmb3a6i3qV3uU+NYJPI/E2XSC0HcsZh5rsLPzeXrkb2DS0Cg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.41.tgz", + "integrity": "sha512-u2tyjaxJJzW8UtW4SM1ZcPMDwO6y+kV+llvou+Adts0FAKyzes5jG4izQN+KX3yE8ZROpS5y1LJ//xL2iSf76w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-login": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.41.tgz", + "integrity": "sha512-0LBitxXiAiaE5nlFPfpNIww/8FRY/I7WIndWsc9GmNFOM7cE1wNpVNQEGEk9Outg5l8xl+3vybxFyUy4l9q/LQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.42", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.42.tgz", + "integrity": "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-ini": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.37.tgz", + "integrity": "sha512-7nVaHBUaWIddASYfVaA9O4D5ZVjewU3sCol9WqZPGfW0nR+0WqE0xHZnD/U2L33PlOB8KNXGKZ6wOES/QijKzg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.41.tgz", + "integrity": "sha512-IOWAWEHe5LkjSKkkUUX9ciV6Y1scHTsnfEkdt5yyC4Slrc7AGbkLPrpntjqh18ksJAMOaVhoBsO8p2WyTcY2wQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.41.tgz", + "integrity": "sha512-mbACk9Yypa8nm4iGZLs0PofOXEcTDOUw6wDnsPXNDNSd2WNXs1tSo+6nc/fh0jLYdfVZThhBL98PHW4aXFsG5A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.16.tgz", + "integrity": "sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.12.tgz", + "integrity": "sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.19.tgz", + "integrity": "sha512-mkEhOGYozqKQkbFaVrjwr0faiwwZza1v5/jSY6Tucm3bD+uKTazIUH/4Yo6aMnQD2ua2W9cMP6s8mvwTcjtqHw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/nested-clients": { + "version": "3.997.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.9.tgz", + "integrity": "sha512-jPR3rnmRI4hWYyzfmTGBr7NblMp8QYYeflHXba1H6+7CGrWVqWKQzaXFQ4qbExqPRsXN3T3L3JxFhr6aouXUGQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/signature-v4-multi-region": "^3.996.27", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.27.tgz", + "integrity": "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/token-providers": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/types": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", + "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", + "license": "Apache-2.0", + "dependencies": { + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/chord": { + "version": "0.85.1", + "resolved": "https://registry.npmjs.org/@earendil-works/chord/-/chord-0.85.1.tgz", + "license": "MIT", + "dependencies": { + "esbuild": "0.28.1" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": { + "version": "0.85.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.85.1.tgz", + "license": "MIT", + "dependencies": { + "@earendil-works/chord": "^0.85.1", + "@earendil-works/pi-ai": "^0.85.1", + "@earendil-works/pi-telemetry": "^0.85.1", + "diff": "8.0.4", + "ignore": "7.0.5", + "typebox": "1.3.7", + "yaml": "2.9.0" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": { + "version": "0.85.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.85.1.tgz", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.123.0", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@earendil-works/pi-telemetry": "^0.85.1", + "@google/genai": "1.52.0", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.40.0", + "partial-json": "0.1.7", + "typebox": "1.3.7" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-telemetry": { + "version": "0.85.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.85.1.tgz", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": { + "version": "0.85.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.85.1.tgz", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "1.6.0", + "marked": "18.0.5" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.9.tgz", + "integrity": "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@mariozechner/clipboard-darwin-arm64": "0.3.9", + "@mariozechner/clipboard-darwin-universal": "0.3.9", + "@mariozechner/clipboard-darwin-x64": "0.3.9", + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-arm64-musl": "0.3.9", + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-musl": "0.3.9", + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.9", + "@mariozechner/clipboard-win32-x64-msvc": "0.3.9" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-arm64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.9.tgz", + "integrity": "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-universal": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.9.tgz", + "integrity": "sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-x64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.9.tgz", + "integrity": "sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.9.tgz", + "integrity": "sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.9.tgz", + "integrity": "sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.9.tgz", + "integrity": "sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.9.tgz", + "integrity": "sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.9.tgz", + "integrity": "sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-arm64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.9.tgz", + "integrity": "sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-x64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.9.tgz", + "integrity": "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "license": "Apache-2.0" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/core": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", + "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/credential-provider-imds": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", + "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/fetch-http-handler": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", + "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/signature-v4": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", + "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/grok-mermaid": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/grok-mermaid/-/grok-mermaid-0.2.2.tgz", + "integrity": "sha512-XcJEP5dDC8liHBh52mlLjU18fNvu1ckFsu0QpIG3+APZ270fsj9wxpiA6cOURmbUEuoMVgjbC2+UYgTdCqqgzA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/lru-cache": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", + "integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/marked": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/openai": { + "version": "6.40.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.40.0.tgz", + "integrity": "sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==", + "license": "Apache-2.0", + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/p-retry/node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/standardwebhooks": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.1.1.tgz", + "integrity": "sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/typebox": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.7.tgz", + "integrity": "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz", + "integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/@earendil-works/pi-telemetry": { + "version": "0.85.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.85.1.tgz", + "integrity": "sha512-Bg/YN6kA7Swja/NQxka8xFdecb4E/auIEGF2G5A25EaQXhRnPj300/7/KpgsDDMYUzHTDAv4RyUxaQPJKW81Rw==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@smithy/core": { + "version": "3.34.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.34.1.tgz", + "integrity": "sha512-dLcOUxz8YCv1RZUMKq6GbyUf95pLbrqh34bPvpCZ1+CByFF31BEAFewZjsGCnVsZTKdThNENfGyAgk2TJqVwSw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz", + "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz", + "integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.3.tgz", + "integrity": "sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.18.0.tgz", + "integrity": "sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.2.tgz", + "integrity": "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gaxios": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz", + "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.1.tgz", + "integrity": "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/openai": { + "version": "6.40.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.40.0.tgz", + "integrity": "sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==", + "license": "Apache-2.0", + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "license": "MIT" + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/standardwebhooks": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.1.1.tgz", + "integrity": "sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typebox": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.7.tgz", + "integrity": "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/agent/package.json b/agent/package.json new file mode 100644 index 0000000..af89385 --- /dev/null +++ b/agent/package.json @@ -0,0 +1,24 @@ +{ + "name": "confluence-agent-runtime", + "version": "1.0.0", + "description": "Isolated pi runtime and bridge for Confluence research", + "type": "module", + "scripts": { + "build": "tsc", + "test": "node --import tsx --test tests/*.test.ts", + "start": "python3 supervisor", + "proof": "npm run build && node dist/dev/fake-backend.js", + "test:image": "npm run build && node dist/dev/image-checks.js" + }, + "dependencies": { + "@earendil-works/pi-coding-agent": "0.85.1", + "@earendil-works/pi-agent-core": "0.85.1", + "@earendil-works/pi-ai": "0.85.1", + "typebox": "1.3.7" + }, + "devDependencies": { + "@types/node": "22.20.2", + "tsx": "4.23.13", + "typescript": "5.9.3" + } +} diff --git a/agent/supervision.ts b/agent/supervision.ts new file mode 100644 index 0000000..93eb56d --- /dev/null +++ b/agent/supervision.ts @@ -0,0 +1,22 @@ +import net from 'node:net'; +import { createInterface } from 'node:readline'; +export function supervisorChannel(): { deadline: (ms: number) => void; reap: () => Promise } | undefined { + const fd = process.env.AGENT_SUPERVISOR_FD; + if (!fd || !/^\d+$/.test(fd)) return undefined; + const socket = new net.Socket({ fd: Number(fd), readable: true, writable: true }); + const lines = createInterface({ input: socket }); + let awaiting: { resolve: () => void; reject: (err: Error) => void } | undefined; + lines.on('line', line => { + if (line !== '{"type":"reaped"}' || !awaiting) { socket.destroy(); return; } + awaiting.resolve(); awaiting = undefined; + }); + socket.on('error', () => { awaiting?.reject(new Error('Supervisor unavailable')); awaiting = undefined; }); + socket.on('close', () => { awaiting?.reject(new Error('Supervisor unavailable')); awaiting = undefined; }); + return { + deadline: ms => { socket.write(JSON.stringify({ type: 'set_deadline', remaining_ms: ms }) + '\n'); }, + reap: () => new Promise((resolve, reject) => { + awaiting = { resolve, reject }; + socket.write('{"type":"reap_descendants"}\n'); + }), + }; +} diff --git a/agent/supervisor b/agent/supervisor new file mode 100755 index 0000000..ac85f24 --- /dev/null +++ b/agent/supervisor @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Immutable Linux PID-1 supervisor; host mode is development-only.""" +import ctypes +import json +import os +import select +import signal +import socket +import subprocess +import sys +import time +from pathlib import Path + +libc = ctypes.CDLL(None, use_errno=True) +# Adopt double-forked descendants on hosts, as well as under container PID 1. +if libc.prctl(36, 1, 0, 0, 0) != 0: # PR_SET_CHILD_SUBREAPER + sys.exit(1) +if libc.prctl(4, 0, 0, 0, 0) != 0: # PR_SET_DUMPABLE: deny same-UID proc memory/fd access + sys.exit(1) +container = os.getpid() == 1 +if not container and '--dev' not in sys.argv: + sys.stderr.write('Supervisor requires container PID 1; use --dev only for host tests.\n') + sys.exit(1) +# PID 1 ignores namespace-local SIGKILL/SIGSTOP. Other signals terminate the +# run or are ignored; none can pause or extend the deadline. No exec after prctl. +for sig in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP, signal.SIGUSR1, signal.SIGUSR2): + signal.signal(sig, signal.SIG_IGN) +started = time.monotonic() +deadline = started + 180 +parent, child = socket.socketpair() +child.set_inheritable(True) +root = Path(__file__).resolve().parent +entry = root / 'dist' / 'bridge.js' +args = ['node', str(entry)] +env = {'PATH': '/usr/local/bin:/usr/bin:/bin', 'HOME': '/home/agent', 'LANG': 'C.UTF-8', + 'AGENT_SUPERVISOR_FD': str(child.fileno())} +if '--dev' in sys.argv: + env['PATH'] = os.environ.get('PATH', env['PATH']) + env['HOME'] = os.environ.get('HOME', '/tmp') + env['AGENT_WORK_DIR'] = os.environ.get('AGENT_WORK_DIR', '/work') +bridge = subprocess.Popen(args, env=env, pass_fds=(child.fileno(),)) +child.close() +parent.setblocking(False) +protected = {os.getpid(), bridge.pid} +buffer = b'' + + +def descendants(): + """Never signal unrelated host processes. Adopted children retain this ancestry.""" + parents = {} + for item in Path('/proc').iterdir(): + if item.name.isdecimal(): + try: + fields = (item / 'stat').read_text().rsplit(')', 1)[1].split() + parents[int(item.name)] = int(fields[1]) + except (OSError, ValueError, IndexError): + pass + owned = {os.getpid()} + while True: + extra = {pid for pid, ppid in parents.items() if ppid in owned} - owned + if not extra: + break + owned.update(extra) + return owned - protected + + +def reap(): + # Re-scan until every descendant is gone, including children forked during + # termination. Reap only adopted children, leaving Popen to reap the bridge. + while time.monotonic() < deadline: + victims = descendants() + if not victims: + return + for pid in victims: + try: + os.kill(pid, signal.SIGKILL) + except ProcessLookupError: + pass + try: + os.waitpid(pid, os.WNOHANG) + except ChildProcessError: + pass + time.sleep(0.01) + raise TimeoutError() + + +code = 1 +try: + deadline_set = False + collecting = False + while time.monotonic() < deadline: + if bridge.poll() is not None: + code = bridge.returncode + break + readable, _, _ = select.select([parent], [], [], min(0.05, max(0, deadline - time.monotonic()))) + if not readable: + continue + data = parent.recv(4096) + if not data: + try: + code = bridge.wait(timeout=min(1, max(0.01, deadline - time.monotonic()))) + except subprocess.TimeoutExpired: + pass + break + buffer += data + if len(buffer) > 8192: + break + while b'\n' in buffer: + line, buffer = buffer.split(b'\n', 1) + msg = json.loads(line) + if not isinstance(msg, dict): + raise ValueError() + if msg.get('type') == 'set_deadline' and not deadline_set: + ms = msg.get('remaining_ms') + if type(ms) is not int or not 1 <= ms <= 180000: + raise ValueError() + deadline = min(deadline, time.monotonic() + ms / 1000) + deadline_set = True + elif msg == {'type': 'reap_descendants'} and not collecting: + reap() + collecting = True + parent.sendall(b'{"type":"reaped"}\n') + else: + raise ValueError() +except (OSError, ValueError, TimeoutError): + pass +finally: + if container and time.monotonic() >= deadline: + sys.exit(1) # Kernel destroys the namespace; do not add cleanup time. + # Killing PID 1 ends the container namespace. Host development cleanup is + # still restricted to descendants of this supervisor. + protected.discard(bridge.pid) + deadline = max(deadline, time.monotonic() + 1) + try: + reap() + except (OSError, TimeoutError): + pass + try: + bridge.wait(timeout=1) + except subprocess.TimeoutExpired: + bridge.kill() + parent.close() +sys.exit(code if code >= 0 else 1) diff --git a/agent/tests/artifacts.test.ts b/agent/tests/artifacts.test.ts new file mode 100644 index 0000000..7861b7b --- /dev/null +++ b/agent/tests/artifacts.test.ts @@ -0,0 +1,91 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { scanArtifactsDirectory, exportArtifacts, closeArtifacts, validateRelativeArtifactPath } from '../artifacts.js'; + +function fixture(t: any) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-artifacts-')); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + return root; +} +test('path validation rejects traversal and portable-name violations', () => { + for (const name of ['', '/x', '../x', 'a/./x', 'a//x', 'a\\x', 'a\n', 'x'.repeat(1025)]) assert(!validateRelativeArtifactPath(name).valid); + assert(validateRelativeArtifactPath('docs/雪.md').valid); +}); +test('symlink parents, hardlinks, FIFO and normalized duplicates are omitted', t => { + const root = fixture(t); const exports = path.join(root, 'artifacts'); fs.mkdirSync(exports); + fs.writeFileSync(path.join(exports, 'ok'), 'bytes'); + fs.writeFileSync(path.join(root, 'private'), 'private'); + fs.symlinkSync(root, path.join(exports, 'parent')); + fs.symlinkSync(path.join(root, 'private'), path.join(exports, 'link')); + fs.linkSync(path.join(root, 'private'), path.join(exports, 'hard')); + execFileSync('mkfifo', [path.join(exports, 'fifo')]); + fs.writeFileSync(path.join(exports, 'é'), '1'); fs.writeFileSync(path.join(exports, 'e\u0301'), '2'); + const scan = scanArtifactsDirectory(exports); t.after(() => closeArtifacts(scan.candidates)); + assert.equal(scan.candidates.length, 2); assert(scan.warnings.some(w => w.code === 'artifact_not_regular' && /Repeated 4 times/.test(w.message))); + assert(scan.warnings.some(w => w.code === 'artifact_duplicate_name')); + fs.symlinkSync(exports, path.join(root, 'alias')); + assert.equal(scanArtifactsDirectory(path.join(root, 'alias')).candidates.length, 0); +}); +test('held descriptor prevents a replaced parent from redirecting export', async t => { + const root = fixture(t); const dir = path.join(root, 'artifacts'); fs.mkdirSync(dir); fs.mkdirSync(path.join(dir, 'nested')); + fs.writeFileSync(path.join(dir, 'nested', 'ok'), 'safe'); + const scan = scanArtifactsDirectory(dir); t.after(() => closeArtifacts(scan.candidates)); + fs.renameSync(path.join(dir, 'nested'), path.join(root, 'moved')); + fs.symlinkSync(root, path.join(dir, 'nested')); + fs.writeFileSync(path.join(root, 'ok'), 'evil'); + let bytes = ''; + assert.equal(await exportArtifacts(scan.candidates, { sendBegin: async () => 'accept', sendChunk: async (_id, _i, b) => { bytes += Buffer.from(b, 'base64').toString(); }, sendEnd: async () => {} }), 1); + assert.equal(bytes, 'safe'); +}); +test('empty files, skip and bounded chunks; changed files fail', async t => { + const root = fixture(t); fs.writeFileSync(path.join(root, 'empty'), ''); fs.writeFileSync(path.join(root, 'data'), Buffer.alloc(70000)); + const scan = scanArtifactsDirectory(root); t.after(() => closeArtifacts(scan.candidates)); + const chunks: number[] = []; const ends: number[] = []; + assert.equal(await exportArtifacts(scan.candidates, { sendBegin: async () => 'accept', sendChunk: async (_id, _i, b) => { chunks.push(Buffer.from(b, 'base64').length); }, sendEnd: async (_id, _size, n) => { ends.push(n); } }), 2); + assert.deepEqual(chunks, [65536, 4464]); assert(ends.includes(0)); + assert.equal(await exportArtifacts(scan.candidates, { sendBegin: async () => 'skip', sendChunk: async () => { throw Error(); }, sendEnd: async () => { throw Error(); } }), 0); + fs.appendFileSync(path.join(root, 'data'), 'x'); + await assert.rejects(exportArtifacts(scan.candidates, { sendBegin: async () => 'accept', sendChunk: async () => {}, sendEnd: async () => {} })); +}); +test('file count, individual size and total byte limits', t => { + const root = fixture(t); + for (let i = 0; i < 25; i++) fs.writeFileSync(path.join(root, `f${i}`), ''); + fs.closeSync(fs.openSync(path.join(root, 'oversized'), 'w')); fs.truncateSync(path.join(root, 'oversized'), 10 * 1024 * 1024 + 1); + const scan = scanArtifactsDirectory(root); t.after(() => closeArtifacts(scan.candidates)); + assert.equal(scan.candidates.length, 20); assert(scan.warnings.some(w => w.code === 'artifact_oversized')); + const large = fixture(t); + for (let i = 0; i < 6; i++) { const p = path.join(large, `f${i}`); fs.writeFileSync(p, ''); fs.truncateSync(p, 10 * 1024 * 1024); } + const largeScan = scanArtifactsDirectory(large); t.after(() => closeArtifacts(largeScan.candidates)); + assert.equal(largeScan.candidates.length, 5); assert(largeScan.warnings.some(w => w.code === 'artifact_total_limit_exceeded')); +}); + +test('socket files are omitted', async t => { + const root = fixture(t); + const { createServer } = await import('node:net'); + const server = createServer(); + await new Promise((resolve, reject) => { server.on('error', reject); server.listen(path.join(root, 'socket'), resolve); }); + t.after(() => new Promise(resolve => server.close(() => resolve()))); + const scan = scanArtifactsDirectory(root); t.after(() => closeArtifacts(scan.candidates)); + assert.equal(scan.candidates.length, 0); assert(scan.warnings.some(w => w.code === 'artifact_not_regular')); +}); +test('a growing file fails without sending excess bytes', async t => { + const root = fixture(t); const file = path.join(root, 'growing'); fs.writeFileSync(file, Buffer.alloc(70000)); + const scan = scanArtifactsDirectory(root); t.after(() => closeArtifacts(scan.candidates)); + let sent = 0; + await assert.rejects(exportArtifacts(scan.candidates, { sendBegin: async () => 'accept', sendChunk: async (_id, _index, data) => { + sent += Buffer.from(data, 'base64').length; fs.appendFileSync(file, Buffer.alloc(1000)); + }, sendEnd: async () => { throw new Error('Changed file must not close a successful transfer'); } })); + assert.equal(sent, 70000); +}); + +test('repeated filesystem rejections are aggregated without losing counts', t => { + const root = fixture(t); + for (let i = 0; i < 130; i++) fs.symlinkSync('/nonexistent-dummy-target', path.join(root, `link_${i}`)); + const scan = scanArtifactsDirectory(root); t.after(() => closeArtifacts(scan.candidates)); + assert.equal(scan.candidates.length, 0); assert.equal(scan.warnings.length, 1); + assert.equal(scan.warnings[0].code, 'artifact_not_regular'); assert.match(scan.warnings[0].message, /Repeated 130 times/); +}); diff --git a/agent/tests/bridge.test.ts b/agent/tests/bridge.test.ts new file mode 100644 index 0000000..bb7da78 --- /dev/null +++ b/agent/tests/bridge.test.ts @@ -0,0 +1,220 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { PassThrough, Writable } from 'node:stream'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { Bridge } from '../bridge.js'; + +const start = { prompt: 'dummy', system_instruction: 'trusted', remaining_ms: 1000, model: { id: 'dummy', context_window_tokens: 10000, max_output_tokens: 1000 } }; +function harness(t: any, script: (f: any, send: (type: string, payload: any, reply?: string) => void) => void, remaining = 1000) { + const work = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-bridge-')); t.after(() => fs.rmSync(work, { recursive: true, force: true })); + const input = new PassThrough(), output = new PassThrough(); + const bridge = new Bridge({ stdin: input, stdout: output, workDir: work, onChildReap: async () => {} }); + const frames: any[] = []; let bytes = '', seq = 0; + const send = (type: string, payload: any, reply_to?: string) => input.write(JSON.stringify({ v: 1, type, id: `b_${++seq}`, ...(reply_to ? { reply_to } : {}), payload }) + '\n'); + output.on('data', part => { + bytes += part.toString(); let end; + while ((end = bytes.indexOf('\n')) >= 0) { const f = JSON.parse(bytes.slice(0, end)); bytes = bytes.slice(end + 1); frames.push(f); script(f, send); } + }); + const done = bridge.start(); send('start', { ...start, remaining_ms: remaining }); + return { input, bridge, frames, done, work }; +} +const final = { content: [{ type: 'text', text: 'answer' }], stop_reason: 'stop', usage: { input_tokens: 2, output_tokens: 2 } }; +test('zero artifacts completes only after collection_ready and stays alive until EOF', async t => { + let complete!: () => void; const observed = new Promise(r => complete = r); + const h = harness(t, (f, send) => { + if (f.type === 'model_request') send('model_response', { result: final, error: null }, f.id); + if (f.type === 'collection_start') send('collection_ready', {}, f.id); + if (f.type === 'complete') complete(); + }); + await observed; await new Promise(resolve => setImmediate(resolve)); assert.equal(h.bridge.getState(), 'COMPLETE'); + let settled = false; h.done.then(() => settled = true); + await new Promise(r => setTimeout(r, 10)); assert(!settled); + h.input.end(); await h.done; + assert.equal(h.frames.at(-1).payload.accepted_transfer_count, 0); +}); +for (const mode of ['wrong-reply', 'wrong-type', 'duplicate', 'extra-field', 'eof', 'upstream', 'length', 'empty-answer']) { + test(`terminal model/protocol failure: ${mode}`, async t => { + const h = harness(t, (f, send) => { + if (f.type !== 'model_request') return; + if (mode === 'eof') { h.input.end(); return; } + const result = mode === 'length' ? { ...final, stop_reason: 'length' } : mode === 'empty-answer' ? { ...final, content: [] } : final; + const payload = mode === 'upstream' ? { result: null, error: { code: 'upstream_failed', message: 'SECRET_PAYLOAD' } } : { result, error: null }; + send(mode === 'wrong-type' ? 'tool_response' : 'model_response', mode === 'extra-field' ? { ...payload, bad: 1 } : payload, mode === 'wrong-reply' ? 'a_unknown' : f.id); + if (mode === 'duplicate') send('model_response', payload, f.id); + }); + await assert.rejects(h.done); assert.equal(h.bridge.getState(), 'FAILED'); + await new Promise(r => setTimeout(r, 5)); + assert(!h.frames.some(f => f.type === 'complete')); + assert(!JSON.stringify(h.frames.filter(f => f.type === 'error')).includes('SECRET')); + }); +} +test('collection correlation and stalled collection retain deadline', async t => { + for (const wrong of [true, false]) { + const h = harness(t, (f, send) => { + if (f.type === 'model_request') send('model_response', { result: final, error: null }, f.id); + if (wrong && f.type === 'collection_start') send('collection_ready', {}, 'a_unknown'); + }, 200); + await assert.rejects(h.done); + assert.equal(h.bridge.getState(), 'FAILED'); + } +}); +test('artifact begin/end acknowledgements must match request, transfer and phase', async t => { + for (const phase of ['begin', 'end']) { + const h = harness(t, (f, send) => { + if (f.type === 'model_request') send('model_response', { result: final, error: null }, f.id); + if (f.type === 'collection_start') send('collection_ready', {}, f.id); + if (f.type === 'artifact_begin') send('artifact_ack', { transfer_id: f.payload.transfer_id, decision: phase === 'begin' ? 'stored' : 'accept', warning: null }, f.id); + if (f.type === 'artifact_end') send('artifact_ack', { transfer_id: 'a_wrong', decision: 'stored', warning: null }, f.id); + }); + fs.mkdirSync(path.join(h.work, 'artifacts'), { recursive: true }); fs.writeFileSync(path.join(h.work, 'artifacts', 'empty'), ''); + await assert.rejects(h.done); assert(!h.frames.some(f => f.type === 'complete')); + } +}); + +test('concurrent remote calls correlate independently and stay bounded', async t => { + let modelFrame: any, sendReply!: (type: string, payload: any, reply?: string) => void; + let modelReady!: () => void; const ready = new Promise(resolve => modelReady = resolve); + const h = harness(t, (f, send) => { + sendReply = send; + if (f.type === 'model_request') { modelFrame = f; modelReady(); } + if (f.type === 'tool_request') send('tool_response', { result: { page_id: f.payload.parameters.page_id, title: '', space: '', url: '', markdown: '', truncated: false }, error: null }, f.id); + if (f.type === 'collection_start') send('collection_ready', {}, f.id); + if (f.type === 'complete') h.input.end(); + }); + await ready; + const calls = ['1', '2', '3'].map(page_id => h.bridge.sendToolRequest('confluence_view', { page_id })); + await assert.rejects(h.bridge.sendToolRequest('confluence_view', { page_id: '4' }), /limit/); + assert.deepEqual((await Promise.all(calls)).map(x => x.page_id), ['1', '2', '3']); + sendReply('model_response', { result: final, error: null }, modelFrame.id); + await h.done; + assert.equal(h.frames.filter(f => f.type === 'tool_request').length, 3); +}); +test('model call total cannot exceed fifty turns', async t => { + let calls = 0; + const h = harness(t, (f, send) => { + if (f.type !== 'model_request') return; + calls++; + send('model_response', { result: { content: [{ type: 'tool_call', id: `sdk_${calls}`, name: 'read', arguments: { path: '/nonexistent-pi-fixture' } }], stop_reason: 'tool_calls', usage: { input_tokens: 1, output_tokens: 1 } }, error: null }, f.id); + }, 2000); + await assert.rejects(h.done); assert.equal(calls, 50); assert(!h.frames.some(f => f.type === 'complete')); +}); + +for (const delayed of [false, true]) { + test(`accepted complete stays terminal beyond remaining_ms, delayed model=${delayed}`, async t => { + let observed!: () => void; const complete = new Promise(resolve => observed = resolve); + const h = harness(t, (f, send) => { + if (f.type === 'model_request') { + const respond = () => send('model_response', { result: final, error: null }, f.id); + if (delayed) setTimeout(respond, 200); else respond(); + } + if (f.type === 'collection_start') send('collection_ready', {}, f.id); + if (f.type === 'complete') observed(); + }, 400); + let settled = false; let failure: unknown; + h.done.then(() => settled = true, error => failure = error); + await complete; + await new Promise(resolve => setTimeout(resolve, 500)); + h.bridge.failRun('query_timeout'); + assert.equal(h.bridge.getState(), 'COMPLETE'); assert.equal(failure, undefined); assert(!settled); + assert.deepEqual(h.frames.map(f => f.type), ['model_request', 'collection_start', 'complete']); + h.input.end(); await h.done; + }); +} +test('stalled model still expires before completion', async t => { + const h = harness(t, () => {}, 80); + await assert.rejects(h.done, /deadline expired/); + assert.equal(h.bridge.getState(), 'FAILED'); assert(await h.bridge.flushOutput()); + assert.equal(h.frames.at(-1).type, 'error'); +}); +test('synchronous EOF cannot turn a failed complete write into success', async t => { + const work = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-complete-write-')); + t.after(() => fs.rmSync(work, { recursive: true, force: true })); + const input = new PassThrough(); let seq = 0; const frames: any[] = []; + const send = (type: string, payload: any, reply_to: string) => input.write(JSON.stringify({ v: 1, type, id: `b_${++seq}`, reply_to, payload }) + '\n'); + const output = new Writable({ write(chunk, _encoding, callback) { + const f = JSON.parse(chunk.toString()); frames.push(f); + if (f.type === 'model_request') send('model_response', { result: final, error: null }, f.id); + if (f.type === 'collection_start') send('collection_ready', {}, f.id); + if (f.type === 'complete') { input.end(); setTimeout(() => callback(new Error('DUMMY_PRIVATE_STREAM_ERROR')), 20); } + else callback(); + } }); + const bridge = new Bridge({ stdin: input, stdout: output, workDir: work, onChildReap: async () => {} }); + const done = bridge.start(); input.write(JSON.stringify({ v: 1, type: 'start', id: `b_${++seq}`, payload: start }) + '\n'); + await assert.rejects(done); assert.equal(bridge.getState(), 'FAILED'); + assert.equal(await bridge.flushOutput(), false); + assert.deepEqual(frames.map(f => f.type), ['model_request', 'collection_start', 'complete']); +}); +test('failure flushing waits for slow callbacks and stays bounded if output stalls', async () => { + for (const stalled of [false, true]) { + let release!: () => void; const chunks: string[] = []; + const output = new Writable({ write(chunk, _encoding, callback) { + chunks.push(chunk.toString()); release = () => callback(); + if (!stalled) setTimeout(release, 60); + } }); + const input = new PassThrough(); const bridge = new Bridge({ stdin: input, stdout: output }); + const done = bridge.start(); bridge.failRun('invalid_input'); await assert.rejects(done); + const started = Date.now(); const flushed = await bridge.flushOutput(stalled ? 40 : 500); + assert.equal(flushed, !stalled); assert(Date.now() - started < 600); + if (!stalled) assert(Date.now() - started >= 40); + assert.equal(JSON.parse(chunks.join('')).type, 'error'); + if (stalled) release(); input.end(); + } +}); +test('collection warnings retain repeated and overflow counts on the wire', async t => { + let captured: any[] = []; + const h = harness(t, (f, send) => { + if (f.type === 'model_request') send('model_response', { result: final, error: null }, f.id); + if (f.type === 'collection_start') { captured = f.payload.warnings; send('collection_ready', {}, f.id); } + if (f.type === 'complete') h.input.end(); + }); + for (let i = 0; i < 105; i++) h.bridge.addWarning({ code: `warning_${i}`, message: 'Bounded warning.' }); + h.bridge.addWarning({ code: 'warning_0', message: 'Bounded warning.' }); + await h.done; assert.equal(captured.length, 100); + assert.match(captured[0].message, /Repeated 2 times/); + assert.equal(captured.at(-1).code, 'warnings_aggregated'); assert.match(captured.at(-1).message, /^6 additional/); +}); + +test('deadline remains active until the complete write succeeds', async t => { + const work = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-complete-stall-')); + t.after(() => fs.rmSync(work, { recursive: true, force: true })); + const input = new PassThrough(); let seq = 0, release!: () => void; const frames: any[] = []; + const send = (type: string, payload: any, reply_to: string) => input.write(JSON.stringify({ v: 1, type, id: `b_${++seq}`, reply_to, payload }) + '\n'); + const output = new Writable({ write(chunk, _encoding, callback) { + const f = JSON.parse(chunk.toString()); frames.push(f); + if (f.type === 'model_request') send('model_response', { result: final, error: null }, f.id); + if (f.type === 'collection_start') send('collection_ready', {}, f.id); + if (f.type === 'complete') release = () => callback(); else callback(); + } }); + const bridge = new Bridge({ stdin: input, stdout: output, workDir: work, onChildReap: async () => {} }); + const done = bridge.start(); input.write(JSON.stringify({ v: 1, type: 'start', id: `b_${++seq}`, payload: { ...start, remaining_ms: 150 } }) + '\n'); + await assert.rejects(done, /deadline expired/); assert.equal(bridge.getState(), 'FAILED'); + release(); assert(await bridge.flushOutput()); input.end(); + assert.deepEqual(frames.map(f => f.type), ['model_request', 'collection_start', 'complete']); +}); + +test('EOF while complete is still queued fails before starting the terminal frame', async t => { + const work = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-queued-complete-')); + t.after(() => fs.rmSync(work, { recursive: true, force: true })); + fs.mkdirSync(path.join(work, 'artifacts')); fs.writeFileSync(path.join(work, 'artifacts', 'empty'), ''); + const input = new PassThrough(); let seq = 0, release!: () => void; const frames: any[] = []; + let stored!: () => void; const acknowledged = new Promise(resolve => stored = resolve); + const send = (type: string, payload: any, reply_to: string) => input.write(JSON.stringify({ v: 1, type, id: `b_${++seq}`, reply_to, payload }) + '\n'); + const output = new Writable({ write(chunk, _encoding, callback) { + const f = JSON.parse(chunk.toString()); frames.push(f); + if (f.type === 'model_request') send('model_response', { result: final, error: null }, f.id); + if (f.type === 'collection_start') send('collection_ready', {}, f.id); + if (f.type === 'artifact_begin') send('artifact_ack', { transfer_id: f.payload.transfer_id, decision: 'accept', warning: null }, f.id); + if (f.type === 'artifact_end') { + release = () => callback(); + send('artifact_ack', { transfer_id: f.payload.transfer_id, decision: 'stored', warning: null }, f.id); stored(); + } else callback(); + } }); + const bridge = new Bridge({ stdin: input, stdout: output, workDir: work, onChildReap: async () => {} }); + const done = bridge.start(); const failed = assert.rejects(done, /connection was lost/); + input.write(JSON.stringify({ v: 1, type: 'start', id: `b_${++seq}`, payload: start }) + '\n'); + await acknowledged; await new Promise(resolve => setImmediate(resolve)); + input.end(); await failed; release(); assert(await bridge.flushOutput()); + assert(!frames.some(f => f.type === 'complete')); assert.equal(frames.at(-1).type, 'error'); +}); diff --git a/agent/tests/framing.test.ts b/agent/tests/framing.test.ts new file mode 100644 index 0000000..42c51b9 --- /dev/null +++ b/agent/tests/framing.test.ts @@ -0,0 +1,85 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { PassThrough, Writable } from 'node:stream'; +import { NDJsonFrameParser, StreamMessageWriter, sanitizeError } from '../framing.js'; +import { envelope } from '../validation.js'; + +async function parse(chunks: Buffer[], max = 1024) { + const input = new PassThrough(); const parser = new NDJsonFrameParser(max); + input.pipe(parser); + const result = (async () => { const frames = []; for await (const frame of parser) frames.push(frame); return frames; })(); + for (const c of chunks) input.write(c); input.end(); + return result; +} +test('fragmented and coalesced UTF-8 frames, escaping', async () => { + const a = { v: 1, type: 'collection_ready', id: 'b_1', reply_to: 'a_1', payload: {} }; + const b = { text: '雪 🐈 \\ " \n' }; + const wire = Buffer.from(JSON.stringify(a) + '\n' + JSON.stringify(b) + '\n'); + assert.deepEqual(await parse([...wire].map(x => Buffer.from([x]))), [a, b]); + assert.deepEqual(await parse([wire]), [a, b]); +}); +test('invalid UTF-8, empty, malformed, oversized and truncated lines fail', async () => { + for (const bytes of [Buffer.from('\n'), Buffer.from('{\n'), Buffer.from('{"x":"\xff"}\n', 'latin1'), Buffer.alloc(65, 65), Buffer.from('{}')]) { + await assert.rejects(parse([bytes], 64)); + } + assert.deepEqual(await parse([Buffer.from('{}\n')], 2), [{}]); +}); +test('envelopes reject extra fields, wrong IDs and wrong versions', () => { + const a = { v: 1, type: 'collection_ready', id: 'b_1', reply_to: 'a_1', payload: {} }; + envelope(a); + for (const b of [{ ...a, extra: true }, { ...a, v: 2 }, { ...a, id: 'a_1' }, { ...a, payload: { extra: true } }, { ...a, reply_to: 'invalid/path' }]) assert.throws(() => envelope(b)); +}); +test('serialized concurrent writes await slow callbacks and preserve order', async () => { + const lines: string[] = []; + const stream = new Writable({ highWaterMark: 1, write(chunk, _encoding, cb) { setTimeout(() => { lines.push(chunk.toString()); cb(); }, 5); } }); + const writer = new StreamMessageWriter(stream); + await Promise.all(Array.from({ length: 8 }, (_, i) => writer.write({ v: 1, type: 'x', id: `a_${i}`, payload: {} }))); + assert.deepEqual(lines.map(x => JSON.parse(x).id), Array.from({ length: 8 }, (_, i) => `a_${i}`)); +}); +test('terminal diagnostics never echo arbitrary exception content', () => { + assert(!JSON.stringify(sanitizeError('invalid_input', new Error('SECRET_PROMPT'))).includes('SECRET')); + assert.equal(sanitizeError('SECRET_CODE', 'SECRET_BODY').code, 'execution_failed'); +}); + +test('fragment serializer preserves escaping and surrogate pairs across chunks', async () => { + const { jsonPieces, jsonBytes } = await import('../json.js'); + const value = { text: 'x'.repeat(16383) + '🐈\u0001\n"\\', nested: [{ unicode: '雪' }, null, true, 3] }; + const encoded = [...jsonPieces(value)].join(''); + assert.equal(encoded, JSON.stringify(value)); + assert.deepEqual(JSON.parse(encoded), value); + assert.equal(jsonBytes(value), Buffer.byteLength(encoded)); + assert(jsonBytes(value, 32) > 32); +}); + +test('terminal write replaces backlog only after the active large NDJSON frame', async () => { + const chunks: string[] = []; let started!: () => void; + const first = new Promise(resolve => started = resolve); + const stream = new Writable({ highWaterMark: 1, write(chunk, _encoding, cb) { + chunks.push(chunk.toString()); started(); setTimeout(cb, 8); + } }); + const writer = new StreamMessageWriter(stream); + const large = { v: 1, type: 'collection_start', id: 'a_1', payload: { markdown: '雪'.repeat(100000), warnings: [] } }; + const active = writer.write(large); + const queued = writer.write({ v: 1, type: 'artifact_begin', id: 'a_2', payload: {} }); + const cancelled = assert.rejects(queued, /cancelled/); + await first; + const terminal = writer.writeTerminal({ v: 1, type: 'error', id: 'a_3', payload: sanitizeError('query_timeout') }); + await Promise.all([active, cancelled, terminal, writer.close()]); + assert.deepEqual(chunks.join('').trimEnd().split('\n').map(line => JSON.parse(line)), [large, { v: 1, type: 'error', id: 'a_3', payload: sanitizeError('query_timeout') }]); + await assert.rejects(writer.write(large), /closed/); +}); +test('a started terminal frame cannot be followed by another terminal message', async () => { + let release!: () => void; const chunks: string[] = []; + const stream = new Writable({ write(chunk, _encoding, cb) { chunks.push(chunk.toString()); release = () => cb(); } }); + const writer = new StreamMessageWriter(stream); + const complete = writer.write({ v: 1, type: 'complete', id: 'a_1', payload: { accepted_transfer_count: 0 } }); + await assert.rejects(writer.writeTerminal({ v: 1, type: 'error', id: 'a_2', payload: sanitizeError('query_timeout') }), /already started/); + release(); await complete; await writer.close(); + assert.equal(chunks.join('').trimEnd().split('\n').length, 1); +}); +test('broken output rejects active and queued frames and reports failed drain', async () => { + const stream = new Writable({ write(_chunk, _encoding, cb) { setTimeout(() => cb(new Error('DUMMY_PRIVATE_ERROR')), 5); } }); + const writer = new StreamMessageWriter(stream); + await Promise.all([1, 2].map(i => assert.rejects(writer.write({ v: 1, type: 'x', id: `a_${i}`, payload: {} }), /Output stream failed/))); + await assert.rejects(writer.close(), /Output stream failed/); +}); diff --git a/agent/tests/local-tools.test.ts b/agent/tests/local-tools.test.ts new file mode 100644 index 0000000..b84f302 --- /dev/null +++ b/agent/tests/local-tools.test.ts @@ -0,0 +1,19 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { localTools } from '../local-tools.js'; +test('native shell bounds output and reports timeout as a failed tool', async t => { + const work = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-local-')); t.after(() => fs.rmSync(work, { recursive: true, force: true })); + const bash = localTools(work).find(tool => tool.name === 'bash')!; + await assert.rejects(bash.execute('sdk_output', { command: "python3 -c 'import sys; sys.stdout.write(\"x\"*2000000)'" }), /capped at 1 MiB/); + await assert.rejects(bash.execute('sdk_timeout', { command: 'sleep 10', timeout: 0.05 }), /timed out/); +}); +test('native reads reject large and nonregular files without blocking', async t => { + const work = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-read-')); t.after(() => fs.rmSync(work, { recursive: true, force: true })); + const read = localTools(work).find(tool => tool.name === 'read')!; + fs.writeFileSync(path.join(work, 'large'), ''); fs.truncateSync(path.join(work, 'large'), 17 * 1024 * 1024); + await assert.rejects(read.execute('sdk_large', { path: path.join(work, 'large') }), /at most 16 MiB/); + await assert.rejects(read.execute('sdk_special', { path: '/dev/zero' }), /regular file/); +}); diff --git a/agent/tests/peer.test.ts b/agent/tests/peer.test.ts new file mode 100644 index 0000000..85655ae --- /dev/null +++ b/agent/tests/peer.test.ts @@ -0,0 +1,30 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { runPeer } from '../dev/fake-backend.js'; +for (const mode of ['happy', 'no-artifacts', 'empty-file', 'skip', 'delayed', 'background', 'spaces', 'confluence-error']) { + test(`real SDK and native tools over public wire: ${mode}`, async () => { assert((await runPeer({ mode })).complete); }); +} +for (const mode of ['upstream-failure', 'bad-reply', 'duplicate', 'eof-model', 'context-limit', 'output-limit', 'zero-text']) { + test(`scripted peer failure: ${mode}`, async () => { + const result = await runPeer({ mode }); assert(!result.complete); + if (mode !== 'eof-model') assert(result.failure, result.stderr); + assert(!JSON.stringify(result.failure).includes('DUMMY_PRIVATE')); + }); +} + +test('host supervisor leaves sibling processes alive', async t => { + const { spawn } = await import('node:child_process'); + const sibling = spawn('sleep', ['10']); + t.after(() => sibling.kill('SIGTERM')); + assert((await runPeer()).complete); + assert.equal(sibling.exitCode, null); + assert.equal(sibling.signalCode, null); +}); + +test('failure entrypoint flushes an active large line and error through a slow peer', async () => { + const result = await runPeer({ mode: 'failure-during-collection' }); + assert(!result.complete); assert.equal(result.failure.code, 'invalid_input'); + const outgoing = result.transcript.filter(f => f.id.startsWith('a_')); + assert.deepEqual(outgoing.slice(-2).map(f => f.type), ['collection_start', 'error']); + assert(outgoing.at(-2).payload.markdown.length > 1024 * 1024); +}); diff --git a/agent/tests/sdk.test.ts b/agent/tests/sdk.test.ts new file mode 100644 index 0000000..f8b1fe7 --- /dev/null +++ b/agent/tests/sdk.test.ts @@ -0,0 +1,16 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { contextToModelRequest, modelResponseToAssistantMessage, createModelProvider } from '../model-provider.js'; +const descriptor = { id: 'scripted', context_window_tokens: 1000, max_output_tokens: 100 }; +test('usage, stop reasons and opaque continuation survive SDK translation', () => { + const { model } = createModelProvider(descriptor, async () => { throw Error(); }); + for (const reason of ['stop', 'tool_calls', 'length'] as const) { + const response = { content: [{ type: 'text' as const, text: 'hi' }, { type: 'tool_call' as const, id: 'sdk_1', name: 'read', arguments: { path: '/work/x' } }], stop_reason: reason, usage: { input_tokens: 7, output_tokens: 3 }, provider_state: '' }; + const message = modelResponseToAssistantMessage(response, model); + assert.equal(message.usage.totalTokens, 10); + assert.equal(message.stopReason, reason === 'tool_calls' ? 'toolUse' : reason); + const request = contextToModelRequest({ systemPrompt: 'TRUSTED', messages: [message] }); + assert.deepEqual(request.messages[0], { role: 'assistant', content: response.content, provider_state: '' }); + assert(!JSON.stringify(request).includes('TRUSTED')); + } +}); diff --git a/agent/tests/warnings.test.ts b/agent/tests/warnings.test.ts new file mode 100644 index 0000000..b892512 --- /dev/null +++ b/agent/tests/warnings.test.ts @@ -0,0 +1,23 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { WarningCollector } from '../warnings.js'; +import { warning } from '../validation.js'; + +test('repeats preserve bounded Unicode messages and snapshots do not alias', () => { + const collector = new WarningCollector(); const value = { code: 'repeat', message: '雪'.repeat(341), name: 'file' }; + collector.add(value); const before = collector.snapshot(); + for (let i = 0; i < 150; i++) collector.add(value); + const entries = collector.snapshot(); assert.equal(entries.length, 1); warning(entries[0]); + assert(Buffer.byteLength(entries[0].message) <= 1024); assert(!entries[0].message.includes('�')); assert.match(entries[0].message, /Repeated 151 times/); + assert.equal(before[0].message, value.message); +}); +test('overflow includes repetitions of the replaced last warning', () => { + const collector = new WarningCollector(); + for (let i = 0; i < 100; i++) collector.add({ code: `warning_${i}`, message: 'Warning.' }); + collector.add({ code: 'warning_99', message: 'Warning.' }); + collector.add({ code: 'overflow', message: 'Warning.' }); + collector.add({ code: 'warning_99', message: 'Warning.' }); + const entries = collector.snapshot(); assert.equal(entries.length, 100); + assert.match(entries.at(-1)!.message, /^4 additional/); + entries.forEach(warning); +}); diff --git a/agent/tsconfig.json b/agent/tsconfig.json new file mode 100644 index 0000000..7b4b573 --- /dev/null +++ b/agent/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "*.ts", + "dev/**/*.ts", + "tests/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/agent/types.ts b/agent/types.ts new file mode 100644 index 0000000..412280c --- /dev/null +++ b/agent/types.ts @@ -0,0 +1,252 @@ +/** + * Protocol types and limits defined in CONTRACTS.md Revision 1 + */ + +export const PROTOCOL_VERSION = 1; + +// Size limits in bytes +export const KIB = 1024; +export const MIB = 1024 * 1024; + +export const LIMITS = { + USER_PROMPT_MAX_BYTES: 16 * MIB, + FINAL_MARKDOWN_MAX_BYTES: 128 * MIB, + TOOL_REQUEST_PAYLOAD_MAX_BYTES: 16 * MIB, + TOOL_RESULT_PAYLOAD_MAX_BYTES: 128 * MIB, + MODEL_REQUEST_PAYLOAD_MAX_BYTES: 128 * MIB, + MODEL_RESULT_PAYLOAD_MAX_BYTES: 128 * MIB, + ENVELOPE_ALLOWANCE_BYTES: 64 * KIB, + START_FRAME_MAX_BYTES: 6 * (16 * MIB) + 64 * KIB, + COLLECTION_START_FRAME_MAX_BYTES: 6 * (128 * MIB) + 64 * KIB, + ORDINARY_FRAME_MAX_BYTES: 128 * MIB + 64 * KIB, + ARTIFACT_CHUNK_MAX_DECODED_BYTES: 64 * KIB, + ARTIFACT_MAX_FILES: 20, + ARTIFACT_MAX_FILE_BYTES: 10 * MIB, + ARTIFACT_MAX_TOTAL_BYTES: 50 * MIB, + MAX_DEADLINE_MS: 180_000, + MAX_REMOTE_CONCURRENCY: 4, + MAX_WARNINGS: 100, + MAX_ERROR_MESSAGE_BYTES: 1024, + MAX_PATH_BYTES: 1024 +} as const; + +export interface ErrorShape { + code: string; + message: string; +} + +export interface Warning { + code: string; + message: string; + tool_call_id?: string; + name?: string; +} + +export interface Page { + page_id: string; + title: string; + space: string; + url: string; +} + +export interface Pagination { + offset: number; + limit: number; + has_more: boolean; +} + +export interface ModelDescriptor { + id: string; + context_window_tokens: number; + max_output_tokens: number; +} + +// Model Protocol +export type TextContentBlock = { + type: "text"; + text: string; +}; + +export type ToolCallContentBlock = { + type: "tool_call"; + id: string; + name: string; + arguments: Record; +}; + +export type ContentBlock = TextContentBlock | ToolCallContentBlock; + +export interface UserMessageContract { + role: "user"; + content: Array<{ type: "text"; text: string }>; +} + +export interface AssistantMessageContract { + role: "assistant"; + content: ContentBlock[]; + provider_state?: string; +} + +export interface ToolMessageContract { + role: "tool"; + tool_call_id: string; + name: string; + content: string; + is_error: boolean; +} + +export type MessageContract = + | UserMessageContract + | AssistantMessageContract + | ToolMessageContract; + +export interface ToolDeclaration { + name: string; + description: string; + input_schema: Record; +} + +export interface ModelRequest { + messages: MessageContract[]; + tools: ToolDeclaration[]; +} + +export interface ModelResponse { + content: ContentBlock[]; + stop_reason: "stop" | "tool_calls" | "length"; + usage: { + input_tokens: number; + output_tokens: number; + }; + provider_state?: string; +} + +// Bridge Protocol Frames +export interface BridgeBaseMessage { + v: number; + type: string; + id: string; + reply_to?: string; + payload: any; +} + +export interface StartMessage extends BridgeBaseMessage { + type: "start"; + payload: { + prompt: string; + system_instruction: string; + remaining_ms: number; + model: ModelDescriptor; + }; +} + +export interface ToolRequestMessage extends BridgeBaseMessage { + type: "tool_request"; + payload: { + tool: string; + parameters: Record; + }; +} + +export interface ToolResponseMessage extends BridgeBaseMessage { + type: "tool_response"; + reply_to: string; + payload: { + result: any; + error: ErrorShape | null; + }; +} + +export interface ModelRequestMessage extends BridgeBaseMessage { + type: "model_request"; + payload: ModelRequest; +} + +export interface ModelResponseMessage extends BridgeBaseMessage { + type: "model_response"; + reply_to: string; + payload: { + result: ModelResponse | null; + error: ErrorShape | null; + }; +} + +export interface CollectionStartMessage extends BridgeBaseMessage { + type: "collection_start"; + payload: { + markdown: string; + warnings: Warning[]; + }; +} + +export interface CollectionReadyMessage extends BridgeBaseMessage { + type: "collection_ready"; + reply_to: string; + payload: Record; +} + +export interface ArtifactBeginMessage extends BridgeBaseMessage { + type: "artifact_begin"; + payload: { + transfer_id: string; + name: string; + size_bytes: number; + }; +} + +export interface ArtifactAckMessage extends BridgeBaseMessage { + type: "artifact_ack"; + reply_to: string; + payload: { + transfer_id: string; + decision: "accept" | "skip" | "stored"; + warning: Warning | null; + }; +} + +export interface ArtifactChunkMessage extends BridgeBaseMessage { + type: "artifact_chunk"; + payload: { + transfer_id: string; + index: number; + data_base64: string; + }; +} + +export interface ArtifactEndMessage extends BridgeBaseMessage { + type: "artifact_end"; + payload: { + transfer_id: string; + size_bytes: number; + chunks: number; + }; +} + +export interface CompleteMessage extends BridgeBaseMessage { + type: "complete"; + payload: { + accepted_transfer_count: number; + }; +} + +export interface ErrorMessage extends BridgeBaseMessage { + type: "error"; + payload: ErrorShape; +} + +export type BridgeInboundMessage = + | StartMessage + | ToolResponseMessage + | ModelResponseMessage + | CollectionReadyMessage + | ArtifactAckMessage; + +export type BridgeOutboundMessage = + | ToolRequestMessage + | ModelRequestMessage + | CollectionStartMessage + | ArtifactBeginMessage + | ArtifactChunkMessage + | ArtifactEndMessage + | CompleteMessage + | ErrorMessage; diff --git a/agent/validation.ts b/agent/validation.ts new file mode 100644 index 0000000..e9d322c --- /dev/null +++ b/agent/validation.ts @@ -0,0 +1,101 @@ +import { jsonBytes } from './json.js'; +import { LIMITS } from './types.js'; +import { isValidId } from './framing.js'; +export function object(x: unknown): x is Record { + return !!x && typeof x === 'object' && !Array.isArray(x); +} +export function keys(x: unknown, required: string[], optional: string[] = []): asserts x is Record { + if (!object(x) || required.some(k => !Object.hasOwn(x, k)) || Object.keys(x).some(k => !required.includes(k) && !optional.includes(k))) throw new Error('Invalid fields'); +} +export function integer(x: unknown, min = 0, max = Number.MAX_SAFE_INTEGER): void { + if (!Number.isSafeInteger(x) || (x as number) < min || (x as number) > max) throw new Error('Invalid integer'); +} +export function text(x: unknown, max: number, nonempty = false): void { + if (typeof x !== 'string' || Buffer.byteLength(x) > max || (nonempty && !x.trim())) throw new Error('Invalid text'); +} +export function warning(x: unknown): void { + keys(x, ['code', 'message'], ['tool_call_id', 'name']); + text(x.code, 128, true); text(x.message, 1024); + if (x.tool_call_id !== undefined && !isValidId(x.tool_call_id)) throw new Error('Invalid warning ID'); + if (x.name !== undefined) text(x.name, 1024); +} +export function envelope(x: any): void { + keys(x, ['v', 'type', 'id', 'payload'], ['reply_to']); + if (x.v !== 1 || !isValidId(x.id) || !x.id.startsWith('b_')) throw new Error('Invalid envelope'); + if (x.type === 'start') { + if (Object.hasOwn(x, 'reply_to')) throw new Error('Unexpected correlation'); + keys(x.payload, ['prompt', 'system_instruction', 'remaining_ms', 'model']); + text(x.payload.prompt, LIMITS.USER_PROMPT_MAX_BYTES, true); + text(x.payload.system_instruction, LIMITS.ENVELOPE_ALLOWANCE_BYTES, true); + integer(x.payload.remaining_ms, 1, LIMITS.MAX_DEADLINE_MS); + const m = x.payload.model; + keys(m, ['id', 'context_window_tokens', 'max_output_tokens']); + text(m.id, 1024, true); integer(m.context_window_tokens, 1); integer(m.max_output_tokens, 1, m.context_window_tokens); + const metadata = { ...x, payload: { ...x.payload, prompt: '' } }; + if (jsonBytes(metadata, LIMITS.ENVELOPE_ALLOWANCE_BYTES) > LIMITS.ENVELOPE_ALLOWANCE_BYTES) throw new Error('Start metadata too large'); + } else { + if (!isValidId(x.reply_to) || !x.reply_to.startsWith('a_')) throw new Error('Invalid correlation'); + if (x.type === 'collection_ready') keys(x.payload, []); + else if (x.type === 'artifact_ack') { + keys(x.payload, ['transfer_id', 'decision', 'warning']); + if (!isValidId(x.payload.transfer_id) || !['accept', 'skip', 'stored'].includes(x.payload.decision)) throw new Error('Invalid acknowledgement'); + if (x.payload.warning !== null) warning(x.payload.warning); + if (x.payload.decision === 'stored' && x.payload.warning !== null) throw new Error('Unexpected stored warning'); + } else if (x.type === 'tool_response' || x.type === 'model_response') { + keys(x.payload, ['result', 'error']); + if (x.payload.error !== null) { + keys(x.payload.error, ['code', 'message']); text(x.payload.error.code, 128, true); text(x.payload.error.message, 1024); + if (x.payload.result !== null) throw new Error('Invalid error result'); + } else { + if (!object(x.payload.result)) throw new Error('Invalid success result'); + if (x.type === 'model_response') modelResponse(x.payload.result); + } + if (jsonBytes(x.payload, LIMITS.MODEL_RESULT_PAYLOAD_MAX_BYTES) > LIMITS.MODEL_RESULT_PAYLOAD_MAX_BYTES) throw new Error('Payload too large'); + } else throw new Error('Unknown frame'); + } +} +export function modelResponse(x: any): void { + keys(x, ['content', 'stop_reason', 'usage'], ['provider_state']); + if (!Array.isArray(x.content) || !['stop', 'tool_calls', 'length'].includes(x.stop_reason)) throw new Error('Invalid model result'); + const ids = new Set(); + for (const c of x.content) { + if (c.type === 'text') { keys(c, ['type', 'text']); text(c.text, LIMITS.FINAL_MARKDOWN_MAX_BYTES); } + else if (c.type === 'tool_call') { + keys(c, ['type', 'id', 'name', 'arguments']); + if (!isValidId(c.id) || ids.has(c.id) || !object(c.arguments)) throw new Error('Invalid model call'); + ids.add(c.id); text(c.name, 128, true); + if (jsonBytes(c.arguments, LIMITS.TOOL_REQUEST_PAYLOAD_MAX_BYTES) > LIMITS.TOOL_REQUEST_PAYLOAD_MAX_BYTES) throw new Error('Tool arguments too large'); + } else throw new Error('Invalid content'); + } + if ((ids.size > 0) !== (x.stop_reason === 'tool_calls') && x.stop_reason !== 'length') throw new Error('Invalid stop reason'); + keys(x.usage, ['input_tokens', 'output_tokens']); integer(x.usage.input_tokens); integer(x.usage.output_tokens); + if (x.provider_state !== undefined) text(x.provider_state, 64 * 1024); +} + +export function confluenceResult(x: any, tool: string): void { + const page = (p: any, extra: string[]) => { + keys(p, ['page_id', 'title', 'space', 'url', ...extra]); + text(p.page_id, 128, true); + if (!/^[0-9]+$/.test(p.page_id)) throw new Error('Invalid page ID'); + for (const field of ['title', 'space', 'url']) text(p[field], LIMITS.TOOL_RESULT_PAYLOAD_MAX_BYTES); + }; + const pagination = (p: any, count: number) => { + keys(p, ['offset', 'limit', 'has_more']); + integer(p.offset, 0, 10000); integer(p.limit, 1, 50); + if (typeof p.has_more !== 'boolean' || count > p.limit || (!count && p.has_more)) throw new Error('Invalid pagination'); + }; + if (tool === 'confluence_view') { + page(x, ['markdown', 'truncated']); text(x.markdown, LIMITS.TOOL_RESULT_PAYLOAD_MAX_BYTES); + if (typeof x.truncated !== 'boolean') throw new Error('Invalid truncation'); + } else if (tool === 'confluence_search') { + keys(x, ['pages', 'pagination']); + if (!Array.isArray(x.pages) || x.pages.length > 50) throw new Error('Invalid pages'); + for (const p of x.pages) { page(p, ['snippet']); text(p.snippet, LIMITS.TOOL_RESULT_PAYLOAD_MAX_BYTES); } + pagination(x.pagination, x.pages.length); + } else if (tool === 'confluence_list_spaces') { + keys(x, ['spaces', 'pagination']); + if (!Array.isArray(x.spaces) || x.spaces.length > 50) throw new Error('Invalid spaces'); + for (const s of x.spaces) { keys(s, ['key', 'name']); text(s.key, LIMITS.TOOL_RESULT_PAYLOAD_MAX_BYTES); text(s.name, LIMITS.TOOL_RESULT_PAYLOAD_MAX_BYTES); } + pagination(x.pagination, x.spaces.length); + } else throw new Error('Invalid Confluence operation'); +} diff --git a/agent/warnings.ts b/agent/warnings.ts new file mode 100644 index 0000000..b4b9ce3 --- /dev/null +++ b/agent/warnings.ts @@ -0,0 +1,40 @@ +import { LIMITS, type Warning } from './types.js'; + +function boundedMessage(message: string, suffix: string): string { + const bytes = Buffer.from(message); + let end = Math.min(bytes.length, LIMITS.MAX_ERROR_MESSAGE_BYTES - Buffer.byteLength(suffix)); + while (end < bytes.length && (bytes[end] & 0xc0) === 0x80) end--; + return bytes.subarray(0, end).toString('utf8') + suffix; +} + +/** Keep repeated warnings and overflow visible without extending the wire shape. */ +export class WarningCollector { + private entries: Warning[] = []; + private groups = new Map(); + private overflow = 0; + add(warning: Warning): void { + const key = JSON.stringify([warning.code, warning.message, warning.tool_call_id, warning.name]); + const existing = this.groups.get(key); + if (existing) { + existing.count++; + existing.warning.message = boundedMessage(existing.message, ` (Repeated ${existing.count} times.)`); + return; + } + if (!this.overflow && this.entries.length < LIMITS.MAX_WARNINGS) { + const copy = { ...warning }; + this.entries.push(copy); + this.groups.set(key, { warning: copy, message: copy.message, count: 1 }); + return; + } + if (!this.overflow) { + const replaced = this.entries.pop()!; + const removed = [...this.groups.entries()].find(([, group]) => group.warning === replaced)!; + this.overflow = removed[1].count; + this.groups.delete(removed[0]); + this.entries.push({ code: 'warnings_aggregated', message: '' }); + } + this.overflow++; + this.entries.at(-1)!.message = `${this.overflow} additional warnings were aggregated after the warning limit.`; + } + snapshot(): Warning[] { return this.entries.map(w => ({ ...w })); } +}