From 233eb5916e206e6264c1cabcbda06fe6674eeecc Mon Sep 17 00:00:00 2001 From: Daniel Vataj <153308333+sent-dm@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:43:01 -0400 Subject: [PATCH] Add Claude adapter overlay and MCP directory review fixes Anthropic's MCP Directory review asked that Claude not create or delete Sender Profiles and not send in bulk. The server now enforces that for hosted Claude connections; the Claude adapter mirrors it through a source-controlled overlay (adapter-sources/claude/overlay.yaml) that withholds the provisioning tools and adjusts provisioning, compliance, and messaging guidance. The canonical package, repository root, and Codex adapter keep all 27 tools. Generation fails when an overlay edit no longer matches canonical text, and validation checks the Claude skills keep confirmation wording for every retained consequential tool. Also: neutral feedback guidance, US toll-free verification caveat, correct revocation path (Dashboard > API Keys), and version 0.3.0. --- .claude-plugin/marketplace.json | 2 +- README.md | 4 +- adapter-sources/claude/overlay.yaml | 97 +++++++++++++++++++ adapter-sources/shared/README.md | 4 +- .../sent/.claude-plugin/plugin.json | 2 +- claude-plugins/sent/README.md | 10 +- claude-plugins/sent/public-surface.json | 10 -- .../sent/skills/sent-compliance/SKILL.md | 10 +- .../sent/skills/sent-feedback/SKILL.md | 15 +-- .../sent/skills/sent-messaging/SKILL.md | 2 + .../skills/sent-profile-provisioning/SKILL.md | 14 +-- claude-plugins/sent/skills/sent/SKILL.md | 2 +- .../skills/sms-10dlc-registration/SKILL.md | 2 +- docs/PUBLIC_RELEASE.md | 2 + evals/sent-feedback.yaml | 22 ++--- packages/sent/README.md | 4 +- packages/sent/plugin.json | 2 +- packages/sent/skills/sent-compliance/SKILL.md | 2 +- packages/sent/skills/sent-feedback/SKILL.md | 15 +-- .../skills/sent-profile-provisioning/SKILL.md | 2 +- plugin.json | 2 +- plugins/sent/.codex-plugin/plugin.json | 2 +- plugins/sent/README.md | 4 +- plugins/sent/skills/sent-compliance/SKILL.md | 2 +- plugins/sent/skills/sent-feedback/SKILL.md | 15 +-- .../skills/sent-profile-provisioning/SKILL.md | 2 +- scripts/generate_adapters.py | 20 +++- scripts/repository_metadata.py | 53 +++++++++- scripts/test_contracts.py | 32 ++++++ scripts/test_validation_gates.py | 37 +++++++ scripts/validate.py | 92 ++++++++++++++---- skills/sent-compliance/SKILL.md | 2 +- skills/sent-feedback/SKILL.md | 15 +-- skills/sent-profile-provisioning/SKILL.md | 2 +- 34 files changed, 378 insertions(+), 125 deletions(-) create mode 100644 adapter-sources/claude/overlay.yaml diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 3b6e9c2..ad3b79d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,7 +6,7 @@ }, "metadata": { "description": "Official Sent skills and remote MCP integration for messaging engineering, operations, migration, and specialist channel workflows.", - "version": "0.2.1" + "version": "0.3.0" }, "plugins": [ { diff --git a/README.md b/README.md index 8df8b01..03b1865 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ The plugin declares the Streamable HTTP endpoint `https://mcp.sent.dm/mcp` and e | Sender Profiles | `sender_profiles.list`, `sender_profiles.get`, `sender_profiles.create`, `sender_profiles.update`, `sender_profiles.delete` | | SMS compliance | `compliance.requirements`, `compliance.setup_plan` | -The MCP client performs OAuth 2.1 authorization with PKCE and Dynamic Client Registration. The user selects an organization and Sender Profile during authorization; the client stores the resulting grant. An organization grant can use a schema-supported `profileId` to act as an owned profile; profile grants cannot. `sender_profiles.*` uses target `id` and rejects acting `profileId`. Reauthorize for another organization or scope outside the grant and revoke access from **Sent Dashboard → Settings → MCP Connections**. Do not paste API keys or tokens into prompts. +The MCP client performs OAuth 2.1 authorization with PKCE and Dynamic Client Registration. The user selects an organization and Sender Profile during authorization; the client stores the resulting grant. An organization grant can use a schema-supported `profileId` to act as an owned profile; profile grants cannot. `sender_profiles.*` uses target `id` and rejects acting `profileId`. Reauthorize for another organization or scope outside the grant and revoke access by deactivating the connection's API key in **Sent Dashboard → API Keys**. Do not paste API keys or tokens into prompts. ## Mutation and data-safety contract @@ -162,6 +162,8 @@ See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the public-data policy, [`docs/PUBL The public MCP surface includes 27 tools. `messages.send` uses an existing template with all required parameters; it supports `scheduledAt` with an explicit timezone offset, from 1 minute to 30 days ahead. Acceptance is not delivery, and quiet hours can defer release. +Connections authorized through Claude's hosted connector (Claude on the web, Desktop, mobile, and Cowork) get a server-enforced policy: Sender Profile creation and deletion are unavailable and happen in the Sent Dashboard, each `messages.send` call accepts one recipient, and organization-wide send quotas apply. Other clients, including Claude Code, keep the full surface. + Feedback goes to Sent staff only with user authorization and a reviewed, sanitized report. It is limited to 2000 characters and 20 calls per authenticated account scope per UTC day, shared across acting profiles. Its note does not guarantee storage, open a support ticket, or promise a response. Number lookup is paid and limited to 1000 calls per authenticated account scope per UTC day. Profile create and delete require `idempotencyKey`; interrupted outcomes require reconciliation before another operation. MCP update changes only name, short name, or description. Compliance setup plans create a new profile, support SMS only, and hand required document uploads to the dashboard or REST API. diff --git a/adapter-sources/claude/overlay.yaml b/adapter-sources/claude/overlay.yaml new file mode 100644 index 0000000..3516175 --- /dev/null +++ b/adapter-sources/claude/overlay.yaml @@ -0,0 +1,97 @@ +# Claude adapter overlay. generate_adapters.py applies it to claude-plugins/sent only; +# the canonical package, repository root, and Codex adapter keep the full surface. +# +# It mirrors the Sent server's policy for hosted Claude connections. It is guidance, +# not a security control: the server enforces the policy independently. +# +# withheld_tools: removed from the Claude adapter's public-surface.json. +# edits: exact find/replace on generated Claude adapter files (paths relative to +# claude-plugins/sent). Each find must match exactly once, so canonical edits that +# invalidate an overlay fail generation instead of drifting silently. + +withheld_tools: + sender_profiles.create: Create Sender Profiles in the Sent Dashboard. + sender_profiles.delete: Delete Sender Profiles in the Sent Dashboard. + +edits: + skills/sent-profile-provisioning/SKILL.md: + - find: |- + description: Lists, inspects, creates, updates, and deletes Sender Profiles through Sent MCP tools and executes + replace: |- + description: Lists, inspects, and updates existing Sender Profiles through Sent MCP tools, hands Sender Profile creation and deletion to the Sent Dashboard, and executes + - find: |- + Use `sender_profiles.list`, `sender_profiles.get`, `sender_profiles.create`, `sender_profiles.update`, and `sender_profiles.delete` through client-managed OAuth 2.1/PKCE. + replace: |- + Use `sender_profiles.list`, `sender_profiles.get`, and `sender_profiles.update` through client-managed OAuth 2.1/PKCE. + - find: |- + - **Create:** requires `name`, `shortName`, and `idempotencyKey`. `shortName` is 3-11 letters, numbers, or spaces with at least one letter. Optional arguments are `description`, `smsCountry`, `smsNumberType`, `smsSenderValue`, `smsAreaCodes`, and `compliance`. A supplied `smsCountry` requires `smsNumberType`; omission uses the organization's shared routes. For a dedicated SMS market, use `sent-compliance` to inspect requirements and fill its setup plan first. Provisioning can claim a number and incur registration fees. WhatsApp, RCS, billing, document uploads, inheritance flags, and sandbox arguments are not exposed by this MCP create tool. Required document uploads block it before any write; use the dashboard or documented REST flow. No one-time API key is returned. + replace: |- + - **Create:** Sender Profiles are created in the Sent Dashboard, not from Claude, because provisioning can claim a number and incur registration fees. For a dedicated SMS market, use `sent-compliance` to explain the requirements, then hand the user to the Dashboard. If the connection lists `sender_profiles.create`, do not call it. + - find: |- + - **Delete:** fetch and preview the target. Explain that deletion can disconnect WhatsApp, release numbers, and disable live routes. Supply `id` and a nonempty `idempotencyKey`. `deleted: false` means no matching owned profile was deleted and does not prove another tenant's profile exists. + replace: |- + - **Delete:** Sender Profiles are deleted in the Sent Dashboard, not from Claude, because deletion can disconnect WhatsApp, release numbers, and disable live routes. If the connection lists `sender_profiles.delete`, do not call it. + - find: |- + For every create, update, or delete, preview + replace: |- + For every update, preview + - find: |- + Create and delete use durable receipts. Reuse the original key and arguments for the same intended operation; do not generate a new key after a timeout or an in-flight result. Inspect list/get and returned receipt guidance first. A key reused with different arguments is refused. If interrupted provisioning or teardown needs attention, stop and reconcile in the dashboard rather than retry automatically or replace the key. A new key authorizes a separate operation only after the existing outcome has been reconciled. Verify the returned profile and channel readiness; creation alone does not establish permission to send. + replace: |- + If provisioning or teardown started in the Dashboard looks incomplete, inspect it with list/get and reconcile it in the Dashboard. Profile creation alone does not establish permission to send. + - find: |- + The following `/v3/profiles` guidance describes the legacy REST lifecycle + replace: |- + From Claude, use the REST guidance below to write or review the user's application code; do not run profile-creating or profile-deleting requests on the user's behalf. The following `/v3/profiles` guidance describes the legacy REST lifecycle + + skills/sent-compliance/SKILL.md: + - find: |- + 3. Call `compliance.setup_plan` when the user needs the exact setup call. Its `call.tool` is `sender_profiles.create`, with camelCase argument names and null placeholders that must be filled before execution. Do not execute blank values or translate the plan into an invented REST call. + replace: |- + 3. Call `compliance.setup_plan` when the user needs the setup steps for a market. From Claude, Sender Profiles are provisioned in the Sent Dashboard: present the plan's requirements, and when it returns a Dashboard handoff, give the user that link. If the plan contains a `call` that would create a Sender Profile, do not call `sender_profiles.create`; hand the setup to the Dashboard. Do not translate the plan into an invented REST call. + - find: |- + Required uploads cannot be carried through MCP; do not attempt a guaranteed-to-fail create. + replace: |- + Required uploads cannot be carried through MCP. + - find: |- + 5. When setup can proceed, hand the completed plan to `sent-profile-provisioning` for scope validation, required idempotency key, payload preview, and explicit confirmation before creation. + replace: |- + 5. When setup can proceed, direct the user to finish provisioning in the Sent Dashboard with the requirements you gathered. + - find: |- + The plan creates a new profile. + replace: |- + The plan describes a new profile, which the user creates in the Sent Dashboard. + + skills/sent/SKILL.md: + - find: |- + | List, get, create, update, or delete owned Sender Profiles through MCP | `sent-profile-provisioning` | + replace: |- + | List, get, or update owned Sender Profiles through MCP, or hand creation and deletion to the Sent Dashboard | `sent-profile-provisioning` | + + skills/sent-messaging/SKILL.md: + - find: | + Never call `messages.send` without the preview and explicit confirmation immediately before the call. + replace: | + Never call `messages.send` without the preview and explicit confirmation immediately before the call. + + From Claude, send to exactly one recipient per call. Do not loop over a list or split recipients across repeated calls to reach many people; for bulk or campaign sends, direct the user to the Sent Dashboard or the REST API in their own application. If Sent returns a connection-policy or send-quota error, stop and report it with any retry time; do not retry automatically. + + skills/sms-10dlc-registration/SKILL.md: + - find: |- + Its plan creates a new Sender Profile and does not manage campaigns on an existing profile. + replace: |- + From Claude, its plan is completed by creating the Sender Profile in the Sent Dashboard; it does not manage campaigns on an existing profile. + + README.md: + - find: |- + - Sender Profiles: `sender_profiles.list`, `.get`, `.create`, `.update`, and `.delete`; and + replace: |- + - Sender Profiles: `sender_profiles.list`, `.get`, and `.update`, with creation and deletion in the Sent Dashboard; and + - find: |- + The public MCP surface includes 27 tools. + replace: |- + The public MCP surface includes 27 tools; this Claude plugin documents the 25 available to Claude connections. + - find: |- + Profile create and delete require `idempotencyKey`; interrupted outcomes require reconciliation before another operation. MCP update changes only name, short name, or description. Compliance setup plans create a new profile, support SMS only, and hand required document uploads to the dashboard or REST API. + replace: |- + Sender Profiles are created and deleted in the Sent Dashboard. MCP update changes only name, short name, or description. Compliance setup plans describe a new SMS profile and hand provisioning and required document uploads to the Dashboard. diff --git a/adapter-sources/shared/README.md b/adapter-sources/shared/README.md index 42076da..3e15620 100644 --- a/adapter-sources/shared/README.md +++ b/adapter-sources/shared/README.md @@ -37,7 +37,7 @@ npx skills add https://github.com/sentdm/sent-plugin --skill sent ## MCP and authorization -The adapter connects only to `https://mcp.sent.dm/mcp`. The MCP client performs OAuth 2.1 with PKCE and Dynamic Client Registration; no credentials or credential placeholders are included. The grant is tied to the organization and Sender Profile selected during authorization. An organization grant can use a schema-supported `profileId` to act as an owned profile; profile grants cannot. `sender_profiles.*` uses target `id` and rejects acting `profileId`. Reauthorize for another organization or scope outside the grant and revoke access from **Sent Dashboard → Settings → MCP Connections**. +The adapter connects only to `https://mcp.sent.dm/mcp`. The MCP client performs OAuth 2.1 with PKCE and Dynamic Client Registration; no credentials or credential placeholders are included. The grant is tied to the organization and Sender Profile selected during authorization. An organization grant can use a schema-supported `profileId` to act as an owned profile; profile grants cannot. `sender_profiles.*` uses target `id` and rejects acting `profileId`. Reauthorize for another organization or scope outside the grant and revoke access by deactivating the connection's API key in **Sent Dashboard → API Keys**. MCP-backed live operations cover: @@ -62,6 +62,8 @@ This directory is generated. Make source changes in `packages/sent` or `adapter- The public MCP surface includes 27 tools. `messages.send` uses an existing template with all required parameters; it supports `scheduledAt` with an explicit timezone offset, from 1 minute to 30 days ahead. Acceptance is not delivery, and quiet hours can defer release. +Connections authorized through Claude's hosted connector (Claude on the web, Desktop, mobile, and Cowork) get a server-enforced policy: Sender Profile creation and deletion are unavailable and happen in the Sent Dashboard, each `messages.send` call accepts one recipient, and organization-wide send quotas apply. Other clients, including Claude Code, keep the full surface. + Feedback goes to Sent staff only with user authorization and a reviewed, sanitized report. It is limited to 2000 characters and 20 calls per authenticated account scope per UTC day, shared across acting profiles. Its note does not guarantee storage, open a support ticket, or promise a response. Number lookup is paid and limited to 1000 calls per authenticated account scope per UTC day. Profile create and delete require `idempotencyKey`; interrupted outcomes require reconciliation before another operation. MCP update changes only name, short name, or description. Compliance setup plans create a new profile, support SMS only, and hand required document uploads to the dashboard or REST API. diff --git a/claude-plugins/sent/.claude-plugin/plugin.json b/claude-plugins/sent/.claude-plugin/plugin.json index 85a4a89..5cc9508 100644 --- a/claude-plugins/sent/.claude-plugin/plugin.json +++ b/claude-plugins/sent/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "sent", - "version": "0.2.1", + "version": "0.3.0", "description": "Official Sent plugin for SMS, WhatsApp, RCS, scheduled template sends, Sender Profiles, SMS compliance, authorized feedback, contacts, analytics, API integration, webhooks, routing, and migration.", "author": { "name": "Sent", diff --git a/claude-plugins/sent/README.md b/claude-plugins/sent/README.md index 42076da..9d1eda4 100644 --- a/claude-plugins/sent/README.md +++ b/claude-plugins/sent/README.md @@ -37,7 +37,7 @@ npx skills add https://github.com/sentdm/sent-plugin --skill sent ## MCP and authorization -The adapter connects only to `https://mcp.sent.dm/mcp`. The MCP client performs OAuth 2.1 with PKCE and Dynamic Client Registration; no credentials or credential placeholders are included. The grant is tied to the organization and Sender Profile selected during authorization. An organization grant can use a schema-supported `profileId` to act as an owned profile; profile grants cannot. `sender_profiles.*` uses target `id` and rejects acting `profileId`. Reauthorize for another organization or scope outside the grant and revoke access from **Sent Dashboard → Settings → MCP Connections**. +The adapter connects only to `https://mcp.sent.dm/mcp`. The MCP client performs OAuth 2.1 with PKCE and Dynamic Client Registration; no credentials or credential placeholders are included. The grant is tied to the organization and Sender Profile selected during authorization. An organization grant can use a schema-supported `profileId` to act as an owned profile; profile grants cannot. `sender_profiles.*` uses target `id` and rejects acting `profileId`. Reauthorize for another organization or scope outside the grant and revoke access by deactivating the connection's API key in **Sent Dashboard → API Keys**. MCP-backed live operations cover: @@ -47,7 +47,7 @@ MCP-backed live operations cover: - analytics: number lookup, message volume, deliverability, and contact metrics; and - account: account details, balance, and onboarding status; - feedback: user-authorized reports to Sent with `feedback.send`; -- Sender Profiles: `sender_profiles.list`, `.get`, `.create`, `.update`, and `.delete`; and +- Sender Profiles: `sender_profiles.list`, `.get`, and `.update`, with creation and deletion in the Sent Dashboard; and - SMS compliance: `compliance.requirements` and `compliance.setup_plan`. Mutating workflows preview the exact scope and payload, then require explicit confirmation immediately before execution. They do not retry ambiguous sends blindly and do not equate an accepted message with delivery. @@ -60,11 +60,13 @@ This directory is generated. Make source changes in `packages/sent` or `adapter- ## Additional MCP behavior -The public MCP surface includes 27 tools. `messages.send` uses an existing template with all required parameters; it supports `scheduledAt` with an explicit timezone offset, from 1 minute to 30 days ahead. Acceptance is not delivery, and quiet hours can defer release. +The public MCP surface includes 27 tools; this Claude plugin documents the 25 available to Claude connections. `messages.send` uses an existing template with all required parameters; it supports `scheduledAt` with an explicit timezone offset, from 1 minute to 30 days ahead. Acceptance is not delivery, and quiet hours can defer release. + +Connections authorized through Claude's hosted connector (Claude on the web, Desktop, mobile, and Cowork) get a server-enforced policy: Sender Profile creation and deletion are unavailable and happen in the Sent Dashboard, each `messages.send` call accepts one recipient, and organization-wide send quotas apply. Other clients, including Claude Code, keep the full surface. Feedback goes to Sent staff only with user authorization and a reviewed, sanitized report. It is limited to 2000 characters and 20 calls per authenticated account scope per UTC day, shared across acting profiles. Its note does not guarantee storage, open a support ticket, or promise a response. Number lookup is paid and limited to 1000 calls per authenticated account scope per UTC day. -Profile create and delete require `idempotencyKey`; interrupted outcomes require reconciliation before another operation. MCP update changes only name, short name, or description. Compliance setup plans create a new profile, support SMS only, and hand required document uploads to the dashboard or REST API. +Sender Profiles are created and deleted in the Sent Dashboard. MCP update changes only name, short name, or description. Compliance setup plans describe a new SMS profile and hand provisioning and required document uploads to the Dashboard. Dashboard volume counts delivered/read outbound SMS and WhatsApp over supported windows; deliverability is an all-time outbound percentage including SENT, and contacts is a current total. Do not claim unsupported date filters or trends. diff --git a/claude-plugins/sent/public-surface.json b/claude-plugins/sent/public-surface.json index aeb8ed2..c23668f 100644 --- a/claude-plugins/sent/public-surface.json +++ b/claude-plugins/sent/public-surface.json @@ -91,16 +91,6 @@ "mutation": "read_only", "confirmation_required": false }, - "sender_profiles.create": { - "owner": "sent-profile-provisioning", - "mutation": "state_changing", - "confirmation_required": true - }, - "sender_profiles.delete": { - "owner": "sent-profile-provisioning", - "mutation": "destructive", - "confirmation_required": true - }, "sender_profiles.get": { "owner": "sent-profile-provisioning", "mutation": "read_only", diff --git a/claude-plugins/sent/skills/sent-compliance/SKILL.md b/claude-plugins/sent/skills/sent-compliance/SKILL.md index db8f609..f1703b7 100644 --- a/claude-plugins/sent/skills/sent-compliance/SKILL.md +++ b/claude-plugins/sent/skills/sent-compliance/SKILL.md @@ -16,11 +16,11 @@ The optional `profileId` selector is available only for an organization's owned ## Interpret requirements and plan 1. Call `compliance.requirements` for the selected market. Preserve required flags, conditional rules, options, item fields, sender pattern, and attachment field names. -2. Check `hasRegime` before interpreting an empty requirements list. An empty list with a published regime can mean no fields are demanded; `hasRegime: false` means no schema is published and is not proof that no compliance is required. -3. Call `compliance.setup_plan` when the user needs the exact setup call. Its `call.tool` is `sender_profiles.create`, with camelCase argument names and null placeholders that must be filled before execution. Do not execute blank values or translate the plan into an invented REST call. -4. If `canCompleteHere` is false, report `blocker` and `attachments`, then hand off document submission to the Sent dashboard or the documented multipart REST workflow. Required uploads cannot be carried through MCP; do not attempt a guaranteed-to-fail create. Optional attachment fields alone do not necessarily block creation. -5. When setup can proceed, hand the completed plan to `sent-profile-provisioning` for scope validation, required idempotency key, payload preview, and explicit confirmation before creation. +2. Check `hasRegime` before interpreting an empty requirements list. An empty list with a published regime can mean no fields are demanded; `hasRegime: false` means no schema is published and is not proof that no compliance is required. US `TOLL_FREE` numbers require toll-free verification before production traffic, even when no requirements are returned; never tell the user that a toll-free number needs no registration. +3. Call `compliance.setup_plan` when the user needs the setup steps for a market. From Claude, Sender Profiles are provisioned in the Sent Dashboard: present the plan's requirements, and when it returns a Dashboard handoff, give the user that link. If the plan contains a `call` that would create a Sender Profile, do not call `sender_profiles.create`; hand the setup to the Dashboard. Do not translate the plan into an invented REST call. +4. If `canCompleteHere` is false, report `blocker` and `attachments`, then hand off document submission to the Sent dashboard or the documented multipart REST workflow. Required uploads cannot be carried through MCP. Optional attachment fields alone do not necessarily block creation. +5. When setup can proceed, direct the user to finish provisioning in the Sent Dashboard with the requirements you gathered. -The plan creates a new profile. Adding a market to an existing profile requires the dashboard or REST API. It does not establish approval, readiness, consent, or permission to send. +The plan describes a new profile, which the user creates in the Sent Dashboard. Adding a market to an existing profile requires the dashboard or REST API. It does not establish approval, readiness, consent, or permission to send. For US 10DLC campaign policy, samples, and opt-in evidence, use `sms-10dlc-registration` after resolving the live market requirements. diff --git a/claude-plugins/sent/skills/sent-feedback/SKILL.md b/claude-plugins/sent/skills/sent-feedback/SKILL.md index 08d3a58..2cd0f0a 100644 --- a/claude-plugins/sent/skills/sent-feedback/SKILL.md +++ b/claude-plugins/sent/skills/sent-feedback/SKILL.md @@ -1,27 +1,18 @@ --- name: sent-feedback -description: Reports user-authorized feedback about Sent MCP tools to the Sent team. Use when the user asks to report a bug, request a missing feature, pass along confusing results or praise, or agrees to report a tool gap the agent noticed. Also use proactively when a Sent workflow hits meaningful friction the agent or user experienced, such as missing functionality, an error still unresolved after diagnosis, confusing results, repeated workarounds, or user frustration with Sent, to offer an optional report after the original task. Feedback is read by Sent staff; it does not open a support ticket or return an answer. Do not trigger for routine or expected errors, issues unrelated to Sent, a delivery investigation alone, or friction already offered for reporting. +description: Reports user-authorized feedback about Sent MCP tools to the Sent team. Use when the user asks to report a bug, request a missing feature, or pass along a confusing result or praise about Sent. Feedback is read by Sent staff; it does not open a support ticket or return an answer. Do not trigger for routine errors, issues unrelated to Sent, or a delivery investigation alone. --- # Sent Feedback -Use `feedback.send` only when the user asks to pass feedback to Sent or accepts your offer to report friction, and only after they explicitly authorize the exact sanitized report. Never send unsolicited reports, conversation transcripts, or automatic feedback after a tool error. - -## Offer feedback after meaningful friction - -Sent wants to hear about friction even when the user would not think to report it. During any Sent workflow, notice meaningful friction that you or the user hit: missing functionality, an error that persists after diagnosis, confusing or contradictory results, repeated workarounds, or user frustration with Sent. - -1. Diagnose the friction and try to resolve it first with the skill that owns the task. -2. If it remains meaningful, keep the original task first. At a natural pause or after answering, offer once in one sentence, for example: "Sent has no tool to cancel a scheduled send; want me to draft a feedback report for the Sent team?" Never hold the task on the answer. -3. Do not offer for routine errors you resolved, expected restrictions such as validation, permission, compliance, balance, or rate limits working as documented, problems unrelated to Sent, or an issue already offered in this conversation. If the user declines or ignores the offer, drop it. -4. Accepting the offer permits a draft, not a send. Prepare the report below and send only after confirmation. +Use `feedback.send` only when the user asks to pass feedback to Sent, and only after they explicitly authorize the exact sanitized report. Do not solicit reports, and never send conversation transcripts or automatic feedback after a tool error. ## Prepare an authorized report 1. Use client-managed OAuth 2.1/PKCE; never request or expose credentials. Identify the authenticated organization and effective Sender Profile. An organization grant may use the schema's optional `profileId` for an owned profile; a profile grant cannot use that selector. Reauthorize for another organization or a profile outside the grant. 2. Choose `category`: `bug`, `missing_feature`, `confusing`, `praise`, or `other`. 3. Write a plain-text `message` of at most 2000 characters describing the attempted task and observed result. Remove phone numbers, email addresses, message bodies, credentials, and other personal or customer data yourself. Server redaction is pattern-based and is not a substitute for minimizing the report. -4. Optionally set `toolName` to an actual registered tool name. Unknown names are stored as null. Set `source` to `user` when the user wrote or requested the feedback, or `agent` when you noticed the gap or offered the report; omission defaults to `agent`. +4. Optionally set `toolName` to an actual registered tool name. Unknown names are stored as null. Set `source` to `user` when the user wrote the feedback, or `agent` when you drafted it at the user's request; omission defaults to `agent`. 5. Preview the exact scope, category, message, tool name, and source. Explain that Sent staff read the report. Require explicit confirmation immediately before the call; the user's explicit instruction to send this exact preview satisfies that requirement. If the report changes, obtain new confirmation. ## Receipt and limits diff --git a/claude-plugins/sent/skills/sent-messaging/SKILL.md b/claude-plugins/sent/skills/sent-messaging/SKILL.md index 0a27225..7d6adb9 100644 --- a/claude-plugins/sent/skills/sent-messaging/SKILL.md +++ b/claude-plugins/sent/skills/sent-messaging/SKILL.md @@ -38,6 +38,8 @@ For aggregate trends, funnels, or root-cause analysis across many delivery recor Never call `messages.send` without the preview and explicit confirmation immediately before the call. +From Claude, send to exactly one recipient per call. Do not loop over a list or split recipients across repeated calls to reach many people; for bulk or campaign sends, direct the user to the Sent Dashboard or the REST API in their own application. If Sent returns a connection-policy or send-quota error, stop and report it with any retry time; do not retry automatically. + ## Schedule a send `scheduledAt` is optional. Resolve the user's date, time, and timezone into an ISO-8601 timestamp with an explicit UTC offset or `Z`; offset-free timestamps are rejected. The instant must be 1 minute to 30 days ahead at execution. Include the exact local time and UTC instant in the preview, and revalidate the window before calling. diff --git a/claude-plugins/sent/skills/sent-profile-provisioning/SKILL.md b/claude-plugins/sent/skills/sent-profile-provisioning/SKILL.md index 5b8f8e7..55a3be9 100644 --- a/claude-plugins/sent/skills/sent-profile-provisioning/SKILL.md +++ b/claude-plugins/sent/skills/sent-profile-provisioning/SKILL.md @@ -1,6 +1,6 @@ --- name: sent-profile-provisioning -description: Lists, inspects, creates, updates, and deletes Sender Profiles through Sent MCP tools and executes the broader Sender Profile lifecycle over the API — creating profiles with the right inheritance, sharing, billing, and WhatsApp options, driving profile completion and its callback, managing 10DLC campaigns per profile, and administering users and roles. Use when calling POST /v3/profiles, handling a completion callback or unclear profile status, choosing inherit or dedicated resources, wiring per-tenant onboarding, or inviting and role-managing users. +description: Lists, inspects, and updates existing Sender Profiles through Sent MCP tools, hands Sender Profile creation and deletion to the Sent Dashboard, and executes the broader Sender Profile lifecycle over the API — creating profiles with the right inheritance, sharing, billing, and WhatsApp options, driving profile completion and its callback, managing 10DLC campaigns per profile, and administering users and roles. Use when calling POST /v3/profiles, handling a completion callback or unclear profile status, choosing inherit or dedicated resources, wiring per-tenant onboarding, or inviting and role-managing users. --- # Sent Profile Provisioning @@ -9,24 +9,24 @@ This skill is the execution counterpart to profile architecture: once the tenanc ## Live MCP profile operations -Use `sender_profiles.list`, `sender_profiles.get`, `sender_profiles.create`, `sender_profiles.update`, and `sender_profiles.delete` through client-managed OAuth 2.1/PKCE. Never request or expose credentials. Validate the calling organization with `account.get` before a mutation. +Use `sender_profiles.list`, `sender_profiles.get`, and `sender_profiles.update` through client-managed OAuth 2.1/PKCE. Never request or expose credentials. Validate the calling organization with `account.get` before a mutation. These tools operate under the authenticated organization. The target argument is `id`, not `profileId`; no `sender_profiles.*` tool accepts an acting `profileId`. Profile grants own no child profiles and receive an empty list. Reauthorize with organization scope to manage its profiles. - **List/get:** list uses `page` (1-based) and `pageSize` (1-100, default 20) and reports channel readiness. Use real IDs from that list. Get returns `found`/`item`; `found: false` does not distinguish nonexistent, deleted, and another organization's profile. Do not infer cross-tenant existence. -- **Create:** requires `name`, `shortName`, and `idempotencyKey`. `shortName` is 3-11 letters, numbers, or spaces with at least one letter. Optional arguments are `description`, `smsCountry`, `smsNumberType`, `smsSenderValue`, `smsAreaCodes`, and `compliance`. A supplied `smsCountry` requires `smsNumberType`; omission uses the organization's shared routes. For a dedicated SMS market, use `sent-compliance` to inspect requirements and fill its setup plan first. Provisioning can claim a number and incur registration fees. WhatsApp, RCS, billing, document uploads, inheritance flags, and sandbox arguments are not exposed by this MCP create tool. Required document uploads block it before any write; use the dashboard or documented REST flow. No one-time API key is returned. +- **Create:** Sender Profiles are created in the Sent Dashboard, not from Claude, because provisioning can claim a number and incur registration fees. For a dedicated SMS market, use `sent-compliance` to explain the requirements, then hand the user to the Dashboard. If the connection lists `sender_profiles.create`, do not call it. - **Update:** fetch the exact target, then supply `id` and only the changed fields among `name`, `shortName`, and `description`. Omitted fields stay unchanged; explicit `description: null` clears the description. Name and short name cannot be cleared. At least one field is required. Do not populate omitted fields with null or assume this updates channel configuration. -- **Delete:** fetch and preview the target. Explain that deletion can disconnect WhatsApp, release numbers, and disable live routes. Supply `id` and a nonempty `idempotencyKey`, which is required even if the schema marks it optional. `deleted: false` means no matching owned profile was deleted and does not prove another tenant's profile exists. +- **Delete:** Sender Profiles are deleted in the Sent Dashboard, not from Claude, because deletion can disconnect WhatsApp, release numbers, and disable live routes. If the connection lists `sender_profiles.delete`, do not call it. -For every create, update, or delete, preview the organization, exact target or payload, and side effects, then require explicit confirmation immediately before the call. Changes invalidate confirmation. Every retry needs a new preview and confirmation. +For every update, preview the organization, exact target or payload, and side effects, then require explicit confirmation immediately before the call. Changes invalidate confirmation. Every retry needs a new preview and confirmation. -Create and delete use durable receipts. Reuse the original key and arguments for the same intended operation; do not generate a new key after a timeout or an in-flight result. Inspect list/get and returned receipt guidance first. A key reused with different arguments is refused. If interrupted provisioning or teardown needs attention, stop and reconcile in the dashboard rather than retry automatically or replace the key. A new key authorizes a separate operation only after the existing outcome has been reconciled. Verify the returned profile and channel readiness; creation alone does not establish permission to send. +If provisioning or teardown started in the Dashboard looks incomplete, inspect it with list/get and reconcile it in the Dashboard. Profile creation alone does not establish permission to send. MCP has no profile completion, campaign-management, user-management, or market-addition tools. Use the dashboard or the separately documented REST workflow for those tasks; do not invent tools. ## Broader REST workflow -The following `/v3/profiles` guidance describes the legacy REST lifecycle and its callbacks, not arguments for `sender_profiles.*`. For new REST profile provisioning, check the current API reference for `/v3/sender-profiles`; never mechanically substitute paths, snake_case REST fields, or legacy completion semantics into an MCP call. +From Claude, use the REST guidance below to write or review the user's application code; do not run profile-creating or profile-deleting requests on the user's behalf. The following `/v3/profiles` guidance describes the legacy REST lifecycle and its callbacks, not arguments for `sender_profiles.*`. For new REST profile provisioning, check the current API reference for `/v3/sender-profiles`; never mechanically substitute paths, snake_case REST fields, or legacy completion semantics into an MCP call. ### Provisioning sequence diff --git a/claude-plugins/sent/skills/sent/SKILL.md b/claude-plugins/sent/skills/sent/SKILL.md index 4a5b280..95137a2 100644 --- a/claude-plugins/sent/skills/sent/SKILL.md +++ b/claude-plugins/sent/skills/sent/SKILL.md @@ -38,7 +38,7 @@ Do **not** use when: | Check the selected account, balance, onboarding state, or readiness to send | `sent-account-readiness` | | Report a bug, missing feature, confusing result, or praise to Sent with user authorization | `sent-feedback` | | Inspect SMS market fields, document requirements, or an exact setup call | `sent-compliance` | -| List, get, create, update, or delete owned Sender Profiles through MCP | `sent-profile-provisioning` | +| List, get, or update owned Sender Profiles through MCP, or hand creation and deletion to the Sent Dashboard | `sent-profile-provisioning` | ### Specialist guidance diff --git a/claude-plugins/sent/skills/sms-10dlc-registration/SKILL.md b/claude-plugins/sent/skills/sms-10dlc-registration/SKILL.md index 3aba117..93efc2a 100644 --- a/claude-plugins/sent/skills/sms-10dlc-registration/SKILL.md +++ b/claude-plugins/sent/skills/sms-10dlc-registration/SKILL.md @@ -7,7 +7,7 @@ description: Prepares and validates Sent US A2P 10DLC brand and campaign registr Use this skill for US A2P SMS over 10-digit long codes. Separate the compliance evidence packet from the exact Sent API request; they have different schemas and validators. -For current field and document requirements or an exact SMS setup call, use `sent-compliance` before preparing the registration evidence here. Its plan creates a new Sender Profile and does not manage campaigns on an existing profile. +For current field and document requirements or an exact SMS setup call, use `sent-compliance` before preparing the registration evidence here. From Claude, its plan is completed by creating the Sender Profile in the Sent Dashboard; it does not manage campaigns on an existing profile. ## Current Sent resource model diff --git a/docs/PUBLIC_RELEASE.md b/docs/PUBLIC_RELEASE.md index 99fa71c..d22d247 100644 --- a/docs/PUBLIC_RELEASE.md +++ b/docs/PUBLIC_RELEASE.md @@ -44,3 +44,5 @@ Submit the public repository through the current Claude plugin submission form o OpenAI listings include website, support, privacy policy, and terms URLs in both the portable extension and Codex adapter. Release validation checks their consistency and HTTPS endpoints. Confirmation requirements for sends, feedback, contact mutations, template deletion, and Sender Profile mutations are enforced independently of public-surface metadata. Scanner regression checks cover credential omission and repository containment. The Claude manifest carries `documentationUrl`, `supportUrl`, `privacyPolicyUrl`, and `termsOfServiceUrl` from the shared listing source. Validate with Claude Code 2.1.287 or later, which recognizes these directory fields. Packaged scripts and application examples must not read ambient credentials; the webhook utility accepts only an explicitly supplied secret on standard input. + +The Claude adapter applies `adapter-sources/claude/overlay.yaml`: it withholds Sender Profile creation and deletion from `claude-plugins/sent/public-surface.json` (25 tools) and adjusts the provisioning, compliance, and messaging guidance. Use that file's tool list and these use cases in Claude directory submissions: send one approved template message, inspect message status and activity, manage contacts and templates, query analytics and number capabilities, review account, balance, onboarding, and SMS compliance requirements, update Sender Profile metadata, and send user-authorized feedback. The Sent server enforces the same restrictions for hosted Claude connections; for Claude Code, the overlay is guidance only. Never hand-edit `claude-plugins/sent`. diff --git a/evals/sent-feedback.yaml b/evals/sent-feedback.yaml index ca7dddc..b46fba2 100644 --- a/evals/sent-feedback.yaml +++ b/evals/sent-feedback.yaml @@ -7,32 +7,32 @@ cases: expect: trigger rationale: Authorized report preparation for a confusing tool result. - query: Sent still has no tool to cancel a scheduled send, so I worked around it by hand again. That's the third time this week. - expect: trigger - rationale: Missing functionality, a repeated workaround, and user frustration warrant a proactive offer to report. + expect: no_trigger + rationale: Venting about a gap is not a request to report it; feedback is sent only when the user asks. - query: We checked the account, scopes, and inputs, but templates.list still fails with an internal error, so I'll find the template in the dashboard instead. - expect: trigger - rationale: An error still unresolved after diagnosis warrants an optional report offer that does not block the workaround. + expect: no_trigger + rationale: The user chose a workaround and did not ask to report the error. - query: After matching the date range and timezone, dashboard.messages_sent and dashboard.deliverability still disagree on totals with no explanation. - expect: trigger - rationale: A confusing result that survives diagnosis warrants an offer to report after answering. + expect: no_trigger + rationale: The user needs the discrepancy explained by sent-analytics, not a report they did not request. - query: This tool error is annoying; diagnose it without sending a report. expect: no_trigger rationale: The user asked for diagnosis and declined reporting. - query: contacts.create_many just returned a validation error. What went wrong? expect: no_trigger - rationale: A fresh error needs diagnosis by the owning skill before any feedback offer. + rationale: A fresh error needs diagnosis by the owning skill, not a feedback report. - query: The send failed because a template variable was missing; I added it and it went through. expect: no_trigger - rationale: A routine error that was resolved is not meaningful friction. + rationale: The user did not ask to report the resolved error. - query: Why won't Sent let me send US SMS before my 10DLC campaign is approved? expect: no_trigger rationale: An expected compliance restriction belongs to sms-10dlc-registration. - query: My Postgres migration keeps timing out and it's driving me crazy. expect: no_trigger - rationale: Frustration unrelated to Sent does not warrant a Sent feedback offer. + rationale: The issue is unrelated to Sent. - query: The scheduled-send cancellation gap hit us again, but I already said not to report it. expect: no_trigger - rationale: A repeated occurrence of friction the user already declined does not warrant another offer. + rationale: The user declined reporting. - query: Why did our SMS delivery rate fall yesterday? expect: no_trigger rationale: Delivery diagnosis belongs to messaging-performance-analyzer. @@ -41,4 +41,4 @@ cases: rationale: Use sent-messaging for investigation and sent-feedback for the authorized report. - query: I need tomorrow's scheduled send stopped, but there's no cancel tool and my last two workarounds failed. What can I do? expect: ambiguous - rationale: sent-messaging owns the task; offer a report on the missing tool only after addressing it. + rationale: sent-messaging owns the task; report the missing tool only if the user then asks. diff --git a/packages/sent/README.md b/packages/sent/README.md index 7c507ff..dded9a5 100644 --- a/packages/sent/README.md +++ b/packages/sent/README.md @@ -77,7 +77,7 @@ For engineering work, use `sent-integration-starter` for new integrations, `sent Authorization is client-managed. The MCP client performs OAuth 2.1 with PKCE and Dynamic Client Registration; the package contains no token, API key, authorization header, OAuth client ID, or credential placeholder. -The grant is tied to the organization and Sender Profile selected during authorization. An organization grant can use a schema-supported `profileId` to act as an owned profile; profile grants cannot. `sender_profiles.*` uses target `id` and rejects acting `profileId`. Reauthorize for another organization or scope outside the grant. Revoke access from **Sent Dashboard → Settings → MCP Connections** when the connection is no longer needed. Never ask a user to paste credentials into a prompt. +The grant is tied to the organization and Sender Profile selected during authorization. An organization grant can use a schema-supported `profileId` to act as an owned profile; profile grants cannot. `sender_profiles.*` uses target `id` and rejects acting `profileId`. Reauthorize for another organization or scope outside the grant. Revoke access by deactivating the connection's API key in **Sent Dashboard → API Keys** when the connection is no longer needed. Never ask a user to paste credentials into a prompt. ## Mutation and privacy contract @@ -110,6 +110,8 @@ Live network checks are isolated from pull-request validation. The scheduled/man The public MCP surface includes 27 tools. `messages.send` uses an existing template with all required parameters; it supports `scheduledAt` with an explicit timezone offset, from 1 minute to 30 days ahead. Acceptance is not delivery, and quiet hours can defer release. +Connections authorized through Claude's hosted connector (Claude on the web, Desktop, mobile, and Cowork) get a server-enforced policy: Sender Profile creation and deletion are unavailable and happen in the Sent Dashboard, each `messages.send` call accepts one recipient, and organization-wide send quotas apply. Other clients, including Claude Code, keep the full surface. + Feedback goes to Sent staff only with user authorization and a reviewed, sanitized report. It is limited to 2000 characters and 20 calls per authenticated account scope per UTC day, shared across acting profiles. Its note does not guarantee storage, open a support ticket, or promise a response. Number lookup is paid and limited to 1000 calls per authenticated account scope per UTC day. Profile create and delete require `idempotencyKey`; interrupted outcomes require reconciliation before another operation. MCP update changes only name, short name, or description. Compliance setup plans create a new profile, support SMS only, and hand required document uploads to the dashboard or REST API. diff --git a/packages/sent/plugin.json b/packages/sent/plugin.json index 6f13906..33ae8f6 100644 --- a/packages/sent/plugin.json +++ b/packages/sent/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "sent", - "version": "0.2.1", + "version": "0.3.0", "description": "Official Sent plugin for SMS, WhatsApp, RCS, scheduled template sends, Sender Profiles, SMS compliance, authorized feedback, contacts, analytics, API integration, webhooks, routing, and migration.", "author": { "name": "Sent", diff --git a/packages/sent/skills/sent-compliance/SKILL.md b/packages/sent/skills/sent-compliance/SKILL.md index db8f609..6776bde 100644 --- a/packages/sent/skills/sent-compliance/SKILL.md +++ b/packages/sent/skills/sent-compliance/SKILL.md @@ -16,7 +16,7 @@ The optional `profileId` selector is available only for an organization's owned ## Interpret requirements and plan 1. Call `compliance.requirements` for the selected market. Preserve required flags, conditional rules, options, item fields, sender pattern, and attachment field names. -2. Check `hasRegime` before interpreting an empty requirements list. An empty list with a published regime can mean no fields are demanded; `hasRegime: false` means no schema is published and is not proof that no compliance is required. +2. Check `hasRegime` before interpreting an empty requirements list. An empty list with a published regime can mean no fields are demanded; `hasRegime: false` means no schema is published and is not proof that no compliance is required. US `TOLL_FREE` numbers require toll-free verification before production traffic, even when no requirements are returned; never tell the user that a toll-free number needs no registration. 3. Call `compliance.setup_plan` when the user needs the exact setup call. Its `call.tool` is `sender_profiles.create`, with camelCase argument names and null placeholders that must be filled before execution. Do not execute blank values or translate the plan into an invented REST call. 4. If `canCompleteHere` is false, report `blocker` and `attachments`, then hand off document submission to the Sent dashboard or the documented multipart REST workflow. Required uploads cannot be carried through MCP; do not attempt a guaranteed-to-fail create. Optional attachment fields alone do not necessarily block creation. 5. When setup can proceed, hand the completed plan to `sent-profile-provisioning` for scope validation, required idempotency key, payload preview, and explicit confirmation before creation. diff --git a/packages/sent/skills/sent-feedback/SKILL.md b/packages/sent/skills/sent-feedback/SKILL.md index 08d3a58..2cd0f0a 100644 --- a/packages/sent/skills/sent-feedback/SKILL.md +++ b/packages/sent/skills/sent-feedback/SKILL.md @@ -1,27 +1,18 @@ --- name: sent-feedback -description: Reports user-authorized feedback about Sent MCP tools to the Sent team. Use when the user asks to report a bug, request a missing feature, pass along confusing results or praise, or agrees to report a tool gap the agent noticed. Also use proactively when a Sent workflow hits meaningful friction the agent or user experienced, such as missing functionality, an error still unresolved after diagnosis, confusing results, repeated workarounds, or user frustration with Sent, to offer an optional report after the original task. Feedback is read by Sent staff; it does not open a support ticket or return an answer. Do not trigger for routine or expected errors, issues unrelated to Sent, a delivery investigation alone, or friction already offered for reporting. +description: Reports user-authorized feedback about Sent MCP tools to the Sent team. Use when the user asks to report a bug, request a missing feature, or pass along a confusing result or praise about Sent. Feedback is read by Sent staff; it does not open a support ticket or return an answer. Do not trigger for routine errors, issues unrelated to Sent, or a delivery investigation alone. --- # Sent Feedback -Use `feedback.send` only when the user asks to pass feedback to Sent or accepts your offer to report friction, and only after they explicitly authorize the exact sanitized report. Never send unsolicited reports, conversation transcripts, or automatic feedback after a tool error. - -## Offer feedback after meaningful friction - -Sent wants to hear about friction even when the user would not think to report it. During any Sent workflow, notice meaningful friction that you or the user hit: missing functionality, an error that persists after diagnosis, confusing or contradictory results, repeated workarounds, or user frustration with Sent. - -1. Diagnose the friction and try to resolve it first with the skill that owns the task. -2. If it remains meaningful, keep the original task first. At a natural pause or after answering, offer once in one sentence, for example: "Sent has no tool to cancel a scheduled send; want me to draft a feedback report for the Sent team?" Never hold the task on the answer. -3. Do not offer for routine errors you resolved, expected restrictions such as validation, permission, compliance, balance, or rate limits working as documented, problems unrelated to Sent, or an issue already offered in this conversation. If the user declines or ignores the offer, drop it. -4. Accepting the offer permits a draft, not a send. Prepare the report below and send only after confirmation. +Use `feedback.send` only when the user asks to pass feedback to Sent, and only after they explicitly authorize the exact sanitized report. Do not solicit reports, and never send conversation transcripts or automatic feedback after a tool error. ## Prepare an authorized report 1. Use client-managed OAuth 2.1/PKCE; never request or expose credentials. Identify the authenticated organization and effective Sender Profile. An organization grant may use the schema's optional `profileId` for an owned profile; a profile grant cannot use that selector. Reauthorize for another organization or a profile outside the grant. 2. Choose `category`: `bug`, `missing_feature`, `confusing`, `praise`, or `other`. 3. Write a plain-text `message` of at most 2000 characters describing the attempted task and observed result. Remove phone numbers, email addresses, message bodies, credentials, and other personal or customer data yourself. Server redaction is pattern-based and is not a substitute for minimizing the report. -4. Optionally set `toolName` to an actual registered tool name. Unknown names are stored as null. Set `source` to `user` when the user wrote or requested the feedback, or `agent` when you noticed the gap or offered the report; omission defaults to `agent`. +4. Optionally set `toolName` to an actual registered tool name. Unknown names are stored as null. Set `source` to `user` when the user wrote the feedback, or `agent` when you drafted it at the user's request; omission defaults to `agent`. 5. Preview the exact scope, category, message, tool name, and source. Explain that Sent staff read the report. Require explicit confirmation immediately before the call; the user's explicit instruction to send this exact preview satisfies that requirement. If the report changes, obtain new confirmation. ## Receipt and limits diff --git a/packages/sent/skills/sent-profile-provisioning/SKILL.md b/packages/sent/skills/sent-profile-provisioning/SKILL.md index 5b8f8e7..dd9d8da 100644 --- a/packages/sent/skills/sent-profile-provisioning/SKILL.md +++ b/packages/sent/skills/sent-profile-provisioning/SKILL.md @@ -16,7 +16,7 @@ These tools operate under the authenticated organization. The target argument is - **List/get:** list uses `page` (1-based) and `pageSize` (1-100, default 20) and reports channel readiness. Use real IDs from that list. Get returns `found`/`item`; `found: false` does not distinguish nonexistent, deleted, and another organization's profile. Do not infer cross-tenant existence. - **Create:** requires `name`, `shortName`, and `idempotencyKey`. `shortName` is 3-11 letters, numbers, or spaces with at least one letter. Optional arguments are `description`, `smsCountry`, `smsNumberType`, `smsSenderValue`, `smsAreaCodes`, and `compliance`. A supplied `smsCountry` requires `smsNumberType`; omission uses the organization's shared routes. For a dedicated SMS market, use `sent-compliance` to inspect requirements and fill its setup plan first. Provisioning can claim a number and incur registration fees. WhatsApp, RCS, billing, document uploads, inheritance flags, and sandbox arguments are not exposed by this MCP create tool. Required document uploads block it before any write; use the dashboard or documented REST flow. No one-time API key is returned. - **Update:** fetch the exact target, then supply `id` and only the changed fields among `name`, `shortName`, and `description`. Omitted fields stay unchanged; explicit `description: null` clears the description. Name and short name cannot be cleared. At least one field is required. Do not populate omitted fields with null or assume this updates channel configuration. -- **Delete:** fetch and preview the target. Explain that deletion can disconnect WhatsApp, release numbers, and disable live routes. Supply `id` and a nonempty `idempotencyKey`, which is required even if the schema marks it optional. `deleted: false` means no matching owned profile was deleted and does not prove another tenant's profile exists. +- **Delete:** fetch and preview the target. Explain that deletion can disconnect WhatsApp, release numbers, and disable live routes. Supply `id` and a nonempty `idempotencyKey`. `deleted: false` means no matching owned profile was deleted and does not prove another tenant's profile exists. For every create, update, or delete, preview the organization, exact target or payload, and side effects, then require explicit confirmation immediately before the call. Changes invalidate confirmation. Every retry needs a new preview and confirmation. diff --git a/plugin.json b/plugin.json index 6f13906..33ae8f6 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "sent", - "version": "0.2.1", + "version": "0.3.0", "description": "Official Sent plugin for SMS, WhatsApp, RCS, scheduled template sends, Sender Profiles, SMS compliance, authorized feedback, contacts, analytics, API integration, webhooks, routing, and migration.", "author": { "name": "Sent", diff --git a/plugins/sent/.codex-plugin/plugin.json b/plugins/sent/.codex-plugin/plugin.json index 9878882..c51aa46 100644 --- a/plugins/sent/.codex-plugin/plugin.json +++ b/plugins/sent/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "sent", - "version": "0.2.1", + "version": "0.3.0", "description": "Official Sent plugin for SMS, WhatsApp, RCS, scheduled template sends, Sender Profiles, SMS compliance, authorized feedback, contacts, analytics, API integration, webhooks, routing, and migration.", "author": { "name": "Sent", diff --git a/plugins/sent/README.md b/plugins/sent/README.md index 42076da..3e15620 100644 --- a/plugins/sent/README.md +++ b/plugins/sent/README.md @@ -37,7 +37,7 @@ npx skills add https://github.com/sentdm/sent-plugin --skill sent ## MCP and authorization -The adapter connects only to `https://mcp.sent.dm/mcp`. The MCP client performs OAuth 2.1 with PKCE and Dynamic Client Registration; no credentials or credential placeholders are included. The grant is tied to the organization and Sender Profile selected during authorization. An organization grant can use a schema-supported `profileId` to act as an owned profile; profile grants cannot. `sender_profiles.*` uses target `id` and rejects acting `profileId`. Reauthorize for another organization or scope outside the grant and revoke access from **Sent Dashboard → Settings → MCP Connections**. +The adapter connects only to `https://mcp.sent.dm/mcp`. The MCP client performs OAuth 2.1 with PKCE and Dynamic Client Registration; no credentials or credential placeholders are included. The grant is tied to the organization and Sender Profile selected during authorization. An organization grant can use a schema-supported `profileId` to act as an owned profile; profile grants cannot. `sender_profiles.*` uses target `id` and rejects acting `profileId`. Reauthorize for another organization or scope outside the grant and revoke access by deactivating the connection's API key in **Sent Dashboard → API Keys**. MCP-backed live operations cover: @@ -62,6 +62,8 @@ This directory is generated. Make source changes in `packages/sent` or `adapter- The public MCP surface includes 27 tools. `messages.send` uses an existing template with all required parameters; it supports `scheduledAt` with an explicit timezone offset, from 1 minute to 30 days ahead. Acceptance is not delivery, and quiet hours can defer release. +Connections authorized through Claude's hosted connector (Claude on the web, Desktop, mobile, and Cowork) get a server-enforced policy: Sender Profile creation and deletion are unavailable and happen in the Sent Dashboard, each `messages.send` call accepts one recipient, and organization-wide send quotas apply. Other clients, including Claude Code, keep the full surface. + Feedback goes to Sent staff only with user authorization and a reviewed, sanitized report. It is limited to 2000 characters and 20 calls per authenticated account scope per UTC day, shared across acting profiles. Its note does not guarantee storage, open a support ticket, or promise a response. Number lookup is paid and limited to 1000 calls per authenticated account scope per UTC day. Profile create and delete require `idempotencyKey`; interrupted outcomes require reconciliation before another operation. MCP update changes only name, short name, or description. Compliance setup plans create a new profile, support SMS only, and hand required document uploads to the dashboard or REST API. diff --git a/plugins/sent/skills/sent-compliance/SKILL.md b/plugins/sent/skills/sent-compliance/SKILL.md index db8f609..6776bde 100644 --- a/plugins/sent/skills/sent-compliance/SKILL.md +++ b/plugins/sent/skills/sent-compliance/SKILL.md @@ -16,7 +16,7 @@ The optional `profileId` selector is available only for an organization's owned ## Interpret requirements and plan 1. Call `compliance.requirements` for the selected market. Preserve required flags, conditional rules, options, item fields, sender pattern, and attachment field names. -2. Check `hasRegime` before interpreting an empty requirements list. An empty list with a published regime can mean no fields are demanded; `hasRegime: false` means no schema is published and is not proof that no compliance is required. +2. Check `hasRegime` before interpreting an empty requirements list. An empty list with a published regime can mean no fields are demanded; `hasRegime: false` means no schema is published and is not proof that no compliance is required. US `TOLL_FREE` numbers require toll-free verification before production traffic, even when no requirements are returned; never tell the user that a toll-free number needs no registration. 3. Call `compliance.setup_plan` when the user needs the exact setup call. Its `call.tool` is `sender_profiles.create`, with camelCase argument names and null placeholders that must be filled before execution. Do not execute blank values or translate the plan into an invented REST call. 4. If `canCompleteHere` is false, report `blocker` and `attachments`, then hand off document submission to the Sent dashboard or the documented multipart REST workflow. Required uploads cannot be carried through MCP; do not attempt a guaranteed-to-fail create. Optional attachment fields alone do not necessarily block creation. 5. When setup can proceed, hand the completed plan to `sent-profile-provisioning` for scope validation, required idempotency key, payload preview, and explicit confirmation before creation. diff --git a/plugins/sent/skills/sent-feedback/SKILL.md b/plugins/sent/skills/sent-feedback/SKILL.md index 08d3a58..2cd0f0a 100644 --- a/plugins/sent/skills/sent-feedback/SKILL.md +++ b/plugins/sent/skills/sent-feedback/SKILL.md @@ -1,27 +1,18 @@ --- name: sent-feedback -description: Reports user-authorized feedback about Sent MCP tools to the Sent team. Use when the user asks to report a bug, request a missing feature, pass along confusing results or praise, or agrees to report a tool gap the agent noticed. Also use proactively when a Sent workflow hits meaningful friction the agent or user experienced, such as missing functionality, an error still unresolved after diagnosis, confusing results, repeated workarounds, or user frustration with Sent, to offer an optional report after the original task. Feedback is read by Sent staff; it does not open a support ticket or return an answer. Do not trigger for routine or expected errors, issues unrelated to Sent, a delivery investigation alone, or friction already offered for reporting. +description: Reports user-authorized feedback about Sent MCP tools to the Sent team. Use when the user asks to report a bug, request a missing feature, or pass along a confusing result or praise about Sent. Feedback is read by Sent staff; it does not open a support ticket or return an answer. Do not trigger for routine errors, issues unrelated to Sent, or a delivery investigation alone. --- # Sent Feedback -Use `feedback.send` only when the user asks to pass feedback to Sent or accepts your offer to report friction, and only after they explicitly authorize the exact sanitized report. Never send unsolicited reports, conversation transcripts, or automatic feedback after a tool error. - -## Offer feedback after meaningful friction - -Sent wants to hear about friction even when the user would not think to report it. During any Sent workflow, notice meaningful friction that you or the user hit: missing functionality, an error that persists after diagnosis, confusing or contradictory results, repeated workarounds, or user frustration with Sent. - -1. Diagnose the friction and try to resolve it first with the skill that owns the task. -2. If it remains meaningful, keep the original task first. At a natural pause or after answering, offer once in one sentence, for example: "Sent has no tool to cancel a scheduled send; want me to draft a feedback report for the Sent team?" Never hold the task on the answer. -3. Do not offer for routine errors you resolved, expected restrictions such as validation, permission, compliance, balance, or rate limits working as documented, problems unrelated to Sent, or an issue already offered in this conversation. If the user declines or ignores the offer, drop it. -4. Accepting the offer permits a draft, not a send. Prepare the report below and send only after confirmation. +Use `feedback.send` only when the user asks to pass feedback to Sent, and only after they explicitly authorize the exact sanitized report. Do not solicit reports, and never send conversation transcripts or automatic feedback after a tool error. ## Prepare an authorized report 1. Use client-managed OAuth 2.1/PKCE; never request or expose credentials. Identify the authenticated organization and effective Sender Profile. An organization grant may use the schema's optional `profileId` for an owned profile; a profile grant cannot use that selector. Reauthorize for another organization or a profile outside the grant. 2. Choose `category`: `bug`, `missing_feature`, `confusing`, `praise`, or `other`. 3. Write a plain-text `message` of at most 2000 characters describing the attempted task and observed result. Remove phone numbers, email addresses, message bodies, credentials, and other personal or customer data yourself. Server redaction is pattern-based and is not a substitute for minimizing the report. -4. Optionally set `toolName` to an actual registered tool name. Unknown names are stored as null. Set `source` to `user` when the user wrote or requested the feedback, or `agent` when you noticed the gap or offered the report; omission defaults to `agent`. +4. Optionally set `toolName` to an actual registered tool name. Unknown names are stored as null. Set `source` to `user` when the user wrote the feedback, or `agent` when you drafted it at the user's request; omission defaults to `agent`. 5. Preview the exact scope, category, message, tool name, and source. Explain that Sent staff read the report. Require explicit confirmation immediately before the call; the user's explicit instruction to send this exact preview satisfies that requirement. If the report changes, obtain new confirmation. ## Receipt and limits diff --git a/plugins/sent/skills/sent-profile-provisioning/SKILL.md b/plugins/sent/skills/sent-profile-provisioning/SKILL.md index 5b8f8e7..dd9d8da 100644 --- a/plugins/sent/skills/sent-profile-provisioning/SKILL.md +++ b/plugins/sent/skills/sent-profile-provisioning/SKILL.md @@ -16,7 +16,7 @@ These tools operate under the authenticated organization. The target argument is - **List/get:** list uses `page` (1-based) and `pageSize` (1-100, default 20) and reports channel readiness. Use real IDs from that list. Get returns `found`/`item`; `found: false` does not distinguish nonexistent, deleted, and another organization's profile. Do not infer cross-tenant existence. - **Create:** requires `name`, `shortName`, and `idempotencyKey`. `shortName` is 3-11 letters, numbers, or spaces with at least one letter. Optional arguments are `description`, `smsCountry`, `smsNumberType`, `smsSenderValue`, `smsAreaCodes`, and `compliance`. A supplied `smsCountry` requires `smsNumberType`; omission uses the organization's shared routes. For a dedicated SMS market, use `sent-compliance` to inspect requirements and fill its setup plan first. Provisioning can claim a number and incur registration fees. WhatsApp, RCS, billing, document uploads, inheritance flags, and sandbox arguments are not exposed by this MCP create tool. Required document uploads block it before any write; use the dashboard or documented REST flow. No one-time API key is returned. - **Update:** fetch the exact target, then supply `id` and only the changed fields among `name`, `shortName`, and `description`. Omitted fields stay unchanged; explicit `description: null` clears the description. Name and short name cannot be cleared. At least one field is required. Do not populate omitted fields with null or assume this updates channel configuration. -- **Delete:** fetch and preview the target. Explain that deletion can disconnect WhatsApp, release numbers, and disable live routes. Supply `id` and a nonempty `idempotencyKey`, which is required even if the schema marks it optional. `deleted: false` means no matching owned profile was deleted and does not prove another tenant's profile exists. +- **Delete:** fetch and preview the target. Explain that deletion can disconnect WhatsApp, release numbers, and disable live routes. Supply `id` and a nonempty `idempotencyKey`. `deleted: false` means no matching owned profile was deleted and does not prove another tenant's profile exists. For every create, update, or delete, preview the organization, exact target or payload, and side effects, then require explicit confirmation immediately before the call. Changes invalidate confirmation. Every retry needs a new preview and confirmation. diff --git a/scripts/generate_adapters.py b/scripts/generate_adapters.py index f81264c..7687e5e 100644 --- a/scripts/generate_adapters.py +++ b/scripts/generate_adapters.py @@ -10,7 +10,7 @@ import tempfile from pathlib import Path -from repository_metadata import load_repository_metadata, openai_interface +from repository_metadata import ClaudeOverlay, load_claude_overlay, load_repository_metadata, openai_interface ROOT = Path(__file__).resolve().parents[1] @@ -59,12 +59,29 @@ def reset_tree(path: Path, output_root: Path) -> None: shutil.rmtree(path) +def apply_claude_overlay(claude_root: Path, overlay: ClaudeOverlay, public_surface: dict) -> None: + tools = {name: tool for name, tool in public_surface["tools"].items() if name not in overlay.withheld_tools} + write_json(claude_root / "public-surface.json", {**public_surface, "tools": tools}) + for relative, edits in overlay.edits.items(): + path = claude_root / relative + if not path.is_file(): + raise RuntimeError(f"Claude overlay target does not exist: {relative}") + text = path.read_text(encoding="utf-8") + for index, (find, replace) in enumerate(edits, 1): + matches = text.count(find) + if matches != 1: + raise RuntimeError(f"Claude overlay edit {relative}#{index} matched {matches} times; expected exactly 1") + text = text.replace(find, replace) + path.write_text(text, encoding="utf-8") + + def build(output_root: Path, source_root: Path = ROOT) -> None: package = source_root / "packages" / "sent" commands = source_root / "adapter-sources" / "claude" / "commands" adapter_readme = source_root / "adapter-sources" / "shared" / "README.md" marketplace = read_json(source_root / "adapter-sources" / "shared" / "marketplace.json") metadata = load_repository_metadata(source_root) + claude_overlay = load_claude_overlay(source_root, metadata) portable_manifest = read_json(package / "plugin.json") portable_mcp = read_json(package / "mcp.json") public_surface = read_json(package / "public-surface.json") @@ -97,6 +114,7 @@ def build(output_root: Path, source_root: Path = ROOT) -> None: shutil.copy2(adapter_readme, adapter_root / "README.md") shutil.copy2(package / "LICENSE", adapter_root / "LICENSE") write_json(adapter_root / "public-surface.json", public_surface) + apply_claude_overlay(claude_root, claude_overlay, public_surface) common = { "name": "sent", diff --git a/scripts/repository_metadata.py b/scripts/repository_metadata.py index 9c12e1c..3f418f6 100644 --- a/scripts/repository_metadata.py +++ b/scripts/repository_metadata.py @@ -5,7 +5,7 @@ import json from dataclasses import dataclass -from pathlib import Path +from pathlib import Path, PurePosixPath from typing import Any @@ -140,3 +140,54 @@ def load_repository_metadata(root: Path) -> RepositoryMetadata: raise MetadataError(f"{surface_path}: {name} must retain its owner, mutation class, and confirmation under the independent safety policy") return RepositoryMetadata(version=version, skills=skills, tools=tools) + + +CLAUDE_OVERLAY = Path("adapter-sources") / "claude" / "overlay.yaml" + + +@dataclass(frozen=True) +class ClaudeOverlay: + withheld_tools: dict[str, str] + edits: dict[str, tuple[tuple[str, str], ...]] + + +def load_claude_overlay(root: Path, metadata: RepositoryMetadata) -> ClaudeOverlay: + """Load the Claude-only tool withholding and text edits applied to claude-plugins/sent.""" + import yaml + + path = root / CLAUDE_OVERLAY + try: + raw = yaml.safe_load(path.read_text(encoding="utf-8")) + except (OSError, yaml.YAMLError) as exc: + raise MetadataError(f"could not read {CLAUDE_OVERLAY}: {exc}") from exc + if not isinstance(raw, dict) or set(raw) != {"withheld_tools", "edits"}: + raise MetadataError(f"{CLAUDE_OVERLAY}: expected exactly withheld_tools and edits") + + withheld = raw["withheld_tools"] + if not isinstance(withheld, dict) or not withheld: + raise MetadataError(f"{CLAUDE_OVERLAY}: withheld_tools must be a non-empty mapping") + for name, reason in withheld.items(): + if name not in metadata.tools: + raise MetadataError(f"{CLAUDE_OVERLAY}: withheld tool {name!r} is not in the canonical public surface") + if not isinstance(reason, str) or not reason.strip(): + raise MetadataError(f"{CLAUDE_OVERLAY}: withheld tool {name} needs a reason") + + raw_edits = raw["edits"] + if not isinstance(raw_edits, dict): + raise MetadataError(f"{CLAUDE_OVERLAY}: edits must be a mapping of adapter paths") + edits: dict[str, tuple[tuple[str, str], ...]] = {} + for relative, entries in raw_edits.items(): + target = PurePosixPath(str(relative)) + if target.is_absolute() or ".." in target.parts or not target.parts: + raise MetadataError(f"{CLAUDE_OVERLAY}: edit path {relative!r} must stay inside the Claude adapter") + if not isinstance(entries, list) or not entries: + raise MetadataError(f"{CLAUDE_OVERLAY}: {relative} needs a non-empty list of edits") + pairs = [] + for index, entry in enumerate(entries, 1): + if (not isinstance(entry, dict) or set(entry) != {"find", "replace"} + or not isinstance(entry["find"], str) or not entry["find"] + or not isinstance(entry["replace"], str)): + raise MetadataError(f"{CLAUDE_OVERLAY}: {relative}#{index} needs string find and replace") + pairs.append((entry["find"], entry["replace"])) + edits[str(target)] = tuple(pairs) + return ClaudeOverlay(withheld_tools=dict(withheld), edits=edits) diff --git a/scripts/test_contracts.py b/scripts/test_contracts.py index 7c4bd3a..bd1c3ba 100644 --- a/scripts/test_contracts.py +++ b/scripts/test_contracts.py @@ -116,6 +116,38 @@ def test_version_change_propagates_to_generated_manifests(self) -> None: self.assertEqual(marketplace["metadata"]["version"], "9.8.7") +class ClaudeOverlayContractTests(unittest.TestCase): + def setUp(self) -> None: + self.metadata = REPOSITORY_METADATA.load_repository_metadata(ROOT) + self.overlay = REPOSITORY_METADATA.load_claude_overlay(ROOT, self.metadata) + self.canonical = json.loads((ROOT / "packages" / "sent" / "public-surface.json").read_text(encoding="utf-8")) + + def test_claude_surface_is_canonical_minus_withheld(self) -> None: + claude = json.loads((ROOT / "claude-plugins" / "sent" / "public-surface.json").read_text(encoding="utf-8")) + self.assertEqual(set(self.overlay.withheld_tools), {"sender_profiles.create", "sender_profiles.delete"}) + self.assertEqual( + claude["tools"], + {name: tool for name, tool in self.canonical["tools"].items() if name not in self.overlay.withheld_tools}, + ) + + def test_non_claude_surfaces_match_canonical(self) -> None: + for path in (ROOT / "public-surface.json", ROOT / "plugins" / "sent" / "public-surface.json"): + self.assertEqual(json.loads(path.read_text(encoding="utf-8")), self.canonical, path) + + def test_claude_overlay_applies_each_edit_once(self) -> None: + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) + GENERATOR.build(output) + claude = output / "claude-plugins" / "sent" + codex = output / "plugins" / "sent" + for relative, edits in self.overlay.edits.items(): + claude_text = (claude / relative).read_text(encoding="utf-8") + codex_text = (codex / relative).read_text(encoding="utf-8") + for find, replace in edits: + self.assertIn(replace, claude_text, relative) + self.assertIn(find, codex_text, relative) + + class SkillUiMetadataContractTests(unittest.TestCase): def test_every_skill_has_unique_valid_openai_metadata(self) -> None: metadata = REPOSITORY_METADATA.load_repository_metadata(ROOT) diff --git a/scripts/test_validation_gates.py b/scripts/test_validation_gates.py index e807a2b..1e8ed12 100644 --- a/scripts/test_validation_gates.py +++ b/scripts/test_validation_gates.py @@ -12,6 +12,8 @@ from collections.abc import Callable from pathlib import Path +import yaml + ROOT = Path(__file__).resolve().parents[1] Mutation = Callable[[Path], None] @@ -274,7 +276,42 @@ def missing_claude_privacy_url(root: Path) -> None: subprocess.run([sys.executable, str(path)], cwd=root, check=True, capture_output=True, text=True) +def edit_claude_overlay(root: Path, change: Callable[[dict], None]) -> None: + path = root / "adapter-sources/claude/overlay.yaml" + overlay = yaml.safe_load(path.read_text()) + change(overlay) + path.write_text(yaml.safe_dump(overlay, sort_keys=False, allow_unicode=True)) + subprocess.run([sys.executable, str(root / "scripts/generate_adapters.py")], cwd=root, + check=True, capture_output=True, text=True) + + +def stale_claude_overlay_find(root: Path) -> None: + path = root / "packages/sent/skills/sent-compliance/SKILL.md" + path.write_text(path.read_text().replace("The plan creates a new profile.", "The plan creates one new profile.")) + + +def unknown_claude_withheld_tool(root: Path) -> None: + path = root / "adapter-sources/claude/overlay.yaml" + path.write_text(path.read_text().replace( + "withheld_tools:\n", "withheld_tools:\n sender_profiles.purge: Not a real tool.\n", 1)) + + +def removed_claude_confirmation(root: Path) -> None: + edit_claude_overlay(root, lambda overlay: overlay["edits"]["skills/sent-profile-provisioning/SKILL.md"].append( + {"find": "require explicit confirmation immediately before the call", "replace": "ask the user"})) + + +def claude_withheld_tool_instruction(root: Path) -> None: + def change(overlay: dict) -> None: + overlay["edits"]["skills/sent/SKILL.md"][0]["replace"] += " Call `sender_profiles.create` when asked." + edit_claude_overlay(root, change) + + CASES: tuple[tuple[str, Mutation, str], ...] = ( + ("stale Claude overlay edit", stale_claude_overlay_find, "matched 0 times"), + ("unknown Claude withheld tool", unknown_claude_withheld_tool, "is not in the canonical public surface"), + ("Claude retained-tool confirmation", removed_claude_confirmation, "Claude adapter sent-profile-provisioning"), + ("Claude withheld-tool instruction", claude_withheld_tool_instruction, "outside a do-not-call instruction"), ("ambient credential reads", ambient_credential_read, "public gate rejected ambient credential read"), ("Claude privacy policy listing", missing_claude_privacy_url, "Claude privacyPolicyUrl must match"), ("independent mutation confirmation policy", disabled_profile_confirmation, "independent safety policy"), diff --git a/scripts/validate.py b/scripts/validate.py index 426848f..13f8a84 100644 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -17,7 +17,7 @@ import yaml from jsonschema import Draft202012Validator -from repository_metadata import MetadataError, load_repository_metadata, openai_interface +from repository_metadata import CLAUDE_OVERLAY, MetadataError, load_claude_overlay, load_repository_metadata, openai_interface ROOT = Path(__file__).resolve().parents[1] @@ -26,6 +26,7 @@ ROOT_SKILLS = ROOT / "skills" EVALS = ROOT / "evals" ADAPTER_README = ROOT / "adapter-sources" / "shared" / "README.md" +CLAUDE_ADAPTER = ROOT / "claude-plugins" / "sent" MARKETPLACE_CONTENT = ROOT / "adapter-sources" / "shared" / "marketplace.json" SCHEMAS = ROOT / "schemas" / "agent-plugins" / "1.0.0" OPENAI_SUBMISSION_SCHEMA = ROOT / "schemas" / "openai" / "chatgpt-app-submission.v1.json" @@ -549,7 +550,7 @@ def validate_marketplace_content(validation: Validation) -> None: def validate_public_content(validation: Validation) -> None: scan_roots = (PACKAGE, EVALS, ROOT / "adapter-sources" / "claude" / "commands") text_suffixes = {".md", ".yaml", ".yml", ".json", ".py", ".txt", ".csv"} - candidates = list(PUBLIC_ROOT_FILES) + candidates = [*PUBLIC_ROOT_FILES, ROOT / CLAUDE_OVERLAY] candidates.extend(ROOT.glob("docs/*.md")) for scan_root in scan_roots: candidates.extend(scan_root.rglob("*")) @@ -618,32 +619,80 @@ def validate_repository_hygiene(validation: Validation) -> None: validation.check(path.suffix.lower() not in FORBIDDEN_KEY_SUFFIXES, f"repository contains key material: {relative}") -def validate_tool_contract(validation: Validation) -> None: +def validate_tool_contract( + validation: Validation, + skills: Path = SKILLS, + tools_by_owner: dict[str, set[str]] = MCP_SKILLS, + mutation_tools: dict[str, str] = MUTATION_TOOLS, + expected_tools: set[str] = EXPECTED_TOOLS, + label: str = "", +) -> None: advertised: set[str] = set() - for name, tools in MCP_SKILLS.items(): - text = (SKILLS / name / "SKILL.md").read_text(encoding="utf-8") + for name, tools in tools_by_owner.items(): + text = (skills / name / "SKILL.md").read_text(encoding="utf-8") for tool in tools: - validation.check(tool in text, f"{name}: missing documented MCP tool {tool}") + validation.check(tool in text, f"{label}{name}: missing documented MCP tool {tool}") advertised.update(tools) validation.check( - advertised == EXPECTED_TOOLS, - "documented MCP tool ownership must match public-surface.json; " - f"missing={sorted(EXPECTED_TOOLS - advertised)}, unexpected={sorted(advertised - EXPECTED_TOOLS)}", + advertised == expected_tools, + f"{label}documented MCP tool ownership must match public-surface.json; " + f"missing={sorted(expected_tools - advertised)}, unexpected={sorted(advertised - expected_tools)}", ) - for tool, skill in MUTATION_TOOLS.items(): - normalized = (SKILLS / skill / "SKILL.md").read_text(encoding="utf-8").lower() - validation.check("explicit confirmation" in normalized, f"{skill}: {tool} must require explicit confirmation") - validation.check("immediately before" in normalized, f"{skill}: {tool} confirmation must occur immediately before the call") - validation.check("retry" in normalized and "new" in normalized, f"{skill}: {tool} retries must require a new confirmation") + for tool, skill in mutation_tools.items(): + normalized = (skills / skill / "SKILL.md").read_text(encoding="utf-8").lower() + validation.check("explicit confirmation" in normalized, f"{label}{skill}: {tool} must require explicit confirmation") + validation.check("immediately before" in normalized, f"{label}{skill}: {tool} confirmation must occur immediately before the call") + validation.check("retry" in normalized and "new" in normalized, f"{label}{skill}: {tool} retries must require a new confirmation") - messaging = (SKILLS / "sent-messaging" / "SKILL.md").read_text(encoding="utf-8").lower() - validation.check("ambiguous" in messaging and "never" in messaging and "retry" in messaging, "sent-messaging must forbid blind ambiguous retries") - validation.check("balance" in messaging and "high-volume" in messaging, "sent-messaging must check balance before high-volume sends") - validation.check("accepted" in messaging and "delivered" in messaging, "sent-messaging must distinguish accepted from delivered") - analytics = (SKILLS / "sent-analytics" / "SKILL.md").read_text(encoding="utf-8").lower() - validation.check("timezone" in analytics and "date range" in analytics, "sent-analytics must state date range and timezone") - validation.check("consent" in analytics and "capability" in analytics, "sent-analytics must not treat lookup as consent") + messaging = (skills / "sent-messaging" / "SKILL.md").read_text(encoding="utf-8").lower() + validation.check("ambiguous" in messaging and "never" in messaging and "retry" in messaging, f"{label}sent-messaging must forbid blind ambiguous retries") + validation.check("balance" in messaging and "high-volume" in messaging, f"{label}sent-messaging must check balance before high-volume sends") + validation.check("accepted" in messaging and "delivered" in messaging, f"{label}sent-messaging must distinguish accepted from delivered") + analytics = (skills / "sent-analytics" / "SKILL.md").read_text(encoding="utf-8").lower() + validation.check("timezone" in analytics and "date range" in analytics, f"{label}sent-analytics must state date range and timezone") + validation.check("consent" in analytics and "capability" in analytics, f"{label}sent-analytics must not treat lookup as consent") + + +def validate_claude_adapter(validation: Validation) -> None: + """The Claude adapter withholds overlay tools; every other package keeps the full surface.""" + if REPOSITORY_METADATA is None: + return + try: + overlay = load_claude_overlay(ROOT, REPOSITORY_METADATA) + except MetadataError as exc: + validation.errors.append(str(exc)) + return + + canonical = load_json(PACKAGE / "public-surface.json")["tools"] + for path in (ROOT / "public-surface.json", ROOT / "plugins" / "sent" / "public-surface.json"): + validation.check(load_json(path)["tools"] == canonical, f"{path.relative_to(ROOT)} must keep the full canonical MCP surface") + retained = {name: tool for name, tool in canonical.items() if name not in overlay.withheld_tools} + validation.check( + load_json(CLAUDE_ADAPTER / "public-surface.json")["tools"] == retained, + "Claude public-surface.json must equal the canonical surface minus withheld tools", + ) + + tools_by_owner: dict[str, set[str]] = {} + for name, tool in retained.items(): + tools_by_owner.setdefault(tool["owner"], set()).add(name) + mutation_tools = {name: tool["owner"] for name, tool in retained.items() if tool["confirmation_required"]} + validate_tool_contract(validation, CLAUDE_ADAPTER / "skills", tools_by_owner, mutation_tools, set(retained), "Claude adapter ") + + for skill_file in sorted((CLAUDE_ADAPTER / "skills").glob("*/SKILL.md")): + metadata, _ = parse_skill(skill_file, validation) + description = metadata.get("description") + validation.check( + set(metadata) == {"name", "description"} and metadata.get("name") == skill_file.parent.name + and isinstance(description, str) and 1 <= len(description) <= 1024, + f"{skill_file.relative_to(ROOT)}: Claude overlay broke skill frontmatter", + ) + for path in sorted((CLAUDE_ADAPTER / "skills").rglob("*.md")): + for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + if any(tool in line for tool in overlay.withheld_tools) and "do not call" not in line.lower(): + validation.errors.append( + f"{path.relative_to(ROOT)}:{number}: Claude skill names a withheld tool outside a do-not-call instruction" + ) def validate_documentation(validation: Validation) -> None: @@ -1083,6 +1132,7 @@ def main() -> None: validate_contract_content(validation) validate_claude_command_sources(validation) validate_adapters(validation) + validate_claude_adapter(validation) validate_openai_submission(validation) validation.finish() diff --git a/skills/sent-compliance/SKILL.md b/skills/sent-compliance/SKILL.md index db8f609..6776bde 100644 --- a/skills/sent-compliance/SKILL.md +++ b/skills/sent-compliance/SKILL.md @@ -16,7 +16,7 @@ The optional `profileId` selector is available only for an organization's owned ## Interpret requirements and plan 1. Call `compliance.requirements` for the selected market. Preserve required flags, conditional rules, options, item fields, sender pattern, and attachment field names. -2. Check `hasRegime` before interpreting an empty requirements list. An empty list with a published regime can mean no fields are demanded; `hasRegime: false` means no schema is published and is not proof that no compliance is required. +2. Check `hasRegime` before interpreting an empty requirements list. An empty list with a published regime can mean no fields are demanded; `hasRegime: false` means no schema is published and is not proof that no compliance is required. US `TOLL_FREE` numbers require toll-free verification before production traffic, even when no requirements are returned; never tell the user that a toll-free number needs no registration. 3. Call `compliance.setup_plan` when the user needs the exact setup call. Its `call.tool` is `sender_profiles.create`, with camelCase argument names and null placeholders that must be filled before execution. Do not execute blank values or translate the plan into an invented REST call. 4. If `canCompleteHere` is false, report `blocker` and `attachments`, then hand off document submission to the Sent dashboard or the documented multipart REST workflow. Required uploads cannot be carried through MCP; do not attempt a guaranteed-to-fail create. Optional attachment fields alone do not necessarily block creation. 5. When setup can proceed, hand the completed plan to `sent-profile-provisioning` for scope validation, required idempotency key, payload preview, and explicit confirmation before creation. diff --git a/skills/sent-feedback/SKILL.md b/skills/sent-feedback/SKILL.md index 08d3a58..2cd0f0a 100644 --- a/skills/sent-feedback/SKILL.md +++ b/skills/sent-feedback/SKILL.md @@ -1,27 +1,18 @@ --- name: sent-feedback -description: Reports user-authorized feedback about Sent MCP tools to the Sent team. Use when the user asks to report a bug, request a missing feature, pass along confusing results or praise, or agrees to report a tool gap the agent noticed. Also use proactively when a Sent workflow hits meaningful friction the agent or user experienced, such as missing functionality, an error still unresolved after diagnosis, confusing results, repeated workarounds, or user frustration with Sent, to offer an optional report after the original task. Feedback is read by Sent staff; it does not open a support ticket or return an answer. Do not trigger for routine or expected errors, issues unrelated to Sent, a delivery investigation alone, or friction already offered for reporting. +description: Reports user-authorized feedback about Sent MCP tools to the Sent team. Use when the user asks to report a bug, request a missing feature, or pass along a confusing result or praise about Sent. Feedback is read by Sent staff; it does not open a support ticket or return an answer. Do not trigger for routine errors, issues unrelated to Sent, or a delivery investigation alone. --- # Sent Feedback -Use `feedback.send` only when the user asks to pass feedback to Sent or accepts your offer to report friction, and only after they explicitly authorize the exact sanitized report. Never send unsolicited reports, conversation transcripts, or automatic feedback after a tool error. - -## Offer feedback after meaningful friction - -Sent wants to hear about friction even when the user would not think to report it. During any Sent workflow, notice meaningful friction that you or the user hit: missing functionality, an error that persists after diagnosis, confusing or contradictory results, repeated workarounds, or user frustration with Sent. - -1. Diagnose the friction and try to resolve it first with the skill that owns the task. -2. If it remains meaningful, keep the original task first. At a natural pause or after answering, offer once in one sentence, for example: "Sent has no tool to cancel a scheduled send; want me to draft a feedback report for the Sent team?" Never hold the task on the answer. -3. Do not offer for routine errors you resolved, expected restrictions such as validation, permission, compliance, balance, or rate limits working as documented, problems unrelated to Sent, or an issue already offered in this conversation. If the user declines or ignores the offer, drop it. -4. Accepting the offer permits a draft, not a send. Prepare the report below and send only after confirmation. +Use `feedback.send` only when the user asks to pass feedback to Sent, and only after they explicitly authorize the exact sanitized report. Do not solicit reports, and never send conversation transcripts or automatic feedback after a tool error. ## Prepare an authorized report 1. Use client-managed OAuth 2.1/PKCE; never request or expose credentials. Identify the authenticated organization and effective Sender Profile. An organization grant may use the schema's optional `profileId` for an owned profile; a profile grant cannot use that selector. Reauthorize for another organization or a profile outside the grant. 2. Choose `category`: `bug`, `missing_feature`, `confusing`, `praise`, or `other`. 3. Write a plain-text `message` of at most 2000 characters describing the attempted task and observed result. Remove phone numbers, email addresses, message bodies, credentials, and other personal or customer data yourself. Server redaction is pattern-based and is not a substitute for minimizing the report. -4. Optionally set `toolName` to an actual registered tool name. Unknown names are stored as null. Set `source` to `user` when the user wrote or requested the feedback, or `agent` when you noticed the gap or offered the report; omission defaults to `agent`. +4. Optionally set `toolName` to an actual registered tool name. Unknown names are stored as null. Set `source` to `user` when the user wrote the feedback, or `agent` when you drafted it at the user's request; omission defaults to `agent`. 5. Preview the exact scope, category, message, tool name, and source. Explain that Sent staff read the report. Require explicit confirmation immediately before the call; the user's explicit instruction to send this exact preview satisfies that requirement. If the report changes, obtain new confirmation. ## Receipt and limits diff --git a/skills/sent-profile-provisioning/SKILL.md b/skills/sent-profile-provisioning/SKILL.md index 5b8f8e7..dd9d8da 100644 --- a/skills/sent-profile-provisioning/SKILL.md +++ b/skills/sent-profile-provisioning/SKILL.md @@ -16,7 +16,7 @@ These tools operate under the authenticated organization. The target argument is - **List/get:** list uses `page` (1-based) and `pageSize` (1-100, default 20) and reports channel readiness. Use real IDs from that list. Get returns `found`/`item`; `found: false` does not distinguish nonexistent, deleted, and another organization's profile. Do not infer cross-tenant existence. - **Create:** requires `name`, `shortName`, and `idempotencyKey`. `shortName` is 3-11 letters, numbers, or spaces with at least one letter. Optional arguments are `description`, `smsCountry`, `smsNumberType`, `smsSenderValue`, `smsAreaCodes`, and `compliance`. A supplied `smsCountry` requires `smsNumberType`; omission uses the organization's shared routes. For a dedicated SMS market, use `sent-compliance` to inspect requirements and fill its setup plan first. Provisioning can claim a number and incur registration fees. WhatsApp, RCS, billing, document uploads, inheritance flags, and sandbox arguments are not exposed by this MCP create tool. Required document uploads block it before any write; use the dashboard or documented REST flow. No one-time API key is returned. - **Update:** fetch the exact target, then supply `id` and only the changed fields among `name`, `shortName`, and `description`. Omitted fields stay unchanged; explicit `description: null` clears the description. Name and short name cannot be cleared. At least one field is required. Do not populate omitted fields with null or assume this updates channel configuration. -- **Delete:** fetch and preview the target. Explain that deletion can disconnect WhatsApp, release numbers, and disable live routes. Supply `id` and a nonempty `idempotencyKey`, which is required even if the schema marks it optional. `deleted: false` means no matching owned profile was deleted and does not prove another tenant's profile exists. +- **Delete:** fetch and preview the target. Explain that deletion can disconnect WhatsApp, release numbers, and disable live routes. Supply `id` and a nonempty `idempotencyKey`. `deleted: false` means no matching owned profile was deleted and does not prove another tenant's profile exists. For every create, update, or delete, preview the organization, exact target or payload, and side effects, then require explicit confirmation immediately before the call. Changes invalidate confirmation. Every retry needs a new preview and confirmation.