@sellable/mcp 0.1.559 → 0.1.560

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.
@@ -1027,6 +1027,23 @@ async function gatePlan(input, deps, ctx) {
1027
1027
  fallbackSummary: line.fallbackSummary,
1028
1028
  }));
1029
1029
  const fingerprint = fingerprintFromPlan(plan);
1030
+ const approvedTargetShapeRevision = stringValue(input.approval?.targetShapeRevision);
1031
+ if (approvedTargetShapeRevision &&
1032
+ approvedTargetShapeRevision !== fingerprint.targetShapeRevision) {
1033
+ return {
1034
+ status: "blocked",
1035
+ blocker: "scope_drift",
1036
+ runId: ctx.runId,
1037
+ fence: ctx.fence,
1038
+ gate: ctx.gate,
1039
+ guidance: "The canonical refill target shape changed after approval; render a new packet before another primitive.",
1040
+ report: {
1041
+ approvedTargetShapeRevision,
1042
+ freshTargetShapeRevision: fingerprint.targetShapeRevision,
1043
+ },
1044
+ journalPath: ctx.journalPath,
1045
+ };
1046
+ }
1030
1047
  const persistedCircuit = recordValue(runStateProgress(ctx).prepCircuit);
1031
1048
  if (stringValue(action.type) === "prepare_messages" &&
1032
1049
  openCircuitWaits(persistedCircuit, deps.now?.() ?? new Date())) {
@@ -167,6 +167,17 @@ export declare function prepareRefillSendsV2Approval(input: RefillV2ApprovalPrep
167
167
  response?: undefined;
168
168
  }>;
169
169
  export declare function refillSendsV2Command(input: RefillSendsV2Input): Promise<{
170
+ status: "blocked";
171
+ blocker: "approval_expired" | "approval_fingerprint_mismatch";
172
+ guidance: string;
173
+ report: {
174
+ fresh?: {} | null | undefined;
175
+ changed: string[];
176
+ };
177
+ readOnly: boolean;
178
+ forbiddenActions: string[];
179
+ boundedAuthority: string;
180
+ } | {
170
181
  readOnly: boolean;
171
182
  mode: string;
172
183
  forbiddenActions: string[];
@@ -325,7 +325,11 @@ function validateApprovalEcho(envelope, echo, now = new Date()) {
325
325
  ];
326
326
  const changed = fields.filter((field) => echo[field] !== envelope.approval[field]);
327
327
  if (Date.parse(envelope.approval.approvalExpiresAt) <= now.getTime()) {
328
- return { ok: false, blocker: "approval_expired", changed };
328
+ return {
329
+ ok: false,
330
+ blocker: "approval_expired",
331
+ changed,
332
+ };
329
333
  }
330
334
  return changed.length === 0
331
335
  ? { ok: true }
@@ -335,6 +339,85 @@ function validateApprovalEcho(envelope, echo, now = new Date()) {
335
339
  changed,
336
340
  };
337
341
  }
342
+ function blockedApprovalResult(params) {
343
+ return {
344
+ status: "blocked",
345
+ blocker: params.blocker,
346
+ guidance: "The durable refill approval no longer matches its persisted scope; render and approve a new packet before mutation.",
347
+ report: {
348
+ changed: params.changed,
349
+ ...(params.fresh === undefined ? {} : { fresh: params.fresh }),
350
+ },
351
+ readOnly: false,
352
+ forbiddenActions: FORBIDDEN_ACTIONS,
353
+ boundedAuthority: BOUNDED_AUTHORITY,
354
+ };
355
+ }
356
+ function storedApprovalFromStatus(value) {
357
+ const status = record(value) ?? {};
358
+ const config = record(status.config);
359
+ const scope = record(config?.scope);
360
+ const approval = record(config?.approval);
361
+ const scopeHash = text(config?.scopeHash);
362
+ if (!config || !scope || !approval || !scopeHash)
363
+ return null;
364
+ return {
365
+ config,
366
+ scope,
367
+ scopeHash,
368
+ approval,
369
+ hasDispatched: status.hasDispatched === true,
370
+ };
371
+ }
372
+ function validateStoredApprovalEcho(params) {
373
+ const fields = [
374
+ "scopeHash",
375
+ "targetShapeRevision",
376
+ "actionFingerprint",
377
+ "expectedTargetsHash",
378
+ "approvalExpiresAt",
379
+ "approvalFingerprint",
380
+ ];
381
+ const changed = fields.filter((field) => text(params.approval[field]) !== params.echo[field]);
382
+ const expiresAt = text(params.approval.approvalExpiresAt);
383
+ if (!params.allowExpired &&
384
+ (!expiresAt ||
385
+ Date.parse(expiresAt) <= (params.now ?? new Date()).getTime())) {
386
+ return {
387
+ ok: false,
388
+ blocker: "approval_expired",
389
+ changed,
390
+ };
391
+ }
392
+ return changed.length === 0
393
+ ? { ok: true }
394
+ : {
395
+ ok: false,
396
+ blocker: "approval_fingerprint_mismatch",
397
+ changed,
398
+ };
399
+ }
400
+ function executionScopeFromStored(scope, fallback) {
401
+ const dateSelector = record(scope.dateSelector) ?? {};
402
+ const kind = text(dateSelector.kind);
403
+ const senderIds = list(scope.senderIds)
404
+ .map((value) => text(value))
405
+ .filter((value) => value != null);
406
+ const approvalMode = scope.approvalMode === "mark_ready" ? "mark_ready" : "approve";
407
+ return {
408
+ intent: text(scope.intent) ?? fallback.intent,
409
+ senderIds,
410
+ approvalMode,
411
+ ...(kind === "target_date" && text(dateSelector.targetDate)
412
+ ? { targetDate: text(dateSelector.targetDate) }
413
+ : kind === "until_date" && text(dateSelector.untilDate)
414
+ ? { untilDate: text(dateSelector.untilDate) }
415
+ : kind === "horizon_send_days" &&
416
+ typeof dateSelector.horizonSendDays === "number"
417
+ ? { horizonSendDays: dateSelector.horizonSendDays }
418
+ : {}),
419
+ };
420
+ }
338
421
  async function maybeAddLostFenceGuidance(result, input) {
339
422
  if (result.status !== "blocked" ||
340
423
  result.blocker !== "lease_lost" ||
@@ -365,7 +448,53 @@ export async function refillSendsV2Command(input) {
365
448
  let resolvedScope = input.scope;
366
449
  let resolvedScopeHash = input.scopeHash;
367
450
  let resolvedApproval = input.approval;
368
- if (input.targetPlan && input.approvalEcho) {
451
+ if (input.runId && input.approvalEcho) {
452
+ const status = await refillRunStatus({ workspaceId, runId: input.runId });
453
+ const stored = storedApprovalFromStatus(status);
454
+ if (!stored) {
455
+ return {
456
+ status: "blocked",
457
+ blocker: "approval_run_unavailable",
458
+ guidance: "The durable refill run no longer exposes a valid persisted approval packet.",
459
+ report: { status },
460
+ readOnly: false,
461
+ forbiddenActions: FORBIDDEN_ACTIONS,
462
+ boundedAuthority: BOUNDED_AUTHORITY,
463
+ };
464
+ }
465
+ const validation = validateStoredApprovalEcho({
466
+ approval: stored.approval,
467
+ echo: input.approvalEcho,
468
+ allowExpired: stored.hasDispatched,
469
+ });
470
+ if (!validation.ok) {
471
+ return blockedApprovalResult({
472
+ blocker: validation.blocker,
473
+ changed: validation.changed,
474
+ fresh: stored.approval,
475
+ });
476
+ }
477
+ const freshTargetShapeRevision = input.targetPlan
478
+ ? (text(input.targetPlan.targetShapeRevision) ??
479
+ text(record(input.targetPlan.target)?.targetShapeRevision))
480
+ : null;
481
+ const storedTargetShapeRevision = text(stored.approval.targetShapeRevision);
482
+ if (freshTargetShapeRevision &&
483
+ freshTargetShapeRevision !== storedTargetShapeRevision) {
484
+ return blockedApprovalResult({
485
+ blocker: "approval_fingerprint_mismatch",
486
+ changed: ["targetShapeRevision"],
487
+ fresh: {
488
+ storedTargetShapeRevision,
489
+ freshTargetShapeRevision,
490
+ },
491
+ });
492
+ }
493
+ resolvedScope = stored.scope;
494
+ resolvedScopeHash = stored.scopeHash;
495
+ resolvedApproval = stored.approval;
496
+ }
497
+ else if (input.targetPlan && input.approvalEcho) {
369
498
  const envelope = buildApprovalEnvelope({
370
499
  workspaceId,
371
500
  intent: input.intent,
@@ -379,15 +508,11 @@ export async function refillSendsV2Command(input) {
379
508
  });
380
509
  const validation = validateApprovalEcho(envelope, input.approvalEcho);
381
510
  if (!validation.ok) {
382
- return {
383
- status: "blocked",
511
+ return blockedApprovalResult({
384
512
  blocker: validation.blocker,
385
- guidance: "The durable refill approval no longer matches fresh scope; render and approve a new packet before mutation.",
386
- report: { changed: validation.changed, fresh: envelope.approval },
387
- readOnly: false,
388
- forbiddenActions: FORBIDDEN_ACTIONS,
389
- boundedAuthority: BOUNDED_AUTHORITY,
390
- };
513
+ changed: validation.changed,
514
+ fresh: envelope.approval,
515
+ });
391
516
  }
392
517
  resolvedScope = envelope.scope;
393
518
  resolvedScopeHash = envelope.scopeHash;
@@ -409,13 +534,18 @@ export async function refillSendsV2Command(input) {
409
534
  boundedAuthority: BOUNDED_AUTHORITY,
410
535
  };
411
536
  }
537
+ const executionScope = resolvedScope
538
+ ? executionScopeFromStored(resolvedScope, input)
539
+ : {
540
+ intent: input.intent,
541
+ senderIds: input.senderIds,
542
+ approvalMode: input.approvalMode ?? "approve",
543
+ ...date,
544
+ };
412
545
  const result = await runRefillV2Loop({
413
546
  workspaceId,
414
- intent: input.intent,
415
- senderIds: input.senderIds,
416
- approvalMode: input.approvalMode ?? "approve",
547
+ ...executionScope,
417
548
  resume: resumeHandle(input),
418
- ...date,
419
549
  scope: resolvedScope,
420
550
  scopeHash: resolvedScopeHash,
421
551
  approval: resolvedApproval,
@@ -725,6 +725,23 @@ export declare function executeRefillSendsCommand(input?: RefillSendsCommandInpu
725
725
  intent: RefillSendsIntent;
726
726
  } | {
727
727
  yoloExecution: {
728
+ selectedAction: Record<string, unknown> | null;
729
+ targetPlanReread: boolean;
730
+ status: "blocked";
731
+ blocker: "approval_expired" | "approval_fingerprint_mismatch";
732
+ guidance: string;
733
+ report: {
734
+ fresh?: {} | null | undefined;
735
+ changed: string[];
736
+ };
737
+ readOnly: boolean;
738
+ forbiddenActions: string[];
739
+ boundedAuthority: string;
740
+ enabled: boolean;
741
+ result?: undefined;
742
+ refusalReason?: undefined;
743
+ postActionFirstAction?: undefined;
744
+ } | {
728
745
  selectedAction: Record<string, unknown> | null;
729
746
  targetPlanReread: boolean;
730
747
  readOnly: boolean;
@@ -1,6 +1,6 @@
1
+ import { REFILL_CONTRACT_CACHE_VERSION, REFILL_CONTRACT_HASH, REFILL_CONTRACT_VERSION, renderRefillBootstrapGuidance, renderRefillSafetyGuidance, renderRefillTerminalGuidance, } from "../refill-contract.js";
1
2
  import { actionIds, classifyPaidInmailRefreshReceipt, collectPaidInmailRefreshActions, executeOneYoloPrimitive, firstGlobalAction, normalizeStrings, numberValue, recordValue, refreshPaidInmailCreditsWithRetry, stringValue, } from "./refill-executors.js";
2
3
  import { prepareRefillSendsV2Approval, refillSendsV2Command, } from "./refill-sends-v2.js";
3
- import { REFILL_CONTRACT_CACHE_VERSION, REFILL_CONTRACT_HASH, REFILL_CONTRACT_VERSION, renderRefillBootstrapGuidance, renderRefillSafetyGuidance, renderRefillTerminalGuidance, } from "../refill-contract.js";
4
4
  import { getRefillTargetPlan } from "./refill-target-plan.js";
5
5
  import { createWorkspaceContext, normalizeExplicitWorkspaceId, } from "./workspace-context.js";
6
6
  const REFILL_SEND_ACTION_TYPES = ["send_invite", "send_inmail_closed"];
@@ -563,8 +563,13 @@ export async function executeRefillSendsCommand(input = {}) {
563
563
  }
564
564
  const targetPlanInput = targetPlanInputFor(scopedInput);
565
565
  const targetPlanBeforePaidRefresh = await getRefillTargetPlan(targetPlanInput);
566
+ const durableResumeRequested = Boolean(yolo &&
567
+ workspaceId &&
568
+ scopedInput.runId &&
569
+ typeof scopedInput.fence === "number" &&
570
+ scopedInput.approvalFingerprint);
566
571
  const approvedPacket = approvedPacketComparison(scopedInput, targetPlanBeforePaidRefresh);
567
- if (approvedPacket.changed) {
572
+ if (!durableResumeRequested && approvedPacket.changed) {
568
573
  return {
569
574
  ...command,
570
575
  ok: false,
@@ -595,7 +600,8 @@ export async function executeRefillSendsCommand(input = {}) {
595
600
  }
596
601
  if (yolo &&
597
602
  workspaceId &&
598
- durableV2PlanEligible(targetPlanBeforePaidRefresh)) {
603
+ (durableResumeRequested ||
604
+ durableV2PlanEligible(targetPlanBeforePaidRefresh))) {
599
605
  const base = durableYoloBaseResult({
600
606
  command,
601
607
  targetPlan: targetPlanBeforePaidRefresh,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.559",
3
+ "version": "0.1.560",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code, Codex, and Hermes campaign workflows",
6
6
  "main": "dist/index.js",