@arnilo/prism-coding-agent 0.0.8 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/checks.js ADDED
@@ -0,0 +1,249 @@
1
+ import { spawn } from "node:child_process";
2
+ import { rm } from "node:fs/promises";
3
+ import { isAbsolute } from "node:path";
4
+ import { enforceExecutionPolicy } from "./execution-policy.js";
5
+ import { DEFAULT_CHECK_TIMEOUT_MS, DEFAULT_MAX_CHECK_CONCURRENCY, DEFAULT_MAX_CHECK_DIAGNOSTIC_LINES, DEFAULT_MAX_CHECK_NAMES, DEFAULT_MAX_CHECK_OUTPUT_BYTES, HARD_CHECK_TIMEOUT_MS, HARD_MAX_BYTES, HARD_MAX_CHECK_CONCURRENCY, HARD_MAX_CHECK_DIAGNOSTIC_LINES, HARD_MAX_CHECK_NAMES, HARD_MAX_CHECK_OUTPUT_BYTES, validateCodingLimit, } from "./limits.js";
6
+ import { OutputAccumulator } from "./output-accumulator.js";
7
+ class Semaphore {
8
+ max;
9
+ active = 0;
10
+ waiters = [];
11
+ constructor(max) {
12
+ this.max = max;
13
+ }
14
+ async acquire() {
15
+ if (this.active < this.max) {
16
+ this.active++;
17
+ return;
18
+ }
19
+ await new Promise((resolve) => this.waiters.push(resolve));
20
+ this.active++;
21
+ }
22
+ release() {
23
+ this.active--;
24
+ const next = this.waiters.shift();
25
+ if (next)
26
+ next();
27
+ }
28
+ }
29
+ function errorResult(toolCallId, message) {
30
+ return {
31
+ toolCallId,
32
+ name: "coding_check",
33
+ content: [{ type: "text", text: message }],
34
+ error: { message },
35
+ };
36
+ }
37
+ function validateCheckMap(checks) {
38
+ const names = Object.keys(checks);
39
+ if (names.length < 1)
40
+ throw new Error("checks must declare at least one named command");
41
+ validateCodingLimit("checkNames", names.length, HARD_MAX_CHECK_NAMES);
42
+ const map = new Map();
43
+ for (const name of names) {
44
+ if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name)) {
45
+ throw new Error(`invalid check name: ${name}`);
46
+ }
47
+ const def = checks[name];
48
+ if (!def || typeof def.file !== "string" || def.file.length === 0) {
49
+ throw new Error(`check ${name} requires a non-empty file`);
50
+ }
51
+ if (!Array.isArray(def.args) || def.args.some((a) => typeof a !== "string" || a.includes("\0"))) {
52
+ throw new Error(`check ${name} args must be strings without NUL`);
53
+ }
54
+ map.set(name, def);
55
+ }
56
+ void DEFAULT_MAX_CHECK_NAMES;
57
+ return map;
58
+ }
59
+ async function runNamedCheck(def, cwd, options) {
60
+ if (options.signal?.aborted) {
61
+ return { exitCode: null, output: "", timedOut: false, aborted: true };
62
+ }
63
+ const displayBytes = Math.min(50 * 1024, HARD_MAX_BYTES, options.maxBytes);
64
+ const accumulator = new OutputAccumulator({
65
+ maxLines: options.maxLines,
66
+ maxBytes: displayBytes,
67
+ maxTotalOutputBytes: options.maxBytes,
68
+ tempFilePrefix: "prism-check",
69
+ });
70
+ return await new Promise((resolve, reject) => {
71
+ let settled = false;
72
+ let timedOut = false;
73
+ let aborted = false;
74
+ let child;
75
+ let timer;
76
+ const finish = (exitCode) => {
77
+ if (settled)
78
+ return;
79
+ settled = true;
80
+ if (timer)
81
+ clearTimeout(timer);
82
+ options.signal?.removeEventListener("abort", onAbort);
83
+ accumulator.finish();
84
+ const snap = accumulator.snapshot({ persistIfTruncated: false });
85
+ if (snap.fullOutputPath) {
86
+ void rm(snap.fullOutputPath, { force: true }).catch(() => undefined);
87
+ }
88
+ resolve({
89
+ exitCode,
90
+ output: snap.content,
91
+ timedOut,
92
+ aborted,
93
+ });
94
+ };
95
+ const onAbort = () => {
96
+ aborted = true;
97
+ try {
98
+ if (child.pid)
99
+ process.kill(-child.pid, "SIGKILL");
100
+ }
101
+ catch {
102
+ try {
103
+ child.kill("SIGKILL");
104
+ }
105
+ catch {
106
+ /* ignore */
107
+ }
108
+ }
109
+ };
110
+ try {
111
+ const env = { PATH: "/usr/bin:/bin", LANG: "C", ...(def.env ?? {}) };
112
+ child = spawn(def.file, [...def.args], {
113
+ cwd: def.cwd ?? cwd,
114
+ env,
115
+ stdio: ["ignore", "pipe", "pipe"],
116
+ detached: process.platform !== "win32",
117
+ windowsHide: true,
118
+ shell: false,
119
+ });
120
+ }
121
+ catch (error) {
122
+ reject(error instanceof Error ? error : new Error(String(error)));
123
+ return;
124
+ }
125
+ child.stdout.on("data", (chunk) => accumulator.append(chunk));
126
+ child.stderr.on("data", (chunk) => accumulator.append(chunk));
127
+ child.on("error", (error) => {
128
+ if (!settled) {
129
+ settled = true;
130
+ reject(error);
131
+ }
132
+ });
133
+ child.on("close", (code) => finish(code));
134
+ timer = setTimeout(() => {
135
+ timedOut = true;
136
+ try {
137
+ if (child.pid)
138
+ process.kill(-child.pid, "SIGKILL");
139
+ }
140
+ catch {
141
+ child.kill("SIGKILL");
142
+ }
143
+ }, options.timeoutMs);
144
+ options.signal?.addEventListener("abort", onAbort, { once: true });
145
+ });
146
+ }
147
+ /**
148
+ * Create the `coding_check` tool. Model may only select a declared name — never
149
+ * executable path or arguments.
150
+ */
151
+ export function createCodingCheckTool(cwd, options) {
152
+ const checks = validateCheckMap(options.checks);
153
+ const maxConcurrency = validateCodingLimit("maxConcurrency", options.maxConcurrency ?? DEFAULT_MAX_CHECK_CONCURRENCY, HARD_MAX_CHECK_CONCURRENCY);
154
+ const maxDiagnosticLines = validateCodingLimit("maxDiagnosticLines", options.maxDiagnosticLines ?? DEFAULT_MAX_CHECK_DIAGNOSTIC_LINES, HARD_MAX_CHECK_DIAGNOSTIC_LINES);
155
+ const maxOutputBytes = validateCodingLimit("maxOutputBytes", options.maxOutputBytes ?? DEFAULT_MAX_CHECK_OUTPUT_BYTES, HARD_MAX_CHECK_OUTPUT_BYTES);
156
+ const defaultTimeoutMs = validateCodingLimit("defaultTimeoutMs", options.defaultTimeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS, HARD_CHECK_TIMEOUT_MS);
157
+ const semaphore = new Semaphore(maxConcurrency);
158
+ const names = [...checks.keys()].sort();
159
+ return {
160
+ name: "coding_check",
161
+ description: `Run a host-declared named check. Allowed names: ${names.join(", ")}. The model cannot choose executables or arguments.`,
162
+ exclusive: true,
163
+ parameters: {
164
+ type: "object",
165
+ properties: {
166
+ name: {
167
+ type: "string",
168
+ description: "Declared check name",
169
+ enum: names,
170
+ },
171
+ },
172
+ required: ["name"],
173
+ additionalProperties: false,
174
+ },
175
+ async execute(args, context) {
176
+ const toolCallId = context.toolCallId;
177
+ if (context.signal?.aborted)
178
+ return errorResult(toolCallId, "Operation aborted");
179
+ const name = typeof args.name === "string" ? args.name : "";
180
+ const def = checks.get(name);
181
+ if (!def)
182
+ return errorResult(toolCallId, `unknown check name: ${name}`);
183
+ if (!isAbsolute(def.file) && !(def.env && typeof def.env.PATH === "string")) {
184
+ return errorResult(toolCallId, `check ${name} file must be absolute unless env.PATH is explicitly provided`);
185
+ }
186
+ const timeoutMs = validateCodingLimit("timeoutMs", def.timeoutMs ?? defaultTimeoutMs, HARD_CHECK_TIMEOUT_MS);
187
+ const policyCheck = await enforceExecutionPolicy(options.executionPolicy, {
188
+ kind: "check",
189
+ operation: name,
190
+ paths: [def.cwd ?? cwd],
191
+ risk: "medium",
192
+ metadata: {
193
+ checkName: name,
194
+ file: def.file,
195
+ args: def.args,
196
+ sessionId: context.sessionId,
197
+ runId: context.runId,
198
+ signal: context.signal,
199
+ },
200
+ }, toolCallId, "coding_check");
201
+ if (!policyCheck.allowed)
202
+ return policyCheck.result;
203
+ await semaphore.acquire();
204
+ try {
205
+ const result = await runNamedCheck(def, cwd, {
206
+ signal: context.signal,
207
+ timeoutMs,
208
+ maxLines: maxDiagnosticLines,
209
+ maxBytes: maxOutputBytes,
210
+ });
211
+ if (result.aborted)
212
+ return errorResult(toolCallId, "Operation aborted");
213
+ if (result.timedOut) {
214
+ return {
215
+ toolCallId,
216
+ name: "coding_check",
217
+ content: [{ type: "text", text: `${result.output}\n[check timed out]`.trim() }],
218
+ error: { message: `check ${name} timed out` },
219
+ metadata: { name, exitCode: result.exitCode, timedOut: true },
220
+ };
221
+ }
222
+ const footer = result.exitCode === 0 ? "" : `\n[check exited with code ${result.exitCode}]`;
223
+ return {
224
+ toolCallId,
225
+ name: "coding_check",
226
+ content: [
227
+ {
228
+ type: "text",
229
+ text: `${result.output}${footer}`.trim() || `(check ${name} produced no output)`,
230
+ },
231
+ ],
232
+ metadata: {
233
+ name,
234
+ exitCode: result.exitCode,
235
+ summary: result.exitCode === 0 ? "passed" : `failed (${result.exitCode})`,
236
+ },
237
+ };
238
+ }
239
+ catch (error) {
240
+ const message = error instanceof Error ? error.message : String(error);
241
+ return errorResult(toolCallId, message);
242
+ }
243
+ finally {
244
+ semaphore.release();
245
+ }
246
+ },
247
+ };
248
+ }
249
+ //# sourceMappingURL=checks.js.map
@@ -0,0 +1,159 @@
1
+ import type { JsonObject } from "@arnilo/prism";
2
+ export declare const CODING_CHECKPOINT_SCHEMA_VERSION: 1;
3
+ /** Workflow shared-state key that holds coding checkpoint metadata. */
4
+ export declare const CODING_STATE_KEY = "coding";
5
+ export type CodingArtifactKind = "plan" | "workspace" | "patch" | "bundle" | "diff" | "other";
6
+ export type CodingTaskStatus = "planned" | "editing" | "checking" | "awaiting_approval" | "ready_for_handoff" | "completed" | "failed" | "cancelled";
7
+ export interface CodingArtifactRef {
8
+ readonly kind: CodingArtifactKind;
9
+ readonly uri: string;
10
+ readonly sha256: string;
11
+ readonly bytes: number;
12
+ }
13
+ export interface CodingTodoItem {
14
+ readonly id: string;
15
+ readonly text: string;
16
+ readonly done: boolean;
17
+ }
18
+ export interface CodingCheckSummary {
19
+ readonly name: string;
20
+ readonly exitCode: number;
21
+ readonly summary: string;
22
+ }
23
+ export interface CodingFingerprints {
24
+ /** Workflow definition revision string. */
25
+ readonly workflowRevision: string;
26
+ /** Optional definition hash captured after `defineWorkflow`. */
27
+ readonly definitionHash?: string;
28
+ /** Host-pinned sandbox/image digest when a disposable sandbox is used. */
29
+ readonly imageDigest?: string;
30
+ /** Host-computed hash of the selected tool surface. */
31
+ readonly toolFingerprint: string;
32
+ /** Host-computed hash of the selected execution/approval policy. */
33
+ readonly policyFingerprint: string;
34
+ }
35
+ export interface CodingHandoffSummary {
36
+ readonly base: string;
37
+ readonly head: string;
38
+ readonly changedPathCount: number;
39
+ readonly checkCount: number;
40
+ readonly artifact?: CodingArtifactRef;
41
+ }
42
+ export interface CodingCheckpointMetadata {
43
+ readonly schemaVersion: typeof CODING_CHECKPOINT_SCHEMA_VERSION;
44
+ readonly taskId: string;
45
+ readonly workspaceRoot: string;
46
+ readonly baseBranch: string;
47
+ readonly branch: string;
48
+ readonly worktreePath?: string;
49
+ /** Workspace-relative plan path (for example `plans/task-1.md`). */
50
+ readonly planPath: string;
51
+ readonly plan: CodingArtifactRef;
52
+ readonly workspaceExport?: CodingArtifactRef;
53
+ readonly artifacts: readonly CodingArtifactRef[];
54
+ readonly checks: readonly CodingCheckSummary[];
55
+ readonly handoff?: CodingHandoffSummary;
56
+ readonly status: CodingTaskStatus;
57
+ readonly fingerprints: CodingFingerprints;
58
+ readonly todos: readonly CodingTodoItem[];
59
+ readonly updatedAt: string;
60
+ }
61
+ export interface CodingCheckpointLimitOptions {
62
+ readonly maxPlanBytes?: number;
63
+ readonly maxTodos?: number;
64
+ readonly maxTodoTextBytes?: number;
65
+ readonly maxArtifacts?: number;
66
+ readonly maxArtifactBytes?: number;
67
+ readonly maxCheckSummaryBytes?: number;
68
+ readonly maxCheckpointBytes?: number;
69
+ }
70
+ export interface ResolvedCodingCheckpointLimits {
71
+ readonly maxPlanBytes: number;
72
+ readonly maxTodos: number;
73
+ readonly maxTodoTextBytes: number;
74
+ readonly maxArtifacts: number;
75
+ readonly maxArtifactBytes: number;
76
+ readonly maxCheckSummaryBytes: number;
77
+ readonly maxCheckpointBytes: number;
78
+ }
79
+ export declare class CodingCheckpointError extends Error {
80
+ readonly code = "ERR_PRISM_CODING_CHECKPOINT";
81
+ constructor(message: string);
82
+ }
83
+ export declare function resolveCodingCheckpointLimits(options?: CodingCheckpointLimitOptions): ResolvedCodingCheckpointLimits;
84
+ /** Deterministic SHA-256 fingerprint over a JSON-stable encoding. */
85
+ export declare function fingerprintJson(value: unknown): string;
86
+ export declare function createCodingArtifactRef(input: {
87
+ readonly kind: CodingArtifactKind;
88
+ readonly uri: string;
89
+ readonly bytes: Buffer;
90
+ readonly maxBytes?: number;
91
+ }): CodingArtifactRef;
92
+ export declare function verifyCodingArtifactBytes(ref: CodingArtifactRef, bytes: Buffer, limits?: CodingCheckpointLimitOptions): void;
93
+ export declare function createCodingPlanMarkdown(input: {
94
+ readonly title: string;
95
+ readonly taskId: string;
96
+ readonly status?: CodingTaskStatus;
97
+ readonly todos: readonly {
98
+ readonly id?: string;
99
+ readonly text: string;
100
+ readonly done?: boolean;
101
+ }[];
102
+ readonly notes?: string;
103
+ readonly limits?: CodingCheckpointLimitOptions;
104
+ }): string;
105
+ export declare function parseCodingPlanTodos(markdown: string, limits?: CodingCheckpointLimitOptions): CodingTodoItem[];
106
+ export declare function writeCodingPlanFile(input: {
107
+ readonly workspaceRoot: string;
108
+ readonly planPath: string;
109
+ readonly markdown: string;
110
+ readonly limits?: CodingCheckpointLimitOptions;
111
+ }): Promise<CodingArtifactRef>;
112
+ export declare function readCodingPlanFile(input: {
113
+ readonly workspaceRoot: string;
114
+ readonly planPath: string;
115
+ readonly expected?: CodingArtifactRef;
116
+ readonly limits?: CodingCheckpointLimitOptions;
117
+ }): Promise<{
118
+ markdown: string;
119
+ artifact: CodingArtifactRef;
120
+ todos: CodingTodoItem[];
121
+ }>;
122
+ export declare function buildCodingCheckpointMetadata(input: {
123
+ readonly taskId: string;
124
+ readonly workspaceRoot: string;
125
+ readonly baseBranch: string;
126
+ readonly branch: string;
127
+ readonly planPath: string;
128
+ readonly plan: CodingArtifactRef;
129
+ readonly fingerprints: CodingFingerprints;
130
+ readonly status?: CodingTaskStatus;
131
+ readonly todos?: readonly CodingTodoItem[];
132
+ readonly worktreePath?: string;
133
+ readonly workspaceExport?: CodingArtifactRef;
134
+ readonly artifacts?: readonly CodingArtifactRef[];
135
+ readonly checks?: readonly CodingCheckSummary[];
136
+ readonly handoff?: CodingHandoffSummary;
137
+ readonly updatedAt?: string;
138
+ readonly limits?: CodingCheckpointLimitOptions;
139
+ }): CodingCheckpointMetadata;
140
+ export declare function validateCodingCheckpointMetadata(value: unknown, limits?: CodingCheckpointLimitOptions): CodingCheckpointMetadata;
141
+ /**
142
+ * Fail closed before resume/import when ownership-equivalent fingerprints diverge
143
+ * or artifact references cannot be verified.
144
+ */
145
+ export declare function assertCodingResumeAllowed(input: {
146
+ readonly metadata: CodingCheckpointMetadata;
147
+ readonly expected: CodingFingerprints;
148
+ readonly expectedWorkspaceRoot?: string;
149
+ readonly expectedBaseBranch?: string;
150
+ readonly planBytes?: Buffer;
151
+ readonly workspaceExportBytes?: Buffer;
152
+ readonly limits?: CodingCheckpointLimitOptions;
153
+ }): CodingCheckpointMetadata;
154
+ /** Extract and validate `state.coding` when present. */
155
+ export declare function readCodingCheckpointFromState(state: Readonly<Record<string, unknown>>, limits?: CodingCheckpointLimitOptions): CodingCheckpointMetadata | undefined;
156
+ export declare function codingCheckpointStatePatch(metadata: CodingCheckpointMetadata): JsonObject;
157
+ /** Exported for tests that need a quick digest helper without importing crypto. */
158
+ export declare function codingSha256Hex(data: Buffer | string): string;
159
+ export declare function codingPlanPathForTask(taskId: string): string;