@sellable/mcp 0.1.770-wip.145.20260727141913 → 0.1.770

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/agent-integration-request-context.d.ts +10 -0
  2. package/dist/agent-integration-request-context.js +15 -1
  3. package/dist/agent-scope.d.ts +17 -0
  4. package/dist/agent-scope.js +40 -0
  5. package/dist/agent-tool-names.d.ts +19 -1
  6. package/dist/agent-tool-names.js +42 -4
  7. package/dist/api.d.ts +2 -0
  8. package/dist/api.js +24 -3
  9. package/dist/auth.js +24 -6
  10. package/dist/refill-run-loop.js +156 -6
  11. package/dist/server.js +45 -22
  12. package/dist/tool-result-envelope.d.ts +0 -1
  13. package/dist/tool-result-envelope.js +3 -120
  14. package/dist/tools/auth.js +6 -2
  15. package/dist/tools/campaign-horizon-fill.d.ts +5 -0
  16. package/dist/tools/campaign-horizon-fill.js +7 -3
  17. package/dist/tools/campaign-message-preparation.d.ts +6 -0
  18. package/dist/tools/campaign-message-preparation.js +1 -0
  19. package/dist/tools/campaigns.d.ts +18 -11
  20. package/dist/tools/campaigns.js +22 -9
  21. package/dist/tools/cells.d.ts +10 -2
  22. package/dist/tools/cells.js +12 -4
  23. package/dist/tools/inbox.d.ts +11 -0
  24. package/dist/tools/inbox.js +20 -4
  25. package/dist/tools/integration-connect.d.ts +23 -0
  26. package/dist/tools/integration-connect.js +570 -0
  27. package/dist/tools/integrations.d.ts +69 -0
  28. package/dist/tools/integrations.js +68 -10
  29. package/dist/tools/leads.d.ts +20 -15
  30. package/dist/tools/leads.js +8 -1
  31. package/dist/tools/prompts.d.ts +3 -19
  32. package/dist/tools/prompts.js +2 -24
  33. package/dist/tools/refill-exact-run.d.ts +123 -0
  34. package/dist/tools/{refill-sends-v2.js → refill-exact-run.js} +6 -66
  35. package/dist/tools/{evergreen-refill-plan.d.ts → refill-execution-plan.d.ts} +4 -56
  36. package/dist/tools/{evergreen-refill-plan.js → refill-execution-plan.js} +14 -66
  37. package/dist/tools/refill-executors.d.ts +1 -0
  38. package/dist/tools/refill-executors.js +21 -6
  39. package/dist/tools/refill-sends.d.ts +1398 -1
  40. package/dist/tools/refill-sends.js +80 -27
  41. package/dist/tools/refill-target-plan.js +1 -1
  42. package/dist/tools/registry.d.ts +137 -1058
  43. package/dist/tools/registry.js +5 -13
  44. package/dist/tools/sender-routing.d.ts +5 -1
  45. package/dist/tools/sender-routing.js +6 -1
  46. package/dist/tools/waterfalls.d.ts +5 -0
  47. package/dist/tools/waterfalls.js +5 -1
  48. package/dist/tools/workspaces.d.ts +41 -0
  49. package/dist/tools/workspaces.js +62 -4
  50. package/package.json +2 -1
  51. package/skills/refill-sends/SKILL.md +14 -47
  52. package/skills/refill-sends-workflow/SKILL.md +17 -8
  53. package/skills/refill-sends-workflow/core/flow.v1.json +8 -5
  54. package/dist/tools/refill-sends-v2.d.ts +0 -537
  55. package/dist/tools/refill-v3-eligibility.d.ts +0 -190
  56. package/dist/tools/refill-v3-eligibility.js +0 -217
  57. package/dist/tools/refill-v3-inventory-contract.d.ts +0 -5
  58. package/dist/tools/refill-v3-inventory-contract.js +0 -26
  59. package/dist/tools/refill-v3-inventory.d.ts +0 -555
  60. package/dist/tools/refill-v3-inventory.js +0 -469
  61. package/skills/refill-sends-evergreen/SKILL.md +0 -24
  62. package/skills/refill-sends-evergreen-workflow/SKILL.md +0 -25
  63. package/skills/refill-sends-evergreen-workflow/core/flow.v1.json +0 -259
  64. package/skills/refill-sends-v2/SKILL.md +0 -207
  65. package/skills/refill-sends-v2-workflow/SKILL.md +0 -171
  66. package/skills/refill-sends-v2-workflow/core/flow.v1.json +0 -397
  67. package/skills/research/config.json +0 -9
@@ -38,6 +38,16 @@
38
38
  */
39
39
  export declare const AGENT_INTEGRATION_CONTEXT_TOOL_NAMES: readonly ["integrations_list_tools", "integrations_describe_tool", "integrations_call_tool"];
40
40
  export type AgentIntegrationContextToolName = (typeof AGENT_INTEGRATION_CONTEXT_TOOL_NAMES)[number];
41
+ /**
42
+ * Plan 142-05's connect trio, in a SECOND frozen set rather than appended to the
43
+ * one above: two suites pin `AGENT_INTEGRATION_CONTEXT_TOOL_NAMES` to exactly
44
+ * the three `integrations_*` names 141-06 registers, and those pins are correct
45
+ * — they guard 141's registration, not 142's. `isAgentIntegrationContextTool`
46
+ * spans both sets, so the fail-closed dispatch guard in `server.ts` covers the
47
+ * connect tools with no change to the switch-side plumbing.
48
+ */
49
+ export declare const AGENT_INTEGRATION_CONNECT_TOOL_NAMES: readonly ["search_available_integrations", "start_integration_connection", "get_integration_connection_status"];
50
+ export type AgentIntegrationConnectToolName = (typeof AGENT_INTEGRATION_CONNECT_TOOL_NAMES)[number];
41
51
  /** The trusted actor triple a managed-integration call forwards to the backend. */
42
52
  export type AgentIntegrationRequestContext = {
43
53
  requesterId: string;
@@ -41,9 +41,23 @@ export const AGENT_INTEGRATION_CONTEXT_TOOL_NAMES = Object.freeze([
41
41
  "integrations_describe_tool",
42
42
  "integrations_call_tool",
43
43
  ]);
44
+ /**
45
+ * Plan 142-05's connect trio, in a SECOND frozen set rather than appended to the
46
+ * one above: two suites pin `AGENT_INTEGRATION_CONTEXT_TOOL_NAMES` to exactly
47
+ * the three `integrations_*` names 141-06 registers, and those pins are correct
48
+ * — they guard 141's registration, not 142's. `isAgentIntegrationContextTool`
49
+ * spans both sets, so the fail-closed dispatch guard in `server.ts` covers the
50
+ * connect tools with no change to the switch-side plumbing.
51
+ */
52
+ export const AGENT_INTEGRATION_CONNECT_TOOL_NAMES = Object.freeze([
53
+ "search_available_integrations",
54
+ "start_integration_connection",
55
+ "get_integration_connection_status",
56
+ ]);
44
57
  export const AGENT_INTEGRATION_CONTEXT_REQUIRED_ERROR = "agent_request_context_required";
45
58
  export function isAgentIntegrationContextTool(name) {
46
- return AGENT_INTEGRATION_CONTEXT_TOOL_NAMES.includes(name);
59
+ return (AGENT_INTEGRATION_CONTEXT_TOOL_NAMES.includes(name) ||
60
+ AGENT_INTEGRATION_CONNECT_TOOL_NAMES.includes(name));
47
61
  }
48
62
  /**
49
63
  * Resolves the trusted actor for a managed-integration tool call.
@@ -0,0 +1,17 @@
1
+ export type SellableAgentScope = "WORKSPACE" | "CROSS_WORKSPACE";
2
+ /**
3
+ * Returns the last server-reported affordance scope. An unknown scope fails
4
+ * closed to WORKSPACE; authorization still belongs to the API middleware.
5
+ */
6
+ export declare function getAgentScope(): SellableAgentScope;
7
+ export declare function hasRecordedAgentScope(): boolean;
8
+ export declare function recordAgentScope(value: unknown): SellableAgentScope;
9
+ /**
10
+ * Coalesces the one lazy membership read needed by a fresh Agent session.
11
+ * The loader returns the same response body that carries `agentScope`.
12
+ */
13
+ export declare function ensureAgentScope(loader: () => Promise<{
14
+ agentScope?: unknown;
15
+ }>): Promise<SellableAgentScope>;
16
+ /** Test-only reset; production keeps one server-reported scope per process. */
17
+ export declare function resetAgentScopeForTests(): void;
@@ -0,0 +1,40 @@
1
+ let reportedAgentScope = null;
2
+ let pendingScopeLoad = null;
3
+ function parseAgentScope(value) {
4
+ return value === "WORKSPACE" || value === "CROSS_WORKSPACE" ? value : null;
5
+ }
6
+ /**
7
+ * Returns the last server-reported affordance scope. An unknown scope fails
8
+ * closed to WORKSPACE; authorization still belongs to the API middleware.
9
+ */
10
+ export function getAgentScope() {
11
+ return reportedAgentScope ?? "WORKSPACE";
12
+ }
13
+ export function hasRecordedAgentScope() {
14
+ return reportedAgentScope !== null;
15
+ }
16
+ export function recordAgentScope(value) {
17
+ reportedAgentScope = parseAgentScope(value) ?? "WORKSPACE";
18
+ return reportedAgentScope;
19
+ }
20
+ /**
21
+ * Coalesces the one lazy membership read needed by a fresh Agent session.
22
+ * The loader returns the same response body that carries `agentScope`.
23
+ */
24
+ export async function ensureAgentScope(loader) {
25
+ if (reportedAgentScope)
26
+ return reportedAgentScope;
27
+ if (!pendingScopeLoad) {
28
+ pendingScopeLoad = loader()
29
+ .then((response) => recordAgentScope(response.agentScope))
30
+ .finally(() => {
31
+ pendingScopeLoad = null;
32
+ });
33
+ }
34
+ return pendingScopeLoad;
35
+ }
36
+ /** Test-only reset; production keeps one server-reported scope per process. */
37
+ export function resetAgentScopeForTests() {
38
+ reportedAgentScope = null;
39
+ pendingScopeLoad = null;
40
+ }
@@ -3,4 +3,22 @@
3
3
  * manifests. Keep this free of tool implementation imports so Next can bundle
4
4
  * it; registry parity is enforced in sellable-agent-tool-policy.test.ts.
5
5
  */
6
- export declare const SELLABLE_MCP_TOOL_NAMES: readonly ["add_column", "add_on_demand_leads", "add_rubric_item", "add_teammate", "add_to_comment_campaign", "add_to_connection_campaign", "add_to_inmail_campaign", "archive_campaign", "attach_recommended_sequence", "attach_sequence", "bootstrap_create_campaign", "bootstrap_engage", "bootstrap_engage_multi", "bootstrap_find_leads", "bulk_enrich_with_prospeo", "calculate_linkedin_hook_preview", "cancel_campaign_message_preparation", "cancel_find_leads", "cancel_lead_import", "capture_post_idea", "check_inbox_reply_eligibility", "check_rubric", "commit_blueprint", "complete_sender_research", "confirm_harvest_job_companies", "confirm_lead_list", "confirm_prospeo_company_accounts", "copy_sender_config", "create_campaign", "create_on_demand_campaign", "create_on_demand_table", "create_workflow_table", "create_workspace", "delete_column", "delete_rubric_item", "draft_rubrics", "duplicate_campaign", "enrich_with_prospeo", "export_table_csv", "export_workspace_csv", "fetch_company", "fetch_company_posts", "fetch_linkedin_posts", "fetch_linkedin_profile", "fetch_post_engagers", "fill_campaign_horizon", "get_active_workspace", "get_auth_status", "get_campaign", "get_campaign_context", "get_campaign_framework", "get_campaign_message_preparation_status", "get_campaign_messages_preview", "get_campaign_navigation_state", "get_campaign_refill_state", "get_campaign_table_schema", "get_campaign_waterfall", "get_campaigns", "get_column_schema", "get_company_info", "get_engage_memory", "get_engage_state", "get_engaged_posts", "get_find_leads_run", "get_inbox_thread", "get_linkedin_profile", "get_or_create_direct_campaign_table", "get_post_draft", "get_post_find_leads_scout_registry", "get_post_idea", "get_prospeo_credits", "get_provider_prompt", "get_published_post", "get_refill_plan_v2", "get_refill_target_plan", "get_rows", "get_rows_minimal", "get_runtime_identity", "get_scheduler_fill_capacity", "get_sender", "get_sender_routing", "get_source_scout_registry", "get_subskill_asset", "get_subskill_prompt", "get_table_rows", "get_user_posts", "get_workspace", "import_leads", "init_on_demand_sequence", "integrations_call_tool", "integrations_describe_tool", "integrations_list_tools", "list_column_types", "list_dnc_entries", "list_post_draft_iterations", "list_post_drafts", "list_post_ideas", "list_published_posts", "list_senders", "list_subskill_prompts", "list_tables", "list_workspaces", "load_csv_dnc_entries", "load_csv_domains", "load_csv_linkedin_leads", "lookup_sales_nav_filter", "mark_post_published", "migrate_flat_configs", "pause_campaign", "pause_direct_campaign", "pause_on_demand_campaign", "preflight_find_leads_provider", "prepare_campaign_ab_test", "queue_campaign_cells", "queue_cells", "record_campaign_review_batch", "record_engage_proven_search", "refill_sends", "refill_sends_v2", "refill_v3_sender_date_eligibility", "refill_v3_workspace_inventory", "refresh_paid_inmail_credits", "refresh_sender_engagement", "reissue_find_leads_watch_link", "render_linkedin_post_preview", "reorder_columns", "resolve_campaign_fill_route", "revise_message_template_and_rerun", "run_scheduler_sweep", "save_domain_filters", "save_hook_research", "save_post_draft", "save_rubrics", "search_apollo", "search_engagement_posts", "search_harvest_jobs", "search_inbox_threads", "search_prospeo", "search_prospeo_companies", "search_sales_nav", "search_signals", "search_subskill_prompts", "select_campaign_cells", "select_necessary_rubrics", "select_promising_posts", "send_inbox_draft", "send_inbox_manual_reply", "set_active_workspace", "set_campaign_waterfall_order", "set_company_info", "set_engage_state", "set_engage_style_guide", "set_headline_icp_criteria", "set_sender_calendar_link", "set_sender_routing", "setup_evergreen_campaigns", "start_campaign", "start_campaign_message_preparation", "start_cli_login", "start_direct_campaign", "start_on_demand_campaign", "update_campaign", "update_campaign_brief", "update_cell", "update_column", "update_find_leads_run", "update_inbox_draft", "update_post_draft", "update_published_post_metrics", "update_rubric_item", "upsert_engage_tracked_person", "upsert_rubric", "validate_campaign1_kickoff_handoff", "verify_table_row", "wait_for_campaign_processing", "wait_for_campaign_table_ready", "wait_for_cli_login", "wait_for_find_leads_run", "wait_for_lead_list_ready", "wait_for_rubric_results"];
6
+ export declare const SELLABLE_MCP_TOOL_NAMES: readonly ["add_column", "add_on_demand_leads", "add_rubric_item", "add_teammate", "add_to_comment_campaign", "add_to_connection_campaign", "add_to_inmail_campaign", "archive_campaign", "attach_recommended_sequence", "attach_sequence", "bootstrap_create_campaign", "bootstrap_engage", "bootstrap_engage_multi", "bootstrap_find_leads", "bulk_enrich_with_prospeo", "calculate_linkedin_hook_preview", "cancel_campaign_message_preparation", "cancel_find_leads", "cancel_lead_import", "capture_post_idea", "check_inbox_reply_eligibility", "check_rubric", "commit_blueprint", "complete_sender_research", "confirm_harvest_job_companies", "confirm_lead_list", "confirm_prospeo_company_accounts", "copy_sender_config", "create_campaign", "create_on_demand_campaign", "create_on_demand_table", "create_workflow_table", "create_workspace", "delete_column", "delete_rubric_item", "draft_rubrics", "duplicate_campaign", "enrich_with_prospeo", "export_table_csv", "export_workspace_csv", "fetch_company", "fetch_company_posts", "fetch_linkedin_posts", "fetch_linkedin_profile", "fetch_post_engagers", "fill_campaign_horizon", "get_active_workspace", "get_auth_status", "get_campaign", "get_campaign_context", "get_campaign_framework", "get_campaign_message_preparation_status", "get_campaign_messages_preview", "get_campaign_navigation_state", "get_campaign_refill_state", "get_campaign_table_schema", "get_campaign_waterfall", "get_campaigns", "get_column_schema", "get_company_info", "get_engage_memory", "get_engage_state", "get_engaged_posts", "get_find_leads_run", "get_inbox_thread", "get_integration_connection_status", "get_linkedin_profile", "get_or_create_direct_campaign_table", "get_post_draft", "get_post_find_leads_scout_registry", "get_post_idea", "get_prospeo_credits", "get_provider_prompt", "get_published_post", "get_refill_target_plan", "get_rows", "get_rows_minimal", "get_runtime_identity", "get_scheduler_fill_capacity", "get_sender", "get_sender_routing", "get_source_scout_registry", "get_subskill_asset", "get_subskill_prompt", "get_table_rows", "get_user_posts", "get_workspace", "import_leads", "init_on_demand_sequence", "integrations_call_tool", "integrations_describe_tool", "integrations_list_tools", "list_column_types", "list_dnc_entries", "list_post_draft_iterations", "list_post_drafts", "list_post_ideas", "list_published_posts", "list_senders", "list_subskill_prompts", "list_tables", "list_workspaces", "load_csv_dnc_entries", "load_csv_domains", "load_csv_linkedin_leads", "lookup_sales_nav_filter", "mark_post_published", "migrate_flat_configs", "pause_campaign", "pause_direct_campaign", "pause_on_demand_campaign", "preflight_find_leads_provider", "prepare_campaign_ab_test", "queue_campaign_cells", "queue_cells", "record_campaign_review_batch", "record_engage_proven_search", "refill_sends", "refresh_paid_inmail_credits", "refresh_sender_engagement", "reissue_find_leads_watch_link", "render_linkedin_post_preview", "reorder_columns", "resolve_campaign_fill_route", "revise_message_template_and_rerun", "run_scheduler_sweep", "save_domain_filters", "save_hook_research", "save_post_draft", "save_rubrics", "search_apollo", "search_available_integrations", "search_engagement_posts", "search_harvest_jobs", "search_inbox_threads", "search_prospeo", "search_prospeo_companies", "search_sales_nav", "search_signals", "search_subskill_prompts", "select_campaign_cells", "select_necessary_rubrics", "select_promising_posts", "send_inbox_draft", "send_inbox_manual_reply", "set_active_workspace", "set_campaign_waterfall_order", "set_company_info", "set_engage_state", "set_engage_style_guide", "set_headline_icp_criteria", "set_sender_calendar_link", "set_sender_routing", "setup_evergreen_campaigns", "start_campaign", "start_campaign_message_preparation", "start_cli_login", "start_direct_campaign", "start_integration_connection", "start_on_demand_campaign", "update_campaign", "update_campaign_brief", "update_cell", "update_column", "update_find_leads_run", "update_inbox_draft", "update_post_draft", "update_published_post_metrics", "update_rubric_item", "upsert_engage_tracked_person", "upsert_rubric", "validate_campaign1_kickoff_handoff", "verify_table_row", "wait_for_campaign_processing", "wait_for_campaign_table_ready", "wait_for_cli_login", "wait_for_find_leads_run", "wait_for_lead_list_ready", "wait_for_rubric_results"];
7
+ /**
8
+ * Explicit mutation classification lives beside (but outside) the flat tool
9
+ * registry. Neither this list nor `isMutatingTool` is an input to
10
+ * `compileAgentToolPolicy`, so editing it cannot move the fleet-pinned
11
+ * `policyHash`.
12
+ */
13
+ export declare const MUTATING_TOOL_NAMES: readonly ["pause_campaign", "archive_campaign", "update_campaign", "fill_campaign_horizon", "set_campaign_waterfall_order", "set_sender_routing", "send_inbox_draft", "send_inbox_manual_reply", "load_csv_dnc_entries", "update_cell", "queue_cells"];
14
+ export declare function isMutatingTool(name: string): boolean;
15
+ export declare const CROSS_WORKSPACE_EXPLICIT_WORKSPACE_REQUIRED: {
16
+ readonly code: "cross_workspace_explicit_workspace_required";
17
+ readonly message: "Cross-workspace mutations require an explicit non-empty workspaceId. Name the target workspace and retry.";
18
+ };
19
+ export declare function crossWorkspaceMutationRefusal(input: {
20
+ scope: "WORKSPACE" | "CROSS_WORKSPACE";
21
+ toolName: string;
22
+ workspaceBinding: string | undefined;
23
+ args: unknown;
24
+ }): typeof CROSS_WORKSPACE_EXPLICIT_WORKSPACE_REQUIRED | null;
@@ -69,6 +69,7 @@ export const SELLABLE_MCP_TOOL_NAMES = [
69
69
  "get_engaged_posts",
70
70
  "get_find_leads_run",
71
71
  "get_inbox_thread",
72
+ "get_integration_connection_status",
72
73
  "get_linkedin_profile",
73
74
  "get_or_create_direct_campaign_table",
74
75
  "get_post_draft",
@@ -77,7 +78,6 @@ export const SELLABLE_MCP_TOOL_NAMES = [
77
78
  "get_prospeo_credits",
78
79
  "get_provider_prompt",
79
80
  "get_published_post",
80
- "get_refill_plan_v2",
81
81
  "get_refill_target_plan",
82
82
  "get_rows",
83
83
  "get_rows_minimal",
@@ -122,9 +122,6 @@ export const SELLABLE_MCP_TOOL_NAMES = [
122
122
  "record_campaign_review_batch",
123
123
  "record_engage_proven_search",
124
124
  "refill_sends",
125
- "refill_sends_v2",
126
- "refill_v3_sender_date_eligibility",
127
- "refill_v3_workspace_inventory",
128
125
  "refresh_paid_inmail_credits",
129
126
  "refresh_sender_engagement",
130
127
  "reissue_find_leads_watch_link",
@@ -138,6 +135,7 @@ export const SELLABLE_MCP_TOOL_NAMES = [
138
135
  "save_post_draft",
139
136
  "save_rubrics",
140
137
  "search_apollo",
138
+ "search_available_integrations",
141
139
  "search_engagement_posts",
142
140
  "search_harvest_jobs",
143
141
  "search_inbox_threads",
@@ -164,6 +162,7 @@ export const SELLABLE_MCP_TOOL_NAMES = [
164
162
  "start_campaign_message_preparation",
165
163
  "start_cli_login",
166
164
  "start_direct_campaign",
165
+ "start_integration_connection",
167
166
  "start_on_demand_campaign",
168
167
  "update_campaign",
169
168
  "update_campaign_brief",
@@ -185,3 +184,42 @@ export const SELLABLE_MCP_TOOL_NAMES = [
185
184
  "wait_for_lead_list_ready",
186
185
  "wait_for_rubric_results",
187
186
  ];
187
+ /**
188
+ * Explicit mutation classification lives beside (but outside) the flat tool
189
+ * registry. Neither this list nor `isMutatingTool` is an input to
190
+ * `compileAgentToolPolicy`, so editing it cannot move the fleet-pinned
191
+ * `policyHash`.
192
+ */
193
+ export const MUTATING_TOOL_NAMES = [
194
+ "pause_campaign",
195
+ "archive_campaign",
196
+ "update_campaign",
197
+ "fill_campaign_horizon",
198
+ "set_campaign_waterfall_order",
199
+ "set_sender_routing",
200
+ "send_inbox_draft",
201
+ "send_inbox_manual_reply",
202
+ "load_csv_dnc_entries",
203
+ "update_cell",
204
+ "queue_cells",
205
+ ];
206
+ const MUTATING_TOOL_NAME_SET = new Set(MUTATING_TOOL_NAMES);
207
+ export function isMutatingTool(name) {
208
+ return MUTATING_TOOL_NAME_SET.has(name);
209
+ }
210
+ export const CROSS_WORKSPACE_EXPLICIT_WORKSPACE_REQUIRED = {
211
+ code: "cross_workspace_explicit_workspace_required",
212
+ message: "Cross-workspace mutations require an explicit non-empty workspaceId. Name the target workspace and retry.",
213
+ };
214
+ export function crossWorkspaceMutationRefusal(input) {
215
+ const workspaceId = input.args && typeof input.args === "object"
216
+ ? input.args.workspaceId
217
+ : undefined;
218
+ const explicitWorkspaceNamed = typeof workspaceId === "string" && workspaceId.trim().length > 0;
219
+ return input.scope === "CROSS_WORKSPACE" &&
220
+ input.workspaceBinding !== "" &&
221
+ isMutatingTool(input.toolName) &&
222
+ !explicitWorkspaceNamed
223
+ ? CROSS_WORKSPACE_EXPLICIT_WORKSPACE_REQUIRED
224
+ : null;
225
+ }
package/dist/api.d.ts CHANGED
@@ -35,6 +35,8 @@ export declare class SellableApiError extends Error {
35
35
  constructor(status: number, body: string, guidance?: string);
36
36
  }
37
37
  export declare class SellableApi {
38
+ private readonly explicitWorkspaceContext;
39
+ withWorkspaceId<T>(workspaceId: string | null | undefined, callback: () => Promise<T>): Promise<T>;
38
40
  private buildError;
39
41
  private requestResponse;
40
42
  private request;
package/dist/api.js CHANGED
@@ -3,7 +3,9 @@ import { mkdir, rename, rm, stat } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { Readable } from "node:stream";
5
5
  import { pipeline } from "node:stream/promises";
6
+ import { AsyncLocalStorage } from "node:async_hooks";
6
7
  import { getConfig, getConfigPath, getEffectiveConfiguredWorkspaceId, resolveWorkspaceIdForRequest, } from "./auth.js";
8
+ import { recordAgentScope } from "./agent-scope.js";
7
9
  export class SellableApiError extends Error {
8
10
  status;
9
11
  body;
@@ -19,6 +21,13 @@ export class SellableApiError extends Error {
19
21
  }
20
22
  }
21
23
  export class SellableApi {
24
+ explicitWorkspaceContext = new AsyncLocalStorage();
25
+ async withWorkspaceId(workspaceId, callback) {
26
+ const normalized = typeof workspaceId === "string" && workspaceId.trim()
27
+ ? workspaceId.trim()
28
+ : null;
29
+ return this.explicitWorkspaceContext.run(normalized, callback);
30
+ }
22
31
  buildError(status, errorText) {
23
32
  const isAuthError = status === 401 || status === 403;
24
33
  const isWorkspaceMembershipError = status === 403 && errorText.includes("No access to workspace");
@@ -42,10 +51,16 @@ export class SellableApi {
42
51
  // Re-read config on every request so workspace switches take effect immediately
43
52
  const config = getConfig();
44
53
  const url = `${config.apiUrl}${path}`;
54
+ const contextualWorkspaceId = this.explicitWorkspaceContext.getStore();
55
+ const hasExplicitWorkspace = (options &&
56
+ Object.prototype.hasOwnProperty.call(options, "workspaceId")) ||
57
+ contextualWorkspaceId !== undefined;
45
58
  const requestedWorkspaceId = options && Object.prototype.hasOwnProperty.call(options, "workspaceId")
46
59
  ? options.workspaceId
47
- : getEffectiveConfiguredWorkspaceId(config);
48
- const workspaceId = options && Object.prototype.hasOwnProperty.call(options, "workspaceId")
60
+ : contextualWorkspaceId !== undefined
61
+ ? contextualWorkspaceId
62
+ : getEffectiveConfiguredWorkspaceId(config);
63
+ const workspaceId = hasExplicitWorkspace
49
64
  ? resolveWorkspaceIdForRequest(requestedWorkspaceId, `${method} ${path}`)
50
65
  : requestedWorkspaceId;
51
66
  // Opt-in only: no `signal` is attached unless the caller asked for a bound,
@@ -93,7 +108,13 @@ export class SellableApi {
93
108
  }
94
109
  async request(method, path, body, options) {
95
110
  const response = await this.requestResponse(method, path, body, options);
96
- return response.json();
111
+ const parsed = (await response.json());
112
+ if (parsed &&
113
+ typeof parsed === "object" &&
114
+ Object.prototype.hasOwnProperty.call(parsed, "agentScope")) {
115
+ recordAgentScope(parsed.agentScope);
116
+ }
117
+ return parsed;
97
118
  }
98
119
  async get(path, options) {
99
120
  return this.request("GET", path, undefined, options);
package/dist/auth.js CHANGED
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
3
3
  import * as os from "os";
4
4
  import * as path from "path";
5
5
  import { readMcpAgentServiceContext } from "./agent-service-auth.js";
6
+ import { getAgentScope } from "./agent-scope.js";
6
7
  export const DEFAULT_API_URL = "https://app.sellable.dev";
7
8
  const WORKSPACE_LOCK_ENV = "SELLABLE_LOCK_WORKSPACE_ID";
8
9
  const REQUIRE_WORKSPACE_LOCK_ENV = "SELLABLE_REQUIRE_WORKSPACE_LOCK";
@@ -177,6 +178,11 @@ function normalizeWorkspaceId(value) {
177
178
  }
178
179
  export function getEffectiveConfiguredWorkspaceId(config) {
179
180
  assertWorkspaceLockSatisfied("workspace resolution");
181
+ if (getAgentScope() === "CROSS_WORKSPACE") {
182
+ return (normalizeWorkspaceId(config.activeWorkspaceId) ||
183
+ normalizeWorkspaceId(config.workspaceId) ||
184
+ getLockedWorkspaceId());
185
+ }
180
186
  return (getLockedWorkspaceId() ||
181
187
  normalizeWorkspaceId(config.activeWorkspaceId) ||
182
188
  normalizeWorkspaceId(config.workspaceId));
@@ -187,6 +193,9 @@ export function resolveWorkspaceIdForRequest(requestedWorkspaceId, action = "Sel
187
193
  const requested = normalizeWorkspaceId(requestedWorkspaceId);
188
194
  if (!lockedWorkspaceId)
189
195
  return requested;
196
+ if (getAgentScope() === "CROSS_WORKSPACE") {
197
+ return requested || lockedWorkspaceId;
198
+ }
190
199
  if (requested && requested !== lockedWorkspaceId) {
191
200
  throw new Error(`Workspace lock violation: ${action} requested workspace ${requested}, but this profile is locked to ${lockedWorkspaceId}.`);
192
201
  }
@@ -248,12 +257,20 @@ export function getConfig() {
248
257
  const agent = readMcpAgentServiceContext(process.env, config.token);
249
258
  if (!agent)
250
259
  throw new Error("Invalid Agent service context");
251
- const configuredWorkspace = normalizeWorkspaceId(config.activeWorkspaceId) ||
252
- normalizeWorkspaceId(config.workspaceId);
253
- if (configuredWorkspace && configuredWorkspace !== agent.workspaceId) {
254
- throw new Error("Agent service credential workspace binding mismatch");
260
+ const configuredActiveWorkspace = normalizeWorkspaceId(selectedRaw.activeWorkspaceId);
261
+ const configuredWorkspace = normalizeWorkspaceId(selectedRaw.workspaceId);
262
+ if (getAgentScope() === "CROSS_WORKSPACE") {
263
+ config.activeWorkspaceId =
264
+ configuredActiveWorkspace || agent.workspaceId;
265
+ }
266
+ else {
267
+ const configuredIdentity = configuredActiveWorkspace || configuredWorkspace;
268
+ if (configuredIdentity &&
269
+ configuredIdentity !== agent.workspaceId) {
270
+ throw new Error("Agent service credential workspace binding mismatch");
271
+ }
272
+ config.activeWorkspaceId = agent.workspaceId;
255
273
  }
256
- config.activeWorkspaceId = agent.workspaceId;
257
274
  config.workspaceId = agent.workspaceId;
258
275
  config.credentialKind = "agent_service";
259
276
  }
@@ -298,7 +315,8 @@ export function getConfiguredCredentialIntent() {
298
315
  }
299
316
  }
300
317
  export function updateActiveWorkspace(params) {
301
- if (getConfig().credentialKind === "agent_service") {
318
+ if (getConfig().credentialKind === "agent_service" &&
319
+ getAgentScope() !== "CROSS_WORKSPACE") {
302
320
  throw new Error("Agent service profiles cannot switch workspaces");
303
321
  }
304
322
  resolveWorkspaceIdForRequest(params.workspaceId, "set_active_workspace");
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { SellableApiError } from "./api.js";
3
3
  import { decodeRefillReadinessAuthority, refillReadinessExpectationInput, refillReadinessFingerprint, } from "./refill-readiness.js";
4
4
  import { normalizeMessageTemplateRevision, normalizeRefillRunHandle, normalizeRefillTargetConfig, } from "./tools/campaign-processing.js";
5
- import { buildRunStateFromLocalHints } from "./tools/evergreen-refill-plan.js";
5
+ import { buildRunStateFromLocalHints } from "./tools/refill-execution-plan.js";
6
6
  import { classifyPaidInmailRefreshReceipt, prepareRowSelectorValue as defaultPrepareRowSelectorValue, refillPrepareRequestHash as defaultRefillPrepareRequestHash, validateAgentSuppliedBroadenKeywords, } from "./tools/refill-executors.js";
7
7
  import { sanitizeRefillReportingContextProjection, sanitizeRefillReportingProjection, } from "./tools/refill-target-plan.js";
8
8
  /**
@@ -699,7 +699,7 @@ function isSelectedSenderOwnedPostEngagersLane(plan, action) {
699
699
  stringValue(actionToolInput(action).campaignName) ??
700
700
  stringValue(targetCampaign?.campaignName) ??
701
701
  stringValue(targetCampaign?.name);
702
- // Exact v2 packets retain target.campaigns identity even when a plain
702
+ // Exact refill packets retain target.campaigns identity even when a plain
703
703
  // active-campaign route omits the optional evergreen lane projection.
704
704
  // Prefer that authority so a resumed g3 checkpoint never falls through to
705
705
  // the generic agent-keyword gate for a sender-owned Post Engagers campaign.
@@ -893,6 +893,39 @@ function runScopedSignalDiscoveryAttemptUpdate(action, runState, now) {
893
893
  ],
894
894
  };
895
895
  }
896
+ function runScopedSignalSourceValidationUpdate(action, runState, now) {
897
+ const toolInput = actionToolInput(action);
898
+ const provider = stringValue(toolInput.leadSourceProvider);
899
+ if (provider !== "signal-discovery" && provider !== "campaign-tracked-post") {
900
+ return {};
901
+ }
902
+ const campaignId = actionCampaignId(action);
903
+ const payload = resultPayload(runState.lastOutcome);
904
+ const reviewBatch = recordValue(payload.reviewBatch);
905
+ const reviewBatchBasisHash = stringValue(reviewBatch?.basisHash);
906
+ const reviewBatchRowCount = numberValue(reviewBatch?.rowCount);
907
+ if (!campaignId || !reviewBatchBasisHash || !reviewBatchRowCount)
908
+ return {};
909
+ const attempts = arrayValue(runState.signalSourceValidationAttempts).filter(isRecord);
910
+ return {
911
+ signalSourceValidationAttempts: [
912
+ ...attempts.filter((attempt) => stringValue(attempt.campaignId) !== campaignId),
913
+ {
914
+ campaignId,
915
+ tableId: actionTableId(action),
916
+ senderId: actionSenderId(action),
917
+ sourceLeadListId: stringValue(toolInput.sourceLeadListId),
918
+ sourceFingerprint: actionSourceFingerprint(action),
919
+ reviewBatchBasisHash,
920
+ reviewBatchRowCount,
921
+ threshold: 0.1,
922
+ status: "pending",
923
+ copiedAt: now.toISOString(),
924
+ scope: "refill_run",
925
+ },
926
+ ],
927
+ };
928
+ }
896
929
  // Phase 110-05: Sales Nav / Prospeo same-source continuation gets exactly one
897
930
  // bounded add per (campaign, sender) per run, mirroring the Signal Discovery
898
931
  // run-scoped attempt bound so the loop never repeats an unchanged failed
@@ -2413,7 +2446,7 @@ async function gateBootstrap(input, deps, ctx) {
2413
2446
  input.reportingContext = issuedReportingContext;
2414
2447
  }
2415
2448
  const pinnedRunState = pinInitialSenderLaneChains(plan, ctx.runState);
2416
- // Exact fenced plans are projected by projectedV2Packet(), whose legacy
2449
+ // Exact fenced plans are projected by projectedRefillExecutionPacket(), whose
2417
2450
  // bootstrap.paidCredit array is intentionally empty. The canonical refresh
2418
2451
  // authority is packet.target.globalActionQueue; reading the bootstrap array
2419
2452
  // minted `{ version: 2, receipts: [] }` on every run, which made the next
@@ -2524,7 +2557,7 @@ async function gateBootstrap(input, deps, ctx) {
2524
2557
  })
2525
2558
  : runStateWithoutCreditTrust;
2526
2559
  await safeJournalAppend(ctx, deps, deps.journal.renderBootstrapSection({
2527
- summary: "Refill v2 execution bootstrap",
2560
+ summary: "Refill exact-run execution bootstrap",
2528
2561
  senderSummary: safeJson(recordValue(plan.bootstrap)?.senders ?? []),
2529
2562
  laneSummary: safeJson(recordValue(plan.bootstrap)?.laneOrder ?? []),
2530
2563
  targetSummary: safeJson(recordValue(plan.bootstrap)?.target ?? []),
@@ -4444,8 +4477,66 @@ async function gateVerify(input, deps, ctx, budgets) {
4444
4477
  return advanceBackToPlan(input, deps, ctx);
4445
4478
  }
4446
4479
  const actionType = stringValue(action.type);
4447
- const schedulerOutcome = resultPayload(ctx.runState.lastOutcome);
4448
- const schedulerOutcomeStatus = stringValue(schedulerOutcome.status);
4480
+ let schedulerOutcome = resultPayload(ctx.runState.lastOutcome);
4481
+ let schedulerOutcomeStatus = stringValue(schedulerOutcome.status);
4482
+ // HOTFIX 2026-07-27: executeOneYoloPrimitive deliberately returns
4483
+ // matching_scheduler_active when the exact request key is still running
4484
+ // after its bounded dispatch-site settle. g3_verify previously ignored that
4485
+ // status, reread the ready-buffer plan immediately, and terminalized the
4486
+ // exact fence as loaded_awaiting_scheduler while the scheduler was still
4487
+ // working. That released the workspace coordinator before the request's
4488
+ // official receipt existed, so a later cron/read could show 30/30 even
4489
+ // though the refill command itself stopped at 25/30.
4490
+ //
4491
+ // Keep this exact fence at g3_verify and poll through the SAME action. The
4492
+ // executor's status preflight is request-key scoped and never redispatches a
4493
+ // matching active request. When the per-invocation wait budget is reached,
4494
+ // heartbeat and return the same runId/fence continuation; never convert an
4495
+ // active request into loaded_awaiting_scheduler.
4496
+ if (actionType === "run_scheduler_sweep" &&
4497
+ schedulerOutcomeStatus === "matching_scheduler_active") {
4498
+ for (let poll = 0; poll < budgets.maxPollsPerWait; poll += 1) {
4499
+ const did = await deps.executors.executeOneYoloPrimitive(action, input.workspaceId);
4500
+ schedulerOutcome = resultPayload(did);
4501
+ schedulerOutcomeStatus = stringValue(schedulerOutcome.status);
4502
+ await deps.runClient.recordRunOutcome({
4503
+ workspaceId: input.workspaceId,
4504
+ ...exactRunTarget(ctx),
4505
+ runId: ctx.runId,
4506
+ fence: ctx.fence,
4507
+ eventId: lastEventId(ctx),
4508
+ did,
4509
+ outcome: did,
4510
+ });
4511
+ ctx.runState = mergeRunState(ctx.runState, {
4512
+ lastOutcome: did,
4513
+ progress: mergeProgress(ctx, {
4514
+ schedulerRequestLastCheckedAt: (deps.now?.() ?? new Date()).toISOString(),
4515
+ schedulerRequestStatus: schedulerOutcomeStatus ?? "scheduler_status_unreadable",
4516
+ }),
4517
+ });
4518
+ if (schedulerOutcomeStatus !== "matching_scheduler_active")
4519
+ break;
4520
+ const waitResult = await sleepForWait(input, deps, ctx, budgets);
4521
+ if (waitResult) {
4522
+ if (waitResult.status !== "in_progress")
4523
+ return waitResult;
4524
+ return {
4525
+ ...waitResult,
4526
+ guidance: "The exact workspace scheduler request is still running. Re-invoke this same runId/fence; verification will poll the same request key until its official terminal receipt, without redispatching it.",
4527
+ };
4528
+ }
4529
+ }
4530
+ if (schedulerOutcomeStatus === "matching_scheduler_active") {
4531
+ const continuation = await heartbeatAndContinue(input, deps, ctx);
4532
+ if (continuation.status !== "in_progress")
4533
+ return continuation;
4534
+ return {
4535
+ ...continuation,
4536
+ guidance: "The exact workspace scheduler request is still running. Re-invoke this same runId/fence; verification will poll the same request key until its official terminal receipt, without redispatching it.",
4537
+ };
4538
+ }
4539
+ }
4449
4540
  if (actionType === "run_scheduler_sweep" &&
4450
4541
  (schedulerOutcomeStatus === "uncertain_outcome" ||
4451
4542
  schedulerOutcomeStatus === "scheduler_receipt_incomplete")) {
@@ -4469,6 +4560,57 @@ async function gateVerify(input, deps, ctx, budgets) {
4469
4560
  guidance: "The exact scheduler request has no authoritative terminal receipt. The request key was not redispatched; use a fresh planner-issued action key after scheduler reconciliation.",
4470
4561
  });
4471
4562
  }
4563
+ const terminalSchedulerReceipt = actionType === "run_scheduler_sweep"
4564
+ ? schedulerStatusTerminalReceipt(schedulerOutcome)
4565
+ : null;
4566
+ if (actionType === "run_scheduler_sweep" && terminalSchedulerReceipt) {
4567
+ // The scheduler request is now officially done. Only this post-receipt
4568
+ // authoritative reread may decide whether the refill target completed.
4569
+ // A still-loaded plan is not success: surface a concrete retryable blocker
4570
+ // with the terminal receipt instead of claiming loaded while leaving the
4571
+ // operator unable to distinguish a scheduler miss from an active run.
4572
+ const fresh = await deps.readPlan(refillPlanReadInput(input, { ctx, journal: false }));
4573
+ ctx.plan = fresh;
4574
+ const rawDoneReason = doneReasonFromPlan(fresh);
4575
+ const normalizedDoneReason = rawDoneReason
4576
+ ? normalizeDoneReason(rawDoneReason)
4577
+ : null;
4578
+ if (canonicalReportingCoverageComplete(fresh) ||
4579
+ normalizedDoneReason === "complete" ||
4580
+ normalizedDoneReason === "capped_by_scheduler") {
4581
+ return completeTerminal(input, deps, ctx, normalizedDoneReason ?? "complete", {
4582
+ plan: fresh,
4583
+ schedulerRunReceipt: terminalSchedulerReceipt,
4584
+ });
4585
+ }
4586
+ const freshHead = firstAction(fresh);
4587
+ if (normalizedDoneReason === "loaded_awaiting_scheduler" ||
4588
+ stringValue(freshHead?.type) === "wait_for_scheduler") {
4589
+ return completeTerminal(input, deps, ctx, "blocked", {
4590
+ blocker: "scheduler_terminal_with_remaining_gap",
4591
+ retryable: true,
4592
+ plan: fresh,
4593
+ schedulerRunReceipt: terminalSchedulerReceipt,
4594
+ requestKey: stringValue(actionToolInput(action).requestKey),
4595
+ actionKey: stringValue(action.actionKey),
4596
+ targetDate: stringValue(actionToolInput(action).targetDate) ?? input.targetDate,
4597
+ coverage: coverageSnapshot(fresh),
4598
+ guidance: "The exact workspace scheduler request produced an official terminal receipt, but the post-receipt target plan still has a projected gap. Do not report loaded_awaiting_scheduler; retry only from a fresh planner-issued scheduler action after the receipt's named capacity, window, cooldown, or eligibility condition changes.",
4599
+ });
4600
+ }
4601
+ if (rawDoneReason) {
4602
+ return completeTerminal(input, deps, ctx, rawDoneReason, {
4603
+ plan: fresh,
4604
+ schedulerRunReceipt: terminalSchedulerReceipt,
4605
+ });
4606
+ }
4607
+ return advanceBackToPlan(input, deps, ctx, {
4608
+ lastOutcome: ctx.runState.lastOutcome,
4609
+ progress: mergeProgress(ctx, {
4610
+ schedulerTerminalReceiptObservedAt: (deps.now?.() ?? new Date()).toISOString(),
4611
+ }),
4612
+ });
4613
+ }
4472
4614
  if (!ctx.plan &&
4473
4615
  typeof actionType === "string" &&
4474
4616
  BROADEN_SIGNAL_SEARCH_TYPES.has(actionType)) {
@@ -4513,6 +4655,14 @@ async function gateVerify(input, deps, ctx, budgets) {
4513
4655
  // keep going — instead of terminalizing the invite lane with confirmed-fillable
4514
4656
  // slots still open. Once a broadening round proves the universe dry this run, this
4515
4657
  // no longer intercepts and the refusal terminalizes typed below.
4658
+ if (actionType === "copy_selected_source_rows" &&
4659
+ stringValue(recordValue(ctx.runState.lastOutcome)?.status) ===
4660
+ "executed_and_reread") {
4661
+ return advanceBackToPlan(input, deps, ctx, {
4662
+ ...runScopedSignalSourceValidationUpdate(action, ctx.runState, deps.now?.() ?? new Date()),
4663
+ ...clearRunScopedDrySourcePrepAttempts(actionCampaignId(action), ctx.runState),
4664
+ });
4665
+ }
4516
4666
  if (actionType === "continue_signal_discovery_source" &&
4517
4667
  isSignalSelectionBroadenableRefusal(ctx.runState.lastOutcome) &&
4518
4668
  !hasRunScopedSignalUniverseExhausted(action, ctx.runState)) {