@dharma-ai-labs/agent-fabric 0.2.186 → 0.2.187
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/dist/index.d.ts +74 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +296 -12
- package/dist/index.js.map +1 -1
- package/dist/repositoryCandidateSync.d.ts +109 -2
- package/dist/repositoryCandidateSync.d.ts.map +1 -1
- package/dist/repositoryCandidateSync.js +327 -22
- package/dist/repositoryCandidateSync.js.map +1 -1
- package/dist/repositoryCandidateSync.test.js +912 -6
- package/dist/repositoryCandidateSync.test.js.map +1 -1
- package/dist/repositoryKnowledgeRetention.test.js +319 -3
- package/dist/repositoryKnowledgeRetention.test.js.map +1 -1
- package/dist/repositorySourceInventoryClient.d.ts +1 -0
- package/dist/repositorySourceInventoryClient.d.ts.map +1 -1
- package/dist/repositorySourceInventoryClient.js +4 -1
- package/dist/repositorySourceInventoryClient.js.map +1 -1
- package/dist/repositorySourceRestoration.d.ts +47 -0
- package/dist/repositorySourceRestoration.d.ts.map +1 -0
- package/dist/repositorySourceRestoration.js +140 -0
- package/dist/repositorySourceRestoration.js.map +1 -0
- package/dist/repositorySourceRestorationCommand.test.d.ts +2 -0
- package/dist/repositorySourceRestorationCommand.test.d.ts.map +1 -0
- package/dist/repositorySourceRestorationCommand.test.js +754 -0
- package/dist/repositorySourceRestorationCommand.test.js.map +1 -0
- package/dist/repositorySourceSync.d.ts +1 -0
- package/dist/repositorySourceSync.d.ts.map +1 -1
- package/dist/repositorySourceSync.js +10 -4
- package/dist/repositorySourceSync.js.map +1 -1
- package/dist/repositorySourceSync.test.js +38 -4
- package/dist/repositorySourceSync.test.js.map +1 -1
- package/dist/schemas/repository-candidate-fence-v1.schema.json +55 -0
- package/dist/schemas/repository-candidate-upload-v1.schema.json +60 -0
- package/dist/schemas/repository-knowledge-restoration-v1.schema.json +16 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +3 -1
- package/dist/usage.js.map +1 -1
- package/package.json +1 -1
|
@@ -30,9 +30,65 @@ type CandidateOutbox = {
|
|
|
30
30
|
expectedLatestSourceFingerprint?: string;
|
|
31
31
|
};
|
|
32
32
|
candidateId: string | null;
|
|
33
|
-
state: 'pending_upload' | RepositoryCandidateState;
|
|
33
|
+
state: 'pending_upload' | 'fenced' | RepositoryCandidateState;
|
|
34
34
|
releaseId: string | null;
|
|
35
|
+
restoration?: RestorationCheckpoint;
|
|
36
|
+
fence?: RepositoryCandidateFence;
|
|
37
|
+
fenceRequestGenerationId?: string;
|
|
38
|
+
registryReceiptHash?: string;
|
|
35
39
|
};
|
|
40
|
+
type RestorationCheckpoint = {
|
|
41
|
+
planHash: string;
|
|
42
|
+
localSnapshotHash: string;
|
|
43
|
+
baselineSnapshotHash: string | null;
|
|
44
|
+
previousOperationId: string | null;
|
|
45
|
+
priorReleaseId: string;
|
|
46
|
+
attemptId?: string;
|
|
47
|
+
headCandidateId?: string;
|
|
48
|
+
};
|
|
49
|
+
type RestorationContext = {
|
|
50
|
+
headCandidateId: string;
|
|
51
|
+
priorReleaseId: string;
|
|
52
|
+
};
|
|
53
|
+
type RepositoryCandidateFence = {
|
|
54
|
+
fenceId: string;
|
|
55
|
+
operationId: string;
|
|
56
|
+
workspaceId: string;
|
|
57
|
+
repositoryBindingId: string;
|
|
58
|
+
repositoryAgentId: string;
|
|
59
|
+
sourceSnapshotHash: string;
|
|
60
|
+
sourceManifestHash: string;
|
|
61
|
+
policyGenerationId: string;
|
|
62
|
+
requestPolicyGenerationId?: string;
|
|
63
|
+
state: 'fenced';
|
|
64
|
+
createdAt: string;
|
|
65
|
+
};
|
|
66
|
+
export declare function repositoryCandidateOperationId(input: {
|
|
67
|
+
scope: RepositoryCandidateScope;
|
|
68
|
+
snapshotHash: string;
|
|
69
|
+
sourceManifestHash: string;
|
|
70
|
+
consolidation: CandidateOutbox['consolidation'];
|
|
71
|
+
recoveryAttempt?: boolean;
|
|
72
|
+
restorationAttempt?: string;
|
|
73
|
+
restorationContext?: RestorationContext;
|
|
74
|
+
}): string;
|
|
75
|
+
export declare function checkpointRepositoryRestorationRegistry(input: {
|
|
76
|
+
outboxRoot: string;
|
|
77
|
+
scope: RepositoryCandidateScope;
|
|
78
|
+
repositoryPackage: {
|
|
79
|
+
state: string;
|
|
80
|
+
candidateId: string | null;
|
|
81
|
+
operationId: string | null;
|
|
82
|
+
snapshotHash: string | null;
|
|
83
|
+
releaseId: string | null;
|
|
84
|
+
sourceManifestHash: string | null;
|
|
85
|
+
consolidationMode: string | null;
|
|
86
|
+
localBaselineSnapshotHash?: string | null;
|
|
87
|
+
publishedLocalSnapshotHash?: string | null;
|
|
88
|
+
pendingLocalSnapshotHash?: string | null;
|
|
89
|
+
pendingLocalOperationId?: string | null;
|
|
90
|
+
};
|
|
91
|
+
}): Promise<void>;
|
|
36
92
|
export declare function synchronizeRepositoryCandidate(input: {
|
|
37
93
|
transport: RepositoryCandidateTransport;
|
|
38
94
|
outboxRoot: string;
|
|
@@ -41,7 +97,51 @@ export declare function synchronizeRepositoryCandidate(input: {
|
|
|
41
97
|
initialRepository: boolean;
|
|
42
98
|
expectedLatestSourceFingerprint?: string;
|
|
43
99
|
blockedCandidate?: RepositoryCandidateReceipt;
|
|
100
|
+
restoration?: RestorationCheckpoint;
|
|
44
101
|
}): Promise<RepositoryCandidateReceipt>;
|
|
102
|
+
export declare function recoverRepositoryRestorationCandidate(input: {
|
|
103
|
+
transport: Pick<RepositoryCandidateTransport, 'signedGet'>;
|
|
104
|
+
outboxRoot: string;
|
|
105
|
+
scope: RepositoryCandidateScope;
|
|
106
|
+
planHash: string;
|
|
107
|
+
}): Promise<{
|
|
108
|
+
planHash: string;
|
|
109
|
+
localSnapshotHash: string;
|
|
110
|
+
baselineSnapshotHash: string | null;
|
|
111
|
+
previousOperationId: string | null;
|
|
112
|
+
priorReleaseId: string;
|
|
113
|
+
attemptId?: string;
|
|
114
|
+
headCandidateId?: string;
|
|
115
|
+
candidate: RepositoryCandidateReceipt;
|
|
116
|
+
sourceManifestHash: string;
|
|
117
|
+
} | null>;
|
|
118
|
+
export declare function checkpointRepositoryRestorationCandidate(input: {
|
|
119
|
+
outboxRoot: string;
|
|
120
|
+
scope: RepositoryCandidateScope;
|
|
121
|
+
recovered: NonNullable<Awaited<ReturnType<typeof recoverRepositoryRestorationCandidate>>>;
|
|
122
|
+
}): Promise<RepositoryCandidateReceipt>;
|
|
123
|
+
export declare function readRepositoryRestorationAttempt(input: {
|
|
124
|
+
outboxRoot: string;
|
|
125
|
+
scope: RepositoryCandidateScope;
|
|
126
|
+
}): Promise<string | undefined>;
|
|
127
|
+
export declare function fenceRepositoryRestorationUpload(input: {
|
|
128
|
+
transport: RepositoryCandidateTransport;
|
|
129
|
+
outboxRoot: string;
|
|
130
|
+
scope: RepositoryCandidateScope;
|
|
131
|
+
planHash: string;
|
|
132
|
+
policyGenerationId: string;
|
|
133
|
+
expectedPreviousOperationId: string | null;
|
|
134
|
+
}): Promise<{
|
|
135
|
+
checkpointHash: string;
|
|
136
|
+
pending: CandidateOutbox;
|
|
137
|
+
fence: RepositoryCandidateFence | null;
|
|
138
|
+
candidate: RepositoryCandidateReceipt | null;
|
|
139
|
+
}>;
|
|
140
|
+
export declare function checkpointRepositoryRestorationFence(input: {
|
|
141
|
+
outboxRoot: string;
|
|
142
|
+
scope: RepositoryCandidateScope;
|
|
143
|
+
fenced: Awaited<ReturnType<typeof fenceRepositoryRestorationUpload>>;
|
|
144
|
+
}): Promise<RepositoryCandidateFence>;
|
|
45
145
|
export declare function pollRepositoryCandidate(input: {
|
|
46
146
|
transport: Pick<RepositoryCandidateTransport, 'signedGet'>;
|
|
47
147
|
outboxRoot: string;
|
|
@@ -53,6 +153,13 @@ export declare function adoptRepositoryCandidate(input: {
|
|
|
53
153
|
sourceManifestHash: string;
|
|
54
154
|
consolidationMode: CandidateOutbox['consolidation']['mode'];
|
|
55
155
|
candidate: RepositoryCandidateReceipt;
|
|
56
|
-
}): Promise<RepositoryCandidateReceipt
|
|
156
|
+
}): Promise<RepositoryCandidateReceipt & {
|
|
157
|
+
restorationRegistryBaseline?: {
|
|
158
|
+
localBaselineSnapshotHash: string | null;
|
|
159
|
+
publishedLocalSnapshotHash: string | null;
|
|
160
|
+
pendingLocalSnapshotHash: null;
|
|
161
|
+
pendingLocalOperationId: null;
|
|
162
|
+
};
|
|
163
|
+
}>;
|
|
57
164
|
export {};
|
|
58
165
|
//# sourceMappingURL=repositoryCandidateSync.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repositoryCandidateSync.d.ts","sourceRoot":"","sources":["../src/repositoryCandidateSync.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"repositoryCandidateSync.d.ts","sourceRoot":"","sources":["../src/repositoryCandidateSync.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,17 +1,49 @@
|
|
|
1
1
|
import { constants } from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { link, lstat, mkdir, open, readFile, rename, rm } from 'node:fs/promises';
|
|
3
4
|
import { dirname, resolve } from 'node:path';
|
|
4
|
-
import {
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { canonicalize, sha256, validateContract } from '@dharma-ai-labs/agent-fabric-contracts';
|
|
5
7
|
import { serializeRepositoryPackageSnapshot, } from './repositoryPackage.js';
|
|
6
8
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$(?![\s\S])/i;
|
|
7
9
|
const HASH = /^sha256:[0-9a-f]{64}$(?![\s\S])/;
|
|
8
10
|
const ORG = /^org_[A-Za-z0-9_]{1,156}$(?![\s\S])/;
|
|
9
11
|
const STATES = new Set(['accepted', 'processing', 'published', 'blocked']);
|
|
10
|
-
const OUTBOX_STATES = new Set(['pending_upload', ...STATES]);
|
|
12
|
+
const OUTBOX_STATES = new Set(['pending_upload', 'fenced', ...STATES]);
|
|
13
|
+
export function repositoryCandidateOperationId(input) {
|
|
14
|
+
requireFact(input.restorationAttempt === undefined || UUID.test(input.restorationAttempt), 'Repository restoration attempt is invalid.');
|
|
15
|
+
requireFact(input.restorationContext === undefined || UUID.test(input.restorationContext.headCandidateId)
|
|
16
|
+
&& UUID.test(input.restorationContext.priorReleaseId), 'Repository restoration context is invalid.');
|
|
17
|
+
return sha256(canonicalize({ schema: 'dharma.repository-candidate-operation/v1',
|
|
18
|
+
organizationId: input.scope.organizationId, workspaceId: input.scope.workspaceId,
|
|
19
|
+
repositoryBindingId: input.scope.repositoryBindingId, repositoryAgentId: input.scope.repositoryAgentId,
|
|
20
|
+
snapshotHash: input.snapshotHash, sourceManifestHash: input.sourceManifestHash, consolidation: input.consolidation,
|
|
21
|
+
...(input.recoveryAttempt ? { recoveryAttempt: 1 } : {}),
|
|
22
|
+
...(input.restorationAttempt ? { restorationAttempt: input.restorationAttempt } : {}),
|
|
23
|
+
...(input.restorationContext ? { restorationContext: input.restorationContext } : {}) }));
|
|
24
|
+
}
|
|
11
25
|
function requireFact(value, message) {
|
|
12
26
|
if (!value)
|
|
13
27
|
throw new Error(message);
|
|
14
28
|
}
|
|
29
|
+
function restorationCheckpoint(value) {
|
|
30
|
+
requireFact(value !== null && typeof value === 'object' && !Array.isArray(value), 'Repository restoration checkpoint is invalid.');
|
|
31
|
+
const row = value;
|
|
32
|
+
requireFact(Object.keys(row).sort().join(',') === ['baselineSnapshotHash', 'localSnapshotHash', 'planHash',
|
|
33
|
+
'previousOperationId', 'priorReleaseId', ...(Object.hasOwn(row, 'attemptId') ? ['attemptId'] : []),
|
|
34
|
+
...(Object.hasOwn(row, 'headCandidateId') ? ['headCandidateId'] : [])].sort().join(',')
|
|
35
|
+
&& typeof row.planHash === 'string' && HASH.test(row.planHash)
|
|
36
|
+
&& typeof row.localSnapshotHash === 'string' && HASH.test(row.localSnapshotHash)
|
|
37
|
+
&& (row.baselineSnapshotHash === null || typeof row.baselineSnapshotHash === 'string' && HASH.test(row.baselineSnapshotHash))
|
|
38
|
+
&& (row.previousOperationId === null || typeof row.previousOperationId === 'string' && HASH.test(row.previousOperationId))
|
|
39
|
+
&& typeof row.priorReleaseId === 'string' && UUID.test(row.priorReleaseId)
|
|
40
|
+
&& (row.attemptId === undefined || typeof row.attemptId === 'string' && UUID.test(row.attemptId))
|
|
41
|
+
&& (!Object.hasOwn(row, 'headCandidateId') || typeof row.headCandidateId === 'string' && UUID.test(row.headCandidateId)), 'Repository restoration checkpoint is invalid.');
|
|
42
|
+
return row;
|
|
43
|
+
}
|
|
44
|
+
function restorationContext(checkpoint) {
|
|
45
|
+
return checkpoint?.headCandidateId ? { headCandidateId: checkpoint.headCandidateId, priorReleaseId: checkpoint.priorReleaseId } : undefined;
|
|
46
|
+
}
|
|
15
47
|
function checkedScope(value) {
|
|
16
48
|
requireFact(ORG.test(value.organizationId), 'Repository candidate organization is invalid.');
|
|
17
49
|
for (const id of [value.workspaceId, value.repositoryBindingId, value.repositoryAgentId]) {
|
|
@@ -19,7 +51,7 @@ function checkedScope(value) {
|
|
|
19
51
|
}
|
|
20
52
|
return { ...value };
|
|
21
53
|
}
|
|
22
|
-
function receipt(value, scope, operationId, snapshotHash) {
|
|
54
|
+
function receipt(value, scope, operationId, snapshotHash, archivedReleaseId) {
|
|
23
55
|
requireFact(value !== null && typeof value === 'object' && !Array.isArray(value), 'Repository candidate response is invalid.');
|
|
24
56
|
const response = value;
|
|
25
57
|
requireFact(response.ok === true && response.organizationId === scope.organizationId
|
|
@@ -33,6 +65,7 @@ function receipt(value, scope, operationId, snapshotHash) {
|
|
|
33
65
|
&& typeof candidate.state === 'string' && STATES.has(candidate.state)
|
|
34
66
|
&& (candidate.releaseId === null || typeof candidate.releaseId === 'string' && UUID.test(candidate.releaseId))
|
|
35
67
|
&& (candidate.state === 'published') === (candidate.releaseId !== null), 'Repository candidate receipt is inconsistent.');
|
|
68
|
+
requireFact(candidate.state !== 'published' || candidate.releaseId !== archivedReleaseId, 'Repository restoration cannot reuse the archived release identity.');
|
|
36
69
|
return candidate;
|
|
37
70
|
}
|
|
38
71
|
function outboxPath(root, scope) {
|
|
@@ -64,7 +97,8 @@ function outbox(value, expectedScope) {
|
|
|
64
97
|
'candidateId', 'state', 'releaseId'];
|
|
65
98
|
requireFact(row.schema === 'dharma.repository-candidate-outbox/v1'
|
|
66
99
|
&& Reflect.ownKeys(row).every(key => typeof key === 'string')
|
|
67
|
-
&& keys.every(key => Object.hasOwn(row, key)) && Object.keys(row).every(key => keys.includes(key)
|
|
100
|
+
&& keys.every(key => Object.hasOwn(row, key)) && Object.keys(row).every(key => keys.includes(key)
|
|
101
|
+
|| ['restoration', 'fence', 'fenceRequestGenerationId', 'registryReceiptHash'].includes(key)), 'Repository candidate outbox is invalid.');
|
|
68
102
|
const scope = checkedScope(row.scope);
|
|
69
103
|
if (expectedScope)
|
|
70
104
|
requireFact(canonicalize(scope) === canonicalize(expectedScope), 'Repository candidate outbox scope is invalid.');
|
|
@@ -74,7 +108,7 @@ function outbox(value, expectedScope) {
|
|
|
74
108
|
&& typeof row.state === 'string' && OUTBOX_STATES.has(row.state)
|
|
75
109
|
&& (row.candidateId === null || typeof row.candidateId === 'string' && UUID.test(row.candidateId))
|
|
76
110
|
&& (row.releaseId === null || typeof row.releaseId === 'string' && UUID.test(row.releaseId))
|
|
77
|
-
&& (row.state
|
|
111
|
+
&& ['pending_upload', 'fenced'].includes(String(row.state)) === (row.candidateId === null)
|
|
78
112
|
&& (row.state === 'published') === (row.releaseId !== null), 'Repository candidate outbox is inconsistent.');
|
|
79
113
|
requireFact(row.consolidation !== null && typeof row.consolidation === 'object' && !Array.isArray(row.consolidation), 'Repository candidate consolidation is invalid.');
|
|
80
114
|
const consolidation = row.consolidation;
|
|
@@ -86,11 +120,65 @@ function outbox(value, expectedScope) {
|
|
|
86
120
|
&& (consolidation.expectedLatestSourceFingerprint === undefined
|
|
87
121
|
|| consolidation.mode === 'repository_update' && typeof consolidation.expectedLatestSourceFingerprint === 'string'
|
|
88
122
|
&& HASH.test(consolidation.expectedLatestSourceFingerprint)), 'Repository candidate consolidation is invalid.');
|
|
123
|
+
const restoration = Object.hasOwn(row, 'restoration') ? restorationCheckpoint(row.restoration) : undefined;
|
|
124
|
+
requireFact(!Object.hasOwn(row, 'registryReceiptHash') || Boolean(restoration) && typeof row.registryReceiptHash === 'string'
|
|
125
|
+
&& HASH.test(row.registryReceiptHash), 'Repository restoration registry checkpoint is invalid.');
|
|
126
|
+
requireFact((row.state === 'fenced') === Object.hasOwn(row, 'fence') && (row.state !== 'fenced' || Boolean(restoration)), 'Repository candidate fence checkpoint is inconsistent.');
|
|
127
|
+
requireFact(!Object.hasOwn(row, 'fenceRequestGenerationId') || Boolean(restoration)
|
|
128
|
+
&& typeof row.fenceRequestGenerationId === 'string' && UUID.test(row.fenceRequestGenerationId), 'Repository candidate fence request provenance is invalid.');
|
|
129
|
+
const fence = row.state === 'fenced' ? checkedFence(row.fence, scope, String(row.operationId), String(row.snapshotHash), String(row.sourceManifestHash), row.fenceRequestGenerationId) : undefined;
|
|
89
130
|
return { schema: 'dharma.repository-candidate-outbox/v1', scope,
|
|
90
131
|
operationId: row.operationId, sourceManifestHash: row.sourceManifestHash,
|
|
91
132
|
snapshotHash: row.snapshotHash, consolidation: consolidation,
|
|
92
133
|
candidateId: row.candidateId, state: row.state,
|
|
93
|
-
releaseId: row.releaseId
|
|
134
|
+
releaseId: row.releaseId,
|
|
135
|
+
...(restoration ? { restoration } : {}), ...(fence ? { fence } : {}),
|
|
136
|
+
...(typeof row.fenceRequestGenerationId === 'string' ? { fenceRequestGenerationId: row.fenceRequestGenerationId } : {}),
|
|
137
|
+
...(typeof row.registryReceiptHash === 'string' ? { registryReceiptHash: row.registryReceiptHash } : {}) };
|
|
138
|
+
}
|
|
139
|
+
function registryReceiptHash(pending) {
|
|
140
|
+
return sha256(canonicalize({ scope: pending.scope, operationId: pending.operationId, candidateId: pending.candidateId,
|
|
141
|
+
snapshotHash: pending.snapshotHash, sourceManifestHash: pending.sourceManifestHash, state: pending.state,
|
|
142
|
+
releaseId: pending.releaseId, restoration: pending.restoration }));
|
|
143
|
+
}
|
|
144
|
+
function assertRestorationReconciled(pending) {
|
|
145
|
+
requireFact(!pending.restoration || pending.registryReceiptHash === registryReceiptHash(pending), 'Repository restoration requires registry reconciliation before replacement.');
|
|
146
|
+
}
|
|
147
|
+
// Called under the registry mutation lock, only after its durable save. A crash
|
|
148
|
+
// before this marker leaves provenance retained and exact recovery retryable.
|
|
149
|
+
export async function checkpointRepositoryRestorationRegistry(input) {
|
|
150
|
+
const scope = checkedScope(input.scope), path = outboxPath(input.outboxRoot, scope), pending = await readOutbox(path, scope);
|
|
151
|
+
if (!pending?.restoration || pending.state === 'fenced')
|
|
152
|
+
return;
|
|
153
|
+
const saved = input.repositoryPackage;
|
|
154
|
+
const restoration = pending.restoration, inFlight = ['accepted', 'processing'].includes(pending.state);
|
|
155
|
+
requireFact(STATES.has(pending.state)
|
|
156
|
+
&& ['state', 'candidateId', 'operationId', 'snapshotHash', 'releaseId', 'sourceManifestHash'].every(key => saved[key] === pending[key])
|
|
157
|
+
&& saved.consolidationMode === 'repository_update'
|
|
158
|
+
&& saved.localBaselineSnapshotHash === (pending.state === 'blocked' ? restoration.baselineSnapshotHash : restoration.localSnapshotHash)
|
|
159
|
+
&& saved.publishedLocalSnapshotHash === (pending.state === 'published' ? restoration.localSnapshotHash : restoration.baselineSnapshotHash)
|
|
160
|
+
&& saved.pendingLocalSnapshotHash === (inFlight ? restoration.localSnapshotHash : null)
|
|
161
|
+
&& saved.pendingLocalOperationId === (inFlight ? pending.operationId : null), 'Repository restoration registry receipt does not match its durable checkpoint.');
|
|
162
|
+
await writeOutbox(path, { ...pending, registryReceiptHash: registryReceiptHash(pending) });
|
|
163
|
+
}
|
|
164
|
+
function checkedFence(value, scope, operationId, snapshotHash, sourceManifestHash, requestPolicyGenerationId) {
|
|
165
|
+
requireFact(value !== null && typeof value === 'object' && !Array.isArray(value), 'Repository candidate fence receipt is invalid.');
|
|
166
|
+
const row = value;
|
|
167
|
+
const keys = ['createdAt', 'fenceId', 'operationId', 'policyGenerationId', 'repositoryAgentId', 'repositoryBindingId',
|
|
168
|
+
'sourceManifestHash', 'sourceSnapshotHash', 'state', 'workspaceId',
|
|
169
|
+
...(Object.hasOwn(row, 'requestPolicyGenerationId') ? ['requestPolicyGenerationId'] : [])];
|
|
170
|
+
requireFact(Object.keys(row).sort().join(',') === keys.sort().join(',')
|
|
171
|
+
&& typeof row.fenceId === 'string' && UUID.test(row.fenceId) && row.operationId === operationId
|
|
172
|
+
&& row.workspaceId === scope.workspaceId && row.repositoryBindingId === scope.repositoryBindingId && row.repositoryAgentId === scope.repositoryAgentId
|
|
173
|
+
&& row.sourceSnapshotHash === snapshotHash && row.sourceManifestHash === sourceManifestHash && row.state === 'fenced'
|
|
174
|
+
&& typeof row.policyGenerationId === 'string' && UUID.test(row.policyGenerationId)
|
|
175
|
+
&& (!Object.hasOwn(row, 'requestPolicyGenerationId') || typeof row.requestPolicyGenerationId === 'string'
|
|
176
|
+
&& UUID.test(row.requestPolicyGenerationId))
|
|
177
|
+
&& (requestPolicyGenerationId === undefined || (Object.hasOwn(row, 'requestPolicyGenerationId')
|
|
178
|
+
? row.requestPolicyGenerationId === requestPolicyGenerationId : row.policyGenerationId === requestPolicyGenerationId))
|
|
179
|
+
&& typeof row.createdAt === 'string' && Number.isFinite(Date.parse(row.createdAt))
|
|
180
|
+
&& new Date(row.createdAt).toISOString() === row.createdAt, 'Repository candidate fence receipt is inconsistent.');
|
|
181
|
+
return row;
|
|
94
182
|
}
|
|
95
183
|
async function readOutbox(path, expectedScope) {
|
|
96
184
|
try {
|
|
@@ -107,7 +195,7 @@ async function readOutbox(path, expectedScope) {
|
|
|
107
195
|
export async function synchronizeRepositoryCandidate(input) {
|
|
108
196
|
const scope = checkedScope(input.scope);
|
|
109
197
|
const serialized = serializeRepositoryPackageSnapshot(input.snapshot);
|
|
110
|
-
requireFact(Buffer.byteLength(serialized) <=
|
|
198
|
+
requireFact(Buffer.byteLength(serialized) <= 4_194_304, 'Repository candidate exceeds its upload byte limit.');
|
|
111
199
|
requireFact(input.snapshot.schema === 'dharma.repository-package-snapshot/v1'
|
|
112
200
|
&& /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(input.snapshot.capturedAt)
|
|
113
201
|
&& Number.isFinite(Date.parse(input.snapshot.capturedAt))
|
|
@@ -137,53 +225,241 @@ export async function synchronizeRepositoryCandidate(input) {
|
|
|
137
225
|
expectedLatestSourceFingerprint: input.expectedLatestSourceFingerprint,
|
|
138
226
|
} : {}),
|
|
139
227
|
};
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
228
|
+
const restoration = input.restoration === undefined ? undefined : restorationCheckpoint(input.restoration);
|
|
229
|
+
requireFact(!restoration || !input.initialRepository && Boolean(input.expectedLatestSourceFingerprint)
|
|
230
|
+
&& !input.blockedCandidate, 'Repository restoration checkpoint requires an update.');
|
|
231
|
+
const operationId = repositoryCandidateOperationId({ scope, snapshotHash: snapshot.manifest.snapshotHash,
|
|
232
|
+
sourceManifestHash, consolidation, recoveryAttempt: Boolean(input.blockedCandidate), restorationAttempt: restoration?.attemptId,
|
|
233
|
+
restorationContext: restorationContext(restoration) });
|
|
143
234
|
const path = outboxPath(input.outboxRoot, scope);
|
|
144
235
|
const prior = await readOutbox(path, scope);
|
|
236
|
+
requireFact(prior?.state !== 'pending_upload' || !prior.fenceRequestGenerationId, 'Repository restoration fence outcome must be reconciled before any upload retry.');
|
|
237
|
+
if (prior?.state === 'fenced') {
|
|
238
|
+
requireFact(prior.fence && restoration?.attemptId === prior.fence.fenceId && prior.operationId !== operationId, 'A fenced operation requires its verified new restoration attempt.');
|
|
239
|
+
await archiveFencedCheckpoint(input.outboxRoot, prior);
|
|
240
|
+
}
|
|
145
241
|
if (prior && (prior.operationId !== operationId || canonicalize(prior.scope) !== canonicalize(scope))) {
|
|
146
|
-
requireFact(['published', 'blocked'].includes(prior.state)
|
|
242
|
+
requireFact(['published', 'blocked', 'fenced'].includes(prior.state)
|
|
147
243
|
|| input.blockedCandidate?.candidateId === prior.candidateId, 'A different repository candidate is still pending.');
|
|
244
|
+
if (prior.state !== 'fenced')
|
|
245
|
+
assertRestorationReconciled(prior);
|
|
148
246
|
}
|
|
149
247
|
const pending = prior?.operationId === operationId ? prior : {
|
|
150
248
|
schema: 'dharma.repository-candidate-outbox/v1', scope, operationId,
|
|
151
249
|
sourceManifestHash, snapshotHash: snapshot.manifest.snapshotHash, consolidation,
|
|
152
250
|
candidateId: null, state: 'pending_upload', releaseId: null,
|
|
251
|
+
...(restoration ? { restoration } : {}),
|
|
153
252
|
};
|
|
154
253
|
requireFact(pending.sourceManifestHash === sourceManifestHash && pending.snapshotHash === snapshot.manifest.snapshotHash
|
|
155
|
-
&& canonicalize(pending.consolidation) === canonicalize(consolidation)
|
|
254
|
+
&& canonicalize(pending.consolidation) === canonicalize(consolidation)
|
|
255
|
+
&& canonicalize(pending.restoration ?? null) === canonicalize(restoration ?? null), 'Repository candidate outbox does not match the current upload.');
|
|
256
|
+
const body = {
|
|
257
|
+
schema: 'dharma.repository-candidate-upload/v1', operationId, workspaceId: scope.workspaceId,
|
|
258
|
+
repositoryBindingId: scope.repositoryBindingId, repositoryAgentId: scope.repositoryAgentId,
|
|
259
|
+
sourceSnapshotHash: snapshot.manifest.snapshotHash, sourceManifestHash: pending.sourceManifestHash,
|
|
260
|
+
consolidation: pending.consolidation, snapshot,
|
|
261
|
+
...(restoration?.attemptId ? { restorationAttempt: restoration.attemptId } : {}),
|
|
262
|
+
...(restorationContext(restoration) ? { restorationContext: restorationContext(restoration) } : {}),
|
|
263
|
+
};
|
|
264
|
+
// The relay caps the complete encoded request, not only raw repository bytes.
|
|
265
|
+
requireFact(Buffer.byteLength(canonicalize(body)) <= 4_194_304, 'Repository candidate exceeds its upload byte limit.');
|
|
266
|
+
requireFact((await validateContract(fileURLToPath(new URL('./schemas/', import.meta.url)), 'https://schemas.dharma-ai.io/repository-candidate-upload/v1', body)).ok, 'Repository candidate upload contract is invalid.');
|
|
156
267
|
await writeOutbox(path, pending);
|
|
157
268
|
const route = `/agent-fabric/repository-agents/${encodeURIComponent(scope.repositoryAgentId)}/package-candidates`;
|
|
158
269
|
let accepted;
|
|
159
270
|
if (pending.candidateId) {
|
|
160
271
|
const query = new URLSearchParams({ workspaceId: scope.workspaceId, operationId });
|
|
161
|
-
accepted = receipt(await input.transport.signedGet(`${route}/${encodeURIComponent(pending.candidateId)}?${query}`), scope, operationId, snapshot.manifest.snapshotHash);
|
|
272
|
+
accepted = receipt(await input.transport.signedGet(`${route}/${encodeURIComponent(pending.candidateId)}?${query}`), scope, operationId, snapshot.manifest.snapshotHash, restoration?.priorReleaseId);
|
|
162
273
|
}
|
|
163
274
|
else {
|
|
164
|
-
accepted = receipt(await input.transport.signedPost(route,
|
|
165
|
-
schema: 'dharma.repository-candidate-upload/v1', operationId, workspaceId: scope.workspaceId,
|
|
166
|
-
repositoryBindingId: scope.repositoryBindingId, repositoryAgentId: scope.repositoryAgentId,
|
|
167
|
-
sourceSnapshotHash: snapshot.manifest.snapshotHash, sourceManifestHash: pending.sourceManifestHash,
|
|
168
|
-
consolidation: pending.consolidation, snapshot,
|
|
169
|
-
}), scope, operationId, snapshot.manifest.snapshotHash);
|
|
275
|
+
accepted = receipt(await input.transport.signedPost(route, body), scope, operationId, snapshot.manifest.snapshotHash, restoration?.priorReleaseId);
|
|
170
276
|
}
|
|
171
277
|
await writeOutbox(path, { ...pending, candidateId: accepted.candidateId, state: accepted.state, releaseId: accepted.releaseId });
|
|
172
278
|
return accepted;
|
|
173
279
|
}
|
|
280
|
+
// Only reconcile an already admitted operation. This never uploads or obtains
|
|
281
|
+
// new restoration authority, even if the original registry write was lost.
|
|
282
|
+
export async function recoverRepositoryRestorationCandidate(input) {
|
|
283
|
+
const scope = checkedScope(input.scope);
|
|
284
|
+
requireFact(HASH.test(input.planHash), 'Repository restoration plan hash is invalid.');
|
|
285
|
+
const path = outboxPath(input.outboxRoot, scope), pending = await readOutbox(path, scope);
|
|
286
|
+
if (!pending?.restoration || pending.state === 'fenced' || pending.restoration.planHash !== input.planHash)
|
|
287
|
+
return null;
|
|
288
|
+
requireFact(pending.consolidation.mode === 'repository_update' && Boolean(pending.consolidation.expectedLatestSourceFingerprint)
|
|
289
|
+
&& repositoryCandidateOperationId({ scope, snapshotHash: pending.snapshotHash,
|
|
290
|
+
sourceManifestHash: pending.sourceManifestHash, consolidation: pending.consolidation,
|
|
291
|
+
restorationAttempt: pending.restoration.attemptId, restorationContext: restorationContext(pending.restoration) }) === pending.operationId, 'Repository restoration checkpoint operation is invalid.');
|
|
292
|
+
const query = new URLSearchParams({ workspaceId: scope.workspaceId, operationId: pending.operationId });
|
|
293
|
+
const suffix = pending.candidateId ? `/${encodeURIComponent(pending.candidateId)}` : '';
|
|
294
|
+
const response = await input.transport.signedGet(`/agent-fabric/repository-agents/${encodeURIComponent(scope.repositoryAgentId)}/package-candidates${suffix}?${query}`);
|
|
295
|
+
if (!pending.candidateId && response.ok === true && response.organizationId === scope.organizationId && response.candidate === null) {
|
|
296
|
+
throw new Error('repository_source_restoration_upload_unresolved');
|
|
297
|
+
}
|
|
298
|
+
const candidate = receipt(response, scope, pending.operationId, pending.snapshotHash, pending.restoration.priorReleaseId);
|
|
299
|
+
requireFact(!pending.candidateId || candidate.candidateId === pending.candidateId, 'Repository restoration checkpoint candidate changed.');
|
|
300
|
+
return { candidate, sourceManifestHash: pending.sourceManifestHash, ...pending.restoration };
|
|
301
|
+
}
|
|
302
|
+
// Called only after the command's final authority, CAS and stopped-runtime
|
|
303
|
+
// checks. Keep restoration provenance if the following registry write fails.
|
|
304
|
+
export async function checkpointRepositoryRestorationCandidate(input) {
|
|
305
|
+
const scope = checkedScope(input.scope), recovered = input.recovered;
|
|
306
|
+
const path = outboxPath(input.outboxRoot, scope), pending = await readOutbox(path, scope);
|
|
307
|
+
const expected = restorationCheckpoint({ planHash: recovered.planHash, localSnapshotHash: recovered.localSnapshotHash,
|
|
308
|
+
baselineSnapshotHash: recovered.baselineSnapshotHash, previousOperationId: recovered.previousOperationId,
|
|
309
|
+
priorReleaseId: recovered.priorReleaseId, ...(recovered.attemptId ? { attemptId: recovered.attemptId } : {}),
|
|
310
|
+
...(recovered.headCandidateId ? { headCandidateId: recovered.headCandidateId } : {}) });
|
|
311
|
+
requireFact(pending?.restoration && pending.state !== 'fenced' && canonicalize(pending.restoration) === canonicalize(expected)
|
|
312
|
+
&& pending.sourceManifestHash === recovered.sourceManifestHash
|
|
313
|
+
&& pending.consolidation.mode === 'repository_update' && Boolean(pending.consolidation.expectedLatestSourceFingerprint)
|
|
314
|
+
&& repositoryCandidateOperationId({ scope, snapshotHash: pending.snapshotHash,
|
|
315
|
+
sourceManifestHash: pending.sourceManifestHash, consolidation: pending.consolidation,
|
|
316
|
+
restorationAttempt: pending.restoration.attemptId, restorationContext: restorationContext(pending.restoration) }) === pending.operationId, 'Repository restoration checkpoint changed.');
|
|
317
|
+
const candidate = receipt({ ok: true, organizationId: scope.organizationId, candidate: recovered.candidate }, scope, pending.operationId, pending.snapshotHash, pending.restoration.priorReleaseId);
|
|
318
|
+
requireFact(!pending.candidateId || pending.candidateId === candidate.candidateId, 'Repository restoration checkpoint candidate changed.');
|
|
319
|
+
requireFact(!['published', 'blocked'].includes(pending.state)
|
|
320
|
+
|| pending.state === candidate.state && pending.releaseId === candidate.releaseId, 'Repository restoration checkpoint terminal state changed.');
|
|
321
|
+
await writeOutbox(path, { ...pending, ...candidate });
|
|
322
|
+
return candidate;
|
|
323
|
+
}
|
|
324
|
+
export async function readRepositoryRestorationAttempt(input) {
|
|
325
|
+
const scope = checkedScope(input.scope), pending = await readOutbox(outboxPath(input.outboxRoot, scope), scope);
|
|
326
|
+
return pending?.state === 'fenced' ? pending.fence.fenceId
|
|
327
|
+
: pending?.state === 'pending_upload' ? pending.restoration?.attemptId : undefined;
|
|
328
|
+
}
|
|
329
|
+
// Explicit recovery only: this can fence an unadmitted operation but never
|
|
330
|
+
// cancels admitted work, uploads source, or retires a local checkpoint.
|
|
331
|
+
export async function fenceRepositoryRestorationUpload(input) {
|
|
332
|
+
const scope = checkedScope(input.scope);
|
|
333
|
+
requireFact(HASH.test(input.planHash) && UUID.test(input.policyGenerationId), 'Repository restoration fence authority is invalid.');
|
|
334
|
+
let pending = await readOutbox(outboxPath(input.outboxRoot, scope), scope);
|
|
335
|
+
requireFact(pending?.restoration && pending.restoration.planHash === input.planHash
|
|
336
|
+
&& pending.restoration.previousOperationId === input.expectedPreviousOperationId
|
|
337
|
+
&& ['pending_upload', 'fenced'].includes(pending.state) && pending.candidateId === null
|
|
338
|
+
&& pending.consolidation.mode === 'repository_update' && Boolean(pending.consolidation.expectedLatestSourceFingerprint)
|
|
339
|
+
&& repositoryCandidateOperationId({ scope, snapshotHash: pending.snapshotHash, sourceManifestHash: pending.sourceManifestHash,
|
|
340
|
+
consolidation: pending.consolidation, restorationAttempt: pending.restoration.attemptId,
|
|
341
|
+
restorationContext: restorationContext(pending.restoration) }) === pending.operationId, 'Repository restoration fence checkpoint is invalid.');
|
|
342
|
+
const schemaDirectory = fileURLToPath(new URL('./schemas/', import.meta.url));
|
|
343
|
+
if (pending.state === 'fenced') {
|
|
344
|
+
// Containment is already durable. Reposting under a newer policy cannot
|
|
345
|
+
// change this receipt; the guarded checkpoint verifies the immutable archive.
|
|
346
|
+
requireFact((await validateContract(schemaDirectory, 'https://schemas.dharma-ai.io/repository-candidate-fence/v1#/$defs/fence', pending.fence)).ok, 'Repository restoration fence receipt schema is invalid.');
|
|
347
|
+
return { checkpointHash: sha256(canonicalize(pending)), pending, fence: pending.fence, candidate: null };
|
|
348
|
+
}
|
|
349
|
+
// The command holds its registry mutation lock. Persist request provenance
|
|
350
|
+
// before transport so response loss cannot erase the original retry binding.
|
|
351
|
+
const restoration = pending.restoration;
|
|
352
|
+
if (!pending.fenceRequestGenerationId) {
|
|
353
|
+
pending = { ...pending, fenceRequestGenerationId: input.policyGenerationId };
|
|
354
|
+
await writeOutbox(outboxPath(input.outboxRoot, scope), pending);
|
|
355
|
+
if (process.platform !== 'win32') {
|
|
356
|
+
const parent = await open(resolve(input.outboxRoot), constants.O_RDONLY | constants.O_DIRECTORY | (constants.O_NOFOLLOW || 0));
|
|
357
|
+
try {
|
|
358
|
+
await parent.sync();
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
await parent.close();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const body = {
|
|
366
|
+
schema: 'dharma.repository-candidate-fence/v1', operationId: pending.operationId,
|
|
367
|
+
workspaceId: scope.workspaceId, repositoryBindingId: scope.repositoryBindingId, repositoryAgentId: scope.repositoryAgentId,
|
|
368
|
+
sourceSnapshotHash: pending.snapshotHash, sourceManifestHash: pending.sourceManifestHash, consolidation: pending.consolidation,
|
|
369
|
+
requestPolicyGenerationId: pending.fenceRequestGenerationId,
|
|
370
|
+
...(restoration.attemptId ? { restorationAttempt: restoration.attemptId } : {}),
|
|
371
|
+
...(restorationContext(restoration) ? { restorationContext: restorationContext(restoration) } : {}),
|
|
372
|
+
};
|
|
373
|
+
requireFact((await validateContract(schemaDirectory, 'https://schemas.dharma-ai.io/repository-candidate-fence/v1', body)).ok
|
|
374
|
+
&& Buffer.byteLength(canonicalize(body)) <= 4096, 'Repository restoration fence metadata is invalid.');
|
|
375
|
+
const response = await input.transport.signedPost(`/agent-fabric/repository-agents/${encodeURIComponent(scope.repositoryAgentId)}/package-candidates/fence`, body);
|
|
376
|
+
requireFact(response.ok === true && response.organizationId === scope.organizationId
|
|
377
|
+
&& Object.hasOwn(response, 'candidate') && Object.hasOwn(response, 'fence')
|
|
378
|
+
&& (response.candidate === null) !== (response.fence === null), 'Repository restoration fence response is invalid.');
|
|
379
|
+
const fence = response.fence === null ? null : checkedFence(response.fence, scope, pending.operationId, pending.snapshotHash, pending.sourceManifestHash, pending.fenceRequestGenerationId);
|
|
380
|
+
requireFact(!fence || (await validateContract(schemaDirectory, 'https://schemas.dharma-ai.io/repository-candidate-fence/v1#/$defs/fence', fence)).ok, 'Repository restoration fence receipt schema is invalid.');
|
|
381
|
+
const candidate = fence ? null : receipt(response, scope, pending.operationId, pending.snapshotHash, restoration.priorReleaseId);
|
|
382
|
+
return { checkpointHash: sha256(canonicalize(pending)), pending, fence, candidate };
|
|
383
|
+
}
|
|
384
|
+
async function archiveFencedCheckpoint(root, pending) {
|
|
385
|
+
requireFact(pending.state === 'fenced' && pending.fence && pending.restoration, 'Repository restoration terminal fence is required.');
|
|
386
|
+
const { fence, ...checkpoint } = pending;
|
|
387
|
+
const value = { checkpoint: { ...checkpoint, state: 'pending_upload' }, fence };
|
|
388
|
+
const bytes = `${canonicalize(value)}\n`, directory = resolve(root, 'fenced');
|
|
389
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
390
|
+
const directoryStat = await lstat(directory);
|
|
391
|
+
requireFact(directoryStat.isDirectory() && !directoryStat.isSymbolicLink(), 'Repository restoration archive directory is invalid.');
|
|
392
|
+
const path = resolve(directory, `${pending.scope.workspaceId}.${fence.fenceId}.json`);
|
|
393
|
+
const temporary = resolve(directory, `.${pending.scope.workspaceId}.${fence.fenceId}.${randomUUID()}.tmp`);
|
|
394
|
+
const handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
|
|
395
|
+
try {
|
|
396
|
+
try {
|
|
397
|
+
await handle.writeFile(bytes);
|
|
398
|
+
await handle.sync();
|
|
399
|
+
}
|
|
400
|
+
finally {
|
|
401
|
+
await handle.close();
|
|
402
|
+
}
|
|
403
|
+
// Publishing a fully flushed inode with a link cannot replace an existing archive.
|
|
404
|
+
try {
|
|
405
|
+
await link(temporary, path);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
if (error.code !== 'EEXIST')
|
|
409
|
+
throw error;
|
|
410
|
+
const before = await lstat(path);
|
|
411
|
+
requireFact(before.isFile() && !before.isSymbolicLink() && before.size <= 65_536, 'Repository restoration archive is invalid.');
|
|
412
|
+
const existing = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
|
|
413
|
+
try {
|
|
414
|
+
const after = await existing.stat();
|
|
415
|
+
requireFact(after.isFile() && before.ino === after.ino && before.dev === after.dev && after.size <= 65_536
|
|
416
|
+
&& await existing.readFile('utf8') === bytes, 'Repository restoration archive changed.');
|
|
417
|
+
}
|
|
418
|
+
finally {
|
|
419
|
+
await existing.close();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (process.platform !== 'win32') {
|
|
423
|
+
const parent = await open(directory, constants.O_RDONLY | constants.O_DIRECTORY | (constants.O_NOFOLLOW || 0));
|
|
424
|
+
try {
|
|
425
|
+
await parent.sync();
|
|
426
|
+
}
|
|
427
|
+
finally {
|
|
428
|
+
await parent.close();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
finally {
|
|
433
|
+
await rm(temporary, { force: true });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// The command invokes this only after the registry CAS, current authority and
|
|
437
|
+
// stopped-runtime guards. Persist the terminal archive before replacement.
|
|
438
|
+
export async function checkpointRepositoryRestorationFence(input) {
|
|
439
|
+
const scope = checkedScope(input.scope), path = outboxPath(input.outboxRoot, scope), pending = await readOutbox(path, scope);
|
|
440
|
+
requireFact(pending && sha256(canonicalize(pending)) === input.fenced.checkpointHash
|
|
441
|
+
&& input.fenced.fence && input.fenced.candidate === null, 'Repository restoration fence checkpoint changed.');
|
|
442
|
+
const fence = checkedFence(input.fenced.fence, scope, pending.operationId, pending.snapshotHash, pending.sourceManifestHash, pending.fenceRequestGenerationId);
|
|
443
|
+
const next = { ...pending, state: 'fenced', fence };
|
|
444
|
+
await archiveFencedCheckpoint(input.outboxRoot, next);
|
|
445
|
+
if (canonicalize(next) !== canonicalize(pending))
|
|
446
|
+
await writeOutbox(path, next);
|
|
447
|
+
return fence;
|
|
448
|
+
}
|
|
174
449
|
export async function pollRepositoryCandidate(input) {
|
|
175
450
|
const scope = checkedScope(input.scope);
|
|
176
451
|
const path = outboxPath(input.outboxRoot, scope);
|
|
177
452
|
const pending = await readOutbox(path, scope);
|
|
178
|
-
if (!pending || pending.state === 'pending_upload')
|
|
453
|
+
if (!pending || pending.state === 'pending_upload' || pending.state === 'fenced')
|
|
179
454
|
return null;
|
|
180
455
|
if (pending.state === 'published' || pending.state === 'blocked') {
|
|
456
|
+
requireFact(pending.state !== 'published' || pending.releaseId !== pending.restoration?.priorReleaseId, 'Repository restoration cannot reuse the archived release identity.');
|
|
181
457
|
return { candidateId: pending.candidateId, operationId: pending.operationId, snapshotHash: pending.snapshotHash,
|
|
182
458
|
state: pending.state, releaseId: pending.releaseId };
|
|
183
459
|
}
|
|
184
460
|
const route = `/agent-fabric/repository-agents/${encodeURIComponent(scope.repositoryAgentId)}/package-candidates`;
|
|
185
461
|
const query = new URLSearchParams({ workspaceId: scope.workspaceId, operationId: pending.operationId });
|
|
186
|
-
const current = receipt(await input.transport.signedGet(`${route}/${encodeURIComponent(pending.candidateId)}?${query}`), scope, pending.operationId, pending.snapshotHash);
|
|
462
|
+
const current = receipt(await input.transport.signedGet(`${route}/${encodeURIComponent(pending.candidateId)}?${query}`), scope, pending.operationId, pending.snapshotHash, pending.restoration?.priorReleaseId);
|
|
187
463
|
await writeOutbox(path, { ...pending, candidateId: current.candidateId, state: current.state, releaseId: current.releaseId });
|
|
188
464
|
return current;
|
|
189
465
|
}
|
|
@@ -191,6 +467,35 @@ export async function adoptRepositoryCandidate(input) {
|
|
|
191
467
|
const scope = checkedScope(input.scope);
|
|
192
468
|
requireFact(HASH.test(input.sourceManifestHash), 'Canonical repository candidate manifest hash is invalid.');
|
|
193
469
|
const candidate = receipt({ ok: true, organizationId: scope.organizationId, candidate: input.candidate }, scope, input.candidate.operationId, input.candidate.snapshotHash);
|
|
470
|
+
const prior = await readOutbox(outboxPath(input.outboxRoot, scope), scope);
|
|
471
|
+
if (prior?.state === 'fenced') {
|
|
472
|
+
requireFact(prior.operationId !== candidate.operationId, 'A fenced operation cannot be adopted.');
|
|
473
|
+
requireFact(['published', 'blocked'].includes(candidate.state), 'A fenced restoration requires a terminal canonical candidate before adoption. Retry after canonical publication completes.');
|
|
474
|
+
await archiveFencedCheckpoint(input.outboxRoot, prior);
|
|
475
|
+
// Registry adoption is not a new restoration. Keep its terminal retry
|
|
476
|
+
// identity active until a restoration explicitly carries this fence ID.
|
|
477
|
+
return candidate;
|
|
478
|
+
}
|
|
479
|
+
else if (prior?.restoration) {
|
|
480
|
+
requireFact(['published', 'blocked'].includes(prior.state), 'An unresolved restoration checkpoint cannot be replaced by adoption.');
|
|
481
|
+
if (prior.operationId === candidate.operationId) {
|
|
482
|
+
requireFact(prior.candidateId === candidate.candidateId && prior.snapshotHash === candidate.snapshotHash
|
|
483
|
+
&& prior.sourceManifestHash === input.sourceManifestHash && prior.state === candidate.state && prior.releaseId === candidate.releaseId
|
|
484
|
+
&& prior.consolidation.mode === input.consolidationMode, 'Restoration adoption changed its checkpoint.');
|
|
485
|
+
requireFact(prior.consolidation.mode === 'repository_update' && Boolean(prior.consolidation.expectedLatestSourceFingerprint)
|
|
486
|
+
&& repositoryCandidateOperationId({ scope, snapshotHash: prior.snapshotHash,
|
|
487
|
+
sourceManifestHash: prior.sourceManifestHash, consolidation: prior.consolidation,
|
|
488
|
+
restorationAttempt: prior.restoration.attemptId, restorationContext: restorationContext(prior.restoration) }) === prior.operationId, 'Repository restoration checkpoint operation is invalid.');
|
|
489
|
+
requireFact(candidate.state !== 'published' || candidate.releaseId !== prior.restoration.priorReleaseId, 'Repository restoration cannot reuse the archived release identity.');
|
|
490
|
+
// Internal save metadata only; never add it to a signed candidate receipt.
|
|
491
|
+
return { ...candidate, restorationRegistryBaseline: {
|
|
492
|
+
localBaselineSnapshotHash: prior.state === 'blocked' ? prior.restoration.baselineSnapshotHash : prior.restoration.localSnapshotHash,
|
|
493
|
+
publishedLocalSnapshotHash: prior.state === 'published' ? prior.restoration.localSnapshotHash : prior.restoration.baselineSnapshotHash,
|
|
494
|
+
pendingLocalSnapshotHash: null, pendingLocalOperationId: null,
|
|
495
|
+
} };
|
|
496
|
+
}
|
|
497
|
+
assertRestorationReconciled(prior);
|
|
498
|
+
}
|
|
194
499
|
requireFact(candidate.state !== 'accepted' || candidate.candidateId !== null, 'Canonical repository candidate is incomplete.');
|
|
195
500
|
await writeOutbox(outboxPath(input.outboxRoot, scope), {
|
|
196
501
|
schema: 'dharma.repository-candidate-outbox/v1', scope, operationId: candidate.operationId,
|