@sentropic/artifacts 0.1.3 → 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 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, a platform adapter, and the
143
- `@sentropic/mcp-platform` kernel registry definition (the platform is
144
- an optional peer, required only by the `./mcp` subpath).
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
@@ -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 { status, headers: { 'content-type': 'application/json' }, body: errorBody(status) };
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
- return {
56
- async handle(request) {
57
- if (request.method !== 'GET' && request.method !== 'HEAD')
58
- return fail(404);
59
- const origin = parseContentHost(request.host, deps.baseDomain);
60
- if (origin === undefined)
61
- return fail(404);
62
- let artifactId;
63
- let release;
64
- if (origin.kind === 'stable') {
65
- artifactId = origin.artifactId;
66
- release = await deps.publication.getCurrent(artifactId);
67
- }
68
- else {
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
- if (request.ticket !== undefined) {
80
- if (request.actorId === undefined || request.actorId.length === 0)
81
- return fail(401);
82
- try {
83
- await deps.tickets.redeem(request.ticket, {
84
- subjectId: request.actorId, artifactId, releaseId: release.releaseId, channel: origin.kind,
85
- });
86
- }
87
- catch {
88
- return fail(401);
89
- }
90
- if (!(await canReadNow(artifactId, request.actorId)))
91
- return fail(404);
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
- const token = parseGrantCookie(request.cookie);
107
- if (token === undefined)
93
+ catch {
108
94
  return fail(401);
109
- let claims;
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
- claims = deps.grants.verify(token);
133
+ response = await handle(request);
112
134
  }
113
135
  catch {
114
- return fail(401);
136
+ response = fail(500);
115
137
  }
116
- if (claims.artifactId !== artifactId || claims.releaseId !== release.releaseId) {
117
- return fail(401); // fresh version consent: activate this release
138
+ if (request.method === 'HEAD') {
139
+ const { bytes: _bytes, body: _body, ...head } = response;
140
+ return head;
118
141
  }
119
- if (request.actorId !== undefined && request.actorId !== claims.subjectId)
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
@@ -12,6 +12,8 @@ export interface HttpRequest {
12
12
  path: string;
13
13
  body: unknown;
14
14
  actorId: string;
15
+ /** Trusted host context, resolved alongside actorId; never copy from the body. */
16
+ storageScopeId?: string;
15
17
  }
16
18
  export interface HttpResponse {
17
19
  status: number;
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(body['storageScopeId'], 'storageScopeId'),
164
+ storageScopeId: needString(request.storageScopeId, 'host storageScopeId'),
159
165
  idempotencyKey: needString(body['idempotencyKey'], 'idempotencyKey'),
160
- paramsHash: typeof body['paramsHash'] === 'string' ? body['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
- paramsHash: ensured.paramsHash,
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
- if (!Array.isArray(rawFiles))
191
- throw new Error('invalid request: missing files');
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: needFileBytes(entry, mimeType),
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.isInteger(maxSize) || maxSize < 1)) {
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.isInteger(offset) || offset < 0) {
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 appended = await uploads.append(uploadId, offset, needBytes(body['bytes']));
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 {};