@coderifts/agent-guard 17.2.0 → 17.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/cas-attestation.d.ts +15 -0
- package/dist/cjs/cas-attestation.d.ts.map +1 -1
- package/dist/cjs/cas-attestation.js +136 -6
- package/dist/cjs/cas-attestation.js.map +1 -1
- package/dist/cjs/vendor/VENDOR.sha256 +29 -9
- package/dist/cjs/vendor/evidence-root.js +188 -0
- package/dist/cjs/vendor/verified-execution-binding.js +507 -0
- package/dist/cjs/vendor/verify-evidence.js +172 -0
- package/dist/cjs/vendor/verify-grant.js +79 -4
- package/dist/cjs/vendor/verify.js +6 -6
- package/dist/esm/cas-attestation.d.ts +15 -0
- package/dist/esm/cas-attestation.d.ts.map +1 -1
- package/dist/esm/cas-attestation.js +136 -6
- package/dist/esm/cas-attestation.js.map +1 -1
- package/dist/esm/vendor/VENDOR.sha256 +29 -9
- package/dist/esm/vendor/evidence-root.js +188 -0
- package/dist/esm/vendor/verified-execution-binding.js +507 -0
- package/dist/esm/vendor/verify-evidence.js +172 -0
- package/dist/esm/vendor/verify-grant.js +79 -4
- package/dist/esm/vendor/verify.js +6 -6
- package/package.json +1 -1
|
@@ -36,8 +36,18 @@ const crypto = require('node:crypto');
|
|
|
36
36
|
const { verifyReceipt, keyringFromDocument } = require('./verify.js');
|
|
37
37
|
const { verifyExecutionGrant } = require('./verify-grant.js');
|
|
38
38
|
const { verifyProveTranscript } = require('./verify-prove-transcript.js');
|
|
39
|
+
const {
|
|
40
|
+
ROOT_V, SLOTS, SLOT_NAMES, digestToken, verifyEvidenceRoot, canonicalJson,
|
|
41
|
+
} = require('./evidence-root.js');
|
|
39
42
|
|
|
40
43
|
const CORRELATION_V = 'cr.exec.correlation.v1';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The version four consumers must agree on. `verifyEvidenceRootBinding` returns it, so "prove,
|
|
47
|
+
* conformance, the guard and the provider all ran the same library" is something a report can
|
|
48
|
+
* SHOW rather than assert. Bump it when a check is added, removed or changed in meaning.
|
|
49
|
+
*/
|
|
50
|
+
const LIBRARY_VERSION = 'cr.evidence-verifier.1';
|
|
41
51
|
const US = '\x1f';
|
|
42
52
|
|
|
43
53
|
/** The slots this verifier knows how to authenticate. */
|
|
@@ -227,7 +237,169 @@ function verifyEvidenceEnvelope(artifact, o = {}) {
|
|
|
227
237
|
return { ok: failures.length === 0, slots, failures };
|
|
228
238
|
}
|
|
229
239
|
|
|
240
|
+
/**
|
|
241
|
+
* THE ROOT CHECK — is this set of tokens ONE run?
|
|
242
|
+
*
|
|
243
|
+
* Ten questions, in the order a reader would ask them. Each is answered against something the
|
|
244
|
+
* producer signed, never against a value copied out of the thing being checked.
|
|
245
|
+
*
|
|
246
|
+
* 1 the root's own signature verifies against the executor key
|
|
247
|
+
* 2 every mandatory slot is present in the root (absence is refused, not skipped)
|
|
248
|
+
* 3 every token PRESENT in the envelope has the EXACT byte digest the root recorded
|
|
249
|
+
* 4 every token the root records is present to be checked, or named as unavailable
|
|
250
|
+
* 5 the grant's claims match the root's claims (grant_id, scope, policy)
|
|
251
|
+
* 6 grant.receipt_hash === sha256(the chain_receipt bytes actually carried)
|
|
252
|
+
* 7 grant_id === the consumed jti === the attested jti
|
|
253
|
+
* 8 run_id === the transcript's run_id === the correlation's scope binding
|
|
254
|
+
* 9 the outer artifact's summaries agree with what the tokens say
|
|
255
|
+
* 10 the verifier reports its own version, so four consumers can be shown to run one library
|
|
256
|
+
*
|
|
257
|
+
* @param {object} artifact
|
|
258
|
+
* @param {object} o
|
|
259
|
+
* @param {import('crypto').KeyObject} o.executorKey the key the root and correlation are signed with
|
|
260
|
+
* @param {object} [o.sidecars] tokens the artifact does not carry but the caller holds, by slot
|
|
261
|
+
* name (e.g. provider_readback bytes, atomic_attestation token)
|
|
262
|
+
*/
|
|
263
|
+
function verifyEvidenceRootBinding(artifact, o = {}) {
|
|
264
|
+
const failures = [];
|
|
265
|
+
const checks = [];
|
|
266
|
+
const note = (id, ok, detail) => { checks.push({ id, ok, detail }); if (!ok) failures.push(detail); };
|
|
267
|
+
|
|
268
|
+
const root = artifact && artifact.evidence_root;
|
|
269
|
+
if (!root) {
|
|
270
|
+
return {
|
|
271
|
+
ok: false,
|
|
272
|
+
present: false,
|
|
273
|
+
library: LIBRARY_VERSION,
|
|
274
|
+
checks: [],
|
|
275
|
+
failures: ['cross_run_collage: the artifact carries no cr.evidence.root.v1, so nothing binds '
|
|
276
|
+
+ 'its tokens to ONE run'],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 1 — the root's own signature.
|
|
281
|
+
const rv = verifyEvidenceRoot(root, o.executorKey);
|
|
282
|
+
note('root_signature', rv.valid,
|
|
283
|
+
rv.valid ? 'the evidence root is signed by the executor key'
|
|
284
|
+
: `the evidence root signature does not verify (${rv.status}: ${rv.reason})`);
|
|
285
|
+
// Everything below reads the root. A root that does not verify is not a source of truth about
|
|
286
|
+
// anything, so the remaining checks are not run rather than run against unsigned values.
|
|
287
|
+
if (!rv.valid) return { ok: false, present: true, library: LIBRARY_VERSION, checks, failures };
|
|
288
|
+
|
|
289
|
+
// The tokens as they travel, by slot. Sidecars are tokens the artifact does not republish but
|
|
290
|
+
// the caller holds — the provider readback is one, and binding it is what stops a readback from
|
|
291
|
+
// another run being paired with this artifact.
|
|
292
|
+
const carried = {
|
|
293
|
+
chain_receipt: artifact.issuance && artifact.issuance.chain_receipt,
|
|
294
|
+
execution_grant: artifact.issuance && artifact.issuance.execution_grant,
|
|
295
|
+
transcript_token: artifact.transcript_token,
|
|
296
|
+
correlation: artifact.correlation || null,
|
|
297
|
+
atomic_attestation: null,
|
|
298
|
+
provider_readback: null,
|
|
299
|
+
...(o.sidecars || {}),
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// 2 — mandatory slots. A root that omits a token would make deletion look like "not applicable".
|
|
303
|
+
for (const name of SLOT_NAMES) {
|
|
304
|
+
if (!SLOTS[name].mandatory) continue;
|
|
305
|
+
note(`root_slot_${name}`, root.artifact_digests && root.artifact_digests[name] != null,
|
|
306
|
+
`the root records no digest for the mandatory ${name}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 3 & 4 — EXACT BYTES. This is the check the collage fails: a substituted token is authentic and
|
|
310
|
+
// has different bytes, so its digest cannot match whatever it says inside.
|
|
311
|
+
for (const name of SLOT_NAMES) {
|
|
312
|
+
const want = root.artifact_digests ? root.artifact_digests[name] : null;
|
|
313
|
+
const got = digestToken(carried[name]);
|
|
314
|
+
if (want == null && got == null) continue;
|
|
315
|
+
if (want == null) {
|
|
316
|
+
note(`digest_${name}`, false,
|
|
317
|
+
`the envelope carries a ${name} the root does not account for — an extra token is not evidence`);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (got == null) {
|
|
321
|
+
// Not a failure for an optional slot the caller simply did not supply: it is UNCHECKED, and
|
|
322
|
+
// saying so beats grading a token nobody looked at.
|
|
323
|
+
note(`digest_${name}`, !SLOTS[name].mandatory,
|
|
324
|
+
`the root records a ${name} digest but no such token was supplied to check it`);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
note(`digest_${name}`, got === want,
|
|
328
|
+
got === want ? `${name} bytes match the root`
|
|
329
|
+
: `${name} does not match the root's digest — these bytes were not emitted by run ${root.run_id}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 5 — the grant's own claims vs the root's.
|
|
333
|
+
const grantBody = (() => {
|
|
334
|
+
const t = carried.execution_grant;
|
|
335
|
+
if (typeof t !== 'string') return null;
|
|
336
|
+
try { return JSON.parse(Buffer.from(t.split('.')[0], 'base64url').toString('utf8')); } catch (_) { return null; }
|
|
337
|
+
})();
|
|
338
|
+
if (grantBody) {
|
|
339
|
+
const gid = grantBody.grant_id || grantBody.jti || null;
|
|
340
|
+
note('claim_grant_id', root.grant_id == null || root.grant_id === gid,
|
|
341
|
+
`the root names grant ${root.grant_id} but the grant it carries is ${gid}`);
|
|
342
|
+
const scope = grantBody.after_payload_hash || grantBody.scope_hash || null;
|
|
343
|
+
note('claim_scope_hash', root.scope_hash == null || root.scope_hash === scope,
|
|
344
|
+
`the root names scope ${root.scope_hash} but the grant scopes ${scope}`);
|
|
345
|
+
note('claim_policy_hash', root.policy_hash == null || grantBody.policy_hash == null
|
|
346
|
+
|| root.policy_hash === grantBody.policy_hash,
|
|
347
|
+
'the root and the grant disagree about policy_hash');
|
|
348
|
+
|
|
349
|
+
// 6 — grant → receipt, by the digest of the receipt actually carried, not by a copied string.
|
|
350
|
+
const rh = grantBody.receipt_hash || grantBody.receipt_digest || null;
|
|
351
|
+
if (rh && typeof carried.chain_receipt === 'string') {
|
|
352
|
+
const actual = digestToken(carried.chain_receipt);
|
|
353
|
+
note('grant_binds_receipt', rh === actual,
|
|
354
|
+
`the grant was issued against receipt ${rh}, but the receipt carried here hashes to ${actual}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 7 — one grant, through consume and attestation. Read from the continuity block the producer
|
|
359
|
+
// signed into the transcript, re-derived rather than trusted: the identities must agree with the
|
|
360
|
+
// root's grant_id too, or the root and the chain are describing different executions.
|
|
361
|
+
const ids = (artifact.continuity && artifact.continuity.identities) || {};
|
|
362
|
+
if (root.grant_id != null && ids.issued_jti != null) {
|
|
363
|
+
const oneGrant = ids.issued_jti === root.grant_id
|
|
364
|
+
&& ids.consumed_jti === root.grant_id
|
|
365
|
+
&& ids.attestation_jti === root.grant_id;
|
|
366
|
+
note('identity_chain', oneGrant,
|
|
367
|
+
`the root names grant ${root.grant_id}, the chain records issued ${ids.issued_jti} / `
|
|
368
|
+
+ `consumed ${ids.consumed_jti} / attested ${ids.attestation_jti}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 8 — one run, through the transcript and the correlation.
|
|
372
|
+
note('run_id_artifact', artifact.run_id === root.run_id,
|
|
373
|
+
`the artifact says run ${artifact.run_id}, the root says ${root.run_id}`);
|
|
374
|
+
if (carried.correlation && root.scope_hash != null) {
|
|
375
|
+
note('run_id_correlation', carried.correlation.scope_hash === root.scope_hash,
|
|
376
|
+
`the correlation binds scope ${carried.correlation.scope_hash}, the root ${root.scope_hash}`);
|
|
377
|
+
}
|
|
378
|
+
if (root.contract_commit != null && carried.correlation) {
|
|
379
|
+
note('contract_commit', carried.correlation.contract_commit === root.contract_commit,
|
|
380
|
+
`the correlation names commit ${carried.correlation.contract_commit}, the root ${root.contract_commit}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 9 — the outer summary vs the tokens. The artifact is a wrapper; a wrapper that disagrees with
|
|
384
|
+
// what it wraps is the thing that is wrong.
|
|
385
|
+
const tv = typeof carried.transcript_token === 'string'
|
|
386
|
+
? verifyProveTranscript(carried.transcript_token, { keyring: null, publicKey: o.executorKey })
|
|
387
|
+
: null;
|
|
388
|
+
if (tv && tv.valid && tv.payload) {
|
|
389
|
+
const claimed = tv.payload.run_id || tv.payload.deployment_id || null;
|
|
390
|
+
if (claimed && tv.payload.run_id) {
|
|
391
|
+
note('transcript_run_id', tv.payload.run_id === root.run_id,
|
|
392
|
+
`the signed transcript is run ${tv.payload.run_id}, the root says ${root.run_id}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return { ok: failures.length === 0, present: true, library: LIBRARY_VERSION, checks, failures };
|
|
397
|
+
}
|
|
398
|
+
|
|
230
399
|
module.exports = {
|
|
400
|
+
verifyEvidenceRootBinding,
|
|
401
|
+
LIBRARY_VERSION,
|
|
402
|
+
ROOT_V,
|
|
231
403
|
verifyEvidenceEnvelope,
|
|
232
404
|
verifyCorrelation,
|
|
233
405
|
verifyAtomicAttestationToken,
|
|
@@ -58,6 +58,35 @@ const V2_REQUIRED_STRINGS = Object.freeze([
|
|
|
58
58
|
'operation', 'target_uri', 'expected_state_token', 'after_payload_hash',
|
|
59
59
|
'nonce_hash', 'policy_hash', 'audience_hash', 'not_before', 'expires_at',
|
|
60
60
|
]);
|
|
61
|
+
/**
|
|
62
|
+
* RESERVED, AND INERT. Optional v2 fields a grant MAY carry and this verifier reads as NOTHING.
|
|
63
|
+
*
|
|
64
|
+
* ── WHY THEY EXIST BEFORE THE GATE THAT USES THEM ───────────────────────────────────────────
|
|
65
|
+
*
|
|
66
|
+
* The admitted key set is closed: an unknown field is `MALFORMED/unknown_field`, which is the
|
|
67
|
+
* right default and also means a future field cannot be introduced without every deployed
|
|
68
|
+
* verifier refusing the grants that carry it. Reserving the two names now is what keeps that
|
|
69
|
+
* introduction from being a breaking change later.
|
|
70
|
+
*
|
|
71
|
+
* ── WHAT THEY DO NOT DO, WHICH IS THE POINT ─────────────────────────────────────────────────
|
|
72
|
+
*
|
|
73
|
+
* NOTHING. A grant carrying `call_hash` is graded EXACTLY as one without it. There is no check,
|
|
74
|
+
* no comparison, and no `intended` field they bind to.
|
|
75
|
+
*
|
|
76
|
+
* A VERIFIER THAT READS THEIR PRESENCE AS AUTHORIZATION IS WRONG. `call_hash` present does not
|
|
77
|
+
* mean a tool call was bound; `executor_image_digest` present does not mean an executor image was
|
|
78
|
+
* pinned. Nothing signs a promise that the value is true, nothing compares it to anything, and an
|
|
79
|
+
* attacker who can mint a grant can put any value in them. Presence is not proof — it is a slot.
|
|
80
|
+
*
|
|
81
|
+
* They are unsigned-by-default only in the sense that no separate signature covers them: the v2
|
|
82
|
+
* signing input is the canonical JSON of the WHOLE body, so a grant that carries them signs
|
|
83
|
+
* different bytes than one that does not, and neither can be edited into the other. That binds
|
|
84
|
+
* the VALUE to the issuer; it says nothing about whether the value means anything.
|
|
85
|
+
*
|
|
86
|
+
* When a gate for them lands it will be a NEW check with its own negative fixtures, and this
|
|
87
|
+
* comment is what a reader should be shown if anyone claims otherwise before then.
|
|
88
|
+
*/
|
|
89
|
+
const V2_RESERVED_INERT = Object.freeze(['call_hash', 'executor_image_digest']);
|
|
61
90
|
const TARGET_SCHEMES = Object.freeze(['fs', 'git', 'api', 'db', 'registry', 'deploy']);
|
|
62
91
|
const DEFAULT_FETCH_URL = 'https://app.coderifts.com/api/v1/attestation/public-key';
|
|
63
92
|
|
|
@@ -70,8 +99,39 @@ function scalar(v) {
|
|
|
70
99
|
return v == null ? '' : String(v);
|
|
71
100
|
}
|
|
72
101
|
|
|
102
|
+
/**
|
|
103
|
+
* cr.exec.v1 OPTIONAL signed fields — the ATOMIC profile's, and a DELIBERATE widening (1470).
|
|
104
|
+
*
|
|
105
|
+
* ── MEASURED BEFORE OPENING THE SET ─────────────────────────────────────────────────────────
|
|
106
|
+
*
|
|
107
|
+
* This verifier's allowed set was `['v', ...SIGNED_FIELDS]`, so a v1 grant carrying `state_nonce`
|
|
108
|
+
* read MALFORMED / unknown_field. That grant is not malformed: it is what the demo executor issues
|
|
109
|
+
* for the ATOMIC profile, and capability-demo's middleware has always signed and accepted it
|
|
110
|
+
* (OPTIONAL_SIGNED_FIELDS = ['state_nonce', 'deployment_id']). Fail-CLOSED, and wrong — this core
|
|
111
|
+
* is vendored into five consumers, so every one of them refused a valid ATOMIC grant.
|
|
112
|
+
*
|
|
113
|
+
* ── WHY WIDENING IS SAFE HERE, AND WHY IT IS STILL A DECISION ───────────────────────────────
|
|
114
|
+
*
|
|
115
|
+
* These fields are SIGNED: appended to the preimage, so a forger cannot add one without breaking
|
|
116
|
+
* the signature. The closed set never protected against injection; it protects against SEMANTIC
|
|
117
|
+
* DRIFT — a future field that RESTRICTS use must not be silently ignored by an older verifier that
|
|
118
|
+
* then says GRANT_CURRENT. `state_nonce` and `deployment_id` do not restrict: they NARROW, and a
|
|
119
|
+
* verifier that ignores them is not more permissive than one that does not know them.
|
|
120
|
+
*
|
|
121
|
+
* So the set is widened BY NAME, not opened. An actually-unknown field is still unknown_field, and
|
|
122
|
+
* test/v1-atomic-optional-fields.test.js records both halves so the next change is a decision
|
|
123
|
+
* rather than a rediscovery.
|
|
124
|
+
*
|
|
125
|
+
* ── APPENDED ONLY WHEN NON-EMPTY ────────────────────────────────────────────────────────────
|
|
126
|
+
*
|
|
127
|
+
* Byte-identical to capability-demo/packages/middleware/src/verify-grant.js. A BEARER grant's
|
|
128
|
+
* signing input must stay exactly what it was before ATOMIC existed, or every pre-ATOMIC issuance
|
|
129
|
+
* stops verifying — which is why presence, not declaration, decides the slot.
|
|
130
|
+
*/
|
|
131
|
+
const V1_OPTIONAL_SIGNED_FIELDS = Object.freeze(['state_nonce', 'deployment_id']);
|
|
132
|
+
|
|
73
133
|
function reconstructSignedInput(payload) {
|
|
74
|
-
|
|
134
|
+
const parts = [
|
|
75
135
|
SIGNING_PREFIX,
|
|
76
136
|
scalar(payload.kid),
|
|
77
137
|
scalar(payload.receipt_digest),
|
|
@@ -82,7 +142,11 @@ function reconstructSignedInput(payload) {
|
|
|
82
142
|
scalar(payload.jti),
|
|
83
143
|
scalar(payload.iat),
|
|
84
144
|
scalar(payload.exp),
|
|
85
|
-
]
|
|
145
|
+
];
|
|
146
|
+
for (const k of V1_OPTIONAL_SIGNED_FIELDS) {
|
|
147
|
+
if (payload[k] != null && String(payload[k]).length > 0) parts.push(String(payload[k]));
|
|
148
|
+
}
|
|
149
|
+
return parts.join('|');
|
|
86
150
|
}
|
|
87
151
|
|
|
88
152
|
function sha256pref(s) {
|
|
@@ -142,7 +206,9 @@ function verifyExecutionGrantV2(payload, sigB64, ctx, opts = {}) {
|
|
|
142
206
|
if (!Number.isInteger(payload.max_attempts) || payload.max_attempts < 1) {
|
|
143
207
|
return { valid: false, status: 'MALFORMED', reason: 'bad_max_attempts', payload };
|
|
144
208
|
}
|
|
145
|
-
|
|
209
|
+
// The reserved names are ADMITTED, never inspected. Everything below this line treats a payload
|
|
210
|
+
// carrying them identically to one that does not — verified by test, not by intent.
|
|
211
|
+
const allowed = new Set([...V2_REQUIRED_STRINGS, 'max_attempts', ...V2_RESERVED_INERT]);
|
|
146
212
|
for (const k of Object.keys(payload)) {
|
|
147
213
|
if (!allowed.has(k)) return { valid: false, status: 'MALFORMED', reason: 'unknown_field', payload };
|
|
148
214
|
}
|
|
@@ -262,13 +328,19 @@ function verifyExecutionGrantInner(token, ctx, opts = {}) {
|
|
|
262
328
|
return { valid: false, status: 'MALFORMED', reason: 'missing_field', payload };
|
|
263
329
|
}
|
|
264
330
|
}
|
|
265
|
-
const allowed = new Set(['v', ...SIGNED_FIELDS]);
|
|
331
|
+
const allowed = new Set(['v', ...SIGNED_FIELDS, ...V1_OPTIONAL_SIGNED_FIELDS]);
|
|
266
332
|
for (const k of Object.keys(payload)) {
|
|
267
333
|
if (!allowed.has(k)) {
|
|
268
334
|
return { valid: false, status: 'MALFORMED', reason: 'unknown_field', payload };
|
|
269
335
|
}
|
|
270
336
|
}
|
|
271
337
|
|
|
338
|
+
for (const k of V1_OPTIONAL_SIGNED_FIELDS) {
|
|
339
|
+
if (payload[k] != null && typeof payload[k] !== 'string') {
|
|
340
|
+
return { valid: false, status: 'MALFORMED', reason: 'missing_field', payload };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
272
344
|
// 3. delimiter guard
|
|
273
345
|
for (const k of SIGNED_FIELDS) {
|
|
274
346
|
if (payload[k].includes('|')) {
|
|
@@ -513,6 +585,9 @@ module.exports = {
|
|
|
513
585
|
SIGNING_PREFIX,
|
|
514
586
|
SIGNING_PREFIX_V2,
|
|
515
587
|
SIGNED_FIELDS,
|
|
588
|
+
V1_OPTIONAL_SIGNED_FIELDS,
|
|
589
|
+
V2_REQUIRED_STRINGS,
|
|
590
|
+
V2_RESERVED_INERT,
|
|
516
591
|
CLOCK_SKEW_LEEWAY_MS,
|
|
517
592
|
isIssuedInFuture,
|
|
518
593
|
};
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
* the command, so it left.
|
|
24
24
|
*
|
|
25
25
|
* CLI usage now:
|
|
26
|
-
* node cli.js <receipt> [--key pub.pem | --keys <url|file>] [--kid <kid>] [--fetch <url>]
|
|
27
|
-
* node cli.js --chain receipts.txt [--key pub.pem | --keys <url|file>] [--kid <kid>] [--fetch <url>]
|
|
26
|
+
* node cli.js <receipt> [--key pub.pem | --keys <url|file>] [--kid <kid>] [--fetch <url>] [--refresh-keys]
|
|
27
|
+
* node cli.js --chain receipts.txt [--key pub.pem | --keys <url|file>] [--kid <kid>] [--fetch <url>] [--refresh-keys]
|
|
28
28
|
*
|
|
29
|
-
* Key discovery: with no --key/--keys,
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* Key discovery: with no --key/--keys/--fetch/--refresh-keys, the command loads the
|
|
30
|
+
* vendored snapshot at keys/coderifts-keys.json (offline). Live fetch is opt-in
|
|
31
|
+
* (--refresh-keys or --fetch <url> / --keys <url>). DEFAULT_FETCH_URL is the well-known
|
|
32
|
+
* registry used by those opt-in flags. This library never fetches.
|
|
33
33
|
* --keys resolves each receipt's key by kid from a registry
|
|
34
34
|
* ({ keys: [{ kid, public_key_pem, status, valid_from, retired_at }] }); accepts a URL or file.
|
|
35
35
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coderifts/agent-guard",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.3.1",
|
|
4
4
|
"description": "Fail-closed guard for AI agent tool calls — preflight contract changes before they execute. Security core frozen (agent-guard-api v1.0); v1.1 adds client-side enforcement: receipt→envelope binding, decision↔action reconciliation, safe_for_agent + degraded fail-closed, and enforced⟺executed (P0 client-enforcement pack).",
|
|
5
5
|
"main": "./dist/cjs/index.js",
|
|
6
6
|
"module": "./dist/esm/index.js",
|