@agent-native/core 0.206.0 → 0.206.1-nightly-20261009213000

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.
Files changed (93) hide show
  1. package/dist/action.d.ts +2 -0
  2. package/dist/agent/default-agent-engine.js +25 -0
  3. package/dist/agent/durable-background.d.ts +2 -0
  4. package/dist/agent/durable-background.js +2 -0
  5. package/dist/agent/engine/error-detail.d.ts +1 -0
  6. package/dist/agent/engine/error-detail.js +6 -1
  7. package/dist/agent/engine/tool-call-journal-seed.d.ts +1 -1
  8. package/dist/agent/engine/tool-call-journal-seed.js +47 -11
  9. package/dist/agent/engine/translate-anthropic.d.ts +9 -0
  10. package/dist/agent/engine/translate-anthropic.js +18 -3
  11. package/dist/agent/production-agent.d.ts +6 -4
  12. package/dist/agent/production-agent.js +126 -85
  13. package/dist/agent/run-store.js +15 -3
  14. package/dist/agent/thread-data-builder.js +1 -2
  15. package/dist/agent/types.d.ts +1 -0
  16. package/dist/automation/index.d.ts +1 -1
  17. package/dist/chat-threads/store.js +6 -2
  18. package/dist/cli/design-connect.d.ts +1 -0
  19. package/dist/cli/design-connect.js +72 -19
  20. package/dist/client/analytics.js +48 -58
  21. package/dist/client/chat/agentkit-agent-native.js +11 -10
  22. package/dist/client/chat/agentkit-protocol.js +5 -1
  23. package/dist/client/chat/runtime.d.ts +4 -1
  24. package/dist/client/chat/runtime.js +135 -43
  25. package/dist/client/session-replay-endpoint.d.ts +1 -0
  26. package/dist/client/session-replay-endpoint.js +35 -0
  27. package/dist/client/session-replay.js +2 -24
  28. package/dist/client/sharing/share-controller-helpers.d.ts +1 -1
  29. package/dist/client/sharing/share-controller-helpers.js +2 -1
  30. package/dist/client/sharing/useShareButtonController.js +2 -9
  31. package/dist/client/use-action.d.ts +1 -0
  32. package/dist/client/use-action.js +2 -1
  33. package/dist/deploy/build.js +1 -1
  34. package/dist/deploy/workspace-deploy.js +65 -6
  35. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  36. package/dist/file-upload/registry.d.ts +7 -1
  37. package/dist/file-upload/registry.js +25 -7
  38. package/dist/mcp/build-server.d.ts +6 -0
  39. package/dist/mcp/build-server.js +97 -28
  40. package/dist/mcp/embed-app.js +9 -8
  41. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  42. package/dist/notifications/routes.d.ts +3 -3
  43. package/dist/observability/metrics.d.ts +5 -0
  44. package/dist/observability/metrics.js +62 -7
  45. package/dist/observability/routes.d.ts +4 -4
  46. package/dist/org/handlers.d.ts +1 -1
  47. package/dist/org/index.d.ts +1 -1
  48. package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
  49. package/dist/provider-api/actions/provider-api.d.ts +5 -5
  50. package/dist/scripts/docs/search.d.ts +1 -0
  51. package/dist/scripts/docs/search.js +17 -4
  52. package/dist/secrets/index.d.ts +1 -1
  53. package/dist/secrets/index.js +1 -1
  54. package/dist/secrets/storage.d.ts +1 -0
  55. package/dist/secrets/storage.js +8 -0
  56. package/dist/server/action-routes.js +62 -29
  57. package/dist/server/agent-chat/mcp-options.d.ts +2 -0
  58. package/dist/server/agent-chat/prompt-resources.js +2 -2
  59. package/dist/server/agent-engine-api-key-route.d.ts +2 -1
  60. package/dist/server/agent-engine-api-key-route.js +66 -2
  61. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  62. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  63. package/dist/server/agent-run-context.js +21 -1
  64. package/dist/server/agent-teams-run-queue.js +2 -1
  65. package/dist/server/agents-bundle.js +1 -1
  66. package/dist/server/auth.js +1 -0
  67. package/dist/server/core-routes-plugin.js +12 -0
  68. package/dist/server/embed-session.js +15 -1
  69. package/dist/server/http-response-telemetry.js +37 -8
  70. package/dist/server/realtime-token.d.ts +1 -1
  71. package/dist/shared/embed-auth.d.ts +4 -0
  72. package/dist/shared/embed-auth.js +156 -23
  73. package/dist/shared/inline-bytes.d.ts +29 -0
  74. package/dist/shared/inline-bytes.js +170 -0
  75. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  76. package/dist/shared/mcp-embed-headers.js +1 -1
  77. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  78. package/dist/sharing/actions/list-resource-shares.js +10 -4
  79. package/dist/sharing/actions/set-resource-visibility.js +3 -1
  80. package/dist/sharing/actions/share-resource.js +4 -2
  81. package/dist/sharing/actions/unshare-resource.js +3 -1
  82. package/dist/sharing/widget-grant.d.ts +19 -0
  83. package/dist/sharing/widget-grant.js +55 -0
  84. package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +20 -21
  85. package/dist/testing.d.ts +1 -0
  86. package/dist/testing.js +1 -0
  87. package/dist/tracking/registry.d.ts +3 -2
  88. package/dist/tracking/registry.js +12 -3
  89. package/dist/vite/client.js +6 -1
  90. package/docs/content/observability.mdx +15 -10
  91. package/docs/content/tracking.mdx +1 -1
  92. package/package.json +4 -4
  93. package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +20 -21
@@ -10,12 +10,145 @@ export const MCP_DIRECTORY_WIDGET_SESSION_EXPIRED_HEADER = "x-agent-native-widge
10
10
  export const MCP_DIRECTORY_WIDGET_READ_CAPABILITY_PREFIX = "capability:mcp-directory-widget-read:";
11
11
  export const MCP_DIRECTORY_WIDGET_WRITE_CAPABILITY_PREFIX = "capability:mcp-directory-widget-write:";
12
12
  export const MCP_DIRECTORY_WIDGET_READ_CAPABILITY_MAX_LENGTH = 2048;
13
+ // The scope is signed into the embed token (about 1.3x its length plus the
14
+ // session claims), and that token is also the embed session cookie, which a
15
+ // browser drops above 4096 bytes. A realistic Slides grant signs to about 3.1 KB;
16
+ // only the widest identities exceed a cookie, and setEmbedSessionCookie then
17
+ // leaves the page on its query/bearer token. embed-session.spec.ts measures both.
13
18
  export const MCP_DIRECTORY_WIDGET_WRITE_CAPABILITY_MAX_LENGTH = 4096;
14
19
  export const MCP_DIRECTORY_WIDGET_WRITE_CAPABILITY_MAX_AGE_MS = 15 * 60 * 1000;
15
20
  const MCP_DIRECTORY_WIDGET_INTEGER_ARGUMENT_MAX = 5_000;
16
21
  const MCP_DIRECTORY_ACTION_NAME = /^[A-Za-z0-9_.-]{1,128}$/;
17
22
  const MCP_DIRECTORY_SCOPE_KEY = /^[A-Za-z0-9_.-]{1,128}$/;
18
23
  const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
24
+ // Capability scopes travel inside a signed cookie that browsers drop above
25
+ // 4096 bytes, so the payload is base64url JSON with two compact argument
26
+ // markers: `0` is `{type:"actionSchema"}` and `["key"]` is the literal
27
+ // `resourceIds[key]`. Markers are non-strings so they can never collide with a
28
+ // string literal. Scopes minted before this form are URI-encoded JSON (always
29
+ // `%7B...`) and still decode.
30
+ const ACTION_ARGUMENT_FIELDS = [
31
+ "actionArguments",
32
+ "readActionArguments",
33
+ "writeActionArguments",
34
+ ];
35
+ function isPlainRecord(value) {
36
+ return !!value && typeof value === "object" && !Array.isArray(value);
37
+ }
38
+ function encodeBase64Url(text) {
39
+ const binary = encodeURIComponent(text).replace(/%([0-9A-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
40
+ return btoa(binary)
41
+ .replace(/\+/g, "-")
42
+ .replace(/\//g, "_")
43
+ .replace(/=+$/, "");
44
+ }
45
+ function decodeBase64Url(payload) {
46
+ if (!/^[A-Za-z0-9_-]*$/.test(payload) || payload.length % 4 === 1) {
47
+ throw new URIError("Malformed base64url payload.");
48
+ }
49
+ const binary = atob(payload.replace(/-/g, "+").replace(/_/g, "/") +
50
+ "=".repeat((4 - (payload.length % 4)) % 4));
51
+ return decodeURIComponent(Array.from(binary, (char) => `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`).join(""));
52
+ }
53
+ function compactArgument(argument, resourceIds) {
54
+ if (typeof argument !== "string") {
55
+ return argument.type === "actionSchema" ? 0 : argument;
56
+ }
57
+ const key = Object.keys(resourceIds).find((resourceKey) => resourceIds[resourceKey] === argument);
58
+ return key !== undefined &&
59
+ JSON.stringify([key]).length < JSON.stringify(argument).length
60
+ ? [key]
61
+ : argument;
62
+ }
63
+ function encodeCapabilityPayload(capability) {
64
+ const wire = { ...capability };
65
+ for (const field of ACTION_ARGUMENT_FIELDS) {
66
+ const actions = capability[field];
67
+ if (!actions)
68
+ continue;
69
+ wire[field] = Object.fromEntries(Object.entries(actions).map(([actionName, args]) => [
70
+ actionName,
71
+ Object.fromEntries(Object.entries(args).map(([name, argument]) => [
72
+ name,
73
+ compactArgument(argument, capability.resourceIds),
74
+ ])),
75
+ ]));
76
+ }
77
+ return encodeBase64Url(JSON.stringify(wire));
78
+ }
79
+ // Returns undefined for a marker that is not `0` or a one-element reference to
80
+ // an existing resource id, so the whole capability is rejected.
81
+ function expandCompactArgument(value, resourceIds) {
82
+ if (value === 0)
83
+ return { type: "actionSchema" };
84
+ if (Array.isArray(value)) {
85
+ const [key] = value;
86
+ return value.length === 1 &&
87
+ typeof key === "string" &&
88
+ isPlainRecord(resourceIds) &&
89
+ Object.hasOwn(resourceIds, key) &&
90
+ typeof resourceIds[key] === "string"
91
+ ? resourceIds[key]
92
+ : undefined;
93
+ }
94
+ return typeof value === "string" || isPlainRecord(value) ? value : undefined;
95
+ }
96
+ function expandCompactCapability(value) {
97
+ if (!isPlainRecord(value))
98
+ return value;
99
+ const expanded = { ...value };
100
+ for (const field of ACTION_ARGUMENT_FIELDS) {
101
+ const actions = value[field];
102
+ if (!isPlainRecord(actions))
103
+ continue;
104
+ const expandedActions = [];
105
+ for (const [actionName, args] of Object.entries(actions)) {
106
+ if (!isPlainRecord(args)) {
107
+ expandedActions.push([actionName, args]);
108
+ continue;
109
+ }
110
+ const expandedArgs = [];
111
+ for (const [name, argument] of Object.entries(args)) {
112
+ const result = expandCompactArgument(argument, value.resourceIds);
113
+ if (result === undefined)
114
+ return undefined;
115
+ expandedArgs.push([name, result]);
116
+ }
117
+ expandedActions.push([actionName, Object.fromEntries(expandedArgs)]);
118
+ }
119
+ expanded[field] = Object.fromEntries(expandedActions);
120
+ }
121
+ return expanded;
122
+ }
123
+ function parseCapabilityPayload(payload) {
124
+ const legacy = payload.startsWith("%");
125
+ let json;
126
+ try {
127
+ json = legacy ? decodeURIComponent(payload) : decodeBase64Url(payload);
128
+ }
129
+ catch (error) {
130
+ if (error instanceof URIError) {
131
+ return { ok: false, reason: "invalid-encoding" };
132
+ }
133
+ throw error;
134
+ }
135
+ let value;
136
+ try {
137
+ value = JSON.parse(json);
138
+ }
139
+ catch (error) {
140
+ if (error instanceof SyntaxError) {
141
+ return { ok: false, reason: "invalid-json" };
142
+ }
143
+ throw error;
144
+ }
145
+ if (legacy)
146
+ return { ok: true, value };
147
+ const expanded = expandCompactCapability(value);
148
+ return expanded === undefined
149
+ ? { ok: false, reason: "invalid-capability" }
150
+ : { ok: true, value: expanded };
151
+ }
19
152
  function isStringRecord(value, { minEntries = 1, maxEntries = 16 } = {}) {
20
153
  if (!value || typeof value !== "object" || Array.isArray(value)) {
21
154
  return false;
@@ -96,26 +229,10 @@ function decodeMcpDirectoryWidgetReadCapability(scope) {
96
229
  scope.length > MCP_DIRECTORY_WIDGET_READ_CAPABILITY_MAX_LENGTH) {
97
230
  return { ok: false, reason: "invalid-scope" };
98
231
  }
99
- let json;
100
- try {
101
- json = decodeURIComponent(scope.slice(MCP_DIRECTORY_WIDGET_READ_CAPABILITY_PREFIX.length));
102
- }
103
- catch (error) {
104
- if (error instanceof URIError) {
105
- return { ok: false, reason: "invalid-encoding" };
106
- }
107
- throw error;
108
- }
109
- let value;
110
- try {
111
- value = JSON.parse(json);
112
- }
113
- catch (error) {
114
- if (error instanceof SyntaxError) {
115
- return { ok: false, reason: "invalid-json" };
116
- }
117
- throw error;
118
- }
232
+ const parsed = parseCapabilityPayload(scope.slice(MCP_DIRECTORY_WIDGET_READ_CAPABILITY_PREFIX.length));
233
+ if (!parsed.ok)
234
+ return parsed;
235
+ const value = parsed.value;
119
236
  return isWidgetReadCapability(value)
120
237
  ? { ok: true, capability: value }
121
238
  : { ok: false, reason: "invalid-capability" };
@@ -126,7 +243,10 @@ function decodeMcpDirectoryWidgetWriteCapability(scope) {
126
243
  return undefined;
127
244
  }
128
245
  try {
129
- const value = JSON.parse(decodeURIComponent(scope.slice(MCP_DIRECTORY_WIDGET_WRITE_CAPABILITY_PREFIX.length)));
246
+ const parsed = parseCapabilityPayload(scope.slice(MCP_DIRECTORY_WIDGET_WRITE_CAPABILITY_PREFIX.length));
247
+ if (!parsed.ok)
248
+ return undefined;
249
+ const value = parsed.value;
130
250
  if (value.version !== 1 ||
131
251
  typeof value.appId !== "string" ||
132
252
  !MCP_DIRECTORY_SCOPE_KEY.test(value.appId) ||
@@ -224,7 +344,7 @@ export function createMcpDirectoryWidgetReadCapability(input) {
224
344
  ])),
225
345
  };
226
346
  const scope = MCP_DIRECTORY_WIDGET_READ_CAPABILITY_PREFIX +
227
- encodeURIComponent(JSON.stringify(capability));
347
+ encodeCapabilityPayload(capability);
228
348
  return scope.length <= MCP_DIRECTORY_WIDGET_READ_CAPABILITY_MAX_LENGTH
229
349
  ? scope
230
350
  : undefined;
@@ -285,7 +405,7 @@ export function createMcpDirectoryWidgetWriteCapability(input) {
285
405
  ])),
286
406
  };
287
407
  const scope = MCP_DIRECTORY_WIDGET_WRITE_CAPABILITY_PREFIX +
288
- encodeURIComponent(JSON.stringify(capability));
408
+ encodeCapabilityPayload(capability);
289
409
  return scope.length <= MCP_DIRECTORY_WIDGET_WRITE_CAPABILITY_MAX_LENGTH
290
410
  ? scope
291
411
  : undefined;
@@ -407,6 +527,14 @@ export function isExpiredMcpDirectoryWidgetWriteCapability(scope, input) {
407
527
  capability.userEmail === input.userEmail.trim().toLowerCase() &&
408
528
  capability.orgId === (input.orgId ?? undefined));
409
529
  }
530
+ export function getMcpDirectoryWidgetReadCapabilityResourceIds(scope, input) {
531
+ if (!matchesMcpDirectoryWidgetReadCapabilityResource(scope, input) ||
532
+ !scope) {
533
+ return undefined;
534
+ }
535
+ const decoded = decodeMcpDirectoryWidgetReadCapability(scope);
536
+ return decoded.ok ? { ...decoded.capability.resourceIds } : undefined;
537
+ }
410
538
  export function getMcpDirectoryWidgetWriteCapabilityGrant(scope, input) {
411
539
  if (!matchesMcpDirectoryWidgetWriteCapability(scope, input) || !scope) {
412
540
  return undefined;
@@ -552,6 +680,11 @@ export function normalizeMcpDirectoryWidgetWriteActionArguments(scope, input) {
552
680
  return undefined;
553
681
  }
554
682
  const suppliedArgs = Object.entries(input.args ?? {});
683
+ // Every literal-bound argument must be supplied and equal: an omitted
684
+ // binding would otherwise rely on the action's own schema to reject it.
685
+ const unboundLiteral = Object.entries(expectedArgs).some(([name, expected]) => typeof expected === "string" && input.args?.[name] !== expected);
686
+ if (unboundLiteral)
687
+ return undefined;
555
688
  const includesResourceBinding = suppliedArgs.some(([name, value]) => typeof expectedArgs[name] === "string" && expectedArgs[name] === value) ||
556
689
  Object.values(expectedArgs).some((expected) => typeof expected !== "string" &&
557
690
  expected.type === "actionSchemaResourceBound" &&
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Inline file bytes reached a SQL write with no durable URL to stand in for
3
+ * them. Raised only by the `reject` policy; a durable dispatch payload must
4
+ * carry a reference the worker can re-read, never a placeholder.
5
+ */
6
+ export declare class DurableAttachmentReferenceRequiredError extends Error {
7
+ readonly code = "attachment_storage_required";
8
+ constructor();
9
+ }
10
+ /**
11
+ * `reject` throws when an attachment has bytes but no durable URL;
12
+ * `placeholder` keeps a visible `{ type: "file", name, mediaType, omitted:
13
+ * "inline-bytes" }` stub, for snapshots saved before the upload returned a URL.
14
+ */
15
+ export type InlineBytesPolicy = "reject" | "placeholder";
16
+ /**
17
+ * The one boundary every chat SQL write passes through (thread snapshots, run
18
+ * events, durable dispatch payloads): inline image/file bytes become their
19
+ * durable URL when the part carries one, and otherwise follow `policy`. Data
20
+ * URLs embedded in any other string become a visible `[inline … omitted]`.
21
+ */
22
+ export declare function stripInlineBytes<T>(value: T, policy: InlineBytesPolicy): T;
23
+ /** `stripInlineBytes` for serialized JSON; skips the parse when no body can be present. */
24
+ export declare function stripInlineBytesFromJson(json: string, policy: InlineBytesPolicy): string;
25
+ /**
26
+ * Throws with the JSON path of the first inline file body in `value` (an
27
+ * object, or a JSON string as read back from a SQL column). For tests.
28
+ */
29
+ export declare function assertNoInlineImageBytes(value: unknown, label?: string): void;
@@ -0,0 +1,170 @@
1
+ import { parseDataUrl } from "./data-url.js";
2
+ /**
3
+ * Inline file bytes reached a SQL write with no durable URL to stand in for
4
+ * them. Raised only by the `reject` policy; a durable dispatch payload must
5
+ * carry a reference the worker can re-read, never a placeholder.
6
+ */
7
+ export class DurableAttachmentReferenceRequiredError extends Error {
8
+ code = "attachment_storage_required";
9
+ constructor() {
10
+ super("An attachment has inline bytes but no durable file URL. Configure file storage and retry this background run.");
11
+ this.name = "DurableAttachmentReferenceRequiredError";
12
+ }
13
+ }
14
+ const ATTACHMENT_TYPES = new Set(["image", "file", "document"]);
15
+ const ATTACHMENT_LIST_KEYS = new Set([
16
+ "attachments",
17
+ "requestAttachments",
18
+ "images",
19
+ "_agentImages",
20
+ ]);
21
+ const INLINE_URL_KEYS = ["image", "url", "referenceUrl", "dataUrl"];
22
+ const DATA_URL_PREFIX = /^\s*data:/i;
23
+ // URL schemes are case-insensitive: `DATA:image/png;base64,...` is still bytes.
24
+ const DATA_SCHEME = /data:/i;
25
+ const HTTP_URL = /^https?:\/\//i;
26
+ const EMBEDDED_DATA_URL = /\bdata:[\w.+-]+\/[\w.+-]+(?:;[^,;\s"'<>]*)*,[^\s"'<>)\]]*/gi;
27
+ function isDataUrl(value) {
28
+ return typeof value === "string" && DATA_URL_PREFIX.test(value);
29
+ }
30
+ function durableUrl(item) {
31
+ const metadata = item.metadata;
32
+ for (const candidate of [
33
+ item.url,
34
+ item.referenceUrl,
35
+ item.uploadUrl,
36
+ item.image,
37
+ metadata?.uploadUrl,
38
+ ]) {
39
+ if (typeof candidate === "string" &&
40
+ candidate.trim() &&
41
+ !isDataUrl(candidate)) {
42
+ return candidate;
43
+ }
44
+ }
45
+ return undefined;
46
+ }
47
+ function inlineKeys(item) {
48
+ const keys = INLINE_URL_KEYS.filter((key) => isDataUrl(item[key]));
49
+ if (typeof item.data === "string" &&
50
+ item.data.length > 0 &&
51
+ !HTTP_URL.test(item.data)) {
52
+ keys.push("data");
53
+ }
54
+ return keys;
55
+ }
56
+ function inlineMediaType(item, keys) {
57
+ for (const key of ["mediaType", "mimeType", "contentType"]) {
58
+ if (typeof item[key] === "string")
59
+ return item[key];
60
+ }
61
+ for (const key of keys) {
62
+ const parsed = typeof item[key] === "string" ? parseDataUrl(item[key].trim()) : null;
63
+ if (parsed)
64
+ return parsed.mediaType;
65
+ }
66
+ return undefined;
67
+ }
68
+ function scrubString(value) {
69
+ return DATA_SCHEME.test(value)
70
+ ? value.replace(EMBEDDED_DATA_URL, (match) => {
71
+ const mediaType = /^data:([^;,]+)/i.exec(match)?.[1] ?? "unknown";
72
+ return `[inline ${mediaType.toLowerCase()} data omitted]`;
73
+ })
74
+ : value;
75
+ }
76
+ function sanitize(value, policy, inAttachmentList, inheritedUrl) {
77
+ if (typeof value === "string")
78
+ return scrubString(value);
79
+ if (Array.isArray(value)) {
80
+ return value.map((item) => sanitize(item, policy, inAttachmentList, inheritedUrl));
81
+ }
82
+ if (!value || typeof value !== "object")
83
+ return value;
84
+ const item = value;
85
+ const isAttachment = inAttachmentList || ATTACHMENT_TYPES.has(item.type);
86
+ const ownUrl = isAttachment ? durableUrl(item) : undefined;
87
+ const fallbackUrl = ownUrl ?? (isAttachment ? inheritedUrl : undefined);
88
+ const inline = isAttachment ? inlineKeys(item) : [];
89
+ const unreferenced = inline.length > 0 &&
90
+ !fallbackUrl &&
91
+ !(typeof item.fileId === "string" && item.fileId);
92
+ if (unreferenced && policy === "reject") {
93
+ throw new DurableAttachmentReferenceRequiredError();
94
+ }
95
+ const out = {};
96
+ for (const [key, child] of Object.entries(item)) {
97
+ if (inline.includes(key)) {
98
+ if (fallbackUrl && (key === "image" || key === "url")) {
99
+ out[key] = fallbackUrl;
100
+ }
101
+ continue;
102
+ }
103
+ out[key] = sanitize(child, policy, ATTACHMENT_LIST_KEYS.has(key), fallbackUrl);
104
+ }
105
+ if (unreferenced) {
106
+ const name = item.name ?? item.filename ?? item.label;
107
+ const mediaType = inlineMediaType(item, inline);
108
+ return {
109
+ ...out,
110
+ type: "file",
111
+ ...(typeof name === "string" ? { name } : {}),
112
+ ...(mediaType ? { mediaType } : {}),
113
+ omitted: "inline-bytes",
114
+ };
115
+ }
116
+ return out;
117
+ }
118
+ /**
119
+ * The one boundary every chat SQL write passes through (thread snapshots, run
120
+ * events, durable dispatch payloads): inline image/file bytes become their
121
+ * durable URL when the part carries one, and otherwise follow `policy`. Data
122
+ * URLs embedded in any other string become a visible `[inline … omitted]`.
123
+ */
124
+ export function stripInlineBytes(value, policy) {
125
+ return sanitize(value, policy, false, undefined);
126
+ }
127
+ /** `stripInlineBytes` for serialized JSON; skips the parse when no body can be present. */
128
+ export function stripInlineBytesFromJson(json, policy) {
129
+ if (!DATA_SCHEME.test(json) && !json.includes('"data"'))
130
+ return json;
131
+ return JSON.stringify(stripInlineBytes(JSON.parse(json), policy));
132
+ }
133
+ /**
134
+ * Throws with the JSON path of the first inline file body in `value` (an
135
+ * object, or a JSON string as read back from a SQL column). For tests.
136
+ */
137
+ export function assertNoInlineImageBytes(value, label = "value") {
138
+ const visit = (node, path, inList) => {
139
+ if (typeof node === "string") {
140
+ if (/base64,|data:image/i.test(node)) {
141
+ throw new Error(`${label} stores inline image bytes at ${path}: ${node.slice(0, 80)}`);
142
+ }
143
+ return;
144
+ }
145
+ if (Array.isArray(node)) {
146
+ node.forEach((child, index) => visit(child, `${path}[${index}]`, inList));
147
+ return;
148
+ }
149
+ if (!node || typeof node !== "object")
150
+ return;
151
+ const record = node;
152
+ if ((inList || ATTACHMENT_TYPES.has(record.type)) &&
153
+ inlineKeys(record).includes("data")) {
154
+ throw new Error(`${label} stores inline bytes at ${path}.data`);
155
+ }
156
+ for (const [key, child] of Object.entries(record)) {
157
+ visit(child, `${path}.${key}`, ATTACHMENT_LIST_KEYS.has(key));
158
+ }
159
+ };
160
+ visit(typeof value === "string" ? safeJson(value) : value, label, false);
161
+ }
162
+ function safeJson(value) {
163
+ try {
164
+ return JSON.parse(value);
165
+ }
166
+ catch {
167
+ // coercion-ok: a non-JSON string is checked as the text it is.
168
+ return value;
169
+ }
170
+ }
@@ -1,4 +1,4 @@
1
- export declare const MCP_EMBED_CORS_ALLOW_HEADERS = "Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Agent-Native-Browser-Tab,X-Agent-Native-CSRF,X-Agent-Native-Frontend,X-Agent-Native-Client-Compatibility,X-Agent-Native-Build-Id,X-User-Timezone,X-Agent-Native-Session-Id,X-Agent-Native-Client-Platform,X-Agent-Native-Desktop-Verifier,X-Agent-Native-Embed-Target,X-Agent-Native-Embed-Transplant";
1
+ export declare const MCP_EMBED_CORS_ALLOW_HEADERS = "Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Content-Save-Origin,X-Agent-Native-Browser-Tab,X-Agent-Native-CSRF,X-Agent-Native-Frontend,X-Agent-Native-Client-Compatibility,X-Agent-Native-Build-Id,X-User-Timezone,X-Agent-Native-Session-Id,X-Agent-Native-Client-Platform,X-Agent-Native-Desktop-Verifier,X-Agent-Native-Embed-Target,X-Agent-Native-Embed-Transplant";
2
2
  export declare const EMBED_TRANSPLANT_HEADER = "x-agent-native-embed-transplant";
3
3
  export declare function isLocalMcpEmbedOrigin(origin: string | null | undefined): boolean;
4
4
  export declare function isClaudeMcpContentOrigin(origin: string | null | undefined): boolean;
@@ -1,4 +1,4 @@
1
- export const MCP_EMBED_CORS_ALLOW_HEADERS = "Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Agent-Native-Browser-Tab,X-Agent-Native-CSRF,X-Agent-Native-Frontend,X-Agent-Native-Client-Compatibility,X-Agent-Native-Build-Id,X-User-Timezone,X-Agent-Native-Session-Id,X-Agent-Native-Client-Platform,X-Agent-Native-Desktop-Verifier,X-Agent-Native-Embed-Target,X-Agent-Native-Embed-Transplant";
1
+ export const MCP_EMBED_CORS_ALLOW_HEADERS = "Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Content-Save-Origin,X-Agent-Native-Browser-Tab,X-Agent-Native-CSRF,X-Agent-Native-Frontend,X-Agent-Native-Client-Compatibility,X-Agent-Native-Build-Id,X-User-Timezone,X-Agent-Native-Session-Id,X-Agent-Native-Client-Platform,X-Agent-Native-Desktop-Verifier,X-Agent-Native-Embed-Target,X-Agent-Native-Embed-Transplant";
2
2
  export const EMBED_TRANSPLANT_HEADER = "x-agent-native-embed-transplant";
3
3
  const CLAUDE_MCP_CONTENT_HOST_RE = /^[a-f0-9]{32}\.claudemcpcontent\.com$/i;
4
4
  const CHATGPT_MCP_SANDBOX_HOST_RE = /^(?:[^.]+\.)?web-sandbox\.oaiusercontent\.com$/i;
@@ -2,7 +2,6 @@ declare const _default: import("../../action.js").ActionDefinition<{
2
2
  resourceType: string;
3
3
  resourceId: string;
4
4
  }, {
5
- role?: undefined;
6
5
  ownerEmail: any;
7
6
  visibility: any;
8
7
  shares: any[];
@@ -12,6 +11,7 @@ declare const _default: import("../../action.js").ActionDefinition<{
12
11
  supportsGroupShares?: boolean;
13
12
  };
14
13
  orgId?: undefined;
14
+ role?: undefined;
15
15
  agentReadable?: undefined;
16
16
  } | {
17
17
  ownerEmail: string;
@@ -3,8 +3,9 @@ import { z } from "zod";
3
3
  import { defineAction } from "../../action.js";
4
4
  import { organizations } from "../../org/schema.js";
5
5
  import { listWorkspaceUserGroupsForOrg } from "../../workspace-connections/groups.js";
6
- import { resolveAccess } from "../access.js";
6
+ import { assertAccess, resolveAccess } from "../access.js";
7
7
  import { requireShareableResource } from "../registry.js";
8
+ import { assertWidgetShareReadGrant } from "../widget-grant.js";
8
9
  async function loadOrgDisplayNames(db, shares) {
9
10
  const orgIds = Array.from(new Set(shares
10
11
  .filter((s) => s.principalType === "org" && s.principalId)
@@ -47,13 +48,14 @@ async function loadGroupDisplayNames(orgId, shares) {
47
48
  }
48
49
  }
49
50
  export default defineAction({
50
- description: "List the current visibility and share grants on a shareable resource. Any read access is sufficient.",
51
+ description: "List the current visibility and share grants on a shareable resource. Ordinary reads require resource access; widget reads require admin access.",
51
52
  schema: z.object({
52
53
  resourceType: z.string(),
53
54
  resourceId: z.string(),
54
55
  }),
55
56
  http: { method: "GET" },
56
- run: async (args) => {
57
+ run: async (args, context) => {
58
+ assertWidgetShareReadGrant(context, args);
57
59
  const reg = requireShareableResource(args.resourceType);
58
60
  const policy = {
59
61
  allowPublic: reg.allowPublic !== false,
@@ -62,7 +64,11 @@ export default defineAction({
62
64
  ? { supportsGroupShares: true }
63
65
  : {}),
64
66
  };
65
- const access = await resolveAccess(args.resourceType, args.resourceId, undefined, { skipResourceBody: true });
67
+ const access = context?.mcpDirectoryWidgetReadOnly
68
+ ? await assertAccess(args.resourceType, args.resourceId, "admin", undefined, { skipResourceBody: true })
69
+ : await resolveAccess(args.resourceType, args.resourceId, undefined, {
70
+ skipResourceBody: true,
71
+ });
66
72
  if (!access)
67
73
  return { ownerEmail: null, visibility: null, shares: [], policy };
68
74
  const db = reg.getDb();
@@ -6,6 +6,7 @@ import { invalidateCollabAccessCache } from "../../server/poll.js";
6
6
  import { track } from "../../tracking/registry.js";
7
7
  import { assertAccess, currentAccess, ForbiddenError, resolveRegisteredAccessContext, } from "../access.js";
8
8
  import { requireShareableResource } from "../registry.js";
9
+ import { assertWidgetShareWriteGrant } from "../widget-grant.js";
9
10
  import { resourceSharingChange } from "./change-result.js";
10
11
  import { getExtensionShareChangeTargets, notifyExtensionShareChanged, } from "./extension-change.js";
11
12
  export default defineAction({
@@ -28,7 +29,8 @@ export default defineAction({
28
29
  const access = await assertAccess(args.resourceType, args.resourceId, "admin", undefined, { skipResourceBody: true });
29
30
  return access.resource.visibility !== "public";
30
31
  },
31
- run: async (args) => {
32
+ run: async (args, ctx) => {
33
+ assertWidgetShareWriteGrant(ctx, "set-resource-visibility", args);
32
34
  const reg = requireShareableResource(args.resourceType);
33
35
  if (args.visibility === "public" && reg.allowPublic === false) {
34
36
  throw new ForbiddenError(`${reg.displayName} cannot be made public — share with specific people or your organization instead.`);
@@ -11,6 +11,7 @@ import { getUserProfile } from "../../user-profile/store.js";
11
11
  import { assertAccess } from "../access.js";
12
12
  import { announceResourceAccessChange, grantResourceAccess, isEmailPrincipalId, isOrgMemberOrInvited, normalizePrincipalId, principalIdMatches, } from "../grant.js";
13
13
  import { requireShareableResource } from "../registry.js";
14
+ import { assertWidgetShareWriteGrant, widgetShareMessage, } from "../widget-grant.js";
14
15
  import { resourceSharingChange } from "./change-result.js";
15
16
  function appPath(path) {
16
17
  if (!path.startsWith("/"))
@@ -123,7 +124,8 @@ export default defineAction({
123
124
  .describe("Optional short note included in the notification email to an individual recipient."),
124
125
  }),
125
126
  needsApproval: needsExternalShareApproval,
126
- run: async (args) => {
127
+ run: async (args, ctx) => {
128
+ assertWidgetShareWriteGrant(ctx, "share-resource", args);
127
129
  const reg = requireShareableResource(args.resourceType);
128
130
  const grant = await grantResourceAccess({
129
131
  resourceType: args.resourceType,
@@ -236,7 +238,7 @@ export default defineAction({
236
238
  url: notificationUrl,
237
239
  },
238
240
  role: args.role,
239
- message: args.message,
241
+ message: widgetShareMessage(ctx, args.message),
240
242
  app: { name: brandName, logoUrl: brandLogoUrl },
241
243
  heroHtml,
242
244
  extras,
@@ -4,6 +4,7 @@ import { defineAction } from "../../action.js";
4
4
  import { invalidateCollabAccessCache } from "../../server/poll.js";
5
5
  import { assertAccess } from "../access.js";
6
6
  import { requireShareableResource } from "../registry.js";
7
+ import { assertWidgetShareWriteGrant } from "../widget-grant.js";
7
8
  import { resourceSharingChange } from "./change-result.js";
8
9
  import { getExtensionShareChangeTargets, notifyExtensionShareChanged, } from "./extension-change.js";
9
10
  function normalizePrincipalId(principalType, principalId) {
@@ -25,7 +26,8 @@ export default defineAction({
25
26
  principalType: z.enum(["user", "group", "org"]),
26
27
  principalId: z.string(),
27
28
  }),
28
- run: async (args) => {
29
+ run: async (args, ctx) => {
30
+ assertWidgetShareWriteGrant(ctx, "unshare-resource", args);
29
31
  const reg = requireShareableResource(args.resourceType);
30
32
  const access = await assertAccess(args.resourceType, args.resourceId, "admin");
31
33
  const beforeExtensionTargets = await getExtensionShareChangeTargets(args.resourceType, args.resourceId);
@@ -0,0 +1,19 @@
1
+ import type { ActionRunContext } from "../action.js";
2
+ interface WidgetShareTarget {
3
+ resourceType: string;
4
+ resourceId: string;
5
+ }
6
+ interface WidgetShareGrantee {
7
+ principalType?: string;
8
+ role?: string;
9
+ }
10
+ /**
11
+ * The widget route already pins these arguments to the signed grant; this
12
+ * repeats the pin inside the action so a mistake in the route's wiring cannot
13
+ * widen what a widget may share. Other callers pass through untouched.
14
+ */
15
+ export declare function assertWidgetShareWriteGrant(ctx: ActionRunContext | undefined, actionName: string, args: WidgetShareTarget & WidgetShareGrantee): void;
16
+ /** The note in a share email is the caller's free text, so a widget sends none. */
17
+ export declare function widgetShareMessage(ctx: ActionRunContext | undefined, message: string | undefined): string | undefined;
18
+ export declare function assertWidgetShareReadGrant(ctx: ActionRunContext | undefined, args: WidgetShareTarget): void;
19
+ export {};
@@ -0,0 +1,55 @@
1
+ import { ForbiddenError } from "./access.js";
2
+ function assertGrantMatches(resourceIds, args) {
3
+ // A widget grant names its resource as `resourceType` plus `<type>Id`
4
+ // (`{ resourceType: "deck", deckId }`), the same keys the route binds.
5
+ const idKey = `${args.resourceType}Id`;
6
+ if (!resourceIds ||
7
+ resourceIds.resourceType !== args.resourceType ||
8
+ !Object.hasOwn(resourceIds, idKey) ||
9
+ resourceIds[idKey] !== args.resourceId) {
10
+ throw new ForbiddenError("This widget grant is missing or scoped to a different resource.");
11
+ }
12
+ }
13
+ /**
14
+ * The widget route already pins these arguments to the signed grant; this
15
+ * repeats the pin inside the action so a mistake in the route's wiring cannot
16
+ * widen what a widget may share. Other callers pass through untouched.
17
+ */
18
+ export function assertWidgetShareWriteGrant(ctx, actionName, args) {
19
+ if (ctx?.caller === "mcp-widget") {
20
+ throw new ForbiddenError("A read-only widget session cannot change sharing.");
21
+ }
22
+ if (ctx?.caller !== "mcp-widget-write")
23
+ return;
24
+ const grant = ctx.mcpDirectoryWidgetWrite;
25
+ if (!grant?.actionNames.includes(actionName)) {
26
+ throw new ForbiddenError("This widget grant is missing or does not include this action.");
27
+ }
28
+ assertGrantMatches(grant.resourceIds, args);
29
+ if (actionName === "share-resource")
30
+ assertWidgetMayGrant(args);
31
+ }
32
+ /**
33
+ * A widget ticket is a short-lived credential an iframe holds, not the person
34
+ * clicking: what it can grant outlives it. It hands out access to individuals,
35
+ * and never the admin role that lets the grantee manage sharing in turn.
36
+ */
37
+ function assertWidgetMayGrant(args) {
38
+ if (args.principalType !== undefined && args.principalType !== "user") {
39
+ throw new ForbiddenError("A widget can share only with individual people, not an organization or group.");
40
+ }
41
+ if (args.role === "admin") {
42
+ throw new ForbiddenError("A widget cannot grant the admin role. Change that access in the app.");
43
+ }
44
+ }
45
+ /** The note in a share email is the caller's free text, so a widget sends none. */
46
+ export function widgetShareMessage(ctx, message) {
47
+ return ctx?.caller === "mcp-widget-write" ? undefined : message;
48
+ }
49
+ export function assertWidgetShareReadGrant(ctx, args) {
50
+ if (ctx?.caller !== "mcp-widget" && ctx?.caller !== "mcp-widget-write") {
51
+ return;
52
+ }
53
+ assertGrantMatches(ctx.mcpDirectoryWidgetResourceIds ??
54
+ ctx.mcpDirectoryWidgetWrite?.resourceIds, args);
55
+ }