@skill-harness/adapters 0.11.0 → 0.13.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 (117) hide show
  1. package/README.md +2 -1
  2. package/dist/archive-access.d.ts +58 -0
  3. package/dist/archive-access.js +61 -0
  4. package/dist/archive-checkpoint.d.ts +47 -0
  5. package/dist/archive-checkpoint.js +116 -0
  6. package/dist/archive-facts.d.ts +19 -0
  7. package/dist/archive-facts.js +20 -0
  8. package/dist/archive-observer.d.ts +21 -0
  9. package/dist/archive-observer.js +79 -0
  10. package/dist/archive-policy.d.ts +107 -0
  11. package/dist/archive-policy.js +135 -0
  12. package/dist/archive-read-capability.d.ts +49 -0
  13. package/dist/archive-read-capability.js +45 -0
  14. package/dist/archive-retention-policy.d.ts +52 -0
  15. package/dist/archive-retention-policy.js +49 -0
  16. package/dist/archived-work.d.ts +44 -0
  17. package/dist/archived-work.js +59 -0
  18. package/dist/blind-intervention.d.ts +29 -0
  19. package/dist/blind-intervention.js +211 -0
  20. package/dist/closed-schema.d.ts +6 -0
  21. package/dist/closed-schema.js +70 -14
  22. package/dist/codex-diagnostic-model.d.ts +14 -0
  23. package/dist/codex-diagnostic-model.js +39 -0
  24. package/dist/codex-host-observer.d.ts +111 -0
  25. package/dist/codex-host-observer.js +309 -0
  26. package/dist/codex-https.d.ts +7 -0
  27. package/dist/codex-https.js +221 -0
  28. package/dist/codex-model-profile.d.ts +38 -0
  29. package/dist/codex-model-profile.js +54 -0
  30. package/dist/codex-producer-ipc.d.ts +55 -0
  31. package/dist/codex-producer-ipc.js +67 -0
  32. package/dist/codex-sdk-transport.d.ts +50 -0
  33. package/dist/codex-sdk-transport.js +294 -0
  34. package/dist/codex-subscription.d.ts +114 -0
  35. package/dist/codex-subscription.js +268 -0
  36. package/dist/evidence-archive.d.ts +57 -0
  37. package/dist/evidence-archive.js +221 -0
  38. package/dist/execution-projection-schema.d.ts +10 -0
  39. package/dist/execution-projection-schema.js +30 -0
  40. package/dist/execution-retention-archive.d.ts +69 -0
  41. package/dist/execution-retention-archive.js +154 -0
  42. package/dist/generated/retention-v2-contract.d.ts +72 -0
  43. package/dist/generated/retention-v2-contract.js +105 -0
  44. package/dist/generated/retention-v2-json.d.ts +24 -0
  45. package/dist/generated/retention-v2-json.js +268 -0
  46. package/dist/generated/retention-v2-native-json.d.ts +2 -0
  47. package/dist/generated/retention-v2-native-json.js +33 -0
  48. package/dist/generated/retention-v2-native.d.ts +58 -0
  49. package/dist/generated/retention-v2-native.js +155 -0
  50. package/dist/generated/retention-v2-pin.d.ts +2 -0
  51. package/dist/generated/retention-v2-pin.js +4 -0
  52. package/dist/generated/retention-v2-types.d.ts +49 -0
  53. package/dist/generated/retention-v2-types.js +2 -0
  54. package/dist/generated/retention-v2-work-types.d.ts +260 -0
  55. package/dist/generated/retention-v2-work-types.js +13 -0
  56. package/dist/generated/work-v4/execution-id.d.ts +5 -0
  57. package/dist/generated/work-v4/execution-id.js +14 -0
  58. package/dist/generated/work-v4/json.d.ts +24 -0
  59. package/dist/generated/work-v4/json.js +268 -0
  60. package/dist/generated/work-v4/occurrences.d.ts +226 -0
  61. package/dist/generated/work-v4/occurrences.js +116 -0
  62. package/dist/generated/work-v4/pin.d.ts +1 -0
  63. package/dist/generated/work-v4/pin.js +3 -0
  64. package/dist/generated/work-v4/projection.d.ts +294 -0
  65. package/dist/generated/work-v4/projection.js +238 -0
  66. package/dist/generated/work-v4/reader.d.ts +24 -0
  67. package/dist/generated/work-v4/reader.js +109 -0
  68. package/dist/generated/work-v4/snapshot.d.ts +28 -0
  69. package/dist/generated/work-v4/snapshot.js +268 -0
  70. package/dist/generated/work-v4/types.d.ts +260 -0
  71. package/dist/generated/work-v4/types.js +13 -0
  72. package/dist/generated/work-v4/validation.d.ts +13 -0
  73. package/dist/generated/work-v4/validation.js +227 -0
  74. package/dist/index.d.ts +24 -0
  75. package/dist/index.js +24 -0
  76. package/dist/installed-review-session.d.ts +28 -0
  77. package/dist/installed-review-session.js +166 -0
  78. package/dist/intervention-run.d.ts +403 -0
  79. package/dist/intervention-run.js +169 -0
  80. package/dist/learning-case.d.ts +12 -0
  81. package/dist/learning-case.js +15 -0
  82. package/dist/learning-journal.d.ts +18 -0
  83. package/dist/learning-journal.js +256 -0
  84. package/dist/pi-daddy-ledger-v2.d.ts +4 -4
  85. package/dist/pi-daddy-ledger-v2.js +9 -7
  86. package/dist/pi-daddy-ledger-v3.d.ts +683 -0
  87. package/dist/pi-daddy-ledger-v3.js +961 -0
  88. package/dist/pi.js +115 -24
  89. package/dist/principal-payload-port.d.ts +25 -0
  90. package/dist/principal-payload-port.js +40 -0
  91. package/dist/producer-product.d.ts +70 -0
  92. package/dist/producer-product.js +186 -0
  93. package/dist/producer-review.d.ts +53 -0
  94. package/dist/producer-review.js +207 -0
  95. package/dist/prompt-capture-extension.d.ts +10 -0
  96. package/dist/prompt-capture-extension.js +26 -0
  97. package/dist/prompt-provenance.d.ts +22 -0
  98. package/dist/prompt-provenance.js +146 -0
  99. package/dist/reviewed-archive-export.d.ts +42 -0
  100. package/dist/reviewed-archive-export.js +78 -0
  101. package/dist/trajectory.d.ts +15 -9
  102. package/dist/trajectory.js +375 -55
  103. package/dist/trust-lifecycle.d.ts +128 -0
  104. package/dist/trust-lifecycle.js +200 -0
  105. package/dist/weekly-investigation.d.ts +237 -0
  106. package/dist/weekly-investigation.js +330 -0
  107. package/dist/work-candidates.d.ts +29 -0
  108. package/dist/work-candidates.js +78 -0
  109. package/dist/work-case-archive.d.ts +25 -0
  110. package/dist/work-case-archive.js +63 -0
  111. package/dist/work-case-review.d.ts +45 -0
  112. package/dist/work-case-review.js +220 -0
  113. package/dist/work-signal-cases.d.ts +15 -0
  114. package/dist/work-signal-cases.js +54 -0
  115. package/dist/work-signal-observation.d.ts +23 -0
  116. package/dist/work-signal-observation.js +89 -0
  117. package/package.json +4 -3
@@ -0,0 +1,211 @@
1
+ import { constants, closeSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readSync, writeSync } from "node:fs";
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { join } from "node:path";
4
+ import { assessIntervention, createBlindComparison, interventionCanonicalJson } from "@skill-harness/core";
5
+ import { readArchiveSource, retainArchiveSource } from "./evidence-archive.js";
6
+ const SHA = /^[a-f0-9]{64}$/;
7
+ const missing = (e) => e?.code === "ENOENT";
8
+ const encode = interventionCanonicalJson;
9
+ const copy = (value) => JSON.parse(encode(value));
10
+ function keys(value, names) {
11
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).sort().join() !== names.sort().join())
12
+ throw new Error("invalid blind record fields");
13
+ }
14
+ function authorValid(author) {
15
+ if (typeof author !== "string" || !author.length || author.length > 512 || /[\u0000-\u001f\u007f]/.test(author))
16
+ throw new Error("explicit blind author required");
17
+ }
18
+ function claimsValid(claims, manifest) {
19
+ keys(claims, ["manifestId", "proposer", "judge", "subjects", "evidenceDigests"]);
20
+ for (const role of [claims.proposer, claims.judge])
21
+ keys(role, ["requested", "canonical"]);
22
+ keys(claims.subjects, manifest.arms.map(a => a.id));
23
+ keys(claims.evidenceDigests, manifest.arms.map(a => a.id));
24
+ for (const role of Object.values(claims.subjects))
25
+ keys(role, ["requested", "canonical"]);
26
+ }
27
+ /** Explicit private retention. Host role declarations are NOT authentication. No provider is called. */
28
+ export function retainBlindIntervention(root, manifest, evidence, qualification, author) {
29
+ authorValid(author);
30
+ const descriptors = Object.getOwnPropertyDescriptors(qualification);
31
+ if (Reflect.ownKeys(descriptors).some(k => typeof k !== "string" || !descriptors[k].enumerable || !Object.hasOwn(descriptors[k], "value")))
32
+ throw new Error("plain qualification required");
33
+ keys(qualification, ["manifestId", "proposer", "judge", "subjects", "evidenceDigests", "artifacts"]);
34
+ const { artifacts: artifactDescriptor, ...rest } = descriptors;
35
+ const claims = copy(Object.fromEntries(Object.entries(rest).map(([k, d]) => [k, d.value])));
36
+ const stableManifest = copy(manifest), stableEvidence = copy(evidence);
37
+ claimsValid(claims, stableManifest);
38
+ if (!(artifactDescriptor.value instanceof Map))
39
+ throw new Error("retained artifact map required");
40
+ const artifacts = new Map();
41
+ let total = 0;
42
+ for (const [hash, bytes] of Map.prototype.entries.call(artifactDescriptor.value)) {
43
+ if (typeof hash !== "string" || !SHA.test(hash) || !(bytes instanceof Uint8Array) || bytes.byteLength > 8 * 1024 * 1024 || artifacts.size >= 4096 || (total += bytes.byteLength) > 64 * 1024 * 1024)
44
+ throw new Error("bounded blind artifacts required");
45
+ const stable = Buffer.from(bytes);
46
+ if (createHash("sha256").update(stable).digest("hex") !== hash)
47
+ throw new Error("blind artifact digest mismatch");
48
+ artifacts.set(hash, stable);
49
+ }
50
+ const assessment = assessIntervention(stableManifest, stableEvidence, { ...claims, artifacts });
51
+ const seed = randomBytes(32).toString("hex");
52
+ createBlindComparison(stableManifest, assessment, seed); // Refuse incomplete inputs BEFORE writes.
53
+ const referenced = new Set(stableEvidence.flatMap(e => e.artifactDigests));
54
+ if ([...artifacts.keys()].some(h => !referenced.has(h)))
55
+ throw new Error("unreferenced blind artifact");
56
+ const refs = [...artifacts].sort(([a], [b]) => a.localeCompare(b)).map(([hash, bytes]) => ({ hash,
57
+ manifestId: retainArchiveSource(root, { sourceId: `blind-artifact-${hash}`, parser: { id: "blind-artifact", version: "1" }, retention: "exact", bytes }).manifestId }));
58
+ const bundle = { version: "retained-blind-intervention-v1", manifest: stableManifest, evidence: stableEvidence, qualification: claims, artifacts: refs, seed, author };
59
+ return retainArchiveSource(root, { sourceId: `blind-${stableManifest.id}`, parser: { id: "blind-intervention", version: "1" }, retention: "exact", bytes: Buffer.from(encode(bundle)) }).manifestId;
60
+ }
61
+ function load(root, id, author) {
62
+ authorValid(author);
63
+ const source = readArchiveSource(root, id);
64
+ if (source.status !== "available" || source.reference.retention !== "exact" || source.reference.parser.id !== "blind-intervention" || source.reference.parser.version !== "1")
65
+ throw new Error("blind input unavailable");
66
+ let bundle;
67
+ try {
68
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(source.bytes);
69
+ bundle = JSON.parse(text);
70
+ if (encode(bundle) !== text)
71
+ throw new Error();
72
+ }
73
+ catch {
74
+ throw new Error("invalid blind input JSON");
75
+ }
76
+ keys(bundle, ["version", "manifest", "evidence", "qualification", "artifacts", "seed", "author"]);
77
+ if (bundle.version !== "retained-blind-intervention-v1" || bundle.author !== author)
78
+ throw new Error("blind author or version mismatch");
79
+ claimsValid(bundle.qualification, bundle.manifest);
80
+ if (!Array.isArray(bundle.artifacts) || bundle.artifacts.length > 4096)
81
+ throw new Error("invalid blind artifacts");
82
+ const artifacts = new Map();
83
+ let total = 0;
84
+ for (const ref of bundle.artifacts) {
85
+ keys(ref, ["hash", "manifestId"]);
86
+ if (!SHA.test(ref.hash) || artifacts.has(ref.hash))
87
+ throw new Error("invalid blind artifact identity");
88
+ const artifact = readArchiveSource(root, ref.manifestId);
89
+ if (artifact.status !== "available" || artifact.reference.retention !== "exact" || artifact.reference.parser.id !== "blind-artifact" || artifact.reference.parser.version !== "1" || artifact.reference.sha256 !== ref.hash || (total += artifact.bytes.length) > 64 * 1024 * 1024)
90
+ throw new Error("blind artifact unavailable");
91
+ artifacts.set(ref.hash, artifact.bytes);
92
+ }
93
+ return createBlindComparison(bundle.manifest, assessIntervention(bundle.manifest, bundle.evidence, { ...bundle.qualification, artifacts }), bundle.seed);
94
+ }
95
+ function directory(path) {
96
+ const s = lstatSync(path);
97
+ if (!s.isDirectory() || s.isSymbolicLink() || (s.mode & 0o077) || (process.getuid && s.uid !== process.getuid()))
98
+ throw new Error("private blind directory required");
99
+ }
100
+ function syncDirectory(path) {
101
+ const fd = openSync(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
102
+ try {
103
+ fsyncSync(fd);
104
+ }
105
+ finally {
106
+ closeSync(fd);
107
+ }
108
+ }
109
+ function readChoice(root, id) {
110
+ const parent = join(root, "blind-decisions"), dir = join(parent, id);
111
+ try {
112
+ directory(parent);
113
+ directory(dir);
114
+ }
115
+ catch (e) {
116
+ if (missing(e))
117
+ return null;
118
+ throw e;
119
+ }
120
+ let fd;
121
+ try {
122
+ fd = openSync(join(dir, "choice.json"), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
123
+ }
124
+ catch (e) {
125
+ if (missing(e))
126
+ throw new Error("blind choice incomplete; explicit recovery required");
127
+ throw e;
128
+ }
129
+ try {
130
+ const s = fstatSync(fd);
131
+ if (!s.isFile() || s.nlink !== 1 || (s.mode & 0o077) || (process.getuid && s.uid !== process.getuid()) || s.size > 16384)
132
+ throw new Error("invalid blind choice file");
133
+ const bytes = Buffer.alloc(16385);
134
+ let used = 0;
135
+ while (used < bytes.length) {
136
+ const n = readSync(fd, bytes, used, bytes.length - used, used);
137
+ if (!n)
138
+ break;
139
+ used += n;
140
+ }
141
+ if (used > 16384)
142
+ throw new Error("invalid blind choice bound");
143
+ try {
144
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, used));
145
+ const value = JSON.parse(text);
146
+ if (encode(value) !== text)
147
+ throw new Error();
148
+ return value;
149
+ }
150
+ catch {
151
+ throw new Error("invalid blind choice JSON; explicit recovery required");
152
+ }
153
+ }
154
+ finally {
155
+ closeSync(fd);
156
+ }
157
+ }
158
+ /** One immutable quality choice per comparison. Reopening cannot reset the reveal gate. */
159
+ export function openBlindIntervention(root, id, author) {
160
+ if (!SHA.test(id))
161
+ throw new Error("invalid blind comparison identity");
162
+ const current = () => load(root, id, author);
163
+ return Object.freeze({
164
+ view: () => current().view(),
165
+ quality() { const blind = current(), choice = readChoice(root, id); return choice ? blind.choose(choice) : null; },
166
+ readArtifact: (label, hash) => current().readArtifact(label, hash),
167
+ choose(input) {
168
+ const blind = current(), choice = blind.choose(input);
169
+ const before = readChoice(root, id);
170
+ if (before) {
171
+ blind.choose(before);
172
+ if (encode(before) !== encode(choice))
173
+ throw new Error("blind quality choice locked");
174
+ return before;
175
+ }
176
+ const parent = join(root, "blind-decisions"), dir = join(parent, id);
177
+ try {
178
+ mkdirSync(parent, { mode: 0o700 });
179
+ }
180
+ catch (e) {
181
+ if (e.code !== "EEXIST")
182
+ throw e;
183
+ }
184
+ directory(parent);
185
+ // Atomic mkdir is the single-writer claim. Never remove or recover an incomplete claim automatically.
186
+ mkdirSync(dir, { mode: 0o700 });
187
+ const fd = openSync(join(dir, "choice.json"), constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
188
+ try {
189
+ const data = Buffer.from(encode(choice));
190
+ let offset = 0;
191
+ while (offset < data.length) {
192
+ const n = writeSync(fd, data, offset, data.length - offset);
193
+ if (!n)
194
+ throw new Error("blind choice write stalled");
195
+ offset += n;
196
+ }
197
+ fsyncSync(fd);
198
+ }
199
+ finally {
200
+ closeSync(fd);
201
+ }
202
+ syncDirectory(dir);
203
+ syncDirectory(parent);
204
+ syncDirectory(root);
205
+ return choice;
206
+ },
207
+ reveal() { const blind = current(), choice = readChoice(root, id); if (!choice)
208
+ throw new Error("durable quality choice required before reveal"); blind.choose(choice); return blind.reveal(); },
209
+ });
210
+ }
211
+ //# sourceMappingURL=blind-intervention.js.map
@@ -33,6 +33,8 @@ type Schema = Record<string, unknown>;
33
33
  * a keyword fails loudly rather than validating less than it claims.
34
34
  */
35
35
  export declare function assertSupportedSchema(schema: unknown, label: string, path?: string): void;
36
+ /** V3-only evaluator profile; the frozen v2 profile above remains unchanged. */
37
+ export declare function assertSupportedSchemaV3(schema: unknown, label: string, path?: string): void;
36
38
  /**
37
39
  * Validate `value` against `schema`. Returns every violation found; an empty
38
40
  * array means the instance satisfies the pinned contract.
@@ -45,6 +47,10 @@ export declare function assertSupportedSchema(schema: unknown, label: string, pa
45
47
  export declare function validateClosedSchema(schema: Schema, value: unknown, options?: {
46
48
  knownFieldNames?: ReadonlySet<string>;
47
49
  }): SchemaViolation[];
50
+ /** Evaluate the additional closed constructs used by the separately pinned v3 schema. */
51
+ export declare function validateClosedSchemaV3(schema: Schema, value: unknown, options?: {
52
+ knownFieldNames?: ReadonlySet<string>;
53
+ }): SchemaViolation[];
48
54
  /** Every property name declared anywhere in the document — safe to echo. */
49
55
  export declare function declaredPropertyNames(schema: unknown): Set<string>;
50
56
  export {};
@@ -31,6 +31,11 @@ const SUPPORTED_KEYWORDS = new Set([
31
31
  // value constraints
32
32
  "enum", "const", "minLength", "maxLength", "minimum", "pattern", "format",
33
33
  ]);
34
+ /** Ledger v3 adds only closed composition/conditionals and property-name schemas. */
35
+ const V3_SUPPORTED_KEYWORDS = new Set([
36
+ ...SUPPORTED_KEYWORDS,
37
+ "allOf", "anyOf", "if", "then", "propertyNames", "minItems", "maxItems",
38
+ ]);
34
39
  /**
35
40
  * The shape each supported keyword must have. A keyword whose *name* is known but
36
41
  * whose *value* has an unexpected shape is the same hole as an unknown keyword:
@@ -40,6 +45,11 @@ const SUPPORTED_KEYWORDS = new Set([
40
45
  const KEYWORD_SHAPES = {
41
46
  $ref: { check: (value) => typeof value === "string", expected: "a string" },
42
47
  oneOf: { check: (value) => Array.isArray(value) && value.length > 0, expected: "a non-empty array" },
48
+ allOf: { check: (value) => Array.isArray(value) && value.length > 0, expected: "a non-empty array" },
49
+ anyOf: { check: (value) => Array.isArray(value) && value.length > 0, expected: "a non-empty array" },
50
+ if: { check: (value) => isSchemaObject(value), expected: "a schema object" },
51
+ then: { check: (value) => isSchemaObject(value), expected: "a schema object" },
52
+ propertyNames: { check: (value) => isSchemaObject(value), expected: "a schema object" },
43
53
  type: { check: (value) => typeof value === "string" || (Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === "string")), expected: "a string or array of strings" },
44
54
  properties: { check: (value) => isSchemaObject(value), expected: "an object" },
45
55
  required: { check: (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string"), expected: "an array of strings" },
@@ -49,6 +59,8 @@ const KEYWORD_SHAPES = {
49
59
  minLength: { check: (value) => typeof value === "number", expected: "a number" },
50
60
  maxLength: { check: (value) => typeof value === "number", expected: "a number" },
51
61
  minimum: { check: (value) => typeof value === "number", expected: "a number" },
62
+ minItems: { check: (value) => Number.isInteger(value) && Number(value) >= 0, expected: "a non-negative integer" },
63
+ maxItems: { check: (value) => Number.isInteger(value) && Number(value) >= 0, expected: "a non-negative integer" },
52
64
  pattern: { check: (value) => typeof value === "string", expected: "a string" },
53
65
  format: { check: (value) => typeof value === "string", expected: "a string" },
54
66
  // `const` may legitimately be any JSON value, including null.
@@ -61,12 +73,20 @@ const SUPPORTED_TYPES = new Set(["object", "array", "string", "number", "integer
61
73
  * a keyword fails loudly rather than validating less than it claims.
62
74
  */
63
75
  export function assertSupportedSchema(schema, label, path = "#") {
76
+ assertSchemaSupported(schema, label, path, false);
77
+ }
78
+ /** V3-only evaluator profile; the frozen v2 profile above remains unchanged. */
79
+ export function assertSupportedSchemaV3(schema, label, path = "#") {
80
+ assertSchemaSupported(schema, label, path, true);
81
+ }
82
+ function assertSchemaSupported(schema, label, path, v3) {
64
83
  if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
65
84
  throw new Error(`${label} is not a JSON Schema object at ${path}`);
66
85
  }
67
86
  const node = schema;
87
+ const supported = v3 ? V3_SUPPORTED_KEYWORDS : SUPPORTED_KEYWORDS;
68
88
  for (const keyword of Object.keys(node)) {
69
- if (!SUPPORTED_KEYWORDS.has(keyword)) {
89
+ if (!supported.has(keyword)) {
70
90
  throw new Error(`${label} uses unsupported JSON Schema keyword \`${keyword}\` at ${path}; the closed-contract evaluator refuses to validate less than the schema declares`);
71
91
  }
72
92
  const shape = KEYWORD_SHAPES[keyword];
@@ -94,15 +114,20 @@ export function assertSupportedSchema(schema, label, path = "#") {
94
114
  }
95
115
  }
96
116
  for (const [name, entry] of Object.entries(object(node.$defs) ?? {}))
97
- assertSupportedSchema(entry, label, `${path}/$defs/${name}`);
98
- for (const [index, entry] of (Array.isArray(node.oneOf) ? node.oneOf : []).entries())
99
- assertSupportedSchema(entry, label, `${path}/oneOf/${index}`);
117
+ assertSchemaSupported(entry, label, `${path}/$defs/${name}`, v3);
118
+ for (const keyword of ["oneOf", "allOf", "anyOf"]) {
119
+ for (const [index, entry] of (Array.isArray(node[keyword]) ? node[keyword] : []).entries()) {
120
+ assertSchemaSupported(entry, label, `${path}/${keyword}/${index}`, v3);
121
+ }
122
+ }
100
123
  for (const [name, entry] of Object.entries(object(node.properties) ?? {}))
101
- assertSupportedSchema(entry, label, `${path}/properties/${name}`);
102
- if (node.items !== undefined)
103
- assertSupportedSchema(node.items, label, `${path}/items`);
124
+ assertSchemaSupported(entry, label, `${path}/properties/${name}`, v3);
125
+ for (const keyword of ["items", "propertyNames", "if", "then"]) {
126
+ if (node[keyword] !== undefined)
127
+ assertSchemaSupported(node[keyword], label, `${path}/${keyword}`, v3);
128
+ }
104
129
  if (node.additionalProperties !== undefined && node.additionalProperties !== false && node.additionalProperties !== true) {
105
- assertSupportedSchema(node.additionalProperties, label, `${path}/additionalProperties`);
130
+ assertSchemaSupported(node.additionalProperties, label, `${path}/additionalProperties`, v3);
106
131
  }
107
132
  }
108
133
  /**
@@ -117,6 +142,10 @@ export function assertSupportedSchema(schema, label, path = "#") {
117
142
  export function validateClosedSchema(schema, value, options = {}) {
118
143
  return validate(schema, schema, value, "", options.knownFieldNames ?? new Set());
119
144
  }
145
+ /** Evaluate the additional closed constructs used by the separately pinned v3 schema. */
146
+ export function validateClosedSchemaV3(schema, value, options = {}) {
147
+ return validate(schema, schema, value, "", options.knownFieldNames ?? new Set());
148
+ }
120
149
  /** Every property name declared anywhere in the document — safe to echo. */
121
150
  export function declaredPropertyNames(schema) {
122
151
  const names = new Set();
@@ -130,10 +159,13 @@ export function declaredPropertyNames(schema) {
130
159
  }
131
160
  for (const entry of Object.values(object(current.$defs) ?? {}))
132
161
  walk(entry);
133
- for (const entry of Array.isArray(current.oneOf) ? current.oneOf : [])
134
- walk(entry);
135
- if (current.items !== undefined)
136
- walk(current.items);
162
+ for (const keyword of ["oneOf", "allOf", "anyOf"]) {
163
+ for (const entry of Array.isArray(current[keyword]) ? current[keyword] : [])
164
+ walk(entry);
165
+ }
166
+ for (const keyword of ["items", "propertyNames", "if", "then"])
167
+ if (current[keyword] !== undefined)
168
+ walk(current[keyword]);
137
169
  if (current.additionalProperties && typeof current.additionalProperties === "object")
138
170
  walk(current.additionalProperties);
139
171
  };
@@ -146,6 +178,18 @@ function validate(root, schema, value, path, known) {
146
178
  return validate(root, resolved, value, path, known);
147
179
  }
148
180
  const violations = [];
181
+ if (Array.isArray(schema.allOf)) {
182
+ for (const branch of schema.allOf)
183
+ violations.push(...validate(root, branch, value, path, known));
184
+ }
185
+ if (Array.isArray(schema.anyOf)) {
186
+ const branches = schema.anyOf.map((branch) => validate(root, branch, value, path, known));
187
+ if (!branches.some((branch) => branch.length === 0))
188
+ violations.push(...bestBranch(root, schema.anyOf, branches, value, path));
189
+ }
190
+ if (schema.if !== undefined && validate(root, schema.if, value, path, known).length === 0 && schema.then !== undefined) {
191
+ violations.push(...validate(root, schema.then, value, path, known));
192
+ }
149
193
  const types = typeList(schema);
150
194
  if (types.length && !types.some((type) => matchesType(type, value))) {
151
195
  return [{ path, message: `must be ${describeTypes(types)}` }];
@@ -169,8 +213,16 @@ function validate(root, schema, value, path, known) {
169
213
  violations.push(...validateString(schema, value, path));
170
214
  if (typeof value === "number")
171
215
  violations.push(...validateNumber(schema, value, path));
172
- if (Array.isArray(value) && schema.items !== undefined) {
173
- value.forEach((entry, index) => violations.push(...validate(root, schema.items, entry, `${path}[${index}]`, known)));
216
+ if (Array.isArray(value)) {
217
+ if (typeof schema.minItems === "number" && value.length < schema.minItems) {
218
+ violations.push({ path, message: `must contain at least ${schema.minItems} item(s)` });
219
+ }
220
+ if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
221
+ violations.push({ path, message: `must contain at most ${schema.maxItems} item(s)` });
222
+ }
223
+ if (schema.items !== undefined) {
224
+ value.forEach((entry, index) => violations.push(...validate(root, schema.items, entry, `${path}[${index}]`, known)));
225
+ }
174
226
  }
175
227
  const record = object(value);
176
228
  if (record)
@@ -180,6 +232,10 @@ function validate(root, schema, value, path, known) {
180
232
  function validateObject(root, schema, record, path, known) {
181
233
  const violations = [];
182
234
  const properties = object(schema.properties) ?? {};
235
+ if (schema.propertyNames !== undefined) {
236
+ for (const name of Object.keys(record))
237
+ violations.push(...validate(root, schema.propertyNames, name, path, known));
238
+ }
183
239
  for (const name of (Array.isArray(schema.required) ? schema.required : [])) {
184
240
  if (!Object.hasOwn(record, name))
185
241
  violations.push({ path: child(path, name), message: "is required" });
@@ -0,0 +1,14 @@
1
+ import type { CodexSdkBinding } from './codex-sdk-transport.js';
2
+ export interface CodexDiagnosticModelPin {
3
+ path: string;
4
+ sha256: string;
5
+ }
6
+ /** Local catalogue declaration only, NOT canonical resolution or account entitlement. */
7
+ export declare function validateCodexDiagnosticModel(raw: unknown): CodexSdkBinding['model'];
8
+ export declare function loadCodexDiagnosticModel(pin: CodexDiagnosticModelPin): {
9
+ [key: string]: unknown;
10
+ id: string;
11
+ provider: string;
12
+ api: string;
13
+ baseUrl: string;
14
+ };
@@ -0,0 +1,39 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { isAbsolute } from 'node:path';
3
+ import { learningCopy, learningFile } from './learning-journal.js';
4
+ const closed = (v, keys) => { if (!v || typeof v !== 'object' || Array.isArray(v) || Object.keys(v).sort().join() !== keys.sort().join())
5
+ throw Error('closed diagnostic model definition required'); };
6
+ /** Local catalogue declaration only, NOT canonical resolution or account entitlement. */
7
+ export function validateCodexDiagnosticModel(raw) {
8
+ const m = learningCopy(raw);
9
+ closed(m, ['id', 'name', 'provider', 'api', 'baseUrl', 'reasoning', 'input', 'cost', 'contextWindow', 'maxTokens', 'thinkingLevelMap', 'compat']);
10
+ if (m.id !== 'gpt-6-astra' || m.name !== 'GPT-6 Astra' || m.provider !== 'openai-codex' || m.api !== 'openai-codex-responses' || m.baseUrl !== 'https://chatgpt.com/backend-api' || m.reasoning !== true || JSON.stringify(m.input) !== '["text","image"]' || ![m.contextWindow, m.maxTokens].every(n => Number.isSafeInteger(n) && n > 0))
11
+ throw Error('diagnostic model identity/capability mismatch');
12
+ closed(m.thinkingLevelMap, ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
13
+ if (m.thinkingLevelMap.off !== null || m.thinkingLevelMap.minimal !== 'low' || ['low', 'medium', 'high', 'xhigh', 'max'].some(k => m.thinkingLevelMap[k] !== k))
14
+ throw Error('diagnostic effort mapping mismatch');
15
+ closed(m.compat, ['supportsOpenAIGrammarTools', 'supportsAdditionalTools', 'supportsToolSearch']);
16
+ if (!Object.values(m.compat).every(v => typeof v === 'boolean'))
17
+ throw Error('diagnostic compatibility mismatch');
18
+ const prices = ['input', 'output', 'cacheRead', 'cacheWrite'];
19
+ closed(m.cost, [...prices, 'tiers']);
20
+ if (!Array.isArray(m.cost.tiers) || m.cost.tiers.length > 8)
21
+ throw Error('diagnostic cost declaration');
22
+ for (const [i, cost] of [m.cost, ...m.cost.tiers].entries()) {
23
+ if (i)
24
+ closed(cost, ['inputTokensAbove', ...prices]);
25
+ if (![...prices, ...(i ? ['inputTokensAbove'] : [])].every(k => typeof cost[k] === 'number' && Number.isFinite(cost[k]) && cost[k] >= 0))
26
+ throw Error('diagnostic cost declaration');
27
+ }
28
+ return m;
29
+ }
30
+ export function loadCodexDiagnosticModel(pin) {
31
+ closed(pin, ['path', 'sha256']);
32
+ if (typeof pin.path !== 'string' || !isAbsolute(pin.path) || !/^[a-f0-9]{64}$/.test(pin.sha256))
33
+ throw Error('diagnostic definition pin required');
34
+ const bytes = learningFile(pin.path, 8192);
35
+ if (createHash('sha256').update(bytes).digest('hex') !== pin.sha256)
36
+ throw Error('diagnostic definition source mismatch');
37
+ return validateCodexDiagnosticModel(JSON.parse(bytes.toString('utf8')));
38
+ }
39
+ //# sourceMappingURL=codex-diagnostic-model.js.map
@@ -0,0 +1,111 @@
1
+ import type { Readable, Writable } from 'node:stream';
2
+ import { type CodexSdkBinding, type CodexWireTransport } from './codex-sdk-transport.js';
3
+ /** LOCAL host protocol with an inert installed-SDK seam; no auth loader or live network switch.
4
+ * The trusted host owns this module and transport streams; the subject gets framed IPC only.
5
+ * Stream finish proves a host-side write, NOT provider receipt, backend identity or token limits.
6
+ * Journal integrity assumes a trusted cooperative host/filesystem, not hostile same-UID rollback. */
7
+ export declare const CODEX_SUBSCRIPTION_DESTINATION = "https://chatgpt.com/backend-api/codex/responses";
8
+ type Role = 'proposer' | 'subject' | 'judge';
9
+ export interface LocalCodexInvocation {
10
+ id: string;
11
+ role: Role;
12
+ model: string;
13
+ effort: 'low' | 'medium' | 'high';
14
+ instructions: string;
15
+ input: string;
16
+ expectedSha256: string;
17
+ subjectId: string | null;
18
+ }
19
+ export interface LocalCodexHostSpec {
20
+ version: 'codex-host-local-v1';
21
+ maxCalls: number;
22
+ wallMs: number;
23
+ invocations: LocalCodexInvocation[];
24
+ }
25
+ export interface SubscriptionReservations {
26
+ charterSha256: string;
27
+ mode: 'fixture' | 'subscription-live';
28
+ requestBytes: number;
29
+ responseBytes: number;
30
+ totalRequestBytes: number;
31
+ totalResponseBytes: number;
32
+ callMs: number;
33
+ }
34
+ export interface LocalCodexObservation {
35
+ id: string;
36
+ sequence: number;
37
+ requestBody: string;
38
+ requestSha256: string;
39
+ outputBase64: string;
40
+ outputSha256: string;
41
+ objective: 'PASS' | 'FAIL';
42
+ providerInternalFacts: null;
43
+ liveQualified: false;
44
+ }
45
+ /** This checks supplied policy consistency, never authenticates its source or invents lineage. */
46
+ export declare function inspectCodexRoleSeparation(raw: Array<{
47
+ role: Role;
48
+ model: string;
49
+ canonical: string | null;
50
+ lineage: string | null;
51
+ }>): {
52
+ state: "BLOCKED";
53
+ reason: string;
54
+ liveQualified: false;
55
+ } | {
56
+ state: "CONSISTENT_DECLARATION_ONLY";
57
+ reason: string;
58
+ disclosures: string[];
59
+ liveQualified: false;
60
+ };
61
+ export declare function createLocalCodexHost(path: string, input: LocalCodexHostSpec, rolePolicy?: Parameters<typeof inspectCodexRoleSeparation>[0], subscription?: SubscriptionReservations): {
62
+ inspect: () => {
63
+ calls: number;
64
+ complete: boolean;
65
+ aborted: boolean;
66
+ liveQualified: false;
67
+ };
68
+ exchangeSubscriptionSdk(subjectFrames: Readable, binding: CodexSdkBinding, transport: CodexWireTransport, charterSha256: string, sourceSignal?: AbortSignal): Promise<LocalCodexObservation>;
69
+ exchangeSdk(subjectFrames: Readable, binding: CodexSdkBinding, fixture: () => Promise<Response>): Promise<LocalCodexObservation>;
70
+ exchange(subjectFrames: Readable, hostTransport: Writable, hostResponse: Readable, subscriptionMode?: boolean, sourceSignal?: AbortSignal): Promise<LocalCodexObservation>;
71
+ panel(subjectId: string, judgeIds: string[], rawPolicy: Parameters<typeof inspectCodexRoleSeparation>[0]): {
72
+ label: string;
73
+ collapse: import("@skill-harness/core").VotePanelCollapse;
74
+ identityStatus: "CONSISTENT_DECLARATION_ONLY";
75
+ disclosures: string[];
76
+ liveQualified: false;
77
+ routingDefault: null;
78
+ };
79
+ eligibleOutputs(): {
80
+ label: string;
81
+ outputBase64: string;
82
+ outputSha256: string;
83
+ liveQualified: false;
84
+ }[];
85
+ };
86
+ export declare function openLocalCodexHost(path: string): {
87
+ inspect: () => {
88
+ calls: number;
89
+ complete: boolean;
90
+ aborted: boolean;
91
+ liveQualified: false;
92
+ };
93
+ exchangeSubscriptionSdk(subjectFrames: Readable, binding: CodexSdkBinding, transport: CodexWireTransport, charterSha256: string, sourceSignal?: AbortSignal): Promise<LocalCodexObservation>;
94
+ exchangeSdk(subjectFrames: Readable, binding: CodexSdkBinding, fixture: () => Promise<Response>): Promise<LocalCodexObservation>;
95
+ exchange(subjectFrames: Readable, hostTransport: Writable, hostResponse: Readable, subscriptionMode?: boolean, sourceSignal?: AbortSignal): Promise<LocalCodexObservation>;
96
+ panel(subjectId: string, judgeIds: string[], rawPolicy: Parameters<typeof inspectCodexRoleSeparation>[0]): {
97
+ label: string;
98
+ collapse: import("@skill-harness/core").VotePanelCollapse;
99
+ identityStatus: "CONSISTENT_DECLARATION_ONLY";
100
+ disclosures: string[];
101
+ liveQualified: false;
102
+ routingDefault: null;
103
+ };
104
+ eligibleOutputs(): {
105
+ label: string;
106
+ outputBase64: string;
107
+ outputSha256: string;
108
+ liveQualified: false;
109
+ }[];
110
+ };
111
+ export {};