@sentropic/artifacts 0.1.2 → 0.1.4
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 +57 -4
- package/dist/content/handler.js +79 -63
- package/dist/http.d.ts +2 -0
- package/dist/http.js +38 -22
- package/dist/mcp-core.d.ts +62 -0
- package/dist/mcp-core.js +272 -0
- package/dist/mcp.d.ts +4 -60
- package/dist/mcp.js +3 -272
- package/dist/server/publication-limits.d.ts +10 -0
- package/dist/server/publication-limits.js +38 -0
- package/dist/server/publication.d.ts +9 -1
- package/dist/server/publication.js +23 -5
- package/dist/server/service.d.ts +4 -1
- package/dist/server/service.js +6 -1
- package/dist/server/uploads.d.ts +4 -1
- package/dist/server/uploads.js +63 -35
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -4,6 +4,59 @@ Generic, host-independent artifact domain state for the Sentropic artifacts
|
|
|
4
4
|
provider (`sentropic.artifacts`, spec-v8 §9), covering L-5a–5e. Version
|
|
5
5
|
`0.1.2` adds UTF-8 text publication, exposes storage adapters and ships SQL migrations.
|
|
6
6
|
Version `0.1.1` fixed activation ticket release and channel binding (FL-ACT).
|
|
7
|
+
Version `0.1.4` hardens host authority, replay isolation, publication and content
|
|
8
|
+
responses, and adds the platform-free `@sentropic/artifacts/mcp-core` entry.
|
|
9
|
+
|
|
10
|
+
## Migrating to 0.1.4
|
|
11
|
+
|
|
12
|
+
HTTP hosts must supply a trusted `HttpRequest.storageScopeId` for `/ensure`.
|
|
13
|
+
MCP hosts configure `defaultStorageScopeId` or supply the trusted invocation's
|
|
14
|
+
`ArtifactsInvokeInput.storageScopeId`; tool arguments cannot choose storage.
|
|
15
|
+
Legacy body/tool `storageScopeId` and client `paramsHash` fields remain accepted
|
|
16
|
+
but are ignored. Missing host scope now returns 400. Ensure hashes cover the
|
|
17
|
+
validated parameters, binding, owner and scope; publication hashes cover release
|
|
18
|
+
metadata, server-derived file digests and validated source provenance (including
|
|
19
|
+
its absence and recipe step order). Source writes occur within the idempotent
|
|
20
|
+
publication operation, so unchanged replays do not write provenance again.
|
|
21
|
+
Direct service callers publishing a `source` pass their host `SourceStore` as
|
|
22
|
+
the second argument to `publish`; the HTTP/MCP app supplies its configured store.
|
|
23
|
+
Keys are isolated by operation and
|
|
24
|
+
principal. A changed payload under the same key returns an explicit conflict.
|
|
25
|
+
Persisted unscoped replay keys are no longer consulted after upgrading.
|
|
26
|
+
|
|
27
|
+
Publication defaults to 128 files, 16 MiB per file and 64 MiB total. Hosts can
|
|
28
|
+
override these positive integer limits through `createPublicationService({
|
|
29
|
+
limits: { maxFiles, maxFileBytes, maxTotalBytes } })`; the HTTP decoder uses the
|
|
30
|
+
same byte budgets before allocating bytes. Paths must be unique canonical
|
|
31
|
+
relative paths, at most 1024 UTF-8 bytes: empty/dot segments, backslashes,
|
|
32
|
+
percent escapes, URL delimiters, colons and control characters are rejected.
|
|
33
|
+
Existing integrations using larger bundles or noncanonical paths must adjust
|
|
34
|
+
their limits or filenames. Invalid publications fail before object-store effects.
|
|
35
|
+
Upload sessions cap client `maxSize` by the host `maxUploadBytes` and publication
|
|
36
|
+
file/total limits. Every append checks remaining cumulative capacity before
|
|
37
|
+
decoding bytes or writing staging. Upload publication metadata is validated
|
|
38
|
+
before the session is consumed; a rejected path or MIME type can be corrected
|
|
39
|
+
and committed using the same staged bytes. Finalization validates, hashes and
|
|
40
|
+
publishes one copied snapshot. Overlapping append/commit operations return 409;
|
|
41
|
+
failed verification or metadata validation leaves the session retryable.
|
|
42
|
+
|
|
43
|
+
Content `HEAD` responses have the corresponding GET headers and no body,
|
|
44
|
+
including errors and activation. Errors and activation responses include the
|
|
45
|
+
same security and no-store headers as content responses, with a restrictive CSP.
|
|
46
|
+
|
|
47
|
+
Import `createArtifactsManifest`, `createArtifactsToolExecutor` or
|
|
48
|
+
`createArtifactsAdapter` from `@sentropic/artifacts/mcp-core` when the platform
|
|
49
|
+
is absent. This entry has no runtime platform import. Existing `/mcp` exports,
|
|
50
|
+
including `createArtifactsRegistryDefinition`, retain their platform integration.
|
|
51
|
+
The existing root barrel re-exports `/mcp` and also requires the platform.
|
|
52
|
+
Platform types remain referenced in TypeScript declarations for adapter typing.
|
|
53
|
+
`make probe-artifacts-packed-consumer ENV=test-artifacts-hardening` installs the
|
|
54
|
+
tarball alone in a separate consumer, proves the platform is absent and runs the
|
|
55
|
+
15-tool manifest and ensure/publish/get/list flow before the platform proof.
|
|
56
|
+
|
|
57
|
+
Release coordination: #707 reserves 0.1.3 for optional platform peer widening;
|
|
58
|
+
0.1.4 retains `>=0.3.0 <0.5.0` and fits cluster-mesh's current
|
|
59
|
+
`>=0.1.0 <0.2.0` and #707's `>=0.1.3 <0.2.0` artifacts ranges.
|
|
7
60
|
|
|
8
61
|
## What's here (L-5a)
|
|
9
62
|
|
|
@@ -137,11 +190,11 @@ a registrable domain. Assets within the same artifact origin remain loadable.
|
|
|
137
190
|
method/route/binding checks plus Origin/CSRF evidence for writes.
|
|
138
191
|
- `src/server/templates.ts` — generic template kit + versioned API guide
|
|
139
192
|
catalog port (hosts inject approved local assets).
|
|
140
|
-
- `src/mcp.ts` — the `sentropic.artifacts` MCP provider: 15 exact
|
|
193
|
+
- `src/mcp-core.ts` — the platform-free `sentropic.artifacts` MCP provider: 15 exact
|
|
141
194
|
`artifacts_*` tools with closed schemas, a host-bindable invocation
|
|
142
|
-
scope, an executor over the same HTTP app
|
|
143
|
-
|
|
144
|
-
|
|
195
|
+
scope, an executor over the same HTTP app and a platform adapter.
|
|
196
|
+
- `src/mcp.ts` — the compatible `@sentropic/mcp-platform` kernel registry
|
|
197
|
+
definition wrapper (the optional peer is required here and by the root barrel).
|
|
145
198
|
|
|
146
199
|
Ticket issuance and redemption both require `subjectId`, `artifactId`,
|
|
147
200
|
`releaseId` and `channel`. The handler checks the release resolved from the
|
package/dist/content/handler.js
CHANGED
|
@@ -13,8 +13,15 @@ import { errorBody } from '../http.js';
|
|
|
13
13
|
import { CONTENT_GRANT_TTL_SECONDS } from './grants.js';
|
|
14
14
|
import { contentHeaders, grantCookie, parseGrantCookie } from './headers.js';
|
|
15
15
|
import { isServiceWorkerPath, parseContentHost, resolveContentPath } from './routing.js';
|
|
16
|
+
function jsonResponse(status, body, extra = {}) {
|
|
17
|
+
return { status, body, headers: {
|
|
18
|
+
...contentHeaders({ mimeType: 'application/json', size: Buffer.byteLength(JSON.stringify(body)) }),
|
|
19
|
+
'content-security-policy': "default-src 'none'; frame-ancestors 'none'; base-uri 'none'",
|
|
20
|
+
...extra,
|
|
21
|
+
} };
|
|
22
|
+
}
|
|
16
23
|
function fail(status) {
|
|
17
|
-
return
|
|
24
|
+
return jsonResponse(status, errorBody(status));
|
|
18
25
|
}
|
|
19
26
|
/** Create the content handler over injected host ports (no server). */
|
|
20
27
|
export function createContentHandler(deps) {
|
|
@@ -52,78 +59,87 @@ export function createContentHandler(deps) {
|
|
|
52
59
|
bytes,
|
|
53
60
|
};
|
|
54
61
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
release = await deps.publication.getRelease(origin.releaseId);
|
|
70
|
-
if (release === undefined)
|
|
71
|
-
return fail(404);
|
|
72
|
-
const owner = JSON.parse(release.manifest).artifactId;
|
|
73
|
-
if (typeof owner !== 'string')
|
|
74
|
-
return fail(404);
|
|
75
|
-
artifactId = owner;
|
|
76
|
-
}
|
|
62
|
+
async function handle(request) {
|
|
63
|
+
if (request.method !== 'GET' && request.method !== 'HEAD')
|
|
64
|
+
return fail(404);
|
|
65
|
+
const origin = parseContentHost(request.host, deps.baseDomain);
|
|
66
|
+
if (origin === undefined)
|
|
67
|
+
return fail(404);
|
|
68
|
+
let artifactId;
|
|
69
|
+
let release;
|
|
70
|
+
if (origin.kind === 'stable') {
|
|
71
|
+
artifactId = origin.artifactId;
|
|
72
|
+
release = await deps.publication.getCurrent(artifactId);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
release = await deps.publication.getRelease(origin.releaseId);
|
|
77
76
|
if (release === undefined)
|
|
78
77
|
return fail(404);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const token = deps.grants.issue({
|
|
93
|
-
subjectId: request.actorId,
|
|
94
|
-
artifactId,
|
|
95
|
-
releaseId: release.releaseId,
|
|
78
|
+
const owner = JSON.parse(release.manifest).artifactId;
|
|
79
|
+
if (typeof owner !== 'string')
|
|
80
|
+
return fail(404);
|
|
81
|
+
artifactId = owner;
|
|
82
|
+
}
|
|
83
|
+
if (release === undefined)
|
|
84
|
+
return fail(404);
|
|
85
|
+
if (request.ticket !== undefined) {
|
|
86
|
+
if (request.actorId === undefined || request.actorId.length === 0)
|
|
87
|
+
return fail(401);
|
|
88
|
+
try {
|
|
89
|
+
await deps.tickets.redeem(request.ticket, {
|
|
90
|
+
subjectId: request.actorId, artifactId, releaseId: release.releaseId, channel: origin.kind,
|
|
96
91
|
});
|
|
97
|
-
return {
|
|
98
|
-
status: 200,
|
|
99
|
-
headers: {
|
|
100
|
-
'content-type': 'application/json',
|
|
101
|
-
'set-cookie': grantCookie(token, CONTENT_GRANT_TTL_SECONDS),
|
|
102
|
-
},
|
|
103
|
-
body: { activated: true, artifactId, releaseId: release.releaseId },
|
|
104
|
-
};
|
|
105
92
|
}
|
|
106
|
-
|
|
107
|
-
if (token === undefined)
|
|
93
|
+
catch {
|
|
108
94
|
return fail(401);
|
|
109
|
-
|
|
95
|
+
}
|
|
96
|
+
if (!(await canReadNow(artifactId, request.actorId)))
|
|
97
|
+
return fail(404);
|
|
98
|
+
const token = deps.grants.issue({
|
|
99
|
+
subjectId: request.actorId,
|
|
100
|
+
artifactId,
|
|
101
|
+
releaseId: release.releaseId,
|
|
102
|
+
});
|
|
103
|
+
return jsonResponse(200, { activated: true, artifactId, releaseId: release.releaseId }, {
|
|
104
|
+
'set-cookie': grantCookie(token, CONTENT_GRANT_TTL_SECONDS),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const token = parseGrantCookie(request.cookie);
|
|
108
|
+
if (token === undefined)
|
|
109
|
+
return fail(401);
|
|
110
|
+
let claims;
|
|
111
|
+
try {
|
|
112
|
+
claims = deps.grants.verify(token);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return fail(401);
|
|
116
|
+
}
|
|
117
|
+
if (claims.artifactId !== artifactId || claims.releaseId !== release.releaseId) {
|
|
118
|
+
return fail(401); // fresh version consent: activate this release
|
|
119
|
+
}
|
|
120
|
+
if (request.actorId !== undefined && request.actorId !== claims.subjectId)
|
|
121
|
+
return fail(401);
|
|
122
|
+
if (!(await canReadNow(artifactId, claims.subjectId)))
|
|
123
|
+
return fail(404);
|
|
124
|
+
const filePath = resolveContentPath(request.path);
|
|
125
|
+
if (filePath === undefined || isServiceWorkerPath(filePath))
|
|
126
|
+
return fail(404);
|
|
127
|
+
return serveFile(artifactId, release.manifest, filePath);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
async handle(request) {
|
|
131
|
+
let response;
|
|
110
132
|
try {
|
|
111
|
-
|
|
133
|
+
response = await handle(request);
|
|
112
134
|
}
|
|
113
135
|
catch {
|
|
114
|
-
|
|
136
|
+
response = fail(500);
|
|
115
137
|
}
|
|
116
|
-
if (
|
|
117
|
-
|
|
138
|
+
if (request.method === 'HEAD') {
|
|
139
|
+
const { bytes: _bytes, body: _body, ...head } = response;
|
|
140
|
+
return head;
|
|
118
141
|
}
|
|
119
|
-
|
|
120
|
-
return fail(401);
|
|
121
|
-
if (!(await canReadNow(artifactId, claims.subjectId)))
|
|
122
|
-
return fail(404);
|
|
123
|
-
const filePath = resolveContentPath(request.path);
|
|
124
|
-
if (filePath === undefined || isServiceWorkerPath(filePath))
|
|
125
|
-
return fail(404);
|
|
126
|
-
return serveFile(artifactId, release.manifest, filePath);
|
|
142
|
+
return response;
|
|
127
143
|
},
|
|
128
144
|
};
|
|
129
145
|
}
|
package/dist/http.d.ts
CHANGED
package/dist/http.js
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* 403 answers remain for actors who can already read the artifact.
|
|
21
21
|
*/
|
|
22
22
|
import { buildEnsureRequest } from './client.js';
|
|
23
|
+
import { DEFAULT_PUBLICATION_LIMITS } from './server/publication.js';
|
|
24
|
+
import { validatePublicationFiles } from './server/publication-limits.js';
|
|
23
25
|
import { createContentRuntime } from './server/runtime.js';
|
|
24
26
|
import { evaluateAccess, grantAccess, revokeAccess } from './server/access.js';
|
|
25
27
|
import { parseSourceRef } from './server/sources.js';
|
|
@@ -71,9 +73,11 @@ function needString(value, name) {
|
|
|
71
73
|
throw new Error(`invalid request: missing ${name}`);
|
|
72
74
|
return value;
|
|
73
75
|
}
|
|
74
|
-
function needBytes(value) {
|
|
76
|
+
function needBytes(value, maxBytes) {
|
|
75
77
|
if (!Array.isArray(value))
|
|
76
78
|
throw new Error('invalid file bytes: must be an array');
|
|
79
|
+
if (value.length > maxBytes)
|
|
80
|
+
throw new Error('invalid publication byte budget');
|
|
77
81
|
const bytes = new Uint8Array(value.length);
|
|
78
82
|
for (let i = 0; i < value.length; i += 1) {
|
|
79
83
|
const byte = value[i];
|
|
@@ -86,16 +90,18 @@ function needBytes(value) {
|
|
|
86
90
|
}
|
|
87
91
|
/** Canonical text MIME types; parameters are omitted (encoding is always UTF-8). */
|
|
88
92
|
export const TEXT_MIME_PATTERN = '^(?:text/[a-z0-9!#$&^_.+-]+|application/(?:javascript|ecmascript|json|xml|[a-z0-9!#$&^_.+-]+\\+(?:json|xml))|image/svg\\+xml)(?![\\s\\S])';
|
|
89
|
-
function needFileBytes(entry, mimeType) {
|
|
93
|
+
function needFileBytes(entry, mimeType, maxBytes) {
|
|
90
94
|
const hasBytes = 'bytes' in entry;
|
|
91
95
|
const hasText = 'text' in entry;
|
|
92
96
|
if (hasBytes === hasText)
|
|
93
97
|
throw new Error('invalid file: supply exactly one of bytes or text');
|
|
94
98
|
if (hasBytes)
|
|
95
|
-
return needBytes(entry['bytes']);
|
|
99
|
+
return needBytes(entry['bytes'], maxBytes);
|
|
96
100
|
if (typeof entry['text'] !== 'string' || !new RegExp(TEXT_MIME_PATTERN).test(mimeType)) {
|
|
97
101
|
throw new Error('invalid file text: expected a string and a canonical text MIME type');
|
|
98
102
|
}
|
|
103
|
+
if (Buffer.byteLength(entry['text'], 'utf8') > maxBytes)
|
|
104
|
+
throw new Error('invalid publication byte budget');
|
|
99
105
|
return new TextEncoder().encode(entry['text']);
|
|
100
106
|
}
|
|
101
107
|
/** Create the HTTP app over injected services (no server, no fetch). */
|
|
@@ -155,9 +161,9 @@ export function createHttpArtifactApp(deps) {
|
|
|
155
161
|
if (request.method === 'POST' && request.path === '/ensure') {
|
|
156
162
|
const ensured = buildEnsureRequest(deps.parameterSchema, {
|
|
157
163
|
bindingName: needString(body['bindingName'], 'bindingName'),
|
|
158
|
-
storageScopeId: needString(
|
|
164
|
+
storageScopeId: needString(request.storageScopeId, 'host storageScopeId'),
|
|
159
165
|
idempotencyKey: needString(body['idempotencyKey'], 'idempotencyKey'),
|
|
160
|
-
paramsHash:
|
|
166
|
+
paramsHash: '',
|
|
161
167
|
parameters: (body['parameters'] ?? {}),
|
|
162
168
|
});
|
|
163
169
|
const result = await deps.artifacts.ensurePrivateView({
|
|
@@ -166,7 +172,7 @@ export function createHttpArtifactApp(deps) {
|
|
|
166
172
|
storageScopeId: ensured.storageScopeId,
|
|
167
173
|
bindingName: ensured.bindingName,
|
|
168
174
|
idempotencyKey: ensured.idempotencyKey,
|
|
169
|
-
|
|
175
|
+
parameters: ensured.parameters,
|
|
170
176
|
});
|
|
171
177
|
if ((await deps.acls.getArtifactAcl(result.artifactId)) === undefined) {
|
|
172
178
|
await deps.acls.putArtifactAcl(result.artifactId, {
|
|
@@ -187,15 +193,20 @@ export function createHttpArtifactApp(deps) {
|
|
|
187
193
|
if (acl.storageScopeId === undefined)
|
|
188
194
|
throw new Error('invalid request: missing storage scope');
|
|
189
195
|
const rawFiles = body['files'];
|
|
190
|
-
|
|
191
|
-
|
|
196
|
+
const limits = deps.publication.limits ?? DEFAULT_PUBLICATION_LIMITS;
|
|
197
|
+
if (!Array.isArray(rawFiles) || rawFiles.length === 0 || rawFiles.length > limits.maxFiles) {
|
|
198
|
+
throw new Error('invalid publication file count budget');
|
|
199
|
+
}
|
|
200
|
+
let remaining = limits.maxTotalBytes;
|
|
192
201
|
const files = rawFiles.map((file) => {
|
|
193
202
|
const entry = needObject(file);
|
|
194
203
|
const mimeType = needString(entry['mimeType'], 'file.mimeType');
|
|
204
|
+
const bytes = needFileBytes(entry, mimeType, Math.min(limits.maxFileBytes, remaining));
|
|
205
|
+
remaining -= bytes.byteLength;
|
|
195
206
|
return {
|
|
196
207
|
path: needString(entry['path'], 'file.path'),
|
|
197
208
|
mimeType,
|
|
198
|
-
bytes
|
|
209
|
+
bytes,
|
|
199
210
|
};
|
|
200
211
|
});
|
|
201
212
|
const source = body['source'];
|
|
@@ -206,11 +217,10 @@ export function createHttpArtifactApp(deps) {
|
|
|
206
217
|
artifactId,
|
|
207
218
|
profile: (body['profile'] ?? {}),
|
|
208
219
|
files,
|
|
220
|
+
source: attached,
|
|
209
221
|
idempotencyKey: typeof body['idempotencyKey'] === 'string' ? body['idempotencyKey'] : undefined,
|
|
210
222
|
paramsHash: typeof body['paramsHash'] === 'string' ? body['paramsHash'] : undefined,
|
|
211
|
-
});
|
|
212
|
-
if (attached !== undefined)
|
|
213
|
-
await deps.sources.putSource(artifactId, attached);
|
|
223
|
+
}, deps.sources);
|
|
214
224
|
await deps.lifecycle.trackRelease(artifactId, result.releaseId);
|
|
215
225
|
return { status: 201, body: result };
|
|
216
226
|
}
|
|
@@ -415,12 +425,13 @@ export function createHttpArtifactApp(deps) {
|
|
|
415
425
|
const acl = await requireVisible(artifactId, request.actorId);
|
|
416
426
|
assertArtifactAccess({ actorUserId: request.actorId, artifactOwnerId: acl.ownerId, action: 'mutate' });
|
|
417
427
|
const maxSize = body['maxSize'];
|
|
418
|
-
if (maxSize !== undefined && (!Number.
|
|
428
|
+
if (maxSize !== undefined && (!Number.isSafeInteger(maxSize) || maxSize < 1)) {
|
|
419
429
|
throw new Error('invalid request: maxSize must be a positive integer');
|
|
420
430
|
}
|
|
431
|
+
const limits = deps.publication.limits ?? DEFAULT_PUBLICATION_LIMITS;
|
|
421
432
|
const begun = await uploads.begin({
|
|
422
433
|
mimeType: needString(body['mimeType'], 'mimeType'),
|
|
423
|
-
maxSize: maxSize,
|
|
434
|
+
maxSize: Math.min(maxSize ?? Infinity, limits.maxFileBytes, limits.maxTotalBytes),
|
|
424
435
|
});
|
|
425
436
|
uploadOwners.set(begun.uploadId, { artifactId, ownerId: request.actorId });
|
|
426
437
|
return { status: 201, body: begun };
|
|
@@ -430,10 +441,12 @@ export function createHttpArtifactApp(deps) {
|
|
|
430
441
|
const uploadId = needString(body['uploadId'], 'uploadId');
|
|
431
442
|
requireUploadOwner(uploadId, request.actorId);
|
|
432
443
|
const offset = body['offset'];
|
|
433
|
-
if (!Number.
|
|
444
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
434
445
|
throw new Error('invalid request: offset must be a non-negative integer');
|
|
435
446
|
}
|
|
436
|
-
const
|
|
447
|
+
const limits = deps.publication.limits ?? DEFAULT_PUBLICATION_LIMITS;
|
|
448
|
+
const remaining = Math.min(uploads.remainingBytes(uploadId, offset), limits.maxFileBytes - offset, limits.maxTotalBytes - offset);
|
|
449
|
+
const appended = await uploads.append(uploadId, offset, needBytes(body['bytes'], remaining));
|
|
437
450
|
return { status: 200, body: appended };
|
|
438
451
|
}
|
|
439
452
|
if (request.method === 'POST' && request.path === '/uploads/commit') {
|
|
@@ -452,21 +465,24 @@ export function createHttpArtifactApp(deps) {
|
|
|
452
465
|
if (!Number.isInteger(size) || size < 0) {
|
|
453
466
|
throw new Error('invalid request: size must be a non-negative integer');
|
|
454
467
|
}
|
|
468
|
+
const files = [{
|
|
469
|
+
path: needString(body['path'], 'path'),
|
|
470
|
+
mimeType: needString(body['mimeType'], 'mimeType'),
|
|
471
|
+
bytes: new Uint8Array(0),
|
|
472
|
+
}];
|
|
455
473
|
await uploads.commit(uploadId, {
|
|
456
474
|
digest: needString(body['digest'], 'digest'),
|
|
457
475
|
size: size,
|
|
476
|
+
}, (bytes) => {
|
|
477
|
+
files[0].bytes = bytes;
|
|
478
|
+
validatePublicationFiles(files, deps.publication.limits ?? DEFAULT_PUBLICATION_LIMITS);
|
|
458
479
|
});
|
|
459
|
-
const bytes = await staging.read(uploadId);
|
|
460
480
|
const result = await deps.publication.publish({
|
|
461
481
|
ownerId: acl.ownerId,
|
|
462
482
|
storageScopeId: acl.storageScopeId,
|
|
463
483
|
artifactId,
|
|
464
484
|
profile: (body['profile'] ?? {}),
|
|
465
|
-
files
|
|
466
|
-
path: needString(body['path'], 'path'),
|
|
467
|
-
mimeType: needString(body['mimeType'], 'mimeType'),
|
|
468
|
-
bytes,
|
|
469
|
-
}],
|
|
485
|
+
files,
|
|
470
486
|
idempotencyKey: typeof body['idempotencyKey'] === 'string' ? body['idempotencyKey'] : undefined,
|
|
471
487
|
paramsHash: typeof body['paramsHash'] === 'string' ? body['paramsHash'] : undefined,
|
|
472
488
|
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L-5e — reusable artifacts MCP provider (spec-v8 §9 5e, review B3).
|
|
3
|
+
*
|
|
4
|
+
* Part 1: identity, the 15 exact `artifacts_*` tool specs with closed input
|
|
5
|
+
* schemas, and the platform manifest. The logical provider ID is
|
|
6
|
+
* `sentropic.artifacts`; the generic provider hardcodes no application
|
|
7
|
+
* OAuth vocabulary — a host binds its invocation scope through the
|
|
8
|
+
* manifest factory before digests freeze (§3.5). Part 2 adds the tool
|
|
9
|
+
* executor over the same HTTP app direct calls use; part 3 adds the
|
|
10
|
+
* platform adapter and the kernel registry definition (§3).
|
|
11
|
+
*/
|
|
12
|
+
import type { AppConnectorProviderAdapter, AppMcpProviderManifest, CapabilityTool } from '@sentropic/mcp-platform';
|
|
13
|
+
import type { ProviderParameterSchema } from '@sentropic/mcp-platform/registry';
|
|
14
|
+
import type { HttpRequest, HttpResponse } from './http.js';
|
|
15
|
+
export declare const ARTIFACTS_PROVIDER_ID = "sentropic.artifacts";
|
|
16
|
+
export declare const ARTIFACTS_APP_ID = "sentropic-artifacts";
|
|
17
|
+
export declare const ARTIFACTS_PROVIDER_VERSION = "0.1.4";
|
|
18
|
+
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"];
|
|
19
|
+
export type ArtifactsToolName = (typeof ARTIFACTS_TOOL_NAMES)[number];
|
|
20
|
+
type Schema = Record<string, unknown>;
|
|
21
|
+
interface ToolSpec {
|
|
22
|
+
name: ArtifactsToolName;
|
|
23
|
+
description: string;
|
|
24
|
+
inputSchema: Schema;
|
|
25
|
+
mutability: CapabilityTool['mutability'];
|
|
26
|
+
idempotent?: boolean;
|
|
27
|
+
confirm?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare const ARTIFACTS_TOOL_SPECS: readonly ToolSpec[];
|
|
30
|
+
/** Kernel provider parameter schema (typed non-secret host settings). */
|
|
31
|
+
export declare const ARTIFACTS_PARAMETER_SCHEMA: ProviderParameterSchema;
|
|
32
|
+
export interface ArtifactsManifestOptions {
|
|
33
|
+
/** Host authorization profile: scope bound to every tool invocation. */
|
|
34
|
+
invocationScope?: string;
|
|
35
|
+
displayName?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Build the sentropic.artifacts manifest (host scope bound pre-freeze). */
|
|
38
|
+
export declare function createArtifactsManifest(options?: ArtifactsManifestOptions): AppMcpProviderManifest;
|
|
39
|
+
export interface ArtifactsApp {
|
|
40
|
+
handle(request: HttpRequest): Promise<HttpResponse>;
|
|
41
|
+
}
|
|
42
|
+
export interface ArtifactsInvokeInput {
|
|
43
|
+
tool: string;
|
|
44
|
+
actorId: string;
|
|
45
|
+
/** Trusted host scope; tool arguments cannot select storage. */
|
|
46
|
+
storageScopeId?: string;
|
|
47
|
+
args: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
/** Create the 15-tool executor over any conforming artifacts HTTP app. */
|
|
50
|
+
export declare function createArtifactsToolExecutor(deps: {
|
|
51
|
+
app: ArtifactsApp;
|
|
52
|
+
defaultStorageScopeId?: string;
|
|
53
|
+
}): {
|
|
54
|
+
invoke(input: ArtifactsInvokeInput): Promise<HttpResponse>;
|
|
55
|
+
};
|
|
56
|
+
export interface ArtifactsAdapterOptions extends ArtifactsManifestOptions {
|
|
57
|
+
app: ArtifactsApp;
|
|
58
|
+
defaultStorageScopeId?: string;
|
|
59
|
+
}
|
|
60
|
+
/** Create the platform adapter driving the 15 tools over the shared app. */
|
|
61
|
+
export declare function createArtifactsAdapter(options: ArtifactsAdapterOptions): AppConnectorProviderAdapter;
|
|
62
|
+
export {};
|