@humanagencyp/hap-core 0.4.3 → 0.4.4
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 +79 -1
- package/dist/index.d.ts +79 -1
- package/dist/index.js +99 -28
- package/dist/index.mjs +99 -28
- package/package.json +12 -11
- package/src/gatekeeper.ts +153 -36
- package/src/types.ts +66 -0
package/dist/index.d.mts
CHANGED
|
@@ -25,6 +25,16 @@ interface AttestationPayload {
|
|
|
25
25
|
execution_context_hash: string;
|
|
26
26
|
resolved_domains: ResolvedDomain[];
|
|
27
27
|
gate_content_hashes: Record<string, string>;
|
|
28
|
+
/**
|
|
29
|
+
* v0.4 — commitment mode chosen by the decision owner at attestation time.
|
|
30
|
+
* - 'automatic': agent may invoke bounded tools without per-action review.
|
|
31
|
+
* - 'review': every tool call requires an approved proposal before the
|
|
32
|
+
* receipt route will sign a receipt.
|
|
33
|
+
*
|
|
34
|
+
* Cryptographically bound in the signed payload so a compromised SP cannot
|
|
35
|
+
* silently flip 'review' → 'automatic'. Absent on v0.3 attestations.
|
|
36
|
+
*/
|
|
37
|
+
commitment_mode?: 'automatic' | 'review';
|
|
28
38
|
issued_at: number;
|
|
29
39
|
expires_at: number;
|
|
30
40
|
}
|
|
@@ -53,8 +63,67 @@ interface ProfileFrameField {
|
|
|
53
63
|
constraint?: FieldConstraint;
|
|
54
64
|
enum?: string[];
|
|
55
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Bound enforcement semantics — how a v0.4 bound is checked.
|
|
68
|
+
*
|
|
69
|
+
* Every bounds field in a v0.4 profile declares a `boundType` so the SP
|
|
70
|
+
* receipt route and hap-core gatekeeper can dispatch on it directly,
|
|
71
|
+
* without parsing field names or guessing conventions. This is the
|
|
72
|
+
* single source of truth for "what does this bound mean at enforcement
|
|
73
|
+
* time" — if a new kind is needed, add a variant here and update the
|
|
74
|
+
* dispatch sites.
|
|
75
|
+
*
|
|
76
|
+
* See `ProfileBoundsField.boundType`.
|
|
77
|
+
*/
|
|
78
|
+
type BoundType =
|
|
79
|
+
/**
|
|
80
|
+
* Per-transaction cap. The execution context field named in `of` must
|
|
81
|
+
* satisfy `execution[of] <= bound` for the current call. No cumulative
|
|
82
|
+
* tracking. Used by: amount_max, recipient_max, booking_duration_max, etc.
|
|
83
|
+
*/
|
|
84
|
+
{
|
|
85
|
+
kind: 'per_transaction';
|
|
86
|
+
of: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Cumulative sum within a time window. The SP maintains a running sum
|
|
90
|
+
* of `execution[of]` across all prior executions in the window; the
|
|
91
|
+
* current call is approved iff `running_sum + execution[of] <= bound`.
|
|
92
|
+
* Used by: amount_daily_max, spend_monthly_max, etc.
|
|
93
|
+
*/
|
|
94
|
+
| {
|
|
95
|
+
kind: 'cumulative_sum';
|
|
96
|
+
of: string;
|
|
97
|
+
window: CumulativeWindow;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Cumulative count within a time window. Every qualifying execution
|
|
101
|
+
* counts as +1; the current call is approved iff
|
|
102
|
+
* `running_count + 1 <= bound`. No execution context field is read.
|
|
103
|
+
* Used by: write_daily_max, post_monthly_max, booking_daily_max, etc.
|
|
104
|
+
*/
|
|
105
|
+
| {
|
|
106
|
+
kind: 'cumulative_count';
|
|
107
|
+
window: CumulativeWindow;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* String bound restricted to a fixed set of allowed values. The bound's
|
|
111
|
+
* value must be one of `values` at attestation time. The gateway
|
|
112
|
+
* tool-proxy gates tool calls based on the stored bound (via the
|
|
113
|
+
* integration manifest's `boundField` + `requiredValue`). Not cumulated,
|
|
114
|
+
* not checked by the SP receipt route — it's a capability flag.
|
|
115
|
+
* Used by: read_access, delete_access, archive_access.
|
|
116
|
+
*/
|
|
117
|
+
| {
|
|
118
|
+
kind: 'enum';
|
|
119
|
+
values: readonly string[];
|
|
120
|
+
};
|
|
56
121
|
/**
|
|
57
122
|
* Bounds field definition within a v0.4 profile.
|
|
123
|
+
*
|
|
124
|
+
* v0.4 adds the required `boundType` — an explicit declaration of how
|
|
125
|
+
* the bound is enforced. The older `constraint.enforceable` pattern is
|
|
126
|
+
* deprecated and superseded by `boundType`.
|
|
58
127
|
*/
|
|
59
128
|
interface ProfileBoundsField {
|
|
60
129
|
type: 'string' | 'number';
|
|
@@ -62,7 +131,16 @@ interface ProfileBoundsField {
|
|
|
62
131
|
description?: string;
|
|
63
132
|
displayName?: string;
|
|
64
133
|
format?: 'email' | 'domain' | 'url' | 'currency';
|
|
134
|
+
/**
|
|
135
|
+
* v0.4 enforcement semantics. Required for all new profiles.
|
|
136
|
+
* Optional here only so the type stays backward compatible with v0.3
|
|
137
|
+
* profiles that predate the boundType convention — consumers must
|
|
138
|
+
* treat a missing boundType as an error when running in v0.4 mode.
|
|
139
|
+
*/
|
|
140
|
+
boundType?: BoundType;
|
|
141
|
+
/** @deprecated v0.4: use boundType instead. */
|
|
65
142
|
constraint?: FieldConstraint;
|
|
143
|
+
/** @deprecated v0.4: use boundType: { kind: 'enum', values: [...] }. */
|
|
66
144
|
enum?: string[];
|
|
67
145
|
}
|
|
68
146
|
/**
|
|
@@ -502,4 +580,4 @@ declare function listProfiles(): string[];
|
|
|
502
580
|
declare function getAllProfiles(): AgentProfile[];
|
|
503
581
|
declare function clearProfiles(): void;
|
|
504
582
|
|
|
505
|
-
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type Attestation, type AttestationHeader, type AttestationPayload, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ResolvedDomain, attestationId, canonicalBounds, canonicalContext, canonicalFrame, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContextHash, computeFrameHash, decodeAttestationBlob, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
|
|
583
|
+
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ResolvedDomain, attestationId, canonicalBounds, canonicalContext, canonicalFrame, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContextHash, computeFrameHash, decodeAttestationBlob, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,16 @@ interface AttestationPayload {
|
|
|
25
25
|
execution_context_hash: string;
|
|
26
26
|
resolved_domains: ResolvedDomain[];
|
|
27
27
|
gate_content_hashes: Record<string, string>;
|
|
28
|
+
/**
|
|
29
|
+
* v0.4 — commitment mode chosen by the decision owner at attestation time.
|
|
30
|
+
* - 'automatic': agent may invoke bounded tools without per-action review.
|
|
31
|
+
* - 'review': every tool call requires an approved proposal before the
|
|
32
|
+
* receipt route will sign a receipt.
|
|
33
|
+
*
|
|
34
|
+
* Cryptographically bound in the signed payload so a compromised SP cannot
|
|
35
|
+
* silently flip 'review' → 'automatic'. Absent on v0.3 attestations.
|
|
36
|
+
*/
|
|
37
|
+
commitment_mode?: 'automatic' | 'review';
|
|
28
38
|
issued_at: number;
|
|
29
39
|
expires_at: number;
|
|
30
40
|
}
|
|
@@ -53,8 +63,67 @@ interface ProfileFrameField {
|
|
|
53
63
|
constraint?: FieldConstraint;
|
|
54
64
|
enum?: string[];
|
|
55
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Bound enforcement semantics — how a v0.4 bound is checked.
|
|
68
|
+
*
|
|
69
|
+
* Every bounds field in a v0.4 profile declares a `boundType` so the SP
|
|
70
|
+
* receipt route and hap-core gatekeeper can dispatch on it directly,
|
|
71
|
+
* without parsing field names or guessing conventions. This is the
|
|
72
|
+
* single source of truth for "what does this bound mean at enforcement
|
|
73
|
+
* time" — if a new kind is needed, add a variant here and update the
|
|
74
|
+
* dispatch sites.
|
|
75
|
+
*
|
|
76
|
+
* See `ProfileBoundsField.boundType`.
|
|
77
|
+
*/
|
|
78
|
+
type BoundType =
|
|
79
|
+
/**
|
|
80
|
+
* Per-transaction cap. The execution context field named in `of` must
|
|
81
|
+
* satisfy `execution[of] <= bound` for the current call. No cumulative
|
|
82
|
+
* tracking. Used by: amount_max, recipient_max, booking_duration_max, etc.
|
|
83
|
+
*/
|
|
84
|
+
{
|
|
85
|
+
kind: 'per_transaction';
|
|
86
|
+
of: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Cumulative sum within a time window. The SP maintains a running sum
|
|
90
|
+
* of `execution[of]` across all prior executions in the window; the
|
|
91
|
+
* current call is approved iff `running_sum + execution[of] <= bound`.
|
|
92
|
+
* Used by: amount_daily_max, spend_monthly_max, etc.
|
|
93
|
+
*/
|
|
94
|
+
| {
|
|
95
|
+
kind: 'cumulative_sum';
|
|
96
|
+
of: string;
|
|
97
|
+
window: CumulativeWindow;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Cumulative count within a time window. Every qualifying execution
|
|
101
|
+
* counts as +1; the current call is approved iff
|
|
102
|
+
* `running_count + 1 <= bound`. No execution context field is read.
|
|
103
|
+
* Used by: write_daily_max, post_monthly_max, booking_daily_max, etc.
|
|
104
|
+
*/
|
|
105
|
+
| {
|
|
106
|
+
kind: 'cumulative_count';
|
|
107
|
+
window: CumulativeWindow;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* String bound restricted to a fixed set of allowed values. The bound's
|
|
111
|
+
* value must be one of `values` at attestation time. The gateway
|
|
112
|
+
* tool-proxy gates tool calls based on the stored bound (via the
|
|
113
|
+
* integration manifest's `boundField` + `requiredValue`). Not cumulated,
|
|
114
|
+
* not checked by the SP receipt route — it's a capability flag.
|
|
115
|
+
* Used by: read_access, delete_access, archive_access.
|
|
116
|
+
*/
|
|
117
|
+
| {
|
|
118
|
+
kind: 'enum';
|
|
119
|
+
values: readonly string[];
|
|
120
|
+
};
|
|
56
121
|
/**
|
|
57
122
|
* Bounds field definition within a v0.4 profile.
|
|
123
|
+
*
|
|
124
|
+
* v0.4 adds the required `boundType` — an explicit declaration of how
|
|
125
|
+
* the bound is enforced. The older `constraint.enforceable` pattern is
|
|
126
|
+
* deprecated and superseded by `boundType`.
|
|
58
127
|
*/
|
|
59
128
|
interface ProfileBoundsField {
|
|
60
129
|
type: 'string' | 'number';
|
|
@@ -62,7 +131,16 @@ interface ProfileBoundsField {
|
|
|
62
131
|
description?: string;
|
|
63
132
|
displayName?: string;
|
|
64
133
|
format?: 'email' | 'domain' | 'url' | 'currency';
|
|
134
|
+
/**
|
|
135
|
+
* v0.4 enforcement semantics. Required for all new profiles.
|
|
136
|
+
* Optional here only so the type stays backward compatible with v0.3
|
|
137
|
+
* profiles that predate the boundType convention — consumers must
|
|
138
|
+
* treat a missing boundType as an error when running in v0.4 mode.
|
|
139
|
+
*/
|
|
140
|
+
boundType?: BoundType;
|
|
141
|
+
/** @deprecated v0.4: use boundType instead. */
|
|
65
142
|
constraint?: FieldConstraint;
|
|
143
|
+
/** @deprecated v0.4: use boundType: { kind: 'enum', values: [...] }. */
|
|
66
144
|
enum?: string[];
|
|
67
145
|
}
|
|
68
146
|
/**
|
|
@@ -502,4 +580,4 @@ declare function listProfiles(): string[];
|
|
|
502
580
|
declare function getAllProfiles(): AgentProfile[];
|
|
503
581
|
declare function clearProfiles(): void;
|
|
504
582
|
|
|
505
|
-
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type Attestation, type AttestationHeader, type AttestationPayload, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ResolvedDomain, attestationId, canonicalBounds, canonicalContext, canonicalFrame, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContextHash, computeFrameHash, decodeAttestationBlob, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
|
|
583
|
+
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ResolvedDomain, attestationId, canonicalBounds, canonicalContext, canonicalFrame, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContextHash, computeFrameHash, decodeAttestationBlob, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
|
package/dist/index.js
CHANGED
|
@@ -435,15 +435,9 @@ async function verifyV4(request, profile, publicKeyHex, now, executionLog) {
|
|
|
435
435
|
if (errors.length > 0) {
|
|
436
436
|
return { approved: false, errors };
|
|
437
437
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return { approved: false, errors: cumulativeErrors };
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
const boundsErrors = checkBoundsFromBoundsSchema(request, profile);
|
|
445
|
-
if (boundsErrors.length > 0) {
|
|
446
|
-
return { approved: false, errors: boundsErrors };
|
|
438
|
+
const v4BoundsErrors = checkBoundsV4(request, profile, executionLog, now);
|
|
439
|
+
if (v4BoundsErrors.length > 0) {
|
|
440
|
+
return { approved: false, errors: v4BoundsErrors };
|
|
447
441
|
}
|
|
448
442
|
if (context && profile.contextSchema && Object.keys(profile.contextSchema.fields).length > 0) {
|
|
449
443
|
const contextErrors = checkContextConstraints(context, request.execution, profile);
|
|
@@ -505,38 +499,115 @@ function checkBoundsFromFrameSchema(request, profile) {
|
|
|
505
499
|
}
|
|
506
500
|
return errors;
|
|
507
501
|
}
|
|
508
|
-
function
|
|
502
|
+
function checkBoundsV4(request, profile, executionLog, now) {
|
|
509
503
|
const errors = [];
|
|
510
|
-
const bounds = request.frame;
|
|
511
504
|
if (!profile.boundsSchema) return errors;
|
|
505
|
+
const bounds = request.frame;
|
|
506
|
+
const profileId = String(bounds.profile ?? profile.id);
|
|
507
|
+
const path = bounds.path ? String(bounds.path) : "";
|
|
512
508
|
for (const [fieldName, fieldDef] of Object.entries(profile.boundsSchema.fields)) {
|
|
513
|
-
if (
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
509
|
+
if (fieldName === "profile" || fieldName === "path") continue;
|
|
510
|
+
const boundValue = bounds[fieldName];
|
|
511
|
+
if (boundValue === void 0) continue;
|
|
512
|
+
const bt = fieldDef.boundType;
|
|
513
|
+
if (!bt) {
|
|
514
|
+
errors.push({
|
|
515
|
+
code: "BOUND_EXCEEDED",
|
|
516
|
+
field: fieldName,
|
|
517
|
+
message: `Profile ${profile.id} bound "${fieldName}" has no boundType \u2014 enforcement semantics undefined. Add boundType to the profile's boundsSchema.`,
|
|
518
|
+
bound: boundValue,
|
|
519
|
+
actual: boundValue
|
|
520
|
+
});
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
switch (bt.kind) {
|
|
524
|
+
case "per_transaction": {
|
|
525
|
+
const actual = request.execution[bt.of];
|
|
526
|
+
if (actual === void 0) continue;
|
|
527
|
+
if (typeof boundValue !== "number" || typeof actual !== "number") {
|
|
522
528
|
errors.push({
|
|
523
529
|
code: "BOUND_EXCEEDED",
|
|
524
|
-
field:
|
|
525
|
-
message: `Bound
|
|
530
|
+
field: bt.of,
|
|
531
|
+
message: `Bound "${fieldName}" requires numeric values (bound=${boundValue}, actual=${actual})`,
|
|
526
532
|
bound: boundValue,
|
|
527
|
-
actual
|
|
533
|
+
actual
|
|
528
534
|
});
|
|
529
|
-
|
|
535
|
+
break;
|
|
530
536
|
}
|
|
531
|
-
if (
|
|
537
|
+
if (actual > boundValue) {
|
|
532
538
|
errors.push({
|
|
533
539
|
code: "BOUND_EXCEEDED",
|
|
534
|
-
field:
|
|
535
|
-
message: `Value ${
|
|
540
|
+
field: bt.of,
|
|
541
|
+
message: `Value ${actual} exceeds authorized maximum of ${boundValue} for ${fieldName}`,
|
|
536
542
|
bound: boundValue,
|
|
537
|
-
actual
|
|
543
|
+
actual
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
case "cumulative_sum": {
|
|
549
|
+
if (typeof boundValue !== "number") {
|
|
550
|
+
errors.push({
|
|
551
|
+
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
552
|
+
field: bt.of,
|
|
553
|
+
message: `Cumulative bound "${fieldName}" must be numeric`,
|
|
554
|
+
bound: boundValue,
|
|
555
|
+
actual: 0
|
|
556
|
+
});
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
if (!executionLog) break;
|
|
560
|
+
const runningTotal = executionLog.sumByWindow(profileId, path, bt.of, bt.window, now);
|
|
561
|
+
const currentRaw = request.execution[bt.of];
|
|
562
|
+
const current = typeof currentRaw === "number" ? currentRaw : currentRaw !== void 0 ? Number(currentRaw) : 0;
|
|
563
|
+
const total = runningTotal + current;
|
|
564
|
+
if (total > boundValue) {
|
|
565
|
+
errors.push({
|
|
566
|
+
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
567
|
+
field: bt.of,
|
|
568
|
+
message: `Cumulative ${bt.window} sum of ${bt.of} (${total}) exceeds limit of ${boundValue} for ${fieldName}`,
|
|
569
|
+
bound: boundValue,
|
|
570
|
+
actual: total
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
case "cumulative_count": {
|
|
576
|
+
if (typeof boundValue !== "number") {
|
|
577
|
+
errors.push({
|
|
578
|
+
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
579
|
+
field: fieldName,
|
|
580
|
+
message: `Cumulative count bound "${fieldName}" must be numeric`,
|
|
581
|
+
bound: boundValue,
|
|
582
|
+
actual: 0
|
|
583
|
+
});
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
if (!executionLog) break;
|
|
587
|
+
const runningCount = executionLog.sumByWindow(profileId, path, "_count", bt.window, now);
|
|
588
|
+
const total = runningCount + 1;
|
|
589
|
+
if (total > boundValue) {
|
|
590
|
+
errors.push({
|
|
591
|
+
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
592
|
+
field: fieldName,
|
|
593
|
+
message: `Cumulative ${bt.window} count (${total}) exceeds limit of ${boundValue} for ${fieldName}`,
|
|
594
|
+
bound: boundValue,
|
|
595
|
+
actual: total
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
case "enum": {
|
|
601
|
+
if (typeof boundValue === "string" && !bt.values.includes(boundValue)) {
|
|
602
|
+
errors.push({
|
|
603
|
+
code: "BOUND_EXCEEDED",
|
|
604
|
+
field: fieldName,
|
|
605
|
+
message: `Bound "${fieldName}"="${boundValue}" is not in allowed values [${bt.values.join(", ")}]`,
|
|
606
|
+
bound: boundValue,
|
|
607
|
+
actual: boundValue
|
|
538
608
|
});
|
|
539
609
|
}
|
|
610
|
+
break;
|
|
540
611
|
}
|
|
541
612
|
}
|
|
542
613
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -373,15 +373,9 @@ async function verifyV4(request, profile, publicKeyHex, now, executionLog) {
|
|
|
373
373
|
if (errors.length > 0) {
|
|
374
374
|
return { approved: false, errors };
|
|
375
375
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
return { approved: false, errors: cumulativeErrors };
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
const boundsErrors = checkBoundsFromBoundsSchema(request, profile);
|
|
383
|
-
if (boundsErrors.length > 0) {
|
|
384
|
-
return { approved: false, errors: boundsErrors };
|
|
376
|
+
const v4BoundsErrors = checkBoundsV4(request, profile, executionLog, now);
|
|
377
|
+
if (v4BoundsErrors.length > 0) {
|
|
378
|
+
return { approved: false, errors: v4BoundsErrors };
|
|
385
379
|
}
|
|
386
380
|
if (context && profile.contextSchema && Object.keys(profile.contextSchema.fields).length > 0) {
|
|
387
381
|
const contextErrors = checkContextConstraints(context, request.execution, profile);
|
|
@@ -443,38 +437,115 @@ function checkBoundsFromFrameSchema(request, profile) {
|
|
|
443
437
|
}
|
|
444
438
|
return errors;
|
|
445
439
|
}
|
|
446
|
-
function
|
|
440
|
+
function checkBoundsV4(request, profile, executionLog, now) {
|
|
447
441
|
const errors = [];
|
|
448
|
-
const bounds = request.frame;
|
|
449
442
|
if (!profile.boundsSchema) return errors;
|
|
443
|
+
const bounds = request.frame;
|
|
444
|
+
const profileId = String(bounds.profile ?? profile.id);
|
|
445
|
+
const path = bounds.path ? String(bounds.path) : "";
|
|
450
446
|
for (const [fieldName, fieldDef] of Object.entries(profile.boundsSchema.fields)) {
|
|
451
|
-
if (
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
447
|
+
if (fieldName === "profile" || fieldName === "path") continue;
|
|
448
|
+
const boundValue = bounds[fieldName];
|
|
449
|
+
if (boundValue === void 0) continue;
|
|
450
|
+
const bt = fieldDef.boundType;
|
|
451
|
+
if (!bt) {
|
|
452
|
+
errors.push({
|
|
453
|
+
code: "BOUND_EXCEEDED",
|
|
454
|
+
field: fieldName,
|
|
455
|
+
message: `Profile ${profile.id} bound "${fieldName}" has no boundType \u2014 enforcement semantics undefined. Add boundType to the profile's boundsSchema.`,
|
|
456
|
+
bound: boundValue,
|
|
457
|
+
actual: boundValue
|
|
458
|
+
});
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
switch (bt.kind) {
|
|
462
|
+
case "per_transaction": {
|
|
463
|
+
const actual = request.execution[bt.of];
|
|
464
|
+
if (actual === void 0) continue;
|
|
465
|
+
if (typeof boundValue !== "number" || typeof actual !== "number") {
|
|
460
466
|
errors.push({
|
|
461
467
|
code: "BOUND_EXCEEDED",
|
|
462
|
-
field:
|
|
463
|
-
message: `Bound
|
|
468
|
+
field: bt.of,
|
|
469
|
+
message: `Bound "${fieldName}" requires numeric values (bound=${boundValue}, actual=${actual})`,
|
|
464
470
|
bound: boundValue,
|
|
465
|
-
actual
|
|
471
|
+
actual
|
|
466
472
|
});
|
|
467
|
-
|
|
473
|
+
break;
|
|
468
474
|
}
|
|
469
|
-
if (
|
|
475
|
+
if (actual > boundValue) {
|
|
470
476
|
errors.push({
|
|
471
477
|
code: "BOUND_EXCEEDED",
|
|
472
|
-
field:
|
|
473
|
-
message: `Value ${
|
|
478
|
+
field: bt.of,
|
|
479
|
+
message: `Value ${actual} exceeds authorized maximum of ${boundValue} for ${fieldName}`,
|
|
474
480
|
bound: boundValue,
|
|
475
|
-
actual
|
|
481
|
+
actual
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
case "cumulative_sum": {
|
|
487
|
+
if (typeof boundValue !== "number") {
|
|
488
|
+
errors.push({
|
|
489
|
+
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
490
|
+
field: bt.of,
|
|
491
|
+
message: `Cumulative bound "${fieldName}" must be numeric`,
|
|
492
|
+
bound: boundValue,
|
|
493
|
+
actual: 0
|
|
494
|
+
});
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
if (!executionLog) break;
|
|
498
|
+
const runningTotal = executionLog.sumByWindow(profileId, path, bt.of, bt.window, now);
|
|
499
|
+
const currentRaw = request.execution[bt.of];
|
|
500
|
+
const current = typeof currentRaw === "number" ? currentRaw : currentRaw !== void 0 ? Number(currentRaw) : 0;
|
|
501
|
+
const total = runningTotal + current;
|
|
502
|
+
if (total > boundValue) {
|
|
503
|
+
errors.push({
|
|
504
|
+
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
505
|
+
field: bt.of,
|
|
506
|
+
message: `Cumulative ${bt.window} sum of ${bt.of} (${total}) exceeds limit of ${boundValue} for ${fieldName}`,
|
|
507
|
+
bound: boundValue,
|
|
508
|
+
actual: total
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
case "cumulative_count": {
|
|
514
|
+
if (typeof boundValue !== "number") {
|
|
515
|
+
errors.push({
|
|
516
|
+
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
517
|
+
field: fieldName,
|
|
518
|
+
message: `Cumulative count bound "${fieldName}" must be numeric`,
|
|
519
|
+
bound: boundValue,
|
|
520
|
+
actual: 0
|
|
521
|
+
});
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
if (!executionLog) break;
|
|
525
|
+
const runningCount = executionLog.sumByWindow(profileId, path, "_count", bt.window, now);
|
|
526
|
+
const total = runningCount + 1;
|
|
527
|
+
if (total > boundValue) {
|
|
528
|
+
errors.push({
|
|
529
|
+
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
530
|
+
field: fieldName,
|
|
531
|
+
message: `Cumulative ${bt.window} count (${total}) exceeds limit of ${boundValue} for ${fieldName}`,
|
|
532
|
+
bound: boundValue,
|
|
533
|
+
actual: total
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
case "enum": {
|
|
539
|
+
if (typeof boundValue === "string" && !bt.values.includes(boundValue)) {
|
|
540
|
+
errors.push({
|
|
541
|
+
code: "BOUND_EXCEEDED",
|
|
542
|
+
field: fieldName,
|
|
543
|
+
message: `Bound "${fieldName}"="${boundValue}" is not in allowed values [${bt.values.join(", ")}]`,
|
|
544
|
+
bound: boundValue,
|
|
545
|
+
actual: boundValue
|
|
476
546
|
});
|
|
477
547
|
}
|
|
548
|
+
break;
|
|
478
549
|
}
|
|
479
550
|
}
|
|
480
551
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanagencyp/hap-core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "Core types, cryptographic primitives, and verification logic for the Human Agency Protocol",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -21,22 +21,23 @@
|
|
|
21
21
|
"dist",
|
|
22
22
|
"src"
|
|
23
23
|
],
|
|
24
|
-
"dependencies": {
|
|
25
|
-
"@noble/ed25519": "^2.1.0"
|
|
26
|
-
},
|
|
27
|
-
"devDependencies": {
|
|
28
|
-
"@types/node": "^20.10.0",
|
|
29
|
-
"tsup": "^8.0.0",
|
|
30
|
-
"typescript": "^5.3.0",
|
|
31
|
-
"vitest": "^1.0.0"
|
|
32
|
-
},
|
|
33
24
|
"scripts": {
|
|
34
25
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
35
26
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
36
27
|
"test": "vitest run",
|
|
37
28
|
"test:watch": "vitest",
|
|
29
|
+
"prepublishOnly": "pnpm build",
|
|
38
30
|
"release:patch": "pnpm version patch && git push --follow-tags",
|
|
39
31
|
"release:minor": "pnpm version minor && git push --follow-tags",
|
|
40
32
|
"release:major": "pnpm version major && git push --follow-tags"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@noble/ed25519": "^2.1.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.10.0",
|
|
39
|
+
"tsup": "^8.0.0",
|
|
40
|
+
"typescript": "^5.3.0",
|
|
41
|
+
"vitest": "^1.0.0"
|
|
41
42
|
}
|
|
42
|
-
}
|
|
43
|
+
}
|
package/src/gatekeeper.ts
CHANGED
|
@@ -264,18 +264,14 @@ async function verifyV4(
|
|
|
264
264
|
return { approved: false, errors };
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
// Check bounds using boundsSchema
|
|
276
|
-
const boundsErrors = checkBoundsFromBoundsSchema(request, profile);
|
|
277
|
-
if (boundsErrors.length > 0) {
|
|
278
|
-
return { approved: false, errors: boundsErrors };
|
|
267
|
+
// v0.4 bounds check — single pass that dispatches on each field's
|
|
268
|
+
// boundType.kind. Replaces the two-pass (resolveCumulativeFields +
|
|
269
|
+
// checkBoundsFromBoundsSchema) approach, which used name-pattern
|
|
270
|
+
// heuristics and silently failed when ctx field names didn't round-trip
|
|
271
|
+
// through `${fieldName}_max`.
|
|
272
|
+
const v4BoundsErrors = checkBoundsV4(request, profile, executionLog, now);
|
|
273
|
+
if (v4BoundsErrors.length > 0) {
|
|
274
|
+
return { approved: false, errors: v4BoundsErrors };
|
|
279
275
|
}
|
|
280
276
|
|
|
281
277
|
// Check context constraints using contextSchema
|
|
@@ -364,49 +360,170 @@ function checkBoundsFromFrameSchema(request: GatekeeperRequest, profile: AgentPr
|
|
|
364
360
|
}
|
|
365
361
|
|
|
366
362
|
/**
|
|
367
|
-
*
|
|
368
|
-
*
|
|
363
|
+
* v0.4 bounds enforcement — dispatches on each bound field's declared
|
|
364
|
+
* `boundType.kind` with no name-pattern heuristics. This is the single
|
|
365
|
+
* source of truth for client-side bounds checking in v0.4.
|
|
366
|
+
*
|
|
367
|
+
* Why this replaces the old two-function approach:
|
|
368
|
+
*
|
|
369
|
+
* checkBoundsFromBoundsSchema (old) stripped "_max" from the bound name
|
|
370
|
+
* to guess the execution context field, which required a fixed naming
|
|
371
|
+
* convention (e.g., "amount_max" → "amount"). Profiles that used other
|
|
372
|
+
* conventions silently failed to enforce.
|
|
373
|
+
*
|
|
374
|
+
* resolveCumulativeFields (old) appended "_max" to the cumulative
|
|
375
|
+
* execution context field name to find its corresponding bound, which
|
|
376
|
+
* required ANOTHER fixed naming convention (e.g., "amount_daily" →
|
|
377
|
+
* "amount_daily_max"). Profiles where the ctx field added a "_count_"
|
|
378
|
+
* infix (write_count_daily → write_daily_max, not
|
|
379
|
+
* write_count_daily_max) silently failed too.
|
|
380
|
+
*
|
|
381
|
+
* The new function ignores ctx field names entirely and reads
|
|
382
|
+
* enforcement semantics from `fieldDef.boundType`:
|
|
383
|
+
*
|
|
384
|
+
* per_transaction — compares `execution[of] <= bound`
|
|
385
|
+
* cumulative_sum — sumByWindow(of, window) + execution[of] <= bound
|
|
386
|
+
* cumulative_count — sumByWindow('_count', window) + 1 <= bound
|
|
387
|
+
* enum — bound value must be in the allowed set (attest-time
|
|
388
|
+
* validation; not an execution-time check)
|
|
369
389
|
*/
|
|
370
|
-
function
|
|
390
|
+
function checkBoundsV4(
|
|
391
|
+
request: GatekeeperRequest,
|
|
392
|
+
profile: AgentProfile,
|
|
393
|
+
executionLog: ExecutionLogQuery | undefined,
|
|
394
|
+
now: number,
|
|
395
|
+
): GatekeeperError[] {
|
|
371
396
|
const errors: GatekeeperError[] = [];
|
|
372
|
-
const bounds = request.frame as AgentBoundsParams;
|
|
373
|
-
|
|
374
397
|
if (!profile.boundsSchema) return errors;
|
|
375
398
|
|
|
376
|
-
|
|
377
|
-
|
|
399
|
+
const bounds = request.frame as AgentBoundsParams;
|
|
400
|
+
const profileId = String(bounds.profile ?? profile.id);
|
|
401
|
+
const path = bounds.path ? String(bounds.path) : '';
|
|
378
402
|
|
|
379
|
-
|
|
403
|
+
for (const [fieldName, fieldDef] of Object.entries(profile.boundsSchema.fields)) {
|
|
404
|
+
if (fieldName === 'profile' || fieldName === 'path') continue;
|
|
380
405
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
// Convention: bounds field "X_max" maps to execution field "X"
|
|
384
|
-
const execField = fieldName.replace(/_max$/, '');
|
|
385
|
-
const boundValue = bounds[fieldName];
|
|
386
|
-
const actualValue = request.execution[execField];
|
|
406
|
+
const boundValue = bounds[fieldName];
|
|
407
|
+
if (boundValue === undefined) continue;
|
|
387
408
|
|
|
388
|
-
|
|
409
|
+
const bt = fieldDef.boundType;
|
|
410
|
+
if (!bt) {
|
|
411
|
+
// v0.4 requires boundType on every non-metadata bound field. A
|
|
412
|
+
// missing boundType is a profile authoring bug — fail closed on
|
|
413
|
+
// enforcement rather than silently skipping (which is what caused
|
|
414
|
+
// the write_daily_max display bug in the first place).
|
|
415
|
+
errors.push({
|
|
416
|
+
code: 'BOUND_EXCEEDED',
|
|
417
|
+
field: fieldName,
|
|
418
|
+
message: `Profile ${profile.id} bound "${fieldName}" has no boundType — enforcement semantics undefined. Add boundType to the profile's boundsSchema.`,
|
|
419
|
+
bound: boundValue,
|
|
420
|
+
actual: boundValue,
|
|
421
|
+
});
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
389
424
|
|
|
390
|
-
|
|
425
|
+
switch (bt.kind) {
|
|
426
|
+
case 'per_transaction': {
|
|
427
|
+
const actual = request.execution[bt.of];
|
|
428
|
+
if (actual === undefined) continue;
|
|
429
|
+
if (typeof boundValue !== 'number' || typeof actual !== 'number') {
|
|
391
430
|
errors.push({
|
|
392
431
|
code: 'BOUND_EXCEEDED',
|
|
393
|
-
field:
|
|
394
|
-
message: `Bound
|
|
432
|
+
field: bt.of,
|
|
433
|
+
message: `Bound "${fieldName}" requires numeric values (bound=${boundValue}, actual=${actual})`,
|
|
395
434
|
bound: boundValue,
|
|
396
|
-
actual
|
|
435
|
+
actual,
|
|
436
|
+
});
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
if (actual > boundValue) {
|
|
440
|
+
errors.push({
|
|
441
|
+
code: 'BOUND_EXCEEDED',
|
|
442
|
+
field: bt.of,
|
|
443
|
+
message: `Value ${actual} exceeds authorized maximum of ${boundValue} for ${fieldName}`,
|
|
444
|
+
bound: boundValue,
|
|
445
|
+
actual,
|
|
397
446
|
});
|
|
398
|
-
continue;
|
|
399
447
|
}
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
400
450
|
|
|
401
|
-
|
|
451
|
+
case 'cumulative_sum': {
|
|
452
|
+
if (typeof boundValue !== 'number') {
|
|
453
|
+
errors.push({
|
|
454
|
+
code: 'CUMULATIVE_LIMIT_EXCEEDED',
|
|
455
|
+
field: bt.of,
|
|
456
|
+
message: `Cumulative bound "${fieldName}" must be numeric`,
|
|
457
|
+
bound: boundValue,
|
|
458
|
+
actual: 0,
|
|
459
|
+
});
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
if (!executionLog) break;
|
|
463
|
+
|
|
464
|
+
const runningTotal = executionLog.sumByWindow(profileId, path, bt.of, bt.window, now);
|
|
465
|
+
const currentRaw = request.execution[bt.of];
|
|
466
|
+
const current = typeof currentRaw === 'number'
|
|
467
|
+
? currentRaw
|
|
468
|
+
: (currentRaw !== undefined ? Number(currentRaw) : 0);
|
|
469
|
+
const total = runningTotal + current;
|
|
470
|
+
|
|
471
|
+
if (total > boundValue) {
|
|
472
|
+
errors.push({
|
|
473
|
+
code: 'CUMULATIVE_LIMIT_EXCEEDED',
|
|
474
|
+
field: bt.of,
|
|
475
|
+
message: `Cumulative ${bt.window} sum of ${bt.of} (${total}) exceeds limit of ${boundValue} for ${fieldName}`,
|
|
476
|
+
bound: boundValue,
|
|
477
|
+
actual: total,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
case 'cumulative_count': {
|
|
484
|
+
if (typeof boundValue !== 'number') {
|
|
485
|
+
errors.push({
|
|
486
|
+
code: 'CUMULATIVE_LIMIT_EXCEEDED',
|
|
487
|
+
field: fieldName,
|
|
488
|
+
message: `Cumulative count bound "${fieldName}" must be numeric`,
|
|
489
|
+
bound: boundValue,
|
|
490
|
+
actual: 0,
|
|
491
|
+
});
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
if (!executionLog) break;
|
|
495
|
+
|
|
496
|
+
const runningCount = executionLog.sumByWindow(profileId, path, '_count', bt.window, now);
|
|
497
|
+
const total = runningCount + 1;
|
|
498
|
+
|
|
499
|
+
if (total > boundValue) {
|
|
500
|
+
errors.push({
|
|
501
|
+
code: 'CUMULATIVE_LIMIT_EXCEEDED',
|
|
502
|
+
field: fieldName,
|
|
503
|
+
message: `Cumulative ${bt.window} count (${total}) exceeds limit of ${boundValue} for ${fieldName}`,
|
|
504
|
+
bound: boundValue,
|
|
505
|
+
actual: total,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
case 'enum': {
|
|
512
|
+
// Enum bounds are capability flags. The stored bound value must
|
|
513
|
+
// be in the allowed set (a profile authoring check, really
|
|
514
|
+
// belongs at attestation time). They are NOT compared against
|
|
515
|
+
// runtime execution here — tool-proxy gates tool calls against
|
|
516
|
+
// the manifest's required value before they reach the gatekeeper.
|
|
517
|
+
if (typeof boundValue === 'string' && !bt.values.includes(boundValue)) {
|
|
402
518
|
errors.push({
|
|
403
519
|
code: 'BOUND_EXCEEDED',
|
|
404
|
-
field:
|
|
405
|
-
message: `
|
|
520
|
+
field: fieldName,
|
|
521
|
+
message: `Bound "${fieldName}"="${boundValue}" is not in allowed values [${bt.values.join(', ')}]`,
|
|
406
522
|
bound: boundValue,
|
|
407
|
-
actual:
|
|
523
|
+
actual: boundValue,
|
|
408
524
|
});
|
|
409
525
|
}
|
|
526
|
+
break;
|
|
410
527
|
}
|
|
411
528
|
}
|
|
412
529
|
}
|
package/src/types.ts
CHANGED
|
@@ -30,6 +30,16 @@ export interface AttestationPayload {
|
|
|
30
30
|
execution_context_hash: string;
|
|
31
31
|
resolved_domains: ResolvedDomain[];
|
|
32
32
|
gate_content_hashes: Record<string, string>;
|
|
33
|
+
/**
|
|
34
|
+
* v0.4 — commitment mode chosen by the decision owner at attestation time.
|
|
35
|
+
* - 'automatic': agent may invoke bounded tools without per-action review.
|
|
36
|
+
* - 'review': every tool call requires an approved proposal before the
|
|
37
|
+
* receipt route will sign a receipt.
|
|
38
|
+
*
|
|
39
|
+
* Cryptographically bound in the signed payload so a compromised SP cannot
|
|
40
|
+
* silently flip 'review' → 'automatic'. Absent on v0.3 attestations.
|
|
41
|
+
*/
|
|
42
|
+
commitment_mode?: 'automatic' | 'review';
|
|
33
43
|
issued_at: number;
|
|
34
44
|
expires_at: number;
|
|
35
45
|
}
|
|
@@ -64,8 +74,55 @@ export interface ProfileFrameField {
|
|
|
64
74
|
enum?: string[];
|
|
65
75
|
}
|
|
66
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Bound enforcement semantics — how a v0.4 bound is checked.
|
|
79
|
+
*
|
|
80
|
+
* Every bounds field in a v0.4 profile declares a `boundType` so the SP
|
|
81
|
+
* receipt route and hap-core gatekeeper can dispatch on it directly,
|
|
82
|
+
* without parsing field names or guessing conventions. This is the
|
|
83
|
+
* single source of truth for "what does this bound mean at enforcement
|
|
84
|
+
* time" — if a new kind is needed, add a variant here and update the
|
|
85
|
+
* dispatch sites.
|
|
86
|
+
*
|
|
87
|
+
* See `ProfileBoundsField.boundType`.
|
|
88
|
+
*/
|
|
89
|
+
export type BoundType =
|
|
90
|
+
/**
|
|
91
|
+
* Per-transaction cap. The execution context field named in `of` must
|
|
92
|
+
* satisfy `execution[of] <= bound` for the current call. No cumulative
|
|
93
|
+
* tracking. Used by: amount_max, recipient_max, booking_duration_max, etc.
|
|
94
|
+
*/
|
|
95
|
+
| { kind: 'per_transaction'; of: string }
|
|
96
|
+
/**
|
|
97
|
+
* Cumulative sum within a time window. The SP maintains a running sum
|
|
98
|
+
* of `execution[of]` across all prior executions in the window; the
|
|
99
|
+
* current call is approved iff `running_sum + execution[of] <= bound`.
|
|
100
|
+
* Used by: amount_daily_max, spend_monthly_max, etc.
|
|
101
|
+
*/
|
|
102
|
+
| { kind: 'cumulative_sum'; of: string; window: CumulativeWindow }
|
|
103
|
+
/**
|
|
104
|
+
* Cumulative count within a time window. Every qualifying execution
|
|
105
|
+
* counts as +1; the current call is approved iff
|
|
106
|
+
* `running_count + 1 <= bound`. No execution context field is read.
|
|
107
|
+
* Used by: write_daily_max, post_monthly_max, booking_daily_max, etc.
|
|
108
|
+
*/
|
|
109
|
+
| { kind: 'cumulative_count'; window: CumulativeWindow }
|
|
110
|
+
/**
|
|
111
|
+
* String bound restricted to a fixed set of allowed values. The bound's
|
|
112
|
+
* value must be one of `values` at attestation time. The gateway
|
|
113
|
+
* tool-proxy gates tool calls based on the stored bound (via the
|
|
114
|
+
* integration manifest's `boundField` + `requiredValue`). Not cumulated,
|
|
115
|
+
* not checked by the SP receipt route — it's a capability flag.
|
|
116
|
+
* Used by: read_access, delete_access, archive_access.
|
|
117
|
+
*/
|
|
118
|
+
| { kind: 'enum'; values: readonly string[] };
|
|
119
|
+
|
|
67
120
|
/**
|
|
68
121
|
* Bounds field definition within a v0.4 profile.
|
|
122
|
+
*
|
|
123
|
+
* v0.4 adds the required `boundType` — an explicit declaration of how
|
|
124
|
+
* the bound is enforced. The older `constraint.enforceable` pattern is
|
|
125
|
+
* deprecated and superseded by `boundType`.
|
|
69
126
|
*/
|
|
70
127
|
export interface ProfileBoundsField {
|
|
71
128
|
type: 'string' | 'number';
|
|
@@ -73,7 +130,16 @@ export interface ProfileBoundsField {
|
|
|
73
130
|
description?: string;
|
|
74
131
|
displayName?: string;
|
|
75
132
|
format?: 'email' | 'domain' | 'url' | 'currency';
|
|
133
|
+
/**
|
|
134
|
+
* v0.4 enforcement semantics. Required for all new profiles.
|
|
135
|
+
* Optional here only so the type stays backward compatible with v0.3
|
|
136
|
+
* profiles that predate the boundType convention — consumers must
|
|
137
|
+
* treat a missing boundType as an error when running in v0.4 mode.
|
|
138
|
+
*/
|
|
139
|
+
boundType?: BoundType;
|
|
140
|
+
/** @deprecated v0.4: use boundType instead. */
|
|
76
141
|
constraint?: FieldConstraint;
|
|
142
|
+
/** @deprecated v0.4: use boundType: { kind: 'enum', values: [...] }. */
|
|
77
143
|
enum?: string[];
|
|
78
144
|
}
|
|
79
145
|
|