Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions docs/deck/reader-browser-results.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"scope": "Actual browser Fetch/CORS, streamed body limits and Deck DOM; all responses intercepted synthetic fixtures, no native or human proof",
"results": [
{
"mode": "answered",
"status": "Answered \u00b7 fixture-operator \u00b7 2030-03-17T17:46:41.000Z",
"synthetic_http_calls": 11,
"page_errors": [],
"unhandled_routes": []
},
{
"mode": "empty",
"status": "Current request \u00b7 no admitted decision.",
"synthetic_http_calls": 8,
"page_errors": [],
"unhandled_routes": []
},
{
"mode": "changed-query",
"status": "UNKNOWN \u00b7 request or verified answer evidence unavailable.",
"synthetic_http_calls": 6,
"page_errors": [],
"unhandled_routes": []
},
{
"mode": "missing-receipt",
"status": "UNKNOWN \u00b7 request or verified answer evidence unavailable.",
"synthetic_http_calls": 7,
"page_errors": [],
"unhandled_routes": []
},
{
"mode": "oversized",
"status": "UNKNOWN \u00b7 request or verified answer evidence unavailable.",
"synthetic_http_calls": 1,
"page_errors": [],
"unhandled_routes": []
}
]
}
111 changes: 111 additions & 0 deletions docs/deck/reader-native-results.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
{
"candidate": "quipu 0.16.3\ngit_sha: 771e710de7294b718ceaa1a4b825fd28e4b34c23\ngit_dirty: false",
"native_library_sha256": "2ac7b3096f54903dd18664016fc7d102632cb8b079a4fbb239b8818b9d3e1ef8",
"helper_source_sha256": "3bcfd058028c532c711fb02dd3e6743ddd48484ed7813db6c24e5e82e65b2ce3",
"fixture": "Disposable stores; generated software keys kept only in memory; local native methods and actual operator CLI; no HTTP/service/real credential or production profile union",
"results": [
{
"case": "choose",
"state": "ANSWERED",
"canonical_native_match": true,
"native_verified": true,
"replay_no_mutation": true
},
{
"case": "choose-stored-reader",
"native_registered_queries": true,
"graph_creation_bound": true,
"typed_facts_and_provenance": true
},
{
"case": "later",
"state": "LATER",
"canonical_native_match": true,
"native_verified": true,
"replay_no_mutation": true
},
{
"case": "later-stored-reader",
"native_registered_queries": true,
"graph_creation_bound": true,
"typed_facts_and_provenance": true
},
{
"case": "comment",
"state": "WAITING",
"canonical_native_match": true,
"native_verified": true,
"replay_no_mutation": true
},
{
"case": "comment-stored-reader",
"native_registered_queries": true,
"graph_creation_bound": true,
"typed_facts_and_provenance": true
},
{
"case": "forged",
"refused_no_mutation": true,
"nonce_and_counter_unspent": true
},
{
"case": "wrong-origin",
"refused_no_mutation": true,
"nonce_and_counter_unspent": true
},
{
"case": "item-revision",
"refused_no_mutation": true,
"nonce_and_counter_unspent": true
},
{
"case": "closed-item",
"refused_no_mutation": true,
"nonce_and_counter_unspent": true
},
{
"case": "request-revision",
"refused_no_mutation": true,
"nonce_and_counter_unspent": true
},
{
"case": "missing-expectation-0",
"native_verified": true,
"deck_reader_refused": true
},
{
"case": "missing-expectation-1",
"native_verified": true,
"deck_reader_refused": true
},
{
"case": "missing-expectation-2",
"native_verified": true,
"deck_reader_refused": true
},
{
"case": "missing-expectation-3",
"native_verified": true,
"deck_reader_refused": true
},
{
"case": "missing-expectation-4",
"native_verified": true,
"deck_reader_refused": true
},
{
"case": "missing-comment-floor",
"native_verified": true,
"deck_reader_refused": true,
"limitation": "Native post-state admission excludes MinCount floors; full shape validation and client/reader constraints remain distinct"
},
{
"case": "loaded-action-value-constraint",
"refused_no_mutation": true
},
{
"case": "ordinary-graph-and-other-request-revision",
"deck_reader_refused": true
}
]
}
75 changes: 75 additions & 0 deletions docs/deck/reader.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Bounded online reader source

`web/deck/reader.mjs` supplies the read function accepted by the online Deck
view. Importing it performs no request. The shipped app still has signing
disabled and selects no production endpoint, registration receipt or query.

`readerDefinitions(boundary, provenanceNamespace)` constructs five stored-query
definitions: request, item, answers, facts and subject-to-receipt links. They
use explicit graph IRIs and validated IRI/integer parameters. The native
provenance namespace must equal the primitive's fixed vocabulary identifier;
a configurable lookalike could otherwise hide real answers as an empty set.
It is not the Rikuy vocabulary. The adapter sends named `/ask` requests, never raw `/query` text.
The exact rendered text returned by `/ask` must match the source definition.

Before a deployment can use the adapter, its owner must register those exact
definitions through the normal governed path and admit their served plans and
read cost. Each configured query hash is SHA-256 of the UTF-8 JSON encoding of
the source definition. It binds name, template, parameter kinds and output
bound. A hash supplied by a caller is not a registration or read-cost receipt.
This source neither registers queries nor grants permission to run them.

The reader also requires retained creation provenance for all three committed
graphs. It checks each through the bounded `GET /graphs?graph=<iri>` route.
An absent, replaced, overlay or ambiguous graph is UNKNOWN. This metadata
check does not prove graph protection, origin admission or operator enablement.
Native verified point receipts and exact typed fact/provenance read-back are
still required for every answer.

Output limits deliberately include an overflow sentinel. Request and item
queries stop at three rows and require exactly one; answer queries stop at 21
and admit at most 20; fact and link queries stop at 65 and admit at most 64.
Reaching the limit, truncation, duplicate rows, count mismatch, changed query,
missing receipt or incomplete read-back refuses the entire view. An empty
answer list needs the positive request/item reads and all graph bindings.
No missing evidence is converted to a settled answer.

One cycle has a maximum six-second deadline, no overlapping cycle and at most
two requests in flight. An injected transport must honor abort and streaming
byte limits. `readerTransport` provides a browser Fetch implementation with
those byte limits, redirect refusal, omitted credentials, no cache and the
`daily-brief` client attribution. It permits only the pinned HTTPS origin's
graph, named-query and point-receipt routes; it has no commit or registration
method. It neither signs nor retries a write.

Request and item reads bracket answer retrieval. A changed revision or closed
item refuses the result. These separate reads do not establish an atomic
snapshot or rule out every intervening change. The signed proposal's native
CAS remains authoritative at commit time.

`just ci` covers complete positive reads, positive controls for empty answers,
graph and query identity, overflow, missing/foreign receipts, wrong fact types,
changed/closed items, request/item bookends, deadlines, overlapping cycles,
streamed byte limits and restricted HTTP routes. The native qualification
command in [contract.md](contract.md) now loads all five definitions into
disposable stores and uses actual stored-query execution, graph metadata,
point receipts and typed read-back for Choose, Later and comment.

The native transport dispatch in that qualification is synthetic and local.
It proves neither served HTTP/CORS nor production query cost, profile-union
shape admission, graph protection or human/device enrollment. The result
archive in [reader-native-results.json](reader-native-results.json) records
the candidate and fixture hashes. No production adapter is selected by this
change, and there is no newly observable behavior in the disabled page.

The browser qualification in `scripts/qualify-deck-reader-browser.cjs` mounts
the actual view with this reader and uses actual browser Fetch/CORS and body
streaming. Every HTTP reply is intercepted synthetic data. Its five controls
show Answered, empty-but-controlled Waiting, changed query UNKNOWN, missing
receipt UNKNOWN and oversized reply UNKNOWN. The archive is
[reader-browser-results.json](reader-browser-results.json). Run it with an
explicit local Playwright installation and Chrome:

```sh
PLAYWRIGHT_MODULE=/path/to/playwright node scripts/qualify-deck-reader-browser.cjs
```
19 changes: 19 additions & 0 deletions scripts/qualify-deck-native.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import assert from 'node:assert/strict';
import {NS} from '../web/deck/value.mjs';
import {buildProposal, canonicalProposal, operationIri, requestPublication, readRequest, revisionIri} from '../web/deck/model.mjs';
import {admittedChallenge, readAnswer, disposition, readbackProof} from '../web/deck/evidence.mjs';
import {DeckReader, readerDefinitions} from '../web/deck/reader.mjs';
const cli = process.env.QUIPU_CLI, artifacts = process.env.QUIPU_ARTIFACTS;
if (!cli || !artifacts) throw Error('Explicit candidate QUIPU_CLI and fresh QUIPU_ARTIFACTS JSONL required');
const root = mkdtempSync(join(tmpdir(), 'rikuy-native-')), source = resolve(import.meta.dirname, '..');
Expand Down Expand Up @@ -98,6 +99,24 @@ try {
assert.deepEqual(footprint(state, e.body.nonce), before);
assert.throws(() => command(state.db, 'challenge', state.p)); // Decided or operation slot is spent.
results.push({case: kind, state: disposition([answer], state.r, boundary).state, canonical_native_match: true, native_verified: e.receipt.verified, replay_no_mutation: true});
const nativeNamespace = e.facts.find(d => d.p.endsWith('attestedBy')).p.slice(0, -'attestedBy'.length);
const definitions = readerDefinitions(boundary, nativeNamespace);
const queryHashes = Object.fromEntries(Object.entries(definitions).map(([role, def]) => [role, sha(Buffer.from(JSON.stringify(def)))]));
for (const def of Object.values(definitions)) command(state.db, 'query-load', def);
const registrations = [boundary.requestGraph, boundary.answerGraph, boundary.itemGraph].map(graph => {
const metadata = command(state.db, 'graphs', {graph}).graphs[0];
return {iri: graph, creation: metadata.creation};
});
// Real stored-query rendering/execution, native point receipts and registry;
// synthetic transport dispatch only, with no HTTP or production endpoint.
const reader = new DeckReader({endpoint: 'https://reader.example.test/', boundary, operator: state.who, provenanceNamespace: nativeNamespace, queryHashes, registrations, request: async (address, {body}) => {
const url = new URL(address);
return command(state.db, url.pathname === '/graphs' ? 'graphs' : url.pathname === '/ask' ? 'ask' : 'receipt', url.pathname === '/graphs' ? {graph: url.searchParams.get('graph')} : body);
}});
const observed = await reader.read(state.r.id);
assert.equal(observed.rows.length, 1);
assert.equal(observed.rows[0].receipt.receipt_iri, e.receipt.receipt_iri);
results.push({case: `${kind}-stored-reader`, native_registered_queries: true, graph_creation_bound: true, typed_facts_and_provenance: true});
}
for (const [index, kind] of ['forged', 'wrong-origin', 'item-revision', 'closed-item', 'request-revision'].entries()) {
const state = setup(index + 10), issued = command(state.db, 'challenge', state.p);
Expand Down
48 changes: 48 additions & 0 deletions scripts/qualify-deck-reader-browser.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// # arming: agent-invoked synthetic browser qualification; no production traffic.
if (!process.env.PLAYWRIGHT_MODULE) throw Error('Explicit PLAYWRIGHT_MODULE required');
const {chromium}=require(process.env.PLAYWRIGHT_MODULE);
const fs=require('fs'),path=require('path'),assert=require('assert/strict');
const root=path.resolve(__dirname,'..');
(async()=>{
const browser=await chromium.launch({executablePath:'/usr/bin/google-chrome',headless:true,args:['--no-sandbox','--disable-dev-shm-usage']});const results=[];
try{for(const mode of ['answered','empty','changed-query','missing-receipt','oversized']){
const context=await browser.newContext(),page=await context.newPage(),errors=[],blocked=[];let calls=0;
page.on('pageerror',e=>errors.push(e.message));
await context.route('**/*',async route=>{
const req=route.request(),url=new URL(req.url());
if(url.origin==='https://sign.example.test'){
if(url.pathname==='/'){await route.fulfill({contentType:'text/html',body:'<main></main>'});return;}
if(/^\/(web\/deck\/[a-z-]+|tests\/deck-fixture)\.mjs$/.test(url.pathname)){await route.fulfill({contentType:'text/javascript',body:fs.readFileSync(path.join(root,url.pathname))});return;}
}
if(url.origin==='https://reader.example.test'&&['/graphs','/ask','/attest/receipt'].includes(url.pathname)){
const headers={'access-control-allow-origin':'https://sign.example.test','access-control-allow-headers':'content-type,x-quipu-client','access-control-allow-methods':'GET,POST,OPTIONS'};
if(req.method()==='OPTIONS'){await route.fulfill({status:204,headers});return;}
calls++;
const value=await page.evaluate(async ({path,graph,body,mode})=>{
const f=await import('/tests/deck-fixture.mjs'),m=await import('/web/deck/model.mjs'),r=await import('/web/deck/reader.mjs');
const definitions=r.readerDefinitions(f.boundary,r.NATIVE_PROVENANCE_NS),e=await f.evidence();
if(path==='/graphs')return{count:1,graphs:[{iri:graph,g:1,class:'committed',creation:{actor:'fixture-curator',timestamp:'2026-10-25T00:00:00Z'}}]};
if(path==='/attest/receipt')return mode==='missing-receipt'?{found:false}:e.response;
const role=Object.keys(definitions).find(k=>definitions[k].name===body.name),def=definitions[role];
const rows={request:[{revision:f.ask.revision,revisionIri:m.revisionIri(f.ask),manifest:m.requestPublication(f.ask,f.boundary).datums[1].o.value}],item:[{revision:f.ask.itemRevision,status:f.boundary.openStatus}],answers:mode==='empty'?[]:[{subject:e.row.subject,receiptIri:e.row.receiptIri,manifest:e.row.manifest}],facts:e.readback.datums.map(d=>({s:d.s,p:d.p,kind:d.o.kind,value:String(d.o.value)})),links:e.readback.attestedBy.map(({s})=>({s}))}[role];
return{query:def.name,sparql:r.renderReaderQuery(def,body.params)+(mode==='changed-query'&&role==='answers'?' ':''),rows,count:rows.length,truncated:false};
},{path:url.pathname,graph:url.searchParams.get('graph'),body:req.method()==='POST'?req.postDataJSON():null,mode});
await route.fulfill({headers,contentType:'application/json',body:mode==='oversized'?' '.repeat(262145):JSON.stringify(value)});return;
}
blocked.push(url.href);await route.abort();
});
await page.goto('https://sign.example.test/');
await page.evaluate(async()=>{
const f=await import('/tests/deck-fixture.mjs'),r=await import('/web/deck/reader.mjs'),v=await import('/web/deck/value.mjs'),{mount}=await import('/web/deck/view.mjs');
const definitions=r.readerDefinitions(f.boundary,r.NATIVE_PROVENANCE_NS),queryHashes=Object.fromEntries(await Promise.all(Object.entries(definitions).map(async([k,d])=>[k,await v.sha256(v.utf8(JSON.stringify(d)))])));
const reader=new r.DeckReader({endpoint:'https://reader.example.test/',boundary:f.boundary,operator:f.who,provenanceNamespace:r.NATIVE_PROVENANCE_NS,queryHashes,registrations:[f.boundary.requestGraph,f.boundary.answerGraph,f.boundary.itemGraph].map(iri=>({iri,creation:{actor:'fixture-curator',timestamp:'2026-10-25T00:00:00Z'}})),request:r.readerTransport('https://reader.example.test/')});
await mount(document.querySelector('main'),{enabled:true,boundary:f.boundary,identity:f.who,read:()=>reader.read(f.ask.id),writer:{identity:f.who,store:{get:async()=>null}},now:()=>1900000000});
});
const status=await page.locator('[role=status]').textContent();
assert(mode==='answered'?status.startsWith('Answered'):mode==='empty'?status.startsWith('Current request'):status.startsWith('UNKNOWN'),status);
if(!['answered','empty'].includes(mode))assert.equal(await page.locator('button').count(),0);
assert.deepEqual(errors,[]);assert.deepEqual(blocked,[]);assert(calls>0&&calls<=13);
results.push({mode,status,synthetic_http_calls:calls,page_errors:errors,unhandled_routes:blocked});await context.close();
}}finally{await browser.close();}
console.log(JSON.stringify({scope:'Actual browser Fetch/CORS, streamed body limits and Deck DOM; all responses intercepted synthetic fixtures, no native or human proof',results},null,2));
})().catch(e=>{console.error(e.stack);process.exitCode=1;});
Loading
Loading