@skill-harness/core 0.10.0 → 0.12.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 (81) hide show
  1. package/README.md +2 -1
  2. package/dist/adapters/types.d.ts +30 -0
  3. package/dist/adjudication.d.ts +9 -1
  4. package/dist/adjudication.js +41 -33
  5. package/dist/arms.d.ts +51 -0
  6. package/dist/arms.js +255 -0
  7. package/dist/capture-trace-types.d.ts +2 -0
  8. package/dist/command-cost.d.ts +9 -0
  9. package/dist/command-cost.js +23 -0
  10. package/dist/comparison.d.ts +1 -0
  11. package/dist/comparison.js +9 -3
  12. package/dist/execution-trace.js +9 -1
  13. package/dist/grade.d.ts +2 -0
  14. package/dist/grade.js +2 -1
  15. package/dist/index.d.ts +14 -0
  16. package/dist/index.js +14 -0
  17. package/dist/journal.d.ts +2 -1
  18. package/dist/judge-agreement.d.ts +19 -0
  19. package/dist/judge-agreement.js +31 -0
  20. package/dist/judge-policy.js +5 -3
  21. package/dist/lift.js +1 -1
  22. package/dist/lint.js +1 -1
  23. package/dist/prompt-normalization.d.ts +6 -0
  24. package/dist/prompt-normalization.js +14 -0
  25. package/dist/provider-failure.d.ts +56 -0
  26. package/dist/provider-failure.js +98 -0
  27. package/dist/qualification-capture.d.ts +44 -0
  28. package/dist/qualification-capture.js +163 -0
  29. package/dist/qualification-config.d.ts +233 -0
  30. package/dist/qualification-config.js +694 -0
  31. package/dist/qualification-lock.d.ts +15 -0
  32. package/dist/qualification-lock.js +87 -0
  33. package/dist/qualification-oauth-directory.d.ts +79 -0
  34. package/dist/qualification-oauth-directory.js +444 -0
  35. package/dist/qualification-panel-store.d.ts +14 -0
  36. package/dist/qualification-panel-store.js +188 -0
  37. package/dist/qualification-panels.d.ts +99 -0
  38. package/dist/qualification-panels.js +91 -0
  39. package/dist/qualification-process.d.ts +13 -0
  40. package/dist/qualification-process.js +188 -0
  41. package/dist/qualification-runner.d.ts +203 -0
  42. package/dist/qualification-runner.js +2037 -0
  43. package/dist/qualification-store.d.ts +187 -0
  44. package/dist/qualification-store.js +833 -0
  45. package/dist/qualification-validation.d.ts +12 -0
  46. package/dist/qualification-validation.js +8 -0
  47. package/dist/regate.js +49 -10
  48. package/dist/regrade.d.ts +3 -1
  49. package/dist/regrade.js +62 -6
  50. package/dist/reps.d.ts +4 -2
  51. package/dist/reps.js +13 -4
  52. package/dist/rescore.js +9 -1
  53. package/dist/results.d.ts +99 -7
  54. package/dist/results.js +228 -10
  55. package/dist/run.d.ts +14 -0
  56. package/dist/run.js +260 -57
  57. package/dist/score.d.ts +4 -3
  58. package/dist/score.js +13 -4
  59. package/dist/screen.d.ts +31 -0
  60. package/dist/screen.js +105 -0
  61. package/dist/seeded.d.ts +12 -1
  62. package/dist/seeded.js +9 -2
  63. package/dist/sources.d.ts +7 -1
  64. package/dist/sources.js +13 -5
  65. package/dist/spec.d.ts +1 -1
  66. package/dist/spec.js +1 -1
  67. package/dist/stability.js +1 -1
  68. package/dist/trajectory-gates.d.ts +14 -2
  69. package/dist/trajectory-gates.js +26 -6
  70. package/dist/vote-panel.d.ts +33 -0
  71. package/dist/vote-panel.js +41 -0
  72. package/dist/workspace.js +34 -3
  73. package/package.json +3 -3
  74. package/schemas/qualification-cell-result-v1.schema.json +32 -0
  75. package/schemas/qualification-config-v1.schema.json +244 -0
  76. package/schemas/qualification-invocation-request-v1.schema.json +59 -0
  77. package/schemas/qualification-judge-panel-result-v1.schema.json +42 -0
  78. package/schemas/results-v2.schema.json +19 -21
  79. package/schemas/results-v3.schema.json +654 -0
  80. package/schemas/specification-v1.schema.json +1 -1
  81. package/schemas/trajectory-event-v1.schema.json +20 -1
@@ -0,0 +1,15 @@
1
+ import { type QualificationProcessIdentity } from "./qualification-process.js";
2
+ export interface QualificationLockIo {
3
+ read(path: string, context: string): unknown;
4
+ write(path: string, value: unknown, exclusive: boolean): void;
5
+ }
6
+ export interface QualificationLockOwner {
7
+ schema_version: "qualification-lock-owner-v1";
8
+ token: string;
9
+ process: QualificationProcessIdentity;
10
+ }
11
+ export declare function tryPublishQualificationLock(lockPath: string, token: string, io: QualificationLockIo): boolean;
12
+ export declare function readQualificationLockOwner(lockPath: string, io: QualificationLockIo): QualificationLockOwner | null;
13
+ export declare function qualificationLockOwnerIsLive(owner: QualificationLockOwner): boolean;
14
+ export declare function reclaimQualificationLock(lockPath: string, observedToken: string, contenderToken: string, io: QualificationLockIo): "reclaimed" | "retry";
15
+ export declare function releaseQualificationLock(lockPath: string, token: string, io: QualificationLockIo): void;
@@ -0,0 +1,87 @@
1
+ import { existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { qualificationProcessIdentity, qualificationProcessMatches } from "./qualification-process.js";
4
+ export function tryPublishQualificationLock(lockPath, token, io) {
5
+ const candidate = `${lockPath}.candidate-${token}`;
6
+ try {
7
+ mkdirSync(candidate, { mode: 0o700 });
8
+ io.write(join(candidate, "owner.json"), {
9
+ schema_version: "qualification-lock-owner-v1",
10
+ token,
11
+ process: qualificationProcessIdentity(process.pid),
12
+ }, true);
13
+ renameSync(candidate, lockPath);
14
+ return true;
15
+ }
16
+ catch (error) {
17
+ if (!["EEXIST", "ENOTEMPTY"].includes(String(error.code)))
18
+ throw error;
19
+ return false;
20
+ }
21
+ finally {
22
+ rmSync(candidate, { recursive: true, force: true });
23
+ }
24
+ }
25
+ export function readQualificationLockOwner(lockPath, io) {
26
+ const ownerPath = join(lockPath, "owner.json");
27
+ if (!existsSync(ownerPath))
28
+ return null;
29
+ let value;
30
+ try {
31
+ value = io.read(ownerPath, `qualification lock owner ${lockPath}`);
32
+ }
33
+ catch (error) {
34
+ if (error instanceof Error && /missing or unreadable/.test(error.message))
35
+ return null;
36
+ throw error;
37
+ }
38
+ if (!plainObject(value) || value.schema_version !== "qualification-lock-owner-v1" || typeof value.token !== "string") {
39
+ throw new Error(`qualification state lock owner is corrupt: ${lockPath}`);
40
+ }
41
+ const identity = processIdentityFrom(value.process);
42
+ if (!identity)
43
+ throw new Error(`qualification state lock owner is corrupt: ${lockPath}`);
44
+ return { schema_version: "qualification-lock-owner-v1", token: value.token, process: identity };
45
+ }
46
+ export function qualificationLockOwnerIsLive(owner) {
47
+ return qualificationProcessMatches(owner.process);
48
+ }
49
+ export function reclaimQualificationLock(lockPath, observedToken, contenderToken, io) {
50
+ const stalePath = `${lockPath}.stale-${contenderToken}`;
51
+ try {
52
+ renameSync(lockPath, stalePath);
53
+ }
54
+ catch (error) {
55
+ if (error.code === "ENOENT")
56
+ return "retry";
57
+ throw error;
58
+ }
59
+ const moved = readQualificationLockOwner(stalePath, io);
60
+ if (!moved || moved.token !== observedToken) {
61
+ // Another generation replaced the observed stale lock before rename. Put it
62
+ // back if the canonical path is still free; never delete an unobserved owner.
63
+ if (!existsSync(lockPath))
64
+ renameSync(stalePath, lockPath);
65
+ throw new Error(`qualification state lock generation changed during stale reclamation: ${lockPath}`);
66
+ }
67
+ rmSync(stalePath, { recursive: true, force: true });
68
+ return "reclaimed";
69
+ }
70
+ export function releaseQualificationLock(lockPath, token, io) {
71
+ const owner = readQualificationLockOwner(lockPath, io);
72
+ if (owner?.token === token)
73
+ rmSync(lockPath, { recursive: true, force: true });
74
+ }
75
+ function processIdentityFrom(value) {
76
+ if (!plainObject(value) || !Number.isInteger(value.pid) || typeof value.platform !== "string" ||
77
+ (value.boot_id !== null && typeof value.boot_id !== "string") || (value.start_ticks !== null && typeof value.start_ticks !== "string"))
78
+ return null;
79
+ return { pid: Number(value.pid), platform: value.platform, boot_id: value.boot_id, start_ticks: value.start_ticks };
80
+ }
81
+ function plainObject(value) {
82
+ if (!value || typeof value !== "object" || Array.isArray(value))
83
+ return false;
84
+ const prototype = Object.getPrototypeOf(value);
85
+ return prototype === Object.prototype || prototype === null;
86
+ }
87
+ //# sourceMappingURL=qualification-lock.js.map
@@ -0,0 +1,79 @@
1
+ import { QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2, type QualificationOAuthDirectoryPolicy } from "./qualification-config.js";
2
+ export declare const QUALIFICATION_OAUTH_DIRECTORY_INVENTORY_VERSION: "qualification-oauth-directory-inventory-v2";
3
+ export type QualificationOAuthDirectoryValidationPointV2 = "before-oauth-readiness" | "after-oauth-readiness" | "before-launch-claim" | "immediately-before-pi-launch" | "after-child-termination";
4
+ export type QualificationFilesystemTypeV2 = "absent" | "regular" | "directory" | "symlink" | "fifo" | "socket" | "block-device" | "character-device" | "unknown";
5
+ export type QualificationOAuthAllowedBasenameV2 = "auth.json" | "models.json" | "models-store.json";
6
+ export interface QualificationFilesystemOccurrenceTupleV2 {
7
+ basename: QualificationOAuthAllowedBasenameV2;
8
+ present: boolean;
9
+ file_type: QualificationFilesystemTypeV2;
10
+ uid: number | null;
11
+ gid: number | null;
12
+ mode: string | null;
13
+ device: string | null;
14
+ inode: string | null;
15
+ link_count: string | null;
16
+ size_bytes: string | null;
17
+ modified_at_ns: string | null;
18
+ changed_at_ns: string | null;
19
+ validated_at: string;
20
+ }
21
+ export interface QualificationDirectoryOccurrenceV2 {
22
+ path: string;
23
+ realpath: string | null;
24
+ file_type: QualificationFilesystemTypeV2;
25
+ uid: number | null;
26
+ gid: number | null;
27
+ mode: string | null;
28
+ device: string | null;
29
+ inode: string | null;
30
+ link_count: string | null;
31
+ size_bytes: string | null;
32
+ modified_at_ns: string | null;
33
+ changed_at_ns: string | null;
34
+ validated_at: string;
35
+ }
36
+ export interface QualificationOAuthDirectoryInventoryV2 {
37
+ schema_version: typeof QUALIFICATION_OAUTH_DIRECTORY_INVENTORY_VERSION;
38
+ policy: typeof QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2;
39
+ validation_point: QualificationOAuthDirectoryValidationPointV2;
40
+ validated_at: string;
41
+ directory: QualificationDirectoryOccurrenceV2;
42
+ entries: [
43
+ QualificationFilesystemOccurrenceTupleV2,
44
+ QualificationFilesystemOccurrenceTupleV2,
45
+ QualificationFilesystemOccurrenceTupleV2
46
+ ];
47
+ unexpected_entries: string[];
48
+ valid: boolean;
49
+ errors: string[];
50
+ }
51
+ export interface QualificationOAuthDirectoryBoundaryV2 {
52
+ policy: typeof QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2;
53
+ agent_directory: string;
54
+ provider: "openai-codex";
55
+ auth_type: "oauth";
56
+ inventory: QualificationOAuthDirectoryInventoryV2;
57
+ }
58
+ export declare class QualificationOAuthDirectoryValidationError extends Error {
59
+ readonly inventory: QualificationOAuthDirectoryInventoryV2;
60
+ constructor(inventory: QualificationOAuthDirectoryInventoryV2);
61
+ }
62
+ export declare function assertQualificationOAuthDirectoryPolicyV2(env: NodeJS.ProcessEnv, options: {
63
+ validation_point: QualificationOAuthDirectoryValidationPointV2;
64
+ now?: () => string;
65
+ }): QualificationOAuthDirectoryBoundaryV2;
66
+ /**
67
+ * Inspect only names and filesystem metadata for models-store.json. Its bytes are
68
+ * never read, hashed, copied, moved, rewritten, or removed by this module.
69
+ */
70
+ export declare function inspectQualificationOAuthDirectoryPolicyV2(env: NodeJS.ProcessEnv, options: {
71
+ validation_point: QualificationOAuthDirectoryValidationPointV2;
72
+ now?: () => string;
73
+ }): QualificationOAuthDirectoryBoundaryV2;
74
+ export declare function invalidateQualificationOAuthDirectoryInventoryV2(inventory: QualificationOAuthDirectoryInventoryV2, error: string): QualificationOAuthDirectoryInventoryV2;
75
+ export declare function assertQualificationOAuthDirectoryContinuityV2(before: QualificationOAuthDirectoryInventoryV2, after: QualificationOAuthDirectoryInventoryV2, options: {
76
+ allow_models_store_change: boolean;
77
+ }): void;
78
+ export declare function validateQualificationOAuthDirectoryInventoryV2(value: unknown, expectedPoint?: QualificationOAuthDirectoryValidationPointV2): asserts value is QualificationOAuthDirectoryInventoryV2;
79
+ export declare function qualificationOAuthDirectoryPolicyIsV2(policy: QualificationOAuthDirectoryPolicy): policy is typeof QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2;
@@ -0,0 +1,444 @@
1
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readdirSync, realpathSync, } from "node:fs";
2
+ import { basename as pathBasename, dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2, qualificationCanonicalJson, } from "./qualification-config.js";
4
+ export const QUALIFICATION_OAUTH_DIRECTORY_INVENTORY_VERSION = "qualification-oauth-directory-inventory-v2";
5
+ const ALLOWED_BASENAMES = ["auth.json", "models.json", "models-store.json"];
6
+ const INVENTORY_KEYS = ["schema_version", "policy", "validation_point", "validated_at", "directory", "entries", "unexpected_entries", "valid", "errors"];
7
+ const DIRECTORY_KEYS = ["path", "realpath", "file_type", "uid", "gid", "mode", "device", "inode", "link_count", "size_bytes", "modified_at_ns", "changed_at_ns", "validated_at"];
8
+ const ENTRY_KEYS = ["basename", "present", "file_type", "uid", "gid", "mode", "device", "inode", "link_count", "size_bytes", "modified_at_ns", "changed_at_ns", "validated_at"];
9
+ const VALIDATION_POINTS = new Set([
10
+ "before-oauth-readiness",
11
+ "after-oauth-readiness",
12
+ "before-launch-claim",
13
+ "immediately-before-pi-launch",
14
+ "after-child-termination",
15
+ ]);
16
+ const FILE_TYPES = new Set([
17
+ "absent", "regular", "directory", "symlink", "fifo", "socket", "block-device", "character-device", "unknown",
18
+ ]);
19
+ export class QualificationOAuthDirectoryValidationError extends Error {
20
+ inventory;
21
+ constructor(inventory) {
22
+ super(`qualification OAuth directory policy v2 rejected the bound directory: ${inventory.errors[0] ?? "validation failed"}`);
23
+ this.name = "QualificationOAuthDirectoryValidationError";
24
+ this.inventory = inventory;
25
+ }
26
+ }
27
+ export function assertQualificationOAuthDirectoryPolicyV2(env, options) {
28
+ const result = inspectQualificationOAuthDirectoryPolicyV2(env, options);
29
+ if (!result.inventory.valid)
30
+ throw new QualificationOAuthDirectoryValidationError(result.inventory);
31
+ return result;
32
+ }
33
+ /**
34
+ * Inspect only names and filesystem metadata for models-store.json. Its bytes are
35
+ * never read, hashed, copied, moved, rewritten, or removed by this module.
36
+ */
37
+ export function inspectQualificationOAuthDirectoryPolicyV2(env, options) {
38
+ if (!VALIDATION_POINTS.has(options.validation_point))
39
+ throw new Error(`qualification OAuth directory validation point is unsupported: ${String(options.validation_point)}`);
40
+ const validatedAt = timestamp((options.now ?? (() => new Date().toISOString()))(), "qualification OAuth directory validation time");
41
+ const agentDirectory = env.PI_CODING_AGENT_DIR ?? (env.HOME ? join(env.HOME, ".pi", "agent") : undefined);
42
+ if (!agentDirectory || !isAbsolute(agentDirectory)) {
43
+ throw new Error("qualification OAuth directory policy v2 requires an absolute PI_CODING_AGENT_DIR or HOME");
44
+ }
45
+ if (resolve(agentDirectory) !== agentDirectory) {
46
+ throw new Error("qualification OAuth directory policy v2 requires a canonical absolute path without traversal");
47
+ }
48
+ const errors = [];
49
+ let directoryStat = null;
50
+ let directoryRealpath = null;
51
+ try {
52
+ directoryStat = lstatSync(agentDirectory, { bigint: true });
53
+ if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory())
54
+ errors.push("directory must be a real non-symlink directory");
55
+ try {
56
+ directoryRealpath = realpathSync(agentDirectory);
57
+ }
58
+ catch {
59
+ errors.push("directory realpath is unavailable");
60
+ }
61
+ if (directoryRealpath !== null && directoryRealpath !== agentDirectory)
62
+ errors.push("directory path is an alias rather than the exact bound occurrence");
63
+ const euid = effectiveUid();
64
+ if (euid === null || safeNumber(directoryStat.uid) !== euid)
65
+ errors.push("directory must be owned by the effective qualification UID");
66
+ if (permissionMode(directoryStat) !== "0700")
67
+ errors.push("directory must have exact mode 0700 and no effective non-owner ACL access");
68
+ }
69
+ catch {
70
+ errors.push("directory is missing or unreadable");
71
+ }
72
+ const directory = directoryTuple(agentDirectory, directoryRealpath, directoryStat, validatedAt);
73
+ let directoryFd;
74
+ let names = [];
75
+ const bytes = new Map();
76
+ const entries = [];
77
+ try {
78
+ if (!directoryStat || errors.length > 0) {
79
+ for (const name of ALLOWED_BASENAMES)
80
+ entries.push(absentTuple(name, validatedAt));
81
+ }
82
+ else {
83
+ directoryFd = openSync(agentDirectory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
84
+ const openedDirectory = fstatSync(directoryFd, { bigint: true });
85
+ if (!sameOccurrence(directoryStat, openedDirectory))
86
+ errors.push("directory occurrence changed while it was opened");
87
+ const openedDirectoryPath = process.platform === "linux" ? `/proc/${process.pid}/fd/${directoryFd}` : agentDirectory;
88
+ names = readdirSync(openedDirectoryPath).sort();
89
+ const unexpected = names.filter((name) => !ALLOWED_BASENAMES.includes(name));
90
+ for (const name of unexpected)
91
+ errors.push(`undeclared entry ${JSON.stringify(name)} is not permitted`);
92
+ for (const name of ALLOWED_BASENAMES) {
93
+ entries.push(inspectAllowedEntry(openedDirectoryPath, directoryRealpath, name, names.includes(name), validatedAt, errors, bytes));
94
+ }
95
+ const finalNames = readdirSync(openedDirectoryPath).sort();
96
+ if (JSON.stringify(finalNames) !== JSON.stringify(names))
97
+ errors.push("directory inventory changed while it was validated");
98
+ const finalOpenedDirectory = fstatSync(directoryFd, { bigint: true });
99
+ const finalPathDirectory = lstatSync(agentDirectory, { bigint: true });
100
+ if (!sameOccurrence(openedDirectory, finalOpenedDirectory) || !sameOccurrence(openedDirectory, finalPathDirectory)) {
101
+ errors.push("directory occurrence changed while its entries were validated");
102
+ }
103
+ }
104
+ }
105
+ catch (error) {
106
+ errors.push(`directory inventory could not be validated${safeFsCode(error)}`);
107
+ while (entries.length < ALLOWED_BASENAMES.length)
108
+ entries.push(absentTuple(ALLOWED_BASENAMES[entries.length], validatedAt));
109
+ }
110
+ finally {
111
+ if (directoryFd !== undefined)
112
+ closeSync(directoryFd);
113
+ }
114
+ validateCredentialSemantics(bytes, entries, errors);
115
+ const unexpectedEntries = names.filter((name) => !ALLOWED_BASENAMES.includes(name));
116
+ const inventory = {
117
+ schema_version: QUALIFICATION_OAUTH_DIRECTORY_INVENTORY_VERSION,
118
+ policy: QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2,
119
+ validation_point: options.validation_point,
120
+ validated_at: validatedAt,
121
+ directory,
122
+ entries: entries,
123
+ unexpected_entries: unexpectedEntries,
124
+ valid: errors.length === 0,
125
+ errors,
126
+ };
127
+ return {
128
+ policy: QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2,
129
+ agent_directory: agentDirectory,
130
+ provider: "openai-codex",
131
+ auth_type: "oauth",
132
+ inventory,
133
+ };
134
+ }
135
+ export function invalidateQualificationOAuthDirectoryInventoryV2(inventory, error) {
136
+ validateQualificationOAuthDirectoryInventoryV2(inventory, inventory.validation_point);
137
+ return { ...structuredClone(inventory), valid: false, errors: [...inventory.errors, error] };
138
+ }
139
+ export function assertQualificationOAuthDirectoryContinuityV2(before, after, options) {
140
+ validateQualificationOAuthDirectoryInventoryV2(before, before.validation_point);
141
+ validateQualificationOAuthDirectoryInventoryV2(after, after.validation_point);
142
+ if (!before.valid || !after.valid)
143
+ throw new Error("qualification OAuth directory continuity requires valid inventories");
144
+ if (qualificationCanonicalJson(directoryContinuityIdentity(before.directory)) !== qualificationCanonicalJson(directoryContinuityIdentity(after.directory))) {
145
+ throw new Error("qualification OAuth directory occurrence changed between validation points");
146
+ }
147
+ for (const name of ALLOWED_BASENAMES) {
148
+ if (name === "models-store.json" && options.allow_models_store_change)
149
+ continue;
150
+ const left = before.entries.find((entry) => entry.basename === name);
151
+ const right = after.entries.find((entry) => entry.basename === name);
152
+ if (qualificationCanonicalJson(entryContinuityIdentity(left)) !== qualificationCanonicalJson(entryContinuityIdentity(right))) {
153
+ throw new Error(`qualification OAuth ${name} occurrence changed between validation points`);
154
+ }
155
+ }
156
+ }
157
+ export function validateQualificationOAuthDirectoryInventoryV2(value, expectedPoint) {
158
+ if (!plainObject(value))
159
+ throw new Error("qualification OAuth directory inventory v2 must be an object");
160
+ exactKeys(value, INVENTORY_KEYS, "qualification OAuth directory inventory v2");
161
+ if (value.schema_version !== QUALIFICATION_OAUTH_DIRECTORY_INVENTORY_VERSION || value.policy !== QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2) {
162
+ throw new Error("qualification OAuth directory inventory v2 identity is invalid");
163
+ }
164
+ if (typeof value.validation_point !== "string" || !VALIDATION_POINTS.has(value.validation_point) ||
165
+ (expectedPoint !== undefined && value.validation_point !== expectedPoint)) {
166
+ throw new Error("qualification OAuth directory inventory v2 validation point is invalid");
167
+ }
168
+ timestamp(String(value.validated_at), "qualification OAuth directory inventory validation time");
169
+ validateDirectoryTuple(value.directory, String(value.validated_at));
170
+ if (!Array.isArray(value.entries) || value.entries.length !== ALLOWED_BASENAMES.length)
171
+ throw new Error("qualification OAuth directory inventory v2 entries are invalid");
172
+ value.entries.forEach((entry, index) => validateEntryTuple(entry, ALLOWED_BASENAMES[index], String(value.validated_at)));
173
+ if (!Array.isArray(value.unexpected_entries) || value.unexpected_entries.some((entry) => typeof entry !== "string") ||
174
+ JSON.stringify([...value.unexpected_entries].sort()) !== JSON.stringify(value.unexpected_entries) || new Set(value.unexpected_entries).size !== value.unexpected_entries.length) {
175
+ throw new Error("qualification OAuth directory inventory v2 unexpected entries are invalid");
176
+ }
177
+ if (typeof value.valid !== "boolean" || !Array.isArray(value.errors) || value.errors.some((error) => typeof error !== "string" || !error) ||
178
+ value.valid !== (value.errors.length === 0) || (value.valid && value.unexpected_entries.length > 0)) {
179
+ throw new Error("qualification OAuth directory inventory v2 verdict is contradictory");
180
+ }
181
+ if (value.valid) {
182
+ const directory = value.directory;
183
+ const entries = value.entries;
184
+ const auth = entries[0];
185
+ const validDirectory = directory.file_type === "directory" && directory.realpath === directory.path && directory.uid !== null && directory.mode === "0700";
186
+ const validFile = (entry, required) => (!entry.present && !required) ||
187
+ (entry.present && entry.file_type === "regular" && entry.uid === directory.uid && entry.mode === "0600" &&
188
+ (process.platform === "win32" || entry.link_count === "1"));
189
+ if (!validDirectory || !validFile(auth, true) || !validFile(entries[1], false) || !validFile(entries[2], false)) {
190
+ throw new Error("qualification OAuth directory inventory v2 valid verdict does not satisfy the closed metadata policy");
191
+ }
192
+ }
193
+ }
194
+ function inspectAllowedEntry(openedDirectoryPath, directoryRealpath, name, listed, validatedAt, errors, bytes) {
195
+ if (!listed) {
196
+ if (name === "auth.json")
197
+ errors.push("auth.json is required");
198
+ return absentTuple(name, validatedAt);
199
+ }
200
+ const path = join(openedDirectoryPath, name);
201
+ let stat;
202
+ try {
203
+ stat = lstatSync(path, { bigint: true });
204
+ }
205
+ catch {
206
+ errors.push(`${name} changed between inventory and no-follow lstat`);
207
+ return absentTuple(name, validatedAt);
208
+ }
209
+ const tuple = fileTuple(name, stat, validatedAt);
210
+ if (stat.isSymbolicLink() || !stat.isFile()) {
211
+ errors.push(`${name} must be a regular non-symlink file`);
212
+ return tuple;
213
+ }
214
+ const euid = effectiveUid();
215
+ if (euid === null || safeNumber(stat.uid) !== euid)
216
+ errors.push(`${name} must be owned by the effective qualification UID`);
217
+ if (permissionMode(stat) !== "0600")
218
+ errors.push(`${name} must have exact mode 0600`);
219
+ if (process.platform !== "win32" && stat.nlink !== 1n)
220
+ errors.push(`${name} must have exactly one hard link where supported`);
221
+ let resolved;
222
+ try {
223
+ resolved = realpathSync(path);
224
+ }
225
+ catch {
226
+ errors.push(`${name} realpath is unavailable`);
227
+ return tuple;
228
+ }
229
+ if (dirname(resolved) !== directoryRealpath || pathBasename(resolved) !== name || resolved !== join(directoryRealpath, name)) {
230
+ errors.push(`${name} is not a direct child of the exact bound OAuth directory`);
231
+ return tuple;
232
+ }
233
+ let fd;
234
+ try {
235
+ fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
236
+ const opened = fstatSync(fd, { bigint: true });
237
+ if (!sameOccurrence(stat, opened) || !opened.isFile())
238
+ throw new Error("occurrence mismatch");
239
+ // auth.json and models.json retain their historical semantic checks. The
240
+ // runtime-state file is intentionally never passed to readFileSync.
241
+ if (name !== "models-store.json")
242
+ bytes.set(name, readFileSync(fd));
243
+ const finalOpened = fstatSync(fd, { bigint: true });
244
+ const finalPath = lstatSync(path, { bigint: true });
245
+ if (!sameOccurrence(opened, finalOpened) || !sameOccurrence(opened, finalPath))
246
+ throw new Error("occurrence changed");
247
+ }
248
+ catch {
249
+ errors.push(`${name} changed while its no-follow occurrence was validated`);
250
+ }
251
+ finally {
252
+ if (fd !== undefined)
253
+ closeSync(fd);
254
+ }
255
+ return tuple;
256
+ }
257
+ function validateCredentialSemantics(bytes, entries, errors) {
258
+ const authEntry = entries.find((entry) => entry.basename === "auth.json");
259
+ const authBytes = bytes.get("auth.json");
260
+ if (authEntry?.present && authEntry.file_type === "regular" && authBytes) {
261
+ let parsed;
262
+ try {
263
+ parsed = JSON.parse(authBytes.toString("utf8"));
264
+ }
265
+ catch {
266
+ errors.push("auth.json must contain valid JSON");
267
+ parsed = null;
268
+ }
269
+ if (parsed !== null) {
270
+ if (!plainObject(parsed) || JSON.stringify(Object.keys(parsed).sort()) !== JSON.stringify(["openai-codex"])) {
271
+ errors.push("auth.json must contain exactly the openai-codex OAuth credential");
272
+ }
273
+ else {
274
+ const codex = parsed["openai-codex"];
275
+ if (!plainObject(codex) || codex.type !== "oauth")
276
+ errors.push("auth.json openai-codex credential must be OAuth");
277
+ }
278
+ }
279
+ }
280
+ const modelsEntry = entries.find((entry) => entry.basename === "models.json");
281
+ const modelsBytes = bytes.get("models.json");
282
+ if (modelsEntry?.present && modelsEntry.file_type === "regular" && modelsBytes) {
283
+ let parsed;
284
+ try {
285
+ parsed = JSON.parse(modelsBytes.toString("utf8"));
286
+ }
287
+ catch {
288
+ errors.push("models.json must contain valid JSON");
289
+ parsed = null;
290
+ }
291
+ if (parsed !== null && (!plainObject(parsed) || Object.keys(parsed).length !== 0)) {
292
+ errors.push("models.json must be an empty object without provider overrides or embedded credentials");
293
+ }
294
+ }
295
+ }
296
+ function directoryTuple(path, realpath, stat, validatedAt) {
297
+ return {
298
+ path,
299
+ realpath,
300
+ file_type: stat ? fileType(stat) : "absent",
301
+ uid: stat ? safeNumber(stat.uid) : null,
302
+ gid: stat ? safeNumber(stat.gid) : null,
303
+ mode: stat ? permissionMode(stat) : null,
304
+ device: stat ? stat.dev.toString() : null,
305
+ inode: stat ? stat.ino.toString() : null,
306
+ link_count: stat ? stat.nlink.toString() : null,
307
+ size_bytes: stat ? stat.size.toString() : null,
308
+ modified_at_ns: stat ? stat.mtimeNs.toString() : null,
309
+ changed_at_ns: stat ? stat.ctimeNs.toString() : null,
310
+ validated_at: validatedAt,
311
+ };
312
+ }
313
+ function fileTuple(name, stat, validatedAt) {
314
+ return {
315
+ basename: name,
316
+ present: true,
317
+ file_type: fileType(stat),
318
+ uid: safeNumber(stat.uid),
319
+ gid: safeNumber(stat.gid),
320
+ mode: permissionMode(stat),
321
+ device: stat.dev.toString(),
322
+ inode: stat.ino.toString(),
323
+ link_count: stat.nlink.toString(),
324
+ size_bytes: stat.size.toString(),
325
+ modified_at_ns: stat.mtimeNs.toString(),
326
+ changed_at_ns: stat.ctimeNs.toString(),
327
+ validated_at: validatedAt,
328
+ };
329
+ }
330
+ function absentTuple(name, validatedAt) {
331
+ return {
332
+ basename: name,
333
+ present: false,
334
+ file_type: "absent",
335
+ uid: null,
336
+ gid: null,
337
+ mode: null,
338
+ device: null,
339
+ inode: null,
340
+ link_count: null,
341
+ size_bytes: null,
342
+ modified_at_ns: null,
343
+ changed_at_ns: null,
344
+ validated_at: validatedAt,
345
+ };
346
+ }
347
+ function fileType(stat) {
348
+ if (stat.isFile())
349
+ return "regular";
350
+ if (stat.isDirectory())
351
+ return "directory";
352
+ if (stat.isSymbolicLink())
353
+ return "symlink";
354
+ if (stat.isFIFO())
355
+ return "fifo";
356
+ if (stat.isSocket())
357
+ return "socket";
358
+ if (stat.isBlockDevice())
359
+ return "block-device";
360
+ if (stat.isCharacterDevice())
361
+ return "character-device";
362
+ return "unknown";
363
+ }
364
+ function sameOccurrence(left, right) {
365
+ return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode && left.uid === right.uid && left.gid === right.gid &&
366
+ left.nlink === right.nlink && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs &&
367
+ fileType(left) === fileType(right);
368
+ }
369
+ function permissionMode(stat) {
370
+ return Number(stat.mode & 4095n).toString(8).padStart(4, "0");
371
+ }
372
+ function effectiveUid() {
373
+ return process.geteuid ? process.geteuid() : null;
374
+ }
375
+ function safeNumber(value) {
376
+ const number = Number(value);
377
+ return Number.isSafeInteger(number) && number >= 0 ? number : null;
378
+ }
379
+ function directoryContinuityIdentity(value) {
380
+ const { size_bytes: _size, modified_at_ns: _mtime, changed_at_ns: _ctime, validated_at: _validated, ...identity } = value;
381
+ return identity;
382
+ }
383
+ function entryContinuityIdentity(value) {
384
+ const { validated_at: _validated, ...identity } = value;
385
+ return identity;
386
+ }
387
+ function validateDirectoryTuple(value, validatedAt) {
388
+ if (!plainObject(value))
389
+ throw new Error("qualification OAuth directory inventory v2 directory tuple is invalid");
390
+ exactKeys(value, DIRECTORY_KEYS, "qualification OAuth directory inventory v2 directory tuple");
391
+ if (typeof value.path !== "string" || !isAbsolute(value.path) || (value.realpath !== null && (typeof value.realpath !== "string" || !isAbsolute(value.realpath))) ||
392
+ typeof value.file_type !== "string" || !FILE_TYPES.has(value.file_type) || value.file_type === "regular" ||
393
+ !nullableUid(value.uid) || !nullableUid(value.gid) || !nullableMode(value.mode) || !nullableDecimal(value.device) || !nullableDecimal(value.inode) ||
394
+ !nullableDecimal(value.link_count) || !nullableDecimal(value.size_bytes) || !nullableDecimal(value.modified_at_ns) || !nullableDecimal(value.changed_at_ns) ||
395
+ value.validated_at !== validatedAt) {
396
+ throw new Error("qualification OAuth directory inventory v2 directory tuple is invalid");
397
+ }
398
+ }
399
+ function validateEntryTuple(value, basename, validatedAt) {
400
+ if (!plainObject(value))
401
+ throw new Error("qualification OAuth directory inventory v2 entry tuple is invalid");
402
+ exactKeys(value, ENTRY_KEYS, "qualification OAuth directory inventory v2 entry tuple");
403
+ if (value.basename !== basename || typeof value.present !== "boolean" || typeof value.file_type !== "string" || !FILE_TYPES.has(value.file_type) ||
404
+ value.validated_at !== validatedAt || !nullableUid(value.uid) || !nullableUid(value.gid) || !nullableMode(value.mode) || !nullableDecimal(value.device) ||
405
+ !nullableDecimal(value.inode) || !nullableDecimal(value.link_count) || !nullableDecimal(value.size_bytes) || !nullableDecimal(value.modified_at_ns) || !nullableDecimal(value.changed_at_ns)) {
406
+ throw new Error("qualification OAuth directory inventory v2 entry tuple is invalid");
407
+ }
408
+ const metadata = [value.uid, value.gid, value.mode, value.device, value.inode, value.link_count, value.size_bytes, value.modified_at_ns, value.changed_at_ns];
409
+ if ((!value.present && (value.file_type !== "absent" || metadata.some((item) => item !== null))) ||
410
+ (value.present && (value.file_type === "absent" || metadata.some((item) => item === null)))) {
411
+ throw new Error("qualification OAuth directory inventory v2 entry presence is contradictory");
412
+ }
413
+ }
414
+ function nullableUid(value) { return value === null || (Number.isSafeInteger(value) && Number(value) >= 0); }
415
+ function nullableMode(value) { return value === null || (typeof value === "string" && /^[0-7]{4}$/.test(value)); }
416
+ function nullableDecimal(value) { return value === null || (typeof value === "string" && /^(?:0|[1-9][0-9]*)$/.test(value)); }
417
+ function exactKeys(value, keys, ctx) {
418
+ const allowed = new Set(keys);
419
+ const unknown = Object.keys(value).find((key) => !allowed.has(key));
420
+ if (unknown)
421
+ throw new Error(`${ctx} contains unknown field ${unknown}`);
422
+ const missing = keys.find((key) => !Object.hasOwn(value, key));
423
+ if (missing)
424
+ throw new Error(`${ctx} is missing field ${missing}`);
425
+ }
426
+ function plainObject(value) {
427
+ if (!value || typeof value !== "object" || Array.isArray(value))
428
+ return false;
429
+ const prototype = Object.getPrototypeOf(value);
430
+ return prototype === Object.prototype || prototype === null;
431
+ }
432
+ function timestamp(value, ctx) {
433
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(value) || !Number.isFinite(Date.parse(value)))
434
+ throw new Error(`${ctx} is not an RFC 3339 UTC timestamp`);
435
+ return value;
436
+ }
437
+ function safeFsCode(error) {
438
+ const code = error?.code;
439
+ return typeof code === "string" && /^[A-Z0-9_]+$/.test(code) ? ` (${code})` : "";
440
+ }
441
+ export function qualificationOAuthDirectoryPolicyIsV2(policy) {
442
+ return policy === QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2;
443
+ }
444
+ //# sourceMappingURL=qualification-oauth-directory.js.map
@@ -0,0 +1,14 @@
1
+ import { type QualificationCellResult, type QualificationJudgePanelResult } from "./qualification-panels.js";
2
+ export declare function recordQualificationJudgePanel(options: {
3
+ spool_dir: string;
4
+ panel_id: string;
5
+ member_invocation_ids: string[];
6
+ }): QualificationJudgePanelResult;
7
+ export declare function recordQualificationCell(options: {
8
+ spool_dir: string;
9
+ cell_id: string;
10
+ }): QualificationCellResult;
11
+ export declare function validateQualificationPanelOutputs(spoolDir: string): {
12
+ panels: number;
13
+ cells: number;
14
+ };