@noodleseed/one 0.186.1 → 0.188.0

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 (80) hide show
  1. package/dist/commands/solutions-conversations.js +1 -1
  2. package/dist/commands/solutions-conversations.js.map +1 -1
  3. package/dist/commands/solutions-history.d.ts.map +1 -1
  4. package/dist/commands/solutions-history.js +5 -2
  5. package/dist/commands/solutions-history.js.map +1 -1
  6. package/dist/commands/whatsapp-ops.d.ts +1 -1
  7. package/dist/commands/whatsapp-ops.d.ts.map +1 -1
  8. package/dist/commands/whatsapp-ops.js +31 -9
  9. package/dist/commands/whatsapp-ops.js.map +1 -1
  10. package/dist/commands/whatsapp-output.d.ts +1 -1
  11. package/dist/commands/whatsapp-output.d.ts.map +1 -1
  12. package/dist/commands/whatsapp-output.js +8 -6
  13. package/dist/commands/whatsapp-output.js.map +1 -1
  14. package/node_modules/@noodle-borg/agent-kit/dist/curated/error-fixes.js +1 -0
  15. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +3 -3
  16. package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js +1 -0
  17. package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-auth-ref.js +1 -0
  18. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +3 -1
  19. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  20. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-model-context.js +1 -0
  21. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.d.ts +89 -0
  22. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.js +20 -1
  23. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-inbox.js +16 -1
  24. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-store.d.ts +1 -1
  25. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-types.d.ts +34 -3
  26. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-types.js +10 -4
  27. package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +1 -1
  28. package/node_modules/@noodle-borg/authoring/dist/assistant.js +1 -1
  29. package/node_modules/@noodle-borg/authoring/dist/server.d.ts +6 -0
  30. package/node_modules/@noodle-borg/authoring/dist/server.js +1 -0
  31. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-channels.js +26 -0
  32. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-conversations.js +1 -1
  33. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-history.js +1 -0
  34. package/node_modules/@noodle-borg/compiler/dist/artifact/catalog-emission.js +3 -0
  35. package/node_modules/@noodle-borg/compiler/dist/artifact/types.d.ts +5 -0
  36. package/node_modules/@noodle-borg/compiler/dist/artifact/version.d.ts +2 -1
  37. package/node_modules/@noodle-borg/compiler/dist/artifact/version.js +2 -1
  38. package/node_modules/@noodle-borg/compiler/dist/compile.js +2 -1
  39. package/node_modules/@noodle-borg/compiler/dist/errors.d.ts +1 -1
  40. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +6 -0
  41. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +4 -1
  42. package/node_modules/@noodle-borg/compiler/dist/manifest/website-projection.d.ts +3 -0
  43. package/node_modules/@noodle-borg/compiler/dist/manifest/website-projection.js +23 -2
  44. package/node_modules/@noodle-borg/runtime/dist/connector/types.d.ts +1 -1
  45. package/node_modules/@noodle-borg/service/dist/application-history-settings.js +2 -0
  46. package/node_modules/@noodle-borg/service/dist/build-info.js +1 -0
  47. package/node_modules/@noodle-borg/service/dist/business-information/managed-solution-executable.js +2 -0
  48. package/node_modules/@noodle-borg/service/dist/channels/composition.js +3 -2
  49. package/node_modules/@noodle-borg/service/dist/channels/messaging-eligibility.js +6 -1
  50. package/node_modules/@noodle-borg/service/dist/channels/reply-bound.js +4 -0
  51. package/node_modules/@noodle-borg/service/dist/channels/runtime.js +38 -10
  52. package/node_modules/@noodle-borg/service/dist/channels/twilio-sms.js +188 -0
  53. package/node_modules/@noodle-borg/service/dist/channels/twilio-webhook.js +16 -0
  54. package/node_modules/@noodle-borg/service/dist/conversation-history/contracts.js +1 -0
  55. package/node_modules/@noodle-borg/service/dist/routes/business-information-channels.js +3 -1
  56. package/node_modules/@noodle-borg/service/dist/routes/service-probes.js +1 -0
  57. package/node_modules/@noodle-borg/service/dist/routes/sms-twilio.js +66 -0
  58. package/node_modules/@noodle-borg/service/dist/routes/whatsapp.js +31 -21
  59. package/node_modules/@noodle-borg/service/dist/serve-local.js +2 -0
  60. package/node_modules/@noodle-borg/service/dist/service.js +3 -2
  61. package/node_modules/@noodle-borg/service/package.json +1 -1
  62. package/node_modules/@noodle-borg/transport-http/dist/handler.d.ts +5 -0
  63. package/node_modules/@noodle-borg/transport-http/dist/handler.js +2 -0
  64. package/node_modules/@noodle-borg/transport-http/dist/serve-request.js +3 -1
  65. package/node_modules/@noodle-borg/transport-http/dist/tool-authorization.js +25 -3
  66. package/node_modules/@noodle-borg/wire-contracts/dist/application-channels.d.ts +33 -0
  67. package/node_modules/@noodle-borg/wire-contracts/dist/application-channels.js +3 -1
  68. package/node_modules/@noodle-borg/wire-contracts/dist/application-history-settings.d.ts +7 -0
  69. package/node_modules/@noodle-borg/wire-contracts/dist/application-history-settings.js +2 -1
  70. package/node_modules/@noodle-borg/wire-contracts/dist/conversation-history.d.ts +12 -0
  71. package/node_modules/@noodle-borg/wire-contracts/dist/conversation-history.js +1 -1
  72. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +1 -0
  73. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +1 -0
  74. package/node_modules/@noodle-borg/wire-contracts/dist/service-info.d.ts +2 -0
  75. package/node_modules/@noodle-borg/wire-contracts/dist/service-info.js +2 -0
  76. package/node_modules/@noodle-borg/wire-contracts/dist/sms.d.ts +176 -0
  77. package/node_modules/@noodle-borg/wire-contracts/dist/sms.js +41 -0
  78. package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.d.ts +10 -2
  79. package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.js +2 -1
  80. package/package.json +2 -2
@@ -11,7 +11,8 @@ export async function requireChannel(tx, id) {
11
11
  }
12
12
  export async function channelBlocked(tx, id, participant, now) {
13
13
  const block = await channelValue(tx, id, `block:${participant}`);
14
- return !!block && (block.until === null || block.until > now);
14
+ const optout = await channelValue(tx, id, `optout:${participant}`);
15
+ return optout?.blocked === true || (!!block && (block.until === null || block.until > now));
15
16
  }
16
17
  export async function requireChannelLive(tx, id) {
17
18
  const binding = await requireChannel(tx, id);
@@ -38,6 +39,8 @@ async function mutation(tx, id, key, input, now, work) {
38
39
  }
39
40
  /** Validates a channel's provider fields and names the asset its binding will own. */
40
41
  function configuredAsset(input) {
42
+ if (input.channel === 'sms')
43
+ return channelAssetKey('sms', input.phoneNumber);
41
44
  if (input.channel === 'instagram')
42
45
  return channelAssetKey('instagram', input.instagramAccountId);
43
46
  const meta = input.provider === 'meta';
@@ -47,6 +50,8 @@ function configuredAsset(input) {
47
50
  return channelAssetKey('whatsapp', input.phoneNumberId);
48
51
  }
49
52
  function configuredBinding(input, id, state) {
53
+ if (input.channel === 'sms')
54
+ return { ...input, id, provider: 'twilio', ...state };
50
55
  if (input.channel === 'instagram')
51
56
  return { ...input, id, provider: 'meta', ...state };
52
57
  const { provider, channel: _whatsapp, ...configured } = input;
@@ -216,6 +221,20 @@ export class ChannelCoordinator {
216
221
  })).map((row) => row.value);
217
222
  });
218
223
  }
224
+ async optouts(id, after) {
225
+ return this.store.transaction([id], async (tx) => {
226
+ await requireChannel(tx, id);
227
+ const rows = await tx.list(id, {
228
+ kind: 'optout',
229
+ limit: 100,
230
+ ...(after ? { after: `optout:${after}` } : {}),
231
+ });
232
+ return rows.map((row) => ({
233
+ participantId: row.id.slice('optout:'.length),
234
+ ...row.value,
235
+ }));
236
+ });
237
+ }
219
238
  async events(id, after) {
220
239
  return this.store.transaction([id], async (tx) => {
221
240
  await requireChannel(tx, id);
@@ -63,7 +63,22 @@ export async function receiveChannelMessages(store, id, messages, now, retention
63
63
  if ((await channelCounter(tx, id, 'ingress', now, CHANNEL_DAY_MS)) >= 10_000)
64
64
  throw new ChannelError('ingress_capacity');
65
65
  const participantId = channelParticipantId(binding, message.address);
66
- const code = await refusal(tx, binding, message, participantId, pending, now);
66
+ let code = await refusal(tx, binding, message, participantId, pending, now);
67
+ if (binding.channel === 'sms' &&
68
+ message.control &&
69
+ message.eventAt >= now - CHANNEL_RETENTION_MS &&
70
+ message.eventAt <= now + 60_000) {
71
+ if (message.control !== 'help') {
72
+ const key = `optout:${participantId}`;
73
+ const prior = await channelValue(tx, id, key);
74
+ // Older callbacks cannot undo a newer preference; STOP wins equal-second timestamps.
75
+ if (!prior ||
76
+ message.eventAt > prior.at ||
77
+ (message.eventAt === prior.at && message.control === 'stop'))
78
+ await tx.put(id, channelRow(key, 'optout', { at: message.eventAt, blocked: message.control === 'stop' }, now));
79
+ }
80
+ code = `provider_${message.control}`;
81
+ }
67
82
  const sequence = ((await channelValue(tx, id, 'sequence')) ?? 0) + 1;
68
83
  await tx.put(id, channelRow('sequence', 'counter', sequence, now));
69
84
  const eventId = `e_${String(sequence).padStart(20, '0')}`;
@@ -1,7 +1,7 @@
1
1
  /** Private channel journal. Each binding is a transaction/lease authority, not a browser session. */
2
2
  export interface ChannelRow {
3
3
  readonly id: string;
4
- readonly kind: 'binding' | 'asset' | 'participant' | 'event' | 'counter' | 'spend' | 'block' | 'mutation' | 'interaction';
4
+ readonly kind: 'binding' | 'asset' | 'participant' | 'event' | 'counter' | 'spend' | 'block' | 'optout' | 'mutation' | 'interaction';
5
5
  readonly state?: string;
6
6
  readonly updatedAt: number;
7
7
  readonly expiresAt?: number;
@@ -28,7 +28,7 @@ export type ChannelCapability = {
28
28
  readonly name: string;
29
29
  };
30
30
  /** One binding per channel per environment; each channel owns its own provider asset. */
31
- export declare const CHANNEL_KINDS: readonly ["whatsapp", "instagram"];
31
+ export declare const CHANNEL_KINDS: readonly ["whatsapp", "instagram", "sms"];
32
32
  export type ChannelKind = (typeof CHANNEL_KINDS)[number];
33
33
  export interface ChannelBindingBase {
34
34
  readonly id: string;
@@ -68,7 +68,14 @@ export interface InstagramBinding extends ChannelBindingBase {
68
68
  readonly instagramAccountId: string;
69
69
  readonly pageId: string;
70
70
  }
71
- export type ChannelBinding = WhatsAppBinding | InstagramBinding;
71
+ export interface SmsBinding extends ChannelBindingBase {
72
+ readonly channel: 'sms';
73
+ readonly provider: 'twilio';
74
+ readonly accountSid: string;
75
+ readonly phoneNumberSid: string;
76
+ readonly phoneNumber: string;
77
+ }
78
+ export type ChannelBinding = WhatsAppBinding | InstagramBinding | SmsBinding;
72
79
  type ChannelConfigureBase = Pick<ChannelBindingBase, 'tenant' | 'apiKeySecret' | 'deploymentId' | 'capabilities' | 'supportEmail'> & {
73
80
  readonly limits?: ChannelLimitsInput | undefined;
74
81
  };
@@ -79,7 +86,8 @@ export type WhatsAppConfigure = ChannelConfigureBase & Pick<WhatsAppBinding, 'ph
79
86
  readonly provider?: ChannelProvider | undefined;
80
87
  };
81
88
  export type InstagramConfigure = ChannelConfigureBase & Pick<InstagramBinding, 'channel' | 'instagramAccountId' | 'pageId'>;
82
- export type ChannelConfigure = WhatsAppConfigure | InstagramConfigure;
89
+ export type SmsConfigure = ChannelConfigureBase & Pick<SmsBinding, 'channel' | 'accountSid' | 'phoneNumberSid' | 'phoneNumber'>;
90
+ export type ChannelConfigure = WhatsAppConfigure | InstagramConfigure | SmsConfigure;
83
91
  export interface ChannelAddress {
84
92
  readonly kind: 'phone' | 'opaque';
85
93
  readonly value: string;
@@ -130,6 +138,8 @@ export interface ChannelEvent {
130
138
  readonly code?: string;
131
139
  }
132
140
  export interface ChannelInbound {
141
+ /** Authenticated provider opt-out control, never model input. */
142
+ readonly control?: 'stop' | 'start' | 'help';
133
143
  readonly providerId: string;
134
144
  readonly address: ChannelAddress;
135
145
  readonly eventAt: number;
@@ -179,6 +189,27 @@ export declare function publicChannelBinding(binding: ChannelBinding): {
179
189
  actor: string;
180
190
  readyRevision?: number | undefined;
181
191
  readyAt?: number | undefined;
192
+ } | {
193
+ channel: "sms";
194
+ provider: "twilio";
195
+ accountSid: string;
196
+ phoneNumberSid: string;
197
+ phoneNumber: string;
198
+ id: string;
199
+ tenant: TenantRef;
200
+ apiKeySecret: string;
201
+ deploymentId: string;
202
+ capabilities: readonly ChannelCapability[];
203
+ supportEmail: string;
204
+ revision: number;
205
+ generation: number;
206
+ state: "paused" | "enabled" | "disconnected";
207
+ limits: ChannelLimits;
208
+ createdAt: number;
209
+ updatedAt: number;
210
+ actor: string;
211
+ readyRevision?: number | undefined;
212
+ readyAt?: number | undefined;
182
213
  } | {
183
214
  provider: ChannelProvider;
184
215
  phoneNumberId: string;
@@ -15,7 +15,7 @@ export const CHANNEL_DEFAULT_LIMITS = Object.freeze({
15
15
  dailyMicroUsd: 20_000_000,
16
16
  });
17
17
  /** One binding per channel per environment; each channel owns its own provider asset. */
18
- export const CHANNEL_KINDS = ['whatsapp', 'instagram'];
18
+ export const CHANNEL_KINDS = ['whatsapp', 'instagram', 'sms'];
19
19
  export class ChannelError extends Error {
20
20
  code;
21
21
  constructor(code) {
@@ -39,7 +39,11 @@ export function channelAssetKey(channel, assetId) {
39
39
  : `asset:${channel}:${channelDigest(assetId)}`;
40
40
  }
41
41
  export function channelBindingAsset(binding) {
42
- return channelAssetKey(binding.channel, binding.channel === 'whatsapp' ? binding.phoneNumberId : binding.instagramAccountId);
42
+ return channelAssetKey(binding.channel, binding.channel === 'sms'
43
+ ? binding.phoneNumber
44
+ : binding.channel === 'whatsapp'
45
+ ? binding.phoneNumberId
46
+ : binding.instagramAccountId);
43
47
  }
44
48
  export function channelParticipantId(binding, address) {
45
49
  return `p_${createHmac('sha256', Buffer.from(binding.indexKey, 'base64'))
@@ -80,7 +84,7 @@ export async function writeChannelEvent(tx, scope, event, now) {
80
84
  }
81
85
  /** WhatsApp rows are stored without `channel`, byte-identical to rows written before Instagram. */
82
86
  function storedChannelBinding(binding) {
83
- if (binding.channel === 'instagram')
87
+ if (binding.channel !== 'whatsapp')
84
88
  return binding;
85
89
  const { channel: _whatsapp, ...stored } = binding;
86
90
  return stored;
@@ -91,7 +95,9 @@ export function channelBindingRow(binding, now) {
91
95
  /** Rows written before Instagram carry no `channel`; they are WhatsApp. Nothing is written back. */
92
96
  export function readChannelBinding(stored) {
93
97
  const binding = stored;
94
- return binding.channel === 'instagram' ? binding : { ...binding, channel: 'whatsapp' };
98
+ return binding.channel === 'instagram' || binding.channel === 'sms'
99
+ ? binding
100
+ : { ...binding, channel: 'whatsapp' };
95
101
  }
96
102
  export function publicChannelBinding(binding) {
97
103
  const { indexKey: _privateIndexKey, credentialDigest: _credentialDigest, ...visible } = storedChannelBinding(binding);
@@ -224,7 +224,7 @@ export interface PublicWebsiteAccess {
224
224
  }
225
225
  export interface PublicMessagingInput {
226
226
  /** One surface per channel; the operator binds each channel's account separately. */
227
- readonly channel: 'whatsapp' | 'instagram';
227
+ readonly channel: 'whatsapp' | 'instagram' | 'sms';
228
228
  readonly capabilities: readonly CapabilityRef[];
229
229
  readonly instructions?: string;
230
230
  /** @see AuthenticatedWebsiteInput.history */
@@ -9,7 +9,7 @@ export function publicMessaging(input) {
9
9
  throw new Error(`publicMessaging does not accept ${key}`);
10
10
  }
11
11
  }
12
- if (input.channel !== 'whatsapp' && input.channel !== 'instagram')
12
+ if (input.channel !== 'whatsapp' && input.channel !== 'instagram' && input.channel !== 'sms')
13
13
  throw new Error('Unsupported public messaging channel');
14
14
  return {
15
15
  kind: 'messaging',
@@ -137,6 +137,12 @@ export interface ToolOptions {
137
137
  readonly description: string;
138
138
  /** Verified caller claims required to invoke this tool; discovery is restricted by default. */
139
139
  readonly authorization?: ToolAuthorizationOptions;
140
+ /**
141
+ * Let anonymous callers of a public or mixed MCP app run this tool although it can change something.
142
+ * By default they must sign in for any tool not marked `readOnly()`. Use it for low-risk handoffs such
143
+ * as a contact or callback request; never on a tool that reads `user` or declares `authorization`.
144
+ */
145
+ readonly anonymous?: 'allowed';
140
146
  /** Designate this ordinary zero-input MCP tool as the application context provider. */
141
147
  readonly contextProvider?: true;
142
148
  readonly input: JsonSchema | z.ZodType;
@@ -145,6 +145,7 @@ class ServerBuilder {
145
145
  },
146
146
  }
147
147
  : {}),
148
+ ...(tool.options.anonymous === 'allowed' ? { anonymous: 'allowed' } : {}),
148
149
  ...(tool.options.contextProvider ? { contextProvider: true } : {}),
149
150
  inputSchema: toJsonSchema(tool.options.input, 'input'),
150
151
  ...(tool.options.output ? { outputSchema: toJsonSchema(tool.options.output) } : {}),
@@ -86,6 +86,32 @@ export const CHANNELS_COMMAND = {
86
86
  arguments: [],
87
87
  flags: [],
88
88
  subcommands: [
89
+ group('sms', 'Configure a Twilio SMS number and control two-way text messaging.', operations(leaf('configure', 'Save a paused SMS binding using a managed Twilio Auth Token reference.', [
90
+ revision,
91
+ ...mutation,
92
+ flag('account-sid', 'Twilio Account SID (AC...).'),
93
+ flag('phone-number-sid', 'Twilio IncomingPhoneNumber SID (PN...).'),
94
+ flag('phone-number', 'Business sending number in E.164 format.'),
95
+ flag('api-key-secret', 'Managed secret name for the Twilio account Auth Token.'),
96
+ flag('capabilities', 'Comma-separated knowledge:name or tool:name selections.'),
97
+ flag('support-email', 'Contact shown when an answer cannot be completed.'),
98
+ ...limits,
99
+ ]), [
100
+ leaf('block', 'Block locally; Twilio opt-outs remain separately enforced.', [
101
+ participant,
102
+ blockScope,
103
+ flag('phone-stdin', 'Read a phone number securely from standard input.', 'boolean'),
104
+ flag('until', 'Expiry as an ISO timestamp.'),
105
+ flag('indefinite', 'Retain this block until removed.', 'boolean'),
106
+ ...mutation,
107
+ ]),
108
+ leaf('unblock', 'Remove a local block; does not override a recipient opt-out.', [
109
+ participant,
110
+ blockScope,
111
+ flag('phone-stdin', 'Supply the number if retained context expired.', 'boolean'),
112
+ ...mutation,
113
+ ]),
114
+ ])),
89
115
  group('whatsapp', 'Configure a 360dialog or Meta Cloud API number, test readiness, and control public messaging.', operations(leaf('configure', 'Save a paused binding using managed secret references.', [
90
116
  revision,
91
117
  ...mutation,
@@ -31,7 +31,7 @@ const paging = (maximum) => [
31
31
  type: 'string',
32
32
  value: '<channel>',
33
33
  summary: 'Only conversations from this channel.',
34
- constraints: { choices: ['website', 'whatsapp', 'instagram'] },
34
+ constraints: { choices: ['website', 'whatsapp', 'instagram', 'sms'] },
35
35
  },
36
36
  {
37
37
  ...OPTIONAL_FLAG,
@@ -59,6 +59,7 @@ export const SOLUTION_HISTORY = {
59
59
  channel('signed-in-customers', 'Record signed-in customers.'),
60
60
  channel('whatsapp', 'Record WhatsApp conversations.'),
61
61
  channel('instagram', 'Record Instagram DM conversations.'),
62
+ channel('sms', 'Record SMS conversations.'),
62
63
  {
63
64
  ...OPTIONAL_FLAG,
64
65
  required: true,
@@ -51,6 +51,9 @@ export function emitCatalogArtifactSurfaces({ manifest, catalog, structuralAmbie
51
51
  },
52
52
  }
53
53
  : {}),
54
+ ...('anonymous' in s.tool && s.tool.anonymous === 'allowed'
55
+ ? { anonymous: 'allowed' }
56
+ : {}),
54
57
  inputSchema,
55
58
  ...(s.outputSchema ? { outputSchema: withDialect(s.outputSchema) } : {}),
56
59
  ...(s.tool.annotations ? { annotations: s.tool.annotations } : {}),
@@ -136,6 +136,11 @@ export interface ArtifactTool {
136
136
  readonly title?: string;
137
137
  readonly description: string;
138
138
  readonly authorization?: ArtifactToolAuthorization;
139
+ /**
140
+ * Anonymous callers of a public or mixed MCP app may run this tool although it is not read-only.
141
+ * Without it, such a tool asks anonymous MCP callers to sign in first.
142
+ */
143
+ readonly anonymous?: 'allowed';
139
144
  readonly inputSchema: JsonSchema;
140
145
  readonly outputSchema?: JsonSchema;
141
146
  readonly annotations?: Record<string, unknown>;
@@ -65,7 +65,8 @@
65
65
  /** `0.21.0`: protected tools may opt into public descriptor discovery without execution permission. */
66
66
  /** `0.22.0`: governed public-page extraction declarations and ordinary generated tools. */
67
67
  /** `0.23.0`: bounded `collect` interaction metadata keyed by opener tool (ADR 0240); never MCP-visible. */
68
- export declare const ARTIFACT_SCHEMA_VERSION = "0.23.0";
68
+ /** `0.24.0`: tools may carry `anonymous: 'allowed'`, opening a non-read-only tool to anonymous MCP callers. */
69
+ export declare const ARTIFACT_SCHEMA_VERSION = "0.24.0";
69
70
  /** mimeType for an MCP Apps UI resource (SEP-1865). Widgets are served under this profile. */
70
71
  export declare const MCP_APP_MIME_TYPE = "text/html;profile=mcp-app";
71
72
  //# sourceMappingURL=version.d.ts.map
@@ -65,7 +65,8 @@
65
65
  /** `0.21.0`: protected tools may opt into public descriptor discovery without execution permission. */
66
66
  /** `0.22.0`: governed public-page extraction declarations and ordinary generated tools. */
67
67
  /** `0.23.0`: bounded `collect` interaction metadata keyed by opener tool (ADR 0240); never MCP-visible. */
68
- export const ARTIFACT_SCHEMA_VERSION = '0.23.0';
68
+ /** `0.24.0`: tools may carry `anonymous: 'allowed'`, opening a non-read-only tool to anonymous MCP callers. */
69
+ export const ARTIFACT_SCHEMA_VERSION = '0.24.0';
69
70
  /** mimeType for an MCP Apps UI resource (SEP-1865). Widgets are served under this profile. */
70
71
  export const MCP_APP_MIME_TYPE = 'text/html;profile=mcp-app';
71
72
  //# sourceMappingURL=version.js.map
@@ -19,7 +19,7 @@ import { compileToolInteractions } from './manifest/interaction-validation.js';
19
19
  import { parseManifestDocument } from './manifest/parse-document.js';
20
20
  import { manifestSchema } from './manifest/schema.js';
21
21
  import { resolveSchemaUses } from './manifest/schema-refs.js';
22
- import { validateWebsiteProjection } from './manifest/website-projection.js';
22
+ import { validateAnonymousTools, validateWebsiteProjection, } from './manifest/website-projection.js';
23
23
  import { compileState } from './state-handles.js';
24
24
  import { suggestionFields } from './suggest.js';
25
25
  import { expandWebCapabilities } from './web-capabilities.js';
@@ -418,6 +418,7 @@ export function compileManifest(raw, options = {}) {
418
418
  errors,
419
419
  });
420
420
  const toolInteractions = compileToolInteractions(manifest.tools, artifactTools, errors);
421
+ errors.push(...validateAnonymousTools(artifactTools));
421
422
  errors.push(...validateWebsiteProjection(manifest.server.assistant, {
422
423
  tools: artifactTools,
423
424
  resources: artifactResources,
@@ -1,7 +1,7 @@
1
1
  import type { AppPackageArtifactV1 } from '@noodle-borg/app-package';
2
2
  import type { RuntimeArtifact } from './artifact/types.js';
3
3
  import type { PackagedAsset } from './assets.js';
4
- export type CompileErrorCode = 'invalid_context_provider' | 'yaml_parse_error' | 'invalid_shape' | 'invalid_name' | 'duplicate_name' | 'reserved_name' | 'unsupported_manifest_version' | 'reserved_for_future_version' | 'invalid_operation_ref' | 'external_ref' | 'invalid_schema_ref' | 'unknown_schema_ref' | 'schema_ref_conflict' | 'invalid_expression' | 'expr_unknown_root' | 'expr_root_unavailable' | 'expr_operator_not_allowed' | 'expr_if_not_boolean' | 'unknown_step_ref' | 'forward_step_ref' | 'self_step_ref' | 'duplicate_step_id' | 'invalid_fulfilment' | 'invalid_elicitation_schema' | 'invalid_elicitation_flow' | 'invalid_confirmation_flow' | 'arg_type_mismatch' | 'ambient_context_action' | 'duplicate_resource' | 'duplicate_prompt' | 'duplicate_resource_uri' | 'unsupported_uri_template' | 'duplicate_widget' | 'unknown_widget_tool' | 'unknown_widget_action_tool' | 'duplicate_widget_tool' | 'invalid_widget_binding' | 'invalid_widget_state_handle' | 'widget_html_too_large' | 'widget_html_total_too_large' | 'invalid_asset' | 'invalid_capability_requirement' | 'state_secret_field' | 'invalid_knowledge' | 'knowledge_unhashed' | 'invalid_managed_collection' | 'invalid_variable_declaration' | 'invalid_managed_collection_source' | 'unknown_connector_alias' | 'connector_not_in_catalog' | 'unknown_operation' | 'connector_binding_required' | 'ambiguous_nested_connector_binding' | 'invalid_connector_call_graph' | 'unsupported_credential_profile' | 'credential_scope_mismatch' | 'credential_audience_mismatch' | 'customer_endpoint_auth_required' | 'customer_endpoint_mapping_required' | 'customer_endpoint_unknown_mapping' | 'customer_endpoint_bridge_unsupported' | 'assistant_messaging_unsupported' | 'assistant_capability_unknown' | 'assistant_public_user_reference' | 'assistant_public_effect_unconfirmed' | 'interaction_action_invalid' | 'interaction_field_unknown' | 'interaction_field_control_mismatch' | 'interaction_field_missing' | 'interaction_initial_value_unknown' | 'interaction_consent_invalid' | 'interaction_expiry_invalid' | 'channel_dependency_missing' | 'channel_requirement_unsupported' | 'customer_endpoint_action_unsupported' | 'customer_endpoint_surface_unsupported' | 'customer_endpoint_credential_source_unsupported' | 'customer_endpoint_policy_conflict' | 'customer_endpoint_routing_inconsistent' | 'unused_connector_alias' | 'arg_mismatch' | 'agent_guide_invalid' | 'agent_guide_duplicate_workflow' | 'agent_guide_duplicate_example' | 'agent_guide_example_workflow_missing' | 'agent_guide_capability_missing' | 'agent_guide_capability_kind' | 'app_package_sensitive_content';
4
+ export type CompileErrorCode = 'invalid_context_provider' | 'yaml_parse_error' | 'invalid_shape' | 'invalid_name' | 'duplicate_name' | 'reserved_name' | 'unsupported_manifest_version' | 'reserved_for_future_version' | 'invalid_operation_ref' | 'external_ref' | 'invalid_schema_ref' | 'unknown_schema_ref' | 'schema_ref_conflict' | 'invalid_expression' | 'expr_unknown_root' | 'expr_root_unavailable' | 'expr_operator_not_allowed' | 'expr_if_not_boolean' | 'unknown_step_ref' | 'forward_step_ref' | 'self_step_ref' | 'duplicate_step_id' | 'invalid_fulfilment' | 'invalid_elicitation_schema' | 'invalid_elicitation_flow' | 'invalid_confirmation_flow' | 'arg_type_mismatch' | 'ambient_context_action' | 'duplicate_resource' | 'duplicate_prompt' | 'duplicate_resource_uri' | 'unsupported_uri_template' | 'duplicate_widget' | 'unknown_widget_tool' | 'unknown_widget_action_tool' | 'duplicate_widget_tool' | 'invalid_widget_binding' | 'invalid_widget_state_handle' | 'widget_html_too_large' | 'widget_html_total_too_large' | 'invalid_asset' | 'invalid_capability_requirement' | 'state_secret_field' | 'invalid_knowledge' | 'knowledge_unhashed' | 'invalid_managed_collection' | 'invalid_variable_declaration' | 'invalid_managed_collection_source' | 'unknown_connector_alias' | 'connector_not_in_catalog' | 'unknown_operation' | 'connector_binding_required' | 'ambiguous_nested_connector_binding' | 'invalid_connector_call_graph' | 'unsupported_credential_profile' | 'credential_scope_mismatch' | 'credential_audience_mismatch' | 'customer_endpoint_auth_required' | 'customer_endpoint_mapping_required' | 'customer_endpoint_unknown_mapping' | 'customer_endpoint_bridge_unsupported' | 'assistant_messaging_unsupported' | 'assistant_capability_unknown' | 'assistant_public_user_reference' | 'assistant_public_effect_unconfirmed' | 'tool_anonymous_requires_identity' | 'interaction_action_invalid' | 'interaction_field_unknown' | 'interaction_field_control_mismatch' | 'interaction_field_missing' | 'interaction_initial_value_unknown' | 'interaction_consent_invalid' | 'interaction_expiry_invalid' | 'channel_dependency_missing' | 'channel_requirement_unsupported' | 'customer_endpoint_action_unsupported' | 'customer_endpoint_surface_unsupported' | 'customer_endpoint_credential_source_unsupported' | 'customer_endpoint_policy_conflict' | 'customer_endpoint_routing_inconsistent' | 'unused_connector_alias' | 'arg_mismatch' | 'agent_guide_invalid' | 'agent_guide_duplicate_workflow' | 'agent_guide_duplicate_example' | 'agent_guide_example_workflow_missing' | 'agent_guide_capability_missing' | 'agent_guide_capability_kind' | 'app_package_sensitive_content';
5
5
  export interface CompileError {
6
6
  readonly code: CompileErrorCode;
7
7
  /** Dotted path to the offending location (e.g. `tools.1.name`); empty string for the document root. */
@@ -364,6 +364,7 @@ export declare const manifestV1Schema: z.ZodObject<{
364
364
  channel: z.ZodEnum<{
365
365
  whatsapp: "whatsapp";
366
366
  instagram: "instagram";
367
+ sms: "sms";
367
368
  }>;
368
369
  mode: z.ZodLiteral<"public">;
369
370
  capabilities: z.ZodArray<z.ZodObject<{
@@ -874,6 +875,7 @@ export declare const manifestV2Schema: z.ZodObject<{
874
875
  channel: z.ZodEnum<{
875
876
  whatsapp: "whatsapp";
876
877
  instagram: "instagram";
878
+ sms: "sms";
877
879
  }>;
878
880
  mode: z.ZodLiteral<"public">;
879
881
  capabilities: z.ZodArray<z.ZodObject<{
@@ -1278,6 +1280,7 @@ export declare const manifestV2Schema: z.ZodObject<{
1278
1280
  seconds: z.ZodNumber;
1279
1281
  }, z.core.$strict>>;
1280
1282
  }, z.core.$strict>>;
1283
+ anonymous: z.ZodOptional<z.ZodLiteral<"allowed">>;
1281
1284
  }, z.core.$strip>>;
1282
1285
  connectors: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
1283
1286
  id: z.ZodString;
@@ -1678,6 +1681,7 @@ export declare const manifestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
1678
1681
  channel: z.ZodEnum<{
1679
1682
  whatsapp: "whatsapp";
1680
1683
  instagram: "instagram";
1684
+ sms: "sms";
1681
1685
  }>;
1682
1686
  mode: z.ZodLiteral<"public">;
1683
1687
  capabilities: z.ZodArray<z.ZodObject<{
@@ -2187,6 +2191,7 @@ export declare const manifestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
2187
2191
  channel: z.ZodEnum<{
2188
2192
  whatsapp: "whatsapp";
2189
2193
  instagram: "instagram";
2194
+ sms: "sms";
2190
2195
  }>;
2191
2196
  mode: z.ZodLiteral<"public">;
2192
2197
  capabilities: z.ZodArray<z.ZodObject<{
@@ -2591,6 +2596,7 @@ export declare const manifestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
2591
2596
  seconds: z.ZodNumber;
2592
2597
  }, z.core.$strict>>;
2593
2598
  }, z.core.$strict>>;
2599
+ anonymous: z.ZodOptional<z.ZodLiteral<"allowed">>;
2594
2600
  }, z.core.$strip>>;
2595
2601
  connectors: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
2596
2602
  id: z.ZodString;
@@ -122,6 +122,9 @@ const toolV2Schema = toolSchema.extend({
122
122
  // Additive optional field: a v1.x minor under ADR 0150. Bounded `collect` interaction metadata
123
123
  // (ADR 0240) an opener tool declares for one confirmed action; references are checked in compile().
124
124
  interaction: toolInteractionSchema.optional(),
125
+ // Additive optional field: anonymous callers of a public or mixed MCP app may run this tool although
126
+ // it is not read-only. Tools that read `${user}` or require claims are rejected in compile().
127
+ anonymous: z.literal('allowed').optional(),
125
128
  });
126
129
  /**
127
130
  * An MCP resource. Like a tool, it carries a `fulfilment` (it can return a constant or call connectors);
@@ -196,7 +199,7 @@ const embeddedAssistantSchema = z
196
199
  z
197
200
  .object({
198
201
  kind: z.literal('messaging'),
199
- channel: z.enum(['whatsapp', 'instagram']),
202
+ channel: z.enum(['whatsapp', 'instagram', 'sms']),
200
203
  mode: z.literal('public'),
201
204
  capabilities: z.array(assistantCapabilitySchema),
202
205
  instructions: z.string().trim().min(1).max(MAX_SERVER_INSTRUCTIONS).optional(),
@@ -19,6 +19,7 @@ import type { CompileError } from '../errors.js';
19
19
  */
20
20
  interface AssistantSurfaceInput {
21
21
  readonly kind?: 'website' | 'messaging' | undefined;
22
+ readonly channel?: string | undefined;
22
23
  readonly mode: 'authenticated' | 'public' | 'mixed';
23
24
  readonly capabilities?: readonly {
24
25
  readonly kind: string;
@@ -45,6 +46,8 @@ export interface WebsiteProjectionSurfaces {
45
46
  }
46
47
  /** A tool touches identity when it reads `${user}` or requires verified claims (ADR 0185). */
47
48
  export declare function anonymousBehavior(tool: ArtifactTool): 'public-safe' | 'requires-identity';
49
+ /** `anonymous: 'allowed'` cannot open a tool that needs to know who the caller is. */
50
+ export declare function validateAnonymousTools(tools: readonly ArtifactTool[]): readonly CompileError[];
48
51
  export declare function validateWebsiteProjection(assistant: AssistantProjectionInput | undefined, surfaces: WebsiteProjectionSurfaces): readonly CompileError[];
49
52
  /** Resolved refs carry the connector id; a shape-only ref is looked up through its declared alias. */
50
53
  export declare function isNativeRecordsOperation(operation: OperationRef, connectors: WebsiteProjectionSurfaces['connectors']): boolean;
@@ -6,6 +6,18 @@ export function anonymousBehavior(tool) {
6
6
  return 'requires-identity';
7
7
  return readsUser(tool.fulfilment) ? 'requires-identity' : 'public-safe';
8
8
  }
9
+ /** `anonymous: 'allowed'` cannot open a tool that needs to know who the caller is. */
10
+ export function validateAnonymousTools(tools) {
11
+ return tools.flatMap((tool, index) => tool.anonymous === 'allowed' && anonymousBehavior(tool) === 'requires-identity'
12
+ ? [
13
+ {
14
+ code: 'tool_anonymous_requires_identity',
15
+ path: `tools.${index}.anonymous`,
16
+ message: `tool "${tool.name}" reads the signed-in user or requires verified claims, so anonymous callers cannot run it; remove anonymous: 'allowed' or stop reading \${user}`,
17
+ },
18
+ ]
19
+ : []);
20
+ }
9
21
  export function validateWebsiteProjection(assistant, surfaces) {
10
22
  if (assistant?.surfaces === undefined)
11
23
  return [];
@@ -43,7 +55,7 @@ export function validateWebsiteProjection(assistant, surfaces) {
43
55
  const path = `tools.${tool.name}`;
44
56
  if (surface.kind === 'messaging') {
45
57
  const selected = new Set(surface.capabilities?.flatMap((entry) => (entry.kind === 'tool' ? [entry.name] : [])));
46
- const requirement = messagingRequirement(tool, selected, surfaces);
58
+ const requirement = messagingRequirement(tool, selected, surfaces, surface.channel);
47
59
  if (requirement !== undefined)
48
60
  errors.push({ ...requirement, path });
49
61
  }
@@ -75,9 +87,18 @@ export function validateWebsiteProjection(assistant, surfaces) {
75
87
  * opener may carry a browser widget (the website keeps its React view) because the profile renders
76
88
  * the same block as natural collection. Anything else names the requirement it lacks.
77
89
  */
78
- function messagingRequirement(tool, selected, surfaces) {
90
+ function messagingRequirement(tool, selected, surfaces, channel) {
79
91
  const operations = operationRefs(tool.fulfilment);
80
92
  const interaction = surfaces.interactions?.[tool.name];
93
+ if (channel === 'sms' &&
94
+ (interaction !== undefined ||
95
+ operations.length > 0 ||
96
+ tool.annotations?.readOnlyHint !== true ||
97
+ tool._meta?.ui !== undefined))
98
+ return {
99
+ code: 'channel_requirement_unsupported',
100
+ message: `"${tool.name}" requires an interaction not supported by the SMS question-answering profile`,
101
+ };
81
102
  if (interaction !== undefined && !selected.has(interaction.action)) {
82
103
  return {
83
104
  code: 'channel_dependency_missing',
@@ -92,7 +92,7 @@ export interface PublicAdmissionContext {
92
92
  readonly messaging?: string;
93
93
  readonly provenance?: {
94
94
  readonly kind: 'messaging';
95
- readonly channel: 'whatsapp' | 'instagram';
95
+ readonly channel: 'whatsapp' | 'instagram' | 'sms';
96
96
  };
97
97
  }
98
98
  /** A single connector-operation invocation, fully resolved and credentialed by the runtime. */
@@ -5,6 +5,7 @@ const WIRE_SOURCES = {
5
5
  signedInCustomers: 'signed_in_customers',
6
6
  whatsapp: 'whatsapp',
7
7
  instagram: 'instagram',
8
+ sms: 'sms',
8
9
  };
9
10
  /**
10
11
  * The surfaces the served application declared `history: false` on (ADR 0241 decision 11), so staff
@@ -58,6 +59,7 @@ export function historySettingsProjection(setting, allowance, revision, canEdit)
58
59
  signedInCustomers: setting.sources.signed_in_customers,
59
60
  whatsapp: setting.sources.whatsapp,
60
61
  instagram: setting.sources.instagram,
62
+ sms: setting.sources.sms,
61
63
  },
62
64
  },
63
65
  };
@@ -7,6 +7,7 @@ export function serviceInfoPayload(buildInfo, developerMcp, channels = {}) {
7
7
  features: {
8
8
  mixedCustomerAuth: MIXED_CUSTOMER_AUTH_FEATURE_VERSION,
9
9
  ...(channels.whatsapp ? { whatsapp: 1 } : {}),
10
+ ...(channels.sms ? { sms: 1 } : {}),
10
11
  ...(channels.instagram ? { instagram: 1 } : {}),
11
12
  },
12
13
  ...(developerMcp
@@ -66,6 +66,8 @@ export function managedSolutionManifest(definition, hostedPageOrigin) {
66
66
  description: blueprint.description,
67
67
  inputSchema,
68
68
  outputSchema: RECORD_OPERATION_SIGNATURES.submit_record?.output,
69
+ // Lead capture stays open to anonymous MCP callers; native-record admission bounds it.
70
+ anonymous: 'allowed',
69
71
  annotations: {
70
72
  confirm: true,
71
73
  readOnlyHint: false,
@@ -15,10 +15,11 @@ export async function createPostgresWhatsApp(pool, secretBox, options) {
15
15
  };
16
16
  }
17
17
  export function createWhatsAppRuntime(options, deps, history) {
18
- if (!options)
18
+ if (!options.whatsapp)
19
19
  return undefined;
20
20
  return new WhatsAppRuntime({
21
- ...options,
21
+ ...options.whatsapp,
22
+ publicBaseUrl: options.publicBaseUrl,
22
23
  ...(history.installations ? { installations: history.installations } : {}),
23
24
  ...(history.allowance
24
25
  ? {
@@ -6,7 +6,7 @@ import { isNativeRecordsOperation, } from '@noodle-borg/compiler';
6
6
  * its confirmed native-record action selected beside it; everything else names, as a channel error
7
7
  * code, the requirement the messaging profile lacks.
8
8
  */
9
- export function messagingProjectionIneligibility(artifact) {
9
+ export function messagingProjectionIneligibility(artifact, channel = 'whatsapp') {
10
10
  const interactions = artifact.toolInteractions ?? {};
11
11
  const present = new Set(artifact.tools.map((tool) => tool.name));
12
12
  const actions = new Set(Object.entries(interactions)
@@ -14,6 +14,11 @@ export function messagingProjectionIneligibility(artifact) {
14
14
  .map(([, interaction]) => interaction.action));
15
15
  for (const tool of artifact.tools) {
16
16
  const interaction = interactions[tool.name];
17
+ if (channel === 'sms' &&
18
+ (interaction !== undefined ||
19
+ tool.annotations?.readOnlyHint !== true ||
20
+ tool._meta?.ui !== undefined))
21
+ return 'messaging_action_unsupported';
17
22
  if (interaction !== undefined && !present.has(interaction.action))
18
23
  return 'channel_dependency_missing';
19
24
  const operations = operationRefs(tool.fulfilment);
@@ -3,6 +3,10 @@ const EXPAND = '\n\nAsk me to expand on any part.';
3
3
  const INSTAGRAM_BYTES = 1000;
4
4
  /** One reply within its channel's limit, cut on a character boundary and inviting a follow-up. */
5
5
  export function boundReply(channel, answer) {
6
+ if (channel === 'sms')
7
+ return answer.length <= 600
8
+ ? answer
9
+ : `${answer.slice(0, 530).replace(/[\uD800-\uDBFF]$/u, '')}${EXPAND}\nReply STOP to unsubscribe.`;
6
10
  if (channel === 'whatsapp')
7
11
  return answer.length <= 3500
8
12
  ? answer