@sprigr/apps-app-sdk 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -99,8 +99,9 @@ Three platform tiers, and they are not substitutes for each other. Full guide: [
99
99
  - `undoEnvelope({...})` throws on a blank field. The platform drops the whole envelope on a blank and mints nothing, silently.
100
100
  - `refuseWithoutForce(name, handler, opts)` / `archiveOfferRefusal(...)`: a delete refuses once and offers the vendor's native archive unless `force: true`.
101
101
  - `buildConfirmationPolicy({ irreversible, always, rules })` declares a dispatcher's policy beside its action registry; `checkConfirmationPolicy({ policy, registry, ungated, requiredInput })` returns every silent failure (dead rule, `(unset)` placeholder, money threshold, unclassified write) for a one-line test.
102
+ - **Ask once when T1 and T2 both gate an action.** Pass `approval: { covered, conditional }` to `buildConfirmationPolicy` (or `deriveConfirmationPolicy`) and to `checkConfirmationPolicy`: `covered` is your approval spec keys (`Object.keys(APPROVAL_SPECS)`), `conditional` the covered actions your app sends through the gate only under a condition of its own (simpro's `GATE_ONLY_WHEN`), `[]` when there are none. Both are required. Each covered, unconditional, gated action gets `attended: 'approval_card'`, and the platform then skips the confirmation on a turn with a person on it, so the card is the only prompt; unattended runs still confirm. The builder never marks a conditional action, and the check fails a key that no spec covers or that sits on a conditional action (no prompt at all on a call that skips the card), a covered unconditional action without one (it asks twice), and a `conditional` entry with no spec. For flat `requireApproval` tools, run `attendedFindings(tool, rule, { covered, conditional })` over each tool-level rule. Needs a platform with sprigr-team decision 0167; an older one ignores the key and keeps asking twice.
102
103
  - `runUndoApply(args, { env, journal, specs, pin })` is the body of your `internal: true` reverse tool: load, re-pin to the journalled connection, restore, drop. `pin` may hand `restore` a different type from the env (a per-actor token state), may throw (`connection_unavailable`) or return null to refuse (`connection_mismatch`); a restore that throws is `restore_failed` and the copy is kept; a successful restore may return `newId`, `notRestored`, `extra` (spread into the payload) and `note` (appended after the fidelity sentence).
103
104
  - `safeCapture(scope, name, id, read)` and `offerUndo({ journal, entity, id, before, connection, fidelity, resource, describe, warning })`: the capture-then-offer halves for a write that is not behind an approval card (a dispatcher `update_*` action). `requireApproval` uses the same two on its granted pass.
104
- - `sprigr-check-write-protection` (bin): every destructive-by-name tool or enumerated dispatcher action must carry a policy, or a reason in `apps/<app>/write-protection.json`; ratchets on `tools/write-protection-baseline.json`.
105
+ - `sprigr-check-write-protection` (bin): every destructive-by-name tool or enumerated dispatcher action must carry a policy, or a reason in `apps/<app>/write-protection.json`; ratchets on `tools/write-protection-baseline.json`. It also refuses an `attended` key with any value other than `'approval_card'`, or on a rule that gates nothing (never baselined).
105
106
 
106
107
  **Never offer `_undo` for money or messages.** Refunds, captures, cancellations, sent mail: the approval gate is the only control there.
@@ -93,6 +93,33 @@ export function scanManifest(manifest) {
93
93
  return out;
94
94
  }
95
95
 
96
+ /**
97
+ * Malformed `attended` keys (sprigr-team decision 0167), as `tool` or
98
+ * `tool:action` findings. Not baselined: no app may ship one.
99
+ *
100
+ * A manifest cannot say whether an approval card really covers the action,
101
+ * because the specs are code. That half is `checkConfirmationPolicy` /
102
+ * `attendedFindings` with `approvalCovered`, in the app's own test. What a
103
+ * manifest CAN show is a key the platform would ignore (any value but
104
+ * 'approval_card', so the person is quietly asked twice) and a key on a rule
105
+ * that gates nothing (so there is no confirmation for it to step aside from,
106
+ * which means it was written by hand).
107
+ */
108
+ export function scanAttended(manifest) {
109
+ const out = [];
110
+ const check = (label, rule) => {
111
+ if (!rule || typeof rule !== 'object' || !('attended' in rule)) return;
112
+ if (rule.attended !== 'approval_card') out.push(`${label}: attended must be 'approval_card', got ${JSON.stringify(rule.attended)}`);
113
+ else if (!isGatedRule(rule)) out.push(`${label}: attended on a rule with no always or when, so it gates nothing`);
114
+ };
115
+ for (const tool of manifest.tools || []) {
116
+ if (!tool || !tool.confirmation || typeof tool.confirmation !== 'object') continue;
117
+ check(tool.name, tool.confirmation);
118
+ for (const [a, rule] of Object.entries(tool.confirmation.actions || {})) check(`${tool.name}:${a}`, rule);
119
+ }
120
+ return out;
121
+ }
122
+
96
123
  /** Apply the app's allowlist. Returns { violations, stale, unreasoned }. */
97
124
  export function applyAllowlist(violations, allowlist) {
98
125
  const allow = (allowlist && allowlist.allow) || {};
@@ -151,6 +178,7 @@ export function run(argv, cwd = process.cwd()) {
151
178
  const allowlist = existsSync(allowPath) ? readJson(allowPath) : null;
152
179
  const raw = scanManifest(manifest);
153
180
  const { violations, stale, unreasoned } = applyAllowlist(raw, allowlist);
181
+ for (const f of scanAttended(manifest)) failures.push(`${slug}: ${f}`);
154
182
  for (const k of unreasoned) failures.push(`${slug}: write-protection.json allow entry "${k}" has no reason`);
155
183
  for (const k of stale) failures.push(`${slug}: write-protection.json allow entry "${k}" is stale (no longer destructive-and-ungated); remove it`);
156
184
 
package/dist/index.d.ts CHANGED
@@ -1453,6 +1453,18 @@ interface ConfirmRule {
1453
1453
  describe?: string;
1454
1454
  /** Appends "which cannot be undone" so the model cannot undersell it. */
1455
1455
  irreversible?: boolean;
1456
+ /**
1457
+ * `'approval_card'`: this action's human-approval card (an approval spec)
1458
+ * covers it whenever a person is on the turn, so the platform skips the
1459
+ * confirmation there and the person is asked once, by the card. Unattended
1460
+ * runs still confirm. Never hand-write it: pass `approval: { covered,
1461
+ * conditional }` to `buildConfirmationPolicy` / `deriveConfirmationPolicy`
1462
+ * so it follows the specs, and to `checkConfirmationPolicy` so a stale or
1463
+ * conditionally gated key fails the test.
1464
+ * Needs a platform with sprigr-team decision 0167; an older one ignores it
1465
+ * and keeps asking twice.
1466
+ */
1467
+ attended?: 'approval_card';
1456
1468
  }
1457
1469
  interface ConfirmCondition {
1458
1470
  count: string;
@@ -2021,6 +2033,34 @@ interface PolicySource {
2021
2033
  always?: Record<string, string>;
2022
2034
  /** Full rules, for thresholds and anything the two maps above cannot express. */
2023
2035
  rules?: Record<string, ConfirmRule>;
2036
+ /**
2037
+ * Which actions an approval card covers; see {@link ApprovalCoverage}. Each
2038
+ * covered action that also has a rule here, and is not `conditional`, gets
2039
+ * `attended: 'approval_card'`, so the person is asked once on an attended
2040
+ * turn (the card) instead of twice.
2041
+ */
2042
+ approval?: ApprovalCoverage;
2043
+ }
2044
+ /**
2045
+ * Which actions an approval card covers, for deriving and checking
2046
+ * `attended: 'approval_card'` (sprigr-team decision 0167).
2047
+ *
2048
+ * Both fields are required on purpose. The SDK can see an app's approval
2049
+ * specs but not any condition the app wraps around the gate (simpro sends its
2050
+ * deletes to the gate only with `force: true`), and an action marked attended
2051
+ * whose call skips the card would skip the confirmation too: no prompt at all.
2052
+ * So every app states its conditional actions, `[]` when it has none, and the
2053
+ * builder and the check refuse to mark them.
2054
+ */
2055
+ interface ApprovalCoverage {
2056
+ /** Every action with an approval spec: the keys of the specs object you hand to the gate. */
2057
+ covered: Iterable<string>;
2058
+ /**
2059
+ * Covered actions the app sends through the gate only under a condition of
2060
+ * its own (simpro's `GATE_ONLY_WHEN`). Never marked attended; they keep the
2061
+ * confirmation. Pass `[]` when every covered action always reaches the gate.
2062
+ */
2063
+ conditional: Iterable<string>;
2024
2064
  }
2025
2065
  /** Build a sorted `{ actions }` policy. An action named in two groups throws. */
2026
2066
  declare function buildConfirmationPolicy(src: PolicySource): ConfirmationPolicy;
@@ -2053,7 +2093,23 @@ interface PolicyCheckInput {
2053
2093
  */
2054
2094
  nestedUnderInput?: boolean;
2055
2095
  moneyFields?: readonly string[];
2096
+ /**
2097
+ * Which actions an approval card covers. With it, the check fails a rule
2098
+ * carrying `attended: 'approval_card'` on an action no spec covers or that
2099
+ * the app gates conditionally (either way the key removes the person's only
2100
+ * prompt), a covered, unconditional, gated action that lacks the key (it asks
2101
+ * twice), and a `conditional` entry with no spec. Without it, any `attended`
2102
+ * key is itself a finding, because nothing can vouch for it.
2103
+ */
2104
+ approval?: ApprovalCoverage;
2056
2105
  }
2106
+ /**
2107
+ * Findings for one rule's `attended` key against the approval coverage.
2108
+ * Exported for apps whose gated tools are flat (`requireApproval`) rather than
2109
+ * one dispatcher: run it over each tool-level rule with the tool names the
2110
+ * specs cover and the ones gated conditionally.
2111
+ */
2112
+ declare function attendedFindings(name: string, rule: ConfirmRule, coverage: ApprovalCoverage | null): string[];
2057
2113
  /**
2058
2114
  * Every way a policy can be wrong without erroring, as a list of findings.
2059
2115
  * An app's test is `expect(checkConfirmationPolicy(...)).toEqual([])`.
@@ -2233,6 +2289,8 @@ interface DerivePolicyOptions {
2233
2289
  exempt?: readonly string[];
2234
2290
  /** Explicit rules that win over the verb table (e.g. a hand-written describe). */
2235
2291
  overrides?: Readonly<Record<string, ConfirmRule>>;
2292
+ /** Which actions an approval card covers; see `ApprovalCoverage`. */
2293
+ approval?: ApprovalCoverage;
2236
2294
  }
2237
2295
  interface DerivedPolicy {
2238
2296
  policy: ConfirmationPolicy & {
@@ -3149,4 +3207,4 @@ interface ExecutionContextLike {
3149
3207
  }
3150
3208
  type HandlerFn<TArgs, TResult> = (args: TArgs, env: Record<string, unknown>, ctx?: ExecutionContextLike) => Promise<TResult>;
3151
3209
 
3152
- export { ACTOR_RETIRED_HOOK_TOOL, ACTOR_RETIRE_REASONS, APPROVAL_GRANTED_KEY, APPROVAL_PREVIEW_MAX, APP_FILES_ROOT, APP_OWNER_SEGMENT, type Actor, type ActorRetireReason, type ActorRetired, type ActorRetiredHookBody, type ActorRetiredResult, type AppApprovalEnvelope, type AppFileListItem, type AppFileUrlArgs, type AppFileUrlResult, type AppFilesEnv, type AppToolEffectsDeclaration, type AppUndoEnvelope, type ApplyPoliciesOptions, type ApprovalGateOptions, type ApprovalSpec, type ArchiveOfferOptions, type ArchiveOfferRefusal, type CallerKeyResolution, type CaptureJournal, type ConfirmCondition, type ConfirmRule, type ConfirmationPolicy, type ConnectCompleteRequest, type ConnectCompleteResult, type D1Like, type D1PreparedStatementLike, DATA_PARTIAL_UPDATE_PATH, DEFAULT_EMIT_TIMEOUT_MS, DEFAULT_LOG_TIMEOUT_MS, DEFAULT_MAX_FILE_BYTES, DEFAULT_MONEY_FIELDS, DEFAULT_WRITE_PREFIXES, type DerivePolicyOptions, type DerivedPolicy, type DispatcherApprovalGate, type DispatcherApprovalOptions, type EmitMarketplaceEventOptions, type EmitResult, type EmitTransport, type EventArgs, type ExecutionContextLike, type FetchFileResult, type FetchWithRetryOptions, type GetAppFileResult, type HandlerFn, INBOX_SEND_CONTRACT_V1, type InboxAddress, type InboxAppendRequest, type InboxAppendResult, type InboxChannelDeclaration, type InboxFoldersRequest, type InboxFoldersResult, type InboxOwner, type InboxSendConnection, type InboxSendHandlerImpl, InboxSendRefusal, type InboxSendSupports, type InboxSendV1Args, type InboxSendV1Result, type InstallBridge, InvalidUndoEnvelopeError, type JournalRowLike, type LogToPlatformOptions, type ManifestDispatchBlock, type ManifestLike, type ManifestToolLike, type MarketplaceEventEmitOpts, type MarketplaceEventSourceIntegration, NotConnectedError, type OAuthBouncerSignals, type OAuthState, type OfferUndoArgs, type PartialUpdateDataOptions, type PlatformHostEnv, type PolicyCheckInput, type PolicySource, type PutAppFileArgs, type PutAppFileResult, type PutAppFileStreamArgs, type RequireApprovalOptions, type ResolveViewerOptions, type RestoreResult, type RestoreSpec, SPRIGR_DATA_MAX_OBJECTS_PER_CALL, SPRIGR_LOG_LEVELS, SPRIGR_LOG_LIMITS, type ScheduleArgs, type SprigrDataAggregationCell, type SprigrDataAggregationResult, type SprigrDataAggregationSpec, type SprigrDataApi, type SprigrDataImportOpts, type SprigrDataIndexOpts, type SprigrDataPartialUpdateBody, type SprigrDataPartialUpdateOpts, type SprigrDataPartialUpdateResult, type SprigrDataPatch, type SprigrDataSearchOpts, type SprigrDataSearchResult, SprigrDataValidationError, type SprigrFilesApi, type SprigrFilesApiWithEdit, type SprigrFilesCreateInput, type SprigrFilesCreateResult, type SprigrFilesEditInput, type SprigrFilesEditResult, type SprigrFilesJobResult, type SprigrFilesListItem, type SprigrFilesListResult, type SprigrInboxApi, type SprigrLogEntry, type SprigrLogFn, type SprigrLogLevel, type SprigrLogResult, SprigrLogValidationError, type SyncedAttachment, type SyncedMessage, type ToolArgs, type ToolHandler, type ToolResult, type ToolWrapperOptions, UNIT_SEP, type UndoApplyOptions, type UndoApplyResult, type UndoCaptureSpec, type UndoFidelity, VIEWER_ANONYMOUS, VIEWER_CONTEXT_HEADER, VIEWER_PLATFORM_USER_ID_HEADER, VIEWER_ROLE_HEADER, VIEWER_USER_ID_HEADER, type VerbRule, type ViewerContext, type ViewerContextFailure, type ViewerContextVerification, type ViewerEnv, type WebhookArgs, type WfpBridgeEnv, type WfpEmitReply, actorKey, actorRetiredHook, actorTool, agentFileKeyFromUrl, appFileUrl, appKeyFromCallerKey, applyConfirmationPolicies, approvalHash, approvalPreview, approvalRecipients, archiveOfferRefusal, base64ToBytes, buildConfirmationPolicy, buildMarketplaceWebhookUrl, buildPartialUpdateBody, bytesToBase64, callerKeyRefusal, canEmit, canLog, canPartialUpdate, checkConfirmationPolicy, commonDestructiveVerbs, connectComplete, connectDisconnect, constantTimeEqual, createMarketplaceEmitter, createToolWrappers, decodeState, defineInboxSendHandler, deleteAppFile, deriveConfirmationPolicy, describeMissingBridge, dispatcherApproval, emitMarketplaceEvent, encodeState, fetchFileAsBase64, fetchFileBytes, fetchWithRetry, fullWarning, getAppFile, hmacSha256Hex, installTokenPost, isNotConnectedError, isStagingHost, listAppFiles, logToPlatform, manifestIsFresh, offerUndo, overlaySprigr, ownerFromActorKey, parseActor, parseActorRetiredArgs, partialUpdateData, putAppFile, putAppFileStream, randomHex, readableStoredKey, recreatedWarning, refuseWithoutForce, requireApproval, resolveInstallBridge, resolveOAuthBouncerBase, resolveOAuthBouncerCallbackUrl, resolvePlatformWebhookBase, resolveViewerContext, resolveViewerUserId, runUndoApply, safeCapture, seq, serializeManifest, set, tool, undoEnvelope, validateLogEntries, validateLogEntry, verifyViewerToken, withSprigrEmitFallback, withSprigrInboxFallback, withSprigrLogFallback };
3210
+ export { ACTOR_RETIRED_HOOK_TOOL, ACTOR_RETIRE_REASONS, APPROVAL_GRANTED_KEY, APPROVAL_PREVIEW_MAX, APP_FILES_ROOT, APP_OWNER_SEGMENT, type Actor, type ActorRetireReason, type ActorRetired, type ActorRetiredHookBody, type ActorRetiredResult, type AppApprovalEnvelope, type AppFileListItem, type AppFileUrlArgs, type AppFileUrlResult, type AppFilesEnv, type AppToolEffectsDeclaration, type AppUndoEnvelope, type ApplyPoliciesOptions, type ApprovalCoverage, type ApprovalGateOptions, type ApprovalSpec, type ArchiveOfferOptions, type ArchiveOfferRefusal, type CallerKeyResolution, type CaptureJournal, type ConfirmCondition, type ConfirmRule, type ConfirmationPolicy, type ConnectCompleteRequest, type ConnectCompleteResult, type D1Like, type D1PreparedStatementLike, DATA_PARTIAL_UPDATE_PATH, DEFAULT_EMIT_TIMEOUT_MS, DEFAULT_LOG_TIMEOUT_MS, DEFAULT_MAX_FILE_BYTES, DEFAULT_MONEY_FIELDS, DEFAULT_WRITE_PREFIXES, type DerivePolicyOptions, type DerivedPolicy, type DispatcherApprovalGate, type DispatcherApprovalOptions, type EmitMarketplaceEventOptions, type EmitResult, type EmitTransport, type EventArgs, type ExecutionContextLike, type FetchFileResult, type FetchWithRetryOptions, type GetAppFileResult, type HandlerFn, INBOX_SEND_CONTRACT_V1, type InboxAddress, type InboxAppendRequest, type InboxAppendResult, type InboxChannelDeclaration, type InboxFoldersRequest, type InboxFoldersResult, type InboxOwner, type InboxSendConnection, type InboxSendHandlerImpl, InboxSendRefusal, type InboxSendSupports, type InboxSendV1Args, type InboxSendV1Result, type InstallBridge, InvalidUndoEnvelopeError, type JournalRowLike, type LogToPlatformOptions, type ManifestDispatchBlock, type ManifestLike, type ManifestToolLike, type MarketplaceEventEmitOpts, type MarketplaceEventSourceIntegration, NotConnectedError, type OAuthBouncerSignals, type OAuthState, type OfferUndoArgs, type PartialUpdateDataOptions, type PlatformHostEnv, type PolicyCheckInput, type PolicySource, type PutAppFileArgs, type PutAppFileResult, type PutAppFileStreamArgs, type RequireApprovalOptions, type ResolveViewerOptions, type RestoreResult, type RestoreSpec, SPRIGR_DATA_MAX_OBJECTS_PER_CALL, SPRIGR_LOG_LEVELS, SPRIGR_LOG_LIMITS, type ScheduleArgs, type SprigrDataAggregationCell, type SprigrDataAggregationResult, type SprigrDataAggregationSpec, type SprigrDataApi, type SprigrDataImportOpts, type SprigrDataIndexOpts, type SprigrDataPartialUpdateBody, type SprigrDataPartialUpdateOpts, type SprigrDataPartialUpdateResult, type SprigrDataPatch, type SprigrDataSearchOpts, type SprigrDataSearchResult, SprigrDataValidationError, type SprigrFilesApi, type SprigrFilesApiWithEdit, type SprigrFilesCreateInput, type SprigrFilesCreateResult, type SprigrFilesEditInput, type SprigrFilesEditResult, type SprigrFilesJobResult, type SprigrFilesListItem, type SprigrFilesListResult, type SprigrInboxApi, type SprigrLogEntry, type SprigrLogFn, type SprigrLogLevel, type SprigrLogResult, SprigrLogValidationError, type SyncedAttachment, type SyncedMessage, type ToolArgs, type ToolHandler, type ToolResult, type ToolWrapperOptions, UNIT_SEP, type UndoApplyOptions, type UndoApplyResult, type UndoCaptureSpec, type UndoFidelity, VIEWER_ANONYMOUS, VIEWER_CONTEXT_HEADER, VIEWER_PLATFORM_USER_ID_HEADER, VIEWER_ROLE_HEADER, VIEWER_USER_ID_HEADER, type VerbRule, type ViewerContext, type ViewerContextFailure, type ViewerContextVerification, type ViewerEnv, type WebhookArgs, type WfpBridgeEnv, type WfpEmitReply, actorKey, actorRetiredHook, actorTool, agentFileKeyFromUrl, appFileUrl, appKeyFromCallerKey, applyConfirmationPolicies, approvalHash, approvalPreview, approvalRecipients, archiveOfferRefusal, attendedFindings, base64ToBytes, buildConfirmationPolicy, buildMarketplaceWebhookUrl, buildPartialUpdateBody, bytesToBase64, callerKeyRefusal, canEmit, canLog, canPartialUpdate, checkConfirmationPolicy, commonDestructiveVerbs, connectComplete, connectDisconnect, constantTimeEqual, createMarketplaceEmitter, createToolWrappers, decodeState, defineInboxSendHandler, deleteAppFile, deriveConfirmationPolicy, describeMissingBridge, dispatcherApproval, emitMarketplaceEvent, encodeState, fetchFileAsBase64, fetchFileBytes, fetchWithRetry, fullWarning, getAppFile, hmacSha256Hex, installTokenPost, isNotConnectedError, isStagingHost, listAppFiles, logToPlatform, manifestIsFresh, offerUndo, overlaySprigr, ownerFromActorKey, parseActor, parseActorRetiredArgs, partialUpdateData, putAppFile, putAppFileStream, randomHex, readableStoredKey, recreatedWarning, refuseWithoutForce, requireApproval, resolveInstallBridge, resolveOAuthBouncerBase, resolveOAuthBouncerCallbackUrl, resolvePlatformWebhookBase, resolveViewerContext, resolveViewerUserId, runUndoApply, safeCapture, seq, serializeManifest, set, tool, undoEnvelope, validateLogEntries, validateLogEntry, verifyViewerToken, withSprigrEmitFallback, withSprigrInboxFallback, withSprigrLogFallback };
package/dist/index.js CHANGED
@@ -1367,6 +1367,13 @@ async function runUndoApply(args, opts) {
1367
1367
  }
1368
1368
 
1369
1369
  // src/write-protection/confirmation-policy.ts
1370
+ function coverageSets(c) {
1371
+ if (!c) return null;
1372
+ if (c.covered == null || c.conditional == null) {
1373
+ throw new Error("approval coverage needs both `covered` and `conditional` (pass [] when no action is gated conditionally)");
1374
+ }
1375
+ return { covered: new Set(c.covered), conditional: new Set(c.conditional) };
1376
+ }
1370
1377
  function buildConfirmationPolicy(src) {
1371
1378
  const actions = {};
1372
1379
  const put = (name, rule, group) => {
@@ -1376,6 +1383,10 @@ function buildConfirmationPolicy(src) {
1376
1383
  for (const [name, describe] of Object.entries(src.irreversible ?? {})) put(name, { always: true, describe, irreversible: true }, "irreversible");
1377
1384
  for (const [name, describe] of Object.entries(src.always ?? {})) put(name, { always: true, describe }, "always");
1378
1385
  for (const [name, rule] of Object.entries(src.rules ?? {})) put(name, rule, "rules");
1386
+ const cov = coverageSets(src.approval);
1387
+ for (const name of cov?.covered ?? []) {
1388
+ if (actions[name] && !cov?.conditional.has(name)) actions[name] = { ...actions[name], attended: "approval_card" };
1389
+ }
1379
1390
  return { actions: Object.fromEntries(Object.entries(actions).sort(([a], [b]) => a.localeCompare(b))) };
1380
1391
  }
1381
1392
  var DEFAULT_WRITE_PREFIXES = [
@@ -1479,6 +1490,22 @@ var PLACEHOLDER = /\{([a-zA-Z0-9_.]+)\}/g;
1479
1490
  function conditions(rule) {
1480
1491
  return !rule.when ? [] : Array.isArray(rule.when) ? rule.when : [rule.when];
1481
1492
  }
1493
+ function attendedFindings(name, rule, coverage) {
1494
+ const cov = coverageSets(coverage ?? void 0);
1495
+ const out = [];
1496
+ const attended = rule.attended;
1497
+ const gated = rule.always === true || conditions(rule).length > 0;
1498
+ if (attended !== void 0 && attended !== "approval_card") {
1499
+ out.push(`${name}: attended must be 'approval_card' (got ${JSON.stringify(attended)}); the platform ignores anything else`);
1500
+ } else if (attended === "approval_card") {
1501
+ if (!cov) out.push(`${name}: attended: 'approval_card' cannot be checked; pass approval: { covered, conditional }`);
1502
+ else if (!cov.covered.has(name)) out.push(`${name}: attended: 'approval_card' but no approval spec covers it, so it removes the only prompt`);
1503
+ else if (cov.conditional.has(name)) out.push(`${name}: attended: 'approval_card' on a conditionally gated action, so a call that skips the card gets no prompt at all`);
1504
+ } else if (cov?.covered.has(name) && !cov.conditional.has(name) && gated) {
1505
+ out.push(`${name}: an approval spec covers it but the rule lacks attended: 'approval_card', so it asks twice`);
1506
+ }
1507
+ return out;
1508
+ }
1482
1509
  function checkConfirmationPolicy(input) {
1483
1510
  const registry = new Set(input.registry);
1484
1511
  const ungated = input.ungated ?? [];
@@ -1486,8 +1513,13 @@ function checkConfirmationPolicy(input) {
1486
1513
  const nested = input.nestedUnderInput ?? true;
1487
1514
  const money = new Set(input.moneyFields ?? DEFAULT_MONEY_FIELDS);
1488
1515
  const actions = input.policy.actions ?? {};
1516
+ const cov = coverageSets(input.approval);
1489
1517
  const out = [];
1518
+ for (const name of cov?.conditional ?? []) {
1519
+ if (!cov?.covered.has(name)) out.push(`approval.conditional: "${name}" has no approval spec (not in covered)`);
1520
+ }
1490
1521
  for (const [name, rule] of Object.entries(actions)) {
1522
+ out.push(...attendedFindings(name, rule, input.approval ?? null));
1491
1523
  if (!registry.has(name)) out.push(`dead rule: "${name}" is not in the registry, so it gates nothing`);
1492
1524
  if (!rule.always && conditions(rule).length === 0) out.push(`${name}: rule says nothing (no always, no when)`);
1493
1525
  if (!rule.describe || !rule.describe.trim()) out.push(`${name}: describe missing`);
@@ -1596,7 +1628,7 @@ function deriveConfirmationPolicy(registry, opts) {
1596
1628
  }
1597
1629
  if (!hit) unmatched.push(name);
1598
1630
  }
1599
- const policy = buildConfirmationPolicy({ irreversible, always, rules: overrides });
1631
+ const policy = buildConfirmationPolicy({ irreversible, always, rules: overrides, approval: opts.approval });
1600
1632
  return { policy, ungated, unmatched };
1601
1633
  }
1602
1634
  function commonDestructiveVerbs(subject) {
@@ -1833,6 +1865,7 @@ export {
1833
1865
  approvalPreview,
1834
1866
  approvalRecipients,
1835
1867
  archiveOfferRefusal,
1868
+ attendedFindings,
1836
1869
  base64ToBytes,
1837
1870
  buildConfirmationPolicy,
1838
1871
  buildMarketplaceWebhookUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprigr/apps-app-sdk",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",