@bitkyc08/opencodex 2.19.0 → 2.21.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/gui/dist/assets/index-BOFeam5a.js +76 -0
- package/gui/dist/assets/index-Xq49CY8F.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +2 -28
- package/src/adapters/google.ts +31 -3
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat.ts +392 -40
- package/src/adapters/registry.ts +144 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/bridge.ts +15 -2
- package/src/chat/inbound.ts +13 -7
- package/src/claude/gateway-cache.ts +41 -4
- package/src/cli/claude.ts +3 -2
- package/src/cli/combo.ts +3 -0
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/export-command.ts +2 -2
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +3 -2
- package/src/cli/lab.ts +135 -1
- package/src/cli/minimax.ts +491 -0
- package/src/cli/models-runtime.ts +22 -1
- package/src/cli/models.ts +67 -2
- package/src/cli/opencode.ts +2 -1
- package/src/cli/registry.ts +22 -2
- package/src/clients/config-export.ts +125 -7
- package/src/codex/app-server-processes.ts +57 -2
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/catalog/aggregation.ts +10 -1
- package/src/codex/catalog/effort.ts +15 -3
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +45 -5
- package/src/codex/catalog/sync.ts +74 -4
- package/src/codex/convergence.ts +2 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/request.ts +30 -0
- package/src/combos/types.ts +6 -0
- package/src/config.ts +52 -0
- package/src/generated/compatibility-version.json +241 -85
- package/src/images/loop.ts +26 -6
- package/src/integrations/registry.ts +7 -0
- package/src/lab/conformance/jcs.ts +42 -2
- package/src/lab/conformance/negative-controls.ts +6 -2
- package/src/lab/conformance/runner.ts +16 -5
- package/src/lab/fabric/observe.ts +49 -14
- package/src/lab/index.ts +16 -0
- package/src/lab/ledger/purge.ts +152 -83
- package/src/lab/ledger/store.ts +168 -54
- package/src/lab/observe/from-conformance.ts +8 -6
- package/src/lab/observe/from-live.ts +8 -2
- package/src/lab/paths.ts +23 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +219 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/lab-activation.ts +109 -47
- package/src/lib/lab-live-pinned-sender.ts +16 -5
- package/src/lib/pinned-http.ts +70 -16
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/upstream-reachability.ts +4 -0
- package/src/lib/windows-elevation.ts +10 -1
- package/src/providers/derive.ts +24 -4
- package/src/providers/registry.ts +14 -4
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +143 -0
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/reasoning-effort.ts +27 -1
- package/src/responses/custom-tool-compat.ts +4 -1
- package/src/responses/parser.ts +7 -1
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/schema.ts +6 -0
- package/src/router.ts +23 -6
- package/src/routing/capability.ts +4 -2
- package/src/routing/compatibility/behavior.ts +5 -1
- package/src/server/adapter-resolve.ts +2 -32
- package/src/server/auth-cors.ts +50 -6
- package/src/server/chat-completions.ts +74 -36
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +371 -0
- package/src/server/management/combo-routes.ts +16 -2
- package/src/server/management/config-routes.ts +6 -4
- package/src/server/management/context.ts +17 -0
- package/src/server/management/lab-routes.ts +181 -19
- package/src/server/management/model-routes.ts +76 -2
- package/src/server/management/model-rows.ts +8 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +76 -4
- package/src/server/management/system-restart.ts +4 -2
- package/src/server/management/system-routes.ts +38 -0
- package/src/server/relay.ts +17 -3
- package/src/server/responses/compact.ts +4 -1
- package/src/server/responses/core.ts +257 -46
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/policy-fallback.ts +16 -2
- package/src/server/responses/terminal-guard.ts +1 -1
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses.ts +17 -2
- package/src/server/system-env.ts +1 -1
- package/src/types.ts +84 -5
- package/src/update/index.ts +6 -5
- package/src/update/job.ts +5 -6
- package/src/update/notify.ts +5 -3
- package/src/usage/log.ts +11 -1
- package/src/web-search/loop.ts +21 -5
- package/gui/dist/assets/index-CQ7bIKee.css +0 -1
- package/gui/dist/assets/index-D_JUZLEC.js +0 -76
package/src/lab/paths.ts
CHANGED
|
@@ -81,10 +81,25 @@ export function labScratchDir(configDir = getConfigDir()): string {
|
|
|
81
81
|
return join(labRoot(configDir), "scratch");
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/** Shared Lab export directory. Public evidence bundles intentionally live here too. */
|
|
84
85
|
export function labExportDir(configDir = getConfigDir()): string {
|
|
85
86
|
return join(labRoot(configDir), "export");
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
export function labCommunityDir(configDir = getConfigDir()): string {
|
|
90
|
+
return join(labRoot(configDir), "community");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function labPublicOriginDir(configDir = getConfigDir()): string {
|
|
94
|
+
return join(labRoot(configDir), "public-origin-v1");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const LAB_PUBLIC_PUBLISHER_KEY_FILE = "publisher-ed25519.pem";
|
|
98
|
+
|
|
99
|
+
export function labPublicPublisherKeyPath(configDir = getConfigDir()): string {
|
|
100
|
+
return join(labRoot(configDir), LAB_PUBLIC_PUBLISHER_KEY_FILE);
|
|
101
|
+
}
|
|
102
|
+
|
|
88
103
|
/** Opaque per-installation salt for local fingerprinting (never exported as evidence). */
|
|
89
104
|
export function labInstallationSaltPath(configDir = getConfigDir()): string {
|
|
90
105
|
return join(labRoot(configDir), "installation-salt.bin");
|
|
@@ -110,15 +125,21 @@ export function ensureLabDirs(configDir = getConfigDir()): {
|
|
|
110
125
|
artifactsDir: string;
|
|
111
126
|
scratchDir: string;
|
|
112
127
|
exportDir: string;
|
|
128
|
+
communityDir: string;
|
|
129
|
+
publicOriginDir: string;
|
|
113
130
|
} {
|
|
114
131
|
const root = labRoot(configDir);
|
|
115
132
|
const artifactsDir = labArtifactsDir(configDir);
|
|
116
133
|
const scratchDir = labScratchDir(configDir);
|
|
117
134
|
const exportDir = labExportDir(configDir);
|
|
135
|
+
const communityDir = labCommunityDir(configDir);
|
|
136
|
+
const publicOriginDir = labPublicOriginDir(configDir);
|
|
118
137
|
ensureRestrictedDir(root, root);
|
|
119
138
|
ensureRestrictedDir(artifactsDir, root);
|
|
120
139
|
ensureRestrictedDir(scratchDir, root);
|
|
121
140
|
ensureRestrictedDir(exportDir, root);
|
|
141
|
+
ensureRestrictedDir(communityDir, root);
|
|
142
|
+
ensureRestrictedDir(publicOriginDir, root);
|
|
122
143
|
return {
|
|
123
144
|
root,
|
|
124
145
|
ledgerPath: labLedgerPath(configDir),
|
|
@@ -126,5 +147,7 @@ export function ensureLabDirs(configDir = getConfigDir()): {
|
|
|
126
147
|
artifactsDir,
|
|
127
148
|
scratchDir,
|
|
128
149
|
exportDir,
|
|
150
|
+
communityDir,
|
|
151
|
+
publicOriginDir,
|
|
129
152
|
};
|
|
130
153
|
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { jcsStringify } from "../digest";
|
|
2
|
+
import { publicEvidenceId } from "./ids";
|
|
3
|
+
import {
|
|
4
|
+
PUBLIC_EVIDENCE_BUNDLE_SCHEMA_VERSION,
|
|
5
|
+
PUBLIC_EXPORT_POLICY_VERSION,
|
|
6
|
+
type PublicArtifactV1,
|
|
7
|
+
type PublicEvidenceBundleUnsignedV1,
|
|
8
|
+
type PublicEvidenceRecordV1,
|
|
9
|
+
type PublicPublisherV1,
|
|
10
|
+
} from "./types";
|
|
11
|
+
import { PublicEvidenceValidationError, validatePublicEvidenceRecord } from "./validate";
|
|
12
|
+
|
|
13
|
+
export const MAX_PUBLIC_BUNDLE_BYTES = 2 * 1024 * 1024;
|
|
14
|
+
export const MAX_PUBLIC_BUNDLE_RECORDS = 256;
|
|
15
|
+
export const MAX_PUBLIC_BUNDLE_ARTIFACTS = 16;
|
|
16
|
+
export const MAX_PUBLIC_ARTIFACT_BYTES = 256 * 1024;
|
|
17
|
+
export const MAX_PUBLIC_ARTIFACT_BYTES_TOTAL = 1024 * 1024;
|
|
18
|
+
|
|
19
|
+
export interface PublicEvidenceContentInput {
|
|
20
|
+
records: PublicEvidenceRecordV1[];
|
|
21
|
+
artifacts: PublicArtifactV1[];
|
|
22
|
+
createdDayUtc: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BuildPublicEvidenceBundleInput extends PublicEvidenceContentInput {
|
|
26
|
+
publisher: PublicPublisherV1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Deterministic, locale-independent code-unit ordering for canonical identity. */
|
|
30
|
+
function compareCanonicalId(a: string, b: string): number {
|
|
31
|
+
if (a < b) return -1;
|
|
32
|
+
if (a > b) return 1;
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function utcDay(value: string): string {
|
|
37
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
38
|
+
throw new PublicEvidenceValidationError("invalid_day", "createdDayUtc must be YYYY-MM-DD");
|
|
39
|
+
}
|
|
40
|
+
const parsed = new Date(`${value}T00:00:00.000Z`);
|
|
41
|
+
if (Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== value) {
|
|
42
|
+
throw new PublicEvidenceValidationError("invalid_day", "createdDayUtc must be a real UTC day");
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validatePublisher(publisher: PublicPublisherV1): PublicPublisherV1 {
|
|
48
|
+
const raw = publisher as unknown as Record<string, unknown>;
|
|
49
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
50
|
+
throw new PublicEvidenceValidationError("invalid_publisher", "publisher must be an object");
|
|
51
|
+
}
|
|
52
|
+
const keys = Object.keys(raw);
|
|
53
|
+
if (keys.some((key) => !["algorithm", "keyId", "publicKey"].includes(key))) {
|
|
54
|
+
throw new PublicEvidenceValidationError("unknown_field", "publisher contains unknown fields");
|
|
55
|
+
}
|
|
56
|
+
if (publisher.algorithm !== "ed25519") {
|
|
57
|
+
throw new PublicEvidenceValidationError("unsupported_algorithm", "publisher must use ed25519");
|
|
58
|
+
}
|
|
59
|
+
if (!/^[0-9a-f]{64}$/.test(publisher.keyId)) {
|
|
60
|
+
throw new PublicEvidenceValidationError("invalid_publisher", "publisher.keyId must be sha256 hex");
|
|
61
|
+
}
|
|
62
|
+
if (typeof publisher.publicKey !== "string" || publisher.publicKey.length === 0 || publisher.publicKey.length > 1024) {
|
|
63
|
+
throw new PublicEvidenceValidationError("invalid_publisher", "publisher.publicKey is invalid");
|
|
64
|
+
}
|
|
65
|
+
const publicKeyBytes = Buffer.from(publisher.publicKey, "base64");
|
|
66
|
+
if (publicKeyBytes.byteLength === 0 || publicKeyBytes.toString("base64") !== publisher.publicKey) {
|
|
67
|
+
throw new PublicEvidenceValidationError("invalid_publisher", "publisher.publicKey must use canonical base64");
|
|
68
|
+
}
|
|
69
|
+
const expectedKeyId = publicEvidenceId("publisher_key", {
|
|
70
|
+
algorithm: publisher.algorithm,
|
|
71
|
+
publicKey: publisher.publicKey,
|
|
72
|
+
});
|
|
73
|
+
if (publisher.keyId !== expectedKeyId) {
|
|
74
|
+
throw new PublicEvidenceValidationError("publisher_key_id_mismatch", "publisher.keyId does not match public key");
|
|
75
|
+
}
|
|
76
|
+
return { algorithm: "ed25519", keyId: publisher.keyId, publicKey: publisher.publicKey };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function validateArtifacts(artifacts: PublicArtifactV1[]): PublicArtifactV1[] {
|
|
80
|
+
if (!Array.isArray(artifacts) || artifacts.length > MAX_PUBLIC_BUNDLE_ARTIFACTS) {
|
|
81
|
+
throw new PublicEvidenceValidationError("array_too_large", `artifacts exceeds ${MAX_PUBLIC_BUNDLE_ARTIFACTS}`);
|
|
82
|
+
}
|
|
83
|
+
let aggregate = 0;
|
|
84
|
+
const ids = new Set<string>();
|
|
85
|
+
return artifacts.map((artifact, index) => {
|
|
86
|
+
const raw = artifact as unknown as Record<string, unknown>;
|
|
87
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
88
|
+
throw new PublicEvidenceValidationError("invalid_artifact", `artifacts[${index}] must be an object`);
|
|
89
|
+
}
|
|
90
|
+
if (Object.keys(raw).some((key) => !["artifactId", "artifactClass", "mediaType", "byteCount", "contentBase64"].includes(key))) {
|
|
91
|
+
throw new PublicEvidenceValidationError("unknown_field", `artifacts[${index}] contains unknown fields`);
|
|
92
|
+
}
|
|
93
|
+
if (!/^[0-9a-f]{64}$/.test(artifact.artifactId)) {
|
|
94
|
+
throw new PublicEvidenceValidationError("invalid_artifact", `artifacts[${index}].artifactId is invalid`);
|
|
95
|
+
}
|
|
96
|
+
if (typeof artifact.artifactClass !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:+-]{0,255}$/.test(artifact.artifactClass)) {
|
|
97
|
+
throw new PublicEvidenceValidationError("invalid_artifact", `artifacts[${index}].artifactClass is invalid`);
|
|
98
|
+
}
|
|
99
|
+
if (typeof artifact.mediaType !== "string" || artifact.mediaType.length === 0 || artifact.mediaType.length > 256) {
|
|
100
|
+
throw new PublicEvidenceValidationError("invalid_artifact", `artifacts[${index}].mediaType is invalid`);
|
|
101
|
+
}
|
|
102
|
+
if (!Number.isInteger(artifact.byteCount) || artifact.byteCount < 0 || artifact.byteCount > MAX_PUBLIC_ARTIFACT_BYTES) {
|
|
103
|
+
throw new PublicEvidenceValidationError("artifact_too_large", `artifacts[${index}].byteCount is invalid`);
|
|
104
|
+
}
|
|
105
|
+
let bytes: Buffer;
|
|
106
|
+
try {
|
|
107
|
+
bytes = Buffer.from(artifact.contentBase64, "base64");
|
|
108
|
+
} catch {
|
|
109
|
+
throw new PublicEvidenceValidationError("invalid_artifact", `artifacts[${index}].contentBase64 is invalid`);
|
|
110
|
+
}
|
|
111
|
+
if (bytes.byteLength !== artifact.byteCount || bytes.toString("base64") !== artifact.contentBase64) {
|
|
112
|
+
throw new PublicEvidenceValidationError("invalid_artifact", `artifacts[${index}] byte count or base64 is non-canonical`);
|
|
113
|
+
}
|
|
114
|
+
const expectedArtifactId = publicEvidenceId("artifact", {
|
|
115
|
+
artifactClass: artifact.artifactClass,
|
|
116
|
+
mediaType: artifact.mediaType,
|
|
117
|
+
byteCount: artifact.byteCount,
|
|
118
|
+
contentBase64: artifact.contentBase64,
|
|
119
|
+
});
|
|
120
|
+
if (artifact.artifactId !== expectedArtifactId) {
|
|
121
|
+
throw new PublicEvidenceValidationError("artifact_id_mismatch", `artifacts[${index}].artifactId mismatch`);
|
|
122
|
+
}
|
|
123
|
+
aggregate += artifact.byteCount;
|
|
124
|
+
if (aggregate > MAX_PUBLIC_ARTIFACT_BYTES_TOTAL) {
|
|
125
|
+
throw new PublicEvidenceValidationError("artifact_aggregate_too_large", "artifact aggregate exceeds 1 MiB");
|
|
126
|
+
}
|
|
127
|
+
if (ids.has(artifact.artifactId)) {
|
|
128
|
+
throw new PublicEvidenceValidationError("duplicate_id", "artifacts contains duplicate ids");
|
|
129
|
+
}
|
|
130
|
+
ids.add(artifact.artifactId);
|
|
131
|
+
return { ...artifact };
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Validate all publisher-independent bundle content before any signing-key state is touched. */
|
|
136
|
+
export function normalizePublicEvidenceContent(input: PublicEvidenceContentInput): PublicEvidenceContentInput {
|
|
137
|
+
if (!Array.isArray(input.records) || input.records.length > MAX_PUBLIC_BUNDLE_RECORDS) {
|
|
138
|
+
throw new PublicEvidenceValidationError("array_too_large", `records exceeds ${MAX_PUBLIC_BUNDLE_RECORDS}`);
|
|
139
|
+
}
|
|
140
|
+
const records = input.records
|
|
141
|
+
.map(validatePublicEvidenceRecord)
|
|
142
|
+
.sort((a, b) => compareCanonicalId(a.recordId, b.recordId));
|
|
143
|
+
if (new Set(records.map((record) => record.recordId)).size !== records.length) {
|
|
144
|
+
throw new PublicEvidenceValidationError("duplicate_id", "records contains duplicate ids");
|
|
145
|
+
}
|
|
146
|
+
const artifacts = validateArtifacts(input.artifacts)
|
|
147
|
+
.sort((a, b) => compareCanonicalId(a.artifactId, b.artifactId));
|
|
148
|
+
const artifactIds = new Set(artifacts.map((artifact) => artifact.artifactId));
|
|
149
|
+
for (const record of records) {
|
|
150
|
+
for (const artifactId of record.artifactRefs ?? []) {
|
|
151
|
+
if (!artifactIds.has(artifactId)) {
|
|
152
|
+
throw new PublicEvidenceValidationError("artifact_ref_missing", `record ${record.recordId} references a missing public artifact`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { records, artifacts, createdDayUtc: utcDay(input.createdDayUtc) };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function canonicalPublicEvidenceContent(
|
|
160
|
+
input: PublicEvidenceContentInput,
|
|
161
|
+
): { canonical: boolean; normalized: PublicEvidenceContentInput } {
|
|
162
|
+
const normalized = normalizePublicEvidenceContent(input);
|
|
163
|
+
const canonical = input.records.length === normalized.records.length
|
|
164
|
+
&& input.artifacts.length === normalized.artifacts.length
|
|
165
|
+
&& input.records.every((record, index) => record.recordId === normalized.records[index]!.recordId)
|
|
166
|
+
&& input.artifacts.every((artifact, index) => artifact.artifactId === normalized.artifacts[index]!.artifactId);
|
|
167
|
+
return { canonical, normalized };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function hasCanonicalPublicEvidenceOrder(input: PublicEvidenceContentInput): boolean {
|
|
171
|
+
return canonicalPublicEvidenceContent(input).canonical;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function buildFromNormalizedContent(
|
|
175
|
+
normalized: PublicEvidenceContentInput,
|
|
176
|
+
publisherInput: PublicPublisherV1,
|
|
177
|
+
): PublicEvidenceBundleUnsignedV1 {
|
|
178
|
+
const publisher = validatePublisher(publisherInput);
|
|
179
|
+
const content = {
|
|
180
|
+
schemaVersion: PUBLIC_EVIDENCE_BUNDLE_SCHEMA_VERSION,
|
|
181
|
+
exportPolicyVersion: PUBLIC_EXPORT_POLICY_VERSION,
|
|
182
|
+
createdDayUtc: normalized.createdDayUtc,
|
|
183
|
+
publisher,
|
|
184
|
+
records: normalized.records,
|
|
185
|
+
artifacts: normalized.artifacts,
|
|
186
|
+
};
|
|
187
|
+
const bundleId = publicEvidenceId("bundle", content);
|
|
188
|
+
const bundleDigest = publicEvidenceId("bundle_digest", { ...content, bundleId });
|
|
189
|
+
const bundle: PublicEvidenceBundleUnsignedV1 = { ...content, bundleId, bundleDigest };
|
|
190
|
+
if (new TextEncoder().encode(jcsStringify(bundle)).byteLength > MAX_PUBLIC_BUNDLE_BYTES) {
|
|
191
|
+
throw new PublicEvidenceValidationError("bundle_too_large", "public bundle exceeds 2 MiB");
|
|
192
|
+
}
|
|
193
|
+
return bundle;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function buildPublicEvidenceBundle(input: BuildPublicEvidenceBundleInput): PublicEvidenceBundleUnsignedV1 {
|
|
197
|
+
return buildFromNormalizedContent(normalizePublicEvidenceContent(input), input.publisher);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function expectedPublicBundleIdentityFromNormalized(
|
|
201
|
+
normalized: PublicEvidenceContentInput,
|
|
202
|
+
publisher: PublicPublisherV1,
|
|
203
|
+
): { bundleId: string; bundleDigest: string } {
|
|
204
|
+
const rebuilt = buildFromNormalizedContent(normalized, publisher);
|
|
205
|
+
return { bundleId: rebuilt.bundleId, bundleDigest: rebuilt.bundleDigest };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function expectedPublicBundleIdentity(bundle: PublicEvidenceBundleUnsignedV1): { bundleId: string; bundleDigest: string } {
|
|
209
|
+
return expectedPublicBundleIdentityFromNormalized(
|
|
210
|
+
normalizePublicEvidenceContent({
|
|
211
|
+
records: bundle.records,
|
|
212
|
+
artifacts: bundle.artifacts,
|
|
213
|
+
createdDayUtc: bundle.createdDayUtc,
|
|
214
|
+
}),
|
|
215
|
+
bundle.publisher,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { loadCaseAuthority } from "../conformance/manifest";
|
|
2
|
+
import {
|
|
3
|
+
FABRIC_COMPATIBILITY_VERSION,
|
|
4
|
+
FABRIC_SCENARIO_ID,
|
|
5
|
+
FABRIC_SCENARIO_VERSION,
|
|
6
|
+
FABRIC_SUITE_ID,
|
|
7
|
+
FABRIC_SUITE_VERSION,
|
|
8
|
+
FABRIC_TASK_CLASS_ID,
|
|
9
|
+
FABRIC_TASK_CLASS_VERSION,
|
|
10
|
+
} from "../fabric/constants";
|
|
11
|
+
import { loadFabricCaseAuthority } from "../fabric/manifest";
|
|
12
|
+
import { verifierManifestDigest } from "../fabric/subject";
|
|
13
|
+
import { findPublicRouteRegistryEntry } from "./registry";
|
|
14
|
+
import type { PublicEvidenceBundleV1, PublicEvidenceRecordV1, PublicRouteSubjectV1 } from "./types";
|
|
15
|
+
import { PublicEvidenceValidationError } from "./validate";
|
|
16
|
+
|
|
17
|
+
type ProtocolCaseAuthority = ReturnType<typeof loadCaseAuthority>;
|
|
18
|
+
|
|
19
|
+
interface ProtocolAuthoritySnapshot {
|
|
20
|
+
scenarioVersion: string;
|
|
21
|
+
suiteVersion: string;
|
|
22
|
+
sourceCommit: string;
|
|
23
|
+
load: () => ProtocolCaseAuthority;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Public records are historical evidence. Never replace an authority entry when a
|
|
27
|
+
// protocol version advances: retain the old loader and append a new snapshot.
|
|
28
|
+
const PROTOCOL_AUTHORITY_SNAPSHOTS: readonly ProtocolAuthoritySnapshot[] = Object.freeze([
|
|
29
|
+
Object.freeze({
|
|
30
|
+
scenarioVersion: "1.0.0",
|
|
31
|
+
suiteVersion: "1.0.0",
|
|
32
|
+
sourceCommit: "3ad5bb6bd3f76f6879d84b78ea39edd3e01ec296",
|
|
33
|
+
load: loadCaseAuthority,
|
|
34
|
+
}),
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const cachedCaseAuthorities = new Map<string, ProtocolCaseAuthority>();
|
|
38
|
+
let cachedFabricCaseAuthority: ReturnType<typeof loadFabricCaseAuthority> | null = null;
|
|
39
|
+
let cachedVerifierManifestDigest: string | null = null;
|
|
40
|
+
|
|
41
|
+
function protocolAuthorityKey(snapshot: ProtocolAuthoritySnapshot): string {
|
|
42
|
+
return `${snapshot.suiteVersion}\0${snapshot.scenarioVersion}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function caseAuthorityFor(record: PublicEvidenceRecordV1): ProtocolCaseAuthority {
|
|
46
|
+
const snapshot = PROTOCOL_AUTHORITY_SNAPSHOTS.find((candidate) =>
|
|
47
|
+
candidate.scenarioVersion === record.scenarioVersion
|
|
48
|
+
&& candidate.suiteVersion === record.suiteVersion
|
|
49
|
+
);
|
|
50
|
+
if (!snapshot) {
|
|
51
|
+
throw new PublicEvidenceValidationError(
|
|
52
|
+
"public_authority",
|
|
53
|
+
"scenario/suite authority version is not retained",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const key = protocolAuthorityKey(snapshot);
|
|
58
|
+
const cached = cachedCaseAuthorities.get(key);
|
|
59
|
+
if (cached) return cached;
|
|
60
|
+
|
|
61
|
+
const authority = snapshot.load();
|
|
62
|
+
if (
|
|
63
|
+
String(authority.manifestDefaults.version) !== snapshot.scenarioVersion
|
|
64
|
+
|| String(authority.manifestDefaults.suiteVersion) !== snapshot.suiteVersion
|
|
65
|
+
|| authority.sourceCommit !== snapshot.sourceCommit
|
|
66
|
+
) {
|
|
67
|
+
throw new Error("public protocol authority snapshot drift");
|
|
68
|
+
}
|
|
69
|
+
cachedCaseAuthorities.set(key, authority);
|
|
70
|
+
return authority;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function fabricCaseAuthority(): ReturnType<typeof loadFabricCaseAuthority> {
|
|
74
|
+
cachedFabricCaseAuthority ??= loadFabricCaseAuthority();
|
|
75
|
+
return cachedFabricCaseAuthority;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function reviewedVerifierManifestDigest(): string {
|
|
79
|
+
cachedVerifierManifestDigest ??= verifierManifestDigest();
|
|
80
|
+
return cachedVerifierManifestDigest;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function validateRouteAuthority(subject: PublicRouteSubjectV1): void {
|
|
84
|
+
const entry = findPublicRouteRegistryEntry(subject.providerId, subject.modelId);
|
|
85
|
+
if (!entry || !entry.adapterFamilies.includes(subject.adapterFamily)) {
|
|
86
|
+
throw new PublicEvidenceValidationError("public_authority", "public route is not in reviewed registry authority");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function validateAssertionAuthority(
|
|
91
|
+
record: PublicEvidenceRecordV1,
|
|
92
|
+
assertions: readonly { id: string; required: boolean }[],
|
|
93
|
+
): void {
|
|
94
|
+
const allowed = new Map(assertions.map((assertion) => [assertion.id, assertion.required] as const));
|
|
95
|
+
if (allowed.size !== assertions.length) {
|
|
96
|
+
throw new PublicEvidenceValidationError("public_authority", "reviewed scenario assertion authority contains duplicates");
|
|
97
|
+
}
|
|
98
|
+
if (record.assertions.length !== allowed.size) {
|
|
99
|
+
throw new PublicEvidenceValidationError(
|
|
100
|
+
"public_authority",
|
|
101
|
+
"public assertion set does not exactly match reviewed scenario authority",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const seen = new Set<string>();
|
|
105
|
+
for (const assertion of record.assertions) {
|
|
106
|
+
if (seen.has(assertion.id)) {
|
|
107
|
+
throw new PublicEvidenceValidationError("public_authority", "public assertion set contains duplicate assertion ids");
|
|
108
|
+
}
|
|
109
|
+
seen.add(assertion.id);
|
|
110
|
+
if (!allowed.has(assertion.id) || allowed.get(assertion.id) !== assertion.required) {
|
|
111
|
+
throw new PublicEvidenceValidationError(
|
|
112
|
+
"public_authority",
|
|
113
|
+
"public assertion id/required flag is not in reviewed scenario authority",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const assertionId of allowed.keys()) {
|
|
118
|
+
if (!seen.has(assertionId)) {
|
|
119
|
+
throw new PublicEvidenceValidationError("public_authority", "public assertion set is missing reviewed scenario authority");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function validateTaskAuthority(record: PublicEvidenceRecordV1): void {
|
|
125
|
+
const fabricAuthority = fabricCaseAuthority();
|
|
126
|
+
const caseRecord = fabricAuthority.cases.find((candidate) => candidate.id === FABRIC_SCENARIO_ID);
|
|
127
|
+
if (
|
|
128
|
+
!caseRecord
|
|
129
|
+
|| record.suiteId !== FABRIC_SUITE_ID
|
|
130
|
+
|| record.suiteVersion !== FABRIC_SUITE_VERSION
|
|
131
|
+
|| record.scenarioId !== FABRIC_SCENARIO_ID
|
|
132
|
+
|| record.scenarioVersion !== FABRIC_SCENARIO_VERSION
|
|
133
|
+
|| record.subject.subjectKind !== "task"
|
|
134
|
+
|| record.subject.taskClassId !== FABRIC_TASK_CLASS_ID
|
|
135
|
+
|| record.subject.taskClassVersion !== FABRIC_TASK_CLASS_VERSION
|
|
136
|
+
|| record.subject.taskFixtureDigest !== caseRecord.fixture.digest
|
|
137
|
+
|| record.subject.verifierManifestDigest !== reviewedVerifierManifestDigest()
|
|
138
|
+
|| record.subject.fabricCompatibilityVersion !== FABRIC_COMPATIBILITY_VERSION
|
|
139
|
+
) {
|
|
140
|
+
throw new PublicEvidenceValidationError("public_authority", "task scenario/verifier authority mismatch");
|
|
141
|
+
}
|
|
142
|
+
validateAssertionAuthority(record, caseRecord.assertions);
|
|
143
|
+
validateRouteAuthority(record.subject.route);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function validateScenarioAuthority(record: PublicEvidenceRecordV1): void {
|
|
147
|
+
if (record.evidenceLayer === "task_effectiveness") {
|
|
148
|
+
validateTaskAuthority(record);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const authority = caseAuthorityFor(record);
|
|
153
|
+
const caseRecord = authority.cases.find((candidate) => candidate.id === record.scenarioId);
|
|
154
|
+
if (!caseRecord || caseRecord.suite !== record.suiteId) {
|
|
155
|
+
throw new PublicEvidenceValidationError("public_authority", "scenario/suite authority mismatch");
|
|
156
|
+
}
|
|
157
|
+
validateAssertionAuthority(record, caseRecord.assertions);
|
|
158
|
+
|
|
159
|
+
if (record.evidenceLayer === "live_route_compatibility") {
|
|
160
|
+
if (record.subject.subjectKind !== "route") {
|
|
161
|
+
throw new PublicEvidenceValidationError("public_authority", "live route subject mismatch");
|
|
162
|
+
}
|
|
163
|
+
validateRouteAuthority(record.subject);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Repository-owned authority gate used by both local signing and community imports. */
|
|
168
|
+
export function validatePublicEvidenceAuthorities(records: readonly PublicEvidenceRecordV1[]): void {
|
|
169
|
+
for (const record of records) validateScenarioAuthority(record);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function validateCommunityEvidenceAuthorities(bundle: PublicEvidenceBundleV1): PublicEvidenceBundleV1 {
|
|
173
|
+
validatePublicEvidenceAuthorities(bundle.records);
|
|
174
|
+
return bundle;
|
|
175
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const COMMUNITY_ID_FRAGMENT = "[0-9a-f]{64}";
|
|
2
|
+
const COMMUNITY_BUNDLE_FILE_RE = new RegExp(
|
|
3
|
+
`^bundle-(${COMMUNITY_ID_FRAGMENT})-(${COMMUNITY_ID_FRAGMENT})\\.json$`,
|
|
4
|
+
);
|
|
5
|
+
const COMMUNITY_REVOCATION_FILE_RE = new RegExp(
|
|
6
|
+
`^revocation-(${COMMUNITY_ID_FRAGMENT})\\.json$`,
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
export interface CommunityBundleFileIdentity {
|
|
10
|
+
publisherKeyId: string;
|
|
11
|
+
bundleId: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function communityBundleFileName(publisherKeyId: string, bundleId: string): string {
|
|
15
|
+
return `bundle-${publisherKeyId}-${bundleId}.json`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function communityRevocationFileName(revocationId: string): string {
|
|
19
|
+
return `revocation-${revocationId}.json`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseCommunityBundleFileName(name: string): CommunityBundleFileIdentity | null {
|
|
23
|
+
const match = COMMUNITY_BUNDLE_FILE_RE.exec(name);
|
|
24
|
+
return match ? { publisherKeyId: match[1]!, bundleId: match[2]! } : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isCommunityRevocationFileName(name: string): boolean {
|
|
28
|
+
return COMMUNITY_REVOCATION_FILE_RE.test(name);
|
|
29
|
+
}
|