@sentropic/artifacts 0.2.6 → 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 CHANGED
@@ -1,5 +1,23 @@
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
+
3
21
  ## 0.2.6
4
22
 
5
23
  - Integrate part 1 with the source reads, sharing, terminal deletion, masking and
package/README.md CHANGED
@@ -439,9 +439,13 @@ empty defaults. Existing profile selections remain explicit.
439
439
 
440
440
  `POST /get_source` accepts `artifactId`, optional `releaseId`, and optional unique
441
441
  `paths` (1–32 entries). Bind `publication.getRelease` and `sources.getSource` on
442
- the HTTP app. It returns the canonical manifest, verified author file bytes,
443
- and separate nullable `sourceRef`; readers need the distinct source permission.
444
- The snapshot is limited to 32 files and 262144 decoded bytes. Hosts must qualify
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
445
449
  a 2 MiB serialized MCP result budget before selecting this capability.
446
450
 
447
451
  `POST /set_access` grants or revokes a named principal's read access, requires a
package/dist/http.d.ts CHANGED
@@ -64,7 +64,7 @@ export interface ArtifactApp {
64
64
  handle(request: HttpRequest, invocation: ArtifactInvocationContext): Promise<HttpResponse>;
65
65
  }
66
66
  /** Retained for consumers describing canonical textual publication inputs. */
67
- export declare const TEXT_MIME_PATTERN = "^(?:text/[a-z0-9!#$&^_.+-]+|application/(?:javascript|ecmascript|json|xml|[a-z0-9!#$&^_.+-]+\\+(?:json|xml))|image/svg\\+xml)(?![\\s\\S])";
67
+ export { TEXT_MIME_PATTERN } from './server/mime.js';
68
68
  /** Other mutation surfaces need their own qualification before host activation. */
69
69
  export declare function assertArtifactsTransportProfile(value: unknown): asserts value is CompiledArtifactsProfile;
70
70
  export declare function createHttpArtifactApp(deps: HttpAppDeps): ArtifactApp;
package/dist/http.js CHANGED
@@ -15,6 +15,8 @@ import { artifactSummary } from './server/summary.js';
15
15
  import { artifactVersions } from './server/versions.js';
16
16
  import { manageArtifact } from './server/management.js';
17
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';
18
20
  export { projectArtifactHttpOutcome } from './server/outcome-projection.js';
19
21
  /** Stable content-route error codes; private driver messages never leave the host. */
20
22
  export function errorBody(status) {
@@ -23,8 +25,7 @@ export function errorBody(status) {
23
25
  500: 'internal_error', 503: 'capability_unavailable' }[status] ?? 'internal_error' };
24
26
  }
25
27
  /** Retained for consumers describing canonical textual publication inputs. */
26
- export const TEXT_MIME_PATTERN = '^(?:text/[a-z0-9!#$&^_.+-]+|application/(?:javascript|ecmascript|json|xml|[a-z0-9!#$&^_.+-]+\\+(?:json|xml))|image/svg\\+xml)(?![\\s\\S])';
27
- const TEXT_MIME_RE = new RegExp(TEXT_MIME_PATTERN);
28
+ export { TEXT_MIME_PATTERN } from './server/mime.js';
28
29
  function object(value) {
29
30
  if (typeof value !== 'object' || value === null || Array.isArray(value))
30
31
  throw new ArtifactProfileInputError();
@@ -160,11 +161,13 @@ export function createHttpArtifactApp(deps) {
160
161
  const files = (await readManifestFiles(release.manifest, invocation.storageScopeId)).filter(file => paths.has(file.path))
161
162
  .map(file => ({ path: file.path, mimeType: file.mimeType, size: file.bytes.byteLength,
162
163
  digest: entries.find(entry => entry.path === file.path).digest,
163
- ...(file.text === undefined ? { bytes: Array.from(file.bytes) } : { text: file.text }) }));
164
+ ...(file.text === undefined
165
+ ? { base64: Buffer.from(file.bytes).toString('base64'), encoding: 'base64' }
166
+ : { text: file.text, encoding: 'utf8' }) }));
164
167
  await visible(artifactId, invocation, 'source');
165
168
  const failure = { ...outcome.failure, conflict: { ...conflict, currentSource: {
166
169
  artifactId, releaseId: release.releaseId, digest: conflict.currentDigest, manifest: release.manifest,
167
- files, sourceRef: null, ...deps.publicationUrls?.(artifactId, release.releaseId),
170
+ files, sourceRef: null, guideRef: API_GUIDE_REF, ...deps.publicationUrls?.(artifactId, release.releaseId),
168
171
  } } };
169
172
  Object.defineProperty(failure, 'cause', { value: outcome.failure.cause, enumerable: false });
170
173
  return { ok: false, failure: Object.freeze(failure) };
@@ -10,7 +10,7 @@ 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.6";
13
+ export declare const ARTIFACTS_PROVIDER_VERSION = "0.2.7";
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", "artifacts_list_versions", "artifacts_revert"];
15
15
  export type ArtifactsToolName = (typeof ARTIFACTS_TOOL_NAMES)[number];
16
16
  export declare const ARTIFACTS_TOOL_SPECS: {
package/dist/mcp-core.js CHANGED
@@ -7,10 +7,13 @@ export * from './profile/index.js';
7
7
  export { createRadarKeyDerivation } from './server/command-identity.js';
8
8
  export const ARTIFACTS_PROVIDER_ID = 'sentropic.artifacts';
9
9
  export const ARTIFACTS_APP_ID = 'sentropic-artifacts';
10
- export const ARTIFACTS_PROVIDER_VERSION = '0.2.6';
10
+ export const ARTIFACTS_PROVIDER_VERSION = '0.2.7';
11
11
  export const ARTIFACTS_TOOL_NAMES = ARTIFACT_CAPABILITY_NAMES;
12
12
  export const ARTIFACTS_TOOL_SPECS = ARTIFACTS_TOOL_NAMES.map((name) => ({ name,
13
- description: `Artifacts ${name.slice('artifacts_'.length).replaceAll('_', ' ')}.`,
13
+ description: name === 'artifacts_get_source'
14
+ ? 'Artifacts get source: verified files of the current or a selected release as `text` (encoding "utf8") '
15
+ + 'or `base64` (encoding "base64"), never an integer array, plus a guideRef instead of the guide body.'
16
+ : `Artifacts ${name.slice('artifacts_'.length).replaceAll('_', ' ')}.`,
14
17
  mutability: (['artifacts_list', 'artifacts_get', 'artifacts_get_template', 'artifacts_get_api_guide',
15
18
  'artifacts_get_source', 'artifacts_list_versions'].includes(name) ? 'read-only' : name === 'artifacts_publish' ? 'append' : 'state-transition'),
16
19
  }));
@@ -86,7 +86,10 @@ export function capabilitySchema(name, profile) {
86
86
  case 'list_versions': return profile.resultEnvelopeVersion !== 2 ? idOnly : obj({ artifactId: id,
87
87
  limit: { ...integer(1, 100), default: 20 }, cursor: { ...str, maxLength: 4096 } }, ['artifactId']);
88
88
  case 'get_source': return obj({ artifactId: id, releaseId: id,
89
- paths: { type: 'array', minItems: 1, maxItems: 32, items: { ...str, maxLength: profile.rules.pathMaxBytes } } }, ['artifactId']);
89
+ paths: { type: 'array', minItems: 1, maxItems: 32, items: { ...str, maxLength: profile.rules.pathMaxBytes },
90
+ description: 'Unique manifest paths; omit to read the whole snapshot (max 32 files, 256 KiB decoded). ' +
91
+ 'Result files are `text` with encoding "utf8" or `base64` with encoding "base64" (never an integer ' +
92
+ 'array), plus a guideRef; the guide body is never embedded.' } }, ['artifactId']);
90
93
  case 'delete': return obj({ artifactId: id, expectedCurrentReleaseId: { ...id, type: ['string', 'null'] },
91
94
  idempotencyKey: key, confirmationRef: { ...str, maxLength: 4096 } }, ['artifactId', 'idempotencyKey', ...(profile.requireExpectedCurrentReleaseId ? ['expectedCurrentReleaseId'] : [])]);
92
95
  case 'revert': {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Canonical text-mime pattern shared by publication input rules and source
3
+ * reads. Leaf module: both `http.ts` and `server/source-read.ts` import it
4
+ * without an import cycle.
5
+ */
6
+ export declare const TEXT_MIME_PATTERN = "^(?:text/[a-z0-9!#$&^_.+-]+|application/(?:javascript|ecmascript|json|xml|[a-z0-9!#$&^_.+-]+\\+(?:json|xml))|image/svg\\+xml)(?![\\s\\S])";
7
+ export declare const TEXT_MIME_RE: RegExp;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Canonical text-mime pattern shared by publication input rules and source
3
+ * reads. Leaf module: both `http.ts` and `server/source-read.ts` import it
4
+ * without an import cycle.
5
+ */
6
+ export const TEXT_MIME_PATTERN = '^(?:text/[a-z0-9!#$&^_.+-]+|application/(?:javascript|ecmascript|json|xml|[a-z0-9!#$&^_.+-]+\\+(?:json|xml))|image/svg\\+xml)(?![\\s\\S])';
7
+ export const TEXT_MIME_RE = new RegExp(TEXT_MIME_PATTERN);
@@ -7,11 +7,19 @@ const object = (properties, required = Object.keys(properties)) => ({
7
7
  const error = (code, details) => object({
8
8
  code: { enum: [code] }, message: string, retriable: { type: 'boolean' }, details,
9
9
  });
10
+ /** get_source file: verified raw bytes encoded as UTF-8 text or base64, never an integer array. */
11
+ const sourceFile = { type: 'object', additionalProperties: false,
12
+ properties: { path: string, mimeType: string, size: { type: 'integer', minimum: 0 }, digest: string,
13
+ encoding: { enum: ['utf8', 'base64'] }, text: string, base64: string },
14
+ required: ['path', 'mimeType', 'size', 'digest', 'encoding'],
15
+ oneOf: [{ required: ['text'], not: { required: ['base64'] } },
16
+ { required: ['base64'], not: { required: ['text'] } }] };
17
+ const guideRef = object({ tool: string, version: string }, ['tool', 'version']);
10
18
  const pointer = object({ variant: { enum: ['pointer_conflict'] }, artifactId: string,
11
19
  expectedCurrentReleaseId: nullable, currentReleaseId: nullable, currentDigest: nullable,
12
20
  currentSource: object({ artifactId: string, releaseId: string, digest: nullable,
13
- manifest: string, files: { type: 'array', items: { type: 'object' } }, sourceRef: { type: ['object', 'null'] },
14
- stableUrl: string, pinnedUrl: string }, ['artifactId', 'releaseId', 'digest', 'manifest', 'files', 'sourceRef']),
21
+ manifest: string, files: { type: 'array', items: sourceFile }, sourceRef: { type: ['object', 'null'] },
22
+ guideRef, stableUrl: string, pinnedUrl: string }, ['artifactId', 'releaseId', 'digest', 'manifest', 'files', 'sourceRef', 'guideRef']),
15
23
  }, ['variant', 'artifactId', 'expectedCurrentReleaseId', 'currentReleaseId', 'currentDigest']);
16
24
  const common = { invocationId: string,
17
25
  commitState: { enum: ['not_committed', 'committed', 'unknown'] } };
@@ -11,6 +11,11 @@ export interface SourceReadPorts {
11
11
  stableUrl: string;
12
12
  pinnedUrl: string;
13
13
  };
14
+ /** Guide reference returned instead of embedding the guide body. */
15
+ guideRef?: {
16
+ tool: string;
17
+ version: string;
18
+ };
14
19
  maxFiles: number;
15
20
  maxTotalBytes: number;
16
21
  }
@@ -27,12 +32,27 @@ export declare function readArtifactSource(ports: SourceReadPorts, input: Source
27
32
  releaseId: string;
28
33
  digest: string;
29
34
  manifest: string;
30
- files: {
31
- bytes: number[];
35
+ files: ({
36
+ text: string;
37
+ encoding: "utf8";
38
+ path: string;
39
+ mimeType: string;
40
+ digest: string;
41
+ size: number;
42
+ } | {
43
+ base64: string;
44
+ encoding: "base64";
32
45
  path: string;
33
46
  mimeType: string;
34
47
  digest: string;
35
48
  size: number;
36
- }[];
49
+ })[];
37
50
  sourceRef: SourceRef | null;
51
+ guideRef: {
52
+ readonly tool: "artifacts_get_api_guide";
53
+ readonly version: "v1";
54
+ } | {
55
+ tool: string;
56
+ version: string;
57
+ };
38
58
  }>;
@@ -1,6 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { parseManifest } from '../codecs.js';
3
3
  import { ArtifactOperationError } from './failures.js';
4
+ import { TEXT_MIME_RE } from './mime.js';
5
+ import { API_GUIDE_REF } from './templates.js';
4
6
  /** Read author bytes only. Provenance never substitutes for a stored snapshot. */
5
7
  export async function readArtifactSource(ports, input) {
6
8
  await ports.authorize();
@@ -59,10 +61,23 @@ export async function readArtifactSource(ports, input) {
59
61
  createHash('sha256').update(bytes).digest('hex') !== file.digest.slice(7)) {
60
62
  throw new ArtifactOperationError('verification_failed');
61
63
  }
62
- files.push({ ...file, bytes: Array.from(bytes) });
64
+ // Verified raw bytes are encoded for the wire: text mime types as UTF-8
65
+ // text, everything else (including non-UTF-8 "text") as base64. Never an
66
+ // integer array, which blows the tool-result budget on small pages.
67
+ let text;
68
+ if (TEXT_MIME_RE.test(file.mimeType)) {
69
+ try {
70
+ text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
71
+ }
72
+ catch { /* Not valid UTF-8: fall back to base64. */ }
73
+ }
74
+ files.push({ ...file, ...(text !== undefined
75
+ ? { text, encoding: 'utf8' }
76
+ : { base64: Buffer.from(bytes).toString('base64'), encoding: 'base64' }) });
63
77
  }
64
78
  const sourceRef = await ports.getSource(input.artifactId) ?? null;
65
79
  await ports.authorize();
66
80
  return { artifactId: input.artifactId, releaseId: release.releaseId, digest: manifest.digest,
67
- manifest: release.manifest, files, sourceRef, ...ports.urls?.(input.artifactId, release.releaseId) };
81
+ manifest: release.manifest, files, sourceRef, guideRef: ports.guideRef ?? API_GUIDE_REF,
82
+ ...ports.urls?.(input.artifactId, release.releaseId) };
68
83
  }
@@ -29,5 +29,10 @@ export interface TemplateCatalogPort {
29
29
  }
30
30
  export declare const DEFAULT_TEMPLATE_ID = "static-html";
31
31
  export declare const DEFAULT_API_GUIDE_VERSION = "v1";
32
+ /** Wire reference returned by get_source instead of embedding the guide body. */
33
+ export declare const API_GUIDE_REF: {
34
+ readonly tool: "artifacts_get_api_guide";
35
+ readonly version: "v1";
36
+ };
32
37
  /** Generic in-package catalog; hosts override with approved local assets. */
33
38
  export declare function createDefaultTemplateCatalog(): TemplateCatalogPort;
@@ -9,6 +9,8 @@
9
9
  */
10
10
  export const DEFAULT_TEMPLATE_ID = 'static-html';
11
11
  export const DEFAULT_API_GUIDE_VERSION = 'v1';
12
+ /** Wire reference returned by get_source instead of embedding the guide body. */
13
+ export const API_GUIDE_REF = { tool: 'artifacts_get_api_guide', version: DEFAULT_API_GUIDE_VERSION };
12
14
  const INDEX_HTML = [
13
15
  '<!doctype html>',
14
16
  '<html lang="en">',
@@ -66,7 +68,11 @@ const GUIDE_V1 = [
66
68
  '- `artifacts_list_versions`: full release history of one artifact —',
67
69
  ' per release: id, created at, author, state, whether it is current,',
68
70
  ' and a content digest.',
69
- '- `artifacts_get_source`: current file contents (text decoded) for editing.',
71
+ '- `artifacts_get_source`: verified file contents of the current or a selected',
72
+ ' release, for editing. Text mime types come back as `text` with',
73
+ ' `encoding: "utf8"`; every other file as `base64` with `encoding: "base64"` —',
74
+ ' never an integer array. The result carries a `guideRef` (tool + version),',
75
+ ' never the guide body. Per call: at most 32 files and 256 KiB decoded.',
70
76
  '- `artifacts_revert`: make an earlier release current again as a NEW',
71
77
  ' release that copies its content — the artifact URL stays stable and',
72
78
  ' history only grows. Purged releases are refused with `release_unavailable`.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentropic/artifacts",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
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",