@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.
- package/README.md +2 -1
- package/dist/archive-access.d.ts +58 -0
- package/dist/archive-access.js +61 -0
- package/dist/archive-checkpoint.d.ts +47 -0
- package/dist/archive-checkpoint.js +116 -0
- package/dist/archive-facts.d.ts +19 -0
- package/dist/archive-facts.js +20 -0
- package/dist/archive-observer.d.ts +21 -0
- package/dist/archive-observer.js +79 -0
- package/dist/archive-policy.d.ts +107 -0
- package/dist/archive-policy.js +135 -0
- package/dist/archive-read-capability.d.ts +49 -0
- package/dist/archive-read-capability.js +45 -0
- package/dist/archive-retention-policy.d.ts +52 -0
- package/dist/archive-retention-policy.js +49 -0
- package/dist/archived-work.d.ts +44 -0
- package/dist/archived-work.js +59 -0
- package/dist/blind-intervention.d.ts +29 -0
- package/dist/blind-intervention.js +211 -0
- package/dist/closed-schema.d.ts +6 -0
- package/dist/closed-schema.js +70 -14
- package/dist/codex-diagnostic-model.d.ts +14 -0
- package/dist/codex-diagnostic-model.js +39 -0
- package/dist/codex-host-observer.d.ts +111 -0
- package/dist/codex-host-observer.js +309 -0
- package/dist/codex-https.d.ts +7 -0
- package/dist/codex-https.js +221 -0
- package/dist/codex-model-profile.d.ts +38 -0
- package/dist/codex-model-profile.js +54 -0
- package/dist/codex-producer-ipc.d.ts +55 -0
- package/dist/codex-producer-ipc.js +67 -0
- package/dist/codex-sdk-transport.d.ts +50 -0
- package/dist/codex-sdk-transport.js +294 -0
- package/dist/codex-subscription.d.ts +114 -0
- package/dist/codex-subscription.js +268 -0
- package/dist/evidence-archive.d.ts +57 -0
- package/dist/evidence-archive.js +221 -0
- package/dist/execution-projection-schema.d.ts +10 -0
- package/dist/execution-projection-schema.js +30 -0
- package/dist/execution-retention-archive.d.ts +69 -0
- package/dist/execution-retention-archive.js +154 -0
- package/dist/generated/retention-v2-contract.d.ts +72 -0
- package/dist/generated/retention-v2-contract.js +105 -0
- package/dist/generated/retention-v2-json.d.ts +24 -0
- package/dist/generated/retention-v2-json.js +268 -0
- package/dist/generated/retention-v2-native-json.d.ts +2 -0
- package/dist/generated/retention-v2-native-json.js +33 -0
- package/dist/generated/retention-v2-native.d.ts +58 -0
- package/dist/generated/retention-v2-native.js +155 -0
- package/dist/generated/retention-v2-pin.d.ts +2 -0
- package/dist/generated/retention-v2-pin.js +4 -0
- package/dist/generated/retention-v2-types.d.ts +49 -0
- package/dist/generated/retention-v2-types.js +2 -0
- package/dist/generated/retention-v2-work-types.d.ts +260 -0
- package/dist/generated/retention-v2-work-types.js +13 -0
- package/dist/generated/work-v4/execution-id.d.ts +5 -0
- package/dist/generated/work-v4/execution-id.js +14 -0
- package/dist/generated/work-v4/json.d.ts +24 -0
- package/dist/generated/work-v4/json.js +268 -0
- package/dist/generated/work-v4/occurrences.d.ts +226 -0
- package/dist/generated/work-v4/occurrences.js +116 -0
- package/dist/generated/work-v4/pin.d.ts +1 -0
- package/dist/generated/work-v4/pin.js +3 -0
- package/dist/generated/work-v4/projection.d.ts +294 -0
- package/dist/generated/work-v4/projection.js +238 -0
- package/dist/generated/work-v4/reader.d.ts +24 -0
- package/dist/generated/work-v4/reader.js +109 -0
- package/dist/generated/work-v4/snapshot.d.ts +28 -0
- package/dist/generated/work-v4/snapshot.js +268 -0
- package/dist/generated/work-v4/types.d.ts +260 -0
- package/dist/generated/work-v4/types.js +13 -0
- package/dist/generated/work-v4/validation.d.ts +13 -0
- package/dist/generated/work-v4/validation.js +227 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +24 -0
- package/dist/installed-review-session.d.ts +28 -0
- package/dist/installed-review-session.js +166 -0
- package/dist/intervention-run.d.ts +403 -0
- package/dist/intervention-run.js +169 -0
- package/dist/learning-case.d.ts +12 -0
- package/dist/learning-case.js +15 -0
- package/dist/learning-journal.d.ts +18 -0
- package/dist/learning-journal.js +256 -0
- package/dist/pi-daddy-ledger-v2.d.ts +4 -4
- package/dist/pi-daddy-ledger-v2.js +9 -7
- package/dist/pi-daddy-ledger-v3.d.ts +683 -0
- package/dist/pi-daddy-ledger-v3.js +961 -0
- package/dist/pi.js +115 -24
- package/dist/principal-payload-port.d.ts +25 -0
- package/dist/principal-payload-port.js +40 -0
- package/dist/producer-product.d.ts +70 -0
- package/dist/producer-product.js +186 -0
- package/dist/producer-review.d.ts +53 -0
- package/dist/producer-review.js +207 -0
- package/dist/prompt-capture-extension.d.ts +10 -0
- package/dist/prompt-capture-extension.js +26 -0
- package/dist/prompt-provenance.d.ts +22 -0
- package/dist/prompt-provenance.js +146 -0
- package/dist/reviewed-archive-export.d.ts +42 -0
- package/dist/reviewed-archive-export.js +78 -0
- package/dist/trajectory.d.ts +15 -9
- package/dist/trajectory.js +375 -55
- package/dist/trust-lifecycle.d.ts +128 -0
- package/dist/trust-lifecycle.js +200 -0
- package/dist/weekly-investigation.d.ts +237 -0
- package/dist/weekly-investigation.js +330 -0
- package/dist/work-candidates.d.ts +29 -0
- package/dist/work-candidates.js +78 -0
- package/dist/work-case-archive.d.ts +25 -0
- package/dist/work-case-archive.js +63 -0
- package/dist/work-case-review.d.ts +45 -0
- package/dist/work-case-review.js +220 -0
- package/dist/work-signal-cases.d.ts +15 -0
- package/dist/work-signal-cases.js +54 -0
- package/dist/work-signal-observation.d.ts +23 -0
- package/dist/work-signal-observation.js +89 -0
- 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
|
package/dist/closed-schema.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/closed-schema.js
CHANGED
|
@@ -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 (!
|
|
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
|
-
|
|
98
|
-
for (const
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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)
|
|
173
|
-
|
|
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 {};
|