@humanagencyp/hap-core 0.10.1 → 0.11.1

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/dist/index.d.mts CHANGED
@@ -205,10 +205,29 @@ type BoundType =
205
205
  * Per-transaction cap. The execution context field named in `of` must
206
206
  * satisfy `execution[of] <= bound` for the current call. No cumulative
207
207
  * tracking. Used by: amount_max, recipient_max, booking_duration_max, etc.
208
+ *
209
+ * `requiredFor` (v0.8+) closes the converse: without it, a call that does
210
+ * not carry `of` at all is simply skipped — a 5,000 cap refuses 6,000 but
211
+ * permits a call that declares no value whatsoever, which is indistinguishable
212
+ * from an unenforced bound. The same hole, and the same fix, as
213
+ * `FieldConstraint.requiredFor` on a scope constraint (below): values drawn
214
+ * from the profile's `boundsSchema.actionTypes` registry. For a listed
215
+ * `action_type`, an execution whose context lacks `of` — or whose value is
216
+ * not a finite number — MUST be refused rather than skipped. For an
217
+ * unlisted (or when `requiredFor` is absent) action type, today's
218
+ * skip-on-absence behaviour is unchanged. Meaningful only on
219
+ * `per_transaction`; the other `BoundType` kinds have no per-call "value
220
+ * exposed or not" question — `cumulative_sum`/`cumulative_count` always read
221
+ * from the execution log regardless of what this call declares, and `enum`
222
+ * is a capability flag, not a runtime value — so the field does not exist
223
+ * on those variants and a profile that puts it there has it silently
224
+ * ignored by every enforcement point (TypeScript also refuses it when the
225
+ * profile is authored against this type rather than raw JSON).
208
226
  */
209
227
  {
210
228
  kind: 'per_transaction';
211
229
  of: string;
230
+ requiredFor?: string[];
212
231
  }
213
232
  /**
214
233
  * Cumulative sum within a time window. The SP maintains a running sum
@@ -448,6 +467,11 @@ interface ExecutionPath {
448
467
  *
449
468
  * Supports both v0.3 (frameSchema) and v0.4 (boundsSchema + contextSchema).
450
469
  */
470
+ /**
471
+ * A commitment mode a person can choose when signing a mandate. (`review_above_cap`
472
+ * is not chosen by the signer — it follows from team caps.)
473
+ */
474
+ type SignableCommitmentMode = 'automatic' | 'review';
451
475
  interface AgentProfile {
452
476
  id: string;
453
477
  name?: string;
@@ -489,6 +513,18 @@ interface AgentProfile {
489
513
  * a forwarded message has usually lost the footer that carried it.
490
514
  */
491
515
  receipt_lookup?: boolean;
516
+ /**
517
+ * The commitment modes a mandate under this profile may be signed with.
518
+ * Absent → every mode (`automatic`, `review`), as before this field existed.
519
+ *
520
+ * Lets a profile be review-only: every action it governs then needs a
521
+ * person's approval, whatever the signer would have picked. The Authority
522
+ * Server refuses to sign a mandate whose `commitment_mode` is not listed;
523
+ * a mandate screen offers only the listed modes. Fail-closed: a declared
524
+ * list with no valid entry allows nothing — a malformed declaration never
525
+ * widens what a profile allows. See {@link allowedCommitmentModes}.
526
+ */
527
+ commitment_modes?: readonly SignableCommitmentMode[];
492
528
  /**
493
529
  * v0.3 frame schema (deprecated, kept for backward compat).
494
530
  * Used when boundsSchema is not present.
@@ -503,6 +539,16 @@ interface AgentProfile {
503
539
  boundsSchema?: {
504
540
  keyOrder: string[];
505
541
  fields: Record<string, ProfileBoundsField>;
542
+ /**
543
+ * v0.5+ registry of the action types this profile recognizes (e.g.
544
+ * `["send", "delete", "setup"]`). Every `execution.action_type` the
545
+ * Authority Server and gateway accept for this profile MUST be a member
546
+ * when the registry is declared (protocol.md → Bounds Schema, rule 2);
547
+ * it is also what a `per_transaction` bound's `requiredFor` draws from.
548
+ * Absent on profiles published before the registry existed — membership
549
+ * is then uncheckable, not a violation.
550
+ */
551
+ actionTypes?: readonly string[];
506
552
  };
507
553
  /**
508
554
  * v0.4 context schema — defines the execution context parameters (e.g., currency, action_type).
@@ -983,6 +1029,25 @@ declare function validateContextParams(params: AgentContextParams, profile: Agen
983
1029
  valid: boolean;
984
1030
  errors: string[];
985
1031
  };
1032
+ /**
1033
+ * Authoring-time check for `per_transaction` bound `requiredFor` (v0.8+):
1034
+ * entries must name a real `action_type` from the profile's declared
1035
+ * `boundsSchema.actionTypes` registry, and the field is only meaningful on
1036
+ * `per_transaction` bounds (see `BoundType`'s doc comment for why the other
1037
+ * kinds don't carry it).
1038
+ *
1039
+ * This is NOT wired into `registerProfile` or any enforcement path — nothing
1040
+ * in hap-core validates profile JSON at load time today (the pre-existing
1041
+ * `requiredFor` on a context/scope constraint has the same gap), so adding
1042
+ * an automatic refusal here would be new behaviour beyond this change's
1043
+ * scope. Call it explicitly from an authoring tool, a hap-profiles CI check,
1044
+ * or a test, to catch a `requiredFor` entry that can never match (misspelled
1045
+ * action type) or is attached to a bound kind that silently ignores it.
1046
+ *
1047
+ * Returns an empty array when the profile has no boundsSchema or nothing to
1048
+ * check — never throws.
1049
+ */
1050
+ declare function validateBoundsRequiredFor(profile: AgentProfile): string[];
986
1051
  /**
987
1052
  * Thrown when a value cannot be canonicalized at all — currently only for raw
988
1053
  * LF/CR inside a value. Carries the protocol error code so callers can map it
@@ -1033,6 +1098,31 @@ declare function computeBoundsHash(params: AgentBoundsParams, profile: AgentProf
1033
1098
  */
1034
1099
  declare function computeContextHash(params: AgentContextParams, profile: AgentProfile): string;
1035
1100
 
1101
+ /**
1102
+ * Which commitment modes a profile allows its mandates to be signed with
1103
+ * (`AgentProfile.commitment_modes`). One source for the Authority Server's
1104
+ * refusal and the mandate screen's choice, so the two cannot disagree.
1105
+ */
1106
+
1107
+ /** Every mode a signer can choose, in the order a mandate screen shows them. */
1108
+ declare const SIGNABLE_COMMITMENT_MODES: readonly SignableCommitmentMode[];
1109
+ /**
1110
+ * The modes a mandate under this profile may carry, in display order.
1111
+ * No declaration → all of them. A declaration is read fail-closed: entries
1112
+ * that are not a signable mode are dropped, so a malformed list can only
1113
+ * narrow — `[]`, `"review"` (not a list) or `["reveiw"]` allow nothing.
1114
+ */
1115
+ declare function allowedCommitmentModes(profile: AgentProfile): readonly SignableCommitmentMode[];
1116
+ /** Whether a mandate under this profile may be signed with `mode`. */
1117
+ declare function isCommitmentModeAllowed(profile: AgentProfile, mode: string): boolean;
1118
+ /**
1119
+ * Authoring check for a profile's `commitment_modes` declaration — for
1120
+ * hap-profiles CI, an authoring tool or a test. Like `validateBoundsRequiredFor`
1121
+ * it is not wired into profile loading; enforcement reads the declaration
1122
+ * fail-closed through `allowedCommitmentModes`. Empty array = nothing to fix.
1123
+ */
1124
+ declare function validateCommitmentModes(profile: AgentProfile): string[];
1125
+
1036
1126
  /**
1037
1127
  * Attestation Encoding, Decoding, and Verification
1038
1128
  *
@@ -1353,4 +1443,4 @@ interface ReceiptPayload {
1353
1443
  */
1354
1444
  declare function verifyReceiptSignature(receipt: ReceiptPayload, publicKeyHex: string): Promise<void>;
1355
1445
 
1356
- export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type ApprovalObject, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, CanonicalValueError, type ContentBinding, ContentBindingError, type ContentBindingErrorCode, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type MandateBinding, MandateError, type MandateProjection, type OwnerMandate, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ReceiptPayload, type ResolvedDomain, type SigningSurface, type Subject, type SubjectValidation, approvalSigningBytes, attestationId, bindingAppliesTo, boundActionTypes, buildMandateProjection, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFieldsContentHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, decodeDidKey, deriveIdentityLine, encodeAttestationBlob, encodeDidKey, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isFieldBinding, isKeyBearingDid, isV4Attestation, listProfiles, mandateSigningBytes, registerProfile, selectBoundFields, signApproval, signMandateProjection, validateBoundsParams, validateContextParams, validateFrameParams, validateSubject, verify, verifyApproval, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash, verifyOwnerMandate, verifyOwnerMandates, verifyReceiptSignature };
1446
+ export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type ApprovalObject, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, CanonicalValueError, type ContentBinding, ContentBindingError, type ContentBindingErrorCode, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type MandateBinding, MandateError, type MandateProjection, type OwnerMandate, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ReceiptPayload, type ResolvedDomain, SIGNABLE_COMMITMENT_MODES, type SignableCommitmentMode, type SigningSurface, type Subject, type SubjectValidation, allowedCommitmentModes, approvalSigningBytes, attestationId, bindingAppliesTo, boundActionTypes, buildMandateProjection, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFieldsContentHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, decodeDidKey, deriveIdentityLine, encodeAttestationBlob, encodeDidKey, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isCommitmentModeAllowed, isFieldBinding, isKeyBearingDid, isV4Attestation, listProfiles, mandateSigningBytes, registerProfile, selectBoundFields, signApproval, signMandateProjection, validateBoundsParams, validateBoundsRequiredFor, validateCommitmentModes, validateContextParams, validateFrameParams, validateSubject, verify, verifyApproval, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash, verifyOwnerMandate, verifyOwnerMandates, verifyReceiptSignature };
package/dist/index.d.ts CHANGED
@@ -205,10 +205,29 @@ type BoundType =
205
205
  * Per-transaction cap. The execution context field named in `of` must
206
206
  * satisfy `execution[of] <= bound` for the current call. No cumulative
207
207
  * tracking. Used by: amount_max, recipient_max, booking_duration_max, etc.
208
+ *
209
+ * `requiredFor` (v0.8+) closes the converse: without it, a call that does
210
+ * not carry `of` at all is simply skipped — a 5,000 cap refuses 6,000 but
211
+ * permits a call that declares no value whatsoever, which is indistinguishable
212
+ * from an unenforced bound. The same hole, and the same fix, as
213
+ * `FieldConstraint.requiredFor` on a scope constraint (below): values drawn
214
+ * from the profile's `boundsSchema.actionTypes` registry. For a listed
215
+ * `action_type`, an execution whose context lacks `of` — or whose value is
216
+ * not a finite number — MUST be refused rather than skipped. For an
217
+ * unlisted (or when `requiredFor` is absent) action type, today's
218
+ * skip-on-absence behaviour is unchanged. Meaningful only on
219
+ * `per_transaction`; the other `BoundType` kinds have no per-call "value
220
+ * exposed or not" question — `cumulative_sum`/`cumulative_count` always read
221
+ * from the execution log regardless of what this call declares, and `enum`
222
+ * is a capability flag, not a runtime value — so the field does not exist
223
+ * on those variants and a profile that puts it there has it silently
224
+ * ignored by every enforcement point (TypeScript also refuses it when the
225
+ * profile is authored against this type rather than raw JSON).
208
226
  */
209
227
  {
210
228
  kind: 'per_transaction';
211
229
  of: string;
230
+ requiredFor?: string[];
212
231
  }
213
232
  /**
214
233
  * Cumulative sum within a time window. The SP maintains a running sum
@@ -448,6 +467,11 @@ interface ExecutionPath {
448
467
  *
449
468
  * Supports both v0.3 (frameSchema) and v0.4 (boundsSchema + contextSchema).
450
469
  */
470
+ /**
471
+ * A commitment mode a person can choose when signing a mandate. (`review_above_cap`
472
+ * is not chosen by the signer — it follows from team caps.)
473
+ */
474
+ type SignableCommitmentMode = 'automatic' | 'review';
451
475
  interface AgentProfile {
452
476
  id: string;
453
477
  name?: string;
@@ -489,6 +513,18 @@ interface AgentProfile {
489
513
  * a forwarded message has usually lost the footer that carried it.
490
514
  */
491
515
  receipt_lookup?: boolean;
516
+ /**
517
+ * The commitment modes a mandate under this profile may be signed with.
518
+ * Absent → every mode (`automatic`, `review`), as before this field existed.
519
+ *
520
+ * Lets a profile be review-only: every action it governs then needs a
521
+ * person's approval, whatever the signer would have picked. The Authority
522
+ * Server refuses to sign a mandate whose `commitment_mode` is not listed;
523
+ * a mandate screen offers only the listed modes. Fail-closed: a declared
524
+ * list with no valid entry allows nothing — a malformed declaration never
525
+ * widens what a profile allows. See {@link allowedCommitmentModes}.
526
+ */
527
+ commitment_modes?: readonly SignableCommitmentMode[];
492
528
  /**
493
529
  * v0.3 frame schema (deprecated, kept for backward compat).
494
530
  * Used when boundsSchema is not present.
@@ -503,6 +539,16 @@ interface AgentProfile {
503
539
  boundsSchema?: {
504
540
  keyOrder: string[];
505
541
  fields: Record<string, ProfileBoundsField>;
542
+ /**
543
+ * v0.5+ registry of the action types this profile recognizes (e.g.
544
+ * `["send", "delete", "setup"]`). Every `execution.action_type` the
545
+ * Authority Server and gateway accept for this profile MUST be a member
546
+ * when the registry is declared (protocol.md → Bounds Schema, rule 2);
547
+ * it is also what a `per_transaction` bound's `requiredFor` draws from.
548
+ * Absent on profiles published before the registry existed — membership
549
+ * is then uncheckable, not a violation.
550
+ */
551
+ actionTypes?: readonly string[];
506
552
  };
507
553
  /**
508
554
  * v0.4 context schema — defines the execution context parameters (e.g., currency, action_type).
@@ -983,6 +1029,25 @@ declare function validateContextParams(params: AgentContextParams, profile: Agen
983
1029
  valid: boolean;
984
1030
  errors: string[];
985
1031
  };
1032
+ /**
1033
+ * Authoring-time check for `per_transaction` bound `requiredFor` (v0.8+):
1034
+ * entries must name a real `action_type` from the profile's declared
1035
+ * `boundsSchema.actionTypes` registry, and the field is only meaningful on
1036
+ * `per_transaction` bounds (see `BoundType`'s doc comment for why the other
1037
+ * kinds don't carry it).
1038
+ *
1039
+ * This is NOT wired into `registerProfile` or any enforcement path — nothing
1040
+ * in hap-core validates profile JSON at load time today (the pre-existing
1041
+ * `requiredFor` on a context/scope constraint has the same gap), so adding
1042
+ * an automatic refusal here would be new behaviour beyond this change's
1043
+ * scope. Call it explicitly from an authoring tool, a hap-profiles CI check,
1044
+ * or a test, to catch a `requiredFor` entry that can never match (misspelled
1045
+ * action type) or is attached to a bound kind that silently ignores it.
1046
+ *
1047
+ * Returns an empty array when the profile has no boundsSchema or nothing to
1048
+ * check — never throws.
1049
+ */
1050
+ declare function validateBoundsRequiredFor(profile: AgentProfile): string[];
986
1051
  /**
987
1052
  * Thrown when a value cannot be canonicalized at all — currently only for raw
988
1053
  * LF/CR inside a value. Carries the protocol error code so callers can map it
@@ -1033,6 +1098,31 @@ declare function computeBoundsHash(params: AgentBoundsParams, profile: AgentProf
1033
1098
  */
1034
1099
  declare function computeContextHash(params: AgentContextParams, profile: AgentProfile): string;
1035
1100
 
1101
+ /**
1102
+ * Which commitment modes a profile allows its mandates to be signed with
1103
+ * (`AgentProfile.commitment_modes`). One source for the Authority Server's
1104
+ * refusal and the mandate screen's choice, so the two cannot disagree.
1105
+ */
1106
+
1107
+ /** Every mode a signer can choose, in the order a mandate screen shows them. */
1108
+ declare const SIGNABLE_COMMITMENT_MODES: readonly SignableCommitmentMode[];
1109
+ /**
1110
+ * The modes a mandate under this profile may carry, in display order.
1111
+ * No declaration → all of them. A declaration is read fail-closed: entries
1112
+ * that are not a signable mode are dropped, so a malformed list can only
1113
+ * narrow — `[]`, `"review"` (not a list) or `["reveiw"]` allow nothing.
1114
+ */
1115
+ declare function allowedCommitmentModes(profile: AgentProfile): readonly SignableCommitmentMode[];
1116
+ /** Whether a mandate under this profile may be signed with `mode`. */
1117
+ declare function isCommitmentModeAllowed(profile: AgentProfile, mode: string): boolean;
1118
+ /**
1119
+ * Authoring check for a profile's `commitment_modes` declaration — for
1120
+ * hap-profiles CI, an authoring tool or a test. Like `validateBoundsRequiredFor`
1121
+ * it is not wired into profile loading; enforcement reads the declaration
1122
+ * fail-closed through `allowedCommitmentModes`. Empty array = nothing to fix.
1123
+ */
1124
+ declare function validateCommitmentModes(profile: AgentProfile): string[];
1125
+
1036
1126
  /**
1037
1127
  * Attestation Encoding, Decoding, and Verification
1038
1128
  *
@@ -1353,4 +1443,4 @@ interface ReceiptPayload {
1353
1443
  */
1354
1444
  declare function verifyReceiptSignature(receipt: ReceiptPayload, publicKeyHex: string): Promise<void>;
1355
1445
 
1356
- export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type ApprovalObject, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, CanonicalValueError, type ContentBinding, ContentBindingError, type ContentBindingErrorCode, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type MandateBinding, MandateError, type MandateProjection, type OwnerMandate, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ReceiptPayload, type ResolvedDomain, type SigningSurface, type Subject, type SubjectValidation, approvalSigningBytes, attestationId, bindingAppliesTo, boundActionTypes, buildMandateProjection, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFieldsContentHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, decodeDidKey, deriveIdentityLine, encodeAttestationBlob, encodeDidKey, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isFieldBinding, isKeyBearingDid, isV4Attestation, listProfiles, mandateSigningBytes, registerProfile, selectBoundFields, signApproval, signMandateProjection, validateBoundsParams, validateContextParams, validateFrameParams, validateSubject, verify, verifyApproval, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash, verifyOwnerMandate, verifyOwnerMandates, verifyReceiptSignature };
1446
+ export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type ApprovalObject, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, CanonicalValueError, type ContentBinding, ContentBindingError, type ContentBindingErrorCode, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type MandateBinding, MandateError, type MandateProjection, type OwnerMandate, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ReceiptPayload, type ResolvedDomain, SIGNABLE_COMMITMENT_MODES, type SignableCommitmentMode, type SigningSurface, type Subject, type SubjectValidation, allowedCommitmentModes, approvalSigningBytes, attestationId, bindingAppliesTo, boundActionTypes, buildMandateProjection, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFieldsContentHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, decodeDidKey, deriveIdentityLine, encodeAttestationBlob, encodeDidKey, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isCommitmentModeAllowed, isFieldBinding, isKeyBearingDid, isV4Attestation, listProfiles, mandateSigningBytes, registerProfile, selectBoundFields, signApproval, signMandateProjection, validateBoundsParams, validateBoundsRequiredFor, validateCommitmentModes, validateContextParams, validateFrameParams, validateSubject, verify, verifyApproval, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash, verifyOwnerMandate, verifyOwnerMandates, verifyReceiptSignature };
package/dist/index.js CHANGED
@@ -33,6 +33,8 @@ __export(index_exports, {
33
33
  CanonicalValueError: () => CanonicalValueError,
34
34
  ContentBindingError: () => ContentBindingError,
35
35
  MandateError: () => MandateError,
36
+ SIGNABLE_COMMITMENT_MODES: () => SIGNABLE_COMMITMENT_MODES,
37
+ allowedCommitmentModes: () => allowedCommitmentModes,
36
38
  approvalSigningBytes: () => approvalSigningBytes,
37
39
  attestationId: () => attestationId,
38
40
  bindingAppliesTo: () => bindingAppliesTo,
@@ -62,6 +64,7 @@ __export(index_exports, {
62
64
  getAllProfiles: () => getAllProfiles,
63
65
  getProfile: () => getProfile,
64
66
  intentDisclosureCanonicalBytes: () => intentDisclosureCanonicalBytes,
67
+ isCommitmentModeAllowed: () => isCommitmentModeAllowed,
65
68
  isFieldBinding: () => isFieldBinding,
66
69
  isKeyBearingDid: () => isKeyBearingDid,
67
70
  isV4Attestation: () => isV4Attestation,
@@ -72,6 +75,8 @@ __export(index_exports, {
72
75
  signApproval: () => signApproval,
73
76
  signMandateProjection: () => signMandateProjection,
74
77
  validateBoundsParams: () => validateBoundsParams,
78
+ validateBoundsRequiredFor: () => validateBoundsRequiredFor,
79
+ validateCommitmentModes: () => validateCommitmentModes,
75
80
  validateContextParams: () => validateContextParams,
76
81
  validateFrameParams: () => validateFrameParams,
77
82
  validateSubject: () => validateSubject,
@@ -378,6 +383,38 @@ function validateContextParams(params, profile) {
378
383
  }
379
384
  return { valid: errors.length === 0, errors };
380
385
  }
386
+ function validateBoundsRequiredFor(profile) {
387
+ const errors = [];
388
+ const boundsSchema = profile.boundsSchema;
389
+ if (!boundsSchema) return errors;
390
+ const actionTypes = boundsSchema.actionTypes ? [...boundsSchema.actionTypes] : void 0;
391
+ for (const [fieldName, fieldDef] of Object.entries(boundsSchema.fields)) {
392
+ const bt = fieldDef.boundType;
393
+ if (!bt) continue;
394
+ const requiredFor = bt.requiredFor;
395
+ if (requiredFor === void 0) continue;
396
+ if (!Array.isArray(requiredFor) || !requiredFor.every((v) => typeof v === "string")) {
397
+ errors.push(`Bound "${fieldName}": requiredFor must be a string array.`);
398
+ continue;
399
+ }
400
+ if (bt.kind !== "per_transaction") {
401
+ errors.push(
402
+ `Bound "${fieldName}": requiredFor is only meaningful on a per_transaction bound (found on kind "${bt.kind}") \u2014 every enforcement point ignores it there. Remove it.`
403
+ );
404
+ continue;
405
+ }
406
+ if (actionTypes) {
407
+ for (const actionType of requiredFor) {
408
+ if (!actionTypes.includes(actionType)) {
409
+ errors.push(
410
+ `Bound "${fieldName}": requiredFor names action type "${actionType}", which is not in this profile's boundsSchema.actionTypes registry [${actionTypes.join(", ")}]. It can never match a real execution and the requirement is dead.`
411
+ );
412
+ }
413
+ }
414
+ }
415
+ }
416
+ return errors;
417
+ }
381
418
  var CanonicalValueError = class extends Error {
382
419
  code;
383
420
  field;
@@ -445,6 +482,33 @@ function computeContextHash(params, profile) {
445
482
  return `sha256:${hash}`;
446
483
  }
447
484
 
485
+ // src/commitment-modes.ts
486
+ var SIGNABLE_COMMITMENT_MODES = ["review", "automatic"];
487
+ var isSignable = (v) => typeof v === "string" && SIGNABLE_COMMITMENT_MODES.includes(v);
488
+ function allowedCommitmentModes(profile) {
489
+ const declared = profile.commitment_modes;
490
+ if (declared === void 0) return SIGNABLE_COMMITMENT_MODES;
491
+ if (!Array.isArray(declared)) return [];
492
+ return SIGNABLE_COMMITMENT_MODES.filter((m) => declared.includes(m));
493
+ }
494
+ function isCommitmentModeAllowed(profile, mode) {
495
+ return isSignable(mode) && allowedCommitmentModes(profile).includes(mode);
496
+ }
497
+ function validateCommitmentModes(profile) {
498
+ const declared = profile.commitment_modes;
499
+ if (declared === void 0) return [];
500
+ if (!Array.isArray(declared)) return ["commitment_modes must be a list of modes."];
501
+ const errors = [];
502
+ if (declared.length === 0) errors.push("commitment_modes is empty \u2014 no mandate could ever be signed under this profile.");
503
+ const seen = /* @__PURE__ */ new Set();
504
+ for (const m of declared) {
505
+ if (!isSignable(m)) errors.push(`commitment_modes: "${String(m)}" is not a mode a signer can choose (${SIGNABLE_COMMITMENT_MODES.join(", ")}).`);
506
+ if (seen.has(m)) errors.push(`commitment_modes: "${String(m)}" appears twice.`);
507
+ seen.add(m);
508
+ }
509
+ return errors;
510
+ }
511
+
448
512
  // src/attestation.ts
449
513
  var import_crypto4 = require("crypto");
450
514
  var ed = __toESM(require("@noble/ed25519"));
@@ -820,6 +884,22 @@ function checkBoundsV4(request, profile, executionLog, now) {
820
884
  switch (bt.kind) {
821
885
  case "per_transaction": {
822
886
  const actual = request.execution[bt.of];
887
+ const engaged = !!bt.requiredFor?.length && actionType !== void 0 && bt.requiredFor.includes(actionType);
888
+ if (engaged) {
889
+ const missing = actual === void 0 || actual === null;
890
+ const numericActual = missing ? NaN : typeof actual === "number" ? actual : Number(actual);
891
+ const missingOrNonNumeric = missing || Number.isNaN(numericActual);
892
+ if (missingOrNonNumeric) {
893
+ errors.push({
894
+ code: "BOUND_EXCEEDED",
895
+ field: bt.of,
896
+ message: missing ? `Bound "${fieldName}" requires "${bt.of}" for "${actionType}" calls, but this call exposes no ${bt.of} to check against it. Refusing: the call cannot be shown to stay within ${fieldName}.` : `Bound "${fieldName}" requires a numeric "${bt.of}" for "${actionType}" calls, but this call's value (${String(actual)}) is not a number. Refusing: the call cannot be shown to stay within ${fieldName}.`,
897
+ bound: boundValue,
898
+ actual
899
+ });
900
+ continue;
901
+ }
902
+ }
823
903
  if (actual === void 0) continue;
824
904
  if (typeof boundValue !== "number" || typeof actual !== "number") {
825
905
  errors.push({
@@ -1227,6 +1307,8 @@ async function verifyReceiptSignature(receipt, publicKeyHex) {
1227
1307
  CanonicalValueError,
1228
1308
  ContentBindingError,
1229
1309
  MandateError,
1310
+ SIGNABLE_COMMITMENT_MODES,
1311
+ allowedCommitmentModes,
1230
1312
  approvalSigningBytes,
1231
1313
  attestationId,
1232
1314
  bindingAppliesTo,
@@ -1256,6 +1338,7 @@ async function verifyReceiptSignature(receipt, publicKeyHex) {
1256
1338
  getAllProfiles,
1257
1339
  getProfile,
1258
1340
  intentDisclosureCanonicalBytes,
1341
+ isCommitmentModeAllowed,
1259
1342
  isFieldBinding,
1260
1343
  isKeyBearingDid,
1261
1344
  isV4Attestation,
@@ -1266,6 +1349,8 @@ async function verifyReceiptSignature(receipt, publicKeyHex) {
1266
1349
  signApproval,
1267
1350
  signMandateProjection,
1268
1351
  validateBoundsParams,
1352
+ validateBoundsRequiredFor,
1353
+ validateCommitmentModes,
1269
1354
  validateContextParams,
1270
1355
  validateFrameParams,
1271
1356
  validateSubject,
package/dist/index.mjs CHANGED
@@ -287,6 +287,38 @@ function validateContextParams(params, profile) {
287
287
  }
288
288
  return { valid: errors.length === 0, errors };
289
289
  }
290
+ function validateBoundsRequiredFor(profile) {
291
+ const errors = [];
292
+ const boundsSchema = profile.boundsSchema;
293
+ if (!boundsSchema) return errors;
294
+ const actionTypes = boundsSchema.actionTypes ? [...boundsSchema.actionTypes] : void 0;
295
+ for (const [fieldName, fieldDef] of Object.entries(boundsSchema.fields)) {
296
+ const bt = fieldDef.boundType;
297
+ if (!bt) continue;
298
+ const requiredFor = bt.requiredFor;
299
+ if (requiredFor === void 0) continue;
300
+ if (!Array.isArray(requiredFor) || !requiredFor.every((v) => typeof v === "string")) {
301
+ errors.push(`Bound "${fieldName}": requiredFor must be a string array.`);
302
+ continue;
303
+ }
304
+ if (bt.kind !== "per_transaction") {
305
+ errors.push(
306
+ `Bound "${fieldName}": requiredFor is only meaningful on a per_transaction bound (found on kind "${bt.kind}") \u2014 every enforcement point ignores it there. Remove it.`
307
+ );
308
+ continue;
309
+ }
310
+ if (actionTypes) {
311
+ for (const actionType of requiredFor) {
312
+ if (!actionTypes.includes(actionType)) {
313
+ errors.push(
314
+ `Bound "${fieldName}": requiredFor names action type "${actionType}", which is not in this profile's boundsSchema.actionTypes registry [${actionTypes.join(", ")}]. It can never match a real execution and the requirement is dead.`
315
+ );
316
+ }
317
+ }
318
+ }
319
+ }
320
+ return errors;
321
+ }
290
322
  var CanonicalValueError = class extends Error {
291
323
  code;
292
324
  field;
@@ -354,6 +386,33 @@ function computeContextHash(params, profile) {
354
386
  return `sha256:${hash}`;
355
387
  }
356
388
 
389
+ // src/commitment-modes.ts
390
+ var SIGNABLE_COMMITMENT_MODES = ["review", "automatic"];
391
+ var isSignable = (v) => typeof v === "string" && SIGNABLE_COMMITMENT_MODES.includes(v);
392
+ function allowedCommitmentModes(profile) {
393
+ const declared = profile.commitment_modes;
394
+ if (declared === void 0) return SIGNABLE_COMMITMENT_MODES;
395
+ if (!Array.isArray(declared)) return [];
396
+ return SIGNABLE_COMMITMENT_MODES.filter((m) => declared.includes(m));
397
+ }
398
+ function isCommitmentModeAllowed(profile, mode) {
399
+ return isSignable(mode) && allowedCommitmentModes(profile).includes(mode);
400
+ }
401
+ function validateCommitmentModes(profile) {
402
+ const declared = profile.commitment_modes;
403
+ if (declared === void 0) return [];
404
+ if (!Array.isArray(declared)) return ["commitment_modes must be a list of modes."];
405
+ const errors = [];
406
+ if (declared.length === 0) errors.push("commitment_modes is empty \u2014 no mandate could ever be signed under this profile.");
407
+ const seen = /* @__PURE__ */ new Set();
408
+ for (const m of declared) {
409
+ if (!isSignable(m)) errors.push(`commitment_modes: "${String(m)}" is not a mode a signer can choose (${SIGNABLE_COMMITMENT_MODES.join(", ")}).`);
410
+ if (seen.has(m)) errors.push(`commitment_modes: "${String(m)}" appears twice.`);
411
+ seen.add(m);
412
+ }
413
+ return errors;
414
+ }
415
+
357
416
  // src/attestation.ts
358
417
  import { createHash as createHash4 } from "crypto";
359
418
  import * as ed from "@noble/ed25519";
@@ -729,6 +788,22 @@ function checkBoundsV4(request, profile, executionLog, now) {
729
788
  switch (bt.kind) {
730
789
  case "per_transaction": {
731
790
  const actual = request.execution[bt.of];
791
+ const engaged = !!bt.requiredFor?.length && actionType !== void 0 && bt.requiredFor.includes(actionType);
792
+ if (engaged) {
793
+ const missing = actual === void 0 || actual === null;
794
+ const numericActual = missing ? NaN : typeof actual === "number" ? actual : Number(actual);
795
+ const missingOrNonNumeric = missing || Number.isNaN(numericActual);
796
+ if (missingOrNonNumeric) {
797
+ errors.push({
798
+ code: "BOUND_EXCEEDED",
799
+ field: bt.of,
800
+ message: missing ? `Bound "${fieldName}" requires "${bt.of}" for "${actionType}" calls, but this call exposes no ${bt.of} to check against it. Refusing: the call cannot be shown to stay within ${fieldName}.` : `Bound "${fieldName}" requires a numeric "${bt.of}" for "${actionType}" calls, but this call's value (${String(actual)}) is not a number. Refusing: the call cannot be shown to stay within ${fieldName}.`,
801
+ bound: boundValue,
802
+ actual
803
+ });
804
+ continue;
805
+ }
806
+ }
732
807
  if (actual === void 0) continue;
733
808
  if (typeof boundValue !== "number" || typeof actual !== "number") {
734
809
  errors.push({
@@ -1135,6 +1210,8 @@ export {
1135
1210
  CanonicalValueError,
1136
1211
  ContentBindingError,
1137
1212
  MandateError,
1213
+ SIGNABLE_COMMITMENT_MODES,
1214
+ allowedCommitmentModes,
1138
1215
  approvalSigningBytes,
1139
1216
  attestationId,
1140
1217
  bindingAppliesTo,
@@ -1164,6 +1241,7 @@ export {
1164
1241
  getAllProfiles,
1165
1242
  getProfile,
1166
1243
  intentDisclosureCanonicalBytes,
1244
+ isCommitmentModeAllowed,
1167
1245
  isFieldBinding,
1168
1246
  isKeyBearingDid,
1169
1247
  isV4Attestation,
@@ -1174,6 +1252,8 @@ export {
1174
1252
  signApproval,
1175
1253
  signMandateProjection,
1176
1254
  validateBoundsParams,
1255
+ validateBoundsRequiredFor,
1256
+ validateCommitmentModes,
1177
1257
  validateContextParams,
1178
1258
  validateFrameParams,
1179
1259
  validateSubject,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanagencyp/hap-core",
3
- "version": "0.10.1",
3
+ "version": "0.11.1",
4
4
  "description": "Core types, cryptographic primitives, and verification logic for the Human Agency Protocol",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Which commitment modes a profile allows its mandates to be signed with
3
+ * (`AgentProfile.commitment_modes`). One source for the Authority Server's
4
+ * refusal and the mandate screen's choice, so the two cannot disagree.
5
+ */
6
+ import type { AgentProfile, SignableCommitmentMode } from './types';
7
+
8
+ /** Every mode a signer can choose, in the order a mandate screen shows them. */
9
+ export const SIGNABLE_COMMITMENT_MODES: readonly SignableCommitmentMode[] = ['review', 'automatic'];
10
+
11
+ const isSignable = (v: unknown): v is SignableCommitmentMode =>
12
+ typeof v === 'string' && (SIGNABLE_COMMITMENT_MODES as readonly string[]).includes(v);
13
+
14
+ /**
15
+ * The modes a mandate under this profile may carry, in display order.
16
+ * No declaration → all of them. A declaration is read fail-closed: entries
17
+ * that are not a signable mode are dropped, so a malformed list can only
18
+ * narrow — `[]`, `"review"` (not a list) or `["reveiw"]` allow nothing.
19
+ */
20
+ export function allowedCommitmentModes(profile: AgentProfile): readonly SignableCommitmentMode[] {
21
+ const declared = (profile as { commitment_modes?: unknown }).commitment_modes;
22
+ if (declared === undefined) return SIGNABLE_COMMITMENT_MODES;
23
+ if (!Array.isArray(declared)) return [];
24
+ return SIGNABLE_COMMITMENT_MODES.filter((m) => declared.includes(m));
25
+ }
26
+
27
+ /** Whether a mandate under this profile may be signed with `mode`. */
28
+ export function isCommitmentModeAllowed(profile: AgentProfile, mode: string): boolean {
29
+ return isSignable(mode) && allowedCommitmentModes(profile).includes(mode);
30
+ }
31
+
32
+ /**
33
+ * Authoring check for a profile's `commitment_modes` declaration — for
34
+ * hap-profiles CI, an authoring tool or a test. Like `validateBoundsRequiredFor`
35
+ * it is not wired into profile loading; enforcement reads the declaration
36
+ * fail-closed through `allowedCommitmentModes`. Empty array = nothing to fix.
37
+ */
38
+ export function validateCommitmentModes(profile: AgentProfile): string[] {
39
+ const declared = (profile as { commitment_modes?: unknown }).commitment_modes;
40
+ if (declared === undefined) return [];
41
+ if (!Array.isArray(declared)) return ['commitment_modes must be a list of modes.'];
42
+ const errors: string[] = [];
43
+ if (declared.length === 0) errors.push('commitment_modes is empty — no mandate could ever be signed under this profile.');
44
+ const seen = new Set<unknown>();
45
+ for (const m of declared) {
46
+ if (!isSignable(m)) errors.push(`commitment_modes: "${String(m)}" is not a mode a signer can choose (${SIGNABLE_COMMITMENT_MODES.join(', ')}).`);
47
+ if (seen.has(m)) errors.push(`commitment_modes: "${String(m)}" appears twice.`);
48
+ seen.add(m);
49
+ }
50
+ return errors;
51
+ }
package/src/frame.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import { createHash } from 'crypto';
16
- import type { AgentFrameParams, AgentBoundsParams, AgentContextParams, AgentProfile } from './types';
16
+ import type { AgentFrameParams, AgentBoundsParams, AgentContextParams, AgentProfile, BoundType } from './types';
17
17
 
18
18
  // ─── v0.3 Frame Functions ─────────────────────────────────────────────────────
19
19
 
@@ -179,6 +179,68 @@ export function validateContextParams(
179
179
  return { valid: errors.length === 0, errors };
180
180
  }
181
181
 
182
+ /**
183
+ * Authoring-time check for `per_transaction` bound `requiredFor` (v0.8+):
184
+ * entries must name a real `action_type` from the profile's declared
185
+ * `boundsSchema.actionTypes` registry, and the field is only meaningful on
186
+ * `per_transaction` bounds (see `BoundType`'s doc comment for why the other
187
+ * kinds don't carry it).
188
+ *
189
+ * This is NOT wired into `registerProfile` or any enforcement path — nothing
190
+ * in hap-core validates profile JSON at load time today (the pre-existing
191
+ * `requiredFor` on a context/scope constraint has the same gap), so adding
192
+ * an automatic refusal here would be new behaviour beyond this change's
193
+ * scope. Call it explicitly from an authoring tool, a hap-profiles CI check,
194
+ * or a test, to catch a `requiredFor` entry that can never match (misspelled
195
+ * action type) or is attached to a bound kind that silently ignores it.
196
+ *
197
+ * Returns an empty array when the profile has no boundsSchema or nothing to
198
+ * check — never throws.
199
+ */
200
+ export function validateBoundsRequiredFor(profile: AgentProfile): string[] {
201
+ const errors: string[] = [];
202
+ const boundsSchema = profile.boundsSchema;
203
+ if (!boundsSchema) return errors;
204
+
205
+ const actionTypes = boundsSchema.actionTypes ? [...boundsSchema.actionTypes] : undefined;
206
+
207
+ for (const [fieldName, fieldDef] of Object.entries(boundsSchema.fields)) {
208
+ const bt = fieldDef.boundType as (BoundType & { requiredFor?: unknown }) | undefined;
209
+ if (!bt) continue;
210
+ const requiredFor = (bt as { requiredFor?: unknown }).requiredFor;
211
+ if (requiredFor === undefined) continue;
212
+
213
+ if (!Array.isArray(requiredFor) || !requiredFor.every((v) => typeof v === 'string')) {
214
+ errors.push(`Bound "${fieldName}": requiredFor must be a string array.`);
215
+ continue;
216
+ }
217
+
218
+ if (bt.kind !== 'per_transaction') {
219
+ errors.push(
220
+ `Bound "${fieldName}": requiredFor is only meaningful on a per_transaction bound ` +
221
+ `(found on kind "${bt.kind}") — every enforcement point ignores it there. Remove it.`,
222
+ );
223
+ continue;
224
+ }
225
+
226
+ if (actionTypes) {
227
+ for (const actionType of requiredFor) {
228
+ if (!actionTypes.includes(actionType)) {
229
+ errors.push(
230
+ `Bound "${fieldName}": requiredFor names action type "${actionType}", which is not ` +
231
+ `in this profile's boundsSchema.actionTypes registry [${actionTypes.join(', ')}]. ` +
232
+ `It can never match a real execution and the requirement is dead.`,
233
+ );
234
+ }
235
+ }
236
+ }
237
+ // No actionTypes registry declared: membership cannot be checked (same
238
+ // tolerance the AS receipt route applies to pre-registry profiles).
239
+ }
240
+
241
+ return errors;
242
+ }
243
+
182
244
  // ─── Value Encoding (normative, v0.5+) ───────────────────────────────────────
183
245
 
184
246
  /**
package/src/gatekeeper.ts CHANGED
@@ -546,6 +546,43 @@ function checkBoundsV4(
546
546
  switch (bt.kind) {
547
547
  case 'per_transaction': {
548
548
  const actual = request.execution[bt.of];
549
+
550
+ // `requiredFor`: for a listed action_type, absence (or a non-numeric
551
+ // value) is a denial, not a skip — see BoundType.per_transaction's
552
+ // doc comment for why. Unlisted action types, or no `requiredFor` at
553
+ // all, keep the pre-existing skip-on-absence behaviour below.
554
+ const engaged = !!bt.requiredFor?.length
555
+ && actionType !== undefined
556
+ && bt.requiredFor.includes(actionType);
557
+
558
+ if (engaged) {
559
+ // `null` is treated as "missing", not as the number 0 — JSON has no
560
+ // undefined, so a value lost somewhere upstream (an unmapped
561
+ // connector field serialized through JSON) commonly arrives as
562
+ // null. `Number(null) === 0` would otherwise read as "an amount of
563
+ // zero was declared and is within bound", which is the exact
564
+ // silent-pass this feature exists to close.
565
+ const missing = actual === undefined || actual === null;
566
+ const numericActual = missing ? NaN : (typeof actual === 'number' ? actual : Number(actual));
567
+ const missingOrNonNumeric = missing || Number.isNaN(numericActual);
568
+ if (missingOrNonNumeric) {
569
+ errors.push({
570
+ code: 'BOUND_EXCEEDED',
571
+ field: bt.of,
572
+ message: missing
573
+ ? `Bound "${fieldName}" requires "${bt.of}" for "${actionType}" calls, but this ` +
574
+ `call exposes no ${bt.of} to check against it. Refusing: the call cannot be ` +
575
+ `shown to stay within ${fieldName}.`
576
+ : `Bound "${fieldName}" requires a numeric "${bt.of}" for "${actionType}" calls, ` +
577
+ `but this call's value (${String(actual)}) is not a number. Refusing: the call ` +
578
+ `cannot be shown to stay within ${fieldName}.`,
579
+ bound: boundValue,
580
+ actual,
581
+ });
582
+ continue;
583
+ }
584
+ }
585
+
549
586
  if (actual === undefined) continue;
550
587
  if (typeof boundValue !== 'number' || typeof actual !== 'number') {
551
588
  errors.push({
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export * from './content-binding';
10
10
  export * from './intent-disclosure';
11
11
  export * from './identity';
12
12
  export * from './frame';
13
+ export * from './commitment-modes';
13
14
  export * from './attestation';
14
15
  export * from './gatekeeper';
15
16
  export * from './profiles';
package/src/types.ts CHANGED
@@ -218,8 +218,26 @@ export type BoundType =
218
218
  * Per-transaction cap. The execution context field named in `of` must
219
219
  * satisfy `execution[of] <= bound` for the current call. No cumulative
220
220
  * tracking. Used by: amount_max, recipient_max, booking_duration_max, etc.
221
+ *
222
+ * `requiredFor` (v0.8+) closes the converse: without it, a call that does
223
+ * not carry `of` at all is simply skipped — a 5,000 cap refuses 6,000 but
224
+ * permits a call that declares no value whatsoever, which is indistinguishable
225
+ * from an unenforced bound. The same hole, and the same fix, as
226
+ * `FieldConstraint.requiredFor` on a scope constraint (below): values drawn
227
+ * from the profile's `boundsSchema.actionTypes` registry. For a listed
228
+ * `action_type`, an execution whose context lacks `of` — or whose value is
229
+ * not a finite number — MUST be refused rather than skipped. For an
230
+ * unlisted (or when `requiredFor` is absent) action type, today's
231
+ * skip-on-absence behaviour is unchanged. Meaningful only on
232
+ * `per_transaction`; the other `BoundType` kinds have no per-call "value
233
+ * exposed or not" question — `cumulative_sum`/`cumulative_count` always read
234
+ * from the execution log regardless of what this call declares, and `enum`
235
+ * is a capability flag, not a runtime value — so the field does not exist
236
+ * on those variants and a profile that puts it there has it silently
237
+ * ignored by every enforcement point (TypeScript also refuses it when the
238
+ * profile is authored against this type rather than raw JSON).
221
239
  */
222
- | { kind: 'per_transaction'; of: string }
240
+ | { kind: 'per_transaction'; of: string; requiredFor?: string[] }
223
241
  /**
224
242
  * Cumulative sum within a time window. The SP maintains a running sum
225
243
  * of `execution[of]` across all prior executions in the window; the
@@ -461,6 +479,12 @@ export interface ExecutionPath {
461
479
  *
462
480
  * Supports both v0.3 (frameSchema) and v0.4 (boundsSchema + contextSchema).
463
481
  */
482
+ /**
483
+ * A commitment mode a person can choose when signing a mandate. (`review_above_cap`
484
+ * is not chosen by the signer — it follows from team caps.)
485
+ */
486
+ export type SignableCommitmentMode = 'automatic' | 'review';
487
+
464
488
  export interface AgentProfile {
465
489
  id: string;
466
490
  name?: string;
@@ -505,6 +529,19 @@ export interface AgentProfile {
505
529
  */
506
530
  receipt_lookup?: boolean;
507
531
 
532
+ /**
533
+ * The commitment modes a mandate under this profile may be signed with.
534
+ * Absent → every mode (`automatic`, `review`), as before this field existed.
535
+ *
536
+ * Lets a profile be review-only: every action it governs then needs a
537
+ * person's approval, whatever the signer would have picked. The Authority
538
+ * Server refuses to sign a mandate whose `commitment_mode` is not listed;
539
+ * a mandate screen offers only the listed modes. Fail-closed: a declared
540
+ * list with no valid entry allows nothing — a malformed declaration never
541
+ * widens what a profile allows. See {@link allowedCommitmentModes}.
542
+ */
543
+ commitment_modes?: readonly SignableCommitmentMode[];
544
+
508
545
  /**
509
546
  * v0.3 frame schema (deprecated, kept for backward compat).
510
547
  * Used when boundsSchema is not present.
@@ -520,6 +557,16 @@ export interface AgentProfile {
520
557
  boundsSchema?: {
521
558
  keyOrder: string[];
522
559
  fields: Record<string, ProfileBoundsField>;
560
+ /**
561
+ * v0.5+ registry of the action types this profile recognizes (e.g.
562
+ * `["send", "delete", "setup"]`). Every `execution.action_type` the
563
+ * Authority Server and gateway accept for this profile MUST be a member
564
+ * when the registry is declared (protocol.md → Bounds Schema, rule 2);
565
+ * it is also what a `per_transaction` bound's `requiredFor` draws from.
566
+ * Absent on profiles published before the registry existed — membership
567
+ * is then uncheckable, not a violation.
568
+ */
569
+ actionTypes?: readonly string[];
523
570
  };
524
571
 
525
572
  /**