@suveren/gateway 0.19.5 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/integrations/crm.json +1 -1
- package/content/integrations/deploy-github.json +1 -1
- package/content/integrations/erp.json +1 -1
- package/content/integrations/mail.json +1 -1
- package/content/integrations/records.json +1 -1
- package/dist/control-plane/index.mjs +21 -13
- package/dist/mcp-server/{chunk-ZRRZFGRL.mjs → chunk-APIQIEUA.mjs} +39 -27
- package/dist/mcp-server/http.mjs +308 -97
- package/dist/mcp-server/report-verify-cli.mjs +1 -1
- package/dist/ui/assets/index-1s1q-XLL.js +116 -0
- package/dist/ui/index.html +1 -1
- package/node_modules/@hap/core/README.md +92 -29
- package/node_modules/@hap/core/dist/index.d.mts +614 -593
- package/node_modules/@hap/core/dist/index.d.ts +614 -593
- package/node_modules/@hap/core/dist/index.js +774 -749
- package/node_modules/@hap/core/dist/index.mjs +744 -724
- package/node_modules/@hap/core/package.json +1 -1
- package/node_modules/@hap/core/src/base64url.ts +39 -0
- package/node_modules/@hap/core/src/commitment-modes.ts +0 -21
- package/node_modules/@hap/core/src/errors.ts +76 -0
- package/node_modules/@hap/core/src/frame.ts +47 -178
- package/node_modules/@hap/core/src/gatekeeper.ts +219 -600
- package/node_modules/@hap/core/src/identity.ts +9 -12
- package/node_modules/@hap/core/src/index.ts +8 -4
- package/node_modules/@hap/core/src/mandate.ts +177 -166
- package/node_modules/@hap/core/src/owner-signature.ts +282 -0
- package/node_modules/@hap/core/src/profile.ts +234 -0
- package/node_modules/@hap/core/src/ticket.ts +148 -0
- package/node_modules/@hap/core/src/types.ts +191 -344
- package/node_modules/@hap/core/src/versions.ts +47 -0
- package/package.json +2 -2
- package/profiles/calendar/0.6.profile.json +191 -0
- package/profiles/charge/0.6.profile.json +211 -0
- package/profiles/customers/0.9.profile.json +182 -0
- package/profiles/delegation/0.3.profile.json +118 -0
- package/profiles/deploy/0.11.profile.json +177 -0
- package/profiles/email/0.8.profile.json +226 -0
- package/profiles/index.json +12 -1
- package/profiles/publish/0.6.profile.json +187 -0
- package/profiles/purchase/0.7.profile.json +235 -0
- package/profiles/records/0.6.profile.json +118 -0
- package/profiles/reporting/0.3.profile.json +98 -0
- package/profiles/sales/0.4.profile.json +291 -0
- package/dist/ui/assets/index-Du68zJhH.js +0 -116
- package/node_modules/@hap/core/src/attestation.ts +0 -173
- package/node_modules/@hap/core/src/receipt.ts +0 -74
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* base64url (RFC 4648 §5), strict — no padding, no standard-base64 characters.
|
|
3
|
+
*
|
|
4
|
+
* protocol.md → *Mandate Payload* rule 3: "Signatures MUST be encoded as
|
|
5
|
+
* base64url without padding ... Implementations MUST NOT use standard base64
|
|
6
|
+
* (which differs in `+`/`/` vs `-`/`_` and in padding) — third-party verifiers
|
|
7
|
+
* reading the spec literally will reject standard-base64 signatures."
|
|
8
|
+
*
|
|
9
|
+
* Earlier hap-core verifiers were lenient: they translated `+`/`/` back from
|
|
10
|
+
* `-`/`_` and re-added padding before decoding, so a standard-base64 signature
|
|
11
|
+
* verified exactly as well as a base64url one — silently accepting the wire
|
|
12
|
+
* format the spec forbids. v0.7 closes that: decoding a string that is not
|
|
13
|
+
* strict base64url throws, never silently repairs it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const STRICT_B64URL = /^[A-Za-z0-9_-]*$/;
|
|
17
|
+
|
|
18
|
+
/** Encode bytes as base64url with no padding. */
|
|
19
|
+
export function toBase64Url(bytes: Uint8Array): string {
|
|
20
|
+
return Buffer.from(bytes).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Decode a STRICT base64url string (no padding, no `+`/`/`/`=`).
|
|
25
|
+
*
|
|
26
|
+
* @throws Error (no protocol code — the caller knows which artifact it was
|
|
27
|
+
* decoding and assigns the code) when the input carries a `+`, `/`, `=`, or
|
|
28
|
+
* any other byte outside the base64url alphabet.
|
|
29
|
+
*/
|
|
30
|
+
export function fromBase64Url(s: string): Uint8Array {
|
|
31
|
+
if (!STRICT_B64URL.test(s)) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`${JSON.stringify(s)} is not strict base64url — standard base64 ` +
|
|
34
|
+
'("+"/"/"/padding) is retired (protocol.md → Mandate Payload, rule 3).',
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const padding = s.length % 4 === 0 ? '' : '='.repeat(4 - (s.length % 4));
|
|
38
|
+
return new Uint8Array(Buffer.from(s + padding, 'base64'));
|
|
39
|
+
}
|
|
@@ -28,24 +28,3 @@ export function allowedCommitmentModes(profile: AgentProfile): readonly Signable
|
|
|
28
28
|
export function isCommitmentModeAllowed(profile: AgentProfile, mode: string): boolean {
|
|
29
29
|
return isSignable(mode) && allowedCommitmentModes(profile).includes(mode);
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Authoring check for a profile's `commitment_modes` declaration — for
|
|
34
|
-
* hap-profiles CI, an authoring tool or a test. Like `validateBoundsRequiredFor`
|
|
35
|
-
* it is not wired into profile loading; enforcement reads the declaration
|
|
36
|
-
* fail-closed through `allowedCommitmentModes`. Empty array = nothing to fix.
|
|
37
|
-
*/
|
|
38
|
-
export function validateCommitmentModes(profile: AgentProfile): string[] {
|
|
39
|
-
const declared = (profile as { commitment_modes?: unknown }).commitment_modes;
|
|
40
|
-
if (declared === undefined) return [];
|
|
41
|
-
if (!Array.isArray(declared)) return ['commitment_modes must be a list of modes.'];
|
|
42
|
-
const errors: string[] = [];
|
|
43
|
-
if (declared.length === 0) errors.push('commitment_modes is empty — no mandate could ever be signed under this profile.');
|
|
44
|
-
const seen = new Set<unknown>();
|
|
45
|
-
for (const m of declared) {
|
|
46
|
-
if (!isSignable(m)) errors.push(`commitment_modes: "${String(m)}" is not a mode a signer can choose (${SIGNABLE_COMMITMENT_MODES.join(', ')}).`);
|
|
47
|
-
if (seen.has(m)) errors.push(`commitment_modes: "${String(m)}" appears twice.`);
|
|
48
|
-
seen.add(m);
|
|
49
|
-
}
|
|
50
|
-
return errors;
|
|
51
|
-
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical v0.7 error codes — protocol.md → *Error Codes*.
|
|
3
|
+
*
|
|
4
|
+
* "Error codes are canonical across the protocol. Implementations MUST emit
|
|
5
|
+
* exactly these codes and MUST NOT alias them under different names." This
|
|
6
|
+
* module is the single list both the Gatekeeper (local checks) and any AS
|
|
7
|
+
* client built on this library read from, so the two cannot drift.
|
|
8
|
+
*
|
|
9
|
+
* v0.7 renamed every code that carried a retired word (attestation → mandate,
|
|
10
|
+
* context → scope, receipt → ticket) — see protocol.md → *Migration from
|
|
11
|
+
* v0.6* → "Error-code renames" for the old→new map. There is no alias: a v0.6
|
|
12
|
+
* code name is simply gone from this list, by design (no backward
|
|
13
|
+
* compatibility — CLAUDE.md "Decided by the owner").
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Codes an Authority Server (or a local Gatekeeper check) emits refusing a mandate request. */
|
|
17
|
+
export const MANDATE_ERROR_CODES = [
|
|
18
|
+
'BOUNDS_HASH_MISMATCH',
|
|
19
|
+
'SCOPE_HASH_MISMATCH',
|
|
20
|
+
'INVALID_SIGNATURE',
|
|
21
|
+
'OWNER_NOT_COVERED',
|
|
22
|
+
'TTL_EXPIRED',
|
|
23
|
+
'PROFILE_NOT_FOUND',
|
|
24
|
+
'PROFILE_INVALID',
|
|
25
|
+
'PROFILE_HASH_MISMATCH',
|
|
26
|
+
'COVERAGE_INSUFFICIENT',
|
|
27
|
+
'MALFORMED_MANDATE',
|
|
28
|
+
'VERSION_UNSUPPORTED',
|
|
29
|
+
'OWNER_NOT_APPROVER',
|
|
30
|
+
'BOUNDS_INVALID_VALUE',
|
|
31
|
+
'SCOPE_INVALID_VALUE',
|
|
32
|
+
'ABOVE_CAP_CONFIG_INVALID',
|
|
33
|
+
'OWNER_SIGNATURE_REQUIRED',
|
|
34
|
+
'OWNER_SIGNATURE_INVALID',
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
/** Codes an Authority Server emits refusing a ticket request. */
|
|
38
|
+
export const TICKET_ERROR_CODES = [
|
|
39
|
+
'MANDATE_NOT_FOUND',
|
|
40
|
+
'MANDATE_EXPIRED',
|
|
41
|
+
'MANDATE_REVOKED',
|
|
42
|
+
'BOUND_EXCEEDED',
|
|
43
|
+
'CUMULATIVE_LIMIT_EXCEEDED',
|
|
44
|
+
'COVERAGE_INSUFFICIENT',
|
|
45
|
+
'PROFILE_NOT_FOUND',
|
|
46
|
+
'INVALID_ACTION_TYPE',
|
|
47
|
+
'APPROVAL_REQUIRED',
|
|
48
|
+
'PROPOSAL_REQUIRED',
|
|
49
|
+
'PROPOSAL_NOT_FOUND',
|
|
50
|
+
'PROPOSAL_NOT_APPROVED',
|
|
51
|
+
'PROPOSAL_EXPIRED',
|
|
52
|
+
'PROPOSAL_REJECTED',
|
|
53
|
+
'PROPOSAL_MISMATCH',
|
|
54
|
+
'PROPOSAL_MANDATE_MISMATCH',
|
|
55
|
+
'PROPOSAL_ALREADY_EXECUTED',
|
|
56
|
+
'MALFORMED_TICKET_REQUEST',
|
|
57
|
+
'IDEMPOTENCY_KEY_REQUIRED',
|
|
58
|
+
'IDEMPOTENCY_MISMATCH',
|
|
59
|
+
'APPROVAL_SIGNATURE_REQUIRED',
|
|
60
|
+
'APPROVAL_SIGNATURE_INVALID',
|
|
61
|
+
] as const;
|
|
62
|
+
|
|
63
|
+
/** The full canonical surface — every code a conformant implementation may emit. */
|
|
64
|
+
export type HapErrorCode = (typeof MANDATE_ERROR_CODES)[number] | (typeof TICKET_ERROR_CODES)[number];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A refusal carrying one of the canonical codes above. Thrown by local
|
|
68
|
+
* (Gatekeeper-side, no-wire-round-trip) checks in this library; an AS client
|
|
69
|
+
* built on hap-core maps a wire `{ code, message }` error onto the same type.
|
|
70
|
+
*/
|
|
71
|
+
export class HapError extends Error {
|
|
72
|
+
constructor(public readonly code: HapErrorCode, message: string) {
|
|
73
|
+
super(`${code}: ${message}`);
|
|
74
|
+
this.name = 'HapError';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -1,102 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Bounds & Scope Canonicalization (protocol.md → *Bounds & Scope
|
|
3
|
+
* Canonicalization*).
|
|
3
4
|
*
|
|
4
|
-
* Agent profiles support mixed-type fields (strings and numbers).
|
|
5
5
|
* Canonical form: `key=value` records joined with LF, keys in the profile's
|
|
6
6
|
* keyOrder. Values are stringified with String(value) — the shortest
|
|
7
7
|
* round-trippable form for numbers — then percent-encoded per protocol.md
|
|
8
8
|
* (`=`, `%`, and every byte outside printable ASCII); a value carrying a raw
|
|
9
9
|
* LF/CR is refused, never normalized. See `canonicalRecords` below.
|
|
10
10
|
*
|
|
11
|
-
* v0.3
|
|
12
|
-
* v0.
|
|
11
|
+
* v0.3's `frameSchema` / `canonicalFrame` / `computeFrameHash` are retired
|
|
12
|
+
* with the rest of v0.3 (CLAUDE.md "Decided by the owner": no backward
|
|
13
|
+
* compatibility). v0.7 renames `contextSchema` → `scopeSchema` and
|
|
14
|
+
* `AgentContextParams` → `AgentScopeParams` (protocol.md → *Migration from
|
|
15
|
+
* v0.6*); the functions below follow suit (`canonicalContext` →
|
|
16
|
+
* `canonicalScope`, `computeContextHash` → `computeScopeHash`,
|
|
17
|
+
* `validateContextParams` → `validateScopeParams`).
|
|
13
18
|
*/
|
|
14
19
|
|
|
15
20
|
import { createHash } from 'crypto';
|
|
16
|
-
import type {
|
|
21
|
+
import type { AgentBoundsParams, AgentScopeParams, AgentProfile } from './types';
|
|
17
22
|
|
|
18
|
-
// ───
|
|
23
|
+
// ─── Bounds Validation ────────────────────────────────────────────────────────
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
|
-
* Validates
|
|
22
|
-
*/
|
|
23
|
-
export function validateFrameParams(
|
|
24
|
-
params: AgentFrameParams,
|
|
25
|
-
profile: AgentProfile
|
|
26
|
-
): { valid: boolean; errors: string[] } {
|
|
27
|
-
const errors: string[] = [];
|
|
28
|
-
|
|
29
|
-
if (!profile.frameSchema) {
|
|
30
|
-
return { valid: false, errors: ['Profile does not have a frameSchema'] };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Check all required fields are present
|
|
34
|
-
for (const [fieldName, fieldDef] of Object.entries(profile.frameSchema.fields)) {
|
|
35
|
-
if (fieldDef.required && !(fieldName in params)) {
|
|
36
|
-
errors.push(`Missing required field: ${fieldName}`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Validate each provided field
|
|
41
|
-
for (const [field, value] of Object.entries(params)) {
|
|
42
|
-
const fieldDef = profile.frameSchema.fields[field];
|
|
43
|
-
if (!fieldDef) {
|
|
44
|
-
errors.push(`Unknown field "${field}" not defined in profile ${profile.id}`);
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Type check
|
|
49
|
-
if (fieldDef.type === 'number' && typeof value !== 'number') {
|
|
50
|
-
errors.push(`Field "${field}" must be a number, got ${typeof value}`);
|
|
51
|
-
}
|
|
52
|
-
if (fieldDef.type === 'string' && typeof value !== 'string') {
|
|
53
|
-
errors.push(`Field "${field}" must be a string, got ${typeof value}`);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return { valid: errors.length === 0, errors };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Builds the canonical frame string from parameters.
|
|
62
|
-
* All values are converted to strings. Keys are ordered per profile's keyOrder.
|
|
63
|
-
*
|
|
64
|
-
* @throws Error if any field fails validation
|
|
65
|
-
*/
|
|
66
|
-
export function canonicalFrame(params: AgentFrameParams, profile: AgentProfile): string {
|
|
67
|
-
const validation = validateFrameParams(params, profile);
|
|
68
|
-
if (!validation.valid) {
|
|
69
|
-
throw new Error(`Invalid frame parameters: ${validation.errors.join('; ')}`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const lines = profile.frameSchema!.keyOrder.map(
|
|
73
|
-
(key) => `${key}=${String(params[key])}`
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
return lines.join('\n');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Computes the frame hash from a canonical frame string.
|
|
81
|
-
*
|
|
82
|
-
* @returns Hash in format "sha256:<64 hex chars>"
|
|
83
|
-
*/
|
|
84
|
-
export function frameHash(canonicalFrameString: string): string {
|
|
85
|
-
const hash = createHash('sha256').update(canonicalFrameString, 'utf8').digest('hex');
|
|
86
|
-
return `sha256:${hash}`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Convenience: builds canonical frame and computes hash in one step.
|
|
91
|
-
*/
|
|
92
|
-
export function computeFrameHash(params: AgentFrameParams, profile: AgentProfile): string {
|
|
93
|
-
return frameHash(canonicalFrame(params, profile));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ─── v0.4 Bounds Functions ────────────────────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Validates bounds parameters against the profile's boundsSchema (v0.4).
|
|
26
|
+
* Validates bounds parameters against the profile's boundsSchema.
|
|
100
27
|
*/
|
|
101
28
|
export function validateBoundsParams(
|
|
102
29
|
params: AgentBoundsParams,
|
|
@@ -135,25 +62,27 @@ export function validateBoundsParams(
|
|
|
135
62
|
return { valid: errors.length === 0, errors };
|
|
136
63
|
}
|
|
137
64
|
|
|
65
|
+
// ─── Scope Validation ─────────────────────────────────────────────────────────
|
|
66
|
+
|
|
138
67
|
/**
|
|
139
|
-
* Validates
|
|
68
|
+
* Validates scope parameters against the profile's scopeSchema.
|
|
140
69
|
*/
|
|
141
|
-
export function
|
|
142
|
-
params:
|
|
70
|
+
export function validateScopeParams(
|
|
71
|
+
params: AgentScopeParams,
|
|
143
72
|
profile: AgentProfile
|
|
144
73
|
): { valid: boolean; errors: string[] } {
|
|
145
74
|
const errors: string[] = [];
|
|
146
75
|
|
|
147
|
-
if (!profile.
|
|
148
|
-
// No
|
|
76
|
+
if (!profile.scopeSchema) {
|
|
77
|
+
// No scopeSchema is valid — empty scope
|
|
149
78
|
if (Object.keys(params).length > 0) {
|
|
150
|
-
errors.push('Profile does not have a
|
|
79
|
+
errors.push('Profile does not have a scopeSchema but scope params were provided');
|
|
151
80
|
}
|
|
152
81
|
return { valid: errors.length === 0, errors };
|
|
153
82
|
}
|
|
154
83
|
|
|
155
84
|
// Check all required fields are present
|
|
156
|
-
for (const [fieldName, fieldDef] of Object.entries(profile.
|
|
85
|
+
for (const [fieldName, fieldDef] of Object.entries(profile.scopeSchema.fields)) {
|
|
157
86
|
if (fieldDef.required && !(fieldName in params)) {
|
|
158
87
|
errors.push(`Missing required field: ${fieldName}`);
|
|
159
88
|
}
|
|
@@ -161,9 +90,9 @@ export function validateContextParams(
|
|
|
161
90
|
|
|
162
91
|
// Validate each provided field
|
|
163
92
|
for (const [field, value] of Object.entries(params)) {
|
|
164
|
-
const fieldDef = profile.
|
|
93
|
+
const fieldDef = profile.scopeSchema.fields[field];
|
|
165
94
|
if (!fieldDef) {
|
|
166
|
-
errors.push(`Unknown field "${field}" not defined in
|
|
95
|
+
errors.push(`Unknown field "${field}" not defined in scopeSchema of profile ${profile.id}`);
|
|
167
96
|
continue;
|
|
168
97
|
}
|
|
169
98
|
|
|
@@ -179,69 +108,7 @@ export function validateContextParams(
|
|
|
179
108
|
return { valid: errors.length === 0, errors };
|
|
180
109
|
}
|
|
181
110
|
|
|
182
|
-
|
|
183
|
-
* Authoring-time check for `per_transaction` bound `requiredFor` (v0.8+):
|
|
184
|
-
* entries must name a real `action_type` from the profile's declared
|
|
185
|
-
* `boundsSchema.actionTypes` registry, and the field is only meaningful on
|
|
186
|
-
* `per_transaction` bounds (see `BoundType`'s doc comment for why the other
|
|
187
|
-
* kinds don't carry it).
|
|
188
|
-
*
|
|
189
|
-
* This is NOT wired into `registerProfile` or any enforcement path — nothing
|
|
190
|
-
* in hap-core validates profile JSON at load time today (the pre-existing
|
|
191
|
-
* `requiredFor` on a context/scope constraint has the same gap), so adding
|
|
192
|
-
* an automatic refusal here would be new behaviour beyond this change's
|
|
193
|
-
* scope. Call it explicitly from an authoring tool, a hap-profiles CI check,
|
|
194
|
-
* or a test, to catch a `requiredFor` entry that can never match (misspelled
|
|
195
|
-
* action type) or is attached to a bound kind that silently ignores it.
|
|
196
|
-
*
|
|
197
|
-
* Returns an empty array when the profile has no boundsSchema or nothing to
|
|
198
|
-
* check — never throws.
|
|
199
|
-
*/
|
|
200
|
-
export function validateBoundsRequiredFor(profile: AgentProfile): string[] {
|
|
201
|
-
const errors: string[] = [];
|
|
202
|
-
const boundsSchema = profile.boundsSchema;
|
|
203
|
-
if (!boundsSchema) return errors;
|
|
204
|
-
|
|
205
|
-
const actionTypes = boundsSchema.actionTypes ? [...boundsSchema.actionTypes] : undefined;
|
|
206
|
-
|
|
207
|
-
for (const [fieldName, fieldDef] of Object.entries(boundsSchema.fields)) {
|
|
208
|
-
const bt = fieldDef.boundType as (BoundType & { requiredFor?: unknown }) | undefined;
|
|
209
|
-
if (!bt) continue;
|
|
210
|
-
const requiredFor = (bt as { requiredFor?: unknown }).requiredFor;
|
|
211
|
-
if (requiredFor === undefined) continue;
|
|
212
|
-
|
|
213
|
-
if (!Array.isArray(requiredFor) || !requiredFor.every((v) => typeof v === 'string')) {
|
|
214
|
-
errors.push(`Bound "${fieldName}": requiredFor must be a string array.`);
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (bt.kind !== 'per_transaction') {
|
|
219
|
-
errors.push(
|
|
220
|
-
`Bound "${fieldName}": requiredFor is only meaningful on a per_transaction bound ` +
|
|
221
|
-
`(found on kind "${bt.kind}") — every enforcement point ignores it there. Remove it.`,
|
|
222
|
-
);
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (actionTypes) {
|
|
227
|
-
for (const actionType of requiredFor) {
|
|
228
|
-
if (!actionTypes.includes(actionType)) {
|
|
229
|
-
errors.push(
|
|
230
|
-
`Bound "${fieldName}": requiredFor names action type "${actionType}", which is not ` +
|
|
231
|
-
`in this profile's boundsSchema.actionTypes registry [${actionTypes.join(', ')}]. ` +
|
|
232
|
-
`It can never match a real execution and the requirement is dead.`,
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
// No actionTypes registry declared: membership cannot be checked (same
|
|
238
|
-
// tolerance the AS receipt route applies to pre-registry profiles).
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return errors;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// ─── Value Encoding (normative, v0.5+) ───────────────────────────────────────
|
|
111
|
+
// ─── Value Encoding (normative, protocol.md → *Bounds & Scope Canonicalization*) ─
|
|
245
112
|
|
|
246
113
|
/**
|
|
247
114
|
* Thrown when a value cannot be canonicalized at all — currently only for raw
|
|
@@ -255,10 +122,10 @@ export function validateBoundsRequiredFor(profile: AgentProfile): string[] {
|
|
|
255
122
|
* does not faithfully represent the input."
|
|
256
123
|
*/
|
|
257
124
|
export class CanonicalValueError extends Error {
|
|
258
|
-
readonly code: 'BOUNDS_INVALID_VALUE' | '
|
|
125
|
+
readonly code: 'BOUNDS_INVALID_VALUE' | 'SCOPE_INVALID_VALUE';
|
|
259
126
|
readonly field: string;
|
|
260
127
|
|
|
261
|
-
constructor(code: 'BOUNDS_INVALID_VALUE' | '
|
|
128
|
+
constructor(code: 'BOUNDS_INVALID_VALUE' | 'SCOPE_INVALID_VALUE', field: string, message: string) {
|
|
262
129
|
super(message);
|
|
263
130
|
this.name = 'CanonicalValueError';
|
|
264
131
|
this.code = code;
|
|
@@ -275,7 +142,9 @@ export class CanonicalValueError extends Error {
|
|
|
275
142
|
* - every byte outside printable ASCII 0x20–0x7E
|
|
276
143
|
*
|
|
277
144
|
* LF and CR are deliberately NOT in this list: they are refused upstream, so
|
|
278
|
-
* encoding them is unreachable (v0.7 removed the spec's contradiction here
|
|
145
|
+
* encoding them is unreachable (v0.7 removed the spec's own contradiction here
|
|
146
|
+
* — earlier versions listed them in the percent-encode rule too, which read
|
|
147
|
+
* as permission to encode what the line above already refuses).
|
|
279
148
|
*
|
|
280
149
|
* This runs at canonicalization time only. Stored values keep the human's
|
|
281
150
|
* original bytes.
|
|
@@ -294,7 +163,7 @@ function percentEncodeCanonicalValue(raw: string): string {
|
|
|
294
163
|
}
|
|
295
164
|
|
|
296
165
|
/**
|
|
297
|
-
* The one place `key=value` records are built for bounds and
|
|
166
|
+
* The one place `key=value` records are built for bounds and scope.
|
|
298
167
|
*
|
|
299
168
|
* Rules applied here (all normative, protocol.md → *Bounds & Scope
|
|
300
169
|
* Canonicalization*):
|
|
@@ -306,7 +175,7 @@ function percentEncodeCanonicalValue(raw: string): string {
|
|
|
306
175
|
* - a key with no value is OMITTED entirely — it emits no record
|
|
307
176
|
*
|
|
308
177
|
* On the omission rule: required keys are guaranteed present by the caller's
|
|
309
|
-
* validation (`validateBoundsParams` / `
|
|
178
|
+
* validation (`validateBoundsParams` / `validateScopeParams` reject a missing
|
|
310
179
|
* required field), so "explicit inclusion of all required keys" still holds.
|
|
311
180
|
* What remains are *optional* keys the human never set. Rendering those as the
|
|
312
181
|
* literal string "undefined" — the pre-fix behaviour — hashed a JavaScript
|
|
@@ -319,7 +188,7 @@ function percentEncodeCanonicalValue(raw: string): string {
|
|
|
319
188
|
function canonicalRecords(
|
|
320
189
|
params: Record<string, string | number | undefined>,
|
|
321
190
|
keyOrder: string[],
|
|
322
|
-
code: 'BOUNDS_INVALID_VALUE' | '
|
|
191
|
+
code: 'BOUNDS_INVALID_VALUE' | 'SCOPE_INVALID_VALUE',
|
|
323
192
|
): string {
|
|
324
193
|
const lines: string[] = [];
|
|
325
194
|
|
|
@@ -361,30 +230,30 @@ export function canonicalBounds(params: AgentBoundsParams, profile: AgentProfile
|
|
|
361
230
|
}
|
|
362
231
|
|
|
363
232
|
/**
|
|
364
|
-
* Builds the canonical
|
|
365
|
-
* Keys are ordered per profile's
|
|
233
|
+
* Builds the canonical scope string from parameters.
|
|
234
|
+
* Keys are ordered per profile's scopeSchema.keyOrder; values are encoded per
|
|
366
235
|
* protocol.md → *Value encoding* (see `canonicalRecords`).
|
|
367
|
-
* For empty
|
|
236
|
+
* For empty scope (no scopeSchema or no fields), returns "".
|
|
368
237
|
*
|
|
369
238
|
* @throws Error if any field fails validation
|
|
370
|
-
* @throws CanonicalValueError (code
|
|
239
|
+
* @throws CanonicalValueError (code SCOPE_INVALID_VALUE) if a value carries a raw LF/CR
|
|
371
240
|
*/
|
|
372
|
-
export function
|
|
373
|
-
// No
|
|
374
|
-
if (!profile.
|
|
241
|
+
export function canonicalScope(params: AgentScopeParams, profile: AgentProfile): string {
|
|
242
|
+
// No scopeSchema or no fields → empty scope
|
|
243
|
+
if (!profile.scopeSchema || Object.keys(profile.scopeSchema.fields).length === 0) {
|
|
375
244
|
return '';
|
|
376
245
|
}
|
|
377
246
|
|
|
378
|
-
const validation =
|
|
247
|
+
const validation = validateScopeParams(params, profile);
|
|
379
248
|
if (!validation.valid) {
|
|
380
|
-
throw new Error(`Invalid
|
|
249
|
+
throw new Error(`Invalid scope parameters: ${validation.errors.join('; ')}`);
|
|
381
250
|
}
|
|
382
251
|
|
|
383
|
-
return canonicalRecords(params, profile.
|
|
252
|
+
return canonicalRecords(params, profile.scopeSchema.keyOrder, 'SCOPE_INVALID_VALUE');
|
|
384
253
|
}
|
|
385
254
|
|
|
386
255
|
/**
|
|
387
|
-
* Computes the bounds hash from bounds parameters
|
|
256
|
+
* Computes the bounds hash from bounds parameters.
|
|
388
257
|
*
|
|
389
258
|
* @returns Hash in format "sha256:<64 hex chars>"
|
|
390
259
|
*/
|
|
@@ -395,14 +264,14 @@ export function computeBoundsHash(params: AgentBoundsParams, profile: AgentProfi
|
|
|
395
264
|
}
|
|
396
265
|
|
|
397
266
|
/**
|
|
398
|
-
* Computes the
|
|
399
|
-
* For empty
|
|
267
|
+
* Computes the scope hash from scope parameters.
|
|
268
|
+
* For empty scope {}, returns the sha256 of "":
|
|
400
269
|
* "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
401
270
|
*
|
|
402
271
|
* @returns Hash in format "sha256:<64 hex chars>"
|
|
403
272
|
*/
|
|
404
|
-
export function
|
|
405
|
-
const canonical =
|
|
273
|
+
export function computeScopeHash(params: AgentScopeParams, profile: AgentProfile): string {
|
|
274
|
+
const canonical = canonicalScope(params, profile);
|
|
406
275
|
const hash = createHash('sha256').update(canonical, 'utf8').digest('hex');
|
|
407
276
|
return `sha256:${hash}`;
|
|
408
277
|
}
|