@humanagencyp/hap-core 0.6.2 → 0.8.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/dist/index.d.mts +48 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +30 -1
- package/dist/index.mjs +30 -1
- package/package.json +1 -1
- package/src/gatekeeper.ts +75 -1
- package/src/types.ts +48 -0
package/dist/index.d.mts
CHANGED
|
@@ -113,6 +113,27 @@ interface Attestation {
|
|
|
113
113
|
interface FieldConstraint {
|
|
114
114
|
type: 'number' | 'string';
|
|
115
115
|
enforceable: Array<'max' | 'enum' | 'subset'>;
|
|
116
|
+
/**
|
|
117
|
+
* Action types for which this dimension MUST be present in the execution
|
|
118
|
+
* context. When the grant constrains this field and the action is one of
|
|
119
|
+
* these, an absent value is a denial rather than a skipped check.
|
|
120
|
+
*
|
|
121
|
+
* Without it, a constraint is only enforced against calls that happen to
|
|
122
|
+
* expose the value. Gmail's `send_message` is the live case: pass `raw` and
|
|
123
|
+
* `to` is never populated, so `allowed_recipients` had nothing to compare and
|
|
124
|
+
* the check passed — a send to an unverified recipient looked authorized.
|
|
125
|
+
* `send_draft` has the same shape: it transmits, and the Gatekeeper cannot
|
|
126
|
+
* see to whom.
|
|
127
|
+
*
|
|
128
|
+
* Keyed on `action_type` because "does this action engage recipients?" is a
|
|
129
|
+
* property of the action, not of the field name — deleting a draft has no
|
|
130
|
+
* recipients and must keep passing, while sending one must not. This keeps
|
|
131
|
+
* the engine profile-agnostic: it compares declared strings and knows nothing
|
|
132
|
+
* about email, calendars or payments.
|
|
133
|
+
*
|
|
134
|
+
* Omitted → previous behaviour: absence skips the check.
|
|
135
|
+
*/
|
|
136
|
+
requiredFor?: string[];
|
|
116
137
|
}
|
|
117
138
|
/**
|
|
118
139
|
* Frame field definition within a profile.
|
|
@@ -210,6 +231,21 @@ interface ProfileBoundsField {
|
|
|
210
231
|
* treat a missing boundType as an error when running in v0.4 mode.
|
|
211
232
|
*/
|
|
212
233
|
boundType?: BoundType;
|
|
234
|
+
/**
|
|
235
|
+
* Which execution action types this bound governs, e.g. `["write"]`.
|
|
236
|
+
*
|
|
237
|
+
* When present, this is authoritative. When absent, enforcement falls back to
|
|
238
|
+
* inferring the action type from the FIELD NAME (`send_daily_max` → `send`),
|
|
239
|
+
* which is a convention rather than a contract: a profile that names a bound
|
|
240
|
+
* after the domain concept instead of the action — calendar's
|
|
241
|
+
* `booking_daily_max` against an action type of `write` — matches nothing and
|
|
242
|
+
* is skipped, so a limit the user set is never applied and nothing says so.
|
|
243
|
+
*
|
|
244
|
+
* Declaring it removes the guess. Deliberately additive: profiles migrate one
|
|
245
|
+
* at a time, and every remaining fallback is logged, so the cases still
|
|
246
|
+
* relying on the naming convention become visible instead of staying silent.
|
|
247
|
+
*/
|
|
248
|
+
appliesTo?: string[];
|
|
213
249
|
/**
|
|
214
250
|
* The measurement dimension of the bound's value. UI renders the unit
|
|
215
251
|
* inline next to the input (e.g. `4 min`, `100 EUR`). Independent from
|
|
@@ -474,6 +510,18 @@ interface GatekeeperRequest {
|
|
|
474
510
|
execution: Record<string, string | number>;
|
|
475
511
|
/** v0.4: context parameters (currency, action_type, etc.) */
|
|
476
512
|
context?: AgentContextParams;
|
|
513
|
+
/**
|
|
514
|
+
* Authorization path used to scope cumulative lookups in the execution log.
|
|
515
|
+
*
|
|
516
|
+
* Deliberately OUTSIDE `frame`: the frame is hashed and validated against the
|
|
517
|
+
* profile's boundsSchema, so an extra key there is rejected as an unknown
|
|
518
|
+
* field and breaks attestation verification outright. Cumulative checks
|
|
519
|
+
* previously read the path from the frame, where callers cannot legally put
|
|
520
|
+
* it — so it resolved to "" while the log stored real paths, no entry ever
|
|
521
|
+
* matched, and every running total read zero. The local gate existed but
|
|
522
|
+
* could never fire.
|
|
523
|
+
*/
|
|
524
|
+
path?: string;
|
|
477
525
|
}
|
|
478
526
|
/**
|
|
479
527
|
* Structured error from Gatekeeper verification.
|
package/dist/index.d.ts
CHANGED
|
@@ -113,6 +113,27 @@ interface Attestation {
|
|
|
113
113
|
interface FieldConstraint {
|
|
114
114
|
type: 'number' | 'string';
|
|
115
115
|
enforceable: Array<'max' | 'enum' | 'subset'>;
|
|
116
|
+
/**
|
|
117
|
+
* Action types for which this dimension MUST be present in the execution
|
|
118
|
+
* context. When the grant constrains this field and the action is one of
|
|
119
|
+
* these, an absent value is a denial rather than a skipped check.
|
|
120
|
+
*
|
|
121
|
+
* Without it, a constraint is only enforced against calls that happen to
|
|
122
|
+
* expose the value. Gmail's `send_message` is the live case: pass `raw` and
|
|
123
|
+
* `to` is never populated, so `allowed_recipients` had nothing to compare and
|
|
124
|
+
* the check passed — a send to an unverified recipient looked authorized.
|
|
125
|
+
* `send_draft` has the same shape: it transmits, and the Gatekeeper cannot
|
|
126
|
+
* see to whom.
|
|
127
|
+
*
|
|
128
|
+
* Keyed on `action_type` because "does this action engage recipients?" is a
|
|
129
|
+
* property of the action, not of the field name — deleting a draft has no
|
|
130
|
+
* recipients and must keep passing, while sending one must not. This keeps
|
|
131
|
+
* the engine profile-agnostic: it compares declared strings and knows nothing
|
|
132
|
+
* about email, calendars or payments.
|
|
133
|
+
*
|
|
134
|
+
* Omitted → previous behaviour: absence skips the check.
|
|
135
|
+
*/
|
|
136
|
+
requiredFor?: string[];
|
|
116
137
|
}
|
|
117
138
|
/**
|
|
118
139
|
* Frame field definition within a profile.
|
|
@@ -210,6 +231,21 @@ interface ProfileBoundsField {
|
|
|
210
231
|
* treat a missing boundType as an error when running in v0.4 mode.
|
|
211
232
|
*/
|
|
212
233
|
boundType?: BoundType;
|
|
234
|
+
/**
|
|
235
|
+
* Which execution action types this bound governs, e.g. `["write"]`.
|
|
236
|
+
*
|
|
237
|
+
* When present, this is authoritative. When absent, enforcement falls back to
|
|
238
|
+
* inferring the action type from the FIELD NAME (`send_daily_max` → `send`),
|
|
239
|
+
* which is a convention rather than a contract: a profile that names a bound
|
|
240
|
+
* after the domain concept instead of the action — calendar's
|
|
241
|
+
* `booking_daily_max` against an action type of `write` — matches nothing and
|
|
242
|
+
* is skipped, so a limit the user set is never applied and nothing says so.
|
|
243
|
+
*
|
|
244
|
+
* Declaring it removes the guess. Deliberately additive: profiles migrate one
|
|
245
|
+
* at a time, and every remaining fallback is logged, so the cases still
|
|
246
|
+
* relying on the naming convention become visible instead of staying silent.
|
|
247
|
+
*/
|
|
248
|
+
appliesTo?: string[];
|
|
213
249
|
/**
|
|
214
250
|
* The measurement dimension of the bound's value. UI renders the unit
|
|
215
251
|
* inline next to the input (e.g. `4 min`, `100 EUR`). Independent from
|
|
@@ -474,6 +510,18 @@ interface GatekeeperRequest {
|
|
|
474
510
|
execution: Record<string, string | number>;
|
|
475
511
|
/** v0.4: context parameters (currency, action_type, etc.) */
|
|
476
512
|
context?: AgentContextParams;
|
|
513
|
+
/**
|
|
514
|
+
* Authorization path used to scope cumulative lookups in the execution log.
|
|
515
|
+
*
|
|
516
|
+
* Deliberately OUTSIDE `frame`: the frame is hashed and validated against the
|
|
517
|
+
* profile's boundsSchema, so an extra key there is rejected as an unknown
|
|
518
|
+
* field and breaks attestation verification outright. Cumulative checks
|
|
519
|
+
* previously read the path from the frame, where callers cannot legally put
|
|
520
|
+
* it — so it resolved to "" while the log stored real paths, no entry ever
|
|
521
|
+
* matched, and every running total read zero. The local gate existed but
|
|
522
|
+
* could never fire.
|
|
523
|
+
*/
|
|
524
|
+
path?: string;
|
|
477
525
|
}
|
|
478
526
|
/**
|
|
479
527
|
* Structured error from Gatekeeper verification.
|
package/dist/index.js
CHANGED
|
@@ -626,12 +626,23 @@ function checkBoundsFromFrameSchema(request, profile) {
|
|
|
626
626
|
}
|
|
627
627
|
return errors;
|
|
628
628
|
}
|
|
629
|
+
function boundGovernsAction(fieldName, fieldDef, bt, actionType) {
|
|
630
|
+
if (fieldDef.appliesTo) {
|
|
631
|
+
return actionType ? fieldDef.appliesTo.includes(actionType) : true;
|
|
632
|
+
}
|
|
633
|
+
if (bt.kind !== "cumulative_count" || !actionType) return true;
|
|
634
|
+
const prefix = fieldName.replace(/_(?:daily|monthly|weekly)_max$/, "");
|
|
635
|
+
const namedForAnAction = prefix !== fieldName;
|
|
636
|
+
const matches = prefix === actionType || prefix.startsWith("transaction");
|
|
637
|
+
return !namedForAnAction || matches;
|
|
638
|
+
}
|
|
629
639
|
function checkBoundsV4(request, profile, executionLog, now) {
|
|
630
640
|
const errors = [];
|
|
631
641
|
if (!profile.boundsSchema) return errors;
|
|
632
642
|
const bounds = request.frame;
|
|
633
643
|
const profileId = String(bounds.profile ?? profile.id);
|
|
634
|
-
const
|
|
644
|
+
const actionType = typeof request.execution.action_type === "string" ? request.execution.action_type : void 0;
|
|
645
|
+
const path = request.path ?? (bounds.path ? String(bounds.path) : "");
|
|
635
646
|
for (const [fieldName, fieldDef] of Object.entries(profile.boundsSchema.fields)) {
|
|
636
647
|
if (fieldName === "profile" || fieldName === "path") continue;
|
|
637
648
|
const boundValue = bounds[fieldName];
|
|
@@ -647,6 +658,7 @@ function checkBoundsV4(request, profile, executionLog, now) {
|
|
|
647
658
|
});
|
|
648
659
|
continue;
|
|
649
660
|
}
|
|
661
|
+
if (!boundGovernsAction(fieldName, fieldDef, bt, actionType)) continue;
|
|
650
662
|
switch (bt.kind) {
|
|
651
663
|
case "per_transaction": {
|
|
652
664
|
const actual = request.execution[bt.of];
|
|
@@ -745,6 +757,23 @@ function checkContextConstraints(context, execution, profile) {
|
|
|
745
757
|
if (!profile.contextSchema) return errors;
|
|
746
758
|
for (const [fieldName, fieldDef] of Object.entries(profile.contextSchema.fields)) {
|
|
747
759
|
if (!fieldDef.constraint) continue;
|
|
760
|
+
const requiredFor = fieldDef.constraint.requiredFor;
|
|
761
|
+
if (requiredFor?.length) {
|
|
762
|
+
const boundValue = context[fieldName];
|
|
763
|
+
const actualValue = execution[fieldName];
|
|
764
|
+
const actionType = execution["action_type"];
|
|
765
|
+
const constrained = boundValue !== void 0 && boundValue !== "";
|
|
766
|
+
const engaged = actionType !== void 0 && requiredFor.includes(String(actionType));
|
|
767
|
+
if (constrained && engaged && (actualValue === void 0 || actualValue === "")) {
|
|
768
|
+
errors.push({
|
|
769
|
+
code: "BOUND_EXCEEDED",
|
|
770
|
+
field: fieldName,
|
|
771
|
+
message: `Authorization limits "${fieldName}" to [${boundValue}], but this "${String(actionType)}" call exposes no ${fieldName} to check against it. Refusing: the call cannot be shown to stay in scope.`,
|
|
772
|
+
bound: boundValue
|
|
773
|
+
});
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
748
777
|
for (const enforceType of fieldDef.constraint.enforceable) {
|
|
749
778
|
if (enforceType === "enum") {
|
|
750
779
|
const boundValue = context[fieldName];
|
package/dist/index.mjs
CHANGED
|
@@ -555,12 +555,23 @@ function checkBoundsFromFrameSchema(request, profile) {
|
|
|
555
555
|
}
|
|
556
556
|
return errors;
|
|
557
557
|
}
|
|
558
|
+
function boundGovernsAction(fieldName, fieldDef, bt, actionType) {
|
|
559
|
+
if (fieldDef.appliesTo) {
|
|
560
|
+
return actionType ? fieldDef.appliesTo.includes(actionType) : true;
|
|
561
|
+
}
|
|
562
|
+
if (bt.kind !== "cumulative_count" || !actionType) return true;
|
|
563
|
+
const prefix = fieldName.replace(/_(?:daily|monthly|weekly)_max$/, "");
|
|
564
|
+
const namedForAnAction = prefix !== fieldName;
|
|
565
|
+
const matches = prefix === actionType || prefix.startsWith("transaction");
|
|
566
|
+
return !namedForAnAction || matches;
|
|
567
|
+
}
|
|
558
568
|
function checkBoundsV4(request, profile, executionLog, now) {
|
|
559
569
|
const errors = [];
|
|
560
570
|
if (!profile.boundsSchema) return errors;
|
|
561
571
|
const bounds = request.frame;
|
|
562
572
|
const profileId = String(bounds.profile ?? profile.id);
|
|
563
|
-
const
|
|
573
|
+
const actionType = typeof request.execution.action_type === "string" ? request.execution.action_type : void 0;
|
|
574
|
+
const path = request.path ?? (bounds.path ? String(bounds.path) : "");
|
|
564
575
|
for (const [fieldName, fieldDef] of Object.entries(profile.boundsSchema.fields)) {
|
|
565
576
|
if (fieldName === "profile" || fieldName === "path") continue;
|
|
566
577
|
const boundValue = bounds[fieldName];
|
|
@@ -576,6 +587,7 @@ function checkBoundsV4(request, profile, executionLog, now) {
|
|
|
576
587
|
});
|
|
577
588
|
continue;
|
|
578
589
|
}
|
|
590
|
+
if (!boundGovernsAction(fieldName, fieldDef, bt, actionType)) continue;
|
|
579
591
|
switch (bt.kind) {
|
|
580
592
|
case "per_transaction": {
|
|
581
593
|
const actual = request.execution[bt.of];
|
|
@@ -674,6 +686,23 @@ function checkContextConstraints(context, execution, profile) {
|
|
|
674
686
|
if (!profile.contextSchema) return errors;
|
|
675
687
|
for (const [fieldName, fieldDef] of Object.entries(profile.contextSchema.fields)) {
|
|
676
688
|
if (!fieldDef.constraint) continue;
|
|
689
|
+
const requiredFor = fieldDef.constraint.requiredFor;
|
|
690
|
+
if (requiredFor?.length) {
|
|
691
|
+
const boundValue = context[fieldName];
|
|
692
|
+
const actualValue = execution[fieldName];
|
|
693
|
+
const actionType = execution["action_type"];
|
|
694
|
+
const constrained = boundValue !== void 0 && boundValue !== "";
|
|
695
|
+
const engaged = actionType !== void 0 && requiredFor.includes(String(actionType));
|
|
696
|
+
if (constrained && engaged && (actualValue === void 0 || actualValue === "")) {
|
|
697
|
+
errors.push({
|
|
698
|
+
code: "BOUND_EXCEEDED",
|
|
699
|
+
field: fieldName,
|
|
700
|
+
message: `Authorization limits "${fieldName}" to [${boundValue}], but this "${String(actionType)}" call exposes no ${fieldName} to check against it. Refusing: the call cannot be shown to stay in scope.`,
|
|
701
|
+
bound: boundValue
|
|
702
|
+
});
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
677
706
|
for (const enforceType of fieldDef.constraint.enforceable) {
|
|
678
707
|
if (enforceType === "enum") {
|
|
679
708
|
const boundValue = context[fieldName];
|
package/package.json
CHANGED
package/src/gatekeeper.ts
CHANGED
|
@@ -40,6 +40,8 @@ import type {
|
|
|
40
40
|
AgentContextParams,
|
|
41
41
|
ExecutionLogQuery,
|
|
42
42
|
CumulativeFieldDef,
|
|
43
|
+
ProfileBoundsField,
|
|
44
|
+
BoundType,
|
|
43
45
|
} from './types';
|
|
44
46
|
|
|
45
47
|
/**
|
|
@@ -406,6 +408,41 @@ function checkBoundsFromFrameSchema(request: GatekeeperRequest, profile: AgentPr
|
|
|
406
408
|
* enum — bound value must be in the allowed set (attest-time
|
|
407
409
|
* validation; not an execution-time check)
|
|
408
410
|
*/
|
|
411
|
+
/**
|
|
412
|
+
* Does this bound govern the action being attempted?
|
|
413
|
+
*
|
|
414
|
+
* Cumulative totals are scoped by (profileId, path) only, so without this every
|
|
415
|
+
* cumulative_count bound on a profile sees the same running count. On customers
|
|
416
|
+
* — which declares both write_daily_max and delete_daily_max — the smaller
|
|
417
|
+
* delete limit would count writes and block them.
|
|
418
|
+
*
|
|
419
|
+
* Preferred answer is the profile's declared `appliesTo`. Absent that, fall back
|
|
420
|
+
* to the field-name convention (`delete_daily_max` → `delete`), matching how the
|
|
421
|
+
* Authority Server selects bounds so the two enforcement points cannot disagree.
|
|
422
|
+
*
|
|
423
|
+
* Fails CLOSED: when the action type is unknown, or the bound's name implies no
|
|
424
|
+
* particular action, the bound is enforced.
|
|
425
|
+
*/
|
|
426
|
+
function boundGovernsAction(
|
|
427
|
+
fieldName: string,
|
|
428
|
+
fieldDef: ProfileBoundsField,
|
|
429
|
+
bt: BoundType,
|
|
430
|
+
actionType: string | undefined,
|
|
431
|
+
): boolean {
|
|
432
|
+
if (fieldDef.appliesTo) {
|
|
433
|
+
return actionType ? fieldDef.appliesTo.includes(actionType) : true;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// The name convention only ever disambiguated count bounds; sums are governed
|
|
437
|
+
// by their `of` field and stay unfiltered, as at the Authority Server.
|
|
438
|
+
if (bt.kind !== 'cumulative_count' || !actionType) return true;
|
|
439
|
+
|
|
440
|
+
const prefix = fieldName.replace(/_(?:daily|monthly|weekly)_max$/, '');
|
|
441
|
+
const namedForAnAction = prefix !== fieldName;
|
|
442
|
+
const matches = prefix === actionType || prefix.startsWith('transaction');
|
|
443
|
+
return !namedForAnAction || matches;
|
|
444
|
+
}
|
|
445
|
+
|
|
409
446
|
function checkBoundsV4(
|
|
410
447
|
request: GatekeeperRequest,
|
|
411
448
|
profile: AgentProfile,
|
|
@@ -417,7 +454,14 @@ function checkBoundsV4(
|
|
|
417
454
|
|
|
418
455
|
const bounds = request.frame as AgentBoundsParams;
|
|
419
456
|
const profileId = String(bounds.profile ?? profile.id);
|
|
420
|
-
const
|
|
457
|
+
const actionType = typeof request.execution.action_type === 'string'
|
|
458
|
+
? request.execution.action_type
|
|
459
|
+
: undefined;
|
|
460
|
+
// Prefer the explicit request path. `frame.path` is retained only as a
|
|
461
|
+
// fallback for callers that legitimately declare `path` in their profile's
|
|
462
|
+
// boundsSchema; for every shipped profile it is absent, which is exactly why
|
|
463
|
+
// this silently resolved to "" and disabled the cumulative gate entirely.
|
|
464
|
+
const path = request.path ?? (bounds.path ? String(bounds.path) : '');
|
|
421
465
|
|
|
422
466
|
for (const [fieldName, fieldDef] of Object.entries(profile.boundsSchema.fields)) {
|
|
423
467
|
if (fieldName === 'profile' || fieldName === 'path') continue;
|
|
@@ -441,6 +485,8 @@ function checkBoundsV4(
|
|
|
441
485
|
continue;
|
|
442
486
|
}
|
|
443
487
|
|
|
488
|
+
if (!boundGovernsAction(fieldName, fieldDef, bt, actionType)) continue;
|
|
489
|
+
|
|
444
490
|
switch (bt.kind) {
|
|
445
491
|
case 'per_transaction': {
|
|
446
492
|
const actual = request.execution[bt.of];
|
|
@@ -566,6 +612,34 @@ function checkContextConstraints(
|
|
|
566
612
|
for (const [fieldName, fieldDef] of Object.entries(profile.contextSchema.fields)) {
|
|
567
613
|
if (!fieldDef.constraint) continue;
|
|
568
614
|
|
|
615
|
+
// A constrained dimension that the call does not expose cannot be checked.
|
|
616
|
+
// Skipping is only safe when the action does not engage that dimension —
|
|
617
|
+
// deleting a draft has no recipients. When it DOES engage it, silence must
|
|
618
|
+
// not read as compliance: an absent value means the Gatekeeper cannot show
|
|
619
|
+
// the call is in scope, and an unverifiable send is exactly what the scope
|
|
620
|
+
// exists to prevent. The profile declares which actions engage the field.
|
|
621
|
+
const requiredFor = fieldDef.constraint.requiredFor;
|
|
622
|
+
if (requiredFor?.length) {
|
|
623
|
+
const boundValue = context[fieldName];
|
|
624
|
+
const actualValue = execution[fieldName];
|
|
625
|
+
const actionType = execution['action_type'];
|
|
626
|
+
const constrained = boundValue !== undefined && boundValue !== '';
|
|
627
|
+
const engaged = actionType !== undefined && requiredFor.includes(String(actionType));
|
|
628
|
+
|
|
629
|
+
if (constrained && engaged && (actualValue === undefined || actualValue === '')) {
|
|
630
|
+
errors.push({
|
|
631
|
+
code: 'BOUND_EXCEEDED',
|
|
632
|
+
field: fieldName,
|
|
633
|
+
message:
|
|
634
|
+
`Authorization limits "${fieldName}" to [${boundValue}], but this ` +
|
|
635
|
+
`"${String(actionType)}" call exposes no ${fieldName} to check against it. ` +
|
|
636
|
+
`Refusing: the call cannot be shown to stay in scope.`,
|
|
637
|
+
bound: boundValue,
|
|
638
|
+
});
|
|
639
|
+
continue; // nothing further to compare for this field
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
569
643
|
for (const enforceType of fieldDef.constraint.enforceable) {
|
|
570
644
|
if (enforceType === 'enum') {
|
|
571
645
|
// The context field value is the allowed value; execution must match
|
package/src/types.ts
CHANGED
|
@@ -121,6 +121,27 @@ export interface Attestation {
|
|
|
121
121
|
export interface FieldConstraint {
|
|
122
122
|
type: 'number' | 'string';
|
|
123
123
|
enforceable: Array<'max' | 'enum' | 'subset'>;
|
|
124
|
+
/**
|
|
125
|
+
* Action types for which this dimension MUST be present in the execution
|
|
126
|
+
* context. When the grant constrains this field and the action is one of
|
|
127
|
+
* these, an absent value is a denial rather than a skipped check.
|
|
128
|
+
*
|
|
129
|
+
* Without it, a constraint is only enforced against calls that happen to
|
|
130
|
+
* expose the value. Gmail's `send_message` is the live case: pass `raw` and
|
|
131
|
+
* `to` is never populated, so `allowed_recipients` had nothing to compare and
|
|
132
|
+
* the check passed — a send to an unverified recipient looked authorized.
|
|
133
|
+
* `send_draft` has the same shape: it transmits, and the Gatekeeper cannot
|
|
134
|
+
* see to whom.
|
|
135
|
+
*
|
|
136
|
+
* Keyed on `action_type` because "does this action engage recipients?" is a
|
|
137
|
+
* property of the action, not of the field name — deleting a draft has no
|
|
138
|
+
* recipients and must keep passing, while sending one must not. This keeps
|
|
139
|
+
* the engine profile-agnostic: it compares declared strings and knows nothing
|
|
140
|
+
* about email, calendars or payments.
|
|
141
|
+
*
|
|
142
|
+
* Omitted → previous behaviour: absence skips the check.
|
|
143
|
+
*/
|
|
144
|
+
requiredFor?: string[];
|
|
124
145
|
}
|
|
125
146
|
|
|
126
147
|
/**
|
|
@@ -213,6 +234,21 @@ export interface ProfileBoundsField {
|
|
|
213
234
|
* treat a missing boundType as an error when running in v0.4 mode.
|
|
214
235
|
*/
|
|
215
236
|
boundType?: BoundType;
|
|
237
|
+
/**
|
|
238
|
+
* Which execution action types this bound governs, e.g. `["write"]`.
|
|
239
|
+
*
|
|
240
|
+
* When present, this is authoritative. When absent, enforcement falls back to
|
|
241
|
+
* inferring the action type from the FIELD NAME (`send_daily_max` → `send`),
|
|
242
|
+
* which is a convention rather than a contract: a profile that names a bound
|
|
243
|
+
* after the domain concept instead of the action — calendar's
|
|
244
|
+
* `booking_daily_max` against an action type of `write` — matches nothing and
|
|
245
|
+
* is skipped, so a limit the user set is never applied and nothing says so.
|
|
246
|
+
*
|
|
247
|
+
* Declaring it removes the guess. Deliberately additive: profiles migrate one
|
|
248
|
+
* at a time, and every remaining fallback is logged, so the cases still
|
|
249
|
+
* relying on the naming convention become visible instead of staying silent.
|
|
250
|
+
*/
|
|
251
|
+
appliesTo?: string[];
|
|
216
252
|
/**
|
|
217
253
|
* The measurement dimension of the bound's value. UI renders the unit
|
|
218
254
|
* inline next to the input (e.g. `4 min`, `100 EUR`). Independent from
|
|
@@ -502,6 +538,18 @@ export interface GatekeeperRequest {
|
|
|
502
538
|
execution: Record<string, string | number>;
|
|
503
539
|
/** v0.4: context parameters (currency, action_type, etc.) */
|
|
504
540
|
context?: AgentContextParams;
|
|
541
|
+
/**
|
|
542
|
+
* Authorization path used to scope cumulative lookups in the execution log.
|
|
543
|
+
*
|
|
544
|
+
* Deliberately OUTSIDE `frame`: the frame is hashed and validated against the
|
|
545
|
+
* profile's boundsSchema, so an extra key there is rejected as an unknown
|
|
546
|
+
* field and breaks attestation verification outright. Cumulative checks
|
|
547
|
+
* previously read the path from the frame, where callers cannot legally put
|
|
548
|
+
* it — so it resolved to "" while the log stored real paths, no entry ever
|
|
549
|
+
* matched, and every running total read zero. The local gate existed but
|
|
550
|
+
* could never fire.
|
|
551
|
+
*/
|
|
552
|
+
path?: string;
|
|
505
553
|
}
|
|
506
554
|
|
|
507
555
|
/**
|