{
  "name": "Laituri",
  "pitch": "An open MCP and REST front door that lets a customer's own AI purchasing agent buy from Valio Aimo at a signed fixed price, inside limits a person signed, with a log anyone can verify.",
  "live": "https://laituri.31-97-126-112.sslip.io",
  "licence": "MIT",
  "facts": {
    "tests": {
      "files": 107,
      "total": 903,
      "passed": 903,
      "failed": 0
    },
    "evals": {
      "mode": "recorded",
      "generatedAt": "2026-11-14T07:00:00.000Z",
      "passed": 91,
      "total": 91,
      "suites": [
        {
          "id": "scenarios",
          "name": "Scenarios",
          "passed": 31,
          "total": 31,
          "bar": ">=90%",
          "met": true
        },
        {
          "id": "guardrails",
          "name": "Guardrails",
          "passed": 35,
          "total": 35,
          "bar": "100%",
          "met": true
        },
        {
          "id": "trust",
          "name": "Trust",
          "passed": 17,
          "total": 17,
          "bar": "100%",
          "met": true
        },
        {
          "id": "openness",
          "name": "Openness",
          "passed": 8,
          "total": 8,
          "bar": "100%",
          "met": true
        }
      ],
      "caseIds": [
        "1",
        "2",
        "3",
        "4",
        "5",
        "6",
        "7",
        "8",
        "9",
        "9b",
        "10",
        "11",
        "12",
        "13",
        "14",
        "15",
        "16",
        "17",
        "18",
        "19",
        "20",
        "21",
        "22",
        "23",
        "24",
        "25",
        "26",
        "27",
        "28",
        "29",
        "30",
        "G1",
        "G2",
        "G3",
        "G4",
        "G5",
        "G6",
        "G7",
        "G8",
        "G9",
        "G19",
        "G20",
        "G10",
        "G11",
        "G12",
        "G13",
        "G14",
        "G15",
        "G16",
        "G16b",
        "G16c",
        "G16d",
        "G17",
        "G18",
        "G21",
        "G22",
        "G23",
        "G24",
        "G25",
        "G26",
        "G27",
        "G28",
        "G29",
        "G30",
        "G31",
        "G32",
        "T1",
        "T2",
        "T3",
        "T4",
        "T5",
        "T6",
        "T7",
        "T8",
        "T9",
        "T10",
        "T11",
        "T12",
        "T13",
        "T14",
        "T15",
        "T16",
        "T17",
        "O1",
        "O2",
        "O3",
        "O4",
        "O5",
        "O6",
        "O7",
        "O8"
      ]
    },
    "mandateRules": 17,
    "ruleIds": [
      "mandate.signature",
      "mandate.validity",
      "offer.signature",
      "offer.expiry",
      "audit.integrity",
      "spend.per_order",
      "spend.per_week",
      "category.allowed",
      "dietary.allergens",
      "dietary.claims",
      "substitution.policy",
      "substitution.price",
      "delivery.partial",
      "delivery.deadline",
      "match.confidence",
      "request.words",
      "product.first_time"
    ],
    "mcpTools": 7,
    "toolNames": [
      "search_catalog",
      "get_product",
      "request_quote",
      "get_mandate",
      "place_order",
      "get_order",
      "verify_audit"
    ],
    "restOperations": 24,
    "restPaths": 21,
    "discoveryDocuments": 6,
    "signingKeys": [
      "supplier",
      "buyer",
      "approver"
    ],
    "demoBuyers": 3,
    "buyerNames": [
      "Nordic Harbour Hotels",
      "Kaisla School Kitchens",
      "Ravintola Laine"
    ],
    "seededRequests": 31,
    "seededOrders": 24,
    "recordedProducts": 255
  },
  "criteria": [
    {
      "name": "Working end-to-end demo",
      "weight": 30,
      "claim": "A request in plain words goes through search, a signed fixed-price offer, the mandate check and either an automatic order or a person's signed approval, over the same MCP and REST interface an outside agent uses.",
      "built": [
        "Buyer side (/buyer): the agent takes \"1,000 litres of lactose-free milk by Monday\", gets a signed offer, and the order confirms; 1,600 litres waits for the approver.",
        "Approval on a phone-sized page (/approve/{orderId}): the approver's decision is signed with the approver key.",
        "Wholesaler side (/valio): connected agents, demand, unmet requests and the same audit log.",
        "A guided walkthrough (/buyer?tour=1) drives these steps in the running product, not in a video.",
        "Price and availability come from the challenge's mock API in live mode, or from recorded answers for 255 products in recorded mode.",
        "An outside process buys over plain HTTP with scripts/buyer-agent.ts."
      ],
      "where": [
        "src/lib/gateway/gateway.ts",
        "src/lib/agent/purchase.ts",
        "src/lib/orders/place.ts",
        "src/lib/demo/scenarios.ts",
        "src/lib/tour/steps.ts",
        "src/app/buyer/page.tsx",
        "src/app/approve/[orderId]/page.tsx",
        "scripts/buyer-agent.ts"
      ],
      "check": [
        {
          "kind": "url",
          "value": "/buyer?tour=1"
        },
        {
          "kind": "command",
          "value": "npm run eval -- --suite scenarios"
        },
        {
          "kind": "eval",
          "value": "O8"
        },
        {
          "kind": "command",
          "value": "npx tsx scripts/buyer-agent.ts \"1,000 litres of lactose-free milk by Monday\""
        }
      ],
      "measured": [
        "Scenarios eval suite: 31/31 cases pass (bar >=90%)",
        "An agent that knows only the text of /llms.txt, fetched over HTTP from a server started for the eval, creates its own sandbox with POST /api/v1/workspace, runs the curl lines of the REST flow in order with the returned key, searches, quotes and orders the milk, and the order confirms (eval O8)"
      ],
      "limit": "Orders are simulated: the mock API has price and availability but no order endpoint, so Laituri writes the order and receipt itself and marks each one simulated: true."
    },
    {
      "name": "Solving the problem and user need",
      "weight": 20,
      "claim": "The agent orders what the buyer asked for: the wholesaler turns litres and kilograms into whole sales units, compares the buyer's words with every offer and signs that comparison into it, and an order that does not meet the words in full is never confirmed alone.",
      "built": [
        "Search reads Finnish, Swedish or English needs as a product concept plus claims (for example lactose_free) and returns a match confidence.",
        "Pack maths: 1,000 l becomes 1,000 x 1 l; the scenario evals accept at most one lot more than asked.",
        "Request check, for the product actually delivered: product, every claim and stated attribute (UHT or chilled, fat level, pack type), exclusions such as dairy-free also in a purpose clause, the brand (another brand is marked substituted and the mandate decides), quantity (at most one lot over), date, a budget, zero included, and any instruction to hold (\"do not order yet\", \"älä tilaa ennen kuin hyväksyn\"), each met, unmet, substituted or unchecked, signed inside the offer and shown on the order.",
        "An order whose offer carries no buyer words waits for the approver (rule request.words), so an agent cannot skip the check by leaving the words out.",
        "The match.confidence rule refuses a certain mismatch (wrong product, a missing claim, far too little) and sends anything unmet, unchecked, uncertain or not understood to a person with the requirement named.",
        "3 demo buyers with their own signed mandates (Nordic Harbour Hotels, Kaisla School Kitchens, Ravintola Laine). The same sentence confirms for the hotel and is blocked for the school kitchens, which exclude sesame even as a trace.",
        "Every offer line carries the product's country of origin, labels, delivery reliability and category-average CO2 inside the signature; search can rank a clear match by price, reliability or sustainability."
      ],
      "where": [
        "src/lib/catalog/search.ts",
        "src/lib/offers/packs.ts",
        "src/lib/request/index.ts",
        "src/lib/gateway/confidence.ts",
        "src/components/order/RequestProof.tsx",
        "src/lib/mandate/defaults.ts",
        "src/lib/contracts/facts.ts",
        "src/components/valio/TwoKitchens.tsx"
      ],
      "check": [
        {
          "kind": "eval",
          "value": "2"
        },
        {
          "kind": "eval",
          "value": "12"
        },
        {
          "kind": "eval",
          "value": "19"
        },
        {
          "kind": "eval",
          "value": "22"
        },
        {
          "kind": "test",
          "value": "tests/request/fidelity.test.ts"
        },
        {
          "kind": "test",
          "value": "tests/demo/two-kitchens.test.ts"
        },
        {
          "kind": "test",
          "value": "tests/offers/packs.test.ts"
        }
      ],
      "measured": [
        "31/31 scenario cases end on the expected route with the expected concept, amount and date",
        "3 demo buyers, 31 seeded requests played through the real gateway"
      ],
      "limit": "The product concepts and category groups are our own, built from the product data. The catalogue sells \"lactose-free milk\" as \"lactose-free milk drink\"; Laituri treats the two as one request and names the product it chose."
    },
    {
      "name": "Autonomy and controls",
      "weight": 20,
      "claim": "A buyer-signed mandate sets what the agent may do alone, what waits for a named person and what is never allowed, and every decision is a signed entry in a hash-chained log that both sides verify with public keys.",
      "built": [
        "17 named mandate rules run on every order; each result (pass, ask or block) is listed on the order with its reason.",
        "Three tiers: the agent orders alone, a person is asked, or the order is refused.",
        "Mandates and offers are bound by signature to the workspace that signed them; an order must carry the mandate in force there byte for byte.",
        "While the audit log does not verify, no order confirms alone (rule audit.integrity) and approvals are paused.",
        "Approvals are signed with the approver key. Agent credentials cannot approve, over REST or MCP.",
        "An agent cannot raise its own limit: a changed mandate without the buyer's signature is refused.",
        "The wholesaler can suspend an agent; every refused call, the suspension and the resumption are signed audit entries.",
        "The buyer (/buyer/log) and the wholesaler (/valio/audit) read the same log; the browser re-checks the chain and every signature with the published keys."
      ],
      "where": [
        "src/lib/policy/evaluate.ts",
        "src/lib/policy/rules",
        "src/lib/mandate/index.ts",
        "src/lib/orders/approval.ts",
        "src/lib/gateway/suspension.ts",
        "src/lib/audit/chain.ts",
        "src/lib/audit/verify.ts",
        "src/lib/console/verify.ts"
      ],
      "check": [
        {
          "kind": "command",
          "value": "npm run eval -- --suite guardrails"
        },
        {
          "kind": "eval",
          "value": "G10"
        },
        {
          "kind": "eval",
          "value": "G5"
        },
        {
          "kind": "url",
          "value": "/buyer/mandate"
        },
        {
          "kind": "test",
          "value": "tests/gateway/suspension.test.ts"
        }
      ],
      "measured": [
        "Guardrails eval suite: 35/35 cases pass (bar 100%)",
        "Trust eval suite: 17/17 cases pass (bar 100%)",
        "17 mandate rules, 3 signing keys (supplier, buyer, approver)"
      ],
      "limit": "In the demo one server holds all three private keys, and the approver signs in with a passcode or one-click demo access that the audit log labels as such."
    },
    {
      "name": "Robustness to exception situations",
      "weight": 15,
      "claim": "When a request cannot be met in full and on time, the wholesaler returns signed counter-offers ranked good, better and best, and the buyer's mandate decides whether the agent may take one alone.",
      "built": [
        "Short stock: a split (keep what is in stock, fill the rest like for like) or a full substitute, each signed and each checked against the buyer's words for what it delivers.",
        "A waiting order keeps its price until the order cut-off; stock is checked again at approval, and a shortfall returns fresh signed alternatives.",
        "Stock across orders: availability is the wholesaler's figure minus this workspace's own confirmed and waiting orders, checked again at placement.",
        "Lines of one product are merged before every limit, so splitting a line passes no ceiling.",
        "Order cut-off passed: the offer as asked is blocked and on-time alternatives come with it.",
        "Late delivery within the mandate's grace days asks a person; later is refused.",
        "A substitute that is not like for like, or costs more than the mandate's cap, asks a person; a mandate with no substitutes refuses it.",
        "Edited, expired, replayed or wrongly signed offers are refused.",
        "Simultaneous orders over the weekly limit: only what fits confirms, the rest waits.",
        "Instructions written into the request text do not change the route."
      ],
      "where": [
        "src/lib/offers/alternatives.ts",
        "src/lib/offers/substitutes.ts",
        "src/lib/policy/rules/fulfilment.ts",
        "src/lib/orders/lock.ts",
        "src/components/order/Alternatives.tsx"
      ],
      "check": [
        {
          "kind": "eval",
          "value": "5"
        },
        {
          "kind": "eval",
          "value": "6"
        },
        {
          "kind": "eval",
          "value": "13"
        },
        {
          "kind": "eval",
          "value": "G20"
        },
        {
          "kind": "eval",
          "value": "G14"
        }
      ],
      "measured": [
        "Scenarios eval suite: 31/31 cases pass (bar >=90%)",
        "Guardrails eval suite: 35/35 cases pass (bar 100%)"
      ],
      "limit": "Substitutes are chosen like for like within the wholesaler's own concept and category, not through recipe logic."
    },
    {
      "name": "Openness and credible path toward production",
      "weight": 15,
      "claim": "One set of zod schemas produces the MCP tools, the REST routes and the OpenAPI 3.1 document; signatures are JWS with keys published as JWKS; assumptions, decisions and next steps are written down.",
      "built": [
        "7 MCP tools and 24 REST operations on 21 paths, from one schema set.",
        "6 discovery documents: OpenAPI is served beside the JWKS, agent card, AI catalog, UCP profile, OAuth protected-resource metadata and llms.txt.",
        "An outside agent that knows only the text of llms.txt, fetched over HTTP, buys the milk by following its REST flow.",
        "Standards table separates \"used\" (implemented and tested) from \"follows\" (names and shapes taken from the standard).",
        "Decision records in docs/decisions; assumptions and the path to production in docs/ and on /evidence.",
        "MIT licence."
      ],
      "where": [
        "src/lib/contracts",
        "src/lib/mcp/tools.ts",
        "src/lib/openapi/registry.ts",
        "src/lib/discovery",
        "src/lib/trust/jws.ts",
        "docs/decisions"
      ],
      "check": [
        {
          "kind": "command",
          "value": "npm run eval -- --suite openness"
        },
        {
          "kind": "url",
          "value": "/api/v1/openapi.json"
        },
        {
          "kind": "url",
          "value": "/.well-known/jwks.json"
        },
        {
          "kind": "url",
          "value": "/llms.txt"
        }
      ],
      "measured": [
        "Openness eval suite: 8/8 cases pass (bar 100%)",
        "Unit and integration tests: 903/903 pass in 107 files"
      ],
      "limit": "Agents use demo bearer tokens, not OAuth 2.1. AP2, UCP and Peppol are followed in names and shapes; no Peppol UBL document is produced."
    }
  ],
  "core": [
    {
      "quote": "Product, price & availability lookup: a structured way for an external agent to search for and retrieve what's available and at what price.",
      "status": "covered",
      "answer": "search_catalog and get_product (MCP), POST /api/v1/products/search and GET /api/v1/products/{id} (REST). Hits carry an indicative live price, stock, delivery date and order cut-off, plus pack maths for the stated need. 7 MCP tools in all.",
      "where": [
        "src/lib/catalog/search.ts",
        "src/lib/gateway/preview.ts",
        "src/lib/mcp/tools.ts"
      ],
      "check": [
        {
          "kind": "eval",
          "value": "O6"
        },
        {
          "kind": "url",
          "value": "/api/v1/openapi.json"
        }
      ]
    },
    {
      "quote": "Requesting price & availability, then placing an order: a defined interaction for moving from inquiry to commitment, based on fixed prices rather than a negotiated quote.",
      "status": "covered",
      "answer": "request_quote returns an offer signed by the wholesaler key with a fixed net price, valid 15 minutes and bound to the workspace that issued it. place_order takes that offer unchanged plus the signed mandate in force and an idempotency key. One offer gives one order. An order that waits for a person keeps its price until the order cut-off. Over REST the answer says the outcome: 201 confirmed, 202 waiting for a person, 422 blocked with the blocked order in the problem body.",
      "where": [
        "src/lib/offers/quote.ts",
        "src/lib/offers/assemble.ts",
        "src/lib/orders/place.ts"
      ],
      "check": [
        {
          "kind": "eval",
          "value": "1"
        },
        {
          "kind": "eval",
          "value": "G1"
        },
        {
          "kind": "eval",
          "value": "G9"
        }
      ]
    },
    {
      "quote": "Permission & human-approval management: what the agent may decide on its own, and what must go to a human for sign-off.",
      "status": "covered",
      "answer": "A buyer-signed mandate with 17 rules in three tiers (alone, ask a person, never). Orders in the ask tier wait for the named approver, whose decision is signed with the approver key.",
      "where": [
        "src/lib/policy/evaluate.ts",
        "src/lib/mandate/defaults.ts",
        "src/lib/orders/approval.ts",
        "src/app/approve/[orderId]/page.tsx"
      ],
      "check": [
        {
          "kind": "eval",
          "value": "4"
        },
        {
          "kind": "eval",
          "value": "G10"
        },
        {
          "kind": "url",
          "value": "/buyer/mandate"
        }
      ]
    }
  ],
  "optional": [
    {
      "quote": "Delivery time & sustainability comparison: not just price, but delivery reliability and the certificates/sustainability labels already shown in the webshop. (Category-level carbon-footprint ranges are a stretch goal.)",
      "status": "covered",
      "answer": "Every search hit and every signed offer line carries country of origin, labels, delivery reliability (0 to 1) and the category-average CO2 per kg. Search ranks a clear match by price, reliability or sustainability. The CO2 figure is a category average, not a range and not a product measurement.",
      "where": [
        "src/lib/contracts/facts.ts",
        "src/lib/catalog/facts.ts",
        "src/lib/gateway/ranking.ts"
      ],
      "check": [
        {
          "kind": "eval",
          "value": "T14"
        },
        {
          "kind": "test",
          "value": "tests/offers/facts.test.ts"
        }
      ]
    },
    {
      "quote": "Substitution and other exception handling: what happens when the exact request can't be met.",
      "status": "covered",
      "answer": "Short stock, a passed order cut-off and late delivery each return signed alternatives with a tier (good, better, best), the tier's meaning and the trade-off in one sentence. The mandate decides whether a substitute may be taken alone.",
      "where": [
        "src/lib/offers/alternatives.ts",
        "src/lib/policy/rules/fulfilment.ts"
      ],
      "check": [
        {
          "kind": "eval",
          "value": "5"
        },
        {
          "kind": "eval",
          "value": "6"
        }
      ]
    },
    {
      "quote": "Traceability and explainability: a human-readable record of what the agent did and why.",
      "status": "covered",
      "answer": "Each step appends a signed entry with a one-sentence summary to a hash chain. Each order lists every rule with its result and reason. The agent's runs show each tool call in plain words.",
      "where": [
        "src/lib/audit/chain.ts",
        "src/lib/orders/summaries.ts",
        "src/components/agent/RunBlock.tsx",
        "src/app/buyer/log/page.tsx"
      ],
      "check": [
        {
          "kind": "eval",
          "value": "T6"
        },
        {
          "kind": "url",
          "value": "/buyer/log"
        }
      ]
    }
  ],
  "problems": [
    {
      "quote": "Trust protocol: the minimum data/verification standard for an external agent to trust Valio Aimo's claims on stock, price and origin without a manual check.",
      "status": "covered",
      "answer": "Price, stock, delivery date, order cut-off, origin and labels are inside one offer signed with ES256; the public keys are at /.well-known/jwks.json; the offer expires after 15 minutes, is bound to the workspace that issued it and gives one order. docs/trust-protocol.md lists the verification steps.",
      "where": [
        "docs/trust-protocol.md",
        "src/lib/contracts/offer.ts",
        "src/lib/trust/jws.ts",
        "src/lib/discovery/jwks.ts"
      ],
      "check": [
        {
          "kind": "eval",
          "value": "T2"
        },
        {
          "kind": "eval",
          "value": "T14"
        },
        {
          "kind": "url",
          "value": "/.well-known/jwks.json"
        }
      ]
    },
    {
      "quote": "Guardrails model: which decisions require human sign-off (spend threshold, substitutions outside stated preferences) vs. fully autonomous.",
      "status": "covered",
      "answer": "Spend per order and per week, category, allergens and claims, substitution policy and price cap, partial and late delivery, match confidence and first-time products: 17 rules, each with a defined tier.",
      "where": [
        "src/lib/policy/rules/spend.ts",
        "src/lib/policy/rules/fulfilment.ts",
        "src/lib/policy/rules/products.ts",
        "src/lib/policy/rules/trust.ts"
      ],
      "check": [
        {
          "kind": "command",
          "value": "npm run eval -- --suite guardrails"
        },
        {
          "kind": "test",
          "value": "tests/policy/evaluate.test.ts"
        }
      ]
    },
    {
      "quote": "Response logic: how an agent handles partial availability and substitution ranking (\"good-better-best\") within Valio Aimo's own assortment.",
      "status": "covered",
      "answer": "good is the cheapest like-for-like product that covers the need on time; better keeps what is in stock and fills the rest, or takes the most reliable product; best is an organic or Finnish-label product. Each meaning is returned with the alternative.",
      "where": [
        "src/lib/offers/alternatives.ts",
        "src/components/order/Alternatives.tsx"
      ],
      "check": [
        {
          "kind": "eval",
          "value": "5"
        },
        {
          "kind": "test",
          "value": "tests/offers/quote.test.ts"
        }
      ]
    },
    {
      "quote": "Audit trail: a usable, human-readable log of an agent's purchasing decisions, for both buyer and Valio Aimo.",
      "status": "covered",
      "answer": "One signed hash chain, read by the buyer at /buyer/log and by the wholesaler at /valio/audit (filter by buyer, export as JSON). Anyone can verify it with the published keys.",
      "where": [
        "src/app/buyer/log/page.tsx",
        "src/app/valio/audit/page.tsx",
        "src/lib/audit/verify.ts",
        "src/lib/console/verify.ts"
      ],
      "check": [
        {
          "kind": "eval",
          "value": "T6"
        },
        {
          "kind": "eval",
          "value": "T10"
        },
        {
          "kind": "url",
          "value": "/valio/audit"
        }
      ]
    }
  ],
  "scenarios": [
    {
      "name": "Need → recipe → order",
      "quote": "a chef plans next week's menu around a seasonal ingredient. The agent suggests recipes, works out what's missing from stock, and places the order.",
      "status": "not built",
      "note": "No recipe model and no kitchen stock data. The ordering step at the end is what Laituri provides.",
      "where": [],
      "check": []
    },
    {
      "name": "Last-minute event",
      "quote": "an unexpected booking for tomorrow. The agent identifies what the recipes require, flags what can't be sourced in time, and proposes substitutions through recipe logic rather than a simple SKU swap.",
      "status": "partly",
      "note": "\"By tomorrow\" requests, passed order cut-offs and late dates are flagged with signed on-time alternatives. No recipe logic: alternatives are like for like.",
      "where": [
        "src/lib/policy/rules/fulfilment.ts",
        "src/lib/offers/alternatives.ts"
      ],
      "check": [
        {
          "kind": "eval",
          "value": "6"
        },
        {
          "kind": "eval",
          "value": "13"
        }
      ]
    },
    {
      "name": "Supplier stock-out",
      "quote": "a key product runs out mid-week. The agent proposes a recipe-compatible substitute before the kitchen notices the gap, and explains the trade-off.",
      "status": "partly",
      "note": "Short stock returns a signed split or substitute with a one-sentence trade-off. It answers at quote time, not before the kitchen asks, and substitutes are like for like rather than recipe-compatible.",
      "where": [
        "src/lib/offers/alternatives.ts"
      ],
      "check": [
        {
          "kind": "eval",
          "value": "5"
        }
      ]
    },
    {
      "name": "Proactive selling agent",
      "quote": "Valio Aimo's own agent spots a new or seasonal product and proactively suggests it to the buyer's agent.",
      "status": "not built",
      "note": "The wholesaler side shows unmet demand (what agents asked for and could not get), which such an agent would start from. Nothing is pushed to buyers' agents.",
      "where": [
        "src/lib/insights/requests.ts",
        "src/app/valio/demand/page.tsx"
      ],
      "check": [
        {
          "kind": "url",
          "value": "/valio/demand"
        }
      ]
    },
    {
      "name": "Allergen-safe substitution under pressure",
      "quote": "in a school or hospital kitchen, a substitution must still satisfy strict allergen and nutrition rules.",
      "status": "partly",
      "note": "The school kitchens' mandate excludes peanut, nuts and sesame, also as traces, and every line, substitutes included, is checked. Missing allergen data blocks the line. Nutrition rules are not modelled.",
      "where": [
        "src/lib/policy/rules/products.ts",
        "src/lib/mandate/defaults.ts"
      ],
      "check": [
        {
          "kind": "eval",
          "value": "9b"
        },
        {
          "kind": "test",
          "value": "tests/demo/two-kitchens.test.ts"
        }
      ]
    },
    {
      "name": "Standing order drift detection",
      "quote": "the agent learns a kitchen's recurring weekly order and flags anomalies (portion counts creeping up, cost drifting) before auto-renewing it.",
      "status": "not built",
      "note": "No standing orders. The nearest control is the weekly spend limit and the rule that sends a first-time product to a person.",
      "where": [
        "src/lib/policy/rules/spend.ts"
      ],
      "check": [
        {
          "kind": "eval",
          "value": "14"
        },
        {
          "kind": "eval",
          "value": "18"
        }
      ]
    }
  ],
  "privacy": [
    {
      "quote": "Real systems",
      "status": "covered",
      "answer": "The upstream API key and address stay on the server. Agents call Laituri's own routes; upstream answers are normalised into Laituri's contracts before anything is returned, and no error names an upstream URL, path or key.",
      "where": [
        "src/lib/valio/http.ts",
        "src/lib/valio/normalise.ts"
      ],
      "check": [
        {
          "kind": "url",
          "value": "/api/v1/openapi.json"
        }
      ]
    },
    {
      "quote": "Confidential data",
      "status": "covered",
      "answer": "No credential, no reads: without an agent key, a sandbox cookie or a signed-in session, orders, offers, the mandate, approvals, activity, the supplier overview and audit entries answer 401. An agent key reads only its own buyer's records; in the audit log it sees other buyers' entries only as redacted stubs that still verify the chain. Agents receive only the fields in the published schemas; the MCP server parses every tool result against its output schema and withholds one that does not parse.",
      "where": [
        "src/lib/http/buyer-scope.ts",
        "src/app/api/v1/audit/route.ts",
        "src/lib/audit/verify.ts",
        "src/lib/mcp/server.ts"
      ],
      "check": [
        {
          "kind": "eval",
          "value": "O6"
        },
        {
          "kind": "eval",
          "value": "G21"
        },
        {
          "kind": "test",
          "value": "tests/buyers/isolation.test.ts"
        },
        {
          "kind": "test",
          "value": "tests/mcp/output-validation.test.ts"
        }
      ]
    },
    {
      "quote": "The buyer's terms",
      "status": "covered",
      "answer": "The dashboard does not show a buyer's limits; it shows which rule fired. The wholesaler's overview counts which mandate rule held or stopped an order and its tier, never the limits or values behind it.",
      "where": [
        "src/lib/insights/rules.ts",
        "src/lib/contracts/supplier.ts"
      ],
      "check": [
        {
          "kind": "test",
          "value": "tests/insights/insights.test.ts"
        }
      ]
    },
    {
      "quote": "The marketplace roadmap",
      "status": "covered",
      "answer": "Nothing in the interface depends on a marketplace: it exposes today's catalogue, price, availability and ordering only. All data is the challenge's synthetic data.",
      "where": [
        "src/lib/openapi/registry.ts",
        "fixtures/upstream"
      ],
      "check": [
        {
          "kind": "url",
          "value": "/api/v1/openapi.json"
        }
      ]
    }
  ],
  "simulated": [
    {
      "topic": "Orders",
      "brief": "no order endpoint upstream, so Laituri writes the order and receipt, marked simulated",
      "text": "Orders are simulated. The mock API has price and availability but no order endpoint, so Laituri creates the order, the receipt and the log entry itself and marks every order simulated: true.",
      "where": [
        "src/lib/orders/confirm.ts"
      ]
    },
    {
      "topic": "Keys",
      "brief": "one server holds the buyer, supplier and approver keys",
      "text": "One server holds all three private keys (buyer, supplier, approver) in .data/keys, so one deployment plays all three parties. In production each party keeps its own key.",
      "where": [
        "src/lib/trust/keys.ts"
      ]
    },
    {
      "topic": "Agent identity",
      "brief": "demo bearer keys, not OAuth 2.1",
      "text": "Agents use demo bearer tokens instead of OAuth 2.1. POST /api/v1/workspace creates a private sandbox for a client without a browser and returns its random agent keys, one per demo buyer, and its MCP address; it is rate-limited per client address and never returns a browser cookie. Each browser's sandbox has random keys of its own. The fixed demo-agent-token, demo-kaisla-token and demo-laine-token belong to the shared demo workspace that the evals use. The OAuth protected-resource metadata is already published.",
      "where": [
        "src/lib/http/agent.ts",
        "src/lib/workspace/sandboxes.ts",
        "src/app/api/v1/workspace/route.ts",
        "src/app/.well-known/oauth-protected-resource/route.ts"
      ]
    },
    {
      "topic": "Workspaces",
      "brief": "a private in-memory sandbox per visitor, copied from a seeded week",
      "text": "Each visitor works in a private sandbox: an in-memory copy of all mutable state (offers, orders, audit chain, activity, demand, mandates, suspensions, demo runs), copied from a seeded template written once at start-up, so it starts with the same verifiable week. A browser gets its sandbox through a random HttpOnly cookie; a curl user or an outside agent gets one from POST /api/v1/workspace. At most 300 sandboxes live at once (LAITURI_SANDBOX_MAX); the least recently used is dropped first, and one unused for 60 minutes (LAITURI_SANDBOX_IDLE_MINUTES) is replaced by a fresh copy. Sandboxes are never written to disk. Calls with the fixed demo tokens reach the one shared demo workspace, which is persisted. The catalogue, the signing keys and the upstream client are shared by all.",
      "where": [
        "src/lib/workspace/context.ts",
        "src/lib/workspace/sandboxes.ts",
        "src/lib/http/workspace.ts",
        "src/proxy.ts"
      ]
    },
    {
      "topic": "Demo buyers",
      "brief": "three buyers under the mock API's one customer number",
      "text": "3 demo buyers (Nordic Harbour Hotels, Kaisla School Kitchens, Ravintola Laine) trade under the mock API's one customer number; the signed buyer id keeps them apart.",
      "where": [
        "src/lib/mandate/defaults.ts"
      ]
    },
    {
      "topic": "Seeded history",
      "brief": "a week of requests played through the real gateway at start-up",
      "text": "On an empty state the server plays 31 requests by the demo buyers over about a week through the real gateway, at past times. Every offer, order, approval and audit entry is signed and chained as in live use, and marked seeded. LAITURI_SEED=off turns it off.",
      "where": [
        "src/lib/seed/plan.ts",
        "src/lib/seed/index.ts"
      ]
    },
    {
      "topic": "Demo access",
      "brief": "one-click approver and operator sign-in, in a browser's own sandbox only, labelled in the log",
      "text": "In a browser's own sandbox, and only there, one click signs a visitor in as the hotel's approver or as the wholesaler's operator without a passcode; each approval, mandate change, suspension or resumption made that way says \"demo access\" in the audit log. The session is bound to that sandbox. Any HTTP client that keeps cookies can play both the browser and the agent in its own sandbox; the effect stays inside that sandbox. A sandbox made with POST /api/v1/workspace gets agent keys only, never the cookie that demo access needs. The shared workspace takes the passcodes (LAITURI_APPROVER_PASSCODE, LAITURI_OPERATOR_PASSCODE); without one, a production server offers no sign-in there and only a local development server signs anyone in. LAITURI_DEMO_ACCESS=off disables demo access.",
      "where": [
        "src/lib/http/sign-in.ts",
        "src/lib/http/console-session.ts",
        "src/lib/http/operator-session.ts"
      ]
    },
    {
      "topic": "Who reads what",
      "brief": "no credential, no reads; agent keys read only their own buyer",
      "text": "No credential, no reads. A caller with no agent key, no sandbox cookie and no signed-in session gets 401 on orders, offers, the mandate, approvals, activity, the supplier overview and audit entries. An agent key reads only its own buyer's records. A browser in its own sandbox and a signed-in operator read everything in that workspace. Public: the OpenAPI document, the JWKS and the other .well-known documents, llms.txt, the agent card, health, evidence.json, the signed audit checkpoint and the verify summary.",
      "where": [
        "src/lib/http/buyer-scope.ts",
        "src/lib/http/handler.ts"
      ]
    },
    {
      "topic": "Demo controls",
      "brief": "scenes, log tampering and reset act on the caller's own workspace",
      "text": "Starting a scene, altering one log entry to show detection and resetting act on the caller's own workspace and refuse agent credentials. In a sandbox the browser may use them freely; in the shared workspace altering the log and resetting need the approver. Suspending or resuming an agent needs a wholesaler operator session everywhere. The altered entry is put back after five minutes. A production deployment has none of these controls open.",
      "where": [
        "src/lib/demo/tamper.ts",
        "src/app/api/v1/demo/reset/route.ts",
        "src/app/api/v1/supplier/agents/[agentId]/suspension/route.ts"
      ]
    },
    {
      "topic": "State",
      "brief": "memory with a JSON snapshot on disk, one process",
      "text": "State is kept in memory with a JSON snapshot on disk. An order, an approval, a mandate change, a suspension and each audit entry are written to disk (temporary file, fsync, rename) before the call returns, and a failed write fails the call. Offers, activity and demand records are saved a moment later. An unreadable snapshot is kept aside under a new name and reported by /api/v1/health, and the service starts empty. One process serialises order decisions, so two orders cannot pass a limit together.",
      "where": [
        "src/lib/store/memory.ts",
        "src/lib/store/snapshot.ts",
        "src/lib/orders/lock.ts"
      ]
    },
    {
      "topic": "Recorded mode",
      "brief": "recorded upstream answers and a fixed clock for tests and evals",
      "text": "Without an API key, recorded price and availability answers for 255 products replay with a fixed clock. Tests and evals always use this mode.",
      "where": [
        "src/lib/valio/recorded.ts",
        "fixtures/upstream"
      ]
    },
    {
      "topic": "Product concepts",
      "brief": "our own concept list and category groups",
      "text": "Category groups and the product concept list are our own, built from the product data. \"Lactose-free milk\" is sold as \"lactose-free milk drink\"; Laituri treats them as one request and shows the product name it chose.",
      "where": [
        "src/lib/catalog/build/groups.ts",
        "src/lib/catalog/lexicon.ts"
      ]
    },
    {
      "topic": "Upstream data",
      "brief": "our reading of cut-off times and untracked stock",
      "text": "A closingTime in the past means that delivery date can no longer be ordered. An available amount of 9999999999 or 999999 means the wholesaler does not track stock, not that stock is unlimited: the signed offer line says stock: \"untracked\" and gives no available count. Availability returns the next delivery date whatever date is asked; Laituri compares it with the requested date.",
      "where": [
        "src/lib/valio/normalise.ts"
      ]
    },
    {
      "topic": "Plausible quantities",
      "brief": "our own ceilings per product, after merging lines",
      "text": "Lines of one product are merged before every limit, so two lines cannot pass a ceiling one line would not. One product in one order may ask for at most 20,000 l, 20,000 kg or 20,000 sales units (LAITURI_MAX_LINE_LITRES, LAITURI_MAX_LINE_KG, LAITURI_MAX_LINE_UNITS); a larger quantity, or a delivery date before today, is refused at quote time with 422. A product on untracked stock above 25 % of its ceiling waits for a person (delivery.partial). The ceilings are our assumption, not the wholesaler's.",
      "where": [
        "src/lib/offers/limits.ts",
        "src/lib/policy/rules/fulfilment.ts"
      ]
    },
    {
      "topic": "Pack contents",
      "brief": "litres and kilograms per unit from product data or price ratios",
      "text": "Litres or kilograms per sales unit come from the product's unit data, else from the price divided by the comparison price.",
      "where": [
        "src/lib/offers/packs.ts"
      ]
    },
    {
      "topic": "Carbon figure",
      "brief": "a category average, not a product measurement",
      "text": "The CO2 figure is the average for the product's category (categoryAverageCo2PerKg), not a measurement of the product.",
      "where": [
        "src/lib/contracts/facts.ts"
      ]
    },
    {
      "topic": "Request check",
      "brief": "our reading of the buyer's words, in English, Finnish and Swedish",
      "text": "The wholesaler compares the buyer's words, sent as the quote's intent, with every offer before it signs it, for the product actually delivered: each alternative and each substitute or split line is checked as it would arrive. A claim or attribute the words state (lactose-free, organic, UHT or chilled, fat level, pack type when stated, and exclusions such as dairy-free or nut-free, also in a purpose clause such as \"for a dairy-free breakfast\") that the delivered product lacks is unmet, and such an order is never confirmed alone. A brand the words name, when another brand is delivered, is marked substituted: the mandate's substitution rule decides (like_for_like allows another brand of the same kind and says so by name; none blocks). The quantity, the date, a budget (zero included) and every instruction to hold (\"do not order yet\", \"hold\", \"wait\", \"ask me first\", in Finnish and Swedish too) are checked as well. Words the service does not understand, or a clause it cannot prove harmless, send the order to a person. A certain mismatch (wrong product, a missing claim, less than half the need asked for) blocks the order. Each result is signed inside the offer. An order whose offer carries no buyer words waits for the approver (rule request.words, on for all three demo buyers). The agent's stated match confidence never counts for more than the wholesaler finds.",
      "where": [
        "src/lib/request/index.ts",
        "src/lib/request/requirements.ts",
        "src/lib/policy/rules/products.ts",
        "src/lib/gateway/confidence.ts"
      ]
    },
    {
      "topic": "Approver judgement",
      "brief": "the approver decides what the ask tier flagged; prices held until the cut-off",
      "text": "An approval re-checks the order against the mandate in force at that moment. When an order goes to a person, the wholesaler signs and logs that it holds the offer's price until the product's order cut-off for the delivery date, never beyond it, so the approver can answer after lunch. Stock is checked again at approval; a shortfall returns the order blocked with fresh signed alternatives instead of a confirmation. What the ask tier flagged, such as a weekly limit, is the approver's to decide, so an approver can approve several waiting orders that together pass the weekly limit. A never-tier rule, an invalid or expired mandate, or an offer whose price hold ended or that was used stops the approval, and the order is blocked with a signed audit entry.",
      "where": [
        "src/lib/orders/approval.ts",
        "src/lib/orders/recheck.ts"
      ]
    },
    {
      "topic": "Audit completeness",
      "brief": "a cut tail is caught only against a remembered head",
      "text": "Verification proves that entries were not changed, reordered or removed from the middle. A verifier that remembers a head (size and hash) checks that the entry at that size still carries that hash in any later log, so a log that was cut, cut and extended, or rewritten from an earlier entry is detected whatever its length. The service will not append onto a tail that fails its hash, link, signature or remembered head. While the log does not verify, approvals are paused and no order confirms alone: it waits for a person with the reason named (rule audit.integrity).",
      "where": [
        "src/lib/audit/verify.ts",
        "src/lib/audit/tail.ts",
        "src/lib/audit/chain.ts",
        "src/lib/audit/head.ts",
        "src/lib/gateway/gateway.ts"
      ]
    },
    {
      "topic": "Audit log reads",
      "brief": "other buyers' entries reach an agent only as verifiable stubs",
      "text": "There is one signed chain per workspace. An agent key reads its own buyer's entries in full and every other entry as a redacted stub: sequence, time, event type, previous hash, content digest, hash and signature. Each entry's signed hash covers a digest of its content, so the stubs still verify the chain end to end without showing another buyer's products, quantities or prices. A browser in its own sandbox and a signed-in operator read every entry in full. Production adds an inclusion proof against a signed checkpoint for each buyer's entries.",
      "where": [
        "src/app/api/v1/audit/route.ts",
        "src/lib/audit/verify.ts",
        "src/lib/orders/summaries.ts"
      ]
    },
    {
      "topic": "Workspace binding",
      "brief": "mandates and offers are bound to the workspace that signed them",
      "text": "Signed mandates and offers are bound to the workspace that signed them by an audience claim inside the signature. An order must present, byte for byte, the mandate in force for its buyer in that workspace and an offer that workspace issued. An offer is single use across all workspaces. A refusal for an invalid signature has its own reason (\"Refused: the offer's signature did not verify\"), apart from mandate blocks.",
      "where": [
        "src/lib/orders/place.ts",
        "src/lib/orders/read-offer.ts",
        "src/lib/mandate/claims.ts"
      ]
    },
    {
      "topic": "Stock across orders",
      "brief": "upstream stock minus this workspace's own orders",
      "text": "The mock API does not lower its stock figure when Laituri confirms an order. So availability in an offer is the wholesaler's figure minus this workspace's own confirmed and waiting orders for that delivery date, and it is checked again when the order is placed. Rejected, blocked and expired orders release their units.",
      "where": [
        "src/lib/offers/quote.ts",
        "src/lib/orders/place.ts"
      ]
    },
    {
      "topic": "Buyer agent",
      "brief": "a scripted agent for scenes, tests and evals; Claude or Gemini for free text",
      "text": "The fixed demo scenes, the tests and the evals use a scripted buyer agent, so results repeat. Free-text requests use Claude or Gemini when a key is set, else the scripted agent.",
      "where": [
        "src/lib/agent/purchase.ts",
        "src/lib/agent/llm/engine.ts"
      ]
    },
    {
      "topic": "Voice",
      "brief": "browser speech, ElevenLabs when a key is set",
      "text": "Spoken input uses the browser's Web Speech API. Spoken answers use the browser's voice, or ElevenLabs when a key is set on the server.",
      "where": [
        "src/lib/voice/speech-to-text.ts",
        "src/lib/voice/text-to-speech.ts"
      ]
    }
  ],
  "notBuilt": [
    {
      "topic": "Recipes",
      "text": "No recipe model: substitutes are like for like, not recipe-compatible.",
      "where": []
    },
    {
      "topic": "Kitchen stock",
      "text": "The agent does not read the kitchen's own stock or ERP.",
      "where": []
    },
    {
      "topic": "Proactive selling",
      "text": "No wholesaler agent that pushes products to buyers' agents.",
      "where": []
    },
    {
      "topic": "Standing orders",
      "text": "No recurring orders and no drift detection.",
      "where": []
    },
    {
      "topic": "Payments",
      "text": "No agentic payment (x402 or similar); orders follow the buyer's existing account terms.",
      "where": []
    },
    {
      "topic": "Peppol documents",
      "text": "The receipt carries Peppol order-response codes, but no UBL document is produced.",
      "where": []
    },
    {
      "topic": "Nutrition rules",
      "text": "Allergens and claims are checked; nutrition values are not.",
      "where": []
    }
  ],
  "production": [
    {
      "step": "Real order entry",
      "why": "Replace the simulated order with the ERP's order entry and return its order number in the signed receipt.",
      "where": [
        "src/lib/orders/confirm.ts"
      ]
    },
    {
      "step": "OAuth 2.1 per agent",
      "why": "Scoped tokens (catalog:read, quote:create, order:place) in place of demo tokens. The scopes are already declared in the OpenAPI document and the protected-resource metadata.",
      "where": [
        "src/lib/discovery/documents.ts",
        "src/lib/openapi/registry.ts"
      ]
    },
    {
      "step": "Key custody per party",
      "why": "The wholesaler runs Laituri with the supplier key only; the buyer signs mandates in its own system; the approver signs with a personal credential.",
      "where": [
        "src/lib/trust/keys.ts"
      ]
    },
    {
      "step": "Buyer identity",
      "why": "A verifiable organisation credential (the planned EU Business Wallet) instead of a demo key.",
      "where": []
    },
    {
      "step": "Mandate copy at the wholesaler",
      "why": "Keep the signed mandate in the customer master as well, so both sides hold the same signed copy.",
      "where": [
        "src/lib/mandate/index.ts"
      ]
    },
    {
      "step": "Database",
      "why": "Transactions and row locks in place of the single-process store.",
      "where": [
        "src/lib/store/types.ts"
      ]
    },
    {
      "step": "External audit anchor",
      "why": "Anchor audit checkpoints in an external transparency service (SCITT), so completeness no longer depends on the verifier's memory.",
      "where": [
        "src/lib/audit/checkpoint.ts"
      ]
    },
    {
      "step": "Peppol ordering",
      "why": "Order and order-response documents for customers that already trade over Peppol; the response codes are already in the receipt.",
      "where": [
        "src/lib/orders/receipt.ts"
      ]
    }
  ],
  "links": [
    {
      "label": "Guided walkthrough",
      "href": "/buyer?tour=1",
      "note": "Plays the order, approval, exception and log steps in the running product"
    },
    {
      "label": "Buyer side",
      "href": "/buyer",
      "note": "Today, orders, mandate and log for the buyer's purchasing lead"
    },
    {
      "label": "Valio Aimo side",
      "href": "/valio",
      "note": "Connected agents, demand, unmet requests, audit"
    },
    {
      "label": "Connect an agent",
      "href": "/connect",
      "note": "MCP and REST set-up for common agent applications"
    },
    {
      "label": "OpenAPI 3.1",
      "href": "/api/v1/openapi.json",
      "note": "Every REST operation, with schemas and examples"
    },
    {
      "label": "llms.txt",
      "href": "/llms.txt",
      "note": "Plain-text guide an agent can follow to buy"
    },
    {
      "label": "JWKS",
      "href": "/.well-known/jwks.json",
      "note": "Public keys of the buyer, supplier and approver roles"
    },
    {
      "label": "Audit verification",
      "href": "/api/v1/audit/verify",
      "note": "Live check of the hash chain and every signature"
    },
    {
      "label": "Evidence as JSON",
      "href": "/evidence.json",
      "note": "This page's data, machine-readable"
    }
  ]
}