@zof-ai/agent-evals 0.0.0-stage → 0.1.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (unreleased)
4
+
5
+ Initial independently authored local implementation. Candidate package/repository names and Apache-2.0 licensing require owner and legal approval before publication.
@@ -0,0 +1,5 @@
1
+ # Contributing
2
+
3
+ Use Node.js 22 or 24, run `npm ci --ignore-scripts`, `npm run build`, and `npm test`. Add a regression test for functional or security changes. Keep changes independently authored and free of proprietary code, credentials and customer data. Discuss incompatible API changes before implementation. Contributions use the repository license and require maintainer review; dependency updates are never automatically merged.
4
+
5
+ Maintainers review issue reproductions, security boundaries, tests, compatibility and package contents. Use semantic versioning; announce breaking interfaces in the changelog. Deprecations require a documented replacement and at least one minor release before removal.
package/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/README.md CHANGED
@@ -1,3 +1,47 @@
1
- # Temporary Holding Version
1
+ # Zof Agent Evaluations
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Evaluate a JavaScript candidate snapshot against explicit input/output tests in a constrained **local Docker container**. Candidate output is compared in the parent process; reports include test outcomes and fixed reason codes, never raw output or logs. Three public task definitions come from `@zof-ai/reliability-bench` and results use Evidence 1.0.0.
4
+
5
+ Version 0.1.0 is prepared under the Apache-2.0 license. Use the GitHub source installation below; npm registry publication is not claimed.
6
+
7
+ Requires Node.js 22 or newer, a local Docker engine and the pinned Node image documented in [sandbox controls](docs/SANDBOX.md). The evaluator never pulls images, installs candidate dependencies, or uses a remote Docker endpoint.
8
+
9
+ <!-- ZOF_RELEASE_INSTALL_START -->
10
+ ## Install from GitHub source
11
+
12
+ Requires Node.js 22 or 24, npm, and Git. Filesystem operations require Linux or macOS with Python 3.10 or newer; Windows file operations fail closed. Clone this repository and explicitly prepare its reviewed release dependencies:
13
+
14
+ ```sh
15
+ git clone https://github.com/Zof-AI/zof-agent-evals.git
16
+ cd zof-agent-evals
17
+ node scripts/bootstrap.mjs --allow-network
18
+ npm ci --ignore-scripts --prefer-offline
19
+ npm run build
20
+ npm test
21
+ ```
22
+
23
+ The bootstrap downloads only the exact `v0.1.0` dependency assets from [GitHub Releases](https://github.com/Zof-AI/zof-agent-evals/releases), as listed in `scripts/release-dependencies.json`, verifies SHA256 and byte size, and adds them to the npm cache. It does not extract archives, run package scripts, collect application data, or send credentials. Downloads require the explicit flag and follow only HTTPS GitHub asset redirects. Installation then fetches ordinary third-party dependencies from npm. Use a writable npm cache; set `npm_config_cache` consistently for both commands if needed. An unavailable asset, changed hash, or oversized response stops installation.
24
+
25
+ GitHub source and release artifacts are the initial distribution channel. These instructions do not claim that `@zof-ai` packages are published on npm or that the VS Code extension is listed in Marketplace.
26
+ <!-- ZOF_RELEASE_INSTALL_END -->
27
+
28
+ ## Three examples
29
+
30
+ ```sh
31
+ npm run build
32
+ node dist/cli.js evaluate correct.mjs --root examples --task deduplicate-payments
33
+ node dist/cli.js evaluate incorrect.mjs --root examples --task deduplicate-payments
34
+ node dist/cli.js evaluate profile.mjs --root examples --task compatible-profile
35
+ npm test
36
+ npm run test:container
37
+ ```
38
+
39
+ Correct and profile exit 0; incorrect exits 1. Missing Docker, rejected input, timeouts or incomplete execution exit 2. Known candidate failures remain failed evidence. The explicit evaluate command authorizes running only the selected bounded `.mjs` file against the selected data-only task. It never executes the source on the host.
40
+
41
+ Candidates export `solve(input)` returning a JSON-compatible value, synchronously or asynchronously. Each case runs in a fresh container. Only one candidate file, one case input, and a fixed harness are copied into a fresh protected staging directory; the original directory is not mounted. Expected values remain outside the container.
42
+
43
+ `node dist/cli.js tasks` lists the actual public tasks. `--task-file task.json` loads an explicitly selected custom data-only task under the same root. `--timeout 500..10000` controls total per-case time including container startup. `--agent name` records a bounded, redacted user-provided label. No environment variables are copied into evidence.
44
+
45
+ Library: `evaluateCandidate({root, candidate, task: idOrTask, timeoutMs?, agent?: {name,version?}})` returns `{taskId,outcomes,evidence}`. `validateTask(value)` validates and copies the data-only task. `SANDBOX_IMAGE` exports the pinned image identifier. `sandboxArguments` exposes the constrained argument builder for auditing, without executing a command.
46
+
47
+ Read [task format](docs/TASKS.md), [sandbox and limitations](docs/SANDBOX.md), and [security reporting](SECURITY.md).
package/SECURITY.md ADDED
@@ -0,0 +1,9 @@
1
+ # Security policy
2
+
3
+ The 0.1.x line is the initial supported release line. Local analyses are bounded and offline unless an operation is explicitly documented otherwise. Do not provide production secrets or execute untrusted generated code.
4
+
5
+ Use GitHub private vulnerability reporting when enabled for this repository. If it is unavailable, contact an organization owner through an already verified private channel; do not file credentials, exploit details, or customer data in a public issue. Reports will be triaged, reproduced using synthetic data, fixed and reviewed before coordinated disclosure. No response SLA is promised for this initial release.
6
+
7
+ Untrusted candidates are never imported or executed on the host. They are copied as a bounded module into a protected temporary staging parent and run in a non-root, network-disabled, read-only Docker container with CPU/memory/PID/time/output limits. Raw logs are discarded. No host credential or source-directory mount is allowed. See docs/SANDBOX.md for cleanup verification, local-only engine enforcement, and kernel/runtime limitations. The tool is not a public multi-tenant execution service. Missing isolation produces missing evidence rather than a passing result.
8
+
9
+ Filesystem operations use bounded, directory-relative descriptors on Linux/macOS with Python 3.10 or newer. Symlinks, hard-linked inputs, special files and traversal are rejected. Open descriptors remain attached to the original directory when names change. File contents can still change during a read or snapshot; this is not an atomic filesystem snapshot. Windows filesystem operations fail closed; in-memory JSON validation and analysis do not require the file helper.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import { safeReadJson, serializeEvidence } from '@zof-ai/evidence-spec';
4
+ import { listEvaluationTasks } from '@zof-ai/reliability-bench';
5
+ import { evaluateCandidate, validateTask } from './index.js';
6
+ try {
7
+ const { values, positionals } = parseArgs({ options: { root: { type: 'string', default: '.' }, task: { type: 'string', default: 'deduplicate-payments' }, 'task-file': { type: 'string' }, timeout: { type: 'string', default: '2000' }, agent: { type: 'string' }, help: { type: 'boolean' }, version: { type: 'boolean' } }, allowPositionals: true });
8
+ if (values.help)
9
+ console.log('Usage: zof-agent-evals evaluate <candidate.mjs> --root directory [--task id | --task-file relative.json] [--timeout 500..10000] [--agent name]\nCandidates run only in constrained local Docker; no automatic image pull. Exit0 pass,1 candidate failure,2 incomplete/missing/input.\nUse zof-agent-evals tasks to list public data-only tasks.');
10
+ else if (values.version)
11
+ console.log('0.1.0');
12
+ else if (positionals.length === 1 && positionals[0] === 'tasks')
13
+ console.log(JSON.stringify(listEvaluationTasks(), null, 2));
14
+ else if (positionals.length === 2 && positionals[0] === 'evaluate') {
15
+ const task = values['task-file'] ? validateTask(await safeReadJson(values.root, values['task-file'])) : values.task;
16
+ const result = await evaluateCandidate({ root: values.root, candidate: positionals[1], task, timeoutMs: Number(values.timeout), ...(values.agent ? { agent: { name: values.agent } } : {}) });
17
+ process.stdout.write(serializeEvidence(result.evidence));
18
+ process.exitCode = result.evidence.execution.status === 'completed' ? (result.evidence.execution.result === 'pass' ? 0 : 1) : 2;
19
+ }
20
+ else
21
+ throw new Error('usage');
22
+ }
23
+ catch {
24
+ console.error(JSON.stringify({ error: 'Invalid candidate, task or evaluation options.' }));
25
+ process.exitCode = 2;
26
+ }
@@ -0,0 +1,26 @@
1
+ import type { Evidence } from '@zof-ai/evidence-spec';
2
+ import type { EvaluationTask } from '@zof-ai/reliability-bench';
3
+ export declare const SANDBOX_IMAGE = "node@sha256:43ac6c60b8f89723f746e8a92ce91abd5017e627ce1ddfe4238355d3a30b772c";
4
+ export interface EvaluationOptions {
5
+ root: string;
6
+ candidate: string;
7
+ task: string | EvaluationTask;
8
+ timeoutMs?: number;
9
+ agent?: {
10
+ name: string;
11
+ version?: string;
12
+ };
13
+ }
14
+ export interface EvaluationResult {
15
+ taskId: string;
16
+ outcomes: Array<{
17
+ name: string;
18
+ status: 'passed' | 'failed' | 'error';
19
+ reason: 'MATCH' | 'MISMATCH' | 'CANDIDATE_ERROR' | 'TIMEOUT' | 'OUTPUT_LIMIT' | 'SANDBOX_UNAVAILABLE' | 'CLEANUP_UNCONFIRMED';
20
+ }>;
21
+ evidence: Evidence;
22
+ }
23
+ /** Bounded public data-only task format. No shell commands, harness code, imports or endpoint fields. */
24
+ export declare function validateTask(value: unknown): EvaluationTask;
25
+ export declare function sandboxArguments(directory: string, name: string): string[];
26
+ export declare function evaluateCandidate(options: EvaluationOptions): Promise<EvaluationResult>;
package/dist/index.js ADDED
@@ -0,0 +1,178 @@
1
+ import { execFile, spawn } from 'node:child_process';
2
+ import { promisify, isDeepStrictEqual } from 'node:util';
3
+ import { mkdtemp, writeFile, rm, mkdir } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { randomUUID } from 'node:crypto';
7
+ import { createEvidence, safeReadFile, assertBounded, parseJson, redactText } from '@zof-ai/evidence-spec';
8
+ import { getEvaluationTask } from '@zof-ai/reliability-bench';
9
+ export const SANDBOX_IMAGE = 'node@sha256:43ac6c60b8f89723f746e8a92ce91abd5017e627ce1ddfe4238355d3a30b772c';
10
+ const exec = promisify(execFile);
11
+ const MAX_SOURCE_BYTES = 128 * 1024;
12
+ const MAX_OUTPUT_BYTES = 64 * 1024;
13
+ /** Bounded public data-only task format. No shell commands, harness code, imports or endpoint fields. */
14
+ export function validateTask(value) {
15
+ assertBounded(value);
16
+ if (!value || typeof value !== 'object' || Array.isArray(value))
17
+ throw new Error('Invalid evaluation task.');
18
+ const t = value;
19
+ if (t.version !== '1.0.0' || typeof t.id !== 'string' || !(/^[a-z0-9][a-z0-9-]{0,99}$/).test(t.id) || typeof t.description !== 'string' || t.description.length > 1000 || !Array.isArray(t.cases) || !t.cases.length || t.cases.length > 32 || Object.keys(t).some(k => !['version', 'id', 'description', 'cases'].includes(k)))
20
+ throw new Error('Invalid evaluation task.');
21
+ for (const item of t.cases) {
22
+ if (!item || typeof item !== 'object' || Array.isArray(item))
23
+ throw new Error('Invalid test case.');
24
+ const c = item;
25
+ if (typeof c.name !== 'string' || !c.name || c.name.length > 200 || !Object.hasOwn(c, 'input') || !Object.hasOwn(c, 'expected') || Object.keys(c).some(k => !['name', 'input', 'expected'].includes(k)))
26
+ throw new Error('Invalid test case.');
27
+ if (Buffer.byteLength(JSON.stringify(c)) > 32 * 1024)
28
+ throw new Error('Test case exceeds limit.');
29
+ }
30
+ return structuredClone(value);
31
+ }
32
+ export function sandboxArguments(directory, name) {
33
+ if (!path.isAbsolute(directory) || /[\x00-\x1f,]/.test(directory) || !/^zof-eval-[a-z0-9-]+$/.test(name))
34
+ throw new Error('Invalid sandbox path or name.');
35
+ return ['run', '--rm', '--pull=never', '--name', name, '--network=none', '--read-only', '--cap-drop=ALL', '--security-opt=no-new-privileges', '--pids-limit=32', '--memory=128m', '--memory-swap=128m', '--cpus=0.5', '--user=65534:65534', '--tmpfs=/tmp:rw,noexec,nosuid,size=16m', '--workdir=/task', '--mount', `type=bind,src=${directory},dst=/task,readonly`, SANDBOX_IMAGE, 'node', '--max-old-space-size=64', '/task/runner.mjs'];
36
+ }
37
+ const harness = "import fs from 'node:fs'; const write=process.stdout.write.bind(process.stdout);const stringify=JSON.stringify; const input=JSON.parse(fs.readFileSync('/task/input.json','utf8'));try{const module=await import('/task/candidate.mjs');if(typeof module.solve!=='function')process.exit(41);const output=await module.solve(input);const text=stringify(output);if(typeof text!=='string')process.exit(42);write(text);}catch{process.exit(43);}";
38
+ async function ready() {
39
+ try {
40
+ const context = await exec('docker', ['context', 'inspect', '--format', '{{(index .Endpoints "docker").Host}}'], { timeout: 5000, maxBuffer: 4096 });
41
+ // Only local Unix/named-pipe engines are supported. Never redirect evaluation to a remote endpoint.
42
+ const endpoint = process.env.DOCKER_HOST ?? context.stdout.trim();
43
+ if (!endpoint.startsWith('unix://') && !endpoint.startsWith('npipe://'))
44
+ return;
45
+ await exec('docker', ['--host', endpoint, 'image', 'inspect', SANDBOX_IMAGE, '--format', '{{.Id}}'], { timeout: 5000, maxBuffer: 4096 });
46
+ return endpoint;
47
+ }
48
+ catch {
49
+ return;
50
+ }
51
+ }
52
+ async function runOne(directory, timeoutMs, endpoint) {
53
+ const name = `zof-eval-${randomUUID()}`;
54
+ let result = { reason: 'CANDIDATE_ERROR' };
55
+ try {
56
+ result = await new Promise(resolve => {
57
+ const child = spawn('docker', ['--host', endpoint, ...sandboxArguments(directory, name)], { stdio: ['ignore', 'pipe', 'pipe'] });
58
+ let stdout = Buffer.alloc(0), count = 0, stopped = false, reason;
59
+ const stop = (why) => { if (!stopped) {
60
+ stopped = true;
61
+ reason = why;
62
+ child.kill('SIGKILL');
63
+ } };
64
+ const timer = setTimeout(() => stop('TIMEOUT'), timeoutMs);
65
+ child.stdout.on('data', (chunk) => { count += chunk.length; if (count > MAX_OUTPUT_BYTES)
66
+ stop('OUTPUT_LIMIT');
67
+ else
68
+ stdout = Buffer.concat([stdout, chunk]); });
69
+ // Raw stderr is counted and discarded; it is never recorded or returned.
70
+ child.stderr.on('data', (chunk) => { count += chunk.length; if (count > MAX_OUTPUT_BYTES)
71
+ stop('OUTPUT_LIMIT'); });
72
+ child.on('error', () => { clearTimeout(timer); resolve({ reason: 'SANDBOX_UNAVAILABLE' }); });
73
+ child.on('close', code => { clearTimeout(timer); if (reason) {
74
+ resolve({ reason });
75
+ return;
76
+ } if (code !== 0) {
77
+ resolve({ reason: code === 125 ? 'SANDBOX_UNAVAILABLE' : 'CANDIDATE_ERROR' });
78
+ return;
79
+ } try {
80
+ resolve({ reason: 'MATCH', value: parseJson(stdout.toString('utf8'), MAX_OUTPUT_BYTES) });
81
+ }
82
+ catch {
83
+ resolve({ reason: 'CANDIDATE_ERROR' });
84
+ } });
85
+ });
86
+ }
87
+ finally {
88
+ // Killing a Docker client alone does not stop its container. Always remove the unique job container.
89
+ let confirmed = false;
90
+ for (let attempt = 0; attempt < 5 && !confirmed; attempt++) {
91
+ try {
92
+ await exec('docker', ['--host', endpoint, 'rm', '-f', name], { timeout: 5000, maxBuffer: 4096 });
93
+ confirmed = true;
94
+ }
95
+ catch {
96
+ try {
97
+ const remaining = await exec('docker', ['--host', endpoint, 'ps', '-a', '--filter', `name=^/${name}$`, '--format', '{{.ID}}'], { timeout: 5000, maxBuffer: 4096 });
98
+ confirmed = !remaining.stdout.trim();
99
+ }
100
+ catch { }
101
+ }
102
+ if (!confirmed)
103
+ await new Promise(resolve => setTimeout(resolve, 100));
104
+ }
105
+ if (!confirmed)
106
+ result = { reason: 'CLEANUP_UNCONFIRMED' };
107
+ }
108
+ return result;
109
+ }
110
+ /** Explicit invocation authorizes only this bounded source file and data-only task in local Docker. */
111
+ let activeEvaluations = 0;
112
+ export async function evaluateCandidate(options) {
113
+ if (activeEvaluations >= 2)
114
+ throw new Error('Concurrent evaluation limit reached.');
115
+ activeEvaluations++;
116
+ try {
117
+ return await evaluateInternal(options);
118
+ }
119
+ finally {
120
+ activeEvaluations--;
121
+ }
122
+ }
123
+ async function evaluateInternal(options) {
124
+ if (typeof options.candidate !== 'string' || !options.candidate.endsWith('.mjs'))
125
+ throw new Error('Candidate must be an mjs module.');
126
+ const task = validateTask(typeof options.task === 'string' ? getEvaluationTask(options.task) : options.task);
127
+ const timeout = options.timeoutMs ?? 2000;
128
+ if (!Number.isInteger(timeout) || timeout < 500 || timeout > 10000)
129
+ throw new Error('Timeout must be between 500 and 10000 milliseconds.');
130
+ const source = await safeReadFile(options.root, options.candidate, MAX_SOURCE_BYTES);
131
+ if (options.agent !== undefined) {
132
+ assertBounded(options.agent);
133
+ if (typeof options.agent.name !== 'string' || options.agent.name.length > 100 || (options.agent.version !== undefined && (typeof options.agent.version !== 'string' || options.agent.version.length > 100)))
134
+ throw new Error('Invalid agent metadata.');
135
+ }
136
+ const outcomes = [];
137
+ let status = 'completed';
138
+ const endpoint = await ready();
139
+ if (!endpoint) {
140
+ outcomes.push(...task.cases.map(c => ({ name: redactText(c.name), status: 'error', reason: 'SANDBOX_UNAVAILABLE' })));
141
+ status = 'missing';
142
+ }
143
+ else {
144
+ const stagingRoot = await mkdtemp(path.join(tmpdir(), 'zof-eval-'));
145
+ const directory = path.join(stagingRoot, 'task');
146
+ try {
147
+ await mkdir(directory, { mode: 0o755 });
148
+ await writeFile(path.join(directory, 'candidate.mjs'), source, { mode: 0o444 });
149
+ await writeFile(path.join(directory, 'runner.mjs'), harness, { mode: 0o444 });
150
+ for (const item of task.cases) {
151
+ await rm(path.join(directory, 'input.json'), { force: true });
152
+ await writeFile(path.join(directory, 'input.json'), JSON.stringify(item.input), { mode: 0o444 });
153
+ const result = await runOne(directory, timeout, endpoint);
154
+ const matched = result.reason === 'MATCH' && isDeepStrictEqual(result.value, item.expected);
155
+ const reason = result.reason === 'MATCH' ? (matched ? 'MATCH' : 'MISMATCH') : result.reason;
156
+ outcomes.push({ name: redactText(item.name), status: reason === 'MATCH' ? 'passed' : reason === 'MISMATCH' ? 'failed' : 'error', reason });
157
+ if (reason === 'TIMEOUT' || reason === 'OUTPUT_LIMIT')
158
+ status = 'partial';
159
+ if (reason === 'SANDBOX_UNAVAILABLE' || reason === 'CLEANUP_UNCONFIRMED') {
160
+ status = 'error';
161
+ break;
162
+ }
163
+ }
164
+ }
165
+ finally {
166
+ await rm(stagingRoot, { recursive: true, force: true });
167
+ }
168
+ }
169
+ const failed = outcomes.some(o => o.status !== 'passed');
170
+ const agent = {};
171
+ if (options.agent) {
172
+ agent.name = redactText(options.agent.name);
173
+ if (options.agent.version)
174
+ agent.version = redactText(options.agent.version);
175
+ }
176
+ const evidence = createEvidence({ tool: { name: 'zof-agent-evals', version: '0.1.0' }, type: 'agent-evaluation', execution: { status, result: status === 'missing' || status === 'error' ? 'unknown' : failed ? 'fail' : 'pass' }, tests: outcomes.map(o => ({ name: o.name, status: o.status })), findings: outcomes.filter(o => o.reason !== 'MATCH').map((o, i) => ({ id: `case-${i + 1}`, severity: 'info', message: o.reason })), environment: { os: process.platform, architecture: process.arch, runtime: process.version }, extensions: { 'org.zof.evaluation': { taskId: task.id, taskVersion: task.version, engine: 'docker', image: SANDBOX_IMAGE, timeoutMs: timeout, agent } } });
177
+ return { taskId: task.id, outcomes, evidence };
178
+ }
@@ -0,0 +1,13 @@
1
+ # Release process
2
+
3
+ Version 0.1.0 is prepared under [Apache-2.0](../LICENSE). [GitHub Releases](https://github.com/Zof-AI/zof-agent-evals/releases) is the initial distribution channel. Refer to the release's actual assets and commit SHA; these instructions are not evidence that an upload or installation succeeded.
4
+
5
+ Start with the [source installation](../README.md#install-from-github-source). Run `node scripts/bootstrap.mjs --allow-network`, `npm ci --ignore-scripts --prefer-offline`, `npm run build`, `npm test`, and `npm pack --ignore-scripts`. The bootstrap checks the exact dependency release SHA256 and byte sizes. Do not replace failed checks with an unverified dependency or a `file:` reference in published metadata.
6
+
7
+ Before releasing a new version, inspect source and Git history, dependency licenses, build outputs and every package entry. Resolve security findings; run independent negative and installation tests. Record artifact SHA256 and release commits. Maintain branch protection, private vulnerability reporting, least-privilege workflows and a protected release environment. The manual release-candidate workflow builds artifacts and does not publish automatically. Material release changes require a new review.
8
+
9
+ Publish the reviewed package tarball as a GitHub Release asset. npm registry publication requires independently verified `@zof-ai` scope ownership and an authenticated publisher. Registry publication and registry install acceptance are not claimed by a GitHub release.
10
+
11
+ After publication, retrieve assets anonymously, verify their checksums, install in a clean project, and execute documented examples. Check every intended distribution channel separately. Missing or unrun acceptance is not a pass. Hosted Zof integration remains unavailable until an approved public contract and real service acceptance exist.
12
+
13
+ CI targets Node 22/24 on Linux/macOS. File operations require Python 3.10 or newer and fail closed on Windows; in-memory JSON operations remain portable. Only completed jobs establish platform verification. Docker acceptance runs on Linux; editor acceptance exercises a real VS Code host.
@@ -0,0 +1,17 @@
1
+ # Sandbox controls and privacy
2
+
3
+ Candidate code runs only inside the locally available pinned image:
4
+
5
+ `node@sha256:43ac6c60b8f89723f746e8a92ce91abd5017e627ce1ddfe4238355d3a30b772c`
6
+
7
+ The image is the Node 22 slim reference used for this release candidate. Image availability must be arranged explicitly by an authorized operator. No automatic pull or remote Docker endpoint is accepted.
8
+
9
+ Controls: network none, read-only root filesystem, UID/GID 65534, all capabilities dropped, no-new-privileges, Docker's default seccomp profile, PID limit 32, memory/swap 128 MiB, CPU limit 0.5, Node heap 64 MiB, and a 16 MiB noexec/nosuid temporary filesystem. There are no published ports, privileged mode, host PID namespaces, production directories, credential mounts or Docker socket mounts. The only bind mount is the copied task staging directory, read-only.
10
+
11
+ The host staging parent has mode 0700; its mounted child permits the non-root container to read only the copied candidate, current input and fixed harness. The original source directory and expected values are not mounted. At most two evaluations may be active per process; additional calls are rejected. Each case gets a unique container name. A wall-clock timeout kills the Docker client and triggers forced container removal; bounded follow-up checks confirm absence. Cleanup uncertainty is reported as an error and stops further cases. Staging files are removed in finally blocks.
12
+
13
+ Candidate stdout/stderr share a 64 KiB limit. Stderr is discarded immediately. Stdout is parsed only as a JSON result, compared privately with expected output, and discarded. No raw outputs, logs, source, expected values, arbitrary environment or Docker diagnostics are returned. Only explicit agent metadata, outcomes and safe fixed codes are recorded. Known secret patterns are redacted in supplied labels; do not use production credentials in task data.
14
+
15
+ Docker shares a kernel with its Linux host and is not a complete defense against container-runtime/kernel vulnerabilities. This release demonstrates bounded local isolation for synthetic tasks, not multi-tenant hostile-code hosting. Do not expose it as a public compute service or run it with privileged options. There is no unsafe execution mode. Resource limits apply per process/container; an external service would additionally need server-side global quota/authentication/isolation controls and is not implemented.
16
+
17
+ Input file reads require a stable caller-controlled source root, reject symlinks/traversal and never fetch URLs. Local Docker daemon access is a prerequisite and is trusted. Windows engine behavior and hosted runner matrices have not been accepted in this local release candidate; measured container acceptance uses local Docker Desktop on macOS with Linux containers.
package/docs/TASKS.md ADDED
@@ -0,0 +1,15 @@
1
+ # Task format 1.0.0
2
+
3
+ ```json
4
+ {"version":"1.0.0","id":"double","description":"Double the supplied number","cases":[{"name":"positive","input":2,"expected":4},{"name":"zero","input":0,"expected":0}]}
5
+ ```
6
+
7
+ Tasks contain only id, version, description and 1–32 cases. Each case contains a bounded name, JSON input and JSON expected value. Unknown properties are rejected. Cases cannot contain commands, imports, harness source, environment mappings or endpoint settings. A case is at most 32 KiB, with the shared evidence JSON depth and node bounds.
8
+
9
+ Candidate source is a single `.mjs` file of at most 128 KiB. It may use Node built-ins within the container, but has no installed third-party dependencies and cannot fetch them. Multi-file changes, arbitrary repository build scripts, package installation and non-JavaScript candidates are currently unsupported. Save the resulting self-contained module snapshot for evaluation; this tool does not apply patches to a repository.
10
+
11
+ The parent compares actual JSON output with the expected value using strict deep equality. It does not trust candidate-supplied pass labels. Extra logging causes invalid protocol output and cannot be used to forge a summary report. Public tasks can be overfit and are not hidden security tests. Test passing is a measured observation for these cases, not proof of general correctness.
12
+
13
+ Reasons are MATCH, MISMATCH, CANDIDATE_ERROR, TIMEOUT, OUTPUT_LIMIT, SANDBOX_UNAVAILABLE, or CLEANUP_UNCONFIRMED. No caught candidate exception text or output is included. Missing sandbox produces missing/unknown evidence. Candidate mismatches/errors produce failed tests. Timeout/output limit yields partial/fail; unconfirmed cleanup or infrastructure loss yields error/unknown and stops remaining cases.
14
+
15
+ The optional agent name/version is an explicit assertion supplied by the user. No agent identity is authenticated and no model ranking is claimed. Core evidence source repository/commit are unknown because only a selected file snapshot is loaded. Evaluation metadata records task version, pinned image and time limit. The outer environment field identifies the host runner runtime, not the container's untrusted self-report.
@@ -0,0 +1 @@
1
+ export function solve(payments){const seen=new Set();return payments.reduce((sum,p)=>{if(seen.has(p.id))return sum;seen.add(p.id);return sum+p.amount;},0);}
@@ -0,0 +1 @@
1
+ export function solve(payments){return payments.reduce((sum,p)=>sum+p.amount,0);}
@@ -0,0 +1 @@
1
+ export function solve(profile){return {name:profile.name,displayName:profile.name};}
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "@zof-ai/agent-evals",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Constrained local Docker evaluation of untrusted JavaScript candidates",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "bin": {
15
+ "zof-agent-evals": "./dist/cli.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE",
21
+ "CHANGELOG.md",
22
+ "docs",
23
+ "examples",
24
+ "SECURITY.md",
25
+ "CONTRIBUTING.md"
26
+ ],
27
+ "engines": {
28
+ "node": ">=22"
29
+ },
30
+ "license": "Apache-2.0",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/Zof-AI/zof-agent-evals.git"
34
+ },
35
+ "homepage": "https://github.com/Zof-AI/zof-agent-evals#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/Zof-AI/zof-agent-evals/issues"
38
+ },
39
+ "keywords": [
40
+ "verification",
41
+ "testing",
42
+ "zof"
43
+ ],
44
+ "publishConfig": {
45
+ "access": "public",
46
+ "provenance": true
47
+ },
48
+ "scripts": {
49
+ "build": "tsc -p tsconfig.json",
50
+ "test": "node --test tests/*.test.mjs",
51
+ "check": "npm run build && npm test",
52
+ "test:container": "node tests/container.mjs",
53
+ "test:package": "node tests/package.mjs"
54
+ },
55
+ "devDependencies": {
56
+ "typescript": "5.9.3",
57
+ "@types/node": "22.19.11"
58
+ },
59
+ "dependencies": {
60
+ "@zof-ai/evidence-spec": "0.1.0",
61
+ "@zof-ai/reliability-bench": "0.1.0"
62
+ }
63
+ }