kijito-tools 0.2.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.
Files changed (104) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +6 -0
  3. package/README.md +220 -0
  4. package/bin/cli.js +34 -0
  5. package/install.sh +161 -0
  6. package/package.json +50 -0
  7. package/providers/_shared/wake-core.mjs +358 -0
  8. package/providers/claude/CLAUDE.md.snippet +31 -0
  9. package/providers/claude/install.sh +186 -0
  10. package/providers/claude/scripts/arm-session.sh +66 -0
  11. package/providers/claude/scripts/claude-armed.sh +101 -0
  12. package/providers/claude/scripts/heartbeat-watchdog.sh +209 -0
  13. package/providers/claude/scripts/inbox-selftest.sh +277 -0
  14. package/providers/claude/scripts/kijito-persona-lib.sh +94 -0
  15. package/providers/claude/scripts/kijito-qa-pass.sh +12 -0
  16. package/providers/claude/scripts/lifecycle-lib.sh +167 -0
  17. package/providers/claude/scripts/myctx.sh +46 -0
  18. package/providers/claude/scripts/self-clear.sh +134 -0
  19. package/providers/claude/scripts/session-autosend.sh +85 -0
  20. package/providers/claude/scripts/session-catchup-hint.sh +330 -0
  21. package/providers/claude/scripts/statusline-context.sh +67 -0
  22. package/providers/claude/skills/kijito-qa-memory/SKILL.md +110 -0
  23. package/providers/claude/skills/kijito-recall/SKILL.md +53 -0
  24. package/providers/claude/skills/kijito-start/SKILL.md +122 -0
  25. package/providers/claude/wiring/README.md +93 -0
  26. package/providers/claude/wiring/kijito-heartbeat@.service +21 -0
  27. package/providers/codex/README.md +75 -0
  28. package/providers/codex/codex-kijito-parity-plan.md +265 -0
  29. package/providers/codex/docs-codex-setup.md +64 -0
  30. package/providers/codex/install.mjs +184 -0
  31. package/providers/codex/n0-capability-probe-protocol.md +472 -0
  32. package/providers/codex/n0-harness/README.md +57 -0
  33. package/providers/codex/n0-harness/cli.mjs +58 -0
  34. package/providers/codex/n0-harness/evidence-manifest.mjs +77 -0
  35. package/providers/codex/n0-harness/fixture.mjs +279 -0
  36. package/providers/codex/n0-harness/lib.mjs +141 -0
  37. package/providers/codex/n0-harness/manifest.mjs +36 -0
  38. package/providers/codex/n0-harness/oracle.mjs +464 -0
  39. package/providers/codex/n0-harness/parser.mjs +200 -0
  40. package/providers/codex/n0-harness/prompt.mjs +43 -0
  41. package/providers/codex/n0-harness/snapshot.mjs +120 -0
  42. package/providers/codex/n0-harness/specimen.mjs +37 -0
  43. package/providers/codex/notify/kijito-notify-count.mjs +117 -0
  44. package/providers/codex/notify/kijito-notify-count.test.mjs +76 -0
  45. package/providers/codex/plans/gate4-battery-log.md +184 -0
  46. package/providers/codex/plans/gate4-battery-protocol.md +43 -0
  47. package/providers/codex/plans/hive-user-first-plan-QA.md +34 -0
  48. package/providers/codex/plans/hive-user-first-plan.md +247 -0
  49. package/providers/codex/release-manifest.json +53 -0
  50. package/providers/codex/same-chat-continuation-plan-gate.md +235 -0
  51. package/providers/codex/same-chat-continuation-plan.md +458 -0
  52. package/providers/codex/skills/kijito-qa-memory/SKILL.md +193 -0
  53. package/providers/codex/skills/kijito-qa-memory/agents/openai.yaml +4 -0
  54. package/providers/codex/skills/kijito-recall/SKILL.md +53 -0
  55. package/providers/codex/skills/kijito-start/SKILL.md +173 -0
  56. package/providers/codex/skills/kijito-start/agents/openai.yaml +4 -0
  57. package/providers/codex/test/n0-cli-refusal.test.mjs +31 -0
  58. package/providers/codex/test/n0-counterexample-matrix.test.mjs +118 -0
  59. package/providers/codex/test/n0-guard-census-core.mjs +1372 -0
  60. package/providers/codex/test/n0-guard-census.json +35479 -0
  61. package/providers/codex/test/n0-guard-census.mjs +31 -0
  62. package/providers/codex/test/n0-guard-census.test.mjs +374 -0
  63. package/providers/codex/test/n0-guard-counterexamples.mjs +1068 -0
  64. package/providers/codex/test/n0-guard-independent-projection.mjs +70 -0
  65. package/providers/codex/test/n0-guard-independent-projection.test.mjs +53 -0
  66. package/providers/codex/test/n0-guard-manifest-author.mjs +424 -0
  67. package/providers/codex/test/n0-guard-mutation-operators.test.mjs +244 -0
  68. package/providers/codex/test/n0-guard-mutation-runner.mjs +599 -0
  69. package/providers/codex/test/n0-guard-remainder-classes.test.mjs +314 -0
  70. package/providers/codex/test/n0-guard-sanitizer-pairs.test.mjs +68 -0
  71. package/providers/codex/test/n0-guard-shared-context-predicates.test.mjs +84 -0
  72. package/providers/codex/test/n0-harness.test.mjs +553 -0
  73. package/providers/codex/test/n0-notable-regressions.test.mjs +70 -0
  74. package/providers/codex/test/n0-subject-boundary.mjs +97 -0
  75. package/providers/codex/test/n0-subject-boundary.test.mjs +72 -0
  76. package/providers/codex/test/same-chat-plan-preflight.sh +167 -0
  77. package/providers/codex/tools/refresh-manifest.mjs +81 -0
  78. package/providers/codex/wake-helper/TRANSPORT-NOTES.md +25 -0
  79. package/providers/codex/wake-helper/integration.test.mjs +285 -0
  80. package/providers/codex/wake-helper/kijito-wake-helper.mjs +488 -0
  81. package/providers/codex/wake-helper/kijito-wake-helper.test.mjs +128 -0
  82. package/providers/codex/wake-helper/mock-daemon.mjs +64 -0
  83. package/providers/codex/wake-helper/status-probe.mjs +45 -0
  84. package/providers/codex/wake-helper/ws-uds.mjs +153 -0
  85. package/providers/monitor/.github/workflows/publish-npm.yml +38 -0
  86. package/providers/monitor/.github/workflows/publish-pypi.yml +30 -0
  87. package/providers/monitor/CHANGELOG.md +465 -0
  88. package/providers/monitor/IMPORT-PROVENANCE.md +68 -0
  89. package/providers/monitor/LICENSE +202 -0
  90. package/providers/monitor/NOTICE +7 -0
  91. package/providers/monitor/OPAQUE-OUTPUT-ENFORCEMENT.md +45 -0
  92. package/providers/monitor/README.md +571 -0
  93. package/providers/monitor/RELEASING.md +125 -0
  94. package/providers/monitor/arm-hive-monitor.sh +13 -0
  95. package/providers/monitor/bin/cli.js +43 -0
  96. package/providers/monitor/com.kijito.inbox-monitor.plist.template +59 -0
  97. package/providers/monitor/docs/DESIGN.md +984 -0
  98. package/providers/monitor/kijito-inbox-monitor@.service.template +95 -0
  99. package/providers/monitor/kijito_inbox_monitor.py +3666 -0
  100. package/providers/monitor/package.json +35 -0
  101. package/providers/monitor/pyproject.toml +42 -0
  102. package/providers/monitor/scripts/mutation-check.py +399 -0
  103. package/providers/monitor/scripts/prepublish-gate.sh +284 -0
  104. package/providers/monitor/test_kijito_monitor.py +5609 -0
@@ -0,0 +1,77 @@
1
+ import path from "node:path";
2
+ import {
3
+ assertExactKeys,
4
+ fail,
5
+ readOwnedRegularFile,
6
+ requireGitCommit,
7
+ requireNonce,
8
+ requireObject,
9
+ requireSafeInteger,
10
+ requireSha256,
11
+ requireString,
12
+ sha256,
13
+ stableJson,
14
+ } from "./lib.mjs";
15
+ import { validateSpecimen } from "./oracle.mjs";
16
+
17
+ export function buildEvidenceManifest({ root, specimen, files, createdAt }) {
18
+ validateSpecimen(specimen);
19
+ requireString(root, "MANIFEST_ROOT", "manifest root");
20
+ if (!path.isAbsolute(root)) fail("MANIFEST_ROOT", "manifest root must be absolute");
21
+ if (path.resolve(root) !== path.resolve(specimen.paths.control)) fail("MANIFEST_ROOT", "manifest root must equal the frozen control root");
22
+ if (!Array.isArray(files) || files.length === 0) fail("MANIFEST_FILES", "manifest files must be a non-empty array");
23
+ if (!Number.isFinite(Date.parse(createdAt))) fail("MANIFEST_TIME", "manifest createdAt must be ISO time");
24
+ const unique = new Set();
25
+ const entries = files.map((relative) => {
26
+ requireString(relative, "MANIFEST_PATH", "manifest relative path");
27
+ if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`)) fail("MANIFEST_PATH", `manifest path escapes root: ${relative}`);
28
+ const normalized = path.normalize(relative);
29
+ if (normalized !== relative || unique.has(relative)) fail("MANIFEST_PATH", `manifest path is duplicate or non-canonical: ${relative}`);
30
+ unique.add(relative);
31
+ const { data, stat } = readOwnedRegularFile(root, path.join(root, relative));
32
+ return { path: relative, bytes: stat.size, sha256: sha256(data) };
33
+ }).sort((a, b) => a.path.localeCompare(b.path));
34
+ return {
35
+ schema: "N0_EVIDENCE_MANIFEST_V1",
36
+ probeId: specimen.probeId,
37
+ protocolDigest: specimen.protocol.digest,
38
+ harnessCommit: specimen.harness.commit,
39
+ harnessDigest: specimen.harness.digest,
40
+ createdAt,
41
+ producer: "N0_OUTSIDE_VERIFIER_V1",
42
+ entries,
43
+ };
44
+ }
45
+
46
+ export function validateEvidenceManifest(specimen, manifest) {
47
+ validateSpecimen(specimen);
48
+ requireObject(manifest, "MANIFEST_SCHEMA", "manifest");
49
+ assertExactKeys(manifest, [
50
+ "schema", "probeId", "protocolDigest", "harnessCommit", "harnessDigest",
51
+ "createdAt", "producer", "entries",
52
+ ], "MANIFEST_SCHEMA", "manifest");
53
+ if (manifest.schema !== "N0_EVIDENCE_MANIFEST_V1") fail("MANIFEST_SCHEMA", "unknown evidence manifest schema");
54
+ requireNonce(manifest.probeId, "MANIFEST_PROVENANCE", "manifest probe id");
55
+ requireSha256(manifest.protocolDigest, "MANIFEST_PROVENANCE", "manifest protocol digest");
56
+ requireGitCommit(manifest.harnessCommit, "MANIFEST_PROVENANCE", "manifest harness commit");
57
+ requireSha256(manifest.harnessDigest, "MANIFEST_PROVENANCE", "manifest harness digest");
58
+ if (manifest.probeId !== specimen.probeId || manifest.protocolDigest !== specimen.protocol.digest
59
+ || manifest.harnessCommit !== specimen.harness.commit || manifest.harnessDigest !== specimen.harness.digest) {
60
+ fail("MANIFEST_PROVENANCE", "manifest provenance differs from specimen");
61
+ }
62
+ if (!Number.isFinite(Date.parse(manifest.createdAt)) || manifest.producer !== "N0_OUTSIDE_VERIFIER_V1") fail("MANIFEST_PROVENANCE", "manifest time/producer is invalid");
63
+ if (!Array.isArray(manifest.entries) || manifest.entries.length === 0) fail("MANIFEST_ENTRIES", "manifest entries must be non-empty");
64
+ const paths = [];
65
+ for (const [index, entry] of manifest.entries.entries()) {
66
+ requireObject(entry, "MANIFEST_ENTRIES", `manifest entry ${index}`);
67
+ assertExactKeys(entry, ["path", "bytes", "sha256"], "MANIFEST_ENTRIES", `manifest entry ${index}`);
68
+ requireString(entry.path, "MANIFEST_ENTRIES", `manifest entry ${index} path`);
69
+ if (path.isAbsolute(entry.path) || entry.path === ".." || entry.path.startsWith(`..${path.sep}`)) fail("MANIFEST_PATH", "manifest entry path escapes root");
70
+ requireSafeInteger(entry.bytes, "MANIFEST_ENTRIES", `manifest entry ${index} bytes`, { min: 0 });
71
+ requireSha256(entry.sha256, "MANIFEST_ENTRIES", `manifest entry ${index} digest`);
72
+ paths.push(entry.path);
73
+ }
74
+ const sorted = [...paths].sort((a, b) => a.localeCompare(b));
75
+ if (new Set(paths).size !== paths.length || stableJson(paths) !== stableJson(sorted)) fail("MANIFEST_ORDER", "manifest paths must be unique and sorted");
76
+ return manifest;
77
+ }
@@ -0,0 +1,279 @@
1
+ import { requiredCanaryNames, requiredCaseNames, requiredCasePolicy } from "./oracle.mjs";
2
+ import { renderPrompt } from "./prompt.mjs";
3
+ import { sha256 } from "./lib.mjs";
4
+ import { harnessManifest } from "./manifest.mjs";
5
+
6
+ export const FIXED = Object.freeze({
7
+ probeId: "11111111111111111111111111111111",
8
+ markerNonce: "22222222222222222222222222222222",
9
+ runNonce: "33333333333333333333333333333333",
10
+ bodyNonce: "44444444444444444444444444444444",
11
+ pointerId: 21813,
12
+ sessionId: "019fb500-0000-7000-8000-000000000001",
13
+ taskId: "task-0001",
14
+ runId: "run-0001",
15
+ turnId: "019fb500-0000-7000-8000-000000000002",
16
+ });
17
+
18
+ const SHA_A = "a".repeat(64);
19
+ const SHA_B = "b".repeat(64);
20
+ const COMMIT_A = "e3221428dc89c1d50564731a848e98d6d23f53e3";
21
+ const COMMIT_B = "b".repeat(40);
22
+ const DEFAULT_PROJECT = "/Users/jason/N0-Probes/11111111111111111111111111111111/project";
23
+ export const PROFILE_CONFIG_UTF8 = [
24
+ 'default_permissions = "n0-workspace"',
25
+ "",
26
+ "[permissions.n0-workspace]",
27
+ 'extends = ":workspace"',
28
+ "",
29
+ "[permissions.n0-workspace.filesystem]",
30
+ '":tmpdir" = "deny"',
31
+ '":slash_tmp" = "deny"',
32
+ "",
33
+ ].join("\n");
34
+
35
+ export function fixtureSpecimen() {
36
+ const paths = {
37
+ specimenParent: "/Users/jason/N0-Probes/11111111111111111111111111111111",
38
+ project: "/Users/jason/N0-Probes/11111111111111111111111111111111/project",
39
+ control: "/Users/jason/.local/state/codex-n0/11111111111111111111111111111111",
40
+ reviewWorktree: "/Users/jason/Code/SideProjects/Codex/.qa-tmp/kijito-codex-continuation-plan",
41
+ originalWorkspace: "/Users/jason/Code/SideProjects/Codex",
42
+ originalCodex: "/Users/jason/Code/SideProjects/Codex/.codex",
43
+ ordinaryConfig: "/Users/jason/.codex/config.toml",
44
+ ordinaryAuth: "/Users/jason/.codex/auth.json",
45
+ slashTmp: "/private/tmp",
46
+ tmpdir: "/private/var/folders/example/T",
47
+ };
48
+ const canaryPaths = {
49
+ "control-read": `${paths.control}/read-${FIXED.probeId}`,
50
+ "control-chmod": `${paths.control}/chmod-${FIXED.probeId}`,
51
+ "control-create": `${paths.control}/create-${FIXED.probeId}`,
52
+ "review-worktree-create": `${paths.reviewWorktree}/create-${FIXED.probeId}`,
53
+ "project-sibling-create": `${paths.specimenParent}/sibling-${FIXED.probeId}`,
54
+ "slash-tmp-create": `${paths.slashTmp}/n0-${FIXED.probeId}`,
55
+ "tmpdir-create": `${paths.tmpdir}/n0-${FIXED.probeId}`,
56
+ "cwd-create": `${paths.project}/receipt-${FIXED.probeId}`,
57
+ };
58
+ const canaries = Object.fromEntries(requiredCanaryNames().map((name) => [name, {
59
+ path: canaryPaths[name],
60
+ existedBefore: name === "control-read" || name === "control-chmod",
61
+ nonce: FIXED.probeId,
62
+ }]));
63
+ const cases = Object.fromEntries(requiredCaseNames().map((name, index) => {
64
+ const policy = requiredCasePolicy(name);
65
+ const intendedBoundary = new Date(Date.parse("2026-07-30T22:10:00.000Z") + index * 60_000).toISOString();
66
+ return [name, {
67
+ nonce: (index + 16).toString(16).padStart(32, "0"),
68
+ ...policy,
69
+ intendedBoundary,
70
+ expected: {
71
+ chatSessionId: FIXED.sessionId,
72
+ taskId: policy.requiresRunBinding ? `task-${String(index + 1).padStart(2, "0")}` : null,
73
+ runId: policy.requiresRunBinding ? `run-${String(index + 1).padStart(2, "0")}` : null,
74
+ turnId: policy.requiresRunBinding ? `turn-${String(index + 1).padStart(2, "0")}` : null,
75
+ },
76
+ mailRowId: name === "N0a-K" ? 77 : null,
77
+ }];
78
+ }));
79
+ const rolloutRoot = "/Users/jason/Library/Application Support/com.openai.chat/sessions";
80
+ const localHarness = harnessManifest();
81
+ const parserDigest = localHarness.entries.find((entry) => entry.file === "providers/codex/n0-harness/parser.mjs").sha256;
82
+ const specimen = {
83
+ schema: "N0_TEST_SPECIMEN_V1",
84
+ probeId: FIXED.probeId,
85
+ protocol: { commit: COMMIT_A, digest: "17ef6a6d92713ba81d38e893c3a97ed5fba181a2dc4f665df4d04737714d4c64" },
86
+ harness: { commit: COMMIT_B, digest: localHarness.aggregate },
87
+ target: { path: paths.project, device: 100, inode: 200, gitHead: COMMIT_B, clean: true },
88
+ paths,
89
+ permission: {
90
+ profile: "n0-workspace",
91
+ configUtf8: PROFILE_CONFIG_UTF8,
92
+ configDigest: "a3546800038c3c90a69ea7c45926cc9f12739e66fcf05f97feaf3a03299b54b8",
93
+ addedWorkspaceRoots: [],
94
+ filesystem: { ":tmpdir": "deny", ":slash_tmp": "deny" },
95
+ legacySandboxSettingsPresent: false,
96
+ },
97
+ environment: fixtureEnvironment(paths.project),
98
+ canaries,
99
+ cases,
100
+ prompts: {},
101
+ clock: { intendedMinuteBoundary: "2026-07-30T22:10:00.000Z", maxSkewSeconds: 15, maxHeartbeatAgeSeconds: 135 },
102
+ rollout: {
103
+ root: rolloutRoot,
104
+ preActionSnapshot: { schema: "N0_ROLLOUT_SNAPSHOT_V1", root: rolloutRoot, totalBytes: 0, entries: [] },
105
+ parser: { version: "N0_ROLLOUT_PARSER_V1", digest: parserDigest },
106
+ },
107
+ versions: {
108
+ hostOs: "macOS 26.4.1 (25E253)",
109
+ chatgpt: "26.721.30844 (5813)",
110
+ codexCli: "0.145.0",
111
+ codexBinaryDigest: "1da3f4e0e96028b8a771814293c3033dafd1971f943f6c7e79b0897fe705f590",
112
+ },
113
+ createdAt: "2026-07-30T22:00:00.000Z",
114
+ };
115
+ for (const name of requiredCaseNames()) {
116
+ const utf8 = renderPrompt(specimen, name);
117
+ specimen.prompts[name] = { utf8, sha256: sha256(Buffer.from(utf8, "utf8")) };
118
+ }
119
+ return specimen;
120
+ }
121
+
122
+ export function fixtureEnvironment(project = DEFAULT_PROJECT) {
123
+ return {
124
+ cwd: project,
125
+ project,
126
+ worktree: project,
127
+ workspace_roots: [project],
128
+ model: "gpt-5.6-sol",
129
+ reasoning: "high",
130
+ approval_policy: "never",
131
+ sandbox_policy: { type: "permission-profile", name: "n0-workspace" },
132
+ permission_profile: {
133
+ name: "n0-workspace",
134
+ file_system: { writable_roots: [project], denied: [":tmpdir", ":slash_tmp"] },
135
+ },
136
+ network: { default: "deny", exceptions: { "N0a-K": ["https://api.kijito.ai/mcp/"] } },
137
+ };
138
+ }
139
+
140
+ export function fixtureRollout({
141
+ sessionId = FIXED.sessionId,
142
+ nonce = FIXED.runNonce,
143
+ taskId = FIXED.taskId,
144
+ runId = FIXED.runId,
145
+ turnId = FIXED.turnId,
146
+ environment = fixtureEnvironment(),
147
+ extraRecords = [],
148
+ userText = `N0 CAPABILITY PROBE ${nonce}`,
149
+ } = {}) {
150
+ const records = [
151
+ { type: "session_meta", payload: { id: sessionId } },
152
+ {
153
+ type: "response_item",
154
+ payload: {
155
+ type: "message",
156
+ role: "user",
157
+ content: [{ type: "input_text", text: userText }],
158
+ internal_chat_message_metadata_passthrough: {
159
+ turn_id: turnId,
160
+ scheduled_task_id: taskId,
161
+ scheduled_run_id: runId,
162
+ },
163
+ },
164
+ },
165
+ { type: "turn_context", payload: { turn_id: turnId, ...environment } },
166
+ ...extraRecords,
167
+ ];
168
+ return records.map((record) => JSON.stringify(record)).join("\n") + "\n";
169
+ }
170
+
171
+ export function fixtureRunRecord() {
172
+ return { taskId: FIXED.taskId, runId: FIXED.runId, turnId: FIXED.turnId };
173
+ }
174
+
175
+ export function fixtureExpected(specimen = fixtureSpecimen()) {
176
+ return {
177
+ sessionId: FIXED.sessionId,
178
+ taskId: FIXED.taskId,
179
+ runId: FIXED.runId,
180
+ turnId: FIXED.turnId,
181
+ runNonce: FIXED.runNonce,
182
+ environment: specimen.environment,
183
+ };
184
+ }
185
+
186
+ export function fixtureKijitoGet({
187
+ id = FIXED.pointerId,
188
+ body = "RESUME NOW [CODEX_CURRENT_STATE_POINTER_V1]: synthetic pointer body",
189
+ outerNonce = "a1b2c3",
190
+ edgeNonce = "d4e5f6",
191
+ } = {}) {
192
+ return [
193
+ `Memory [${id}]`,
194
+ "Type: synthesis",
195
+ "edges:",
196
+ `⟦UNTRUSTED id=21814 src=unknown trust=memory-content n=${edgeNonce}⟧`,
197
+ "edge preview",
198
+ `⟦/UNTRUSTED n=${edgeNonce}⟧`,
199
+ "",
200
+ `⟦UNTRUSTED id=${id} src=persona:codex trust=memory-content n=${outerNonce}⟧`,
201
+ body,
202
+ `⟦/UNTRUSTED n=${outerNonce}⟧`,
203
+ ].join("\n");
204
+ }
205
+
206
+ export function fixtureMailRecord(rowId = 77, bodyNonce = FIXED.bodyNonce) {
207
+ return {
208
+ type: "event_msg",
209
+ payload: {
210
+ type: "mcp_tool_call_end",
211
+ invocation: {
212
+ tool: "kijito_hive_inbox",
213
+ arguments: { persona: "codex-n0", before_id: rowId + 1, limit: 1, mark_read: false },
214
+ },
215
+ result: { content: [{ type: "text", text: `msg ${rowId} [codex-n0 -> codex-n0]: ${bodyNonce}` }] },
216
+ },
217
+ };
218
+ }
219
+
220
+ export function fixturePermissionEvidence(specimen = fixtureSpecimen()) {
221
+ return {
222
+ effectiveProfile: "n0-workspace",
223
+ writableRoots: [specimen.paths.project],
224
+ legacySandboxSettingsPresent: false,
225
+ canaryResults: Object.fromEntries(requiredCanaryNames().map((name) => [name, {
226
+ path: specimen.canaries[name].path,
227
+ succeeded: name === "cwd-create",
228
+ }])),
229
+ };
230
+ }
231
+
232
+ export function fixtureEvidence(specimen = fixtureSpecimen(), nowMs = Date.parse("2026-07-30T23:10:00.000Z")) {
233
+ return {
234
+ schema: "N0_TEST_EVIDENCE_V1",
235
+ meta: {
236
+ utcTime: new Date(nowMs).toISOString(),
237
+ hostTime: new Date(nowMs).toISOString(),
238
+ appVersion: specimen.versions.chatgpt,
239
+ cliVersion: specimen.versions.codexCli,
240
+ targetPath: specimen.paths.project,
241
+ producer: "N0_OUTSIDE_VERIFIER_V1",
242
+ },
243
+ probeId: specimen.probeId,
244
+ protocolDigest: specimen.protocol.digest,
245
+ harnessDigest: specimen.harness.digest,
246
+ journalReachable: true,
247
+ signerArmed: true,
248
+ signerBindingValid: true,
249
+ scheduledState: "enabled",
250
+ serverNowMs: nowMs,
251
+ heartbeatServerMs: nowMs - 30_000,
252
+ permission: fixturePermissionEvidence(specimen),
253
+ integrity: Object.fromEntries([
254
+ "controlPreexisting", "reviewWorktree", "originalCodexTree", "ordinaryConfig", "ordinaryAuth",
255
+ ].map((name) => [name, { preDigest: SHA_A, postDigest: SHA_A }])),
256
+ pointer: { pointerId: FIXED.pointerId, runPointerId: FIXED.pointerId, preDigest: SHA_A, postDigest: SHA_A, runDigest: SHA_A, runMatchedWithheldPair: true },
257
+ mail: {
258
+ rowId: specimen.cases["N0a-K"].mailRowId,
259
+ withheldBodyDigest: SHA_B,
260
+ runBodyDigest: SHA_B,
261
+ preBodyDigest: SHA_B,
262
+ postBodyDigest: SHA_B,
263
+ runContainedWithheldBody: true,
264
+ rowRemainedUnread: true,
265
+ fixtureAlreadyRead: false,
266
+ fixtureAbsent: false,
267
+ },
268
+ cases: Object.fromEntries(requiredCaseNames().map((name, index) => [name, {
269
+ status: "GREEN",
270
+ terminalAt: new Date(Date.parse("2026-07-30T22:10:30.000Z") + index * 60_000).toISOString(),
271
+ nonce: specimen.cases[name].nonce,
272
+ promptDigest: specimen.prompts[name].sha256,
273
+ expected: structuredClone(specimen.cases[name].expected),
274
+ runBindingVerified: specimen.cases[name].requiresRunBinding ? true : null,
275
+ receiptVerified: specimen.cases[name].receiptRequired ? true : null,
276
+ }])),
277
+ caseSequence: requiredCaseNames(),
278
+ };
279
+ }
@@ -0,0 +1,141 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ export const MAX_JSON_BYTES = 2 * 1024 * 1024;
6
+ export const MAX_RECORDS = 50_000;
7
+
8
+ export class N0Error extends Error {
9
+ constructor(code, message, details = undefined) {
10
+ super(`${code}: ${message}`);
11
+ this.name = "N0Error";
12
+ this.code = code;
13
+ this.details = details;
14
+ }
15
+ }
16
+
17
+ export function fail(code, message, details) {
18
+ throw new N0Error(code, message, details);
19
+ }
20
+
21
+ export function isObject(value) {
22
+ return value !== null && typeof value === "object" && !Array.isArray(value);
23
+ }
24
+
25
+ export function requireObject(value, code, label) {
26
+ if (!isObject(value)) fail(code, `${label} must be an object`);
27
+ return value;
28
+ }
29
+
30
+ export function requireString(value, code, label, { min = 1, max = 4096 } = {}) {
31
+ if (typeof value !== "string" || value.length < min || value.length > max) {
32
+ fail(code, `${label} must be a string of length ${min}..${max}`);
33
+ }
34
+ return value;
35
+ }
36
+
37
+ export function requireSafeInteger(value, code, label, { min = 1 } = {}) {
38
+ if (!Number.isSafeInteger(value) || value < min) fail(code, `${label} must be a safe integer >= ${min}`);
39
+ return value;
40
+ }
41
+
42
+ export function requireSha256(value, code, label) {
43
+ requireString(value, code, label, { min: 64, max: 64 });
44
+ if (!/^[0-9a-f]{64}$/.test(value)) fail(code, `${label} must be lowercase SHA-256 hex`);
45
+ return value;
46
+ }
47
+
48
+ export function requireGitCommit(value, code, label) {
49
+ requireString(value, code, label, { min: 40, max: 64 });
50
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(value)) fail(code, `${label} must be a full lowercase Git object id`);
51
+ return value;
52
+ }
53
+
54
+ export function requireNonce(value, code, label) {
55
+ requireString(value, code, label, { min: 32, max: 32 });
56
+ if (!/^[0-9a-f]{32}$/.test(value)) fail(code, `${label} must be lowercase 128-bit hex`);
57
+ return value;
58
+ }
59
+
60
+ export function sha256(value) {
61
+ return crypto.createHash("sha256").update(value).digest("hex");
62
+ }
63
+
64
+ export function stableJson(value) {
65
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
66
+ if (isObject(value)) {
67
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
68
+ }
69
+ return JSON.stringify(value);
70
+ }
71
+
72
+ export function assertExactKeys(value, allowed, code, label) {
73
+ requireObject(value, code, label);
74
+ const extras = Object.keys(value).filter((key) => !allowed.includes(key));
75
+ if (extras.length) fail(code, `${label} contains unsupported keys`, extras.sort());
76
+ }
77
+
78
+ export function pathInside(root, target) {
79
+ const absoluteRoot = path.resolve(root);
80
+ const absoluteTarget = path.resolve(target);
81
+ const relative = path.relative(absoluteRoot, absoluteTarget);
82
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
83
+ }
84
+
85
+ export function requirePathInside(root, target, code = "PATH_ESCAPE") {
86
+ if (!pathInside(root, target)) fail(code, `path escapes root: ${target}`);
87
+ return path.resolve(target);
88
+ }
89
+
90
+ export function readOwnedRegularFile(root, target, {
91
+ maxBytes = MAX_JSON_BYTES,
92
+ uid = process.getuid?.(),
93
+ } = {}) {
94
+ const rootStat = fs.lstatSync(path.resolve(root));
95
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) fail("ROOT_INVALID", "evidence root must be a real directory");
96
+ if (uid !== undefined && rootStat.uid !== uid) fail("OWNER_MISMATCH", "evidence root owner mismatch");
97
+ const absolute = requirePathInside(root, target);
98
+ const stat = fs.lstatSync(absolute);
99
+ if (stat.isSymbolicLink()) fail("SYMLINK_REJECTED", `symlink is not evidence: ${absolute}`);
100
+ if (!stat.isFile()) fail("NOT_REGULAR_FILE", `not a regular file: ${absolute}`);
101
+ if (uid !== undefined && stat.uid !== uid) fail("OWNER_MISMATCH", `file owner mismatch: ${absolute}`);
102
+ if (stat.size > maxBytes) fail("FILE_TOO_LARGE", `file exceeds ${maxBytes} bytes: ${absolute}`);
103
+ const realRoot = fs.realpathSync(root);
104
+ const realTarget = fs.realpathSync(absolute);
105
+ requirePathInside(realRoot, realTarget, "REALPATH_ESCAPE");
106
+ const descriptor = fs.openSync(realTarget, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
107
+ try {
108
+ const before = fs.fstatSync(descriptor);
109
+ if (stat.dev !== before.dev || stat.ino !== before.ino || stat.size !== before.size || stat.mtimeMs !== before.mtimeMs) {
110
+ fail("CONCURRENT_MUTATION", `file changed before read: ${absolute}`);
111
+ }
112
+ const data = fs.readFileSync(descriptor);
113
+ const after = fs.fstatSync(descriptor);
114
+ if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
115
+ fail("CONCURRENT_MUTATION", `file changed during read: ${absolute}`);
116
+ }
117
+ return { data, stat: after, path: realTarget };
118
+ } finally {
119
+ fs.closeSync(descriptor);
120
+ }
121
+ }
122
+
123
+ export function parseJsonBuffer(buffer, code = "INVALID_JSON") {
124
+ try {
125
+ return JSON.parse(buffer.toString("utf8"));
126
+ } catch (error) {
127
+ fail(code, error.message);
128
+ }
129
+ }
130
+
131
+ export function countOccurrences(text, needle) {
132
+ if (!needle) return 0;
133
+ let count = 0;
134
+ let offset = 0;
135
+ while (true) {
136
+ const index = text.indexOf(needle, offset);
137
+ if (index === -1) return count;
138
+ count += 1;
139
+ offset = index + needle.length;
140
+ }
141
+ }
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { sha256, stableJson } from "./lib.mjs";
6
+
7
+ const directory = path.dirname(fileURLToPath(import.meta.url));
8
+ const repo = path.resolve(directory, "../../..");
9
+ const files = [
10
+ "providers/codex/n0-harness/README.md",
11
+ "providers/codex/n0-harness/cli.mjs",
12
+ "providers/codex/n0-harness/evidence-manifest.mjs",
13
+ "providers/codex/n0-harness/fixture.mjs",
14
+ "providers/codex/n0-harness/lib.mjs",
15
+ "providers/codex/n0-harness/manifest.mjs",
16
+ "providers/codex/n0-harness/oracle.mjs",
17
+ "providers/codex/n0-harness/parser.mjs",
18
+ "providers/codex/n0-harness/prompt.mjs",
19
+ "providers/codex/n0-harness/snapshot.mjs",
20
+ "providers/codex/n0-harness/specimen.mjs",
21
+ "providers/codex/test/n0-harness.test.mjs",
22
+ ];
23
+
24
+ export function harnessManifest() {
25
+ const entries = files.map((file) => {
26
+ const data = fs.readFileSync(path.join(repo, file));
27
+ return { file, bytes: data.length, sha256: sha256(data) };
28
+ });
29
+ const aggregate = sha256(Buffer.from(stableJson(entries), "utf8"));
30
+ return { schema: "N0_HARNESS_MANIFEST_V1", aggregate, entries };
31
+ }
32
+
33
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
34
+ const output = harnessManifest();
35
+ process.stdout.write(`${process.argv.includes("--json") ? JSON.stringify(output) : output.aggregate}\n`);
36
+ }