-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathverify.html
More file actions
552 lines (492 loc) · 57.1 KB
/
Copy pathverify.html
File metadata and controls
552 lines (492 loc) · 57.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Verify a record · Observer Protocol</title>
<meta name="description" content="You have an Observer Protocol artifact and want to know what it establishes. Here is how to check it yourself, what each field proves, and what it does not." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600&family=Fraunces:ital,wght@0,300;0,600;0,900;1,300;1,600&display=swap" rel="stylesheet" />
<style>
:root {
--bg:#0a0a08; --bg2:#111110; --bg3:#1a1a18; --border:#2a2a26;
--amber:#f5a623; --amber-dim:#b8791a; --amber-glow:#f5a62322;
--green:#4ade80; --green-dim:#166534; --red:#f87171; --red-dim:#7f1d1d;
--text:#e8e8e0; --text-dim:#888880; --text-faint:#444440; --teal:#5eead4;
}
*,*::before,*::after { box-sizing:border-box; margin:0; padding:0; }
html { scroll-behavior:smooth; }
body { background:var(--bg); color:var(--text); font-family:'IBM Plex Sans',sans-serif; font-size:16px; line-height:1.6; overflow-x:hidden; }
code { font-family:'IBM Plex Mono',monospace; font-size:0.92em; color:var(--teal); word-break:break-word; }
nav { position:fixed; top:0; left:0; right:0; z-index:100; display:flex; align-items:center; justify-content:space-between; padding:0 2.5rem; height:56px; background:rgba(10,10,8,0.92); backdrop-filter:blur(12px); border-bottom:1px solid var(--border); }
.nav-logo { font-family:'IBM Plex Mono',monospace; font-size:0.8rem; font-weight:500; color:var(--amber); letter-spacing:0.12em; text-transform:uppercase; text-decoration:none; }
.nav-links { display:flex; gap:2rem; list-style:none; }
.nav-links a { font-family:'IBM Plex Mono',monospace; font-size:0.72rem; color:var(--text-dim); text-decoration:none; letter-spacing:0.06em; }
.nav-links a:hover { color:var(--text); }
.nav-links a.active { color:var(--amber); }
.nav-status { font-family:'IBM Plex Mono',monospace; font-size:0.68rem; color:var(--text-dim); }
@media (max-width:900px) { .nav-links { display:none; } }
.page-header { padding:9rem 2.5rem 3.5rem; border-bottom:1px solid var(--border); }
.page-header-inner { max-width:900px; margin:0 auto; }
.breadcrumb { font-family:'IBM Plex Mono',monospace; font-size:0.68rem; color:var(--text-faint); letter-spacing:0.1em; margin-bottom:1.5rem; }
.breadcrumb span { color:var(--amber); }
h1 { font-family:'Fraunces',serif; font-size:clamp(2.2rem,4vw,3.4rem); font-weight:900; line-height:1.08; letter-spacing:-0.02em; margin-bottom:1.25rem; }
.page-sub { font-size:1.02rem; color:var(--text-dim); max-width:680px; line-height:1.8; font-weight:300; }
section { padding:4rem 2.5rem; }
.section-inner { max-width:1000px; margin:0 auto; }
.section-label { font-family:'IBM Plex Mono',monospace; font-size:0.68rem; letter-spacing:0.16em; text-transform:uppercase; color:var(--amber); margin-bottom:1rem; }
h2 { font-family:'Fraunces',serif; font-size:clamp(1.6rem,2.5vw,2.3rem); font-weight:600; line-height:1.18; margin-bottom:1rem; }
h3 { font-family:'Fraunces',serif; font-size:1.25rem; font-weight:600; margin-bottom:0.75rem; }
.lede { font-size:0.98rem; color:var(--text-dim); max-width:720px; line-height:1.85; margin-bottom:2rem; font-weight:300; }
p.body { font-size:0.94rem; color:var(--text-dim); line-height:1.85; font-weight:300; margin-bottom:1rem; max-width:760px; }
p.body strong { color:var(--text); font-weight:500; }
.code { background:var(--bg2); border:1px solid var(--border); font-family:'IBM Plex Mono',monospace; font-size:0.75rem; line-height:1.85; overflow-x:auto; margin:1.5rem 0; }
.code .head { padding:0.6rem 1rem; border-bottom:1px solid var(--border); background:var(--bg3); font-size:0.62rem; letter-spacing:0.12em; text-transform:uppercase; color:var(--text-faint); display:flex; justify-content:space-between; gap:1rem; flex-wrap:wrap; }
.code pre { padding:1.25rem; margin:0; white-space:pre; }
.c { color:var(--text-faint); } .k { color:var(--amber); } .s { color:var(--teal); } .o { color:var(--green); } .bad { color:var(--red); }
.files { display:grid; grid-template-columns:repeat(3,1fr); gap:1px; background:var(--border); margin:2rem 0; }
@media (max-width:820px) { .files { grid-template-columns:1fr; } }
.file { background:var(--bg2); padding:1.5rem; display:flex; flex-direction:column; gap:0.6rem; }
.file .verdict { font-family:'IBM Plex Mono',monospace; font-size:0.62rem; letter-spacing:0.1em; text-transform:uppercase; padding:0.2rem 0.5rem; border:1px solid; align-self:flex-start; }
.file .verdict.pass { color:var(--green); border-color:var(--green-dim); background:#4ade8011; }
.file .verdict.fail { color:var(--red); border-color:var(--red-dim); background:#f8717111; }
.file .fname { font-family:'IBM Plex Mono',monospace; font-size:0.72rem; color:var(--text); word-break:break-all; }
.file .why { font-size:0.84rem; color:var(--text-dim); line-height:1.7; font-weight:300; }
.file .reason { font-family:'IBM Plex Mono',monospace; font-size:0.66rem; color:var(--text-faint); line-height:1.6; border-top:1px solid var(--border); padding-top:0.6rem; margin-top:auto; }
.file a { font-family:'IBM Plex Mono',monospace; font-size:0.68rem; color:var(--amber); text-decoration:none; }
.fields { border:1px solid var(--border); margin-top:2rem; }
.field { border-bottom:1px solid var(--border); padding:1.5rem 1.75rem; }
.field:last-child { border-bottom:none; }
.field-name { font-family:'IBM Plex Mono',monospace; font-size:0.78rem; color:var(--amber); margin-bottom:0.9rem; word-break:break-all; }
.field-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:1rem 2rem; }
@media (max-width:720px) { .field-grid { grid-template-columns:1fr; } }
.fg { font-size:0.87rem; line-height:1.75; font-weight:300; color:var(--text-dim); }
.fg b { display:block; font-family:'IBM Plex Mono',monospace; font-size:0.6rem; letter-spacing:0.12em; text-transform:uppercase; color:var(--text-faint); margin-bottom:0.3rem; font-weight:500; }
.fg.limit { color:var(--text-dim); }
.fg.limit b { color:var(--amber-dim); }
.callout { border-left:2px solid var(--amber-dim); background:var(--bg2); padding:1.5rem 1.75rem; margin:2rem 0; }
.callout h4 { font-family:'IBM Plex Mono',monospace; font-size:0.68rem; letter-spacing:0.12em; text-transform:uppercase; color:var(--amber); margin-bottom:0.75rem; }
.callout p { font-size:0.9rem; color:var(--text-dim); line-height:1.8; font-weight:300; margin-bottom:0.75rem; }
.callout p:last-child { margin-bottom:0; }
footer { border-top:1px solid var(--border); padding:3rem 2.5rem 2rem; }
.footer-inner { max-width:1000px; margin:0 auto; display:flex; justify-content:space-between; gap:2rem; flex-wrap:wrap; }
.footer-links { display:flex; gap:1.75rem; flex-wrap:wrap; }
.footer-links a { font-family:'IBM Plex Mono',monospace; font-size:0.68rem; color:var(--text-faint); text-decoration:none; }
.footer-links a:hover { color:var(--text-dim); }
.footer-note { max-width:1000px; margin:2rem auto 0; padding-top:1.5rem; border-top:1px solid var(--border); font-family:'IBM Plex Mono',monospace; font-size:0.62rem; color:var(--text-faint); line-height:1.9; }
</style>
</head>
<body>
<nav>
<a class="nav-logo" href="/">Observer Protocol</a>
<ul class="nav-links">
<li><a href="/verify.html" class="active">Verify</a></li>
<li><a href="/check.html">Check</a></li>
<li><a href="/supervisors.html">Supervisors</a></li>
<li><a href="/architecture.html">Architecture</a></li>
<li><a href="/docs.html">Docs</a></li>
<li><a href="/registry.html">Registry</a></li>
<li><a href="/agentic-terminal.html">Agentic Terminal</a></li>
</ul>
<div class="nav-status">AIP v0.9</div>
</nav>
<div class="page-header">
<div class="page-header-inner">
<div class="breadcrumb">OBSERVER PROTOCOL · <span>VERIFY A RECORD</span></div>
<h1>You have a delegation credential or a decision attestation.<br/>Here is how to check it.</h1>
<p class="page-sub">
This page is for the party doing the checking: a supervisor, an external auditor, a counterparty's risk function, someone contesting a determination made about them. <strong data-shared-copy="verifiable-set">It covers delegation credentials, refusal records, lapse records and decision attestations, which is what the published engine knows how to read</strong> — if you are holding a <code>PolicyEvaluationCredential</code>, see the note in section 02 before you start. You are not our customer and you should not have to become one. Everything below runs on your machine, against artifacts we had no part in putting in your hands.
</p>
</div>
</div>
<!-- THE OFFLINE PATH -->
<section style="background:var(--bg2); border-bottom:1px solid var(--border);">
<div class="section-inner">
<div class="section-label" data-shared-copy="offline-scope">01 · The offline path</div>
<h2>Install, one call, done.</h2>
<p class="lede">
No API key. No bearer token. No account. <span data-shared-copy="offline-scope">Point the verifier at a local copy of the issuer's DID document and it makes no network call at all — not to us, not to anyone.</span> That is the mode to use when the artifact you are holding should not leave your machine.
</p>
<div class="code">
<div class="head"><span>verify.mjs</span><span>@observer-protocol/policy-engine <span data-engine-version="current">1.0.0-rc.22</span> · MIT</span></div>
<pre><span class="c">$</span> <span class="k">npm install</span> @observer-protocol/policy-engine
<span class="c">$</span> <span class="k">curl -O</span> https://observerprotocol.org/verify-samples/verifies-delegation-mandate.json
<span class="k">import</span> { verifyCredentialObject } <span class="k">from</span> <span class="s">'@observer-protocol/policy-engine'</span>;
<span class="k">import</span> fs <span class="k">from</span> <span class="s">'node:fs'</span>;
<span class="k">const</span> credential = JSON.parse(fs.readFileSync(<span class="s">'./verifies-delegation-mandate.json'</span>));
<span class="k">const</span> config = {
credentialPath: <span class="s">'verifies-delegation-mandate.json'</span>,
<span class="c">// Pin the issuer you expect. A verifier that trusts whoever the</span>
<span class="c">// credential names as its issuer is not verifying anything,</span>
<span class="c">// it is agreeing. A mismatch here is a denial.</span>
issuerDid: <span class="s">'did:web:bitcoinsingularity.ai'</span>,
schemaAllowlist: [<span class="s">'https://observerprotocol.org/schemas/delegation/v2.2.json'</span>],
revocation: { maxStalenessHours: 24, onUnreachable: <span class="s">'cache-then-deny'</span>, fetchTimeoutMs: 5000 },
didCache: { maxStalenessHours: 24 },
cacheDir: <span class="s">'/tmp/op-cache'</span>, auditLog: <span class="s">'/tmp/op-cache/audit.log'</span>,
rails: {}, allowContractCalls: <span class="k">false</span>,
<span class="c">// offline: { didDocumentPath: './did.json' } // no network at all</span>
};
console.log(<span class="k">await</span> verifyCredentialObject(credential, config, Date.now()));</pre>
</div>
<div class="code">
<div class="head"><span>what that prints</span><span>measured 8 August 2026</span></div>
<pre><span class="o">{
allow: true,
reason: 'credential verified',
notes: [ 'credential carries no credentialStatus entry —
revocation not checkable for this credential' ],
checks: {
validityWindow: 'passed',
issuerResolution: 'network',
issuerProof: 'eddsa-jcs-2022-verified',
signerBoundary: 'not-configured',
revocation: 'status-absent'
}
}</span></pre>
</div>
<p class="body">
<strong>Read the checks, not the verdict.</strong> <code>allow: true</code> on its own tells you almost nothing. This run says the signature verified against a key published at the issuer's own domain, and it also says that no signer boundary was configured and that this credential carries no revocation entry, so neither of those was established. A verifier that returned only the green result would have hidden both. Both are recorded in the section below as limits of this artifact.
</p>
</div>
</section>
<!-- THE FAILING CASES -->
<section style="background:var(--bg); border-bottom:1px solid var(--border);">
<div class="section-inner">
<div class="section-label">02 · The cases that must fail</div>
<h2>A verifier that says yes to everything teaches you nothing.</h2>
<p class="lede">
The fastest way to establish that a tool is real is to hand you the inputs it must reject. All three files below are served from this domain. The verdicts and reason strings are the engine's own output from a run on 8 August 2026, copied rather than described.
</p>
<div class="files">
<div class="file">
<div class="verdict pass">allow: true</div>
<div class="fname">verifies-delegation-mandate.json</div>
<div class="why">A real <code>ObserverDelegationCredential</code>, issued 4 August 2026, byte-identical to the copy under <code>/credentials/</code>.</div>
<div class="reason">credential verified</div>
<a href="/verify-samples/verifies-delegation-mandate.json">Download →</a>
</div>
<div class="file">
<div class="verdict fail">allow: false</div>
<div class="fname">must-not-verify-tampered-signature.json</div>
<div class="why">The same credential with <strong>one character changed</strong> inside <code>proof.proofValue</code>. Every other byte is identical. If this verified, nothing else on this site would be worth reading.</div>
<div class="reason">[proof] eddsa-jcs-2022 signature does not verify against the issuer key</div>
<a href="/verify-samples/must-not-verify-tampered-signature.json">Download →</a>
</div>
<div class="file">
<div class="verdict fail">allow: false</div>
<div class="fname">must-not-verify-expired-mandate.json</div>
<div class="why">The same credential with its validity window moved into the past. Two defects are present, since the signature no longer covers the altered document either; the window is checked first, so that is what is named.</div>
<div class="reason">validity: credential expired (validUntil 2026-02-01T00:00:00Z)</div>
<a href="/verify-samples/must-not-verify-expired-mandate.json">Download →</a>
</div>
</div>
<div class="callout">
<h4>These two files are deliberately broken</h4>
<p>They are fixtures for the verifier and are not authorisations for anything. The agent named inside them is real; the documents are not valid credentials about it. <a href="/verify-samples/README.md" style="color:var(--amber); text-decoration:none;">The directory README states this too ↗</a></p>
</div>
<div class="callout">
<h4>Eight more artifacts in that directory, and they are not credentials</h4>
<p>A delegation credential is one of five constructions this estate signs. <code>/verify-samples/</code> also carries a decision attestation, four evaluation verdicts, two enforcement refusals and a payment instruction, taken verbatim from a run of a determination and payment stack against <strong>two real Federal Register instruments</strong>, over constructed loan cases. Each is published with the case that produced it, with its expected verdict recorded in the same CI check as the three above.</p>
<p><strong>Four of the eight do not verify against the version this page tells you to install, and they stay published saying so.</strong> <strong>The records are sound.</strong> Each verdict was signed under <code>op.evaluation.verdict.v4</code> and says so in its own <code>construction.type</code>; <code>rc.12</code> rebuilds <code>v3</code> and never reads that field, so the bytes differ by one string and the signature fails. The failure is recoverable by trial across a contiguous band: rebuild once per candidate version and exactly one verifies. That is a different fact from the two tampered fixtures above, which must never verify at any version, and the README keeps the two apart rather than leaving you to guess which kind of red you are looking at. A further one of the eight, the payment instruction, is checked by nothing at all at any published version, which is recorded as its own state rather than as a failure. <a href="/verify-samples/README.md" style="color:var(--amber); text-decoration:none;">The three groups, and what verifying any of them does not establish ↗</a></p>
</div>
<div class="callout">
<h4>A decision attestation checks out with the same install, and one is published here</h4>
<p data-shared-copy="offline-scope"><strong>A determination is verifiable by a party who was not there, offline and against a published key.</strong> The record carries the document that was signed, as base64 of the bytes that were received, beside a <code>documentHash</code>. Sha256 over those stored bytes reproduces the hash, so you can rebuild the verifier's input from the record alone, and <code>verifyDecisionAttestation</code> is exported by the version the install line above gives you. <code>/verify-samples/ppp-determination-refused-outcome.json</code> is a real stored determination record and the run below is its output, not a description of it.</p>
<p><strong>Read the state, not the absence of an error.</strong> <code>attested</code> means a key recovered from the decider's own <code>did:key</code> signed a canonicalisation of the whole document, that the decision identifier in the document is the one the payment cited, and that the outcome is a member of the vocabulary the document declares. It does not mean the policy, the vocabulary or the decider's artifact was fetched and compared: those references are confirmed present and resolved against nothing. <code>inputsDigest</code> is not even confirmed present, and the sample below carries none. <a href="/docs.html" style="color:var(--amber); text-decoration:none;">Field by field, including what a reader cannot establish ↗</a></p>
<p><strong>A <code>PolicyEvaluationCredential</code> is a different artifact and the next box is about it.</strong> Both record a determination; only one of them has a verifier. Conflating them is the error this page keeps separate on purpose.</p>
<p data-shared-copy="offline-scope"><strong>Without installing anything:</strong> <a href="/check.html" style="color:var(--amber); text-decoration:none;">/check</a> runs this same sequence in your browser. Paste the record, get the verdict, and nothing you paste leaves your browser. It covers the decision attestation and the enforcement refusal record, so the only artifact on this page it does not reach is the delegation credential, which resolves a <code>did:web</code> issuer over the network.</p>
</div>
<div class="code">
<div class="head"><span>attestation.mjs</span><span>measured 17 August 2026 · no network call</span></div>
<pre><span class="c">$</span> <span class="k">npm install</span> @observer-protocol/policy-engine
<span class="c">$</span> <span class="k">curl -O</span> https://observerprotocol.org/verify-samples/ppp-determination-refused-outcome.json
<span class="k">import</span> { verifyDecisionAttestation, ed25519Verify, base58Decode }
<span class="k">from</span> <span class="s">'@observer-protocol/policy-engine'</span>;
<span class="k">import</span> { createHash } <span class="k">from</span> <span class="s">'node:crypto'</span>;
<span class="k">import</span> fs <span class="k">from</span> <span class="s">'node:fs'</span>;
<span class="k">const</span> record = JSON.parse(fs.readFileSync(<span class="s">'./ppp-determination-refused-outcome.json'</span>));
<span class="c">// The record stores the document as base64 of the bytes that were received.</span>
<span class="k">const</span> bytes = Buffer.from(record.document, <span class="s">'base64'</span>);
<span class="k">const</span> hash = <span class="s">'sha256:'</span> + createHash(<span class="s">'sha256'</span>).update(bytes).digest(<span class="s">'hex'</span>);
console.log(<span class="s">'documentHash matches the stored bytes:'</span>, hash === record.documentHash);
<span class="c">// did:key -> the multicodec-prefixed key bytes the engine expects.</span>
<span class="k">const</span> didKeyBytes = (did) => base58Decode(did.slice(<span class="s">'did:key:z'</span>.length));
console.log(verifyDecisionAttestation(
record.decisionId,
JSON.parse(bytes.toString(<span class="s">'utf8'</span>)),
record.signature,
(msg, sig, pk) => ed25519Verify(Buffer.from(pk), Buffer.from(msg, <span class="s">'utf8'</span>), Buffer.from(sig)),
didKeyBytes,
));</pre>
</div>
<div class="code">
<div class="head"><span>what that prints</span><span>truncated after the two lines that carry the verdict</span></div>
<pre><span class="o">documentHash matches the stored bytes: true
{
state: 'attested',
decisionId: 'urn:ppp-cases:decision:MF-0001:APR',
decider: 'did:key:z6MkoMCP6imN1HfKC1gVHDMffgP3af58VnGY621KRHrEsbDq',
outcome: 'determination-refused-schema',
...
inputsDigest: undefined
}</span></pre>
</div>
<div class="callout">
<h4>An enforcement refusal record checks out with the same install, and two are published here</h4>
<p><strong>This is the artifact a refused party is holding</strong>, and until 20 August 2026 this page named it in its
opening sentence and then never came back to it. The two functions you need are
<code>signableFromRefusal</code> and <code>refusalPayload</code>, both exported by the version the install line above gives
you, and neither appeared anywhere on this site. That was a missing instruction rather than a missing capability: the
capability had been shipped for releases and nobody had written the route.</p>
<p><strong>The record's own construction is what gets rebuilt, not this build's.</strong> <code>signableFromRefusal</code>
reads <code>payloadType</code> off the record and rebuilds under the version <em>that record</em> was signed with, so a
refusal signed against an older engine still verifies against a newer one. The evaluation verdicts in the same directory
have no equivalent, and that is why four of them do not verify here. One design decision, applied to one signed class and
not the other.</p>
<p><strong>The key length is the part that will stop you, and nothing here used to say so.</strong> A
<code>did:key</code> decodes to <strong>34</strong> bytes: the two-byte multicodec prefix <code>0xed 0x01</code>, then the
32-byte Ed25519 key. <code>verifyDecisionAttestation</code> takes the 34-byte form and strips the prefix itself.
<code>ed25519Verify</code> takes the <strong>32</strong>. Hand it the 34-byte form and it fails with
<code>Ed25519 public key must be 32 bytes</code>, which reads like a damaged record and is a wrong call. The
<code>.slice(2)</code> in the sample below is that, and it is the whole of it.</p>
<p data-shared-copy="offline-scope"><strong>Without installing anything:</strong>
<a href="/check.html" style="color:var(--amber); text-decoration:none;">/check</a> now runs this same sequence in your
browser, on a refusal record, and nothing you paste leaves your browser. It also derives, from the record you paste, which
of its fields that signature actually reaches. Paste yours there before you install anything.</p>
</div>
<div class="code">
<div class="head"><span>refusal.mjs</span><span>measured 20 August 2026 · no network call</span></div>
<pre><code>$ npm install @observer-protocol/policy-engine
$ curl -O https://observerprotocol.org/verify-samples/ppp-refusal-applied-bound.json
import { signableFromRefusal, refusalPayload, ed25519Verify, base58Decode }
from '@observer-protocol/policy-engine';
import fs from 'node:fs';
const record = JSON.parse(fs.readFileSync('./ppp-refusal-applied-bound.json'));
// The payload is rebuilt from the record's own fields, under the construction
// the RECORD states in payloadType, not under whichever version you installed.
const signable = signableFromRefusal(record);
const bytes = refusalPayload(signable);
// did:key -> 34 bytes: 0xed 0x01, then the 32-byte key. ed25519Verify wants
// the 32. Without the slice this throws "Ed25519 public key must be 32 bytes",
// which looks like a bad record and is a bad call.
const key = Buffer.from(base58Decode(record.signedBy.slice('did:key:z'.length)).slice(2));
console.log('construction:', signable.payloadType);
console.log('verifies:', ed25519Verify(key, Buffer.from(bytes, 'utf8'),
Buffer.from(record.signature, 'base64')));</code></pre>
<div class="head"><span>what that prints</span><span>run against the published sample</span></div>
<pre><code>construction: op.enforcement.refusal.v2
verifies: true</code></pre>
</div>
<div class="callout">
<h4>Four fields. Read what that signature reaches before you rely on it.</h4>
<p>Where a refusal cites a determination, <strong>the refusal's signature covers four fields of that citation</strong>:
<code>state</code>, <code>decisionId</code>, <code>decider</code> and <code>outcome</code>. The attestation block on the
record carries more than that, and the rest of it sits <strong>outside the refusal's signature</strong>:
<code>policyRef</code>, <code>vocabularyRef</code>, <code>inputsDigest</code>, <code>decidedAt</code>,
<code>deciderArtifactDigest</code>, and the amount, counterparty and rail it repeats. So do the record's
<code>reason</code> prose and its <code>observedAt</code>.</p>
<p><strong>Each of those can be altered and the signature still verifies.</strong> That is not a defect in the record and
it is not a check that failed. It is the edge of what one signature establishes, and the failure mode is a reader carrying
a green result across it: concluding from a verified refusal that the policy reference on it was checked, when nothing
dereferenced it and the signature did not cover the pin either.</p>
<p><strong>The policy pin is usually under a different signature, on a different artifact.</strong> A determination's own
attestation document does cover its <code>policyRef</code>, and that document verifies by the route two boxes above and on
<a href="/check.html" style="color:var(--amber); text-decoration:none;">/check</a>. Two artifacts, two signatures, two
different sets of claims, and joining them is the reader's work rather than something either signature does.</p>
<p><strong>Do not take this list from this page.</strong> It describes the records we have seen, and a description goes
stale against a record shape nobody here has met. <code>/check</code> derives the same boundary from the record you paste,
by rebuilding the payload without each field and reporting which removals the signature does not notice. Run yours through
it rather than reading ours.</p>
<p><strong>And the key is the one the record names.</strong> Verifying establishes that whoever holds the key in
<code>signedBy</code> signed those bytes. Nothing in the record binds that key to an enforcement point, an institution or a
person, and a refusal signed by an ephemeral per-run key verifies exactly as well as one signed by a key somebody stands
behind.</p>
</div>
<div class="callout" style="border-left-color:var(--red-dim);">
<h4 style="color:var(--red);">There is no verifier for a PolicyEvaluationCredential. Not here, not hosted, not anywhere.</h4>
<p>A <code>PolicyEvaluationCredential</code> — the signed artifact recording a <em>determination</em>, that this action was checked and the decision was X — <strong>is not the same artifact as a refusal record</strong>, and it is the one you cannot verify with anything we publish. <strong>Every one ever issued fails the same way.</strong> Two of them are served under <code>/credentials/</code> and you will hit this immediately if you run the check above across that directory.</p>
<p><strong>The signatures are sound.</strong> They verify under canonical W3C Data Integrity tooling — <code>@digitalbazaar/data-integrity</code> with the <code>eddsa-jcs-2022</code> cryptosuite — against <code>did:web:observerprotocol.org#key-3</code>, with no Observer endpoint in the path. The documents are intact and the key is the right one. If you want to check an evaluation credential today, that is the route, and it is not ours.</p>
<p><strong>The gap is ours, and it is wider than a missing field.</strong> The published engine has no dispatch on credential type at all: the string <code>PolicyEvaluationCredential</code> does not appear in it. Its one structural validator assumes a delegation credential and checks delegation fields in sequence, so an evaluation credential fails at whichever delegation assumption it reaches first. That is reported as a missing <code>credentialSchema</code>, which is misleading — supplying one moves the failure to <code>credentialSubject.id must be a DID</code>, because an evaluation credential's subject has no identifier. It has a decision. The hosted service is built the same way: its endpoint accepts <code>{agentDid, mandate}</code> and has no other shape.</p>
<p><strong>And there is no schema to point at.</strong> No <code>PolicyEvaluationCredential</code> schema is published at any URL. AIP v0.8 defines the credential type and requires schema validation when verifying it, without ever assigning it a schema. That is a contradiction in the specification rather than a bug in a deployment, and closing it means publishing a schema at a permanent URL and adding type dispatch to the engine. Both are protocol decisions and neither is a copy change, so neither was done in the pass that wrote this page.</p>
<p>The two artifacts stay published. Removing them would have made this page look clean while leaving the reader to discover the same thing on their own, which is precisely what this page exists to argue against. Everything under <code>/credentials/</code> and <code>/verify-samples/</code> is now checked in CI against a recorded expected verdict, in both directions, so the day this is fixed the build will say so.</p>
</div>
<div class="callout" style="border-left-color:var(--red-dim);">
<h4 style="color:var(--red);" data-derived-claim="engine-payload-exports:latest-exports(resolutionPayload)">A resolution record now checks with the version this page tells you to install. It did not, for four releases.</h4>
<p>An <strong>approval resolution</strong> is the record of a human decision, signed at both ends: the routing and the outcome. Rebuilding its signed bytes needs <code>resolutionPayload</code>. <strong>The version <code>npm install</code> serves exports it. For four releases it did not.</strong></p>
<p>Read out of every published tarball rather than out of a release note: <code>resolutionPayload</code> was exported at <code><span data-measured="engine-payload-exports:withdrawals.resolutionPayload.0.lastPresentBefore">1.0.0-rc.8</span></code>, <strong>withdrawn across <span data-measured="engine-payload-exports:withdrawals.resolutionPayload.0.count">4</span> consecutive releases</strong>, <code><span data-measured="engine-payload-exports:withdrawals.resolutionPayload.0.from">1.0.0-rc.9</span></code> through <code><span data-measured="engine-payload-exports:withdrawals.resolutionPayload.0.to">1.0.0-rc.12</span></code>, and restored at <code><span data-measured="engine-payload-exports:withdrawals.resolutionPayload.0.restoredAt">1.0.0-rc.13</span></code>. <strong>npm's <code>latest</code> tag no longer points inside that band</strong>, at <code><span data-measured="engine-payload-exports:npmLatest">1.0.0-rc.22</span></code>, which is the version this page documents and the version the install line above gives you.</p>
<p data-derived-claim="engine-payload-exports:latest-exports(resolutionPayload)"><strong>The 122 signed resolution records this estate holds were always rebuildable by someone who installed <code>@rc</code>; what they were not was rebuildable by someone who followed the instruction on this page.</strong> That was worse than a missing feature: the instruction was complete, it ran, and it produced a package silently missing one constructor. That number is now <span data-measured="signed-record-coverage:headline.notRebuildableAtNpmLatest.count">0</span>: every signed resolution record this estate holds is rebuildable at the version <code>npm install</code> serves. It was 122 for as long as <code>latest</code> pointed inside the band.</p>
<p><strong>The withdrawal appears in no changelog.</strong> The package's own release notes record neither the removal nor the restoration, so nothing but the tarballs could have told you. That is why the figures in this paragraph are read from a measurement in <code>results/</code> and compared against npm on every build, instead of being typed here where nothing could contradict them.</p>
<p><strong>What to do about it. Nothing. The version this page documents and the version its install line gives you both rebuild a resolution record, because npm’s <code>latest</code> tag now points past the withdrawal band rather than inside it. This site stays pinned to what <code>latest</code> serves, for the same reason it did while that was the broken version: a page that documents a version nobody receives is the failure this whole surface is written against.</p>
</div>
</div>
</section>
<!-- FIELD GUIDE -->
<section style="background:var(--bg2); border-bottom:1px solid var(--border);">
<div class="section-inner">
<div class="section-label">03 · Field by field</div>
<h2>What each element lets you establish, and what it does not.</h2>
<p class="lede">
Every field has a limit. Where a field establishes less than its name suggests, that is said here rather than left for you to discover. Field names and derivations are taken from the published schema at <code><span data-measured="schema-claims:citeInsteadUrl">https://observerprotocol.org/schemas/delegation/v2.4.json</span></code> and from the engine source, not from our marketing copy.
</p>
<div class="callout" style="border-left-color:var(--red-dim);">
<h4 style="color:var(--red);">Correction: this section used to cite a schema that defines none of these fields</h4>
<p>Until now the line above named <code>/schemas/delegation/v2.2.json</code>. <strong>That version defines <span data-measured="schema-claims:citedDefines">0</span> of the <span data-measured="schema-claims:claimsTotal">9</span> fields documented below.</strong> It is a reserved placeholder of <span data-measured="schema-claims:citedBytes">576</span> bytes whose entire body is <code>{"type": "object"}</code> plus a comment, sitting between v2.1 and v2.3. The <strong>values on this page were right</strong>, taken from the artifact and the engine; <strong>the stated source was wrong</strong>, and nothing here could have told you, because the sentence and the schema were never compared. They are compared on every build now.</p>
<p>All <span data-measured="schema-claims:otherVersionsDefiningAll">7</span> other published versions define all <span data-measured="schema-claims:claimsTotal">9</span>. The citation moves to <code><span data-measured="schema-claims:citeInstead">v2.4</span></code> because that is the only published version which both defines every field below <strong>and</strong> accepts the credential this page walks you through: validated with ajv, the artifact fails v2, v2.1 and v2.3 on <code>proof.type</code>, and fails v2.5, v2.6 and v2.7 on a required <code>credentialStatus</code> it does not carry.</p>
<p><strong>Pinning that placeholder establishes nothing about structure, for two independent reasons, and fixing only the first would not help.</strong> The schema imposes no constraints, so validating against it succeeds vacuously: run the flagship credential through it with ajv and it passes, and it would pass with every field below deleted. Separately, <strong>nothing dereferences the URL at all.</strong> <code>schemaAllowlist</code> is an identifier comparison, <code>allowlist.includes(credentialSchema.id)</code>, so the document at that address is never fetched and never applied. Structure is checked by hand-written rules inside the engine. A stricter schema at that URL would change nothing about what the verifier does.</p>
<p><strong>The code samples still allowlist v2.2, deliberately, and here is why.</strong> <span data-measured="schema-claims:servedArtifacts.pinningCited">5</span> of the <span data-measured="schema-claims:servedArtifacts.total">8</span> credentials served from this domain carry <code>credentialSchema.id</code> pointing at v2.2, including the one both samples run against. Since the allowlist is an identifier comparison, a sample allowlisting anything else <strong>would refuse the artifact it is shown verifying</strong>. So the samples are correct as instructions and the pin is still meaningless as evidence, and that gap closes by re-issuing the artifacts, which is a signing operation rather than an edit and has not happened. <strong>The samples were not changed in this pass and this paragraph exists so that the reason is on the page rather than in a commit message.</strong></p>
</div>
<div class="fields">
<div class="field">
<div class="field-name">issuer · "did:web:bitcoinsingularity.ai"</div>
<div class="field-grid">
<div class="fg"><b>What it is</b> The identifier of the party that signed this credential. A <code>did:web</code> resolves to a document at that domain's <code>/.well-known/did.json</code> carrying the public keys.</div>
<div class="fg"><b>What it proves</b> That whoever controls the signing key for that domain asserted this. Combined with the proof check, the document has not changed since.</div>
<div class="fg"><b>How to check</b> Resolve the DID document yourself over HTTPS and confirm the key in <code>proof.verificationMethod</code> appears in its <code>assertionMethod</code>. The engine does this and reports <code>issuerResolution</code>.</div>
<div class="fg limit"><b>Its limit</b> <code>did:web</code> binds to <em>domain control</em>, not to a legal entity. It tells you the same party that controls that hostname signed this. It does not tell you who that party is, and if the domain changes hands the historical signature does not change with it.</div>
</div>
</div>
<div class="field">
<div class="field-name">credentialSubject.id · "did:web:observerprotocol.org:agents:maxi-0001"</div>
<div class="field-grid">
<div class="fg"><b>What it is</b> The agent this mandate was granted to.</div>
<div class="fg"><b>What it proves</b> That the issuer intended this authority for this subject and no other. A credential presented on behalf of a different DID is a different credential.</div>
<div class="fg"><b>How to check</b> Compare it against the DID whose action you are examining. The hosted check reports this as <code>identity.valid</code> with the note that the mandate subject binds to the DID.</div>
<div class="fg limit"><b>Its limit</b> The binding is between a mandate and an identifier, not between an identifier and a running process. It does not establish that the software which took an action is the holder of that DID's key. That requires the enforcement point, not the credential.</div>
</div>
</div>
<div class="field">
<div class="field-name">validFrom / validUntil</div>
<div class="field-grid">
<div class="fg"><b>What it is</b> The window inside which the issuer intends this authority to exist.</div>
<div class="fg"><b>What it proves</b> That authority was time-bounded at the moment of granting, rather than open-ended.</div>
<div class="fg"><b>How to check</b> First check the engine runs. Compare against the time of the action being examined, not against now.</div>
<div class="fg limit"><b>Its limit</b> An unexpired credential is not a live one. Expiry and revocation are different mechanisms and this field only covers the first. See <code>credentialStatus</code> below.</div>
</div>
</div>
<div class="field">
<div class="field-name">credentialSubject.tradingMandate.maxNotionalPerOrder · 10 · unit "USD"</div>
<div class="field-grid">
<div class="fg"><b>What it is</b> The per-order ceiling, carried alongside the unit it is denominated in.</div>
<div class="fg"><b>What it proves</b> The bound the principal signed for. A constraint without its unit is not a constraint, so the unit travels in the credential rather than being assumed by the reader.</div>
<div class="fg"><b>How to check</b> Read it directly. Compare against the notional of the order you are examining, converting at a rate you choose rather than one we supply.</div>
<div class="fg limit"><b>Its limit</b> This is a per-order ceiling and nothing else. It does not bound the number of orders, the aggregate across a day, or the total exposure. Those are separate sub-objects and this credential does not carry them.</div>
</div>
</div>
<div class="field">
<div class="field-name">credentialSubject.tradingMandate.temporal.allowedTimeWindows</div>
<div class="field-grid">
<div class="fg"><b>What it is</b> Permitted hours and days, each with an explicit IANA timezone: 09:00–17:00 <code>America/New_York</code>, Monday to Friday.</div>
<div class="fg"><b>What it proves</b> That the window was specified against a named zone rather than a bare wall-clock string, so it resolves identically for a reader in another jurisdiction.</div>
<div class="fg"><b>How to check</b> Convert the timestamp of the action into the named zone before comparing. Comparing timestamp strings across zones is a defect, not a shortcut.</div>
<div class="fg limit"><b>Its limit</b> It states when action was permitted, not when action occurred. Establishing the latter requires the rail's own record or an attestation, both outside this document.</div>
</div>
</div>
<div class="field">
<div class="field-name">credentialSubject.enforcementMode · "pre_transaction_check"</div>
<div class="field-grid">
<div class="fg"><b>What it is</b> A declaration of where in the flow this mandate is meant to be applied.</div>
<div class="fg"><b>What it proves</b> The issuer's intent that the check happens before the instruction exists, rather than as a review afterwards.</div>
<div class="fg"><b>How to check</b> Against the enforcement point's own evidence. The credential states the intent; only the deployment shows the behaviour.</div>
<div class="fg limit"><b>Its limit</b> <strong>This is the most important limit on the page.</strong> A field in a document cannot enforce anything. It records what the issuer asked for. Whether an enforcement point was actually in the path, and failed closed, is a property of a deployment and must be measured separately from reading this credential.</div>
</div>
</div>
<div class="field">
<div class="field-name">credentialSubject.delegationScope.may_delegate_further · false</div>
<div class="field-grid">
<div class="fg"><b>What it is</b> Whether the holder may issue onward authority derived from this one.</div>
<div class="fg"><b>What it proves</b> That the authority chain is intended to stop here, so a credential citing this one as its parent contradicts it on its face.</div>
<div class="fg"><b>How to check</b> If you are handed a derived credential, check whether its chain passes through a mandate carrying <code>false</code> here.</div>
<div class="fg limit"><b>Its limit</b> It bounds delegation, not sharing. Nothing in the document prevents the subject's key material from being used by another party; that is a custody question, answered by where the key lives.</div>
</div>
</div>
<div class="field">
<div class="field-name">credentialStatus · ABSENT</div>
<div class="field-grid">
<div class="fg"><b>What it is</b> A pointer to a revocation status list. This credential does not carry one.</div>
<div class="fg"><b>What it proves</b> Nothing, by absence. Both verifiers say so explicitly rather than passing quietly: the engine reports <code>revocation: 'status-absent'</code>, and the hosted service returns the note "credential carries no credentialStatus entry — revocation not checkable for this credential".</div>
<div class="fg"><b>How to check</b> Where a credential does carry one, the engine fetches it under <code>onUnreachable: 'cache-then-deny'</code>: if the list cannot be reached, a cached answer is used and then the credential is denied. Nothing is let through on a fetch failure.</div>
<div class="fg limit"><b>Its limit</b> For this artifact, you cannot tell whether the issuer has since revoked it. Treat <code>allow: true</code> here as "validly issued and unexpired", not as "currently in force". A status list hosted on an origin other than the pinned issuer's is also refused until you allowlist it — and Observer Protocol's own clause-zero revocation demonstration is exactly such a pair, so it does not verify out of the box.</div>
</div>
</div>
<div class="field">
<div class="field-name">proof · DataIntegrityProof · eddsa-jcs-2022</div>
<div class="field-grid">
<div class="fg"><b>What it is</b> An Ed25519 signature over the JCS-canonicalised document, naming the key that produced it in <code>verificationMethod</code>.</div>
<div class="fg"><b>What it proves</b> That not one byte of the document has changed since signing. The tampered sample above differs by a single character and fails here.</div>
<div class="fg"><b>How to check</b> The engine reports <code>issuerProof: 'eddsa-jcs-2022-verified'</code>. Canonicalisation matters: two documents that differ only in key order produce the same signature input, and two that differ in any value do not.</div>
<div class="fg limit"><b>Its limit</b> A signature gives continuity, not identity. It establishes that the same key signed this that signed the other things you have from this issuer. Who holds that key, and whether they still should, are questions this field cannot answer.</div>
</div>
</div>
</div>
</div>
</section>
<!-- HOSTED -->
<section style="background:var(--bg); border-bottom:1px solid var(--border);">
<div class="section-inner">
<div class="section-label">04 · The hosted check</div>
<h2>Convenient, ours, and now the same engine two releases back.</h2>
<p class="lede">
There is also a hosted verifier. It is open — no token, no account, rate-limited at 60 requests per caller per minute — and it returns a signed result. Use it if you want a second opinion or a quick look. Do not use it for an artifact that should not leave your machine, and do not mistake it for the claim this site makes.
</p>
<div class="code">
<div class="head"><span>POST /v1/verify · no authentication</span><span>verify.observerprotocol.org</span></div>
<pre><span class="c">$</span> <span class="k">curl -X POST</span> https://verify.observerprotocol.org/v1/verify \
-H <span class="s">'Content-Type: application/json'</span> \
-d <span class="s">'{"agentDid": "<subject DID>", "mandate": <the credential>}'</span>
<span class="o">{ "identity": { "valid": true, ... },
"mandate": { "valid": true,
"notes": ["credential carries no credentialStatus entry —
revocation not checkable for this credential"] },
"proof": { "cryptosuite": "eddsa-jcs-2022",
"verificationMethod": "did:web:observerprotocol.org#key-7" } }</span></pre>
</div>
<div class="callout">
<h4>Correction: the two verifiers converged, and this section said otherwise for a week</h4>
<p><strong>This section used to argue that the hosted service was a different build.</strong> It reported <code>engine.running: "0.3.3"</code> and concluded that any agreement between the two was "agreement observed on samples, not a shared code path". <strong>That argument is false and has been since 9 August 2026.</strong> Convergence landed that day, in commit <code><span data-measured="hosted-verifier:commitShort">d3278efb3cb2</span></code>, built <span data-measured="hosted-verifier:builtAtDate">2026-08-09</span>, and nothing here recorded it. The page went on making an argument its own subject had stopped supporting.</p>
<p>Read from the service's own <code>/version</code> today: <code>engine.running</code> <code><span data-measured="hosted-verifier:engineRunning">1.0.0-rc.10</span></code>, <code>engine.builtAgainst</code> <code><span data-measured="hosted-verifier:engineBuiltAgainst">1.0.0-rc.10</span></code>, <code>agree: true</code>. <strong>It is the published package.</strong> The published package is <code><span data-engine-version="current">1.0.0-rc.22</span></code>, so the honest description is <strong>the same engine, two releases behind</strong>, and not a separate implementation. What remains true is that a version gap is a real gap: rc.10 and rc.12 are not the same code, and a difference between them is a difference you can meet.</p>
<p><strong>The old "agree on 7 of 8" figure is withdrawn, and the denominator was the defect rather than the agreement.</strong> Three of those eight artifacts never reach the hosted engine at all, so a ratio over eight was never reproducible by a reader. One is the testbed-issuer demonstration credential, and <strong>that deployment refuses it at its issuer allowlist, so the refusal is evidence about the allowlist and not about the engine.</strong> The other two are the <code>PolicyEvaluationCredential</code> artifacts, for the reason in the next paragraph. A figure whose denominator counts artifacts that cannot be submitted is not a measurement of agreement; it is a measurement of how many things we tried.</p>
<p><strong>A PolicyEvaluationCredential cannot be submitted here at all.</strong> Post one as the request body and the service answers <code>HTTP 400</code>, <code>body must carry agentDid (string) and mandate (object)</code>. There is no <code>agentDid</code> to supply: a PEC's <code>credentialSubject</code> carries <code>decision</code>, <code>evaluator</code>, <code>proposal</code> and <code>evaluatedAt</code>, and <strong>no <code>id</code></strong>. The endpoint's shape assumes a delegation credential granting authority to a subject, and an evaluation credential has no subject in that sense. <strong>This is the same root cause as the gap in section 02</strong>, met at the transport rather than in the validator: both the package and the service are built around one credential shape, and the artifact that records a determination does not have it. Wrapping a PEC in an invented <code>agentDid</code> does get a response, and what comes back is a structural refusal about <code>credentialSchema</code>, which is the offline failure repeated rather than a second opinion about it.</p>
<p>The service is also candid about a second limit, in its own <code>/version</code> response: the schema and issuer allowlists it enforces come from environment variables on the host, <code>inRepo: false</code>, so a change to what it accepts "leaves no reviewable record". <span data-shared-copy="offline-scope">The offline path has no such property, because the allowlist is the one you wrote.</span></p>
<p><strong>Two further limits, and they are worse than the two above.</strong></p>
<p><strong>A scope answer that does not vary with its input. MEASURED AGAINST THE OLD BUILD, NOT RE-MEASURED SINCE CONVERGENCE.</strong> Attach a proposal and <code>scope.inScope</code> returned <code>false</code> for every USDC amount, a compliant one, an absurd one, any of them. The cause was that engine 0.3.3 could not scale USDC minor units, so the comparison it was asked to make could not be made and it failed closed. Measured on 9 August 2026, at two amounts three orders of magnitude apart, both returning the same <code>inScope: false</code>, with the service naming the cause in the answer: <code>currency "USDC" has no known minor-unit exponent — amount cannot be scaled (fail closed)</code>. Failing closed was the correct behaviour and the outcome was still useless: <strong>a check that returns the same answer for every input is not telling you about your input.</strong> Read a <code>false</code> here as “not evaluated”, never as “out of scope”. <strong>The build under that measurement is no longer the one deployed</strong>, and this limit has not been re-measured against <code><span data-measured="hosted-verifier:engineRunning">1.0.0-rc.10</span></code>. It is left standing rather than deleted or quietly updated, because "we changed the engine" is not evidence that a behaviour changed with it. Treat it as open until somebody re-runs it. The offline package does not share this limit.</p>
<p><strong>A refusal that does not say why — CLOSED, re-measured 9 August 2026.</strong> This said that when <code>mandate.valid</code> was <code>false</code>, <code>notes</code> came back <code>[]</code> and a caller could not tell whether the credential failed on its schema, its signature, its validity window or its issuer. That was true when written. It is not true now: the service returns a specific <code>reason</code>. Measured against the two artifacts published here because they must fail, <code>must-not-verify-tampered-signature</code> returns <code>[proof] eddsa-jcs-2022 signature does not verify against the issuer key</code>, and <code>must-not-verify-expired-mandate</code> returns <code>validity: credential expired (validUntil 2026-02-01T00:00:00Z)</code>. <code>notes</code> is still <code>[]</code>; <code>reason</code> is the field to read. The retired limit is left here rather than deleted, because a disclosure that vanishes reads as one that was never true.</p>
<p><strong data-shared-copy="offline-scope">The offline path is the claim. The hosted path is a convenience.</strong> If they ever disagree, the offline result is the one to act on, and we would like to hear about it.</p>
</div>
</div>
</section>
<!-- NOT ENDORSEMENT -->
<section style="background:var(--bg2);">
<div class="section-inner">
<div class="section-label">05 · What this page is not</div>
<h2>Checking a record is not approving a vendor.</h2>
<p class="body">
Using this tooling implies nothing about Observer Protocol, Agentic Terminal, or any firm whose records you are examining. There is no account to create, no form on this page, and nothing here that asks you for a way to contact you. If you are checking a record because someone is asking you to accept it, the correct outcome of this page is that you can answer that question yourself and then leave.
</p>
<p class="body">
The verifier is MIT-licensed and self-hostable. <strong>A verifier you run from your own infrastructure is worth considerably more than one you load from us</strong>, and we would rather you did that. The source, the known limits, and the schemas are all public.
</p>
<p class="body">
<span data-shared-copy="offline-scope">A browser-based verifier that runs entirely client-side is live at <a href="/check.html" style="color:var(--amber); text-decoration:none;">/check</a>, and it covers one artifact type, the decision attestation.</span> The published package as a whole has not been ported, and we are not going to ship one that sends your artifact to a server while calling it client-side. It depends on Node built-ins, <code>node:crypto</code>, <code>node:fs</code> and <code>node:dns</code> among them, and the DNS use is a live guard against a hostile URL rather than incidental. Porting it means replacing that guard honestly, not stubbing it. Until that happens, the two paths above are the route for every artifact that page does not cover.
</p>
</div>
</section>
<footer>
<div class="footer-inner">
<div style="font-family:'IBM Plex Mono',monospace; font-size:0.72rem; color:var(--amber); letter-spacing:0.12em; text-transform:uppercase;">Observer Protocol</div>
<div class="footer-links">
<a href="/">Home</a>
<a href="./verify.html">Verify</a>
<a href="./supervisors.html">Supervisors</a>
<a href="./architecture.html">Architecture</a>
<a href="./docs.html">Docs</a>
<a href="./registry.html">Registry</a>
<a href="https://github.com/observer-protocol" target="_blank" rel="noopener">GitHub ↗</a>
</div>
</div>
<div class="footer-note">
Engine <code>@observer-protocol/policy-engine</code> <span data-engine-version="current">1.0.0-rc.22</span> · hosted service engine <span data-measured="hosted-verifier:engineRunning">1.0.0-rc.10</span> · verdicts on this page <span data-engine-version="historical">re-measured against rc.6 on 8 August 2026 and against rc.12 on 13 August 2026, identical both times</span> · AIP v0.9, ratified 25 July 2026 · MIT
</div>
</footer>
</body>
</html>