@sentropic/h2a 0.98.5 → 0.98.7-rc.1
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/dist/cli-command-map.d.ts.map +1 -1
- package/dist/cli-command-map.js +1 -0
- package/dist/cli-command-map.js.map +1 -1
- package/dist/cli-contract.js +2 -2
- package/dist/cli-contract.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +224 -16
- package/dist/cli.js.map +1 -1
- package/dist/hosts/agy.js +1 -1
- package/dist/hosts/agy.js.map +1 -1
- package/dist/hosts/installation-doctor.d.ts +1 -0
- package/dist/hosts/installation-doctor.d.ts.map +1 -1
- package/dist/hosts/installation-doctor.js +1 -1
- package/dist/hosts/installation-doctor.js.map +1 -1
- package/dist/hosts/muse.d.ts.map +1 -1
- package/dist/hosts/muse.js +2 -4
- package/dist/hosts/muse.js.map +1 -1
- package/dist/hosts/native-artifacts.d.ts +33 -0
- package/dist/hosts/native-artifacts.d.ts.map +1 -0
- package/dist/hosts/native-artifacts.js +104 -0
- package/dist/hosts/native-artifacts.js.map +1 -0
- package/dist/hosts/native-catalog.d.ts +19 -0
- package/dist/hosts/native-catalog.d.ts.map +1 -0
- package/dist/hosts/native-catalog.js +14 -0
- package/dist/hosts/native-catalog.js.map +1 -0
- package/dist/hosts/native-convergence.d.ts +132 -0
- package/dist/hosts/native-convergence.d.ts.map +1 -0
- package/dist/hosts/native-convergence.js +86 -0
- package/dist/hosts/native-convergence.js.map +1 -0
- package/dist/hosts/native-files.d.ts +40 -0
- package/dist/hosts/native-files.d.ts.map +1 -0
- package/dist/hosts/native-files.js +131 -0
- package/dist/hosts/native-files.js.map +1 -0
- package/dist/hosts/native-inspection.d.ts +31 -0
- package/dist/hosts/native-inspection.d.ts.map +1 -0
- package/dist/hosts/native-inspection.js +79 -0
- package/dist/hosts/native-inspection.js.map +1 -0
- package/dist/hosts/native-paths.d.ts +11 -0
- package/dist/hosts/native-paths.d.ts.map +1 -0
- package/dist/hosts/native-paths.js +21 -0
- package/dist/hosts/native-paths.js.map +1 -0
- package/dist/hosts/native-proof.d.ts +32 -0
- package/dist/hosts/native-proof.d.ts.map +1 -0
- package/dist/hosts/native-proof.js +31 -0
- package/dist/hosts/native-proof.js.map +1 -0
- package/dist/hosts/native-runtime-record.d.ts +3 -0
- package/dist/hosts/native-runtime-record.d.ts.map +1 -0
- package/dist/hosts/native-runtime-record.js +60 -0
- package/dist/hosts/native-runtime-record.js.map +1 -0
- package/dist/hosts/native-skills.d.ts +24 -0
- package/dist/hosts/native-skills.d.ts.map +1 -0
- package/dist/hosts/native-skills.js +56 -0
- package/dist/hosts/native-skills.js.map +1 -0
- package/dist/hosts/native-toml.d.ts +18 -0
- package/dist/hosts/native-toml.d.ts.map +1 -0
- package/dist/hosts/native-toml.js +91 -0
- package/dist/hosts/native-toml.js.map +1 -0
- package/dist/hosts/operator-notices.d.ts +3 -0
- package/dist/hosts/operator-notices.d.ts.map +1 -0
- package/dist/hosts/operator-notices.js +31 -0
- package/dist/hosts/operator-notices.js.map +1 -0
- package/dist/hosts/plugin.d.ts.map +1 -1
- package/dist/hosts/plugin.js +3 -2
- package/dist/hosts/plugin.js.map +1 -1
- package/dist/hosts/upgrade-equipment.d.ts +42 -0
- package/dist/hosts/upgrade-equipment.d.ts.map +1 -0
- package/dist/hosts/upgrade-equipment.js +26 -0
- package/dist/hosts/upgrade-equipment.js.map +1 -0
- package/dist/hosts/vibe-discovery.d.ts +12 -0
- package/dist/hosts/vibe-discovery.d.ts.map +1 -0
- package/dist/hosts/vibe-discovery.js +33 -0
- package/dist/hosts/vibe-discovery.js.map +1 -0
- package/dist/hosts/vibe-equipment.d.ts +71 -0
- package/dist/hosts/vibe-equipment.d.ts.map +1 -0
- package/dist/hosts/vibe-equipment.js +162 -0
- package/dist/hosts/vibe-equipment.js.map +1 -0
- package/dist/hosts/vibe-mcp.d.ts +32 -0
- package/dist/hosts/vibe-mcp.d.ts.map +1 -0
- package/dist/hosts/vibe-mcp.js +210 -0
- package/dist/hosts/vibe-mcp.js.map +1 -0
- package/dist/hosts/vibe.d.ts +12 -3
- package/dist/hosts/vibe.d.ts.map +1 -1
- package/dist/hosts/vibe.js +15 -7
- package/dist/hosts/vibe.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/drive/consent.d.ts +1 -1
- package/dist/runtime/drive/consent.d.ts.map +1 -1
- package/dist/runtime/drive/consent.js +2 -2
- package/dist/runtime/drive/consent.js.map +1 -1
- package/dist/runtime/drive/index.d.ts +1 -1
- package/dist/runtime/drive/index.d.ts.map +1 -1
- package/dist/runtime/drive/index.js +3 -1
- package/dist/runtime/drive/index.js.map +1 -1
- package/dist/runtime/drumbeat/relaunchers.d.ts +1 -1
- package/dist/runtime/drumbeat/relaunchers.d.ts.map +1 -1
- package/dist/runtime/drumbeat/relaunchers.js +2 -1
- package/dist/runtime/drumbeat/relaunchers.js.map +1 -1
- package/dist/runtime/identity/binding-commit.d.ts +31 -0
- package/dist/runtime/identity/binding-commit.d.ts.map +1 -0
- package/dist/runtime/identity/binding-commit.js +211 -0
- package/dist/runtime/identity/binding-commit.js.map +1 -0
- package/dist/runtime/identity/bindings.d.ts +6 -30
- package/dist/runtime/identity/bindings.d.ts.map +1 -1
- package/dist/runtime/identity/bindings.js +85 -96
- package/dist/runtime/identity/bindings.js.map +1 -1
- package/dist/runtime/identity/durable.d.ts +7 -0
- package/dist/runtime/identity/durable.d.ts.map +1 -0
- package/dist/runtime/identity/durable.js +68 -0
- package/dist/runtime/identity/durable.js.map +1 -0
- package/dist/runtime/identity/live.d.ts +4 -1
- package/dist/runtime/identity/live.d.ts.map +1 -1
- package/dist/runtime/identity/live.js +59 -6
- package/dist/runtime/identity/live.js.map +1 -1
- package/dist/runtime/identity/possession.d.ts +34 -0
- package/dist/runtime/identity/possession.d.ts.map +1 -0
- package/dist/runtime/identity/possession.js +162 -0
- package/dist/runtime/identity/possession.js.map +1 -0
- package/dist/runtime/identity/worker.js +16 -5
- package/dist/runtime/identity/worker.js.map +1 -1
- package/dist/runtime/live/contracts.d.ts +55 -0
- package/dist/runtime/live/contracts.d.ts.map +1 -0
- package/dist/runtime/live/contracts.js +59 -0
- package/dist/runtime/live/contracts.js.map +1 -0
- package/dist/runtime/local-files/identity-fence.d.ts +164 -0
- package/dist/runtime/local-files/identity-fence.d.ts.map +1 -0
- package/dist/runtime/local-files/identity-fence.js +1338 -0
- package/dist/runtime/local-files/identity-fence.js.map +1 -0
- package/dist/runtime/local-files/index.d.ts +1 -0
- package/dist/runtime/local-files/index.d.ts.map +1 -1
- package/dist/runtime/local-files/index.js +1 -0
- package/dist/runtime/local-files/index.js.map +1 -1
- package/dist/runtime/local-files/legacy-identity-evidence.d.ts +26 -0
- package/dist/runtime/local-files/legacy-identity-evidence.d.ts.map +1 -0
- package/dist/runtime/local-files/legacy-identity-evidence.js +234 -0
- package/dist/runtime/local-files/legacy-identity-evidence.js.map +1 -0
- package/dist/runtime/local-files/locks.d.ts +3 -0
- package/dist/runtime/local-files/locks.d.ts.map +1 -1
- package/dist/runtime/local-files/locks.js +6 -5
- package/dist/runtime/local-files/locks.js.map +1 -1
- package/dist/runtime/local-files/orphan-gc.d.ts +3 -0
- package/dist/runtime/local-files/orphan-gc.d.ts.map +1 -0
- package/dist/runtime/local-files/orphan-gc.js +69 -0
- package/dist/runtime/local-files/orphan-gc.js.map +1 -0
- package/dist/runtime/local-files/presence.d.ts +5 -2
- package/dist/runtime/local-files/presence.d.ts.map +1 -1
- package/dist/runtime/local-files/presence.js +32 -29
- package/dist/runtime/local-files/presence.js.map +1 -1
- package/dist/runtime/local-files/recipient-hints.d.ts +10 -0
- package/dist/runtime/local-files/recipient-hints.d.ts.map +1 -0
- package/dist/runtime/local-files/recipient-hints.js +156 -0
- package/dist/runtime/local-files/recipient-hints.js.map +1 -0
- package/dist/runtime/local-files/store.d.ts +1 -1
- package/dist/runtime/local-files/store.d.ts.map +1 -1
- package/dist/runtime/local-files/store.js +12 -1
- package/dist/runtime/local-files/store.js.map +1 -1
- package/dist/runtime/local-files/succession-lock.d.ts +71 -6
- package/dist/runtime/local-files/succession-lock.d.ts.map +1 -1
- package/dist/runtime/local-files/succession-lock.js +226 -31
- package/dist/runtime/local-files/succession-lock.js.map +1 -1
- package/dist/runtime/mcp/handlers.d.ts +1 -0
- package/dist/runtime/mcp/handlers.d.ts.map +1 -1
- package/dist/runtime/mcp/handlers.js +7 -4
- package/dist/runtime/mcp/handlers.js.map +1 -1
- package/dist/runtime/mcp/identity-blocker-snapshot.d.ts +2 -0
- package/dist/runtime/mcp/identity-blocker-snapshot.d.ts.map +1 -0
- package/dist/runtime/mcp/identity-blocker-snapshot.js +98 -0
- package/dist/runtime/mcp/identity-blocker-snapshot.js.map +1 -0
- package/dist/runtime/mcp/identity-state.d.ts +20 -3
- package/dist/runtime/mcp/identity-state.d.ts.map +1 -1
- package/dist/runtime/mcp/identity-state.js +98 -13
- package/dist/runtime/mcp/identity-state.js.map +1 -1
- package/dist/runtime/mcp/notifications.d.ts +2 -1
- package/dist/runtime/mcp/notifications.d.ts.map +1 -1
- package/dist/runtime/mcp/notifications.js +16 -2
- package/dist/runtime/mcp/notifications.js.map +1 -1
- package/dist/runtime/mcp/server.d.ts +3 -0
- package/dist/runtime/mcp/server.d.ts.map +1 -1
- package/dist/runtime/mcp/server.js +31 -15
- package/dist/runtime/mcp/server.js.map +1 -1
- package/dist/runtime/mcp/sessions.d.ts +6 -6
- package/dist/runtime/mcp/sessions.d.ts.map +1 -1
- package/dist/runtime/mcp/sessions.js +18 -6
- package/dist/runtime/mcp/sessions.js.map +1 -1
- package/dist/runtime/mcp/stdio.d.ts.map +1 -1
- package/dist/runtime/mcp/stdio.js +11 -0
- package/dist/runtime/mcp/stdio.js.map +1 -1
- package/dist/runtime/mcp/tools.js +1 -1
- package/dist/runtime/mcp/tools.js.map +1 -1
- package/dist/runtime/remote/client.d.ts +1 -1
- package/dist/runtime/remote/client.d.ts.map +1 -1
- package/dist/runtime/remote/client.js +1 -1
- package/dist/runtime/remote/client.js.map +1 -1
- package/dist/runtime/send.d.ts +10 -0
- package/dist/runtime/send.d.ts.map +1 -1
- package/dist/runtime/send.js +80 -5
- package/dist/runtime/send.js.map +1 -1
- package/dist/runtime/upgrade/index.d.ts +1 -0
- package/dist/runtime/upgrade/index.d.ts.map +1 -1
- package/dist/runtime/upgrade/index.js +1 -0
- package/dist/runtime/upgrade/index.js.map +1 -1
- package/dist/vendor/engram-live/contracts.d.ts +208 -0
- package/dist/vendor/engram-live/contracts.js +558 -0
- package/fixtures/native-host-catalog.json +1 -0
- package/fixtures/native-host-history.json +1261 -0
- package/fixtures/native-host-renderers.json +1 -0
- package/hooks/deny-manual-h2a-cli.mjs +38 -0
- package/hooks/hooks.json +17 -0
- package/hooks/vibe-post-agent.mjs +29 -0
- package/hooks/vibe-pre-tool.mjs +40 -0
- package/package.json +9 -6
- package/scripts/qualification-budget.mjs +56 -0
- package/scripts/qualification-cleanup.mjs +15 -0
- package/scripts/qualification-oracles.mjs +44 -0
- package/scripts/qualification-patch.mjs +21 -0
- package/scripts/qualification-poll-hosts.mjs +19 -0
- package/scripts/qualification-probe.mjs +63 -0
- package/scripts/qualification-rpc.mjs +59 -0
- package/scripts/qualification-vibe.mjs +159 -0
- package/scripts/qualify-host.mjs +143 -0
- package/skills/h2a/SKILL.md +6 -2
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
// Vendored from @sentropic/engram-live 0.1.0 dist/contracts.js — extracted
|
|
2
|
+
// byte-identical from vendor/sentropic-engram-live-0.1.0.tgz (graphify@
|
|
3
|
+
// 70ebdfee, PR rhanka/engram#359; the tarball is removed after vendoring and
|
|
4
|
+
// its sha512 is recorded in the GUARD test of live-registration.test.ts).
|
|
5
|
+
// Only the portable entries H consumes are vendored (this module takes no
|
|
6
|
+
// imports at all); upstream is unpublished and os-bounded (linux), so a
|
|
7
|
+
// file: dependency breaks cross-platform install and packed redistribution.
|
|
8
|
+
// Do not hand-edit.
|
|
9
|
+
// @sentropic/engram-live/contracts — built, data-pure contract export
|
|
10
|
+
// (spec/SPEC_LIVE_GIT_INDEXING.md §3, §11; plan/09 L1b).
|
|
11
|
+
//
|
|
12
|
+
// This module is data-pure: DTOs, constants and validators only. It takes no
|
|
13
|
+
// imports at all — neither node: builtins, host types, model modules nor
|
|
14
|
+
// storage drivers. The executable factory lives in ./integration.js; hosts
|
|
15
|
+
// import only this contract at the adapter boundary and lazily load the
|
|
16
|
+
// factory when they compose live (§3).
|
|
17
|
+
export const CONTRACT_NAME = "@sentropic/engram-live/contracts";
|
|
18
|
+
export const CONTRACT_VERSION = 1;
|
|
19
|
+
export const PRODUCER_NAME = "engram-live";
|
|
20
|
+
export const POLICY_VERSION = "live-git-indexing/1";
|
|
21
|
+
export const CAPABILITY_IDS = [
|
|
22
|
+
"refs",
|
|
23
|
+
"history-dag",
|
|
24
|
+
"github-pr",
|
|
25
|
+
"index-snapshot",
|
|
26
|
+
"working-tree",
|
|
27
|
+
"diffs",
|
|
28
|
+
"code-analysis",
|
|
29
|
+
"retention-requests",
|
|
30
|
+
"reconciliation",
|
|
31
|
+
"publication",
|
|
32
|
+
];
|
|
33
|
+
/** The lot that implements each capability; part of the advertised contract. */
|
|
34
|
+
export const CAPABILITY_PLANNED_LOTS = {
|
|
35
|
+
"refs": "L2a",
|
|
36
|
+
"history-dag": "L2b",
|
|
37
|
+
"github-pr": "P1",
|
|
38
|
+
"index-snapshot": "L4a",
|
|
39
|
+
"working-tree": "L4a",
|
|
40
|
+
"diffs": "L4b",
|
|
41
|
+
"code-analysis": "L5",
|
|
42
|
+
"retention-requests": "R1",
|
|
43
|
+
"reconciliation": "L6b",
|
|
44
|
+
"publication": "L6b",
|
|
45
|
+
};
|
|
46
|
+
export const NOT_IMPLEMENTED_REASON = "engram-live L1b supplies the package/factory seams and grammar loading only; this capability is explicitly unavailable until its lot lands";
|
|
47
|
+
export function describeCapabilities() {
|
|
48
|
+
return CAPABILITY_IDS.map((capability) => ({
|
|
49
|
+
capability,
|
|
50
|
+
state: "unavailable",
|
|
51
|
+
reason: NOT_IMPLEMENTED_REASON,
|
|
52
|
+
plannedLot: CAPABILITY_PLANNED_LOTS[capability],
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
export const COVERAGE_AXES = [
|
|
56
|
+
"refs",
|
|
57
|
+
"dag",
|
|
58
|
+
"pr-metadata",
|
|
59
|
+
"object-availability",
|
|
60
|
+
"pr-attribution",
|
|
61
|
+
"ast",
|
|
62
|
+
"index",
|
|
63
|
+
"working-tree",
|
|
64
|
+
"remote-publication",
|
|
65
|
+
];
|
|
66
|
+
/** Languages with a pinned, packaged tree-sitter WASM grammar (L1b matrix). */
|
|
67
|
+
export const ADVERTISED_LANGUAGES = [
|
|
68
|
+
"c",
|
|
69
|
+
"cpp",
|
|
70
|
+
"go",
|
|
71
|
+
"java",
|
|
72
|
+
"javascript",
|
|
73
|
+
"php",
|
|
74
|
+
"python",
|
|
75
|
+
"ruby",
|
|
76
|
+
"rust",
|
|
77
|
+
"typescript",
|
|
78
|
+
"tsx",
|
|
79
|
+
];
|
|
80
|
+
export const GRAMMAR_UNAVAILABLE_REASON = "no packaged grammar is pinned for this language; AST coverage is explicitly unavailable";
|
|
81
|
+
function isRecord(value) {
|
|
82
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
83
|
+
}
|
|
84
|
+
function isNonEmptyString(value) {
|
|
85
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
86
|
+
}
|
|
87
|
+
function isPositiveInteger(value) {
|
|
88
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
89
|
+
}
|
|
90
|
+
function hasNul(value) {
|
|
91
|
+
return typeof value === "string" && value.includes("\0");
|
|
92
|
+
}
|
|
93
|
+
/** Opaque scoped id as minted by the canonical derivation (prefixed, NUL-free). */
|
|
94
|
+
function isOpaqueId(value, prefix) {
|
|
95
|
+
return isNonEmptyString(value) && !hasNul(value) && value.startsWith(prefix);
|
|
96
|
+
}
|
|
97
|
+
function isUncertaintyList(value) {
|
|
98
|
+
return Array.isArray(value) && value.every((state) => isIdentityUncertaintyState(state));
|
|
99
|
+
}
|
|
100
|
+
// Exact-key enforcement (L6a): every DTO validator rejects unknown keys so
|
|
101
|
+
// a misspelled or smuggled field can never pass as a valid declaration,
|
|
102
|
+
// clone, worktree, batch or revocation.
|
|
103
|
+
function rejectUnknownKeys(value, allowed, path, errors) {
|
|
104
|
+
for (const key of Object.keys(value)) {
|
|
105
|
+
if (!allowed.includes(key))
|
|
106
|
+
errors.push(`${path}.${key} is not a known key`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const LIVE_SCOPE_KEYS = ["tenant", "clone", "worktree"];
|
|
110
|
+
export function validateLiveScope(scope) {
|
|
111
|
+
if (!isRecord(scope))
|
|
112
|
+
return { ok: false, errors: ["scope must be an object"] };
|
|
113
|
+
const errors = [];
|
|
114
|
+
rejectUnknownKeys(scope, LIVE_SCOPE_KEYS, "scope", errors);
|
|
115
|
+
if (!isNonEmptyString(scope.tenant))
|
|
116
|
+
errors.push("scope.tenant must be a non-empty string");
|
|
117
|
+
if (!isNonEmptyString(scope.clone))
|
|
118
|
+
errors.push("scope.clone must be a non-empty string");
|
|
119
|
+
if (scope.worktree !== undefined && !isNonEmptyString(scope.worktree)) {
|
|
120
|
+
errors.push("scope.worktree, when present, must be a non-empty string");
|
|
121
|
+
}
|
|
122
|
+
return { ok: errors.length === 0, errors };
|
|
123
|
+
}
|
|
124
|
+
const WORKSPACE_DECLARATION_KEYS = [
|
|
125
|
+
"tenant",
|
|
126
|
+
"principal",
|
|
127
|
+
"hostFingerprint",
|
|
128
|
+
"identityUncertainty",
|
|
129
|
+
"declarationId",
|
|
130
|
+
"cloneId",
|
|
131
|
+
"worktreeId",
|
|
132
|
+
"authorizedRoots",
|
|
133
|
+
"liveCapability",
|
|
134
|
+
"policyVersion",
|
|
135
|
+
"declarationRevision",
|
|
136
|
+
"serverWorkspaceId",
|
|
137
|
+
"serverIssuer",
|
|
138
|
+
"bindingRevision",
|
|
139
|
+
];
|
|
140
|
+
export function validateWorkspaceDeclaration(declaration) {
|
|
141
|
+
if (!isRecord(declaration))
|
|
142
|
+
return { ok: false, errors: ["declaration must be an object"] };
|
|
143
|
+
const errors = [];
|
|
144
|
+
rejectUnknownKeys(declaration, WORKSPACE_DECLARATION_KEYS, "declaration", errors);
|
|
145
|
+
if (!isNonEmptyString(declaration.tenant) || hasNul(declaration.tenant)) {
|
|
146
|
+
errors.push("declaration.tenant must be a non-empty string without NUL");
|
|
147
|
+
}
|
|
148
|
+
if (!isNonEmptyString(declaration.principal) || hasNul(declaration.principal)) {
|
|
149
|
+
errors.push("declaration.principal must be a non-empty string without NUL");
|
|
150
|
+
}
|
|
151
|
+
if (!isUncertaintyList(declaration.identityUncertainty)) {
|
|
152
|
+
errors.push(`declaration.identityUncertainty must be an array of uncertainty states (${IDENTITY_UNCERTAINTY_STATES.join(", ")})`);
|
|
153
|
+
}
|
|
154
|
+
if (declaration.hostFingerprint !== undefined) {
|
|
155
|
+
if (!isNonEmptyString(declaration.hostFingerprint) || hasNul(declaration.hostFingerprint)) {
|
|
156
|
+
errors.push("declaration.hostFingerprint, when present, must be a non-empty string without NUL");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else if (!isUncertaintyList(declaration.identityUncertainty) ||
|
|
160
|
+
!declaration.identityUncertainty.includes("missing_history")) {
|
|
161
|
+
errors.push('declaration.hostFingerprint is absent: identityUncertainty must explicitly contain "missing_history" (LIVE §12)');
|
|
162
|
+
}
|
|
163
|
+
if (!isOpaqueId(declaration.declarationId, ID_PREFIXES.declaration)) {
|
|
164
|
+
errors.push(`declaration.declarationId must be a non-empty opaque id starting with "${ID_PREFIXES.declaration}"`);
|
|
165
|
+
}
|
|
166
|
+
if (!isOpaqueId(declaration.cloneId, ID_PREFIXES.clone)) {
|
|
167
|
+
errors.push(`declaration.cloneId must be a non-empty opaque id starting with "${ID_PREFIXES.clone}"`);
|
|
168
|
+
}
|
|
169
|
+
if (!isOpaqueId(declaration.worktreeId, ID_PREFIXES.worktree)) {
|
|
170
|
+
errors.push(`declaration.worktreeId must be a non-empty opaque id starting with "${ID_PREFIXES.worktree}"`);
|
|
171
|
+
}
|
|
172
|
+
if (!Array.isArray(declaration.authorizedRoots) || declaration.authorizedRoots.length === 0) {
|
|
173
|
+
errors.push("declaration.authorizedRoots must be a non-empty array");
|
|
174
|
+
}
|
|
175
|
+
else if (declaration.authorizedRoots.some((path) => !isNonEmptyString(path) || hasNul(path))) {
|
|
176
|
+
errors.push("declaration.authorizedRoots entries must be non-empty strings without NUL");
|
|
177
|
+
}
|
|
178
|
+
if (declaration.liveCapability !== "live") {
|
|
179
|
+
errors.push('declaration.liveCapability must be the explicit string "live" (bind/upload is not activation)');
|
|
180
|
+
}
|
|
181
|
+
if (declaration.policyVersion !== POLICY_VERSION) {
|
|
182
|
+
errors.push(`declaration.policyVersion must be the fixed live policy version "${POLICY_VERSION}"`);
|
|
183
|
+
}
|
|
184
|
+
if (!isPositiveInteger(declaration.declarationRevision) ||
|
|
185
|
+
declaration.declarationRevision < INITIAL_DECLARATION_REVISION) {
|
|
186
|
+
errors.push(`declaration.declarationRevision must be an integer >= ${INITIAL_DECLARATION_REVISION}`);
|
|
187
|
+
}
|
|
188
|
+
const hasServerWorkspace = declaration.serverWorkspaceId !== undefined;
|
|
189
|
+
const hasServerIssuer = declaration.serverIssuer !== undefined;
|
|
190
|
+
const hasBindingRevision = declaration.bindingRevision !== undefined;
|
|
191
|
+
if (hasServerWorkspace && (!isNonEmptyString(declaration.serverWorkspaceId) || hasNul(declaration.serverWorkspaceId))) {
|
|
192
|
+
errors.push("declaration.serverWorkspaceId, when present, must be a non-empty string without NUL");
|
|
193
|
+
}
|
|
194
|
+
if (hasServerIssuer && (!isNonEmptyString(declaration.serverIssuer) || hasNul(declaration.serverIssuer))) {
|
|
195
|
+
errors.push("declaration.serverIssuer, when present, must be a non-empty string without NUL");
|
|
196
|
+
}
|
|
197
|
+
if (hasBindingRevision && (!isPositiveInteger(declaration.bindingRevision) ||
|
|
198
|
+
declaration.bindingRevision < INITIAL_BINDING_REVISION)) {
|
|
199
|
+
errors.push(`declaration.bindingRevision, when present, must be an integer >= ${INITIAL_BINDING_REVISION}`);
|
|
200
|
+
}
|
|
201
|
+
// Product live intent binds the complete server/issuer/revision triplet
|
|
202
|
+
// (spec §12): a workspace without its issuer or binding revision, an
|
|
203
|
+
// issuer or revision without the workspace, is not a valid product
|
|
204
|
+
// binding. Absence of all three is a valid local-only declaration
|
|
205
|
+
// requiring no enrollment.
|
|
206
|
+
if ((hasServerWorkspace || hasServerIssuer || hasBindingRevision) &&
|
|
207
|
+
!(hasServerWorkspace && hasServerIssuer && hasBindingRevision)) {
|
|
208
|
+
errors.push("the product binding must be the complete serverWorkspaceId + serverIssuer + bindingRevision triplet: a partial binding is not a valid product intent");
|
|
209
|
+
}
|
|
210
|
+
return { ok: errors.length === 0, errors };
|
|
211
|
+
}
|
|
212
|
+
const FACT_BATCH_KEYS = [
|
|
213
|
+
"scope",
|
|
214
|
+
"producerName",
|
|
215
|
+
"contractVersion",
|
|
216
|
+
"policyVersion",
|
|
217
|
+
"generationId",
|
|
218
|
+
"batchId",
|
|
219
|
+
"batchDigest",
|
|
220
|
+
"facts",
|
|
221
|
+
];
|
|
222
|
+
export function validateFactBatch(batch) {
|
|
223
|
+
if (!isRecord(batch))
|
|
224
|
+
return { ok: false, errors: ["batch must be an object"] };
|
|
225
|
+
const errors = [];
|
|
226
|
+
rejectUnknownKeys(batch, FACT_BATCH_KEYS, "batch", errors);
|
|
227
|
+
const scope = validateLiveScope(batch.scope);
|
|
228
|
+
if (!scope.ok)
|
|
229
|
+
errors.push(...scope.errors.map((error) => `batch.scope: ${error}`));
|
|
230
|
+
if (batch.producerName !== PRODUCER_NAME)
|
|
231
|
+
errors.push(`batch.producerName must be "${PRODUCER_NAME}"`);
|
|
232
|
+
if (batch.contractVersion !== CONTRACT_VERSION) {
|
|
233
|
+
errors.push(`batch.contractVersion must be ${CONTRACT_VERSION}`);
|
|
234
|
+
}
|
|
235
|
+
if (!isNonEmptyString(batch.policyVersion))
|
|
236
|
+
errors.push("batch.policyVersion must be a non-empty string");
|
|
237
|
+
if (!isNonEmptyString(batch.generationId))
|
|
238
|
+
errors.push("batch.generationId must be a non-empty string");
|
|
239
|
+
if (!isNonEmptyString(batch.batchId))
|
|
240
|
+
errors.push("batch.batchId must be a non-empty string");
|
|
241
|
+
if (!isNonEmptyString(batch.batchDigest))
|
|
242
|
+
errors.push("batch.batchDigest must be a non-empty string");
|
|
243
|
+
if (!Array.isArray(batch.facts))
|
|
244
|
+
errors.push("batch.facts must be an array");
|
|
245
|
+
return { ok: errors.length === 0, errors };
|
|
246
|
+
}
|
|
247
|
+
// Same identity, same bytes is idempotent; same identity, different bytes is
|
|
248
|
+
// refused (spec §3). Pure decision over validated batches.
|
|
249
|
+
export function classifyDuplicateBatch(previous, next) {
|
|
250
|
+
if (previous.batchId !== next.batchId) {
|
|
251
|
+
throw new Error("classifyDuplicateBatch compares the same batch identity only");
|
|
252
|
+
}
|
|
253
|
+
return previous.batchDigest === next.batchDigest ? "idempotent" : "refused";
|
|
254
|
+
}
|
|
255
|
+
// — L6a: explicit host declarations and clone identity
|
|
256
|
+
// (spec/SPEC_LIVE_GIT_INDEXING.md §§2–3, 10, 12; plan/09 L6a; owner Q4 A).
|
|
257
|
+
//
|
|
258
|
+
// Consumer contract for the h2a (H) and cluster-mesh/sentropic (S) seats.
|
|
259
|
+
// H owns durable local declarations plus clone/worktree resolution
|
|
260
|
+
// (live-registration.ts, live-declarations.ts, data-pure DTOs); S owns the
|
|
261
|
+
// registration persistence, product declarations and migrations; engram
|
|
262
|
+
// owns only this data-pure contract and the pure functions in
|
|
263
|
+
// ./registration.js. Read this block before implementing either host side.
|
|
264
|
+
//
|
|
265
|
+
// - Explicit opt-in, once per workspace, through h2a/cluster-mesh. Only
|
|
266
|
+
// liveCapability "live" (EXPLICIT_LIVE_CAPABILITY) activates collection.
|
|
267
|
+
// Bind, upload, code-workspace creation, session opening or session
|
|
268
|
+
// ending never activate or remove a declaration
|
|
269
|
+
// (NON_ACTIVATING_LIFECYCLE_KINDS). Replay of a stored declaration after
|
|
270
|
+
// projection loss resumes work without renewed activation.
|
|
271
|
+
// - Engram neutrality: this module takes no imports of any kind (no h2a,
|
|
272
|
+
// no sentropic, no storage driver, no network); registration.ts adds pure
|
|
273
|
+
// functions with no host state, no storage and no network. Durable state
|
|
274
|
+
// belongs to the hosts — local h2a state outside source roots plus
|
|
275
|
+
// app-operated product persistence — independent of the live projection.
|
|
276
|
+
// - Opaque identity triangle: every declaration carries a declarationId
|
|
277
|
+
// (unique per declaration, stable across revisions) attached to its
|
|
278
|
+
// cloneId and worktreeId. A revocation names exactly one declaration;
|
|
279
|
+
// withdrawing a whole clone means revoking each of its declarations,
|
|
280
|
+
// each revocation naming its exact declarationId.
|
|
281
|
+
// - Workspace fingerprint vs clone identity: the public h2a `ws:`
|
|
282
|
+
// fingerprint stays on the declaration (hostFingerprint) and is never
|
|
283
|
+
// the sole clone key. The clone key is the (hostScope, common-dir
|
|
284
|
+
// realpath, common-dir project fingerprint) triple. Two clones with an
|
|
285
|
+
// equal project fingerprint remain distinct; linked worktrees sharing one
|
|
286
|
+
// common-dir share one clone with isolated worktree identities. The
|
|
287
|
+
// common-dir realpath is host-local and never published to sentropic:
|
|
288
|
+
// share only the opaque cloneId.
|
|
289
|
+
// - Identity uncertainty (LIVE §12): a repo without history (unborn HEAD)
|
|
290
|
+
// or with grafted (shallow) history declares explicit uncertainty
|
|
291
|
+
// states; a fingerprint may be absent only under "missing_history".
|
|
292
|
+
// Uncertainty never merges clones: the hostScope + common-dir realpath
|
|
293
|
+
// key components keep them distinct.
|
|
294
|
+
// - Revisions increment by exactly one, preserve tenant/principal/
|
|
295
|
+
// hostFingerprint, and preserve the product binding unless the binding
|
|
296
|
+
// itself changes: a revision outside the binding (roots, policy version)
|
|
297
|
+
// keeps it, while a changed serverWorkspaceId or serverIssuer — or an
|
|
298
|
+
// explicit revalidation — requires a strictly increasing
|
|
299
|
+
// bindingRevision (the first binding carries exactly 1). The host must
|
|
300
|
+
// additionally refuse to redirect in-flight collection/publication on
|
|
301
|
+
// rebind.
|
|
302
|
+
// - Revocation is a private-purge request: the host stops work, rejects
|
|
303
|
+
// delayed jobs, and purges private snapshots, derived indexes and
|
|
304
|
+
// decryptability, while preserving authorized committed history. Purging
|
|
305
|
+
// never deletes committed facts.
|
|
306
|
+
//
|
|
307
|
+
// FROZEN L6a CONVENTIONS — both seats align on these values:
|
|
308
|
+
// - POLICY_VERSION ("live-git-indexing/1") is the one policy name stamped
|
|
309
|
+
// on every live declaration; h2a's provisional "live-git-1" retires.
|
|
310
|
+
// - INITIAL_DECLARATION_REVISION (1): the explicit activation itself is
|
|
311
|
+
// the first revision; there is no revision-0 declaration.
|
|
312
|
+
// - INITIAL_BINDING_REVISION (1): the first product binding carries 1,
|
|
313
|
+
// and every rebind/revalidation carries a strictly greater
|
|
314
|
+
// bindingRevision.
|
|
315
|
+
// - Identity uncertainty vocabulary: "missing_history", "shallow_history"
|
|
316
|
+
// (the labels H already produces under LIVE §12).
|
|
317
|
+
// - Opaque ids are minted ONLY through the canonical derivation below
|
|
318
|
+
// (canonicalIdKey + the sha256 minting in ./registration.js): the same
|
|
319
|
+
// inputs give H, S and E byte-identical cloneId/worktreeId/
|
|
320
|
+
// declarationId. MIGRATION (H): identifiers already persisted by h2a
|
|
321
|
+
// were hashed from newline-joined "hostScope\nrealpath\nfingerprint"
|
|
322
|
+
// (clone), "hostScope\ngitDir" (worktree) and
|
|
323
|
+
// "hostScope\ncommonDirRealpath\nws:" (declaration); that scheme is
|
|
324
|
+
// ambiguous and diverges from this contract. Re-derive every persisted
|
|
325
|
+
// id with the canonical derivation and rewrite the local h2a store; no
|
|
326
|
+
// old id has crossed to sentropic (L6a is not consumed yet), so the
|
|
327
|
+
// migration is a purely local rewrite with no product-side replay.
|
|
328
|
+
/** The only capability value that activates live collection (Q4 A). */
|
|
329
|
+
export const EXPLICIT_LIVE_CAPABILITY = "live";
|
|
330
|
+
/**
|
|
331
|
+
* Lifecycle kinds that never activate or remove a live declaration:
|
|
332
|
+
* enrollment bind, transcript upload, code-workspace creation and agent
|
|
333
|
+
* session open/end are distinct capabilities (spec §12).
|
|
334
|
+
*/
|
|
335
|
+
export const NON_ACTIVATING_LIFECYCLE_KINDS = [
|
|
336
|
+
"bind",
|
|
337
|
+
"upload",
|
|
338
|
+
"code-workspace-create",
|
|
339
|
+
"session-open",
|
|
340
|
+
"session-end",
|
|
341
|
+
];
|
|
342
|
+
export function isNonActivatingLifecycleKind(value) {
|
|
343
|
+
return (typeof value === "string" &&
|
|
344
|
+
NON_ACTIVATING_LIFECYCLE_KINDS.includes(value));
|
|
345
|
+
}
|
|
346
|
+
export const OID_ALGORITHMS = ["sha1", "sha256"];
|
|
347
|
+
export const REVOCATION_REASONS = ["workspace-remove", "owner-request", "policy-withdrawn"];
|
|
348
|
+
export function isRevocationReason(value) {
|
|
349
|
+
return (typeof value === "string" &&
|
|
350
|
+
REVOCATION_REASONS.includes(value));
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Explicit identity-coverage uncertainty states (LIVE §12). A repo with no
|
|
354
|
+
* history (unborn HEAD) declares "missing_history"; a grafted (shallow)
|
|
355
|
+
* history declares "shallow_history" while keeping its fingerprints, whose
|
|
356
|
+
* coverage stays uncertain. An absent fingerprint is valid only under
|
|
357
|
+
* "missing_history"; uncertainty never merges clones.
|
|
358
|
+
*/
|
|
359
|
+
export const IDENTITY_UNCERTAINTY_STATES = ["missing_history", "shallow_history"];
|
|
360
|
+
export function isIdentityUncertaintyState(value) {
|
|
361
|
+
return (typeof value === "string" &&
|
|
362
|
+
IDENTITY_UNCERTAINTY_STATES.includes(value));
|
|
363
|
+
}
|
|
364
|
+
/** First declaration revision: the explicit activation is revision 1. */
|
|
365
|
+
export const INITIAL_DECLARATION_REVISION = 1;
|
|
366
|
+
/** First product binding revision; every rebind carries a strictly greater one. */
|
|
367
|
+
export const INITIAL_BINDING_REVISION = 1;
|
|
368
|
+
/** The three opaque id kinds minted by the canonical derivation. */
|
|
369
|
+
export const ID_KINDS = ["clone", "worktree", "declaration"];
|
|
370
|
+
/** Opaque id prefixes; a valid id of a kind starts with its prefix. */
|
|
371
|
+
export const ID_PREFIXES = {
|
|
372
|
+
clone: "clone:",
|
|
373
|
+
worktree: "wt:",
|
|
374
|
+
declaration: "decl:",
|
|
375
|
+
};
|
|
376
|
+
export function isIdKind(value) {
|
|
377
|
+
return (typeof value === "string" &&
|
|
378
|
+
ID_KINDS.includes(value));
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* THE canonical identity serialization shared by every live opaque id
|
|
382
|
+
* (cloneId, worktreeId, declarationId). Both seats and engram mint ids
|
|
383
|
+
* from this exact function, so the same identity components always give
|
|
384
|
+
* the same id on every side. The serialization is unambiguous: a JSON
|
|
385
|
+
* array of the kind plus the components in order, so no component value
|
|
386
|
+
* can shift a boundary. NUL is refused outright in every component, so the
|
|
387
|
+
* ambiguous couples ("a\0b", "c") vs ("a", "b\0c") can never be
|
|
388
|
+
* constructed. Pure and total over valid inputs; throws on malformed
|
|
389
|
+
* components so no silent normalization can produce a colliding key.
|
|
390
|
+
*/
|
|
391
|
+
export function canonicalIdKey(kind, components) {
|
|
392
|
+
if (!isIdKind(kind)) {
|
|
393
|
+
throw new Error(`canonicalIdKey requires a known id kind (${ID_KINDS.join(", ")})`);
|
|
394
|
+
}
|
|
395
|
+
for (const component of components) {
|
|
396
|
+
if (typeof component !== "string") {
|
|
397
|
+
throw new Error("canonicalIdKey requires string components");
|
|
398
|
+
}
|
|
399
|
+
if (component.includes("\0")) {
|
|
400
|
+
throw new Error("canonicalIdKey refuses NUL in identity components");
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return JSON.stringify([kind, ...components]);
|
|
404
|
+
}
|
|
405
|
+
const CLONE_IDENTITY_KEYS = [
|
|
406
|
+
"hostScope",
|
|
407
|
+
"commonDirRealpath",
|
|
408
|
+
"commonDirProjectFingerprint",
|
|
409
|
+
"cloneId",
|
|
410
|
+
"oidAlgorithm",
|
|
411
|
+
"identityUncertainty",
|
|
412
|
+
];
|
|
413
|
+
const WORKTREE_IDENTITY_KEYS = [
|
|
414
|
+
"cloneId",
|
|
415
|
+
"worktreeId",
|
|
416
|
+
"worktreePath",
|
|
417
|
+
"gitDir",
|
|
418
|
+
"worktreeFingerprint",
|
|
419
|
+
"identityUncertainty",
|
|
420
|
+
];
|
|
421
|
+
const PRIVATE_PURGE_REQUEST_KEYS = [
|
|
422
|
+
"purgePrivateSnapshots",
|
|
423
|
+
"purgeDerivedIndexes",
|
|
424
|
+
"revokeDecryptability",
|
|
425
|
+
"preserveCommittedHistory",
|
|
426
|
+
];
|
|
427
|
+
const DECLARATION_REVOCATION_KEYS = [
|
|
428
|
+
"declarationId",
|
|
429
|
+
"tenant",
|
|
430
|
+
"cloneId",
|
|
431
|
+
"worktreeId",
|
|
432
|
+
"declarationRevision",
|
|
433
|
+
"reason",
|
|
434
|
+
"requestPrivatePurge",
|
|
435
|
+
];
|
|
436
|
+
/**
|
|
437
|
+
* Lexical normalization applied to the host-resolved common-dir realpath
|
|
438
|
+
* before key derivation: trailing slashes are stripped (the filesystem
|
|
439
|
+
* root alone is kept). The host resolves symlinks to the realpath before
|
|
440
|
+
* calling; this function performs no I/O.
|
|
441
|
+
*/
|
|
442
|
+
export function normalizeCommonDirRealpath(realpath) {
|
|
443
|
+
if (realpath.length > 1) {
|
|
444
|
+
const stripped = realpath.replace(/\/+$/, "");
|
|
445
|
+
return stripped.length === 0 ? "/" : stripped;
|
|
446
|
+
}
|
|
447
|
+
return realpath;
|
|
448
|
+
}
|
|
449
|
+
export function validateCloneIdentity(identity) {
|
|
450
|
+
if (!isRecord(identity))
|
|
451
|
+
return { ok: false, errors: ["cloneIdentity must be an object"] };
|
|
452
|
+
const errors = [];
|
|
453
|
+
rejectUnknownKeys(identity, CLONE_IDENTITY_KEYS, "cloneIdentity", errors);
|
|
454
|
+
if (!isNonEmptyString(identity.hostScope) || hasNul(identity.hostScope)) {
|
|
455
|
+
errors.push("cloneIdentity.hostScope must be a non-empty string without NUL");
|
|
456
|
+
}
|
|
457
|
+
if (!isNonEmptyString(identity.commonDirRealpath) || hasNul(identity.commonDirRealpath)) {
|
|
458
|
+
errors.push("cloneIdentity.commonDirRealpath must be a non-empty string without NUL");
|
|
459
|
+
}
|
|
460
|
+
if (identity.commonDirProjectFingerprint !== undefined &&
|
|
461
|
+
(!isNonEmptyString(identity.commonDirProjectFingerprint) || hasNul(identity.commonDirProjectFingerprint))) {
|
|
462
|
+
errors.push("cloneIdentity.commonDirProjectFingerprint, when present, must be a non-empty string without NUL");
|
|
463
|
+
}
|
|
464
|
+
else if (identity.commonDirProjectFingerprint === undefined &&
|
|
465
|
+
(!isUncertaintyList(identity.identityUncertainty) ||
|
|
466
|
+
!identity.identityUncertainty.includes("missing_history"))) {
|
|
467
|
+
errors.push('cloneIdentity.commonDirProjectFingerprint is absent: identityUncertainty must explicitly contain "missing_history" (LIVE §12)');
|
|
468
|
+
}
|
|
469
|
+
if (!isOpaqueId(identity.cloneId, ID_PREFIXES.clone)) {
|
|
470
|
+
errors.push(`cloneIdentity.cloneId must be a non-empty opaque id starting with "${ID_PREFIXES.clone}"`);
|
|
471
|
+
}
|
|
472
|
+
if (identity.oidAlgorithm !== "sha1" && identity.oidAlgorithm !== "sha256") {
|
|
473
|
+
errors.push('cloneIdentity.oidAlgorithm must be "sha1" or "sha256"');
|
|
474
|
+
}
|
|
475
|
+
if (!isUncertaintyList(identity.identityUncertainty)) {
|
|
476
|
+
errors.push(`cloneIdentity.identityUncertainty must be an array of uncertainty states (${IDENTITY_UNCERTAINTY_STATES.join(", ")})`);
|
|
477
|
+
}
|
|
478
|
+
return { ok: errors.length === 0, errors };
|
|
479
|
+
}
|
|
480
|
+
export function validateWorktreeIdentity(identity) {
|
|
481
|
+
if (!isRecord(identity))
|
|
482
|
+
return { ok: false, errors: ["worktreeIdentity must be an object"] };
|
|
483
|
+
const errors = [];
|
|
484
|
+
rejectUnknownKeys(identity, WORKTREE_IDENTITY_KEYS, "worktreeIdentity", errors);
|
|
485
|
+
if (!isOpaqueId(identity.cloneId, ID_PREFIXES.clone)) {
|
|
486
|
+
errors.push(`worktreeIdentity.cloneId must be a non-empty opaque id starting with "${ID_PREFIXES.clone}"`);
|
|
487
|
+
}
|
|
488
|
+
if (!isOpaqueId(identity.worktreeId, ID_PREFIXES.worktree)) {
|
|
489
|
+
errors.push(`worktreeIdentity.worktreeId must be a non-empty opaque id starting with "${ID_PREFIXES.worktree}"`);
|
|
490
|
+
}
|
|
491
|
+
if (!isNonEmptyString(identity.worktreePath) || hasNul(identity.worktreePath)) {
|
|
492
|
+
errors.push("worktreeIdentity.worktreePath must be a non-empty string without NUL");
|
|
493
|
+
}
|
|
494
|
+
if (!isNonEmptyString(identity.gitDir) || hasNul(identity.gitDir)) {
|
|
495
|
+
errors.push("worktreeIdentity.gitDir must be a non-empty string without NUL");
|
|
496
|
+
}
|
|
497
|
+
if (identity.worktreeFingerprint !== undefined &&
|
|
498
|
+
(!isNonEmptyString(identity.worktreeFingerprint) || hasNul(identity.worktreeFingerprint))) {
|
|
499
|
+
errors.push("worktreeIdentity.worktreeFingerprint, when present, must be a non-empty string without NUL");
|
|
500
|
+
}
|
|
501
|
+
else if (identity.worktreeFingerprint === undefined &&
|
|
502
|
+
(!isUncertaintyList(identity.identityUncertainty) ||
|
|
503
|
+
!identity.identityUncertainty.includes("missing_history"))) {
|
|
504
|
+
errors.push('worktreeIdentity.worktreeFingerprint is absent: identityUncertainty must explicitly contain "missing_history" (LIVE §12)');
|
|
505
|
+
}
|
|
506
|
+
if (!isUncertaintyList(identity.identityUncertainty)) {
|
|
507
|
+
errors.push(`worktreeIdentity.identityUncertainty must be an array of uncertainty states (${IDENTITY_UNCERTAINTY_STATES.join(", ")})`);
|
|
508
|
+
}
|
|
509
|
+
return { ok: errors.length === 0, errors };
|
|
510
|
+
}
|
|
511
|
+
export function validatePrivatePurgeRequest(request) {
|
|
512
|
+
if (!isRecord(request))
|
|
513
|
+
return { ok: false, errors: ["requestPrivatePurge must be an object"] };
|
|
514
|
+
const errors = [];
|
|
515
|
+
rejectUnknownKeys(request, PRIVATE_PURGE_REQUEST_KEYS, "requestPrivatePurge", errors);
|
|
516
|
+
if (request.purgePrivateSnapshots !== true) {
|
|
517
|
+
errors.push("requestPrivatePurge.purgePrivateSnapshots must be true (revocation always purges private snapshots)");
|
|
518
|
+
}
|
|
519
|
+
if (request.purgeDerivedIndexes !== true) {
|
|
520
|
+
errors.push("requestPrivatePurge.purgeDerivedIndexes must be true (revocation always purges derived indexes)");
|
|
521
|
+
}
|
|
522
|
+
if (request.revokeDecryptability !== true) {
|
|
523
|
+
errors.push("requestPrivatePurge.revokeDecryptability must be true (revocation always revokes decryptability)");
|
|
524
|
+
}
|
|
525
|
+
if (request.preserveCommittedHistory !== true) {
|
|
526
|
+
errors.push("requestPrivatePurge.preserveCommittedHistory must be true (revocation never deletes committed history)");
|
|
527
|
+
}
|
|
528
|
+
return { ok: errors.length === 0, errors };
|
|
529
|
+
}
|
|
530
|
+
export function validateDeclarationRevocation(revocation) {
|
|
531
|
+
if (!isRecord(revocation))
|
|
532
|
+
return { ok: false, errors: ["revocation must be an object"] };
|
|
533
|
+
const errors = [];
|
|
534
|
+
rejectUnknownKeys(revocation, DECLARATION_REVOCATION_KEYS, "revocation", errors);
|
|
535
|
+
if (!isOpaqueId(revocation.declarationId, ID_PREFIXES.declaration)) {
|
|
536
|
+
errors.push(`revocation.declarationId must be a non-empty opaque id starting with "${ID_PREFIXES.declaration}"`);
|
|
537
|
+
}
|
|
538
|
+
if (!isNonEmptyString(revocation.tenant) || hasNul(revocation.tenant)) {
|
|
539
|
+
errors.push("revocation.tenant must be a non-empty string without NUL");
|
|
540
|
+
}
|
|
541
|
+
if (!isOpaqueId(revocation.cloneId, ID_PREFIXES.clone)) {
|
|
542
|
+
errors.push(`revocation.cloneId must be a non-empty opaque id starting with "${ID_PREFIXES.clone}"`);
|
|
543
|
+
}
|
|
544
|
+
if (!isOpaqueId(revocation.worktreeId, ID_PREFIXES.worktree)) {
|
|
545
|
+
errors.push(`revocation.worktreeId must be a non-empty opaque id starting with "${ID_PREFIXES.worktree}"`);
|
|
546
|
+
}
|
|
547
|
+
if (!isPositiveInteger(revocation.declarationRevision) ||
|
|
548
|
+
revocation.declarationRevision < INITIAL_DECLARATION_REVISION) {
|
|
549
|
+
errors.push(`revocation.declarationRevision must be an integer >= ${INITIAL_DECLARATION_REVISION}`);
|
|
550
|
+
}
|
|
551
|
+
if (!isRevocationReason(revocation.reason)) {
|
|
552
|
+
errors.push(`revocation.reason must be one of ${REVOCATION_REASONS.join(", ")}`);
|
|
553
|
+
}
|
|
554
|
+
const purge = validatePrivatePurgeRequest(revocation.requestPrivatePurge);
|
|
555
|
+
if (!purge.ok)
|
|
556
|
+
errors.push(...purge.errors.map((error) => `revocation.${error}`));
|
|
557
|
+
return { ok: errors.length === 0, errors };
|
|
558
|
+
}
|