@sentropic/artifacts 0.1.4 → 0.2.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/README.md +47 -0
- package/dist/adapters/postgres-commit.d.ts +15 -0
- package/dist/adapters/postgres-commit.js +80 -0
- package/dist/adapters/postgres.d.ts +5 -2
- package/dist/adapters/postgres.js +39 -0
- package/dist/http.d.ts +43 -30
- package/dist/http.js +158 -507
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp-core.d.ts +32 -39
- package/dist/mcp-core.js +83 -244
- package/dist/mcp.js +5 -5
- package/dist/profile/contracts.d.ts +67 -0
- package/dist/profile/contracts.js +31 -0
- package/dist/profile/index.d.ts +6 -0
- package/dist/profile/index.js +117 -0
- package/dist/profile/json.d.ts +10 -0
- package/dist/profile/json.js +76 -0
- package/dist/profile/radar.d.ts +3 -0
- package/dist/profile/radar.js +21 -0
- package/dist/profile/schemas.d.ts +6 -0
- package/dist/profile/schemas.js +76 -0
- package/dist/profile/validation.d.ts +5 -0
- package/dist/profile/validation.js +104 -0
- package/dist/profile/validators.d.ts +3 -0
- package/dist/profile/validators.js +51 -0
- package/dist/server/authorized-replay.d.ts +33 -0
- package/dist/server/authorized-replay.js +31 -0
- package/dist/server/command-identity.d.ts +52 -0
- package/dist/server/command-identity.js +71 -0
- package/dist/server/commit/brands.d.ts +7 -0
- package/dist/server/commit/brands.js +3 -0
- package/dist/server/commit/coordinator.d.ts +2 -0
- package/dist/server/commit/coordinator.js +729 -0
- package/dist/server/commit/types.d.ts +382 -0
- package/dist/server/commit/types.js +94 -0
- package/dist/server/commit-contract.d.ts +4 -0
- package/dist/server/commit-contract.js +2 -0
- package/dist/server/failures.d.ts +84 -0
- package/dist/server/failures.js +133 -0
- package/dist/server/gc.d.ts +42 -1
- package/dist/server/gc.js +168 -5
- package/dist/server/idempotency.d.ts +4 -0
- package/dist/server/idempotency.js +14 -0
- package/dist/server/lifecycle.d.ts +2 -1
- package/dist/server/lifecycle.js +15 -1
- package/dist/server/object-intents.d.ts +14 -1
- package/dist/server/object-intents.js +31 -0
- package/dist/server/outcome-contract.d.ts +3 -0
- package/dist/server/outcome-contract.js +3 -0
- package/dist/server/outcome-projection.d.ts +21 -0
- package/dist/server/outcome-projection.js +29 -0
- package/dist/server/preparation-cleanup.d.ts +31 -0
- package/dist/server/preparation-cleanup.js +106 -0
- package/dist/server/publication.d.ts +78 -0
- package/dist/server/publication.js +263 -31
- package/dist/server/service.d.ts +60 -0
- package/dist/server/service.js +181 -0
- package/dist/testing.d.ts +50 -0
- package/dist/testing.js +86 -0
- package/docs/migration-l6.md +134 -0
- package/examples/radar-commit-setup.ts +302 -0
- package/l0-freeze/command-identity-contract.ts +303 -0
- package/l0-freeze/commit-contract.ts +421 -0
- package/l0-freeze/failure-contract.ts +174 -0
- package/l0-freeze/profile-contract.ts +219 -0
- package/migrations/0003_coordinator.sql +28 -0
- package/package.json +27 -1
- package/src/contracts.ts +125 -0
package/dist/server/service.js
CHANGED
|
@@ -11,6 +11,9 @@ import { assertArtifactAccess } from './authorization.js';
|
|
|
11
11
|
import { createBindingService } from './bindings.js';
|
|
12
12
|
import { createIdempotencyStore } from './idempotency.js';
|
|
13
13
|
import { manifestDigest } from '../codecs.js';
|
|
14
|
+
import { ArtifactProfileInputError } from '../profile/index.js';
|
|
15
|
+
import { createArtifactCommand, } from './command-identity.js';
|
|
16
|
+
import { projectEnsureReceipt, replayEnsureGuarded, } from './authorized-replay.js';
|
|
14
17
|
/** Create the artifact service over explicit injected ports (no defaults). */
|
|
15
18
|
export function createArtifactService(ports) {
|
|
16
19
|
const bindings = createBindingService(ports.kv, ports.uuid);
|
|
@@ -35,3 +38,181 @@ export function createArtifactService(ports) {
|
|
|
35
38
|
},
|
|
36
39
|
};
|
|
37
40
|
}
|
|
41
|
+
/** Private causes stay on the failure object but never serialize: projection picks fields. */
|
|
42
|
+
const conceal = (failure) => {
|
|
43
|
+
Object.defineProperty(failure, 'cause', { value: failure.cause, enumerable: false });
|
|
44
|
+
return Object.freeze(failure);
|
|
45
|
+
};
|
|
46
|
+
const profileEntryFailure = (error, invocationId) => {
|
|
47
|
+
if (error.kind === 'operational') {
|
|
48
|
+
return conceal({
|
|
49
|
+
kind: 'operational', category: 'capability_unselected', variant: error.variant,
|
|
50
|
+
httpStatus: error.httpStatus, publicCode: error.publicCode, retriable: false,
|
|
51
|
+
invocationId, operation: 'ensure', phase: error.phase, commitState: 'not_committed', cause: error,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return conceal({
|
|
55
|
+
kind: 'ordinary', category: 'invalid_request',
|
|
56
|
+
httpStatus: error.httpStatus, publicCode: error.publicCode, retriable: false,
|
|
57
|
+
invocationId, operation: 'ensure', phase: error.phase, commitState: 'not_committed', cause: error,
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
const internalFailure = (invocationId, phase, cause) => conceal({
|
|
61
|
+
kind: 'operational', category: 'provider_error', variant: 'unclassified',
|
|
62
|
+
httpStatus: 500, publicCode: 'internal_error', retriable: false,
|
|
63
|
+
invocationId, operation: 'ensure', phase, commitState: 'not_committed', cause,
|
|
64
|
+
});
|
|
65
|
+
const unavailableFailure = (invocationId, phase, cause) => conceal({
|
|
66
|
+
kind: 'operational', category: 'provider_unavailable', variant: 'dispatch_failure',
|
|
67
|
+
httpStatus: 503, publicCode: 'capability_unavailable', retriable: true,
|
|
68
|
+
invocationId, operation: 'ensure', phase, commitState: 'not_committed', cause,
|
|
69
|
+
});
|
|
70
|
+
const anonymousFailure = (invocation) => conceal({
|
|
71
|
+
kind: 'admission', category: 'anonymous',
|
|
72
|
+
httpStatus: 401, publicCode: 'anonymous', retriable: false,
|
|
73
|
+
invocationId: invocation.invocationId, operation: 'ensure', phase: 'admit',
|
|
74
|
+
commitState: 'not_committed', cause: { invocation: invocation.invocationId },
|
|
75
|
+
});
|
|
76
|
+
const coordinatorFailure = (failure) => conceal({
|
|
77
|
+
kind: 'operational', category: failure.category, variant: failure.variant,
|
|
78
|
+
httpStatus: failure.httpStatus, publicCode: failure.publicCode, retriable: failure.retriable,
|
|
79
|
+
invocationId: failure.invocationId, operation: failure.operation, phase: failure.phase,
|
|
80
|
+
commitState: failure.commitState, cause: failure.cause,
|
|
81
|
+
});
|
|
82
|
+
const ensureResult = (receipt, replayed, invocationId) => ({
|
|
83
|
+
ok: true,
|
|
84
|
+
result: { artifactId: receipt.artifactId, created: receipt.created, replayed },
|
|
85
|
+
invocationId,
|
|
86
|
+
commitState: 'committed',
|
|
87
|
+
replayed,
|
|
88
|
+
});
|
|
89
|
+
export function createGuardedEnsureService(deps) {
|
|
90
|
+
const finalizeRequest = (invocation, command, policyRevision) => {
|
|
91
|
+
const binding = {
|
|
92
|
+
ownerId: invocation.actorId,
|
|
93
|
+
storageScopeId: invocation.storageScopeId,
|
|
94
|
+
bindingName: command.payload.bindingName,
|
|
95
|
+
attemptId: invocation.invocationId,
|
|
96
|
+
};
|
|
97
|
+
const captured = {};
|
|
98
|
+
const request = {
|
|
99
|
+
scopeId: invocation.storageScopeId,
|
|
100
|
+
internalKey: command.identity.internalKey,
|
|
101
|
+
commandHash: command.identity.commandHash,
|
|
102
|
+
codecVersion: command.identity.codecVersion,
|
|
103
|
+
invocationId: invocation.invocationId,
|
|
104
|
+
actorId: invocation.actorId,
|
|
105
|
+
operation: 'ensure',
|
|
106
|
+
get artifactId() { return captured.artifactId ?? null; },
|
|
107
|
+
bindingName: binding.bindingName,
|
|
108
|
+
policyRevision,
|
|
109
|
+
locks: { 'binding-identity': [async (client) => {
|
|
110
|
+
await deps.host.lockBinding(client, binding);
|
|
111
|
+
const receipt = await deps.host.resolveBinding(client, binding);
|
|
112
|
+
captured.artifactId = receipt.artifactId;
|
|
113
|
+
captured.created = receipt.created;
|
|
114
|
+
}] },
|
|
115
|
+
validateReplayReceipt: (receipt) => captured.created === false &&
|
|
116
|
+
projectEnsureReceipt(receipt).artifactId === captured.artifactId,
|
|
117
|
+
noteAbandonedAttempt: (attemptId) => deps.host.abandonAttempt(attemptId),
|
|
118
|
+
attemptId: invocation.invocationId,
|
|
119
|
+
guard: invocation.authorizeCommit,
|
|
120
|
+
receiptData: captured,
|
|
121
|
+
};
|
|
122
|
+
return { request, captured };
|
|
123
|
+
};
|
|
124
|
+
return {
|
|
125
|
+
async ensure(invocation, args) {
|
|
126
|
+
if (invocation.actorId.length === 0 || invocation.storageScopeId.length === 0) {
|
|
127
|
+
return { ok: false, failure: anonymousFailure(invocation) };
|
|
128
|
+
}
|
|
129
|
+
let command;
|
|
130
|
+
try {
|
|
131
|
+
const { envelopeKey, ...commandArgs } = args;
|
|
132
|
+
command = createArtifactCommand(deps.profile, 'artifacts_ensure', commandArgs, { storageScopeId: invocation.storageScopeId, actorId: invocation.actorId }, deps.derivation, envelopeKey);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (error instanceof ArtifactProfileInputError) {
|
|
136
|
+
return { ok: false, failure: profileEntryFailure(error, invocation.invocationId) };
|
|
137
|
+
}
|
|
138
|
+
return { ok: false, failure: internalFailure(invocation.invocationId, 'entry', error) };
|
|
139
|
+
}
|
|
140
|
+
let policyRevision;
|
|
141
|
+
try {
|
|
142
|
+
policyRevision = await deps.host.readPolicyRevision();
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
return { ok: false, failure: unavailableFailure(invocation.invocationId, 'acquire', error) };
|
|
146
|
+
}
|
|
147
|
+
const { request } = finalizeRequest(invocation, command, policyRevision);
|
|
148
|
+
const outcome = await deps.coordinator.finalizePrepared(request);
|
|
149
|
+
if (!outcome.ok)
|
|
150
|
+
return { ok: false, failure: coordinatorFailure(outcome.failure) };
|
|
151
|
+
let receipt;
|
|
152
|
+
try {
|
|
153
|
+
receipt = projectEnsureReceipt(outcome.result);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
return { ok: false, failure: internalFailure(invocation.invocationId, 'resolve', error) };
|
|
157
|
+
}
|
|
158
|
+
return ensureResult(receipt, outcome.replayed, outcome.invocationId);
|
|
159
|
+
},
|
|
160
|
+
async replay(invocation, args) {
|
|
161
|
+
if (invocation.actorId.length === 0 || invocation.storageScopeId.length === 0) {
|
|
162
|
+
return { ok: false, failure: anonymousFailure(invocation) };
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
deps.profile.assertSelected('artifacts_ensure');
|
|
166
|
+
if (!args.binding || args.binding.ownerId !== invocation.actorId ||
|
|
167
|
+
args.binding.storageScopeId !== invocation.storageScopeId || !args.binding.bindingName) {
|
|
168
|
+
throw new ArtifactProfileInputError();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (error instanceof ArtifactProfileInputError) {
|
|
173
|
+
return { ok: false, failure: profileEntryFailure(error, invocation.invocationId) };
|
|
174
|
+
}
|
|
175
|
+
return { ok: false, failure: internalFailure(invocation.invocationId, 'entry', error) };
|
|
176
|
+
}
|
|
177
|
+
let policyRevision;
|
|
178
|
+
try {
|
|
179
|
+
policyRevision = await deps.host.readPolicyRevision();
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
return { ok: false, failure: unavailableFailure(invocation.invocationId, 'acquire', error) };
|
|
183
|
+
}
|
|
184
|
+
let artifactId;
|
|
185
|
+
let missingTarget = false;
|
|
186
|
+
const binding = args.binding;
|
|
187
|
+
const outcome = await replayEnsureGuarded(deps.coordinator, {
|
|
188
|
+
invocation,
|
|
189
|
+
internalKey: args.internalKey,
|
|
190
|
+
commandHash: args.commandHash,
|
|
191
|
+
policyRevision,
|
|
192
|
+
get artifactId() { return artifactId; },
|
|
193
|
+
bindingName: binding?.bindingName,
|
|
194
|
+
validateReplayReceipt: (receipt) => !missingTarget &&
|
|
195
|
+
projectEnsureReceipt(receipt).artifactId === artifactId,
|
|
196
|
+
noteAbandonedAttempt: (attemptId) => deps.host.abandonAttempt(attemptId),
|
|
197
|
+
locks: binding === undefined ? undefined : {
|
|
198
|
+
'binding-identity': [async (client) => {
|
|
199
|
+
await deps.host.lockBinding(client, binding);
|
|
200
|
+
const target = await deps.host.resolveBinding(client, binding);
|
|
201
|
+
missingTarget = target.created;
|
|
202
|
+
artifactId = target.artifactId;
|
|
203
|
+
}],
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
if (!outcome.ok)
|
|
207
|
+
return { ok: false, failure: coordinatorFailure(outcome.failure) };
|
|
208
|
+
let receipt;
|
|
209
|
+
try {
|
|
210
|
+
receipt = projectEnsureReceipt(outcome.result);
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
return { ok: false, failure: internalFailure(invocation.invocationId, 'resolve', error) };
|
|
214
|
+
}
|
|
215
|
+
return ensureResult(receipt, outcome.replayed, outcome.invocationId);
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L3 (immo #861) — test-only commit barrier (spec r2 §3.2).
|
|
3
|
+
*
|
|
4
|
+
* Test seam for pausing finalization at deterministic points with
|
|
5
|
+
* invocation correlation. `waitUntilReached` resolves after arrival;
|
|
6
|
+
* `release` resumes only that invocation/point pair. The coordinator
|
|
7
|
+
* rechecks authority after a resumed barrier; a barrier can never mark
|
|
8
|
+
* an operation authorized or successful.
|
|
9
|
+
*
|
|
10
|
+
* Pass `observer` as the coordinator `testObserver`. Register a wait before
|
|
11
|
+
* starting the operation to pause that invocation/point; other arrivals
|
|
12
|
+
* are observed without blocking. The three preparation points are fired by L4
|
|
13
|
+
* preparation adapters holding this same observer, never by the
|
|
14
|
+
* coordinator (arming one throws at construction).
|
|
15
|
+
*/
|
|
16
|
+
import type { InspectableDeletionClaimStore } from './server/gc.js';
|
|
17
|
+
import type { PublicationBlobStore } from './server/publication.js';
|
|
18
|
+
export declare const COMMIT_BARRIER_POINTS: readonly ["before_connection_acquire", "before_key_lock", "before_blob_put", "after_blob_put_before_verify", "before_blob_readback", "before_finalization_row_lock", "after_finalization_locks_before_guard", "after_replay_lookup_before_guard", "before_commit"];
|
|
19
|
+
export type CommitBarrierPoint = (typeof COMMIT_BARRIER_POINTS)[number];
|
|
20
|
+
export type CommitBarrierObserver = (point: CommitBarrierPoint, invocationId: string) => Promise<void>;
|
|
21
|
+
export interface CommitArrivalObserver {
|
|
22
|
+
arrive(point: CommitBarrierPoint, invocationId: string): void;
|
|
23
|
+
}
|
|
24
|
+
/** Explicit host binding: purge must use the supplied deletion adapter for this call only. */
|
|
25
|
+
export interface PurgeTestHost {
|
|
26
|
+
readonly claims: InspectableDeletionClaimStore;
|
|
27
|
+
readonly storageScopeId: string;
|
|
28
|
+
readonly blobs: PublicationBlobStore;
|
|
29
|
+
purge(artifactId: string, deleting: PublicationBlobStore): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export declare function inspectDeletionClaims(host: Pick<PurgeTestHost, 'claims' | 'storageScopeId'>): Promise<Array<{
|
|
32
|
+
claimId: string;
|
|
33
|
+
state: string;
|
|
34
|
+
blobKeys: string[];
|
|
35
|
+
}>>;
|
|
36
|
+
/** Start one correlated purge and pause immediately before its first external DELETE. */
|
|
37
|
+
export declare function pausePurgeBeforeDelete(host: PurgeTestHost, artifactId: string): Promise<{
|
|
38
|
+
claimId: string;
|
|
39
|
+
blobKeys: string[];
|
|
40
|
+
release(): void;
|
|
41
|
+
finished: Promise<void>;
|
|
42
|
+
}>;
|
|
43
|
+
/** Create a correlated test barrier for one finalization run. */
|
|
44
|
+
export declare function createArtifactCommitBarrier(options?: {
|
|
45
|
+
readonly points: readonly CommitBarrierPoint[];
|
|
46
|
+
}): {
|
|
47
|
+
observer: CommitBarrierObserver & CommitArrivalObserver;
|
|
48
|
+
waitUntilReached(point: CommitBarrierPoint, invocationId: string): Promise<void>;
|
|
49
|
+
release(point: CommitBarrierPoint, invocationId: string): void;
|
|
50
|
+
};
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export const COMMIT_BARRIER_POINTS = [
|
|
2
|
+
'before_connection_acquire',
|
|
3
|
+
'before_key_lock',
|
|
4
|
+
'before_blob_put',
|
|
5
|
+
'after_blob_put_before_verify',
|
|
6
|
+
'before_blob_readback',
|
|
7
|
+
'before_finalization_row_lock',
|
|
8
|
+
'after_finalization_locks_before_guard',
|
|
9
|
+
'after_replay_lookup_before_guard',
|
|
10
|
+
'before_commit',
|
|
11
|
+
];
|
|
12
|
+
export async function inspectDeletionClaims(host) {
|
|
13
|
+
return (await host.claims.listClaims()).map((claim) => ({ claimId: claim.claimId, state: claim.state,
|
|
14
|
+
blobKeys: [`artifacts/${host.storageScopeId}/blobs/sha256/${claim.digest.slice(7)}`] }));
|
|
15
|
+
}
|
|
16
|
+
/** Start one correlated purge and pause immediately before its first external DELETE. */
|
|
17
|
+
export async function pausePurgeBeforeDelete(host, artifactId) {
|
|
18
|
+
let release;
|
|
19
|
+
const gate = new Promise((resolve) => { release = resolve; });
|
|
20
|
+
let reached;
|
|
21
|
+
let fail;
|
|
22
|
+
const arrival = new Promise((resolve, reject) => {
|
|
23
|
+
reached = resolve;
|
|
24
|
+
fail = reject;
|
|
25
|
+
});
|
|
26
|
+
const deleting = { ...host.blobs,
|
|
27
|
+
putBlob: (key, bytes) => host.blobs.putBlob(key, bytes),
|
|
28
|
+
getBlob: (key) => host.blobs.getBlob(key), listKeys: (prefix) => host.blobs.listKeys(prefix),
|
|
29
|
+
async deleteBlob(key) {
|
|
30
|
+
const claims = (await host.claims.listClaims()).filter((claim) => claim.holder === `purge:${artifactId}` && claim.state === 'pending');
|
|
31
|
+
const matching = claims.find((claim) => key.endsWith(`/sha256/${claim.digest.slice(7)}`));
|
|
32
|
+
if (matching === undefined)
|
|
33
|
+
return host.blobs.deleteBlob(key);
|
|
34
|
+
reached({ claimId: matching.claimId,
|
|
35
|
+
blobKeys: claims.map((claim) => `artifacts/${host.storageScopeId}/blobs/sha256/${claim.digest.slice(7)}`) });
|
|
36
|
+
await gate;
|
|
37
|
+
await host.blobs.deleteBlob(key);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
const finished = host.purge(artifactId, deleting);
|
|
41
|
+
void finished.then(() => fail(new Error('purge finished without external DELETE')), fail);
|
|
42
|
+
return { ...await arrival, release, finished };
|
|
43
|
+
}
|
|
44
|
+
/** Create a correlated test barrier for one finalization run. */
|
|
45
|
+
export function createArtifactCommitBarrier(options) {
|
|
46
|
+
const gates = new Map();
|
|
47
|
+
const gateFor = (point, invocationId) => {
|
|
48
|
+
const key = `${point}:${invocationId}`;
|
|
49
|
+
const existing = gates.get(key);
|
|
50
|
+
if (existing !== undefined)
|
|
51
|
+
return existing;
|
|
52
|
+
const created = { arrivals: 0, armed: options?.points.includes(point) ?? false, waiters: [], releases: [] };
|
|
53
|
+
gates.set(key, created);
|
|
54
|
+
return created;
|
|
55
|
+
};
|
|
56
|
+
const arrive = (point, invocationId) => {
|
|
57
|
+
const gate = gateFor(point, invocationId);
|
|
58
|
+
gate.arrivals += 1;
|
|
59
|
+
for (const waiter of gate.waiters.splice(0))
|
|
60
|
+
waiter();
|
|
61
|
+
if (!gate.armed)
|
|
62
|
+
return Promise.resolve();
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
gate.releases.push(resolve);
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
observer: Object.assign(arrive, { arrive }),
|
|
69
|
+
waitUntilReached: (point, invocationId) => {
|
|
70
|
+
const gate = gateFor(point, invocationId);
|
|
71
|
+
if (gate.arrivals > 0)
|
|
72
|
+
return Promise.resolve();
|
|
73
|
+
gate.armed = true;
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
gate.waiters.push(resolve);
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
release: (point, invocationId) => {
|
|
79
|
+
const gate = gateFor(point, invocationId);
|
|
80
|
+
const resume = gate.releases.shift();
|
|
81
|
+
if (resume === undefined)
|
|
82
|
+
throw new Error(`no paused barrier at ${point} for ${invocationId}`);
|
|
83
|
+
resume();
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Artifacts 0.2.0 transport migration
|
|
2
|
+
|
|
3
|
+
The registry baseline checked for this change is `@sentropic/artifacts@0.1.4`.
|
|
4
|
+
Radar 0.1.2 and upstream 0.1.4 hosts must migrate their composition before
|
|
5
|
+
loading the 0.2.0 HTTP or MCP surface. Publication is a separate owner-approved
|
|
6
|
+
release step; this branch does not publish packages.
|
|
7
|
+
|
|
8
|
+
## Required host composition
|
|
9
|
+
|
|
10
|
+
Compile an explicit immutable profile with `compileArtifactsProfile`. Radar
|
|
11
|
+
selects `artifacts_ensure`, `artifacts_publish`, `artifacts_list` and
|
|
12
|
+
`artifacts_get`. Pass the same compiled profile to `createHttpArtifactApp`,
|
|
13
|
+
the MCP adapter/executor and `createArtifactsRegistryDefinition`. Empty
|
|
14
|
+
selection exposes nothing. Missing configuration fails construction.
|
|
15
|
+
|
|
16
|
+
Supply a raw PostgreSQL pool to `createPostgresCommitExecutor` and a
|
|
17
|
+
transaction-bound policy participant factory to `createArtifactCommitCoordinator`.
|
|
18
|
+
The coordinator owns BEGIN, key locking, COMMIT, rollback and release.
|
|
19
|
+
Remove session advisory-lock wrappers, pool-routing proxies, nested BEGIN
|
|
20
|
+
and no-op release wrappers from the old provider composition.
|
|
21
|
+
|
|
22
|
+
Create one trusted `ArtifactInvocationContext` for every request, containing
|
|
23
|
+
the verified actor, trusted scope, invocation ID and `authorizeCommit` guard.
|
|
24
|
+
Pass it to `app.execute(request, invocation)` or `app.handle(request, invocation)`.
|
|
25
|
+
Accounting hosts call `execute` once and project its typed outcome themselves.
|
|
26
|
+
Do not call both methods for the same operation. The MCP adapter requires
|
|
27
|
+
`createInvocation` and verifies its actor against `ctx.principal.sub`.
|
|
28
|
+
HTTP and MCP must use the identical canonical verified actor string.
|
|
29
|
+
|
|
30
|
+
Supply one qualified `createRadarKeyDerivation` binding to both transports.
|
|
31
|
+
Caller arguments contain the original caller key; actor, scope, internal key,
|
|
32
|
+
hash and guard come from the host. An optional platform envelope must exactly
|
|
33
|
+
match the argument key. Keep publication URLs under one shared
|
|
34
|
+
`publicationUrls` policy so HTTP and MCP return identical receipts.
|
|
35
|
+
|
|
36
|
+
## Public coordinator example and cleanup
|
|
37
|
+
|
|
38
|
+
The packed `examples/radar-commit-setup.ts` uses public entrypoints only:
|
|
39
|
+
`@sentropic/artifacts/adapters/postgres-commit`, `/server/commit-contract`
|
|
40
|
+
and `/mcp-core`. The commit contract exports `CommitExecutor`,
|
|
41
|
+
`CommitCoordinatorOptions`, `ArtifactPolicyDriver` and `CoordinatorTestPoint`.
|
|
42
|
+
The example compiles the frozen `ArtifactProviderProfile` shape, binds policy
|
|
43
|
+
reads/quota to the acquired PostgreSQL transaction and mints its guard fence.
|
|
44
|
+
|
|
45
|
+
Before activation, configure `cleanupWindowMs` on guarded publication and
|
|
46
|
+
start a cleanup scheduler; the example supplies a 30-second preparation
|
|
47
|
+
lifetime and a one-second serialized sweep. Each sweep processes at most
|
|
48
|
+
100 attempts. Capacity, sweep cadence and storage availability determine
|
|
49
|
+
whether the configured deadline is met; failed cleanup remains observable debt.
|
|
50
|
+
Reads are limited to the same batch size, with unsettled debt first. Settled
|
|
51
|
+
attempts receive metadata-only existence checks for 60 seconds after their
|
|
52
|
+
deadline; subsequent orphan recovery uses the host's ordinary GC. No cleanup
|
|
53
|
+
or inspection downloads blob contents. Invalid metadata is retained as failed
|
|
54
|
+
debt, excluded from subsequent sweeps, and returned in inspection's
|
|
55
|
+
`invalidIntents`; the sweep reports it after processing the remaining batch.
|
|
56
|
+
|
|
57
|
+
`/server/object-intents` exposes `inspectPreparations(host, artifactId)` and
|
|
58
|
+
`scheduleBoundedPreparationCleanup(host, throughMs)`. Bind `host.intents`
|
|
59
|
+
to the PostgreSQL artifact store, plus `blobs`, `refs` and `claims`.
|
|
60
|
+
Keep the claim store's transaction open through external DELETE, then commit
|
|
61
|
+
or roll back and release it, as `startRadarPreparationCleanup` demonstrates.
|
|
62
|
+
Inspection returns `attempts`, `protectedKeys` and `abandonedKeys`; every
|
|
63
|
+
attempt retains its ID, invocation, blob keys, preparation time and deadline.
|
|
64
|
+
Cleanup invalidates expired preparations before removing their pins, retains
|
|
65
|
+
other references/pins, and never promotes an abandoned attempt to success.
|
|
66
|
+
|
|
67
|
+
`/testing` exports `pausePurgeBeforeDelete(host, artifactId)` and
|
|
68
|
+
`inspectDeletionClaims(host)`. Bind the test host's `storageScopeId`, `blobs`,
|
|
69
|
+
inspectable `claims`, and `purge(artifactId, deletingBlobs)` callback. The
|
|
70
|
+
callback must commit its durable `purge:<artifactId>` claims and use the
|
|
71
|
+
supplied deletion adapter for that purge only. The pause returns `claimId`,
|
|
72
|
+
`blobKeys`, `release()` and `finished`; release it in a `finally` block.
|
|
73
|
+
Inspection reads durable claim history without releasing exclusion or
|
|
74
|
+
granting publication authority. The seams do not alter L3 ports or profiles.
|
|
75
|
+
|
|
76
|
+
## Domain adapters and transaction boundaries
|
|
77
|
+
|
|
78
|
+
`ensureHost.lockBinding` and `resolveBinding` receive the coordinator's
|
|
79
|
+
transaction client. Resolve the binding and write ACL/lifecycle state on that
|
|
80
|
+
same client. Concurrent commands must serialize on the binding identity,
|
|
81
|
+
including when their caller keys differ. Replay resolves current identity
|
|
82
|
+
under this lock and compares it to the stored receipt. Missing or changed
|
|
83
|
+
targets deny before the host guard; explicit replay requires a binding.
|
|
84
|
+
|
|
85
|
+
Use `createGuardedEnsureService` and `createGuardedPublicationService` for
|
|
86
|
+
custom services. Pass the request-bound host guard as `publishFinalized(input)`'s
|
|
87
|
+
optional `authorizeCommit` field to preserve host approval policy and refusal codes;
|
|
88
|
+
it runs after lifecycle, ACL and policy revision checks. When omitted, publication
|
|
89
|
+
uses `participant.checkApproval` and denies unapproved or expired commits.
|
|
90
|
+
HTTP/MCP pass the invocation's host guard automatically.
|
|
91
|
+
The canonical coordinator exposes `finalizeOperation` and
|
|
92
|
+
`replayOperation` with the frozen L0 inputs. Bind fresh domain request plans
|
|
93
|
+
through its `operations` option; no process-local replay cache is used.
|
|
94
|
+
Provider adapters use `finalizePrepared` and `replayPrepared` on that same
|
|
95
|
+
coordinator. Guards run after all blocking waits and must be side-effect-free.
|
|
96
|
+
Policy/quota registration is trusted construction, never a public argument.
|
|
97
|
+
|
|
98
|
+
`ArtifactCommitParticipant.readAdmission` reads current approval/revision on
|
|
99
|
+
the leased transaction. Keep policy locks through COMMIT. Quota reservation
|
|
100
|
+
is provisional and rolls back with denial. Object authorization also runs
|
|
101
|
+
for replay; a stored success alone confers no authority.
|
|
102
|
+
|
|
103
|
+
Use `createPostgresGcService(executor, refsForTransaction)` for PostgreSQL GC.
|
|
104
|
+
The reference-store factory must bind its reads to the supplied client.
|
|
105
|
+
GC keeps the transaction and digest exclusion through external deletion and
|
|
106
|
+
claim completion. Do not pass a raw pool to transaction-scoped deletion claims.
|
|
107
|
+
|
|
108
|
+
## Durable state and failures
|
|
109
|
+
|
|
110
|
+
Stop old writers before enabling the new composition. Apply migrations
|
|
111
|
+
`0001_artifacts.sql`, `0002_lifecycle.sql` and additive `0003_coordinator.sql`.
|
|
112
|
+
Preserve historical rows and objects. Supply `legacyRecords`, using
|
|
113
|
+
`createPostgresLegacyArtifactRecords(pool)` for PostgreSQL. An empty inventory
|
|
114
|
+
reader is appropriate only for a verified fresh store.
|
|
115
|
+
|
|
116
|
+
Ambiguous legacy keys and unknown command codecs return a stable
|
|
117
|
+
503 `capability_unavailable`, non-retriable migration condition. They are
|
|
118
|
+
never promoted to newly authorized successes and never trigger allocation
|
|
119
|
+
under another key. Reconcile actual pilot inventory separately; unknown
|
|
120
|
+
historical array order cannot be recovered from a sorted manifest.
|
|
121
|
+
|
|
122
|
+
New receipts store the ordered `artifacts-command/v1` identity in
|
|
123
|
+
`artifacts.operation_receipts`. Manifest digests retain their historical
|
|
124
|
+
codec. A denied attempt has no success receipt; reauthorization may retry the
|
|
125
|
+
same key. An unknown COMMIT outcome requires the same key and a fresh guarded
|
|
126
|
+
lookup. Private failure causes remain internal. HTTP and MCP project the same
|
|
127
|
+
public code and retry classification; status 503 alone does not imply retry.
|
|
128
|
+
|
|
129
|
+
Public host imports are `@sentropic/artifacts/server/commit-contract`,
|
|
130
|
+
`@sentropic/artifacts/server/outcome-contract` and `@sentropic/artifacts/testing`.
|
|
131
|
+
The `/mcp-core` runtime stays independent of the MCP platform runtime.
|
|
132
|
+
For tests that subscribe after arrival, explicitly arm barrier points with
|
|
133
|
+
`createArtifactCommitBarrier({ points: ['before_commit'] })`. Other arrivals
|
|
134
|
+
remain observational; a barrier cannot grant authority or certify success.
|