@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 +2 -1
- package/bin/check-write-protection.mjs +28 -0
- package/dist/index.d.ts +59 -1
- package/dist/index.js +34 -1
- package/package.json +1 -1
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,
|