@humanagencyp/hap-core 0.4.3 → 0.4.5
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 +116 -33
- package/dist/index.mjs +116 -33
- package/package.json +1 -1
- package/src/gatekeeper.ts +176 -40
- 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,20 +435,26 @@ 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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
438
|
+
const v4BoundsErrors = checkBoundsV4(request, profile, executionLog, now);
|
|
439
|
+
if (v4BoundsErrors.length > 0) {
|
|
440
|
+
return { approved: false, errors: v4BoundsErrors };
|
|
441
|
+
}
|
|
442
|
+
if (profile.contextSchema && Object.keys(profile.contextSchema.fields).length > 0) {
|
|
443
|
+
const enforceableFields = Object.entries(profile.contextSchema.fields).filter(([, def]) => def.constraint?.enforceable?.length);
|
|
444
|
+
if (enforceableFields.length > 0 && !context) {
|
|
445
|
+
return {
|
|
446
|
+
approved: false,
|
|
447
|
+
errors: [{
|
|
448
|
+
code: "BOUND_EXCEEDED",
|
|
449
|
+
message: `Declared context required for constraint enforcement but not provided. Fields: ${enforceableFields.map(([n]) => n).join(", ")}`
|
|
450
|
+
}]
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
if (context) {
|
|
454
|
+
const contextErrors = checkContextConstraints(context, request.execution, profile);
|
|
455
|
+
if (contextErrors.length > 0) {
|
|
456
|
+
return { approved: false, errors: contextErrors };
|
|
457
|
+
}
|
|
452
458
|
}
|
|
453
459
|
}
|
|
454
460
|
return { approved: true };
|
|
@@ -505,38 +511,115 @@ function checkBoundsFromFrameSchema(request, profile) {
|
|
|
505
511
|
}
|
|
506
512
|
return errors;
|
|
507
513
|
}
|
|
508
|
-
function
|
|
514
|
+
function checkBoundsV4(request, profile, executionLog, now) {
|
|
509
515
|
const errors = [];
|
|
510
|
-
const bounds = request.frame;
|
|
511
516
|
if (!profile.boundsSchema) return errors;
|
|
517
|
+
const bounds = request.frame;
|
|
518
|
+
const profileId = String(bounds.profile ?? profile.id);
|
|
519
|
+
const path = bounds.path ? String(bounds.path) : "";
|
|
512
520
|
for (const [fieldName, fieldDef] of Object.entries(profile.boundsSchema.fields)) {
|
|
513
|
-
if (
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
521
|
+
if (fieldName === "profile" || fieldName === "path") continue;
|
|
522
|
+
const boundValue = bounds[fieldName];
|
|
523
|
+
if (boundValue === void 0) continue;
|
|
524
|
+
const bt = fieldDef.boundType;
|
|
525
|
+
if (!bt) {
|
|
526
|
+
errors.push({
|
|
527
|
+
code: "BOUND_EXCEEDED",
|
|
528
|
+
field: fieldName,
|
|
529
|
+
message: `Profile ${profile.id} bound "${fieldName}" has no boundType \u2014 enforcement semantics undefined. Add boundType to the profile's boundsSchema.`,
|
|
530
|
+
bound: boundValue,
|
|
531
|
+
actual: boundValue
|
|
532
|
+
});
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
switch (bt.kind) {
|
|
536
|
+
case "per_transaction": {
|
|
537
|
+
const actual = request.execution[bt.of];
|
|
538
|
+
if (actual === void 0) continue;
|
|
539
|
+
if (typeof boundValue !== "number" || typeof actual !== "number") {
|
|
522
540
|
errors.push({
|
|
523
541
|
code: "BOUND_EXCEEDED",
|
|
524
|
-
field:
|
|
525
|
-
message: `Bound
|
|
542
|
+
field: bt.of,
|
|
543
|
+
message: `Bound "${fieldName}" requires numeric values (bound=${boundValue}, actual=${actual})`,
|
|
526
544
|
bound: boundValue,
|
|
527
|
-
actual
|
|
545
|
+
actual
|
|
528
546
|
});
|
|
529
|
-
|
|
547
|
+
break;
|
|
530
548
|
}
|
|
531
|
-
if (
|
|
549
|
+
if (actual > boundValue) {
|
|
532
550
|
errors.push({
|
|
533
551
|
code: "BOUND_EXCEEDED",
|
|
534
|
-
field:
|
|
535
|
-
message: `Value ${
|
|
552
|
+
field: bt.of,
|
|
553
|
+
message: `Value ${actual} exceeds authorized maximum of ${boundValue} for ${fieldName}`,
|
|
536
554
|
bound: boundValue,
|
|
537
|
-
actual
|
|
555
|
+
actual
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
case "cumulative_sum": {
|
|
561
|
+
if (typeof boundValue !== "number") {
|
|
562
|
+
errors.push({
|
|
563
|
+
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
564
|
+
field: bt.of,
|
|
565
|
+
message: `Cumulative bound "${fieldName}" must be numeric`,
|
|
566
|
+
bound: boundValue,
|
|
567
|
+
actual: 0
|
|
568
|
+
});
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
if (!executionLog) break;
|
|
572
|
+
const runningTotal = executionLog.sumByWindow(profileId, path, bt.of, bt.window, now);
|
|
573
|
+
const currentRaw = request.execution[bt.of];
|
|
574
|
+
const current = typeof currentRaw === "number" ? currentRaw : currentRaw !== void 0 ? Number(currentRaw) : 0;
|
|
575
|
+
const total = runningTotal + current;
|
|
576
|
+
if (total > boundValue) {
|
|
577
|
+
errors.push({
|
|
578
|
+
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
579
|
+
field: bt.of,
|
|
580
|
+
message: `Cumulative ${bt.window} sum of ${bt.of} (${total}) exceeds limit of ${boundValue} for ${fieldName}`,
|
|
581
|
+
bound: boundValue,
|
|
582
|
+
actual: total
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
case "cumulative_count": {
|
|
588
|
+
if (typeof boundValue !== "number") {
|
|
589
|
+
errors.push({
|
|
590
|
+
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
591
|
+
field: fieldName,
|
|
592
|
+
message: `Cumulative count bound "${fieldName}" must be numeric`,
|
|
593
|
+
bound: boundValue,
|
|
594
|
+
actual: 0
|
|
595
|
+
});
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
if (!executionLog) break;
|
|
599
|
+
const runningCount = executionLog.sumByWindow(profileId, path, "_count", bt.window, now);
|
|
600
|
+
const total = runningCount + 1;
|
|
601
|
+
if (total > boundValue) {
|
|
602
|
+
errors.push({
|
|
603
|
+
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
604
|
+
field: fieldName,
|
|
605
|
+
message: `Cumulative ${bt.window} count (${total}) exceeds limit of ${boundValue} for ${fieldName}`,
|
|
606
|
+
bound: boundValue,
|
|
607
|
+
actual: total
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
case "enum": {
|
|
613
|
+
if (typeof boundValue === "string" && !bt.values.includes(boundValue)) {
|
|
614
|
+
errors.push({
|
|
615
|
+
code: "BOUND_EXCEEDED",
|
|
616
|
+
field: fieldName,
|
|
617
|
+
message: `Bound "${fieldName}"="${boundValue}" is not in allowed values [${bt.values.join(", ")}]`,
|
|
618
|
+
bound: boundValue,
|
|
619
|
+
actual: boundValue
|
|
538
620
|
});
|
|
539
621
|
}
|
|
622
|
+
break;
|
|
540
623
|
}
|
|
541
624
|
}
|
|
542
625
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -373,20 +373,26 @@ 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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
376
|
+
const v4BoundsErrors = checkBoundsV4(request, profile, executionLog, now);
|
|
377
|
+
if (v4BoundsErrors.length > 0) {
|
|
378
|
+
return { approved: false, errors: v4BoundsErrors };
|
|
379
|
+
}
|
|
380
|
+
if (profile.contextSchema && Object.keys(profile.contextSchema.fields).length > 0) {
|
|
381
|
+
const enforceableFields = Object.entries(profile.contextSchema.fields).filter(([, def]) => def.constraint?.enforceable?.length);
|
|
382
|
+
if (enforceableFields.length > 0 && !context) {
|
|
383
|
+
return {
|
|
384
|
+
approved: false,
|
|
385
|
+
errors: [{
|
|
386
|
+
code: "BOUND_EXCEEDED",
|
|
387
|
+
message: `Declared context required for constraint enforcement but not provided. Fields: ${enforceableFields.map(([n]) => n).join(", ")}`
|
|
388
|
+
}]
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (context) {
|
|
392
|
+
const contextErrors = checkContextConstraints(context, request.execution, profile);
|
|
393
|
+
if (contextErrors.length > 0) {
|
|
394
|
+
return { approved: false, errors: contextErrors };
|
|
395
|
+
}
|
|
390
396
|
}
|
|
391
397
|
}
|
|
392
398
|
return { approved: true };
|
|
@@ -443,38 +449,115 @@ function checkBoundsFromFrameSchema(request, profile) {
|
|
|
443
449
|
}
|
|
444
450
|
return errors;
|
|
445
451
|
}
|
|
446
|
-
function
|
|
452
|
+
function checkBoundsV4(request, profile, executionLog, now) {
|
|
447
453
|
const errors = [];
|
|
448
|
-
const bounds = request.frame;
|
|
449
454
|
if (!profile.boundsSchema) return errors;
|
|
455
|
+
const bounds = request.frame;
|
|
456
|
+
const profileId = String(bounds.profile ?? profile.id);
|
|
457
|
+
const path = bounds.path ? String(bounds.path) : "";
|
|
450
458
|
for (const [fieldName, fieldDef] of Object.entries(profile.boundsSchema.fields)) {
|
|
451
|
-
if (
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
459
|
+
if (fieldName === "profile" || fieldName === "path") continue;
|
|
460
|
+
const boundValue = bounds[fieldName];
|
|
461
|
+
if (boundValue === void 0) continue;
|
|
462
|
+
const bt = fieldDef.boundType;
|
|
463
|
+
if (!bt) {
|
|
464
|
+
errors.push({
|
|
465
|
+
code: "BOUND_EXCEEDED",
|
|
466
|
+
field: fieldName,
|
|
467
|
+
message: `Profile ${profile.id} bound "${fieldName}" has no boundType \u2014 enforcement semantics undefined. Add boundType to the profile's boundsSchema.`,
|
|
468
|
+
bound: boundValue,
|
|
469
|
+
actual: boundValue
|
|
470
|
+
});
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
switch (bt.kind) {
|
|
474
|
+
case "per_transaction": {
|
|
475
|
+
const actual = request.execution[bt.of];
|
|
476
|
+
if (actual === void 0) continue;
|
|
477
|
+
if (typeof boundValue !== "number" || typeof actual !== "number") {
|
|
460
478
|
errors.push({
|
|
461
479
|
code: "BOUND_EXCEEDED",
|
|
462
|
-
field:
|
|
463
|
-
message: `Bound
|
|
480
|
+
field: bt.of,
|
|
481
|
+
message: `Bound "${fieldName}" requires numeric values (bound=${boundValue}, actual=${actual})`,
|
|
464
482
|
bound: boundValue,
|
|
465
|
-
actual
|
|
483
|
+
actual
|
|
466
484
|
});
|
|
467
|
-
|
|
485
|
+
break;
|
|
468
486
|
}
|
|
469
|
-
if (
|
|
487
|
+
if (actual > boundValue) {
|
|
470
488
|
errors.push({
|
|
471
489
|
code: "BOUND_EXCEEDED",
|
|
472
|
-
field:
|
|
473
|
-
message: `Value ${
|
|
490
|
+
field: bt.of,
|
|
491
|
+
message: `Value ${actual} exceeds authorized maximum of ${boundValue} for ${fieldName}`,
|
|
474
492
|
bound: boundValue,
|
|
475
|
-
actual
|
|
493
|
+
actual
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
case "cumulative_sum": {
|
|
499
|
+
if (typeof boundValue !== "number") {
|
|
500
|
+
errors.push({
|
|
501
|
+
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
502
|
+
field: bt.of,
|
|
503
|
+
message: `Cumulative bound "${fieldName}" must be numeric`,
|
|
504
|
+
bound: boundValue,
|
|
505
|
+
actual: 0
|
|
506
|
+
});
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
if (!executionLog) break;
|
|
510
|
+
const runningTotal = executionLog.sumByWindow(profileId, path, bt.of, bt.window, now);
|
|
511
|
+
const currentRaw = request.execution[bt.of];
|
|
512
|
+
const current = typeof currentRaw === "number" ? currentRaw : currentRaw !== void 0 ? Number(currentRaw) : 0;
|
|
513
|
+
const total = runningTotal + current;
|
|
514
|
+
if (total > boundValue) {
|
|
515
|
+
errors.push({
|
|
516
|
+
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
517
|
+
field: bt.of,
|
|
518
|
+
message: `Cumulative ${bt.window} sum of ${bt.of} (${total}) exceeds limit of ${boundValue} for ${fieldName}`,
|
|
519
|
+
bound: boundValue,
|
|
520
|
+
actual: total
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
case "cumulative_count": {
|
|
526
|
+
if (typeof boundValue !== "number") {
|
|
527
|
+
errors.push({
|
|
528
|
+
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
529
|
+
field: fieldName,
|
|
530
|
+
message: `Cumulative count bound "${fieldName}" must be numeric`,
|
|
531
|
+
bound: boundValue,
|
|
532
|
+
actual: 0
|
|
533
|
+
});
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
if (!executionLog) break;
|
|
537
|
+
const runningCount = executionLog.sumByWindow(profileId, path, "_count", bt.window, now);
|
|
538
|
+
const total = runningCount + 1;
|
|
539
|
+
if (total > boundValue) {
|
|
540
|
+
errors.push({
|
|
541
|
+
code: "CUMULATIVE_LIMIT_EXCEEDED",
|
|
542
|
+
field: fieldName,
|
|
543
|
+
message: `Cumulative ${bt.window} count (${total}) exceeds limit of ${boundValue} for ${fieldName}`,
|
|
544
|
+
bound: boundValue,
|
|
545
|
+
actual: total
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
case "enum": {
|
|
551
|
+
if (typeof boundValue === "string" && !bt.values.includes(boundValue)) {
|
|
552
|
+
errors.push({
|
|
553
|
+
code: "BOUND_EXCEEDED",
|
|
554
|
+
field: fieldName,
|
|
555
|
+
message: `Bound "${fieldName}"="${boundValue}" is not in allowed values [${bt.values.join(", ")}]`,
|
|
556
|
+
bound: boundValue,
|
|
557
|
+
actual: boundValue
|
|
476
558
|
});
|
|
477
559
|
}
|
|
560
|
+
break;
|
|
478
561
|
}
|
|
479
562
|
}
|
|
480
563
|
}
|
package/package.json
CHANGED
package/src/gatekeeper.ts
CHANGED
|
@@ -264,25 +264,40 @@ async function verifyV4(
|
|
|
264
264
|
return { approved: false, errors };
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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 };
|
|
273
275
|
}
|
|
274
276
|
|
|
275
|
-
// Check
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
277
|
+
// Check context constraints using contextSchema.
|
|
278
|
+
// Fail closed: if the profile declares enforceable context constraints but
|
|
279
|
+
// no declared context was supplied, reject — the SP cannot enforce these
|
|
280
|
+
// (it only holds context_hash), so skipping the check would silently
|
|
281
|
+
// bypass scope restrictions like allowed_recipients / allowed_domains.
|
|
282
|
+
if (profile.contextSchema && Object.keys(profile.contextSchema.fields).length > 0) {
|
|
283
|
+
const enforceableFields = Object.entries(profile.contextSchema.fields)
|
|
284
|
+
.filter(([, def]) => def.constraint?.enforceable?.length);
|
|
285
|
+
|
|
286
|
+
if (enforceableFields.length > 0 && !context) {
|
|
287
|
+
return {
|
|
288
|
+
approved: false,
|
|
289
|
+
errors: [{
|
|
290
|
+
code: 'BOUND_EXCEEDED',
|
|
291
|
+
message: `Declared context required for constraint enforcement but not provided. Fields: ${enforceableFields.map(([n]) => n).join(', ')}`,
|
|
292
|
+
}],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
280
295
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
296
|
+
if (context) {
|
|
297
|
+
const contextErrors = checkContextConstraints(context, request.execution, profile);
|
|
298
|
+
if (contextErrors.length > 0) {
|
|
299
|
+
return { approved: false, errors: contextErrors };
|
|
300
|
+
}
|
|
286
301
|
}
|
|
287
302
|
}
|
|
288
303
|
|
|
@@ -364,49 +379,170 @@ function checkBoundsFromFrameSchema(request: GatekeeperRequest, profile: AgentPr
|
|
|
364
379
|
}
|
|
365
380
|
|
|
366
381
|
/**
|
|
367
|
-
*
|
|
368
|
-
*
|
|
382
|
+
* v0.4 bounds enforcement — dispatches on each bound field's declared
|
|
383
|
+
* `boundType.kind` with no name-pattern heuristics. This is the single
|
|
384
|
+
* source of truth for client-side bounds checking in v0.4.
|
|
385
|
+
*
|
|
386
|
+
* Why this replaces the old two-function approach:
|
|
387
|
+
*
|
|
388
|
+
* checkBoundsFromBoundsSchema (old) stripped "_max" from the bound name
|
|
389
|
+
* to guess the execution context field, which required a fixed naming
|
|
390
|
+
* convention (e.g., "amount_max" → "amount"). Profiles that used other
|
|
391
|
+
* conventions silently failed to enforce.
|
|
392
|
+
*
|
|
393
|
+
* resolveCumulativeFields (old) appended "_max" to the cumulative
|
|
394
|
+
* execution context field name to find its corresponding bound, which
|
|
395
|
+
* required ANOTHER fixed naming convention (e.g., "amount_daily" →
|
|
396
|
+
* "amount_daily_max"). Profiles where the ctx field added a "_count_"
|
|
397
|
+
* infix (write_count_daily → write_daily_max, not
|
|
398
|
+
* write_count_daily_max) silently failed too.
|
|
399
|
+
*
|
|
400
|
+
* The new function ignores ctx field names entirely and reads
|
|
401
|
+
* enforcement semantics from `fieldDef.boundType`:
|
|
402
|
+
*
|
|
403
|
+
* per_transaction — compares `execution[of] <= bound`
|
|
404
|
+
* cumulative_sum — sumByWindow(of, window) + execution[of] <= bound
|
|
405
|
+
* cumulative_count — sumByWindow('_count', window) + 1 <= bound
|
|
406
|
+
* enum — bound value must be in the allowed set (attest-time
|
|
407
|
+
* validation; not an execution-time check)
|
|
369
408
|
*/
|
|
370
|
-
function
|
|
409
|
+
function checkBoundsV4(
|
|
410
|
+
request: GatekeeperRequest,
|
|
411
|
+
profile: AgentProfile,
|
|
412
|
+
executionLog: ExecutionLogQuery | undefined,
|
|
413
|
+
now: number,
|
|
414
|
+
): GatekeeperError[] {
|
|
371
415
|
const errors: GatekeeperError[] = [];
|
|
372
|
-
const bounds = request.frame as AgentBoundsParams;
|
|
373
|
-
|
|
374
416
|
if (!profile.boundsSchema) return errors;
|
|
375
417
|
|
|
376
|
-
|
|
377
|
-
|
|
418
|
+
const bounds = request.frame as AgentBoundsParams;
|
|
419
|
+
const profileId = String(bounds.profile ?? profile.id);
|
|
420
|
+
const path = bounds.path ? String(bounds.path) : '';
|
|
378
421
|
|
|
379
|
-
|
|
422
|
+
for (const [fieldName, fieldDef] of Object.entries(profile.boundsSchema.fields)) {
|
|
423
|
+
if (fieldName === 'profile' || fieldName === 'path') continue;
|
|
380
424
|
|
|
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];
|
|
425
|
+
const boundValue = bounds[fieldName];
|
|
426
|
+
if (boundValue === undefined) continue;
|
|
387
427
|
|
|
388
|
-
|
|
428
|
+
const bt = fieldDef.boundType;
|
|
429
|
+
if (!bt) {
|
|
430
|
+
// v0.4 requires boundType on every non-metadata bound field. A
|
|
431
|
+
// missing boundType is a profile authoring bug — fail closed on
|
|
432
|
+
// enforcement rather than silently skipping (which is what caused
|
|
433
|
+
// the write_daily_max display bug in the first place).
|
|
434
|
+
errors.push({
|
|
435
|
+
code: 'BOUND_EXCEEDED',
|
|
436
|
+
field: fieldName,
|
|
437
|
+
message: `Profile ${profile.id} bound "${fieldName}" has no boundType — enforcement semantics undefined. Add boundType to the profile's boundsSchema.`,
|
|
438
|
+
bound: boundValue,
|
|
439
|
+
actual: boundValue,
|
|
440
|
+
});
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
389
443
|
|
|
390
|
-
|
|
444
|
+
switch (bt.kind) {
|
|
445
|
+
case 'per_transaction': {
|
|
446
|
+
const actual = request.execution[bt.of];
|
|
447
|
+
if (actual === undefined) continue;
|
|
448
|
+
if (typeof boundValue !== 'number' || typeof actual !== 'number') {
|
|
391
449
|
errors.push({
|
|
392
450
|
code: 'BOUND_EXCEEDED',
|
|
393
|
-
field:
|
|
394
|
-
message: `Bound
|
|
451
|
+
field: bt.of,
|
|
452
|
+
message: `Bound "${fieldName}" requires numeric values (bound=${boundValue}, actual=${actual})`,
|
|
395
453
|
bound: boundValue,
|
|
396
|
-
actual
|
|
454
|
+
actual,
|
|
455
|
+
});
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
if (actual > boundValue) {
|
|
459
|
+
errors.push({
|
|
460
|
+
code: 'BOUND_EXCEEDED',
|
|
461
|
+
field: bt.of,
|
|
462
|
+
message: `Value ${actual} exceeds authorized maximum of ${boundValue} for ${fieldName}`,
|
|
463
|
+
bound: boundValue,
|
|
464
|
+
actual,
|
|
397
465
|
});
|
|
398
|
-
continue;
|
|
399
466
|
}
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
400
469
|
|
|
401
|
-
|
|
470
|
+
case 'cumulative_sum': {
|
|
471
|
+
if (typeof boundValue !== 'number') {
|
|
472
|
+
errors.push({
|
|
473
|
+
code: 'CUMULATIVE_LIMIT_EXCEEDED',
|
|
474
|
+
field: bt.of,
|
|
475
|
+
message: `Cumulative bound "${fieldName}" must be numeric`,
|
|
476
|
+
bound: boundValue,
|
|
477
|
+
actual: 0,
|
|
478
|
+
});
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
if (!executionLog) break;
|
|
482
|
+
|
|
483
|
+
const runningTotal = executionLog.sumByWindow(profileId, path, bt.of, bt.window, now);
|
|
484
|
+
const currentRaw = request.execution[bt.of];
|
|
485
|
+
const current = typeof currentRaw === 'number'
|
|
486
|
+
? currentRaw
|
|
487
|
+
: (currentRaw !== undefined ? Number(currentRaw) : 0);
|
|
488
|
+
const total = runningTotal + current;
|
|
489
|
+
|
|
490
|
+
if (total > boundValue) {
|
|
491
|
+
errors.push({
|
|
492
|
+
code: 'CUMULATIVE_LIMIT_EXCEEDED',
|
|
493
|
+
field: bt.of,
|
|
494
|
+
message: `Cumulative ${bt.window} sum of ${bt.of} (${total}) exceeds limit of ${boundValue} for ${fieldName}`,
|
|
495
|
+
bound: boundValue,
|
|
496
|
+
actual: total,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
case 'cumulative_count': {
|
|
503
|
+
if (typeof boundValue !== 'number') {
|
|
504
|
+
errors.push({
|
|
505
|
+
code: 'CUMULATIVE_LIMIT_EXCEEDED',
|
|
506
|
+
field: fieldName,
|
|
507
|
+
message: `Cumulative count bound "${fieldName}" must be numeric`,
|
|
508
|
+
bound: boundValue,
|
|
509
|
+
actual: 0,
|
|
510
|
+
});
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
if (!executionLog) break;
|
|
514
|
+
|
|
515
|
+
const runningCount = executionLog.sumByWindow(profileId, path, '_count', bt.window, now);
|
|
516
|
+
const total = runningCount + 1;
|
|
517
|
+
|
|
518
|
+
if (total > boundValue) {
|
|
519
|
+
errors.push({
|
|
520
|
+
code: 'CUMULATIVE_LIMIT_EXCEEDED',
|
|
521
|
+
field: fieldName,
|
|
522
|
+
message: `Cumulative ${bt.window} count (${total}) exceeds limit of ${boundValue} for ${fieldName}`,
|
|
523
|
+
bound: boundValue,
|
|
524
|
+
actual: total,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
case 'enum': {
|
|
531
|
+
// Enum bounds are capability flags. The stored bound value must
|
|
532
|
+
// be in the allowed set (a profile authoring check, really
|
|
533
|
+
// belongs at attestation time). They are NOT compared against
|
|
534
|
+
// runtime execution here — tool-proxy gates tool calls against
|
|
535
|
+
// the manifest's required value before they reach the gatekeeper.
|
|
536
|
+
if (typeof boundValue === 'string' && !bt.values.includes(boundValue)) {
|
|
402
537
|
errors.push({
|
|
403
538
|
code: 'BOUND_EXCEEDED',
|
|
404
|
-
field:
|
|
405
|
-
message: `
|
|
539
|
+
field: fieldName,
|
|
540
|
+
message: `Bound "${fieldName}"="${boundValue}" is not in allowed values [${bt.values.join(', ')}]`,
|
|
406
541
|
bound: boundValue,
|
|
407
|
-
actual:
|
|
542
|
+
actual: boundValue,
|
|
408
543
|
});
|
|
409
544
|
}
|
|
545
|
+
break;
|
|
410
546
|
}
|
|
411
547
|
}
|
|
412
548
|
}
|
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
|
|