@sentropic/artifacts 0.2.0 → 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 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.
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.1";
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.1';
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
  },
@@ -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;
@@ -247,7 +247,7 @@ export function createGuardedPublicationService(deps) {
247
247
  const cleanupWindowMs = deps.cleanupWindowMs ?? DEFAULT_CLEANUP_WINDOW_MS;
248
248
  if (!Number.isSafeInteger(cleanupWindowMs) || cleanupWindowMs < 1)
249
249
  throw new Error('invalid preparation lifetime');
250
- const guard = (client) => async (context, transaction) => {
250
+ const guard = (client, authorizeCommit) => async (context, transaction) => {
251
251
  const { state, acl } = await deps.createUnitWriter(client()).readAuthorization(context.artifactId);
252
252
  const verdict = decidePublishAuthorization({ lifecycleState: state, acl, actorId: context.actorId });
253
253
  if (!verdict.allowed) {
@@ -259,19 +259,6 @@ export function createGuardedPublicationService(deps) {
259
259
  retriable: false,
260
260
  };
261
261
  }
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
262
  const revision = await transaction.participant.readRevision();
276
263
  if (revision !== context.policyRevision) {
277
264
  return {
@@ -282,6 +269,12 @@ export function createGuardedPublicationService(deps) {
282
269
  retriable: false,
283
270
  };
284
271
  }
272
+ if (authorizeCommit !== undefined)
273
+ return authorizeCommit(context, transaction);
274
+ const approval = await transaction.participant.checkApproval({ actorId: context.actorId, operation: context.operation });
275
+ if (!approval.approved) {
276
+ return { allowed: false, reason: approval.reason ?? 'denied', publicCode: 'access_denied', status: 403, retriable: false };
277
+ }
285
278
  return { allowed: true, fence: transaction.participant.mintFence(revision, context.invocationId) };
286
279
  };
287
280
  return {
@@ -365,7 +358,7 @@ export function createGuardedPublicationService(deps) {
365
358
  if (finalizationClient === undefined)
366
359
  throw new Error('publication guard needs the finalization client');
367
360
  return finalizationClient;
368
- }),
361
+ }, input.authorizeCommit),
369
362
  finishIntents: async (client) => {
370
363
  await deps.createUnitWriter(client).completeIntent(intent.intentId);
371
364
  },
@@ -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.1",
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",