agentera 3.0.0-dev.72 → 3.0.0-dev.75
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 +111 -0
- package/bundle/.agentera-build-source.json +8 -0
- package/bundle/CHANGELOG.md +17 -17
- package/bundle/extract-corpus-parity.json +1 -1
- package/bundle/references/adapters/package-publication.json +49 -53
- package/bundle/references/adapters/package-registry.yaml +5 -1
- package/bundle/references/adapters/package-surface-characterization.md +6 -4
- package/bundle/references/analysis/evidence-tier-authority.yaml +45 -4
- package/bundle/references/analysis/personal-glossary-evaluation-authority.yaml +175 -0
- package/bundle/references/analysis/personal-glossary-evaluation-corpus.yaml +291 -0
- package/bundle/references/analysis/personal-glossary-holdout.yaml +192 -0
- package/bundle/references/analysis/verification-policy.yaml +8 -13
- package/bundle/references/artifacts/glossary-entry-contract.yaml +1533 -35
- package/bundle/references/cli/update-channels.yaml +2 -2
- package/bundle/references/cli/vocabulary.md +1 -1
- package/bundle/references/meta/retained-reference-authority.yaml +16 -1
- package/bundle/skills/agentera/capabilities/profile/schemas/artifacts.yaml +11 -7
- package/bundle/skills/agentera/capabilities/profile/schemas/exit.yaml +6 -3
- package/bundle/skills/agentera/capabilities/profile/schemas/validation.yaml +13 -7
- package/bundle/skills/agentera/schemas/artifacts/glossary.yaml +5 -0
- package/bundle/skills/agentera/schemas/artifacts/plan.yaml +7 -0
- package/dist/.agentera-build-source.json +8 -0
- package/dist/analytics/extractCorpus/copilotSessions.js +13 -4
- package/dist/analytics/extractCorpus/core.js +223 -3
- package/dist/analytics/extractCorpus/corpus.js +18 -1
- package/dist/analytics/extractCorpus/cursorSessions.js +22 -4
- package/dist/analytics/extractCorpus/evidenceTiers.js +123 -17
- package/dist/analytics/extractCorpus/filesystemSources.js +4 -0
- package/dist/analytics/extractCorpus/index.js +2 -2
- package/dist/analytics/extractCorpus/jsonlSessions.js +52 -3
- package/dist/analytics/extractCorpus/sqliteSessions.js +12 -3
- package/dist/analytics/personalGlossaryAdmission.js +26 -46
- package/dist/analytics/personalGlossaryCandidateProjection.js +692 -0
- package/dist/analytics/personalGlossaryCandidateProjectionExcerpts.js +144 -0
- package/dist/analytics/personalGlossaryCandidateReadView.js +23 -0
- package/dist/analytics/personalGlossaryCurrentGeneration.js +25 -0
- package/dist/analytics/personalGlossaryDecision.js +203 -0
- package/dist/analytics/personalGlossaryExplicit.js +788 -0
- package/dist/analytics/personalGlossaryExplicitMining.js +285 -0
- package/dist/analytics/personalGlossaryExplicitScope.js +41 -0
- package/dist/analytics/personalGlossaryExplicitSegments.js +418 -0
- package/dist/analytics/personalGlossaryExplicitTypes.js +26 -0
- package/dist/analytics/personalGlossaryLexicalClassifier.js +175 -0
- package/dist/analytics/personalGlossaryProfile.js +13 -3
- package/dist/analytics/personalGlossaryRecurrence.js +618 -0
- package/dist/analytics/personalGlossaryReviewRecordStorage.js +428 -0
- package/dist/analytics/personalGlossaryReviewRecords.js +332 -0
- package/dist/capabilities/index.js +5 -1
- package/dist/capabilities/profile/instructions.js +50 -15
- package/dist/cli/commands/personalGlossaryCandidateReads.js +744 -0
- package/dist/cli/commands/personalGlossaryDecision.js +349 -0
- package/dist/cli/commands/personalGlossaryPublish.js +484 -0
- package/dist/cli/commands/personalGlossaryReviewRecordReads.js +209 -0
- package/dist/cli/commands/personalGlossaryReviewRecords.js +623 -0
- package/dist/cli/commands/prime/briefOrientation.js +118 -19
- package/dist/cli/commands/report.js +18 -2
- package/dist/cli/commands/schema.js +179 -0
- package/dist/cli/commands/verify.js +51 -9
- package/dist/cli/dispatch/lifecycle.js +51 -6
- package/dist/cli/help.js +48 -2
- package/dist/cli/migrationRequired.js +4 -1
- package/dist/core/atomicWriter.js +4 -1
- package/dist/core/developmentInvocation.js +4 -1
- package/dist/core/yaml.js +53 -1
- package/dist/eval/glossaryEvaluation.js +751 -0
- package/dist/eval/glossaryEvaluationProcess.js +29 -0
- package/dist/eval/glossaryEvaluationRunner.js +571 -0
- package/dist/eval/glossaryEvaluationSuccessReport.js +297 -0
- package/dist/registries/activationTuples.js +31 -3
- package/dist/registries/evidenceTierContract.js +4 -0
- package/dist/registries/explicitSegmentGrammarContract.js +619 -0
- package/dist/registries/glossaryCandidateContractPaths.js +6 -0
- package/dist/registries/glossaryCandidateContracts.js +820 -0
- package/dist/registries/glossaryCandidateDecisionAuthority.js +119 -0
- package/dist/registries/glossaryCandidateDecisionContract.js +62 -0
- package/dist/registries/glossaryCandidateProjectionAuthority.js +197 -0
- package/dist/registries/glossaryCandidateProjectionContract.js +104 -0
- package/dist/registries/glossaryEntryContract.js +105 -103
- package/dist/registries/glossaryEntryTemporal.js +12 -0
- package/dist/registries/glossaryMiningAuthority.js +764 -0
- package/dist/registries/glossaryProfileFullContract.js +24 -0
- package/dist/registries/glossaryReviewRecordsAuthority.js +426 -0
- package/dist/registries/glossaryReviewRecordsContract.js +129 -0
- package/dist/registries/glossaryTermIdentity.js +122 -0
- package/dist/registries/packagePublication.js +3 -3
- package/dist/registries/personalGlossaryContracts.js +1 -0
- package/dist/release/releaseMetadata.js +15 -8
- package/dist/upgrade/npxPlatformStatus.js +2 -1
- package/dist/upgrade/projectIntegration.js +7 -1
- package/dist/validate/activationArtifactEvidence.js +18 -4
- package/dist/validate/activationEvidenceManifest.js +101 -22
- package/package.json +2 -4
- package/dist/cli/commands/personalGlossary.js +0 -176
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { createPublicKey } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { defaultProfileDir } from "./extractCorpus/core.js";
|
|
5
|
+
import { containsPersonalGlossarySensitiveContent } from "./personalGlossaryCandidateProjectionExcerpts.js";
|
|
6
|
+
import { GLOSSARY_ADMISSION_REASONS_BY_OUTCOME } from "../registries/glossaryCandidateDecisionAuthority.js";
|
|
7
|
+
import { PERSONAL_REVIEW_DISPOSITIONS } from "../registries/glossaryMiningAuthority.js";
|
|
8
|
+
import { personalGlossaryReviewRecordsContract } from "../registries/glossaryReviewRecordsContract.js";
|
|
9
|
+
import { canonicalGlossaryJson, compareGlossaryUnicodeStrings, glossaryCanonicalSha256 } from "../registries/glossaryTermIdentity.js";
|
|
10
|
+
const STORE_SCHEMA_VERSION = "agentera.personalGlossaryReviewStore.v2";
|
|
11
|
+
const RECORD_SCHEMA_VERSION = "agentera.personalGlossaryReviewRecord.v2";
|
|
12
|
+
const LEGACY_STORE_SCHEMA_VERSION = "agentera.personalGlossaryReviewStore.v1";
|
|
13
|
+
const LEGACY_RECORD_SCHEMA_VERSION = "agentera.personalGlossaryPendingReviewRecord.v1";
|
|
14
|
+
const TRUSTED_HOST_KEY_SCHEMA_VERSION = "agentera.personalGlossaryTrustedLocalHost.v1";
|
|
15
|
+
const REVIEW_OWNER = "current_user";
|
|
16
|
+
const REVIEW_ID_SCHEMA_VERSION = "agentera.personalGlossaryReviewIdentity.v1";
|
|
17
|
+
const REPLAY_NONCE_SCHEMA_VERSION = "agentera.personalGlossaryReviewReplayNonce.v1";
|
|
18
|
+
const SHA256 = /^[a-f0-9]{64}$/u;
|
|
19
|
+
const BASE64URL = /^[A-Za-z0-9_-]+$/u;
|
|
20
|
+
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u;
|
|
21
|
+
const SENSITIVE_REVIEW_METADATA_ASSIGNMENT = /(?:^|[^A-Za-z0-9_])(?:api[_-]?key|access[_-]?token|token|password|passwd|cookie|private[_-]?key|authorization(?:[_-]?header)?|session(?:[_-]?id)?|email|phone|contact|secret)\s*[:=]/iu;
|
|
22
|
+
function mapping(value) {
|
|
23
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
function exactKeys(value, expected) {
|
|
26
|
+
return JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...expected].sort());
|
|
27
|
+
}
|
|
28
|
+
function sameStrings(actual, expected) {
|
|
29
|
+
return JSON.stringify(actual) === JSON.stringify(expected);
|
|
30
|
+
}
|
|
31
|
+
function boundedText(value, maximum) {
|
|
32
|
+
return typeof value === "string" && value.length > 0 && Buffer.byteLength(value, "utf8") <= maximum;
|
|
33
|
+
}
|
|
34
|
+
const CONTENT_ADDRESSED_TIER_GENERATION = /^[a-f0-9]{24}$/u;
|
|
35
|
+
/** Keep review binding metadata opaque and safe to persist or return. */
|
|
36
|
+
export function validPersonalGlossaryReviewMetadataBinding(value) {
|
|
37
|
+
return boundedText(value, 256) && !/[\\/]/u.test(value) &&
|
|
38
|
+
!SENSITIVE_REVIEW_METADATA_ASSIGNMENT.test(value) && !containsPersonalGlossarySensitiveContent(value);
|
|
39
|
+
}
|
|
40
|
+
/** A current evidence-tier generation is a bounded opaque digest, not secret content. */
|
|
41
|
+
export function validPersonalGlossaryReviewGenerationBinding(value) {
|
|
42
|
+
return (typeof value === "string" && CONTENT_ADDRESSED_TIER_GENERATION.test(value)) || validPersonalGlossaryReviewMetadataBinding(value);
|
|
43
|
+
}
|
|
44
|
+
function validCorrectedMeaning(value) {
|
|
45
|
+
return boundedText(value, 4_096) && !containsPersonalGlossarySensitiveContent(value);
|
|
46
|
+
}
|
|
47
|
+
function timestamp(value) {
|
|
48
|
+
return typeof value === "string" && TIMESTAMP.test(value) && !Number.isNaN(Date.parse(value));
|
|
49
|
+
}
|
|
50
|
+
function digest(value) {
|
|
51
|
+
return typeof value === "string" && SHA256.test(value);
|
|
52
|
+
}
|
|
53
|
+
function serializedBytes(value) {
|
|
54
|
+
return Buffer.byteLength(`${canonicalGlossaryJson(value)}\n`, "utf8");
|
|
55
|
+
}
|
|
56
|
+
export function personalGlossaryReviewRecordsStorageContract() {
|
|
57
|
+
const value = personalGlossaryReviewRecordsContract();
|
|
58
|
+
if (value.command !== "agentera report personal-glossary-reviews" ||
|
|
59
|
+
value.queueRequestSchemaVersion !== "agentera.personalGlossaryReviewQueueRequest.v1" ||
|
|
60
|
+
!sameStrings(value.queueRequestFields, ["schema_version", "receipt"]) ||
|
|
61
|
+
value.queueMaxRequestUtf8Bytes !== 16_384 || value.queueDecisionOutcome !== "review_required" ||
|
|
62
|
+
!sameStrings(value.queueCurrentBindings, ["candidate_id", "candidate_revision", "candidate_capsule_sha256", "candidate_projection_sha256", "host_receipt_sha256", "cli_decision_sha256", "semantic_fingerprint", "generation", "policy_version", "reason"]) ||
|
|
63
|
+
value.queueResultSchemaVersion !== "agentera.personalGlossaryReviewQueueResult.v1" ||
|
|
64
|
+
!sameStrings(value.queueResultStatuses, ["queued", "unchanged_replay", "suppressed", "reopened"]) ||
|
|
65
|
+
value.queueMaxResultUtf8Bytes !== 4_096 || value.queueNoQuestionChannel !== "queue_without_prompt_or_disposition" ||
|
|
66
|
+
value.dispositionRequestSchemaVersion !== "agentera.personalGlossaryReviewDispositionRequest.v1" ||
|
|
67
|
+
!sameStrings(value.dispositionRequestFields, ["schema_version", "review_id", "receipt", "approval"]) ||
|
|
68
|
+
value.dispositionMaxRequestUtf8Bytes !== 16_384 ||
|
|
69
|
+
value.dispositionResultSchemaVersion !== "agentera.personalGlossaryReviewDispositionResult.v1" ||
|
|
70
|
+
!sameStrings(value.dispositionResultStatuses, ["disposed", "unchanged_replay"]) ||
|
|
71
|
+
value.dispositionMaxResultUtf8Bytes !== 4_096 ||
|
|
72
|
+
!sameStrings(value.dispositionPublicationAuthorizationDispositions, ["accept", "correct"]) ||
|
|
73
|
+
!sameStrings(value.dispositionPublicationAuthorizationFields, ["review_id", "review_record_sha256"]) ||
|
|
74
|
+
value.trustedHostKeyFile !== "trusted-local-host.json" ||
|
|
75
|
+
value.trustedHostKeySchemaVersion !== TRUSTED_HOST_KEY_SCHEMA_VERSION ||
|
|
76
|
+
!sameStrings(value.trustedHostKeyFields, ["schema_version", "owner", "subject", "public_key_spki_base64url"]) ||
|
|
77
|
+
value.trustedHostKeyOwner !== REVIEW_OWNER || value.trustedHostKeyAlgorithm !== "ed25519" ||
|
|
78
|
+
value.trustedHostKeyMaxSerializedUtf8Bytes !== 4_096 || value.storeSchemaVersion !== STORE_SCHEMA_VERSION ||
|
|
79
|
+
value.recordSchemaVersion !== RECORD_SCHEMA_VERSION || value.storeOwner !== REVIEW_OWNER ||
|
|
80
|
+
value.storeFile !== "review-records.json" ||
|
|
81
|
+
!sameStrings(value.storeFields, ["schema_version", "owner", "records", "replay_index", "store_sha256"]) ||
|
|
82
|
+
!sameStrings(value.recordFields, ["schema_version", "owner", "review_id", "candidate_id", "candidate_revision", "candidate_capsule_sha256", "candidate_projection_sha256", "host_receipt_sha256", "cli_decision_sha256", "semantic_fingerprint", "generation", "policy_version", "scope", "reason", "status", "disposition", "review_record", "reopen_reason", "queued_at", "terminal_at", "expires_at", "record_sha256"]) ||
|
|
83
|
+
value.recordsMax !== 100 || !sameStrings(value.replayIndexFields, ["nonce_sha256", "receipt_sha256", "expires_at"]) ||
|
|
84
|
+
value.replayEntriesMax !== 100 || value.recordMaxSerializedUtf8Bytes !== 8_192 ||
|
|
85
|
+
value.storeMaxSerializedUtf8Bytes !== 1_048_576 || value.storeOrder !== "review_id_asc" ||
|
|
86
|
+
value.replay !== "exact_current_binding_is_unchanged_replay_and_exact_receipt_is_idempotent" ||
|
|
87
|
+
value.conflict !== "changed_receipt_nonce_binding_or_signature_fails_before_effects" ||
|
|
88
|
+
!sameStrings(value.compatibilityStoreSchemaVersions, [LEGACY_STORE_SCHEMA_VERSION, STORE_SCHEMA_VERSION]) ||
|
|
89
|
+
!sameStrings(value.compatibilityRecordSchemaVersions, [LEGACY_RECORD_SCHEMA_VERSION, RECORD_SCHEMA_VERSION]) ||
|
|
90
|
+
value.compatibilityReadMutation !== "forbidden" || value.compatibilityMigrationOperation !== "disposition_only" ||
|
|
91
|
+
value.compatibilityScopeDerivation !== "current_validated_host_receipt_only" ||
|
|
92
|
+
value.compatibilityInvalidBehavior !== "fail_before_effects" ||
|
|
93
|
+
!sameStrings(value.compatibilityPreservedBindings, ["review_id", "candidate_id", "candidate_revision", "candidate_capsule_sha256", "candidate_projection_sha256", "host_receipt_sha256", "cli_decision_sha256", "semantic_fingerprint", "generation", "policy_version", "reason"]) ||
|
|
94
|
+
value.compatibilityLegacyDigest !== "validate_before_migration" ||
|
|
95
|
+
value.compatibilityMigratedDigest !== "reseal_v2_lifecycle_record" ||
|
|
96
|
+
!sameStrings(value.suppressionBinding, ["candidate_id", "semantic_fingerprint", "scope", "policy_version"]) ||
|
|
97
|
+
!sameStrings(value.suppressionDispositions, ["reject", "defer"]) ||
|
|
98
|
+
!sameStrings(value.reopenReasons, ["policy_changed", "scope_changed", "meaning_changed"]) ||
|
|
99
|
+
!sameStrings(value.forbiddenFields, ["term", "meaning", "excerpt", "raw_evidence", "source_id", "evidence_anchor", "session_id", "project_id", "source_path", "tool_content", "review_approval", "signature", "nonce"]) ||
|
|
100
|
+
value.retrievalSchemaVersion !== "agentera.personalGlossaryReviewRetrieval.v1" || value.retrievalOwner !== REVIEW_OWNER ||
|
|
101
|
+
value.listDefaultLimit !== 20 || value.listMaximumLimit !== 50 || value.listMaxSerializedUtf8Bytes !== 32_768 ||
|
|
102
|
+
value.listOrder !== "queued_at_desc_then_review_id_asc" || !sameStrings(value.listStatuses, ["pending", "terminal"]) ||
|
|
103
|
+
value.cursorAuthority !== "references/artifacts/state-storage-authority.yaml#entity_target.public_retrieval.policy.cursor" ||
|
|
104
|
+
value.cursorVocabulary !== "opaque_snapshot_cursor" ||
|
|
105
|
+
!sameStrings(value.cursorBinding, ["collection", "owner", "filters", "limit", "order", "snapshot"]) ||
|
|
106
|
+
value.cursorInvalidBehavior !== "cursor_invalid" || value.cursorUnavailableBehavior !== "cursor_snapshot_unavailable" ||
|
|
107
|
+
!sameStrings(value.exactRequiredBindings, ["review_id", "candidate_id", "candidate_revision", "generation", "policy_version"]) ||
|
|
108
|
+
value.exactCurrentBindingField !== "candidate_projection_sha256" || value.exactMaxSerializedUtf8Bytes !== 8_192 ||
|
|
109
|
+
value.terminalMetadataDays !== 90 || value.maintenanceExposure !== "authenticated_review_owner_only" ||
|
|
110
|
+
value.maintenancePurge !== "current_user_authorized_review_records_only" ||
|
|
111
|
+
!sameStrings(value.maintenanceForbiddenEffects, ["profile_entry", "project_state", "candidate_projection", "publication"]))
|
|
112
|
+
throw new TypeError("personal glossary review-record contract is unavailable");
|
|
113
|
+
return {
|
|
114
|
+
storeFile: value.storeFile,
|
|
115
|
+
trustedHostKeyFile: value.trustedHostKeyFile,
|
|
116
|
+
trustedHostKeyMaxSerializedUtf8Bytes: value.trustedHostKeyMaxSerializedUtf8Bytes,
|
|
117
|
+
storeMaxSerializedUtf8Bytes: value.storeMaxSerializedUtf8Bytes,
|
|
118
|
+
recordMaxSerializedUtf8Bytes: value.recordMaxSerializedUtf8Bytes,
|
|
119
|
+
recordsMax: value.recordsMax,
|
|
120
|
+
replayEntriesMax: value.replayEntriesMax,
|
|
121
|
+
terminalMetadataDays: value.terminalMetadataDays,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export function reviewIdentity(record) {
|
|
125
|
+
return glossaryCanonicalSha256({
|
|
126
|
+
schema_version: REVIEW_ID_SCHEMA_VERSION,
|
|
127
|
+
owner: record.owner,
|
|
128
|
+
candidate_id: record.candidate_id,
|
|
129
|
+
candidate_revision: record.candidate_revision,
|
|
130
|
+
candidate_capsule_sha256: record.candidate_capsule_sha256,
|
|
131
|
+
candidate_projection_sha256: record.candidate_projection_sha256,
|
|
132
|
+
host_receipt_sha256: record.host_receipt_sha256,
|
|
133
|
+
cli_decision_sha256: record.cli_decision_sha256,
|
|
134
|
+
semantic_fingerprint: record.semantic_fingerprint,
|
|
135
|
+
generation: record.generation,
|
|
136
|
+
policy_version: record.policy_version,
|
|
137
|
+
reason: record.reason,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function reviewRecordOrder(left, right) {
|
|
141
|
+
return compareGlossaryUnicodeStrings(left.review_id, right.review_id);
|
|
142
|
+
}
|
|
143
|
+
function replayEntryOrder(left, right) {
|
|
144
|
+
return compareGlossaryUnicodeStrings(left.nonce_sha256, right.nonce_sha256);
|
|
145
|
+
}
|
|
146
|
+
function bodyWithoutDigest(value, field) {
|
|
147
|
+
const body = { ...value };
|
|
148
|
+
delete body[field];
|
|
149
|
+
return body;
|
|
150
|
+
}
|
|
151
|
+
export function reviewExpiry(disposedAt) {
|
|
152
|
+
return new Date(Date.parse(disposedAt) + personalGlossaryReviewRecordsStorageContract().terminalMetadataDays * 86_400_000).toISOString();
|
|
153
|
+
}
|
|
154
|
+
function validStoredReviewRecord(value, record) {
|
|
155
|
+
if (!mapping(value) || !exactKeys(value, ["schema_version", "owner", "candidate_id", "candidate_revision", "candidate_capsule_sha256", "host_receipt_sha256", "cli_decision_sha256", "semantic_fingerprint", "generation", "policy_version", "disposition", "corrected_meaning", "corrected_scope", "disposed_at", "expires_at", "record_sha256"]))
|
|
156
|
+
return false;
|
|
157
|
+
if (value.schema_version !== "agentera.personalGlossaryReviewRecord.v1" || value.owner !== "user_local_review_lifecycle" ||
|
|
158
|
+
![value.candidate_id, value.candidate_revision, value.candidate_capsule_sha256, value.host_receipt_sha256, value.cli_decision_sha256, value.semantic_fingerprint, value.record_sha256].every(digest) ||
|
|
159
|
+
!validPersonalGlossaryReviewGenerationBinding(value.generation) || !validPersonalGlossaryReviewMetadataBinding(value.policy_version) ||
|
|
160
|
+
value.candidate_id !== record.candidate_id || value.candidate_revision !== record.candidate_revision ||
|
|
161
|
+
value.candidate_capsule_sha256 !== record.candidate_capsule_sha256 || value.host_receipt_sha256 !== record.host_receipt_sha256 ||
|
|
162
|
+
value.cli_decision_sha256 !== record.cli_decision_sha256 || value.semantic_fingerprint !== record.semantic_fingerprint ||
|
|
163
|
+
value.generation !== record.generation || value.policy_version !== record.policy_version ||
|
|
164
|
+
value.disposition !== record.disposition || !timestamp(value.disposed_at) || !timestamp(value.expires_at) ||
|
|
165
|
+
value.expires_at !== reviewExpiry(value.disposed_at) ||
|
|
166
|
+
value.record_sha256 !== glossaryCanonicalSha256(bodyWithoutDigest(value, "record_sha256")))
|
|
167
|
+
return false;
|
|
168
|
+
return value.disposition === "correct"
|
|
169
|
+
? value.corrected_scope === "personal" && validCorrectedMeaning(value.corrected_meaning)
|
|
170
|
+
: value.corrected_scope === null && value.corrected_meaning === null;
|
|
171
|
+
}
|
|
172
|
+
function commonRecordIsValid(value) {
|
|
173
|
+
return value.owner === REVIEW_OWNER &&
|
|
174
|
+
[value.review_id, value.candidate_id, value.candidate_revision, value.candidate_capsule_sha256, value.candidate_projection_sha256, value.host_receipt_sha256, value.cli_decision_sha256, value.semantic_fingerprint, value.record_sha256].every(digest) &&
|
|
175
|
+
validPersonalGlossaryReviewGenerationBinding(value.generation) && validPersonalGlossaryReviewMetadataBinding(value.policy_version) &&
|
|
176
|
+
boundedText(value.reason, 512) &&
|
|
177
|
+
GLOSSARY_ADMISSION_REASONS_BY_OUTCOME.review_required.includes(value.reason) &&
|
|
178
|
+
timestamp(value.queued_at) && (value.status === "pending" || value.status === "terminal");
|
|
179
|
+
}
|
|
180
|
+
export function isLegacyReviewRecord(value) {
|
|
181
|
+
return value.schema_version === LEGACY_RECORD_SCHEMA_VERSION;
|
|
182
|
+
}
|
|
183
|
+
export function isCurrentReviewRecord(value) {
|
|
184
|
+
return value.schema_version === RECORD_SCHEMA_VERSION;
|
|
185
|
+
}
|
|
186
|
+
function validLegacyReviewRecord(value, valueContract) {
|
|
187
|
+
if (!mapping(value) || !exactKeys(value, ["schema_version", "owner", "review_id", "candidate_id", "candidate_revision", "candidate_capsule_sha256", "candidate_projection_sha256", "host_receipt_sha256", "cli_decision_sha256", "semantic_fingerprint", "generation", "policy_version", "reason", "status", "queued_at", "terminal_at", "expires_at", "record_sha256"]) ||
|
|
188
|
+
value.schema_version !== LEGACY_RECORD_SCHEMA_VERSION || !commonRecordIsValid(value) ||
|
|
189
|
+
value.review_id !== reviewIdentity(value) ||
|
|
190
|
+
value.record_sha256 !== glossaryCanonicalSha256(bodyWithoutDigest(value, "record_sha256")))
|
|
191
|
+
return false;
|
|
192
|
+
if (value.status === "pending")
|
|
193
|
+
return value.terminal_at === null && value.expires_at === null && serializedBytes(value) <= valueContract.recordMaxSerializedUtf8Bytes;
|
|
194
|
+
return timestamp(value.terminal_at) && timestamp(value.expires_at) &&
|
|
195
|
+
Date.parse(value.terminal_at) >= Date.parse(value.queued_at) && serializedBytes(value) <= valueContract.recordMaxSerializedUtf8Bytes;
|
|
196
|
+
}
|
|
197
|
+
function validCurrentReviewRecord(value, valueContract) {
|
|
198
|
+
if (!mapping(value) || !exactKeys(value, ["schema_version", "owner", "review_id", "candidate_id", "candidate_revision", "candidate_capsule_sha256", "candidate_projection_sha256", "host_receipt_sha256", "cli_decision_sha256", "semantic_fingerprint", "generation", "policy_version", "scope", "reason", "status", "disposition", "review_record", "reopen_reason", "queued_at", "terminal_at", "expires_at", "record_sha256"]) ||
|
|
199
|
+
value.schema_version !== RECORD_SCHEMA_VERSION || !commonRecordIsValid(value) ||
|
|
200
|
+
!["personal", "ambiguous"].includes(String(value.scope)) ||
|
|
201
|
+
(value.disposition !== null && !PERSONAL_REVIEW_DISPOSITIONS.includes(value.disposition)) ||
|
|
202
|
+
(value.reopen_reason !== null && !["policy_changed", "scope_changed", "meaning_changed"].includes(String(value.reopen_reason))))
|
|
203
|
+
return false;
|
|
204
|
+
const record = value;
|
|
205
|
+
if (record.review_id !== reviewIdentity(record) || record.record_sha256 !== glossaryCanonicalSha256(bodyWithoutDigest(value, "record_sha256")))
|
|
206
|
+
return false;
|
|
207
|
+
if (record.disposition === null)
|
|
208
|
+
return record.status === "pending" && record.review_record === null && record.terminal_at === null && record.expires_at === null && serializedBytes(record) <= valueContract.recordMaxSerializedUtf8Bytes;
|
|
209
|
+
if (!validStoredReviewRecord(record.review_record, record) || Date.parse(record.review_record.disposed_at) < Date.parse(record.queued_at))
|
|
210
|
+
return false;
|
|
211
|
+
if (record.disposition === "defer")
|
|
212
|
+
return record.status === "pending" && record.terminal_at === null && record.expires_at === null && serializedBytes(record) <= valueContract.recordMaxSerializedUtf8Bytes;
|
|
213
|
+
return record.status === "terminal" && record.terminal_at === record.review_record.disposed_at && record.expires_at === record.review_record.expires_at && Date.parse(record.terminal_at) >= Date.parse(record.queued_at) && serializedBytes(record) <= valueContract.recordMaxSerializedUtf8Bytes;
|
|
214
|
+
}
|
|
215
|
+
function validReplayEntry(value) {
|
|
216
|
+
return mapping(value) && exactKeys(value, ["nonce_sha256", "receipt_sha256", "expires_at"]) && digest(value.nonce_sha256) && digest(value.receipt_sha256) && timestamp(value.expires_at);
|
|
217
|
+
}
|
|
218
|
+
function validLegacyReviewStore(value, valueContract) {
|
|
219
|
+
if (!mapping(value) || !exactKeys(value, ["schema_version", "owner", "records", "store_sha256"]) ||
|
|
220
|
+
value.schema_version !== LEGACY_STORE_SCHEMA_VERSION || value.owner !== REVIEW_OWNER || !Array.isArray(value.records) ||
|
|
221
|
+
value.records.length > valueContract.recordsMax || !digest(value.store_sha256) || serializedBytes(value) > valueContract.storeMaxSerializedUtf8Bytes)
|
|
222
|
+
return false;
|
|
223
|
+
const records = value.records;
|
|
224
|
+
return records.every((record) => validLegacyReviewRecord(record, valueContract)) &&
|
|
225
|
+
!records.some((record, index) => index > 0 && reviewRecordOrder(records[index - 1], record) >= 0) &&
|
|
226
|
+
value.store_sha256 === glossaryCanonicalSha256(bodyWithoutDigest(value, "store_sha256"));
|
|
227
|
+
}
|
|
228
|
+
function validCurrentReviewStore(value, valueContract) {
|
|
229
|
+
if (!mapping(value) || !exactKeys(value, ["schema_version", "owner", "records", "replay_index", "store_sha256"]) ||
|
|
230
|
+
value.schema_version !== STORE_SCHEMA_VERSION || value.owner !== REVIEW_OWNER || !Array.isArray(value.records) ||
|
|
231
|
+
value.records.length > valueContract.recordsMax || !Array.isArray(value.replay_index) ||
|
|
232
|
+
value.replay_index.length > valueContract.replayEntriesMax || !digest(value.store_sha256) ||
|
|
233
|
+
serializedBytes(value) > valueContract.storeMaxSerializedUtf8Bytes)
|
|
234
|
+
return false;
|
|
235
|
+
const records = value.records;
|
|
236
|
+
const replayIndex = value.replay_index;
|
|
237
|
+
return records.every((record) => validLegacyReviewRecord(record, valueContract) || validCurrentReviewRecord(record, valueContract)) &&
|
|
238
|
+
!records.some((record, index) => index > 0 && reviewRecordOrder(records[index - 1], record) >= 0) &&
|
|
239
|
+
replayIndex.every(validReplayEntry) && !replayIndex.some((entry, index) => index > 0 && replayEntryOrder(replayIndex[index - 1], entry) >= 0) &&
|
|
240
|
+
value.store_sha256 === glossaryCanonicalSha256(bodyWithoutDigest(value, "store_sha256"));
|
|
241
|
+
}
|
|
242
|
+
export function isLegacyReviewStore(value) {
|
|
243
|
+
return value.schema_version === LEGACY_STORE_SCHEMA_VERSION;
|
|
244
|
+
}
|
|
245
|
+
export function isCurrentReviewStore(value) {
|
|
246
|
+
return value.schema_version === STORE_SCHEMA_VERSION;
|
|
247
|
+
}
|
|
248
|
+
export function makePersonalGlossaryReviewStore(records, replayIndex) {
|
|
249
|
+
const body = { schema_version: STORE_SCHEMA_VERSION, owner: REVIEW_OWNER, records: [...records].sort(reviewRecordOrder), replay_index: [...replayIndex].sort(replayEntryOrder) };
|
|
250
|
+
const store = { ...body, store_sha256: glossaryCanonicalSha256(body) };
|
|
251
|
+
if (!validCurrentReviewStore(store, personalGlossaryReviewRecordsStorageContract()))
|
|
252
|
+
throw new TypeError("review record store is invalid");
|
|
253
|
+
return store;
|
|
254
|
+
}
|
|
255
|
+
export function sealPersonalGlossaryReviewRecord(body) {
|
|
256
|
+
const recordBody = bodyWithoutDigest(body, "record_sha256");
|
|
257
|
+
const record = { ...recordBody, record_sha256: glossaryCanonicalSha256(recordBody) };
|
|
258
|
+
if (!validCurrentReviewRecord(record, personalGlossaryReviewRecordsStorageContract()))
|
|
259
|
+
throw new TypeError("review record is invalid");
|
|
260
|
+
return record;
|
|
261
|
+
}
|
|
262
|
+
export function migrateLegacyPendingReviewRecord(record, scope) {
|
|
263
|
+
if (record.status !== "pending" || record.terminal_at !== null || record.expires_at !== null) {
|
|
264
|
+
throw new TypeError("legacy review record is not pending");
|
|
265
|
+
}
|
|
266
|
+
return sealPersonalGlossaryReviewRecord({
|
|
267
|
+
schema_version: RECORD_SCHEMA_VERSION,
|
|
268
|
+
owner: REVIEW_OWNER,
|
|
269
|
+
review_id: record.review_id,
|
|
270
|
+
candidate_id: record.candidate_id,
|
|
271
|
+
candidate_revision: record.candidate_revision,
|
|
272
|
+
candidate_capsule_sha256: record.candidate_capsule_sha256,
|
|
273
|
+
candidate_projection_sha256: record.candidate_projection_sha256,
|
|
274
|
+
host_receipt_sha256: record.host_receipt_sha256,
|
|
275
|
+
cli_decision_sha256: record.cli_decision_sha256,
|
|
276
|
+
semantic_fingerprint: record.semantic_fingerprint,
|
|
277
|
+
generation: record.generation,
|
|
278
|
+
policy_version: record.policy_version,
|
|
279
|
+
scope,
|
|
280
|
+
reason: record.reason,
|
|
281
|
+
status: "pending",
|
|
282
|
+
disposition: null,
|
|
283
|
+
review_record: null,
|
|
284
|
+
reopen_reason: null,
|
|
285
|
+
queued_at: record.queued_at,
|
|
286
|
+
terminal_at: null,
|
|
287
|
+
expires_at: null,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
export function privateWritePersonalGlossaryReviewRecords(pathname, text) {
|
|
291
|
+
const directory = path.dirname(pathname);
|
|
292
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
293
|
+
fs.chmodSync(directory, 0o700);
|
|
294
|
+
const temporary = `${pathname}.tmp.${process.pid}.${Date.now()}`;
|
|
295
|
+
let descriptor = null;
|
|
296
|
+
try {
|
|
297
|
+
descriptor = fs.openSync(temporary, "w", 0o600);
|
|
298
|
+
fs.writeFileSync(descriptor, text, "utf8");
|
|
299
|
+
fs.fsyncSync(descriptor);
|
|
300
|
+
fs.closeSync(descriptor);
|
|
301
|
+
descriptor = null;
|
|
302
|
+
fs.renameSync(temporary, pathname);
|
|
303
|
+
fs.chmodSync(pathname, 0o600);
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
if (descriptor !== null)
|
|
307
|
+
fs.closeSync(descriptor);
|
|
308
|
+
try {
|
|
309
|
+
fs.rmSync(temporary, { force: true });
|
|
310
|
+
}
|
|
311
|
+
catch { /* already absent or renamed */ }
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
export function ensurePersonalGlossaryReviewRecordsPrivate(pathname) {
|
|
316
|
+
const metadata = fs.statSync(pathname);
|
|
317
|
+
if (!metadata.isFile())
|
|
318
|
+
throw new TypeError("stored review records are not a private file");
|
|
319
|
+
if ((metadata.mode & 0o777) === 0o600)
|
|
320
|
+
return;
|
|
321
|
+
fs.chmodSync(pathname, 0o600);
|
|
322
|
+
if ((fs.statSync(pathname).mode & 0o777) !== 0o600)
|
|
323
|
+
throw new TypeError("stored review records could not be made private");
|
|
324
|
+
}
|
|
325
|
+
function readBoundedFile(pathname, maximum) {
|
|
326
|
+
let descriptor = null;
|
|
327
|
+
try {
|
|
328
|
+
descriptor = fs.openSync(pathname, fs.constants.O_RDONLY);
|
|
329
|
+
const metadata = fs.fstatSync(descriptor);
|
|
330
|
+
if (!metadata.isFile() || metadata.size > maximum)
|
|
331
|
+
throw new TypeError("review record store is over bound");
|
|
332
|
+
const bytes = Buffer.allocUnsafe(maximum + 1);
|
|
333
|
+
let offset = 0;
|
|
334
|
+
while (offset < bytes.length) {
|
|
335
|
+
const count = fs.readSync(descriptor, bytes, offset, bytes.length - offset, null);
|
|
336
|
+
if (count === 0)
|
|
337
|
+
break;
|
|
338
|
+
offset += count;
|
|
339
|
+
}
|
|
340
|
+
if (offset > maximum)
|
|
341
|
+
throw new TypeError("review record store is over bound");
|
|
342
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, offset));
|
|
343
|
+
}
|
|
344
|
+
finally {
|
|
345
|
+
if (descriptor !== null)
|
|
346
|
+
fs.closeSync(descriptor);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
export function reviewScope(receipt) {
|
|
350
|
+
const classification = mapping(receipt.classification) ? receipt.classification : null;
|
|
351
|
+
return classification?.scope === "personal" || classification?.scope === "ambiguous" ? classification.scope : null;
|
|
352
|
+
}
|
|
353
|
+
export function recordMatchesProjection(record, projection) {
|
|
354
|
+
return record.candidate_projection_sha256 === projection.projection_sha256 && projection.candidates.some((item) => item.capsule.candidate_id === record.candidate_id && item.capsule.candidate_revision === record.candidate_revision &&
|
|
355
|
+
item.capsule.capsule_sha256 === record.candidate_capsule_sha256 && item.capsule.generation === record.generation &&
|
|
356
|
+
item.capsule.policy_version === record.policy_version);
|
|
357
|
+
}
|
|
358
|
+
export function terminalReviewRecordExpired(record, now) {
|
|
359
|
+
return record.status === "terminal" && Date.parse(record.expires_at) <= Date.parse(now);
|
|
360
|
+
}
|
|
361
|
+
export function readablePersonalGlossaryReviewRecord(record) {
|
|
362
|
+
if (isCurrentReviewRecord(record))
|
|
363
|
+
return record;
|
|
364
|
+
return { ...record, scope: null, disposition: null, review_record: null, reopen_reason: null };
|
|
365
|
+
}
|
|
366
|
+
export function replayNonceDigest(nonce) {
|
|
367
|
+
return glossaryCanonicalSha256({ schema_version: REPLAY_NONCE_SCHEMA_VERSION, nonce });
|
|
368
|
+
}
|
|
369
|
+
export function currentReplayIndex(entries, now) {
|
|
370
|
+
return entries.filter((entry) => Date.parse(entry.expires_at) > Date.parse(now));
|
|
371
|
+
}
|
|
372
|
+
export function replayDigestMap(entries) {
|
|
373
|
+
return new Map(entries.map((entry) => [entry.nonce_sha256, entry.receipt_sha256]));
|
|
374
|
+
}
|
|
375
|
+
export function activeReplayDigestMap(entries, now) {
|
|
376
|
+
return replayDigestMap(currentReplayIndex(entries, now));
|
|
377
|
+
}
|
|
378
|
+
export function personalGlossaryReviewRecordsPath(options = {}) {
|
|
379
|
+
return path.join(defaultProfileDir(options.env ?? process.env, options.platform ?? process.platform), "intermediate", "personal-glossary", personalGlossaryReviewRecordsStorageContract().storeFile);
|
|
380
|
+
}
|
|
381
|
+
export function personalGlossaryTrustedLocalHostPath(options = {}) {
|
|
382
|
+
return path.join(path.dirname(personalGlossaryReviewRecordsPath(options)), personalGlossaryReviewRecordsStorageContract().trustedHostKeyFile);
|
|
383
|
+
}
|
|
384
|
+
export function readPersonalGlossaryTrustedLocalHost(options) {
|
|
385
|
+
let text;
|
|
386
|
+
try {
|
|
387
|
+
text = readBoundedFile(personalGlossaryTrustedLocalHostPath(options), personalGlossaryReviewRecordsStorageContract().trustedHostKeyMaxSerializedUtf8Bytes);
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
const value = JSON.parse(text);
|
|
394
|
+
if (!mapping(value) || !exactKeys(value, ["schema_version", "owner", "subject", "public_key_spki_base64url"]) ||
|
|
395
|
+
value.schema_version !== TRUSTED_HOST_KEY_SCHEMA_VERSION || value.owner !== REVIEW_OWNER ||
|
|
396
|
+
!validPersonalGlossaryReviewMetadataBinding(value.subject) || !boundedText(value.public_key_spki_base64url, 4_096) ||
|
|
397
|
+
!BASE64URL.test(value.public_key_spki_base64url) || text !== `${canonicalGlossaryJson(value)}\n`)
|
|
398
|
+
return null;
|
|
399
|
+
const encoded = Buffer.from(value.public_key_spki_base64url, "base64url");
|
|
400
|
+
const publicKey = createPublicKey({ key: encoded, format: "der", type: "spki" });
|
|
401
|
+
const normalized = publicKey.export({ format: "der", type: "spki" });
|
|
402
|
+
return publicKey.asymmetricKeyType === "ed25519" && Buffer.from(normalized).equals(encoded)
|
|
403
|
+
? { subject: value.subject, publicKey }
|
|
404
|
+
: null;
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
export function readPersonalGlossaryReviewRecords(options = {}) {
|
|
411
|
+
const valueContract = personalGlossaryReviewRecordsStorageContract();
|
|
412
|
+
let text;
|
|
413
|
+
try {
|
|
414
|
+
text = readBoundedFile(personalGlossaryReviewRecordsPath(options), valueContract.storeMaxSerializedUtf8Bytes);
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
return error.code === "ENOENT" ? { status: "missing", store: null } : { status: "corrupt", store: null };
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
const parsed = JSON.parse(text);
|
|
421
|
+
if ((!validLegacyReviewStore(parsed, valueContract) && !validCurrentReviewStore(parsed, valueContract)) || text !== `${canonicalGlossaryJson(parsed)}\n`)
|
|
422
|
+
return { status: "corrupt", store: null };
|
|
423
|
+
return { status: "current", store: parsed };
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return { status: "corrupt", store: null };
|
|
427
|
+
}
|
|
428
|
+
}
|