@sentropic/artifacts 0.2.5 → 0.2.7
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/CHANGELOG.md +37 -0
- package/README.md +20 -6
- package/dist/adapters/postgres.js +11 -4
- package/dist/http.d.ts +4 -3
- package/dist/http.js +225 -37
- package/dist/mcp-core.d.ts +3 -3
- package/dist/mcp-core.js +23 -10
- package/dist/profile/contracts.d.ts +15 -5
- package/dist/profile/contracts.js +9 -6
- package/dist/profile/index.js +33 -4
- package/dist/profile/radar.js +4 -1
- package/dist/profile/schemas.js +35 -6
- package/dist/profile/validation.d.ts +3 -1
- package/dist/profile/validation.js +32 -13
- package/dist/server/access.d.ts +8 -0
- package/dist/server/command-identity.d.ts +4 -2
- package/dist/server/command-identity.js +5 -5
- package/dist/server/commit/coordinator.js +12 -9
- package/dist/server/commit/types.d.ts +4 -0
- package/dist/server/edits.d.ts +17 -0
- package/dist/server/edits.js +43 -0
- package/dist/server/failures.d.ts +31 -2
- package/dist/server/failures.js +14 -3
- package/dist/server/lifecycle.d.ts +2 -0
- package/dist/server/lifecycle.js +3 -0
- package/dist/server/management.d.ts +1 -0
- package/dist/server/management.js +9 -2
- package/dist/server/mime.d.ts +7 -0
- package/dist/server/mime.js +7 -0
- package/dist/server/outcome-projection.d.ts +2 -2
- package/dist/server/outcome-projection.js +25 -9
- package/dist/server/publication.d.ts +18 -1
- package/dist/server/publication.js +54 -5
- package/dist/server/result-schema.d.ts +14 -0
- package/dist/server/result-schema.js +43 -0
- package/dist/server/service.d.ts +2 -0
- package/dist/server/service.js +2 -0
- package/dist/server/source-read.d.ts +23 -3
- package/dist/server/source-read.js +17 -2
- package/dist/server/summary.d.ts +4 -3
- package/dist/server/summary.js +4 -3
- package/dist/server/templates.d.ts +5 -0
- package/dist/server/templates.js +30 -3
- package/dist/server/versions.d.ts +29 -0
- package/dist/server/versions.js +39 -0
- package/docs/migration-l6.md +8 -1
- package/migrations/0005_metadata.sql +14 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.7
|
|
4
|
+
|
|
5
|
+
- Fix the `artifacts_get_source` wire payload that broke page editing in claude.ai:
|
|
6
|
+
files are no longer returned as a JSON integer array (`bytes: Array.from(bytes)`).
|
|
7
|
+
Text mime types (the canonical `TEXT_MIME_PATTERN`) come back as `text` with
|
|
8
|
+
`encoding: "utf8"`; every other file — including non-UTF-8 "text" — as `base64`
|
|
9
|
+
with `encoding: "base64"`. Size and digest verification still run on the raw
|
|
10
|
+
bytes before encoding. An 816-byte page now serializes to a few KiB instead of
|
|
11
|
+
blowing the tool-result budget.
|
|
12
|
+
- `get_source` (and the conflict `currentSource`) no longer embeds any authoring
|
|
13
|
+
guide; the result carries only a `guideRef` (`{tool: "artifacts_get_api_guide",
|
|
14
|
+
version}`) naming the tool and version to fetch it from.
|
|
15
|
+
- Per-call budgets are unchanged: at most 32 files and 262144 decoded bytes; the
|
|
16
|
+
2 MiB serialized MCP result budget still holds for the full snapshot.
|
|
17
|
+
- Tool schema (`artifacts_get_source` description, v2 result schema source files),
|
|
18
|
+
the API guide text and the README source section describe the text/base64
|
|
19
|
+
encoding and the guide reference.
|
|
20
|
+
|
|
21
|
+
## 0.2.6
|
|
22
|
+
|
|
23
|
+
- Integrate part 1 with the source reads, sharing, terminal deletion, masking and
|
|
24
|
+
summaries published in 0.2.5. Apply `0005_metadata.sql` after 0004; it adds
|
|
25
|
+
editorial metadata and nullable release authors/lineage without backfilling history.
|
|
26
|
+
- Add version history and revert as a new release on the same stable URL, preserving
|
|
27
|
+
current metadata and ACLs. The v2 profile uses `targetReleaseId` and paginated history.
|
|
28
|
+
- Add full-bundle or exactly-once text edits, metadata on ensure/publish, and
|
|
29
|
+
transactional release CAS, metadata, references, quota and receipts.
|
|
30
|
+
- Separate trusted `profileRevisionId`, `requireExpectedCurrentReleaseId` and
|
|
31
|
+
`resultEnvelopeVersion`. Compatible bindings keep root results; strict v2 retains
|
|
32
|
+
commit state, replay status and typed failures through mcp-platform 0.4.1.
|
|
33
|
+
- Replay historical receipts before enforcing new preconditions or preparing edits.
|
|
34
|
+
Missing strict preconditions use the R2 `precondition_required` message and details.
|
|
35
|
+
|
|
36
|
+
IMMO activates part 1 only after its qualified input and result switchovers.
|
|
37
|
+
Artifacts 0.2.6 stays inside the existing cluster-mesh `>=0.2.0 <0.3.0` range;
|
|
38
|
+
the API consumes the workspace package and retains its pinned APP-P1 profile.
|
|
39
|
+
|
|
3
40
|
## 0.2.5
|
|
4
41
|
|
|
5
42
|
- D1: Read verified author manifests and file bytes for the current or a selected
|
package/README.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @sentropic/artifacts — generic artifact contracts (L-5a)
|
|
2
2
|
|
|
3
|
+
Publication profiles can set `requireExpectedCurrentReleaseId: true` to require an
|
|
4
|
+
explicit UUID or `null` on publish/revert. Strict profiles reject writers without
|
|
5
|
+
CAS. Omission remains supported in generic profiles. Replay the same caller key
|
|
6
|
+
after an unknown commit outcome; changing files, edits, metadata or the precondition
|
|
7
|
+
requires a new key. The `artifacts-outcome/v2` MCP envelope requires
|
|
8
|
+
`@sentropic/mcp-platform >=0.4.1` and preserves commit state and typed error details.
|
|
9
|
+
|
|
3
10
|
## Shared PostgreSQL activation (UP-T, 0.2.3)
|
|
4
11
|
|
|
5
12
|
W1.c/W3.a and Radar S1/A6 use the same imports:
|
|
@@ -314,7 +321,8 @@ The installed package includes SQL resources exported under
|
|
|
314
321
|
```js
|
|
315
322
|
import { readFile } from 'node:fs/promises';
|
|
316
323
|
|
|
317
|
-
const migrations = ['0001_artifacts.sql', '0002_lifecycle.sql'
|
|
324
|
+
const migrations = ['0001_artifacts.sql', '0002_lifecycle.sql', '0003_coordinator.sql',
|
|
325
|
+
'0004_terminal_controls.sql', '0005_metadata.sql'];
|
|
318
326
|
for (const name of migrations) {
|
|
319
327
|
const sql = await readFile(new URL(import.meta.resolve(
|
|
320
328
|
`@sentropic/artifacts/migrations/${name}`,
|
|
@@ -324,8 +332,10 @@ for (const name of migrations) {
|
|
|
324
332
|
```
|
|
325
333
|
|
|
326
334
|
Apply `0001_artifacts.sql` first to create the domain schema, then
|
|
327
|
-
`0002_lifecycle.sql` to add lifecycle, sharing, sources and GC support,
|
|
328
|
-
|
|
335
|
+
`0002_lifecycle.sql` to add lifecycle, sharing, sources and GC support, then
|
|
336
|
+
`0003_coordinator.sql`, `0004_terminal_controls.sql`, and `0005_metadata.sql`
|
|
337
|
+
(artifact display metadata and nullable release lineage),
|
|
338
|
+
before using the Postgres store. The host owns migration credentials, serialization,
|
|
329
339
|
transactions and the migration journal; execute each version through that
|
|
330
340
|
runner rather than running this read loop at application startup.
|
|
331
341
|
|
|
@@ -429,9 +439,13 @@ empty defaults. Existing profile selections remain explicit.
|
|
|
429
439
|
|
|
430
440
|
`POST /get_source` accepts `artifactId`, optional `releaseId`, and optional unique
|
|
431
441
|
`paths` (1–32 entries). Bind `publication.getRelease` and `sources.getSource` on
|
|
432
|
-
the HTTP app. It returns the canonical manifest, verified author
|
|
433
|
-
|
|
434
|
-
|
|
442
|
+
the HTTP app. It returns the canonical manifest, verified author files, and a
|
|
443
|
+
separate nullable `sourceRef`; readers need the distinct source permission.
|
|
444
|
+
Size and digest are verified on the raw bytes, then each file is encoded for the
|
|
445
|
+
wire: text mime types as `text` with `encoding: "utf8"`, all other files as
|
|
446
|
+
`base64` with `encoding: "base64"` — never an integer array. The result carries
|
|
447
|
+
a `guideRef` (`{tool, version}`) instead of the guide body. The snapshot is
|
|
448
|
+
limited to 32 files and 262144 decoded bytes. Hosts must qualify
|
|
435
449
|
a 2 MiB serialized MCP result budget before selecting this capability.
|
|
436
450
|
|
|
437
451
|
`POST /set_access` grants or revokes a named principal's read access, requires a
|
|
@@ -21,6 +21,9 @@ function toIntent(row) {
|
|
|
21
21
|
}
|
|
22
22
|
function toRelease(row) {
|
|
23
23
|
return {
|
|
24
|
+
authorPrincipalId: row.author_principal_id ?? null,
|
|
25
|
+
parentReleaseId: row.parent_release_id ?? null,
|
|
26
|
+
revertedFromReleaseId: row.reverted_from_release_id ?? null,
|
|
24
27
|
releaseId: row.id,
|
|
25
28
|
artifactId: row.artifact_id,
|
|
26
29
|
manifestDigest: row.manifest_digest,
|
|
@@ -143,11 +146,14 @@ export function createPostgresArtifactStore(pool) {
|
|
|
143
146
|
}
|
|
144
147
|
},
|
|
145
148
|
async getRelease(releaseId) {
|
|
146
|
-
const { rows } = await pool.query(`SELECT
|
|
147
|
-
FROM artifacts.releases WHERE id = $1`, [releaseId]);
|
|
149
|
+
const { rows } = await pool.query(`SELECT * FROM artifacts.releases WHERE id = $1`, [releaseId]);
|
|
148
150
|
const row = rows[0];
|
|
149
151
|
return row === undefined ? undefined : toRelease(row);
|
|
150
152
|
},
|
|
153
|
+
async listReleases(artifactId) {
|
|
154
|
+
const { rows } = await pool.query(`SELECT * FROM artifacts.releases WHERE artifact_id = $1 ORDER BY created_at, id`, [artifactId]);
|
|
155
|
+
return rows.map(toRelease);
|
|
156
|
+
},
|
|
151
157
|
async getCurrentReleaseId(artifactId) {
|
|
152
158
|
const { rows } = await pool.query(`SELECT release_id FROM artifacts.artifact_current WHERE artifact_id = $1`, [artifactId]);
|
|
153
159
|
return rows[0]?.release_id;
|
|
@@ -209,14 +215,15 @@ export function createPostgresArtifactStore(pool) {
|
|
|
209
215
|
principalId: grant.principal_id,
|
|
210
216
|
grant: grant.permission,
|
|
211
217
|
})),
|
|
218
|
+
...(row.metadata === null || Object.keys(row.metadata).length === 0 ? {} : { metadata: row.metadata }),
|
|
212
219
|
};
|
|
213
220
|
},
|
|
214
221
|
async putArtifactAcl(artifactId, acl) {
|
|
215
|
-
await pool.query(`UPDATE artifacts.artifacts SET is_public = $2,
|
|
222
|
+
await pool.query(`UPDATE artifacts.artifacts SET is_public = $2, metadata = $4,
|
|
216
223
|
lifecycle_state = CASE WHEN $3 THEN 'withdrawn'
|
|
217
224
|
WHEN lifecycle_state = 'withdrawn' THEN 'active'
|
|
218
225
|
ELSE lifecycle_state END
|
|
219
|
-
WHERE id = $1`, [artifactId, acl.isPublic, acl.withdrawn]);
|
|
226
|
+
WHERE id = $1`, [artifactId, acl.isPublic, acl.withdrawn, JSON.stringify(acl.metadata ?? {})]);
|
|
220
227
|
await pool.query(`DELETE FROM artifacts.artifact_access WHERE artifact_id = $1`, [
|
|
221
228
|
artifactId,
|
|
222
229
|
]);
|
package/dist/http.d.ts
CHANGED
|
@@ -40,7 +40,8 @@ export interface HttpAppDeps {
|
|
|
40
40
|
ensureHost: GuardedEnsureHost;
|
|
41
41
|
publication: Omit<GuardedPublicationDeps, 'coordinator' | 'limits'> & {
|
|
42
42
|
getCurrent: PublicationService['getCurrent'];
|
|
43
|
-
getRelease
|
|
43
|
+
getRelease: PublicationService['getRelease'];
|
|
44
|
+
listReleases: PublicationService['listReleases'];
|
|
44
45
|
};
|
|
45
46
|
sources?: Pick<SourceStore, 'getSource'>;
|
|
46
47
|
management?: {
|
|
@@ -48,7 +49,7 @@ export interface HttpAppDeps {
|
|
|
48
49
|
confirmation?: ArtifactConfirmationPort;
|
|
49
50
|
};
|
|
50
51
|
acls: AclStore;
|
|
51
|
-
lifecycle: Pick<LifecycleService, 'stateOf' | 'canRead'>;
|
|
52
|
+
lifecycle: Pick<LifecycleService, 'stateOf' | 'canRead' | 'getReleaseState'>;
|
|
52
53
|
isApprovedAccount?: (principalId: string) => boolean;
|
|
53
54
|
/** One trusted URL policy shared by HTTP and MCP receipts. */
|
|
54
55
|
publicationUrls?: (artifactId: string, releaseId: string) => {
|
|
@@ -63,7 +64,7 @@ export interface ArtifactApp {
|
|
|
63
64
|
handle(request: HttpRequest, invocation: ArtifactInvocationContext): Promise<HttpResponse>;
|
|
64
65
|
}
|
|
65
66
|
/** Retained for consumers describing canonical textual publication inputs. */
|
|
66
|
-
export
|
|
67
|
+
export { TEXT_MIME_PATTERN } from './server/mime.js';
|
|
67
68
|
/** Other mutation surfaces need their own qualification before host activation. */
|
|
68
69
|
export declare function assertArtifactsTransportProfile(value: unknown): asserts value is CompiledArtifactsProfile;
|
|
69
70
|
export declare function createHttpArtifactApp(deps: HttpAppDeps): ArtifactApp;
|
package/dist/http.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
+
/** Selected HTTP control surface over the authorized atomic services. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
1
3
|
import { parseManifest } from './codecs.js';
|
|
2
4
|
import { assertCompiledArtifactsProfile, ArtifactProfileInputError } from './profile/index.js';
|
|
3
5
|
import { createArtifactCommand } from './server/command-identity.js';
|
|
4
6
|
import { createGuardedEnsureService } from './server/service.js';
|
|
5
7
|
import { assertArtifactReplayMigration } from './server/idempotency.js';
|
|
6
8
|
import { createGuardedPublicationService } from './server/publication.js';
|
|
9
|
+
import { applyEdits } from './server/edits.js';
|
|
7
10
|
import { CommitQuarantineError } from './server/commit/types.js';
|
|
8
11
|
import { ArtifactOperationError, classifyArtifactFailure } from './server/failures.js';
|
|
9
12
|
import { projectArtifactHttpOutcome } from './server/outcome-projection.js';
|
|
10
13
|
import { readArtifactSource } from './server/source-read.js';
|
|
11
14
|
import { artifactSummary } from './server/summary.js';
|
|
15
|
+
import { artifactVersions } from './server/versions.js';
|
|
12
16
|
import { manageArtifact } from './server/management.js';
|
|
13
17
|
import { createPostgresManagementUnit } from './adapters/postgres-management.js';
|
|
18
|
+
import { TEXT_MIME_RE } from './server/mime.js';
|
|
19
|
+
import { API_GUIDE_REF } from './server/templates.js';
|
|
14
20
|
export { projectArtifactHttpOutcome } from './server/outcome-projection.js';
|
|
15
21
|
/** Stable content-route error codes; private driver messages never leave the host. */
|
|
16
22
|
export function errorBody(status) {
|
|
@@ -19,7 +25,7 @@ export function errorBody(status) {
|
|
|
19
25
|
500: 'internal_error', 503: 'capability_unavailable' }[status] ?? 'internal_error' };
|
|
20
26
|
}
|
|
21
27
|
/** Retained for consumers describing canonical textual publication inputs. */
|
|
22
|
-
export
|
|
28
|
+
export { TEXT_MIME_PATTERN } from './server/mime.js';
|
|
23
29
|
function object(value) {
|
|
24
30
|
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
25
31
|
throw new ArtifactProfileInputError();
|
|
@@ -29,7 +35,7 @@ function object(value) {
|
|
|
29
35
|
export function assertArtifactsTransportProfile(value) {
|
|
30
36
|
assertCompiledArtifactsProfile(value);
|
|
31
37
|
const supported = ['artifacts_ensure', 'artifacts_publish', 'artifacts_list', 'artifacts_get',
|
|
32
|
-
'artifacts_get_source', 'artifacts_set_access', 'artifacts_delete'];
|
|
38
|
+
'artifacts_list_versions', 'artifacts_get_source', 'artifacts_revert', 'artifacts_set_access', 'artifacts_delete'];
|
|
33
39
|
if (value.selectedCapabilities.some((name) => !supported.includes(name))) {
|
|
34
40
|
throw new Error('unsupported artifacts transport capability');
|
|
35
41
|
}
|
|
@@ -52,23 +58,140 @@ export function createHttpArtifactApp(deps) {
|
|
|
52
58
|
throw new Error('artifact delete confirmation port required');
|
|
53
59
|
}
|
|
54
60
|
const ensure = createGuardedEnsureService({ profile, coordinator: deps.coordinator,
|
|
55
|
-
derivation: deps.derivation, host:
|
|
56
|
-
|
|
61
|
+
derivation: deps.derivation, host: {
|
|
62
|
+
lockBinding: (client, binding) => deps.ensureHost.lockBinding(client, binding),
|
|
63
|
+
abandonAttempt: id => deps.ensureHost.abandonAttempt(id),
|
|
64
|
+
readPolicyRevision: () => deps.ensureHost.readPolicyRevision(),
|
|
65
|
+
async resolveBinding(client, binding) {
|
|
66
|
+
const receipt = await deps.ensureHost.resolveBinding(client, binding);
|
|
67
|
+
if (receipt.created && binding.metadata) {
|
|
68
|
+
await deps.publication.createUnitWriter(client).updateMetadata(receipt.artifactId, binding.metadata);
|
|
69
|
+
}
|
|
70
|
+
return receipt;
|
|
71
|
+
},
|
|
72
|
+
} });
|
|
73
|
+
async function visible(artifactId, invocation, action = 'read') {
|
|
57
74
|
const acl = await deps.acls.getArtifactAcl(artifactId);
|
|
58
75
|
if (!acl || acl.storageScopeId !== invocation.storageScopeId)
|
|
59
76
|
throw new ArtifactOperationError('not_found');
|
|
60
77
|
const state = await deps.lifecycle.stateOf(artifactId);
|
|
61
78
|
if (acl.ownerId === invocation.actorId && (state === 'deleted' || state === 'purged')) {
|
|
62
79
|
// Management must still authorize terminal owners before replaying a receipt.
|
|
63
|
-
if (
|
|
80
|
+
if (action === true)
|
|
64
81
|
return acl;
|
|
65
82
|
throw new ArtifactOperationError('gone');
|
|
66
83
|
}
|
|
67
|
-
if (!deps.lifecycle.canRead({ artifactId, state }, acl, invocation.actorId, 'read', deps.isApprovedAccount)) {
|
|
84
|
+
if (!deps.lifecycle.canRead({ artifactId, state }, acl, invocation.actorId, action === true ? 'read' : action, deps.isApprovedAccount)) {
|
|
68
85
|
throw new ArtifactOperationError('not_found');
|
|
69
86
|
}
|
|
70
87
|
return acl;
|
|
71
88
|
}
|
|
89
|
+
/** Display metadata fields present on validated args (ensure/publish). */
|
|
90
|
+
function metadataOf(normalized) {
|
|
91
|
+
const meta = {};
|
|
92
|
+
if (typeof normalized.title === 'string')
|
|
93
|
+
meta.title = normalized.title;
|
|
94
|
+
if (typeof normalized.description === 'string')
|
|
95
|
+
meta.description = normalized.description;
|
|
96
|
+
if (typeof normalized.icon === 'string' || normalized.icon === null)
|
|
97
|
+
meta.icon = normalized.icon;
|
|
98
|
+
return Object.keys(meta).length > 0 ? meta : undefined;
|
|
99
|
+
}
|
|
100
|
+
function blobKey(storageScopeId, digest) {
|
|
101
|
+
// Manifest digests are `sha256:<hex>`; physical keys are `.../blobs/sha256/<hex>`.
|
|
102
|
+
return `artifacts/${storageScopeId}/blobs/${digest.replace(':', '/')}`;
|
|
103
|
+
}
|
|
104
|
+
async function readManifestFiles(manifest, storageScopeId) {
|
|
105
|
+
const parsed = parseManifest(manifest);
|
|
106
|
+
const entries = parsed.files ?? [];
|
|
107
|
+
const files = [];
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
const bytes = await deps.publication.blobs.getBlob(blobKey(storageScopeId, entry.digest));
|
|
110
|
+
if (bytes === undefined)
|
|
111
|
+
throw new ArtifactOperationError('release_unavailable', { release: { reason: 'not_retained' } });
|
|
112
|
+
if (bytes.byteLength !== entry.size || `sha256:${createHash('sha256').update(bytes).digest('hex')}` !== entry.digest) {
|
|
113
|
+
throw new ArtifactOperationError('verification_failed');
|
|
114
|
+
}
|
|
115
|
+
let text;
|
|
116
|
+
if (TEXT_MIME_RE.test(entry.mimeType)) {
|
|
117
|
+
try {
|
|
118
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
119
|
+
}
|
|
120
|
+
catch { /* Non-UTF-8 is not editable. */ }
|
|
121
|
+
}
|
|
122
|
+
files.push({ path: entry.path, mimeType: entry.mimeType, bytes: new Uint8Array(bytes),
|
|
123
|
+
...(text === undefined ? {} : { text }) });
|
|
124
|
+
}
|
|
125
|
+
return files;
|
|
126
|
+
}
|
|
127
|
+
/** Apply publish edits to the current release's text files; typed edit_mismatch changes nothing. */
|
|
128
|
+
async function editedFiles(artifactId, invocation, edits, expected) {
|
|
129
|
+
const current = expected === undefined ? await deps.publication.getCurrent(artifactId)
|
|
130
|
+
: expected === null ? undefined : await deps.publication.getRelease(expected);
|
|
131
|
+
if (current === undefined) {
|
|
132
|
+
if (expected === null || expected === undefined)
|
|
133
|
+
throw new ArtifactOperationError('release_unavailable', { release: { reason: 'unpublished' } });
|
|
134
|
+
throw new ArtifactOperationError('not_found');
|
|
135
|
+
}
|
|
136
|
+
if (parseManifest(current.manifest).artifactId !== artifactId)
|
|
137
|
+
throw new ArtifactOperationError('not_found');
|
|
138
|
+
if (await deps.lifecycle.getReleaseState(current.releaseId) === 'purged') {
|
|
139
|
+
throw new ArtifactOperationError('release_unavailable', { release: { reason: 'purged' } });
|
|
140
|
+
}
|
|
141
|
+
const files = await readManifestFiles(current.manifest, invocation.storageScopeId);
|
|
142
|
+
return applyEdits(files, edits).map((file) => ({
|
|
143
|
+
path: file.path, mimeType: file.mimeType,
|
|
144
|
+
bytes: file.text === undefined ? file.bytes : new Uint8Array(Buffer.from(file.text, 'utf8')),
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
async function conflictSource(outcome, normalized, invocation) {
|
|
148
|
+
if (outcome.ok || outcome.failure.kind !== 'operational' || outcome.failure.variant !== 'pointer_conflict')
|
|
149
|
+
return outcome;
|
|
150
|
+
const artifactId = normalized.artifactId;
|
|
151
|
+
await visible(artifactId, invocation, 'source');
|
|
152
|
+
const conflict = outcome.failure.conflict;
|
|
153
|
+
if (!conflict?.currentReleaseId || normalized.edits === undefined)
|
|
154
|
+
return outcome;
|
|
155
|
+
const release = await deps.publication.getRelease(conflict.currentReleaseId);
|
|
156
|
+
if (!release || await deps.lifecycle.getReleaseState(release.releaseId) === 'purged')
|
|
157
|
+
return outcome;
|
|
158
|
+
const manifest = parseManifest(release.manifest);
|
|
159
|
+
const paths = new Set(normalized.edits.map(edit => edit.path));
|
|
160
|
+
const entries = manifest.files;
|
|
161
|
+
const files = (await readManifestFiles(release.manifest, invocation.storageScopeId)).filter(file => paths.has(file.path))
|
|
162
|
+
.map(file => ({ path: file.path, mimeType: file.mimeType, size: file.bytes.byteLength,
|
|
163
|
+
digest: entries.find(entry => entry.path === file.path).digest,
|
|
164
|
+
...(file.text === undefined
|
|
165
|
+
? { base64: Buffer.from(file.bytes).toString('base64'), encoding: 'base64' }
|
|
166
|
+
: { text: file.text, encoding: 'utf8' }) }));
|
|
167
|
+
await visible(artifactId, invocation, 'source');
|
|
168
|
+
const failure = { ...outcome.failure, conflict: { ...conflict, currentSource: {
|
|
169
|
+
artifactId, releaseId: release.releaseId, digest: conflict.currentDigest, manifest: release.manifest,
|
|
170
|
+
files, sourceRef: null, guideRef: API_GUIDE_REF, ...deps.publicationUrls?.(artifactId, release.releaseId),
|
|
171
|
+
} } };
|
|
172
|
+
Object.defineProperty(failure, 'cause', { value: outcome.failure.cause, enumerable: false });
|
|
173
|
+
return { ok: false, failure: Object.freeze(failure) };
|
|
174
|
+
}
|
|
175
|
+
/** Shared guarded-commit response for publish and revert (same commit path, same receipt shape). */
|
|
176
|
+
function commitOutcome(outcome, artifactId, meta, setCommitState) {
|
|
177
|
+
if (!outcome.ok) {
|
|
178
|
+
const failure = { ...outcome.failure, kind: 'operational' };
|
|
179
|
+
Object.defineProperty(failure, 'cause', { value: outcome.failure.cause, enumerable: false });
|
|
180
|
+
return { ok: false, failure: Object.freeze(failure) };
|
|
181
|
+
}
|
|
182
|
+
setCommitState(outcome.commitState);
|
|
183
|
+
const receipt = object(outcome.result.receipt);
|
|
184
|
+
if (typeof receipt.releaseId !== 'string' || typeof receipt.manifest !== 'string') {
|
|
185
|
+
throw new Error('invalid publication receipt');
|
|
186
|
+
}
|
|
187
|
+
return { ...outcome, result: { status: 201, body: {
|
|
188
|
+
artifactId, releaseId: receipt.releaseId, manifest: receipt.manifest,
|
|
189
|
+
previousReleaseId: receipt.previousReleaseId ?? null, updatedAt: receipt.updatedAt ?? null,
|
|
190
|
+
...(typeof receipt.revertedFromReleaseId === 'string' ? { revertedFromReleaseId: receipt.revertedFromReleaseId } : {}),
|
|
191
|
+
digest: parseManifest(receipt.manifest).digest,
|
|
192
|
+
...deps.publicationUrls?.(artifactId, receipt.releaseId), ...(receipt.metadata ?? meta ?? {}),
|
|
193
|
+
} } };
|
|
194
|
+
}
|
|
72
195
|
const app = {
|
|
73
196
|
selectedCapabilities: profile.selectedCapabilities,
|
|
74
197
|
profileDigest: profile.profileDigest,
|
|
@@ -96,10 +219,13 @@ export function createHttpArtifactApp(deps) {
|
|
|
96
219
|
throw new ArtifactProfileInputError();
|
|
97
220
|
}
|
|
98
221
|
let args;
|
|
99
|
-
|
|
222
|
+
const getPrefix = request.method !== 'GET' ? undefined : capability === 'artifacts_get' ? '/meta/'
|
|
223
|
+
: capability === 'artifacts_list_versions' ? '/versions/'
|
|
224
|
+
: capability === 'artifacts_get_source' ? '/source/' : undefined;
|
|
225
|
+
if (getPrefix !== undefined) {
|
|
100
226
|
if (request.body !== undefined && Object.keys(object(request.body)).length)
|
|
101
227
|
throw new ArtifactProfileInputError();
|
|
102
|
-
args = { artifactId: request.path.slice(
|
|
228
|
+
args = { artifactId: request.path.slice(getPrefix.length) };
|
|
103
229
|
}
|
|
104
230
|
else {
|
|
105
231
|
args = { ...object(request.body ?? {}) };
|
|
@@ -109,9 +235,9 @@ export function createHttpArtifactApp(deps) {
|
|
|
109
235
|
delete args.storageScopeId;
|
|
110
236
|
}
|
|
111
237
|
}
|
|
112
|
-
const normalized = profile.validate(capability, args, request.idempotencyKey);
|
|
113
|
-
const command = ['artifacts_ensure', 'artifacts_publish', 'artifacts_set_access', 'artifacts_delete'].includes(capability)
|
|
114
|
-
? createArtifactCommand(profile, capability, args, invocation, deps.derivation, request.idempotencyKey) : undefined;
|
|
238
|
+
const normalized = profile.validate(capability, args, request.idempotencyKey, { deferPrecondition: true });
|
|
239
|
+
const command = ['artifacts_ensure', 'artifacts_publish', 'artifacts_set_access', 'artifacts_delete', 'artifacts_revert'].includes(capability)
|
|
240
|
+
? createArtifactCommand(profile, capability, args, invocation, deps.derivation, request.idempotencyKey, { deferPrecondition: true }) : undefined;
|
|
115
241
|
if (command && ['ensure', 'publish'].includes(command.operation)) {
|
|
116
242
|
phase = 'migration';
|
|
117
243
|
await assertArtifactReplayMigration(deps.legacyRecords, command, invocation, normalized.idempotencyKey);
|
|
@@ -121,11 +247,20 @@ export function createHttpArtifactApp(deps) {
|
|
|
121
247
|
const outcome = await ensure.ensure(invocation, { ...normalized,
|
|
122
248
|
envelopeKey: request.idempotencyKey });
|
|
123
249
|
if (outcome.ok) {
|
|
250
|
+
commitState = outcome.commitState;
|
|
124
251
|
const state = await deps.lifecycle.stateOf(outcome.result.artifactId);
|
|
125
252
|
if (state === 'deleted' || state === 'purged')
|
|
126
253
|
throw new ArtifactOperationError('gone');
|
|
127
254
|
}
|
|
128
|
-
|
|
255
|
+
if (!outcome.ok)
|
|
256
|
+
return outcome;
|
|
257
|
+
const artifactId = outcome.result.artifactId;
|
|
258
|
+
const acl = await visible(artifactId, invocation);
|
|
259
|
+
const summary = artifactSummary({ artifactId, acl, actorId: invocation.actorId,
|
|
260
|
+
state: await deps.lifecycle.stateOf(artifactId), current: await deps.publication.getCurrent(artifactId),
|
|
261
|
+
urls: deps.publicationUrls, approved: deps.isApprovedAccount });
|
|
262
|
+
return { ...outcome, result: { status: 201, body: profile.resultEnvelopeVersion === 2
|
|
263
|
+
? { ...summary, ...outcome.result } : { ...outcome.result, ...acl.metadata } } };
|
|
129
264
|
}
|
|
130
265
|
if (capability === 'artifacts_get_source') {
|
|
131
266
|
const artifactId = normalized.artifactId;
|
|
@@ -145,15 +280,18 @@ export function createHttpArtifactApp(deps) {
|
|
|
145
280
|
commitState: 'not_committed', replayed: false };
|
|
146
281
|
}
|
|
147
282
|
if (capability === 'artifacts_set_access' || capability === 'artifacts_delete') {
|
|
148
|
-
await visible(normalized.artifactId, invocation, true);
|
|
283
|
+
const acl = await visible(normalized.artifactId, invocation, true);
|
|
284
|
+
if (acl.ownerId !== invocation.actorId)
|
|
285
|
+
throw new ArtifactOperationError('access_denied');
|
|
149
286
|
const outcome = await manageArtifact({ coordinator: deps.coordinator,
|
|
150
287
|
createUnit: deps.management?.createUnit ?? createPostgresManagementUnit,
|
|
151
288
|
confirmation: deps.management?.confirmation, readPolicyRevision: () => deps.ensureHost.readPolicyRevision(),
|
|
152
289
|
isApprovedPrincipal: deps.isApprovedAccount ?? (() => false),
|
|
290
|
+
validateNewCommand: () => { profile.validate(capability, args, request.idempotencyKey); },
|
|
153
291
|
}, invocation, command, normalized.confirmationRef);
|
|
154
292
|
return outcome.ok ? { ...outcome, result: { status: 200, body: outcome.result } } : outcome;
|
|
155
293
|
}
|
|
156
|
-
if (capability === 'artifacts_publish') {
|
|
294
|
+
if (capability === 'artifacts_publish' || capability === 'artifacts_revert') {
|
|
157
295
|
const artifactId = normalized.artifactId;
|
|
158
296
|
const acl = await visible(artifactId, invocation);
|
|
159
297
|
if (acl.ownerId !== invocation.actorId)
|
|
@@ -162,31 +300,53 @@ export function createHttpArtifactApp(deps) {
|
|
|
162
300
|
const publication = createGuardedPublicationService({ ...deps.publication, coordinator: deps.coordinator,
|
|
163
301
|
limits: { maxFiles: limits.publishFilesMax, maxFileBytes: limits.maxDecodedFileBytes,
|
|
164
302
|
maxTotalBytes: limits.maxDecodedTotalBytes } });
|
|
165
|
-
const
|
|
166
|
-
|
|
303
|
+
const meta = metadataOf(normalized);
|
|
304
|
+
const input = { ownerId: acl.ownerId, storageScopeId: invocation.storageScopeId, artifactId,
|
|
167
305
|
identity: command.identity, actorId: invocation.actorId, invocationId: invocation.invocationId,
|
|
168
306
|
attemptId: invocation.invocationId, policyRevision: await deps.ensureHost.readPolicyRevision(),
|
|
169
|
-
authorizeCommit: invocation.authorizeCommit,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
307
|
+
authorizeCommit: invocation.authorizeCommit, metadata: meta,
|
|
308
|
+
operation: capability === 'artifacts_revert' ? 'revert' : 'publish',
|
|
309
|
+
expectedCurrentReleaseId: normalized.expectedCurrentReleaseId };
|
|
310
|
+
const replay = await publication.replayFinalized(input);
|
|
311
|
+
if (replay !== undefined)
|
|
312
|
+
return commitOutcome(replay, artifactId, meta, state => { commitState = state; });
|
|
313
|
+
profile.validate(capability, args, request.idempotencyKey);
|
|
314
|
+
let files;
|
|
315
|
+
let source;
|
|
316
|
+
let releaseProfile;
|
|
317
|
+
if (capability === 'artifacts_publish') {
|
|
318
|
+
files = normalized.edits !== undefined
|
|
319
|
+
? await editedFiles(artifactId, invocation, normalized.edits, input.expectedCurrentReleaseId)
|
|
320
|
+
: normalized.files.map((file) => ({
|
|
321
|
+
path: file.path, mimeType: file.mimeType, bytes: Uint8Array.from(file.bytes),
|
|
322
|
+
}));
|
|
323
|
+
if (normalized.edits !== undefined) {
|
|
324
|
+
const { edits: _edits, ...snapshot } = normalized;
|
|
325
|
+
profile.validate(capability, { ...snapshot, files: files.map(file => ({ ...file, bytes: Array.from(file.bytes) })) });
|
|
326
|
+
}
|
|
327
|
+
source = { kind: 'inline-original' };
|
|
328
|
+
releaseProfile = normalized.profile;
|
|
179
329
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
330
|
+
else {
|
|
331
|
+
const releaseId = normalized[profile.resultEnvelopeVersion === 2 ? 'targetReleaseId' : 'releaseId'];
|
|
332
|
+
const target = await deps.publication.getRelease(releaseId);
|
|
333
|
+
const targetManifest = target === undefined ? undefined : parseManifest(target.manifest);
|
|
334
|
+
if (target === undefined || targetManifest?.artifactId !== artifactId) {
|
|
335
|
+
throw new ArtifactOperationError('not_found');
|
|
336
|
+
}
|
|
337
|
+
if (await deps.lifecycle.getReleaseState(releaseId) === 'purged') {
|
|
338
|
+
throw new ArtifactOperationError('release_unavailable', { release: { reason: 'purged' } });
|
|
339
|
+
}
|
|
340
|
+
files = (await readManifestFiles(target.manifest, invocation.storageScopeId))
|
|
341
|
+
.map((file) => ({ path: file.path, mimeType: file.mimeType, bytes: file.bytes }));
|
|
342
|
+
source = { kind: 'base-commit', commit: releaseId };
|
|
343
|
+
releaseProfile = (targetManifest.profile ?? {});
|
|
184
344
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
345
|
+
const outcome = await publication.publishFinalized({
|
|
346
|
+
...input, source, profile: releaseProfile, files,
|
|
347
|
+
});
|
|
348
|
+
const committed = commitOutcome(outcome, artifactId, meta, (state) => { commitState = state; });
|
|
349
|
+
return await conflictSource(committed, normalized, invocation);
|
|
190
350
|
}
|
|
191
351
|
if (capability === 'artifacts_get') {
|
|
192
352
|
const artifactId = normalized.artifactId;
|
|
@@ -196,8 +356,34 @@ export function createHttpArtifactApp(deps) {
|
|
|
196
356
|
return { ok: true, invocationId: invocation.invocationId, commitState: 'not_committed', replayed: false,
|
|
197
357
|
result: { status: 200, body: { ...artifactSummary({ artifactId, acl, state, current,
|
|
198
358
|
actorId: invocation.actorId, urls: deps.publicationUrls, approved: deps.isApprovedAccount }),
|
|
359
|
+
...(acl.metadata !== undefined ? { metadata: acl.metadata } : {}),
|
|
199
360
|
...(acl.ownerId === invocation.actorId ? { grants: acl.grants } : {}) } } };
|
|
200
361
|
}
|
|
362
|
+
if (capability === 'artifacts_list_versions') {
|
|
363
|
+
const artifactId = normalized.artifactId;
|
|
364
|
+
await visible(artifactId, invocation);
|
|
365
|
+
const current = await deps.publication.getCurrent(artifactId);
|
|
366
|
+
if (profile.resultEnvelopeVersion === 2) {
|
|
367
|
+
const body = await artifactVersions({ artifactId, storageScopeId: invocation.storageScopeId,
|
|
368
|
+
currentReleaseId: current?.releaseId ?? null, releases: await deps.publication.listReleases(artifactId),
|
|
369
|
+
limit: normalized.limit, cursor: normalized.cursor,
|
|
370
|
+
state: id => deps.lifecycle.getReleaseState(id), urls: deps.publicationUrls });
|
|
371
|
+
await visible(artifactId, invocation);
|
|
372
|
+
return { ok: true, invocationId: invocation.invocationId, commitState: 'not_committed', replayed: false,
|
|
373
|
+
result: { status: 200, body } };
|
|
374
|
+
}
|
|
375
|
+
const releases = [];
|
|
376
|
+
for (const release of await deps.publication.listReleases(artifactId)) {
|
|
377
|
+
const manifest = parseManifest(release.manifest);
|
|
378
|
+
releases.push({ releaseId: release.releaseId, createdAt: release.createdAt,
|
|
379
|
+
author: typeof manifest.ownerId === 'string' ? manifest.ownerId : null,
|
|
380
|
+
state: await deps.lifecycle.getReleaseState(release.releaseId),
|
|
381
|
+
isCurrent: current?.releaseId === release.releaseId,
|
|
382
|
+
digest: release.manifestDigest });
|
|
383
|
+
}
|
|
384
|
+
return { ok: true, invocationId: invocation.invocationId, commitState: 'not_committed', replayed: false,
|
|
385
|
+
result: { status: 200, body: { artifactId, total: releases.length, releases } } };
|
|
386
|
+
}
|
|
201
387
|
const visibleArtifacts = [];
|
|
202
388
|
for (const artifactId of await deps.acls.listArtifactIds()) {
|
|
203
389
|
const acl = await deps.acls.getArtifactAcl(artifactId);
|
|
@@ -217,7 +403,7 @@ export function createHttpArtifactApp(deps) {
|
|
|
217
403
|
catch (cause) {
|
|
218
404
|
const failure = classifyArtifactFailure(cause, { invocationId: invocation?.invocationId ?? '',
|
|
219
405
|
operation, phase, commitState }, (error) => error instanceof ArtifactProfileInputError
|
|
220
|
-
? new ArtifactOperationError(error.category) : deps.mapHostError?.(error));
|
|
406
|
+
? new ArtifactOperationError(error.category, { publicDetails: error.details }) : deps.mapHostError?.(error));
|
|
221
407
|
if (cause instanceof CommitQuarantineError) {
|
|
222
408
|
const migration = { ...failure, kind: 'operational', category: 'provider_unavailable',
|
|
223
409
|
variant: 'legacy_quarantine', httpStatus: 503, publicCode: 'capability_unavailable', retriable: false };
|
|
@@ -227,7 +413,9 @@ export function createHttpArtifactApp(deps) {
|
|
|
227
413
|
return { ok: false, failure };
|
|
228
414
|
}
|
|
229
415
|
},
|
|
230
|
-
async handle(request, invocation) {
|
|
416
|
+
async handle(request, invocation) {
|
|
417
|
+
return projectArtifactHttpOutcome(await app.execute(request, invocation), profile.resultEnvelopeVersion === 2);
|
|
418
|
+
},
|
|
231
419
|
};
|
|
232
420
|
return Object.freeze(app);
|
|
233
421
|
}
|
package/dist/mcp-core.d.ts
CHANGED
|
@@ -10,11 +10,11 @@ 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.
|
|
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"];
|
|
13
|
+
export declare const ARTIFACTS_PROVIDER_VERSION = "0.2.7";
|
|
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", "artifacts_list_versions", "artifacts_revert"];
|
|
15
15
|
export type ArtifactsToolName = (typeof ARTIFACTS_TOOL_NAMES)[number];
|
|
16
16
|
export declare const ARTIFACTS_TOOL_SPECS: {
|
|
17
|
-
name: "artifacts_ensure" | "artifacts_publish" | "artifacts_set_access" | "artifacts_delete" | "artifacts_get_template" | "artifacts_get_api_guide" | "artifacts_create_upload" | "artifacts_commit_upload" | "artifacts_list" | "artifacts_get" | "artifacts_get_source" | "artifacts_fork" | "artifacts_promote" | "artifacts_renew" | "artifacts_restore";
|
|
17
|
+
name: "artifacts_ensure" | "artifacts_publish" | "artifacts_set_access" | "artifacts_delete" | "artifacts_revert" | "artifacts_get_template" | "artifacts_get_api_guide" | "artifacts_create_upload" | "artifacts_commit_upload" | "artifacts_list" | "artifacts_get" | "artifacts_get_source" | "artifacts_fork" | "artifacts_promote" | "artifacts_renew" | "artifacts_restore" | "artifacts_list_versions";
|
|
18
18
|
description: string;
|
|
19
19
|
mutability: import("@sentropic/mcp-platform").Mutability;
|
|
20
20
|
}[];
|