@sentropic/artifacts 0.2.0 → 0.2.2

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 CHANGED
@@ -1,5 +1,35 @@
1
1
  # @sentropic/artifacts — generic artifact contracts (L-5a)
2
2
 
3
+ Version 0.2.1 fixes host commit authorization being masked by the built-in
4
+ publication guard and wires the optional trusted `mapHostError` dependency.
5
+ Publication retains its lifecycle, ACL and policy revision checks; the host
6
+ `authorizeCommit` binding decides account approval and its sanitized refusal code.
7
+ Custom services using `createGuardedPublicationService` may pass the request-bound
8
+ `authorizeCommit` in `publishFinalized(input)`. It runs after
9
+ lifecycle, ACL and policy revision checks. When omitted, publication keeps the
10
+ default `participant.checkApproval` check and denies unapproved or expired commits.
11
+
12
+ Bind the same mapper on `createHttpArtifactApp`, `createArtifactsToolExecutor`
13
+ (or `createArtifactsAdapter`) and `createArtifactCommitCoordinator` so host wrapper
14
+ errors are classified at their originating layer. For example:
15
+
16
+ ```ts
17
+ import { ArtifactOperationError, type ArtifactHostFailureMapper }
18
+ from '@sentropic/artifacts/server/outcome-contract';
19
+
20
+ const mapHostError: ArtifactHostFailureMapper = cause => {
21
+ const code = (cause as { code?: string } | null)?.code;
22
+ return code === 'artifact_s3_timeout' || code === 'artifact_connection_lost'
23
+ ? new ArtifactOperationError('provider_unavailable', { cause }) : undefined;
24
+ };
25
+ ```
26
+
27
+ Pass `{ ...existingOptions, mapHostError }` to each construction. The packed
28
+ `examples/radar-commit-setup.ts` also accepts this option. Unrecognized failures
29
+ keep their existing classification; private causes never enter HTTP/MCP results.
30
+ Frozen contracts, command identity and selected tools remain unchanged.
31
+
32
+
3
33
  Generic, host-independent artifact domain state for the Sentropic artifacts
4
34
  provider (`sentropic.artifacts`, spec-v8 §9), covering L-5a–5e. Version
5
35
  `0.1.2` adds UTF-8 text publication, exposes storage adapters and ships SQL migrations.
@@ -10,6 +10,7 @@
10
10
  * host and injected — this module never reads env or creates connections.
11
11
  */
12
12
  import { DeleteObjectCommand, GetObjectCommand, ListObjectsV2Command, PutObjectCommand, } from '@aws-sdk/client-s3';
13
+ import { assertBlobPrefix } from '../server/blob-prefix.js';
13
14
  const BLOB_KEY_PATTERN = /^artifacts\/[A-Za-z0-9_-]+\/blobs\/sha256\/[0-9a-f]{64}$/;
14
15
  /** Reject anything but digest-addressed blob keys, before any I/O. */
15
16
  function assertBlobKey(key) {
@@ -17,12 +18,6 @@ function assertBlobKey(key) {
17
18
  throw new Error(`invalid blob key (must be artifacts/<scope>/blobs/sha256/<hex>): ${key}`);
18
19
  }
19
20
  }
20
- /** Reject empty or escaping prefixes before any I/O. */
21
- function assertPrefix(prefix) {
22
- if (prefix.length === 0 || prefix.includes('..')) {
23
- throw new Error(`invalid blob prefix: ${prefix}`);
24
- }
25
- }
26
21
  /** Create an S3-backed blob store over an injected client (no env reads). */
27
22
  export function createS3BlobStore(options) {
28
23
  return {
@@ -49,7 +44,7 @@ export function createS3BlobStore(options) {
49
44
  }
50
45
  },
51
46
  async listKeys(prefix) {
52
- assertPrefix(prefix);
47
+ assertBlobPrefix(prefix);
53
48
  const keys = [];
54
49
  let token;
55
50
  do {
package/dist/http.d.ts CHANGED
@@ -6,7 +6,7 @@ import { type GuardedPublicationDeps, type PublicationService } from './server/p
6
6
  import type { AclStore } from './server/access.js';
7
7
  import type { LifecycleService } from './server/lifecycle.js';
8
8
  import { type ArtifactInvocationContext, type ArtifactCommitCoordinator } from './server/commit/types.js';
9
- import { type ArtifactOutcome } from './server/failures.js';
9
+ import { type ArtifactOutcome, type ArtifactHostFailureMapper } from './server/failures.js';
10
10
  export { projectArtifactHttpOutcome } from './server/outcome-projection.js';
11
11
  export interface HttpRequest {
12
12
  method: string;
@@ -27,6 +27,8 @@ export declare function errorBody(status: number): {
27
27
  error: string;
28
28
  };
29
29
  export interface HttpAppDeps {
30
+ /** Trusted mapping of host wrapper errors; causes remain private. Also bind on the coordinator. */
31
+ mapHostError?: ArtifactHostFailureMapper;
30
32
  profile: CompiledArtifactsProfile;
31
33
  coordinator: ArtifactCommitCoordinator;
32
34
  derivation: ArtifactKeyDerivationBinding;
package/dist/http.js CHANGED
@@ -110,25 +110,15 @@ export function createHttpArtifactApp(deps) {
110
110
  const acl = await visible(artifactId, invocation);
111
111
  if (acl.ownerId !== invocation.actorId)
112
112
  throw new ArtifactOperationError('access_denied');
113
- // Both finalization and fresh replay combine provider checks with this request's host guard.
114
- const bindGuard = (request) => ({ ...request,
115
- guard: async (context, transaction) => {
116
- const decision = await request.guard(context, transaction);
117
- return decision.allowed ? invocation.authorizeCommit(context, transaction) : decision;
118
- } });
119
- const coordinator = {
120
- ...deps.coordinator,
121
- finalizePrepared: (request) => deps.coordinator.finalizePrepared(bindGuard(request)),
122
- replayPrepared: (request) => deps.coordinator.replayPrepared(bindGuard(request)),
123
- };
124
113
  const limits = profile.operations.find((entry) => entry.operation === 'publish').limits;
125
- const publication = createGuardedPublicationService({ ...deps.publication, coordinator,
114
+ const publication = createGuardedPublicationService({ ...deps.publication, coordinator: deps.coordinator,
126
115
  limits: { maxFiles: limits.publishFilesMax, maxFileBytes: limits.maxDecodedFileBytes,
127
116
  maxTotalBytes: limits.maxDecodedTotalBytes } });
128
117
  const outcome = await publication.publishFinalized({
129
118
  ownerId: acl.ownerId, storageScopeId: invocation.storageScopeId, artifactId,
130
119
  identity: command.identity, actorId: invocation.actorId, invocationId: invocation.invocationId,
131
120
  attemptId: invocation.invocationId, policyRevision: await deps.ensureHost.readPolicyRevision(),
121
+ authorizeCommit: invocation.authorizeCommit,
132
122
  source: { kind: 'inline-original' }, profile: normalized.profile,
133
123
  files: normalized.files.map((file) => ({
134
124
  path: file.path, mimeType: file.mimeType, bytes: Uint8Array.from(file.bytes),
@@ -178,7 +168,7 @@ export function createHttpArtifactApp(deps) {
178
168
  catch (cause) {
179
169
  const failure = classifyArtifactFailure(cause, { invocationId: invocation?.invocationId ?? '',
180
170
  operation, phase, commitState }, (error) => error instanceof ArtifactProfileInputError
181
- ? new ArtifactOperationError(error.category) : undefined);
171
+ ? new ArtifactOperationError(error.category) : deps.mapHostError?.(error));
182
172
  if (cause instanceof CommitQuarantineError) {
183
173
  const migration = { ...failure, kind: 'operational', category: 'provider_unavailable',
184
174
  variant: 'legacy_quarantine', httpStatus: 503, publicCode: 'capability_unavailable', retriable: false };
@@ -4,13 +4,13 @@ import type { ProviderParameterSchema } from '@sentropic/mcp-platform/registry';
4
4
  import type { ArtifactApp, HttpResponse } from './http.js';
5
5
  import { type CompiledArtifactsProfile } from './profile/index.js';
6
6
  import type { ArtifactInvocationContext } from './server/commit/types.js';
7
- import { type ArtifactOutcome } from './server/failures.js';
7
+ import { type ArtifactOutcome, type ArtifactHostFailureMapper } from './server/failures.js';
8
8
  export * from './profile/index.js';
9
9
  export { createRadarKeyDerivation } from './server/command-identity.js';
10
10
  export type { ArtifactKeyDerivationBinding } from './server/command-identity.js';
11
11
  export declare const ARTIFACTS_PROVIDER_ID = "sentropic.artifacts";
12
12
  export declare const ARTIFACTS_APP_ID = "sentropic-artifacts";
13
- export declare const ARTIFACTS_PROVIDER_VERSION = "0.2.0";
13
+ export declare const ARTIFACTS_PROVIDER_VERSION = "0.2.2";
14
14
  export declare const ARTIFACTS_TOOL_NAMES: readonly ["artifacts_get_template", "artifacts_get_api_guide", "artifacts_ensure", "artifacts_publish", "artifacts_create_upload", "artifacts_commit_upload", "artifacts_list", "artifacts_get", "artifacts_get_source", "artifacts_fork", "artifacts_set_access", "artifacts_promote", "artifacts_renew", "artifacts_delete", "artifacts_restore"];
15
15
  export type ArtifactsToolName = (typeof ARTIFACTS_TOOL_NAMES)[number];
16
16
  export declare const ARTIFACTS_TOOL_SPECS: {
@@ -40,11 +40,14 @@ export interface ArtifactsInvokeInput {
40
40
  export declare function createArtifactsToolExecutor(deps: {
41
41
  app: ArtifactsApp;
42
42
  profile: CompiledArtifactsProfile;
43
+ mapHostError?: ArtifactHostFailureMapper;
43
44
  }): {
44
45
  execute: (input: ArtifactsInvokeInput) => Promise<ArtifactOutcome<HttpResponse>>;
45
46
  invoke(input: ArtifactsInvokeInput): Promise<HttpResponse>;
46
47
  };
47
48
  export interface ArtifactsAdapterOptions extends ArtifactsManifestOptions {
49
+ /** Trusted mapping for failures in the MCP host binding. */
50
+ mapHostError?: ArtifactHostFailureMapper;
48
51
  app: ArtifactsApp;
49
52
  /** Bind verified principal/scope and request authorization in trusted host code. */
50
53
  createInvocation(request: AppToolInvocation): ArtifactInvocationContext | Promise<ArtifactInvocationContext>;
package/dist/mcp-core.js CHANGED
@@ -6,7 +6,7 @@ export * from './profile/index.js';
6
6
  export { createRadarKeyDerivation } from './server/command-identity.js';
7
7
  export const ARTIFACTS_PROVIDER_ID = 'sentropic.artifacts';
8
8
  export const ARTIFACTS_APP_ID = 'sentropic-artifacts';
9
- export const ARTIFACTS_PROVIDER_VERSION = '0.2.0';
9
+ export const ARTIFACTS_PROVIDER_VERSION = '0.2.2';
10
10
  export const ARTIFACTS_TOOL_NAMES = ARTIFACT_CAPABILITY_NAMES;
11
11
  export const ARTIFACTS_TOOL_SPECS = ARTIFACTS_TOOL_NAMES.map((name) => ({ name,
12
12
  description: `Artifacts ${name.slice('artifacts_'.length).replaceAll('_', ' ')}.`,
@@ -36,9 +36,9 @@ export function createArtifactsManifest(options) {
36
36
  audit: { eventKinds: ['invoke'], artifactsProfileDigest: profile.profileDigest }, durability: {},
37
37
  };
38
38
  }
39
- function rejected(cause, invocationId, operation) {
39
+ function rejected(cause, invocationId, operation, mapHostError) {
40
40
  return { ok: false, failure: classifyArtifactFailure(cause, { invocationId, operation,
41
- phase: cause instanceof ArtifactProfileInputError ? cause.phase : 'entry', commitState: 'not_committed' }, (error) => error instanceof ArtifactProfileInputError ? new ArtifactOperationError(error.category) : undefined) };
41
+ phase: cause instanceof ArtifactProfileInputError ? cause.phase : 'entry', commitState: 'not_committed' }, (error) => error instanceof ArtifactProfileInputError ? new ArtifactOperationError(error.category) : mapHostError?.(error)) };
42
42
  }
43
43
  export function createArtifactsToolExecutor(deps) {
44
44
  assertArtifactsTransportProfile(deps.profile);
@@ -70,7 +70,7 @@ export function createArtifactsToolExecutor(deps) {
70
70
  storageScopeId: input.invocation.storageScopeId, idempotencyKey: input.idempotencyKey }, input.invocation);
71
71
  }
72
72
  catch (cause) {
73
- return rejected(cause, input.invocation?.invocationId ?? '', input.tool);
73
+ return rejected(cause, input.invocation?.invocationId ?? '', input.tool, deps.mapHostError);
74
74
  }
75
75
  }
76
76
  return { execute, async invoke(input) {
@@ -100,7 +100,7 @@ export function createArtifactsAdapter(options) {
100
100
  invocation, idempotencyKey: request.idempotencyKey });
101
101
  }
102
102
  catch (cause) {
103
- outcome = rejected(cause, request.ctx.requestId, request.capabilityRef);
103
+ outcome = rejected(cause, request.ctx.requestId, request.capabilityRef, options.mapHostError);
104
104
  }
105
105
  return projectArtifactMcpOutcome(outcome, request.ctx.auditId);
106
106
  },
@@ -0,0 +1,2 @@
1
+ /** Reject empty or escaping prefixes before enumeration or external I/O. */
2
+ export declare function assertBlobPrefix(prefix: string): void;
@@ -0,0 +1,14 @@
1
+ import { ArtifactOperationError } from './failures.js';
2
+ class InvalidBlobPrefixError extends ArtifactOperationError {
3
+ code = 'invalid_blob_prefix';
4
+ constructor(prefix) {
5
+ super('invalid_request');
6
+ this.message = `invalid blob prefix: ${prefix}`;
7
+ }
8
+ }
9
+ /** Reject empty or escaping prefixes before enumeration or external I/O. */
10
+ export function assertBlobPrefix(prefix) {
11
+ if (prefix.length === 0 || prefix.includes('..')) {
12
+ throw new InvalidBlobPrefixError(prefix);
13
+ }
14
+ }
@@ -59,10 +59,10 @@ const isConnectionLoss = (error) => {
59
59
  const LOCK_TIMEOUT_CODE = '55P03';
60
60
  /** Serialization/deadlock/cancel outcomes: retriable unavailability (r2 E3/P2-2). */
61
61
  const RETRIABLE_SQLSTATES = new Set(['40001', '40P01', '57014']);
62
- const classifyOperational = (request, phase, commitState, error) => {
62
+ const classifyOperational = (request, phase, commitState, error, mapHostError) => {
63
63
  const classified = classifyArtifactFailure(error, {
64
64
  invocationId: request.invocationId, operation: request.operation, phase, commitState,
65
- });
65
+ }, mapHostError);
66
66
  if (classified.kind === 'operational')
67
67
  return classified;
68
68
  return failure('provider_error', request, phase, commitState, error, { variant: 'unclassified' });
@@ -227,7 +227,7 @@ export function createArtifactCommitCoordinator(options) {
227
227
  const code = typeof error === 'object' && error !== null
228
228
  ? error.code : undefined;
229
229
  if (settleError === undefined && typeof code === 'string' && RETRIABLE_SQLSTATES.has(code)) {
230
- return { ok: false, failure: classifyOperational(request, 'commit', 'not_committed', error) };
230
+ return { ok: false, failure: classifyOperational(request, 'commit', 'not_committed', error, options.mapHostError) };
231
231
  }
232
232
  return unknownOutcome(request, 'commit', settleError === undefined ? error : { commit: error, settlement: settleError });
233
233
  };
@@ -374,7 +374,7 @@ export function createArtifactCommitCoordinator(options) {
374
374
  await abandon(request);
375
375
  return {
376
376
  ok: false,
377
- failure: classifyOperational(request, phase, 'not_committed', error),
377
+ failure: classifyOperational(request, phase, 'not_committed', error, options.mapHostError),
378
378
  };
379
379
  }
380
380
  const settle = trackSettlement(connection);
@@ -423,7 +423,7 @@ export function createArtifactCommitCoordinator(options) {
423
423
  }
424
424
  return {
425
425
  ok: false,
426
- failure: classifyOperational(request, phase, 'not_committed', error),
426
+ failure: classifyOperational(request, phase, 'not_committed', error, options.mapHostError),
427
427
  };
428
428
  }
429
429
  };
@@ -484,7 +484,7 @@ export function createArtifactCommitCoordinator(options) {
484
484
  ok: false,
485
485
  failure: isConnectionLoss(error)
486
486
  ? failure('provider_unavailable', request, phase, 'not_committed', error)
487
- : classifyOperational(request, phase, 'not_committed', error),
487
+ : classifyOperational(request, phase, 'not_committed', error, options.mapHostError),
488
488
  };
489
489
  }
490
490
  const active = connection;
@@ -613,7 +613,7 @@ export function createArtifactCommitCoordinator(options) {
613
613
  }
614
614
  return {
615
615
  ok: false,
616
- failure: classifyOperational(request, phase, 'not_committed', error),
616
+ failure: classifyOperational(request, phase, 'not_committed', error, options.mapHostError),
617
617
  };
618
618
  }
619
619
  },
@@ -630,7 +630,7 @@ export function createArtifactCommitCoordinator(options) {
630
630
  await abandon(request);
631
631
  return {
632
632
  ok: false,
633
- failure: classifyOperational(request, phase, 'not_committed', error),
633
+ failure: classifyOperational(request, phase, 'not_committed', error, options.mapHostError),
634
634
  };
635
635
  }
636
636
  const active = connection;
@@ -720,7 +720,7 @@ export function createArtifactCommitCoordinator(options) {
720
720
  }
721
721
  return {
722
722
  ok: false,
723
- failure: classifyOperational(request, phase, 'not_committed', error),
723
+ failure: classifyOperational(request, phase, 'not_committed', error, options.mapHostError),
724
724
  };
725
725
  }
726
726
  },
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import type { CommitArrivalObserver, CommitBarrierObserver, CommitBarrierPoint } from '../../testing.js';
15
15
  import type { commitFenceBrand, commitTransactionBrand } from './brands.js';
16
+ import type { ArtifactHostFailureMapper } from '../failures.js';
16
17
  /** Version of the frozen command-codec shape (r2 C6). */
17
18
  export declare const COMMAND_CODEC_VERSION: "artifacts-command/v1";
18
19
  /** Server-derived internal key: distinct from any caller key. */
@@ -302,6 +303,8 @@ export type ArtifactOutcome<T> = {
302
303
  export type FinalizeResult = ArtifactOutcome<StoredCommitReceipt>;
303
304
  /** Coordinator construction options. */
304
305
  export interface CommitCoordinatorOptions {
306
+ /** Trusted host wrapper classification for failures inside the atomic unit. */
307
+ readonly mapHostError?: ArtifactHostFailureMapper;
305
308
  readonly executor: CommitExecutor;
306
309
  readonly policy: CommitPolicyParticipantFactory;
307
310
  /** Trusted domain adapters; every call resolves fresh state, including replay. */
@@ -7,7 +7,7 @@ import type { ArtifactAcl } from './access.js';
7
7
  import type { ArtifactState } from './lifecycle.js';
8
8
  import { type RefcountStore } from './gc.js';
9
9
  import type { CommandIdentity } from './command-identity.js';
10
- import type { CommitLockFn, ArtifactCommitCoordinator, FinalizeResult, FinalizeTxClient } from './commit/types.js';
10
+ import type { AuthorizeCommit, CommitLockFn, ArtifactCommitCoordinator, FinalizeResult, FinalizeTxClient } from './commit/types.js';
11
11
  export { DEFAULT_PUBLICATION_LIMITS, type PublicationLimits } from './publication-limits.js';
12
12
  export interface ReleaseRecord {
13
13
  releaseId: string;
@@ -127,6 +127,8 @@ export interface GuardedPublishInput extends PublishInput {
127
127
  readonly identity: CommandIdentity;
128
128
  readonly actorId: string;
129
129
  readonly policyRevision: string;
130
+ /** Request-bound host approval guard; defaults to participant.checkApproval. */
131
+ readonly authorizeCommit?: AuthorizeCommit;
130
132
  readonly callerKey?: string;
131
133
  readonly attemptId?: string;
132
134
  readonly invocationId?: string;
@@ -12,6 +12,7 @@
12
12
  * serve unit tests and the Postgres/S3 adapters serve integration.
13
13
  */
14
14
  import { createHash } from 'node:crypto';
15
+ import { assertBlobPrefix } from './blob-prefix.js';
15
16
  import { canonicalManifest, manifestDigest } from '../codecs.js';
16
17
  import { parseSourceRef } from './sources.js';
17
18
  import { publicationLimits, validatePublicationFiles } from './publication-limits.js';
@@ -59,8 +60,9 @@ export function createMemoryBlobStore() {
59
60
  getBlob(key) {
60
61
  return Promise.resolve(blobs.get(key));
61
62
  },
62
- listKeys(prefix) {
63
- return Promise.resolve([...blobs.keys()].filter((key) => key.startsWith(prefix)).sort());
63
+ async listKeys(prefix) {
64
+ assertBlobPrefix(prefix);
65
+ return [...blobs.keys()].filter((key) => key.startsWith(prefix)).sort();
64
66
  },
65
67
  deleteBlob(key) {
66
68
  if (!blobs.has(key))
@@ -247,7 +249,7 @@ export function createGuardedPublicationService(deps) {
247
249
  const cleanupWindowMs = deps.cleanupWindowMs ?? DEFAULT_CLEANUP_WINDOW_MS;
248
250
  if (!Number.isSafeInteger(cleanupWindowMs) || cleanupWindowMs < 1)
249
251
  throw new Error('invalid preparation lifetime');
250
- const guard = (client) => async (context, transaction) => {
252
+ const guard = (client, authorizeCommit) => async (context, transaction) => {
251
253
  const { state, acl } = await deps.createUnitWriter(client()).readAuthorization(context.artifactId);
252
254
  const verdict = decidePublishAuthorization({ lifecycleState: state, acl, actorId: context.actorId });
253
255
  if (!verdict.allowed) {
@@ -259,19 +261,6 @@ export function createGuardedPublicationService(deps) {
259
261
  retriable: false,
260
262
  };
261
263
  }
262
- const approval = await transaction.participant.checkApproval({
263
- actorId: context.actorId,
264
- operation: context.operation,
265
- });
266
- if (!approval.approved) {
267
- return {
268
- allowed: false,
269
- reason: approval.reason ?? 'denied',
270
- publicCode: 'access_denied',
271
- status: 403,
272
- retriable: false,
273
- };
274
- }
275
264
  const revision = await transaction.participant.readRevision();
276
265
  if (revision !== context.policyRevision) {
277
266
  return {
@@ -282,6 +271,12 @@ export function createGuardedPublicationService(deps) {
282
271
  retriable: false,
283
272
  };
284
273
  }
274
+ if (authorizeCommit !== undefined)
275
+ return authorizeCommit(context, transaction);
276
+ const approval = await transaction.participant.checkApproval({ actorId: context.actorId, operation: context.operation });
277
+ if (!approval.approved) {
278
+ return { allowed: false, reason: approval.reason ?? 'denied', publicCode: 'access_denied', status: 403, retriable: false };
279
+ }
285
280
  return { allowed: true, fence: transaction.participant.mintFence(revision, context.invocationId) };
286
281
  };
287
282
  return {
@@ -365,7 +360,7 @@ export function createGuardedPublicationService(deps) {
365
360
  if (finalizationClient === undefined)
366
361
  throw new Error('publication guard needs the finalization client');
367
362
  return finalizationClient;
368
- }),
363
+ }, input.authorizeCommit),
369
364
  finishIntents: async (client) => {
370
365
  await deps.createUnitWriter(client).completeIntent(intent.intentId);
371
366
  },
@@ -83,7 +83,12 @@ under this lock and compares it to the stored receipt. Missing or changed
83
83
  targets deny before the host guard; explicit replay requires a binding.
84
84
 
85
85
  Use `createGuardedEnsureService` and `createGuardedPublicationService` for
86
- custom services. The canonical coordinator exposes `finalizeOperation` and
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
87
92
  `replayOperation` with the frozen L0 inputs. Bind fresh domain request plans
88
93
  through its `operations` option; no process-local replay cache is used.
89
94
  Provider adapters use `finalizePrepared` and `replayPrepared` on that same
@@ -206,12 +206,15 @@ export function createRadarCommitSetup(options: {
206
206
  pool: Pool;
207
207
  clock: { nowMs: number };
208
208
  quotaKey?: string;
209
+ /** Bind this same mapper on createHttpArtifactApp and createArtifactsAdapter. */
210
+ mapHostError?: CommitCoordinatorOptions['mapHostError'];
209
211
  testObserver?: CommitCoordinatorOptions['testObserver'];
210
212
  testPoints?: readonly CoordinatorTestPoint[];
211
213
  }): RadarCommitSetup {
212
214
  const quotaKey = options.quotaKey ?? 'radar-quota';
213
215
  const abandonedAttempts: AbandonedAttempt[] = [];
214
216
  const coordinator = createArtifactCommitCoordinator({
217
+ mapHostError: options.mapHostError,
215
218
  executor: createPostgresCommitExecutor(options.pool),
216
219
  policy: createParticipantFactory(quotaKey, options.clock),
217
220
  nowMs: () => options.clock.nowMs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentropic/artifacts",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Generic configurable artifacts MCP and artifact service: StorageScope/Artifact/Binding/Release model, canonical manifests, object authorization, idempotent intents (spec-v8 §9).",
5
5
  "type": "module",
6
6
  "license": "MIT",