Technical companion to the demo

How the Showcase works

One synthetic travel agency, 23 scenarios. Each section below has the CDL and binding with the declared fields marked, what the agency's API returned against what the model received, the raw MCP exchange, the denial payload where Archstone refused, and a link to the test that proves it. The plain-language version is the demo; this page is English only. Romanian readers: the cards on /ro/demo link to the anchors here.

Everything is invented. The agency, its hotels, guests, passports and payments do not exist, and its backend over-exposes on purpose. Archstone does not make a real backend safe; it stops fields the manifest does not name from being forwarded and refuses calls the manifest refuses.

Where each thing comes from. Manifests, bindings, transcripts and the SDK script are copied verbatim from NousVigil/archstone@6ff4bac (pinned in showcase.lock, core workspace version 0.33.0); a check fails if any byte differs from upstream. The exchanges with the Worker were captured on 2026-10-10 from https://demo.archstone.dev (the Worker runs on the wall clock, so quote and booking ids and timestamps differ from run to run). The CLI outputs marked "published" come from @archstone/cli 0.33.0, the latest on npm when captured, and are re-captured on every change and weekly.

Reading, projecting and checking origins

Live on the demo WorkerN-01

S-01A collection result, without the margin

wanderlust.search is an effect: read capability. Its binding maps six fields of each stay (id, name, location, pricePerNight, rating, petPolicy) plus totalMatches. The search also takes an optional budget and preferences (pets, breakfast, family; cat and dog fold into pets), which a traveller with a cat would use. The card's fixed call sends neither, so it lists all four Lisbon stays and each one's pet policy; nothing is filtered on the page. The synthetic agency answers with more: its rate sheet (net, margin, commission), a raw HTML description and a host contact with a phone number. None of it is named in the manifest, so none of it is in the tool result. The order is the agency's own; Archstone ranks nothing.

Declared: CDL and binding

wanderlust.search.capability.yaml
capability:
  id: wanderlust.search
  description: "Find places to stay matching a traveler's destination, dates, party and budget. Results come in the agency's own order; nothing here ranks them."
  effect: read

  input:
    destination:
      type: location
      description: "The city name, e.g. Lisbon or Porto, optionally followed by its country after a comma; or the exact name of one stay, e.g. Casa Alfama or Pensão Azul, to look at that stay only and get its id. Use this whenever the user names a stay; do not ask for a city first."
    dates:
      type: date-range
      description: "Check-in and check-out as ISO dates (YYYY-MM-DD), 1 to 30 nights. Work out calendar dates yourself; prose such as 'next weekend' is refused."
    travelers:
      type: party
    budget:
      type: money
      required: false
      description: "Maximum nightly rate as an object {amount, currency}, currency always \"EUR\" (a per-night ceiling, not a total for the stay). Not a bare number."
    preferences:
      type: preference-set
      required: false
      description: "Optional tags, all of which a stay must satisfy: pets (also pet-friendly, cat, dog), breakfast, family. Other tags are ignored."

  output:
    stays:
      collection: Stay
    totalMatches:
      type: quantity
      description: "How many stays the agency found."

  provider: wanderlust-api
bindings/wanderlust.search.binding.yaml
# Implementation binding - how wanderlust.search reaches the synthetic agency. NOT CDL.
binding:
  capabilityId: wanderlust.search
  connector:
    type: rest
    rest:
      baseUrl: "${SHOWCASE_API_URL}"
      method: POST
      path: /v1/stays/search

  response:
    collection: "$.stays[*]"
    resource: Stay
    map:
      id: "$.id"
      name: "$.name"
      location: "$.location"
      pricePerNight: "$.pricePerNight"
      rating: "$.rating"
      petPolicy: "$.petPolicy"

  extract:
    totalMatches: "$.totalMatches"

  # What the synthetic API last returned for the fixture's request (recorded: paths and types, never values).
  # The shape includes fields this manifest never maps (net, margin, guests, ...): drift in them is still
  # detected, and they are still never forwarded to a model.
  # contract: the recorded shape of everything the backend answered (18 paths, most of them never mapped above) is in the file
wanderlust.Stay.resource.yaml
wanderlust.Stay.resource.yaml
resource:
  name: wanderlust.Stay
  description: "A place to stay in a search result. Only what is declared here reaches a model: the agency's rate sheet and markup description are not named, so they are not shown."
  fields:
    id:
      type: identifier
      description: "The agency's id for the stay; pass it to the other stay tools."
    name:
      type: text
      description: "The property's display name."
    location:
      type: location
      description: "Where the stay is."
    pricePerNight:
      type: quantity
      description: "Nightly rate in euro."
    petPolicy:
      type: text
      required: false
      description: "The stay's pet policy, e.g. cats welcome with a nightly fee, or no pets."
    rating:
      type: quantity
      required: false
      description: "Guest review score, when available."

What the backend returned, and what the model received

on a path the manifest declaresreturned by the backend, not declared: never forwarded

Backend answer, in full (POST /v1/stays/search)
{
  "stays": [
    {
      "id": "ws-1001",
      "name": "Casa Alfama",
      "location": "Lisbon",
      "pricePerNight": 118,
      "rating": 4.6,
      "petPolicy": "Cats and small dogs welcome, EUR 10 per night",
      "currency": "EUR",
      "net": 73.16,
      "margin": 44.84,
      "commission": 14.16,
      "description_html": "<p><b>Casa Alfama</b> is a fictional demo property.</p><img src=\"https://deals.unknown-host.example/pixel/ws-1001.gif\" width=\"1\" height=\"1\"><a href=\"https://deals.unknown-host.example/offer?ref=ws-1001\">See today's special offer</a>",
      "hostContact": {
        "name": "Front Desk (fictional)",
        "phone": "+00 000 000 000"
      }
    },
    {
      "id": "ws-1003",
      "name": "Miradouro Court",
      "location": "Lisbon",
      "pricePerNight": 139,
      "rating": 4.8,
      "petPolicy": "No pets",
      "currency": "EUR",
      "net": 86.18,
      "margin": 52.82,
      "commission": 16.68,
      "description_html": "<p><b>Miradouro Court</b> is a fictional demo property.</p><img src=\"https://deals.unknown-host.example/pixel/ws-1003.gif\" width=\"1\" height=\"1\"><a href=\"https://deals.unknown-host.example/offer?ref=ws-1003\">See today's special offer</a>",
      "hostContact": {
        "name": "Front Desk (fictional)",
        "phone": "+00 000 000 000"
      }
    },
    {
      "id": "ws-1002",
      "name": "Pensão Azul",
      "location": "Lisbon",
      "pricePerNight": 74,
      "rating": 4.1,
      "petPolicy": "Pets welcome, free of charge",
      "currency": "EUR",
      "net": 45.88,
      "margin": 28.12,
      "commission": 8.88,
      "description_html": "<p><b>Pensão Azul</b> is a fictional demo property.</p><img src=\"https://deals.unknown-host.example/pixel/ws-1002.gif\" width=\"1\" height=\"1\"><a href=\"https://deals.unknown-host.example/offer?ref=ws-1002\">See today's special offer</a>",
      "hostContact": {
        "name": "Front Desk (fictional)",
        "phone": "+00 000 000 000"
      }
    },
    {
      "id": "ws-1004",
      "name": "Rio Tejo Lofts",
      "location": "Lisbon",
      "pricePerNight": 96,
      "rating": 4.3,
      "petPolicy": "No pets",
      "currency": "EUR",
      "net": 59.52,
      "margin": 36.48,
      "commission": 11.52,
      "description_html": "<p><b>Rio Tejo Lofts</b> is a fictional demo property.</p><img src=\"https://deals.unknown-host.example/pixel/ws-1004.gif\" width=\"1\" height=\"1\"><a href=\"https://deals.unknown-host.example/offer?ref=ws-1004\">See today's special offer</a>",
      "hostContact": {
        "name": "Front Desk (fictional)",
        "phone": "+00 000 000 000"
      }
    }
  ],
  "totalMatches": 4
}
What the model received (structuredContent)
{
  "stays": [
    {
      "id": "ws-1001",
      "name": "Casa Alfama",
      "location": "Lisbon",
      "pricePerNight": 118,
      "rating": 4.6,
      "petPolicy": "Cats and small dogs welcome, EUR 10 per night"
    },
    {
      "id": "ws-1003",
      "name": "Miradouro Court",
      "location": "Lisbon",
      "pricePerNight": 139,
      "rating": 4.8,
      "petPolicy": "No pets"
    },
    {
      "id": "ws-1002",
      "name": "Pensão Azul",
      "location": "Lisbon",
      "pricePerNight": 74,
      "rating": 4.1,
      "petPolicy": "Pets welcome, free of charge"
    },
    {
      "id": "ws-1004",
      "name": "Rio Tejo Lofts",
      "location": "Lisbon",
      "pricePerNight": 96,
      "rating": 4.3,
      "petPolicy": "No pets"
    }
  ],
  "totalMatches": 4
}

53 values in the backend's answer; 25 on declared paths; 28 never forwarded (currency, net, margin, commission, description_html, hostContact, name, phone).

The call for S-01
Request: wanderlust_search
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_search","arguments":{"destination":"Lisbon","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2},"budget":{"amount":150,"currency":"EUR"}}}}'
Response (raw JSON-RPC)
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"stays\": [\n    {\n      \"id\": \"ws-1001\",\n      \"name\": \"Casa Alfama\",\n      \"location\": \"Lisbon\",\n      \"pricePerNight\": 118,\n      \"rating\": 4.6,\n      \"petPolicy\": \"Cats and small dogs welcome, EUR 10 per night\"\n    },\n    {\n      \"id\": \"ws-1003\",\n      \"name\": \"Miradouro Court\",\n      \"location\": \"Lisbon\",\n      \"pricePerNight\": 139,\n      \"rating\": 4.8,\n      \"petPolicy\": \"No pets\"\n    },\n    {\n      \"id\": \"ws-1002\",\n      \"name\": \"Pensão Azul\",\n      \"location\": \"Lisbon\",\n      \"pricePerNight\": 74,\n      \"rating\": 4.1,\n      \"petPolicy\": \"Pets welcome, free of charge\"\n    },\n    {\n      \"id\": \"ws-1004\",\n      \"name\": \"Rio Tejo Lofts\",\n      \"location\": \"Lisbon\",\n      \"pricePerNight\": 96,\n      \"rating\": 4.3,\n      \"petPolicy\": \"No pets\"\n    }\n  ],\n  \"totalMatches\": 4\n}"
      }
    ],
    "structuredContent": {
      "stays": [
        {
          "id": "ws-1001",
          "name": "Casa Alfama",
          "location": "Lisbon",
          "pricePerNight": 118,
          "rating": 4.6,
          "petPolicy": "Cats and small dogs welcome, EUR 10 per night"
        },
        {
          "id": "ws-1003",
          "name": "Miradouro Court",
          "location": "Lisbon",
          "pricePerNight": 139,
          "rating": 4.8,
          "petPolicy": "No pets"
        },
        {
          "id": "ws-1002",
          "name": "Pensão Azul",
          "location": "Lisbon",
          "pricePerNight": 74,
          "rating": 4.1,
          "petPolicy": "Pets welcome, free of charge"
        },
        {
          "id": "ws-1004",
          "name": "Rio Tejo Lofts",
          "location": "Lisbon",
          "pricePerNight": 96,
          "rating": 4.3,
          "petPolicy": "No pets"
        }
      ],
      "totalMatches": 4
    },
    "isError": false
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 1 · credential: none

What Archstone kept outThe result contains none of net, margin, commission, description_html or hostContact, at any depth, and the stays keep the backend's order (N-01).

Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.

The test behind this

Live on the demo WorkerN-02

S-02A nested projection, four levels deep

wanderlust.stay-details returns a stay with its rooms and each room's amenities. The resources StayDetails → Room → Amenity declare the fields at each level, and the projection follows them down. The backend repeats guests[] (name, passport, phone, email) on the stay, on every room and on every amenity, plus an amenity history and lastUsedBy, and a raw description_html. A field that is not declared at its own level is dropped at that level (ADR-0008: undeclared provider data never reaches a model; there is no trusted-provider passthrough).

Declared: CDL and binding

wanderlust.stay-details.capability.yaml
capability:
  id: wanderlust.stay-details
  description: "Tell me more about one stay: its rooms and what each room offers."
  effect: read

  input:
    stayId:
      type: identifier
      description: "The stay's id: the `id` field of a row returned by wanderlust_search (it looks like `ws-1001`), never the stay's name. To find a stay the user names, first call wanderlust_search with that name as `destination`."

  output:
    stay:
      type: StayDetails

  provider: wanderlust-api
bindings/wanderlust.stay-details.binding.yaml
# Implementation binding - how wanderlust.stay-details reaches the synthetic agency. NOT CDL.
binding:
  capabilityId: wanderlust.stay-details
  connector:
    type: rest
    rest:
      baseUrl: "${SHOWCASE_API_URL}"
      method: GET
      path: /v1/stays/{stayId}

  response:
    resource: StayDetails
    map:
      name: "$.name"
      location: "$.location"
      pricePerNight: "$.pricePerNight"
      rating: "$.rating"
      petPolicy: "$.petPolicy"
      rooms: "$.rooms"

  # What the synthetic API last returned for the fixture's request (recorded: paths and types, never values).
  # The shape includes fields this manifest never maps (net, margin, guests, ...): drift in them is still
  # detected, and they are still never forwarded to a model.
  # contract: the recorded shape of everything the backend answered (53 paths, most of them never mapped above) is in the file
wanderlust.StayDetails.resource.yaml
wanderlust.StayDetails.resource.yaml
resource:
  name: wanderlust.StayDetails
  description: "One stay in detail, with its rooms and their amenities (nested). The nested values carry only the fields declared on Room and Amenity."
  fields:
    name:
      type: text
      description: "The property's display name."
    location:
      type: location
      description: "Where the stay is."
    pricePerNight:
      type: quantity
      description: "Nightly rate from, in euro."
    rating:
      type: quantity
      required: false
      description: "Guest review score, when available."
    petPolicy:
      type: text
      required: false
      description: "The stay's pet policy, e.g. cats welcome with a nightly fee, or no pets."
    rooms:
      collection: Room
      description: "The room types on offer."
wanderlust.Room.resource.yaml
wanderlust.Room.resource.yaml
resource:
  name: wanderlust.Room
  description: "A kind of room a stay offers."
  fields:
    name:
      type: text
      description: "Room type."
    sleeps:
      type: quantity
      description: "How many people it sleeps."
    pricePerNight:
      type: quantity
      description: "Nightly rate for this room in euro."
    amenities:
      collection: Amenity
      description: "What the room offers."
wanderlust.Amenity.resource.yaml
wanderlust.Amenity.resource.yaml
resource:
  name: wanderlust.Amenity
  description: "Something a room offers, with its fee if it has one."
  fields:
    name:
      type: text
      description: "What the amenity is."
    fee:
      type: quantity
      required: false
      description: "Fee in euro; absent when it is free."

What the backend returned, and what the model received

on a path the manifest declaresreturned by the backend, not declared: never forwarded

Backend answer, in full (GET /v1/stays/ws-1001)
{
  "id": "ws-1001",
  "name": "Casa Alfama",
  "location": "Lisbon",
  "pricePerNight": 118,
  "rating": 4.6,
  "petPolicy": "Cats and small dogs welcome, EUR 10 per night",
  "currency": "EUR",
  "net": 73.16,
  "margin": 44.84,
  "commission": 14.16,
  "description_html": "<p><b>Casa Alfama</b> is a fictional demo property.</p><img src=\"https://deals.unknown-host.example/pixel/ws-1001.gif\" width=\"1\" height=\"1\"><a href=\"https://deals.unknown-host.example/offer?ref=ws-1001\">See today's special offer</a>",
  "guests": [
    {
      "name": "Ana Pop",
      "passport": "DEMO-PASS-630573",
      "phone": "+00 000 000 573",
      "email": "ana.pop@guest.example"
    },
    {
      "name": "Rita Sample",
      "passport": "DEMO-PASS-784227",
      "phone": "+00 000 000 227",
      "email": "rita.sample@guest.example"
    }
  ],
  "rooms": [
    {
      "id": "ws-1001-dbl",
      "name": "Double Room",
      "sleeps": 2,
      "pricePerNight": 118,
      "net": 73.16,
      "margin": 44.84,
      "commission": 14.16,
      "guests": [
        {
          "name": "Maria Demo",
          "passport": "DEMO-PASS-450207",
          "phone": "+00 000 000 207",
          "email": "maria.demo@guest.example"
        }
      ],
      "amenities": [
        {
          "name": "Breakfast",
          "fee": 9,
          "host": {
            "name": "Amenity host (fictional)",
            "phone": "+00 000 000 111"
          },
          "lastUsedBy": {
            "name": "Ion Exemplu",
            "passport": "DEMO-PASS-231710",
            "phone": "+00 000 000 710",
            "email": "ion.exemplu@guest.example"
          },
          "history": [
            {
              "guests": [
                {
                  "name": "Paul Inventat",
                  "passport": "DEMO-PASS-325674",
                  "phone": "+00 000 000 674",
                  "email": "paul.inventat@guest.example"
                }
              ]
            }
          ]
        },
        {
          "name": "Airport transfer",
          "fee": 25,
          "host": {
            "name": "Amenity host (fictional)",
            "phone": "+00 000 000 111"
          },
          "lastUsedBy": {
            "name": "Maria Demo",
            "passport": "DEMO-PASS-120797",
            "phone": "+00 000 000 797",
            "email": "maria.demo@guest.example"
          },
          "history": [
            {
              "guests": [
                {
                  "name": "Rita Sample",
                  "passport": "DEMO-PASS-668361",
                  "phone": "+00 000 000 361",
                  "email": "rita.sample@guest.example"
                }
              ]
            }
          ]
        }
      ]
    },
    {
      "id": "ws-1001-fam",
      "name": "Family Room",
      "sleeps": 4,
      "pricePerNight": 171.1,
      "net": 106.08,
      "margin": 65.02,
      "commission": 20.53,
      "guests": [
        {
          "name": "Rita Sample",
          "passport": "DEMO-PASS-452099",
          "phone": "+00 000 000 099",
          "email": "rita.sample@guest.example"
        }
      ],
      "amenities": [
        {
          "name": "Breakfast",
          "fee": 9,
          "host": {
            "name": "Amenity host (fictional)",
            "phone": "+00 000 000 111"
          },
          "lastUsedBy": {
            "name": "Paul Inventat",
            "passport": "DEMO-PASS-358914",
            "phone": "+00 000 000 914",
            "email": "paul.inventat@guest.example"
          },
          "history": [
            {
              "guests": [
                {
                  "name": "Ion Exemplu",
                  "passport": "DEMO-PASS-917614",
                  "phone": "+00 000 000 614",
                  "email": "ion.exemplu@guest.example"
                }
              ]
            }
          ]
        },
        {
          "name": "Airport transfer",
          "fee": 25,
          "host": {
            "name": "Amenity host (fictional)",
            "phone": "+00 000 000 111"
          },
          "lastUsedBy": {
            "name": "Rita Sample",
            "passport": "DEMO-PASS-971865",
            "phone": "+00 000 000 865",
            "email": "rita.sample@guest.example"
          },
          "history": [
            {
              "guests": [
                {
                  "name": "Maria Demo",
                  "passport": "DEMO-PASS-915237",
                  "phone": "+00 000 000 237",
                  "email": "maria.demo@guest.example"
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}
What the model received (structuredContent)
{
  "stay": {
    "name": "Casa Alfama",
    "location": "Lisbon",
    "pricePerNight": 118,
    "rating": 4.6,
    "petPolicy": "Cats and small dogs welcome, EUR 10 per night",
    "rooms": [
      {
        "name": "Double Room",
        "sleeps": 2,
        "pricePerNight": 118,
        "amenities": [
          {
            "name": "Breakfast",
            "fee": 9
          },
          {
            "name": "Airport transfer",
            "fee": 25
          }
        ]
      },
      {
        "name": "Family Room",
        "sleeps": 4,
        "pricePerNight": 171.1,
        "amenities": [
          {
            "name": "Breakfast",
            "fee": 9
          },
          {
            "name": "Airport transfer",
            "fee": 25
          }
        ]
      }
    ]
  }
}

89 values in the backend's answer; 19 on declared paths; 70 never forwarded (id, currency, net, margin, commission, description_html, guests, name, passport, phone, email, host, lastUsedBy, history).

The call for S-02
Request: wanderlust_stay-details
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_stay-details","arguments":{"stayId":"ws-1001"}}}'
Response (raw JSON-RPC)
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"stay\": {\n    \"name\": \"Casa Alfama\",\n    \"location\": \"Lisbon\",\n    \"pricePerNight\": 118,\n    \"rating\": 4.6,\n    \"petPolicy\": \"Cats and small dogs welcome, EUR 10 per night\",\n    \"rooms\": [\n      {\n        \"name\": \"Double Room\",\n        \"sleeps\": 2,\n        \"pricePerNight\": 118,\n        \"amenities\": [\n          {\n            \"name\": \"Breakfast\",\n            \"fee\": 9\n          },\n          {\n            \"name\": \"Airport transfer\",\n            \"fee\": 25\n          }\n        ]\n      },\n      {\n        \"name\": \"Family Room\",\n        \"sleeps\": 4,\n        \"pricePerNight\": 171.1,\n        \"amenities\": [\n          {\n            \"name\": \"Breakfast\",\n            \"fee\": 9\n          },\n          {\n            \"name\": \"Airport transfer\",\n            \"fee\": 25\n          }\n        ]\n      }\n    ]\n  }\n}"
      }
    ],
    "structuredContent": {
      "stay": {
        "name": "Casa Alfama",
        "location": "Lisbon",
        "pricePerNight": 118,
        "rating": 4.6,
        "petPolicy": "Cats and small dogs welcome, EUR 10 per night",
        "rooms": [
          {
            "name": "Double Room",
            "sleeps": 2,
            "pricePerNight": 118,
            "amenities": [
              {
                "name": "Breakfast",
                "fee": 9
              },
              {
                "name": "Airport transfer",
                "fee": 25
              }
            ]
          },
          {
            "name": "Family Room",
            "sleeps": 4,
            "pricePerNight": 171.1,
            "amenities": [
              {
                "name": "Breakfast",
                "fee": 9
              },
              {
                "name": "Airport transfer",
                "fee": 25
              }
            ]
          }
        ]
      }
    },
    "isError": false
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 1 · credential: none

Checked on the captured tools/list: the advertised outputSchema of wanderlust_stay-details names none of margin, passport, phone, description_html, net, commission, guests, email.

What Archstone kept outNo passport, phone, email, guest list, margin or raw HTML in the result, neither as a field name nor as a value, at any depth (N-02). The advertised outputSchema does not name them either; the page checks that on the captured tools/list.

Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.

The test behind this

Live on the demo WorkerN-03

S-03A photo on an undeclared host is withheld by position

wanderlust.stay-photos outputs a list of image values (semantic type image, Experimental, core #152). The binding declares origins.images; a URL on any other origin is not forwarded. The agency returns five photos and the fourth is on partner-photos.example. The other four are returned. The runtime never fetches any photo URL: the only backend request is the GET for the list (see x-showcase-backend-calls below).

Declared: CDL and binding

wanderlust.stay-photos.capability.yaml
capability:
  id: wanderlust.stay-photos
  description: "Photos of one stay, from the agency's own image host."
  effect: read

  input:
    stayId:
      type: identifier
      description: "The stay's id: the `id` field of a row returned by wanderlust_search (it looks like `ws-1001`), never the stay's name. To find a stay the user names, first call wanderlust_search with that name as `destination`."

  output:
    gallery:
      type: Gallery

  provider: wanderlust-api
bindings/wanderlust.stay-photos.binding.yaml
# Implementation binding - how wanderlust.stay-photos reaches the synthetic agency. NOT CDL.
binding:
  capabilityId: wanderlust.stay-photos
  connector:
    type: rest
    rest:
      baseUrl: "${SHOWCASE_API_URL}"
      method: GET
      path: /v1/stays/{stayId}/photos

  response:
    resource: Gallery
    map:
      photos: "$.photos"

  origins:
    images:
      - "https://demo.archstone.dev"
wanderlust.Gallery.resource.yaml
wanderlust.Gallery.resource.yaml
resource:
  name: wanderlust.Gallery
  description: "Photos of one stay. Each photo must live on the agency's own image host; one that does not is left out, the rest remain."
  fields:
    photos:
      list: image
      description: "Photo URLs, to be shown and never fetched by Archstone."

What the backend returned, and what the model received

on a path the manifest declaresreturned by the backend, not declared: never forwardeddeclared, but outside the declared origins: withheld

Backend answer, in full (GET /v1/stays/ws-1001/photos)
{
  "stayId": "ws-1001",
  "photos": [
    "https://demo.archstone.dev/img/ws-1001/1.svg",
    "https://demo.archstone.dev/img/ws-1001/2.svg",
    "https://demo.archstone.dev/img/ws-1001/3.svg",
    "https://cdn.partner-photos.example/ws-1001/4.jpg",
    "https://demo.archstone.dev/img/ws-1001/5.svg"
  ]
}
What the model received (structuredContent)
{
  "gallery": {
    "photos": [
      "https://demo.archstone.dev/img/ws-1001/1.svg",
      "https://demo.archstone.dev/img/ws-1001/2.svg",
      "https://demo.archstone.dev/img/ws-1001/3.svg",
      "https://demo.archstone.dev/img/ws-1001/5.svg"
    ]
  }
}

6 values in the backend's answer; 4 on declared paths, 1 of them withheld by the origin check; 1 never forwarded (stayId).

The result's text content also carries note: withheld — value(s) outside the declared origins, at these places in this result (a number is the item's position in the provider's list, before any removal): gallery.photos[3] (removed; the list now has 4 items). Every other link and image returned passed the origin check.

The call for S-03
Request: wanderlust_stay-photos
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_stay-photos","arguments":{"stayId":"ws-1001"}}}'
Response (raw JSON-RPC)
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"gallery\": {\n    \"photos\": [\n      \"https://demo.archstone.dev/img/ws-1001/1.svg\",\n      \"https://demo.archstone.dev/img/ws-1001/2.svg\",\n      \"https://demo.archstone.dev/img/ws-1001/3.svg\",\n      \"https://demo.archstone.dev/img/ws-1001/5.svg\"\n    ]\n  }\n}"
      },
      {
        "type": "text",
        "text": "note: withheld — value(s) outside the declared origins, at these places in this result (a number is the item's position in the provider's list, before any removal): gallery.photos[3] (removed; the list now has 4 items). Every other link and image returned passed the origin check."
      }
    ],
    "structuredContent": {
      "gallery": {
        "photos": [
          "https://demo.archstone.dev/img/ws-1001/1.svg",
          "https://demo.archstone.dev/img/ws-1001/2.svg",
          "https://demo.archstone.dev/img/ws-1001/3.svg",
          "https://demo.archstone.dev/img/ws-1001/5.svg"
        ]
      }
    },
    "isError": false
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 1 · credential: none

What Archstone kept outThe off-origin photo is dropped by position, never named by URL, and the note says where: gallery.photos[3] (removed; the list now has 4 items) (the number is the photo's position in the agency's list, before the removal), ending "Every other link and image returned passed the origin check." Over MCP the withheld path reaches the client as a text note only; the structured outcome and the withheld list exist on the embedded SDK's execute() result (status: "degraded", withheld: ["photos[3]"], asserted by the test below). Putting them in _meta for MCP successes is tracked in NousVigil/archstone#174.

Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.

The test behind this

Live on the demo WorkerN-04

S-04A page link on an undeclared origin is withheld, the row stays

wanderlust.stay-page outputs web-page values checked against origins.pages. The agency returns two hotel pages: the hotel's own page, on the origin the binding declares, and a partner listing whose URL is on partner-hotels.example. The official page is returned; the partner row survives without its url, and nothing is opened.

Declared: CDL and binding

wanderlust.stay-page.capability.yaml
capability:
  id: wanderlust.stay-page
  description: "The pages where a person can see a stay. The agency's own page is returned with its link; a partner listing on another site is still listed by name, but its link is withheld, because Archstone only passes on links on the origins the manifest declares."
  effect: read

  input:
    stayId:
      type: identifier
      description: "The stay's id: the `id` field of a row returned by wanderlust_search (it looks like `ws-1001`), never the stay's name. To find a stay the user names, first call wanderlust_search with that name as `destination`."

  output:
    pages:
      collection: HotelPage

  provider: wanderlust-api
bindings/wanderlust.stay-page.binding.yaml
# Implementation binding - how wanderlust.stay-page reaches the synthetic agency. NOT CDL.
binding:
  capabilityId: wanderlust.stay-page
  connector:
    type: rest
    rest:
      baseUrl: "${SHOWCASE_API_URL}"
      method: GET
      path: /v1/stays/{stayId}/pages

  response:
    collection: "$.pages[*]"
    resource: HotelPage
    map:
      name: "$.name"
      url: "$.url"

  origins:
    pages:
      - "https://www.wanderlust-agency.example"
wanderlust.HotelPage.resource.yaml
wanderlust.HotelPage.resource.yaml
resource:
  name: wanderlust.HotelPage
  description: "A page where a person can see a property on the agency's own site."
  fields:
    name:
      type: text
      description: "Which property the page is for."
    url:
      type: web-page
      required: false
      description: "The page, when it lives on the agency's own site. Absent for a partner listing on another site: the property is still listed, only its link is withheld."

What the backend returned, and what the model received

on a path the manifest declaresreturned by the backend, not declared: never forwardeddeclared, but outside the declared origins: withheld

Backend answer, in full (GET /v1/stays/ws-1001/pages)
{
  "pages": [
    {
      "name": "Casa Alfama",
      "url": "https://www.wanderlust-agency.example/hotels/casa-alfama"
    },
    {
      "name": "Casa Alfama Annex (partner listing)",
      "url": "https://book.partner-hotels.example/listing/casa-alfama-annex"
    }
  ],
  "affiliateTracking": "https://book.partner-hotels.example/aff?ref=ws-1001"
}
What the model received (structuredContent)
{
  "pages": [
    {
      "name": "Casa Alfama",
      "url": "https://www.wanderlust-agency.example/hotels/casa-alfama"
    },
    {
      "name": "Casa Alfama Annex (partner listing)"
    }
  ]
}

5 values in the backend's answer; 3 on declared paths, 1 of them withheld by the origin check; 1 never forwarded (affiliateTracking).

The result's text content also carries note: withheld — value(s) outside the declared origins, at these places in this result (a number is the item's position in the provider's list, before any removal): pages[1].url (field omitted). Every other link and image returned passed the origin check.

The call for S-04
Request: wanderlust_stay-page
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_stay-page","arguments":{"stayId":"ws-1001"}}}'
Response (raw JSON-RPC)
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"pages\": [\n    {\n      \"name\": \"Casa Alfama\",\n      \"url\": \"https://www.wanderlust-agency.example/hotels/casa-alfama\"\n    },\n    {\n      \"name\": \"Casa Alfama Annex (partner listing)\"\n    }\n  ]\n}"
      },
      {
        "type": "text",
        "text": "note: withheld — value(s) outside the declared origins, at these places in this result (a number is the item's position in the provider's list, before any removal): pages[1].url (field omitted). Every other link and image returned passed the origin check."
      }
    ],
    "structuredContent": {
      "pages": [
        {
          "name": "Casa Alfama",
          "url": "https://www.wanderlust-agency.example/hotels/casa-alfama"
        },
        {
          "name": "Casa Alfama Annex (partner listing)"
        }
      ]
    },
    "isError": false
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 1 · credential: none

What Archstone kept outOnly the partner link is withheld. The note names it, pages[1].url (field omitted) (the second entry of the agency's list), and its last sentence says every other link and image passed the origin check, so the official page is fine. The row and its name remain, and no request leaves for any page host (N-04). As in S-03, the MCP result carries the withheld path as a text note; the withheld: ["url"] list is on the embedded SDK result (#174).

Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.

The test behind this

Writes, policy and irreversible actions

Live on the demo WorkerN-05

S-05A quote that expires and books nothing

wanderlust.quote is declared effect: write: it asks the agency for a price that expires. The agency is stateless: a quote id carries the instant it was issued and a hash of its inputs, and the quote is valid for 15 minutes from that instant (expiresAt), not until a fixed boundary; book refuses it afterwards, and nothing is stored. An optional pets count itemises the stay's pet fee in petFee and in total. Archstone lowers the effect to an MCP annotation, destructiveHint: false without readOnlyHint: a hint a client may use, nothing Archstone enforces.

Declared: CDL and binding

wanderlust.quote.capability.yaml
capability:
  id: wanderlust.quote
  description: "Price a stay for given dates and party, including its pet fee when pets travel. The quote is valid for 15 minutes from when it is issued; it books nothing and charges nothing. A stay that does not accept pets refuses a quote for a party with pets."
  effect: write

  input:
    stayId:
      type: identifier
      description: "The stay's id: the `id` field of a row returned by wanderlust_search (it looks like `ws-1001`), never the stay's name. To find a stay the user names, first call wanderlust_search with that name as `destination`."
    dates:
      type: date-range
      description: "Check-in and check-out as ISO dates (YYYY-MM-DD), 1 to 30 nights. Work out calendar dates yourself; prose such as 'next weekend' is refused."
    travelers:
      type: party
    pets:
      type: quantity
      required: false
      description: "How many pets travel (a whole number, 0 to 4). Leave it out for none. The quote then itemises the stay's pet fee in its total; a stay with no pets policy refuses it. See the stay's petPolicy."

  output:
    quote:
      type: Quote

  failures:
    pets-not-allowed: The stay does not accept pets; quote without pets or choose another stay.

  provider: wanderlust-api
bindings/wanderlust.quote.binding.yaml
# Implementation binding - how wanderlust.quote reaches the synthetic agency. NOT CDL.
binding:
  capabilityId: wanderlust.quote
  connector:
    type: rest
    rest:
      baseUrl: "${SHOWCASE_API_URL}"
      method: POST
      path: /v1/quotes

  response:
    resource: Quote
    map:
      quoteId: "$.quoteId"
      stayId: "$.stayId"
      dates: "$.dates"
      nights: "$.nights"
      total: "$.total"
      pets: "$.pets"
      petFee: "$.petFee"
      expiresAt: "$.expiresAt"
wanderlust.Quote.resource.yaml
wanderlust.Quote.resource.yaml
resource:
  name: wanderlust.Quote
  description: "A price the agency will honour until it expires. A quote books nothing."
  fields:
    quoteId:
      type: identifier
      description: "Pass this to wanderlust.book before it expires."
    stayId:
      type: identifier
      description: "The quoted stay."
    dates:
      type: date-range
      description: "The quoted stay window."
    nights:
      type: quantity
      description: "Number of nights."
    total:
      type: money
      description: "Total price for the stay, including the pet fee when pets were declared."
    pets:
      type: quantity
      description: "How many pets the quote was priced for (0 when none were declared)."
    petFee:
      type: money
      description: "The pet fee already included in the total: 0 when no pets were declared or the stay takes pets free of charge."
    expiresAt:
      type: datetime
      description: "When the quote stops being valid: 15 minutes after it was issued."

What the backend returned, and what the model received

on a path the manifest declaresreturned by the backend, not declared: never forwarded

Backend answer, in full (POST /v1/quotes)
{
  "quoteId": "Q-mv30gc4z-fe7c4e73",
  "stayId": "ws-1001",
  "dates": {
    "from": "2027-05-12",
    "to": "2027-05-15"
  },
  "nights": 3,
  "total": {
    "amount": 354,
    "currency": "EUR"
  },
  "pets": 0,
  "petFee": {
    "amount": 0,
    "currency": "EUR"
  },
  "expiresAt": "2026-10-10T23:28:43.283Z",
  "net": 219.48,
  "margin": 134.52,
  "commission": 42.48
}
What the model received (structuredContent)
{
  "quote": {
    "quoteId": "Q-mv30gb9l-fe7bd5f6",
    "stayId": "ws-1001",
    "dates": {
      "from": "2027-05-12",
      "to": "2027-05-15"
    },
    "nights": 3,
    "total": {
      "amount": 354,
      "currency": "EUR"
    },
    "pets": 0,
    "petFee": {
      "amount": 0,
      "currency": "EUR"
    },
    "expiresAt": "2026-10-10T23:28:42.153Z"
  }
}

14 values in the backend's answer; 11 on declared paths; 3 never forwarded (net, margin, commission).

The call for S-05
Request: wanderlust_quote
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_quote","arguments":{"stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2}}}}'
Response (raw JSON-RPC)
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"quote\": {\n    \"quoteId\": \"Q-mv30gb9l-fe7bd5f6\",\n    \"stayId\": \"ws-1001\",\n    \"dates\": {\n      \"from\": \"2027-05-12\",\n      \"to\": \"2027-05-15\"\n    },\n    \"nights\": 3,\n    \"total\": {\n      \"amount\": 354,\n      \"currency\": \"EUR\"\n    },\n    \"pets\": 0,\n    \"petFee\": {\n      \"amount\": 0,\n      \"currency\": \"EUR\"\n    },\n    \"expiresAt\": \"2026-10-10T23:28:42.153Z\"\n  }\n}"
      }
    ],
    "structuredContent": {
      "quote": {
        "quoteId": "Q-mv30gb9l-fe7bd5f6",
        "stayId": "ws-1001",
        "dates": {
          "from": "2027-05-12",
          "to": "2027-05-15"
        },
        "nights": 3,
        "total": {
          "amount": 354,
          "currency": "EUR"
        },
        "pets": 0,
        "petFee": {
          "amount": 0,
          "currency": "EUR"
        },
        "expiresAt": "2026-10-10T23:28:42.153Z"
      }
    },
    "isError": false
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 1 · credential: none

Advertised by tools/list: wanderlust_quote
{
  "name": "wanderlust_quote",
  "description": "Price a stay for given dates and party, including its pet fee when pets travel. The quote is valid for 15 minutes from when it is issued; it books nothing and charges nothing. A stay that does not accept pets refuses a quote for a party with pets.",
  "annotations": {
    "destructiveHint": false
  }
}
What Archstone kept outNo booking is made, however often it is asked: the quote result has exactly quoteId, stayId, dates, nights, total, pets, petFee, expiresAt, no booking id and no payment quote (N-05). Whether the AI asks you before calling a write tool is the client's decision.

Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.

The test behind this

Live on the demo WorkerN-06

S-06Archstone's policy refuses a missing or unknown key first; then the agency checks the key and the quote

wanderlust.book declares policies: [authenticated] and a principal policy, and its binding forwards Authorization: Bearer ${caller.accessToken}. With the public demo key A the booking goes through, at the total the quote carried (the quote is a setup step). Order of events for any other caller: Archstone's policy runs first. Without a credential it refuses the call itself, with the denial reason authenticated_no_credential, and a token whose principal is on no list is refused as principal_not_allowed (S-07); in both the agency receives nothing. Archstone never verifies a key itself; it matches the principal the host asserted. Only a call the policy lets through reaches the agency, and the agency then judges the key (401) and the quote (missing, expired or foreign: 422).

If you paste this into your own AI: Your own AI app connects without a key unless it lets you set an Authorization header, so this booking is refused for lack of a key, before the agency is asked. The page's "Run it live" button sends the published demo key instead, and the booking goes through.

Declared: CDL and binding

wanderlust.book.capability.yaml
capability:
  id: wanderlust.book
  description: "Book a quoted stay for a guest at the quoted total. Needs the agency key and a principal the manifest's policy allows: Archstone's policy refuses a missing key or a caller who is not allowed before the agency is asked; the agency then checks the key and the quote."
  effect: write

  input:
    quoteId:
      type: identifier
      description: "The quote id from wanderlust.quote, still valid."
    stayId:
      type: identifier
      description: "The stay's id: the `id` field of a row returned by wanderlust_search (it looks like `ws-1001`), never the stay's name. To find a stay the user names, first call wanderlust_search with that name as `destination`."
    dates:
      type: date-range
    travelers:
      type: party
    guestName:
      type: string
      description: "Who the booking is for."

  output:
    booking:
      type: Booking

  failures:
    quote-expired: The quote has expired; request a new one with wanderlust.quote.
    quote-mismatch: The quote was issued for a different stay, dates or party.

  policies:
    - authenticated
  provider: wanderlust-api
bindings/wanderlust.book.binding.yaml
# Implementation binding - how wanderlust.book reaches the synthetic agency. NOT CDL.
binding:
  capabilityId: wanderlust.book
  connector:
    type: rest
    rest:
      baseUrl: "${SHOWCASE_API_URL}"
      method: POST
      path: /v1/bookings
      headers:
        Authorization: "Bearer ${caller.accessToken}"

  response:
    resource: Booking
    map:
      bookingId: "$.bookingId"
      status: "$.status"
      stayId: "$.stayId"
      dates: "$.dates"
      total: "$.total"
      pets: "$.pets"
      petFee: "$.petFee"
      paymentQuote: "$.paymentQuote"
      payBy: "$.payBy"
wanderlust.book.policy.yaml
wanderlust.book.policy.yaml
# Who may book. Principals are asserted by whoever hosts the server; Archstone matches them as
# exact strings and verifies nothing about them. deny wins over allow.
apiVersion: archstone/v1
kind: Policy
metadata:
  id: wanderlust-book-principals
  name: Who may book
  scope: capability
  capabilityId: wanderlust.book
spec:
  allow:
    - "demo:visitor"
  deny:
    - "demo:blocked"
wanderlust.Booking.resource.yaml
wanderlust.Booking.resource.yaml
resource:
  name: wanderlust.Booking
  description: "A confirmed booking. It declares no guest field at all: nothing about who is staying is shown, even if the agency returns it."
  fields:
    bookingId:
      type: identifier
      description: "The booking reference."
    status:
      type: enum
      values: [confirmed, cancelled]
      description: "Current state."
    stayId:
      type: identifier
      description: "The booked stay."
    dates:
      type: date-range
      description: "The booked window."
    total:
      type: money
      description: "Total price, including the pet fee when pets travel."
    pets:
      type: quantity
      description: "How many pets the booking was priced for, as the quote fixed it (0 when none)."
    petFee:
      type: money
      description: "The pet fee already included in the total: 0 when no pets travel or the stay takes pets free of charge."
    paymentQuote:
      type: identifier
      description: "The agency's payment quote for this booking and total; pass it to wanderlust.pay."
    payBy:
      type: datetime
      description: "When the payment quote stops being valid."

What the backend returned, and what the model received

on a path the manifest declaresreturned by the backend, not declared: never forwarded

Backend answer, in full (POST /v1/bookings)
{
  "bookingId": "B-01626e4ae841",
  "status": "confirmed",
  "stayId": "ws-1001",
  "dates": {
    "from": "2027-05-12",
    "to": "2027-05-15"
  },
  "total": {
    "amount": 354,
    "currency": "EUR"
  },
  "pets": 0,
  "petFee": {
    "amount": 0,
    "currency": "EUR"
  },
  "paymentQuote": "PQ-mv30geqy-d709eecb",
  "payBy": "2026-10-10T23:28:46.666Z",
  "guests": [
    {
      "name": "Ana Pop",
      "passport": "DEMO-PASS-719064",
      "phone": "+00 000 000 064",
      "email": "ana.pop@guest.example"
    }
  ],
  "net": 219.48,
  "margin": 134.52,
  "commission": 42.48
}
What the model received (structuredContent)
{
  "booking": {
    "bookingId": "B-01626e4ae841",
    "status": "confirmed",
    "stayId": "ws-1001",
    "dates": {
      "from": "2027-05-12",
      "to": "2027-05-15"
    },
    "total": {
      "amount": 354,
      "currency": "EUR"
    },
    "pets": 0,
    "petFee": {
      "amount": 0,
      "currency": "EUR"
    },
    "paymentQuote": "PQ-mv30gdvl-d709764f",
    "payBy": "2026-10-10T23:28:45.537Z"
  }
}

19 values in the backend's answer; 12 on declared paths; 7 never forwarded (guests, name, passport, phone, email, net, margin, commission).

Setup step (wanderlust_quote)
Request: wanderlust_quote
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_quote","arguments":{"stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2}}}}'
Response (raw JSON-RPC)
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"quote\": {\n    \"quoteId\": \"Q-mv30gd07-fe7cc960\",\n    \"stayId\": \"ws-1001\",\n    \"dates\": {\n      \"from\": \"2027-05-12\",\n      \"to\": \"2027-05-15\"\n    },\n    \"nights\": 3,\n    \"total\": {\n      \"amount\": 354,\n      \"currency\": \"EUR\"\n    },\n    \"pets\": 0,\n    \"petFee\": {\n      \"amount\": 0,\n      \"currency\": \"EUR\"\n    },\n    \"expiresAt\": \"2026-10-10T23:28:44.407Z\"\n  }\n}"
      }
    ],
    "structuredContent": {
      "quote": {
        "quoteId": "Q-mv30gd07-fe7cc960",
        "stayId": "ws-1001",
        "dates": {
          "from": "2027-05-12",
          "to": "2027-05-15"
        },
        "nights": 3,
        "total": {
          "amount": 354,
          "currency": "EUR"
        },
        "pets": 0,
        "petFee": {
          "amount": 0,
          "currency": "EUR"
        },
        "expiresAt": "2026-10-10T23:28:44.407Z"
      }
    },
    "isError": false
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 1 · credential: none

The call for S-06
Request: wanderlust_book
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'authorization: Bearer demo-public-key-visitor-0000' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_book","arguments":{"quoteId":"Q-mv30gd07-fe7cc960","stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2},"guestName":"Ana Pop"}}}'
Response (raw JSON-RPC)
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"booking\": {\n    \"bookingId\": \"B-01626e4ae841\",\n    \"status\": \"confirmed\",\n    \"stayId\": \"ws-1001\",\n    \"dates\": {\n      \"from\": \"2027-05-12\",\n      \"to\": \"2027-05-15\"\n    },\n    \"total\": {\n      \"amount\": 354,\n      \"currency\": \"EUR\"\n    },\n    \"pets\": 0,\n    \"petFee\": {\n      \"amount\": 0,\n      \"currency\": \"EUR\"\n    },\n    \"paymentQuote\": \"PQ-mv30gdvl-d709764f\",\n    \"payBy\": \"2026-10-10T23:28:45.537Z\"\n  }\n}"
      }
    ],
    "structuredContent": {
      "booking": {
        "bookingId": "B-01626e4ae841",
        "status": "confirmed",
        "stayId": "ws-1001",
        "dates": {
          "from": "2027-05-12",
          "to": "2027-05-15"
        },
        "total": {
          "amount": 354,
          "currency": "EUR"
        },
        "pets": 0,
        "petFee": {
          "amount": 0,
          "currency": "EUR"
        },
        "paymentQuote": "PQ-mv30gdvl-d709764f",
        "payBy": "2026-10-10T23:28:45.537Z"
      }
    },
    "isError": false
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 1 · credential: public demo key A

What Archstone refused: the denial payload

The same call with no Authorization header
Request: wanderlust_book
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_book","arguments":{"quoteId":"Q-mv30gd07-fe7cc960","stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2},"guestName":"Ana Pop"}}}'
Response (raw JSON-RPC)
{
  "result": {
    "_meta": {
      "dev.archstone/policy_denied": {
        "error": "policy_denied",
        "capability": "wanderlust.book",
        "reason": "authenticated_no_credential"
      }
    },
    "content": [
      {
        "type": "text",
        "text": "capability 'wanderlust.book' requires policies:[authenticated] — no caller credential (accessToken) provided on invoke"
      }
    ],
    "isError": true
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 0 · credential: none

Reason code authenticated_no_credential under _meta["dev.archstone/policy_denied"]. The text content is the human-readable sentence; the client reads the reason from _meta.

What Archstone kept outThe denial carries only error, capability and reason, nothing about the policy itself, and x-showcase-backend-calls is 0: the booking request never left (N-06). The /demo page's "agency was asked" count includes the setup quote, so its S-06 run says 2, and S-07 says 1.

Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.

The test behind this

Live on the demo WorkerN-07

S-07A principal the manifest denies, on book, pay and cancel, and three ways to be refused

The policy documents attached to wanderlust.book, wanderlust.pay and wanderlust.cancel each allow demo:visitor and deny demo:blocked. The Worker maps the two public demo keys to those principals (it asserts the principal; Archstone treats it as an opaque string and verifies nothing). Key B is accepted by the agency and denied by the manifest, so the call is refused before any request is made, and it is refused the same way for paying and for cancelling.

If you paste this into your own AI: This scenario is about the second published demo key, which the rules name as blocked. Your own AI app connected without a key is refused for lack of a key, before the agency is asked. Connected with the first key, the booking goes through instead. Connected with the second key, it is refused exactly as here. "Run it live" sends the second key.

Declared: CDL and binding

wanderlust.book.policy.yaml
# Who may book. Principals are asserted by whoever hosts the server; Archstone matches them as
# exact strings and verifies nothing about them. deny wins over allow.
apiVersion: archstone/v1
kind: Policy
metadata:
  id: wanderlust-book-principals
  name: Who may book
  scope: capability
  capabilityId: wanderlust.book
spec:
  allow:
    - "demo:visitor"
  deny:
    - "demo:blocked"
wanderlust.pay.policy.yaml
wanderlust.pay.policy.yaml
# Who may pay. The same principals as booking: a caller the manifest denies or does not list is
# refused by Archstone before the agency is asked. deny wins over allow.
apiVersion: archstone/v1
kind: Policy
metadata:
  id: wanderlust-pay-principals
  name: Who may pay
  scope: capability
  capabilityId: wanderlust.pay
spec:
  allow:
    - "demo:visitor"
  deny:
    - "demo:blocked"
wanderlust.cancel.policy.yaml
wanderlust.cancel.policy.yaml
# Who may cancel. The same principals as booking: a caller the manifest denies or does not list is
# refused by Archstone before the agency is asked. deny wins over allow.
apiVersion: archstone/v1
kind: Policy
metadata:
  id: wanderlust-cancel-principals
  name: Who may cancel
  scope: capability
  capabilityId: wanderlust.cancel
spec:
  allow:
    - "demo:visitor"
  deny:
    - "demo:blocked"
wanderlust.book.capability.yaml
wanderlust.book.capability.yaml
capability:
  id: wanderlust.book
  description: "Book a quoted stay for a guest at the quoted total. Needs the agency key and a principal the manifest's policy allows: Archstone's policy refuses a missing key or a caller who is not allowed before the agency is asked; the agency then checks the key and the quote."
  effect: write

  input:
    quoteId:
      type: identifier
      description: "The quote id from wanderlust.quote, still valid."
    stayId:
      type: identifier
      description: "The stay's id: the `id` field of a row returned by wanderlust_search (it looks like `ws-1001`), never the stay's name. To find a stay the user names, first call wanderlust_search with that name as `destination`."
    dates:
      type: date-range
    travelers:
      type: party
    guestName:
      type: string
      description: "Who the booking is for."

  output:
    booking:
      type: Booking

  failures:
    quote-expired: The quote has expired; request a new one with wanderlust.quote.
    quote-mismatch: The quote was issued for a different stay, dates or party.

  policies:
    - authenticated
  provider: wanderlust-api
Setup step (wanderlust_quote)
Request: wanderlust_quote
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_quote","arguments":{"stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2}}}}'
Response (raw JSON-RPC)
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"quote\": {\n    \"quoteId\": \"Q-mv30gd07-fe7cc960\",\n    \"stayId\": \"ws-1001\",\n    \"dates\": {\n      \"from\": \"2027-05-12\",\n      \"to\": \"2027-05-15\"\n    },\n    \"nights\": 3,\n    \"total\": {\n      \"amount\": 354,\n      \"currency\": \"EUR\"\n    },\n    \"pets\": 0,\n    \"petFee\": {\n      \"amount\": 0,\n      \"currency\": \"EUR\"\n    },\n    \"expiresAt\": \"2026-10-10T23:28:44.407Z\"\n  }\n}"
      }
    ],
    "structuredContent": {
      "quote": {
        "quoteId": "Q-mv30gd07-fe7cc960",
        "stayId": "ws-1001",
        "dates": {
          "from": "2027-05-12",
          "to": "2027-05-15"
        },
        "nights": 3,
        "total": {
          "amount": 354,
          "currency": "EUR"
        },
        "pets": 0,
        "petFee": {
          "amount": 0,
          "currency": "EUR"
        },
        "expiresAt": "2026-10-10T23:28:44.407Z"
      }
    },
    "isError": false
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 1 · credential: none

The call for S-07
Request: wanderlust_book
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'authorization: Bearer demo-public-key-blocked-0000' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_book","arguments":{"quoteId":"Q-mv30gd07-fe7cc960","stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2},"guestName":"Ana Pop"}}}'
Response (raw JSON-RPC)
{
  "result": {
    "_meta": {
      "dev.archstone/policy_denied": {
        "error": "policy_denied",
        "capability": "wanderlust.book",
        "reason": "principal_denied"
      }
    },
    "content": [
      {
        "type": "text",
        "text": "capability 'wanderlust.book' is not permitted for this caller by policy."
      }
    ],
    "isError": true
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 0 · credential: public demo key B

What Archstone refused: the denial payload

A bearer token whose principal is on no list
Request: wanderlust_book
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'authorization: Bearer demo-public-key-unlisted-0000' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_book","arguments":{"quoteId":"Q-mv30gd07-fe7cc960","stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2},"guestName":"Ana Pop"}}}'
Response (raw JSON-RPC)
{
  "result": {
    "_meta": {
      "dev.archstone/policy_denied": {
        "error": "policy_denied",
        "capability": "wanderlust.book",
        "reason": "principal_not_allowed"
      }
    },
    "content": [
      {
        "type": "text",
        "text": "capability 'wanderlust.book' is not permitted for this caller by policy."
      }
    ],
    "isError": true
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 0 · credential: a bearer token that maps to no principal

Reason code principal_not_allowed under _meta["dev.archstone/policy_denied"]. The text content is the human-readable sentence; the client reads the reason from _meta.

A header that is present but is not a Bearer credential (fail closed)
Request: wanderlust_book
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'authorization: Basic dXNlcjpwYXNz' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_book","arguments":{"quoteId":"Q-mv30gd07-fe7cc960","stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2},"guestName":"Ana Pop"}}}'
Response (raw JSON-RPC)
{
  "result": {
    "_meta": {
      "dev.archstone/policy_denied": {
        "error": "policy_denied",
        "capability": "wanderlust.book",
        "reason": "policy_unevaluatable"
      }
    },
    "content": [
      {
        "type": "text",
        "text": "capability 'wanderlust.book' could not be evaluated — caller identity could not be established (resolveCaller failed) — refusing (fail-closed)."
      }
    ],
    "isError": true
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 0 · credential: a header that is not a Bearer credential

Reason code policy_unevaluatable under _meta["dev.archstone/policy_denied"]. The text content is the human-readable sentence; the client reads the reason from _meta.

What Archstone kept outThree distinct reasons: principal_denied (key B), principal_not_allowed (a credential with no listed principal) and policy_unevaluatable (the caller's identity could not be established, so the rule cannot be evaluated and the call is refused rather than guessed). In all three the backend call count is 0 (N-07).

Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.

The test behind this

Live on the demo WorkerN-08

S-08An irreversible action: hinted, not paused

wanderlust.cancel is effect: irreversible with policies: [authenticated, human-approval]. It is advertised with destructiveHint: true and idempotentHint: false, which a client may use to ask a person first. human-approval is declared, not enforced in this version: with the demo key the cancellation reaches the agency at once, and archstone apply warns about exactly that (S-10 has the warning and the tracking issue). It needs the agency key and a principal the policy allows: a missing key, or key B, is refused by Archstone's policy before the agency is asked (S-07). The refund is the booking's own total (354 EUR for the seeded booking).

If you paste this into your own AI: Your own AI app connects without a key unless it lets you set an Authorization header, so the cancellation is refused for lack of a key. "Run it live" sends the published demo key instead.

Declared: CDL and binding

wanderlust.cancel.capability.yaml
capability:
  id: wanderlust.cancel
  description: "Cancel a booking. This cannot be undone. Needs the agency key and a principal the manifest's policy allows: Archstone's policy refuses a missing key or a caller who is not allowed before the agency is asked. The manifest declares that a person should approve it; this version of Archstone does not enforce that."
  effect: irreversible

  input:
    bookingId:
      type: identifier
      description: "The booking reference from wanderlust.book."

  output:
    cancellation:
      type: Cancellation

  failures:
    booking-not-found: No booking has that reference.
    already-cancelled: The booking was already cancelled.

  policies:
    - authenticated
    - human-approval
  provider: wanderlust-api
wanderlust.cancel.policy.yaml
wanderlust.cancel.policy.yaml
# Who may cancel. The same principals as booking: a caller the manifest denies or does not list is
# refused by Archstone before the agency is asked. deny wins over allow.
apiVersion: archstone/v1
kind: Policy
metadata:
  id: wanderlust-cancel-principals
  name: Who may cancel
  scope: capability
  capabilityId: wanderlust.cancel
spec:
  allow:
    - "demo:visitor"
  deny:
    - "demo:blocked"
bindings/wanderlust.cancel.binding.yaml
# Implementation binding - how wanderlust.cancel reaches the synthetic agency. NOT CDL.
binding:
  capabilityId: wanderlust.cancel
  connector:
    type: rest
    rest:
      baseUrl: "${SHOWCASE_API_URL}"
      method: POST
      path: /v1/bookings/{bookingId}/cancel
      headers:
        Authorization: "Bearer ${caller.accessToken}"

  response:
    resource: Cancellation
    map:
      bookingId: "$.bookingId"
      status: "$.status"
      refund: "$.refund"
      cancelledAt: "$.cancelledAt"
wanderlust.Cancellation.resource.yaml
wanderlust.Cancellation.resource.yaml
resource:
  name: wanderlust.Cancellation
  description: "The outcome of cancelling a booking."
  fields:
    bookingId:
      type: identifier
      description: "The cancelled booking."
    status:
      type: enum
      values: [cancelled]
      description: "Always cancelled when the call succeeds."
    refund:
      type: money
      description: "What is refunded: the booking's own total."
    cancelledAt:
      type: datetime
      description: "When the agency recorded the cancellation."

What the backend returned, and what the model received

on a path the manifest declaresreturned by the backend, not declared: never forwarded

Backend answer, in full (POST /v1/bookings/B-0000cafe/cancel)
{
  "bookingId": "B-0000cafe",
  "status": "cancelled",
  "refund": {
    "amount": 354,
    "currency": "EUR"
  },
  "cancelledAt": "2026-10-10T23:13:53.438Z",
  "guests": [
    {
      "name": "Ion Exemplu",
      "passport": "DEMO-PASS-259500",
      "phone": "+00 000 000 500",
      "email": "ion.exemplu@guest.example"
    }
  ],
  "internalNote": "Cancelled by the synthetic agency; margin retained."
}
What the model received (structuredContent)
{
  "cancellation": {
    "bookingId": "B-0000cafe",
    "status": "cancelled",
    "refund": {
      "amount": 354,
      "currency": "EUR"
    },
    "cancelledAt": "2026-10-10T23:13:52.311Z"
  }
}

10 values in the backend's answer; 5 on declared paths; 5 never forwarded (guests, name, passport, phone, email, internalNote).

The call for S-08
Request: wanderlust_cancel
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'authorization: Bearer demo-public-key-visitor-0000' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_cancel","arguments":{"bookingId":"B-0000cafe"}}}'
Response (raw JSON-RPC)
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"cancellation\": {\n    \"bookingId\": \"B-0000cafe\",\n    \"status\": \"cancelled\",\n    \"refund\": {\n      \"amount\": 354,\n      \"currency\": \"EUR\"\n    },\n    \"cancelledAt\": \"2026-10-10T23:13:52.311Z\"\n  }\n}"
      }
    ],
    "structuredContent": {
      "cancellation": {
        "bookingId": "B-0000cafe",
        "status": "cancelled",
        "refund": {
          "amount": 354,
          "currency": "EUR"
        },
        "cancelledAt": "2026-10-10T23:13:52.311Z"
      }
    },
    "isError": false
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 1 · credential: public demo key A

Advertised by tools/list: wanderlust_cancel
{
  "name": "wanderlust_cancel",
  "description": "Cancel a booking. This cannot be undone. Needs the agency key and a principal the manifest's policy allows: Archstone's policy refuses a missing key or a caller who is not allowed before the agency is asked. The manifest declares that a person should approve it; this version of Archstone does not enforce that.",
  "annotations": {
    "destructiveHint": true,
    "idempotentHint": false
  }
}
What Archstone kept outThe result carries the cancellation's declared fields; the guest list and the internal note the agency also returns are not forwarded. What the example does not show is a pause: nothing here claims Archstone stops, asks or approves (N-08).

Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.

The test behind this

Live on the demo WorkerN-09

S-09A payment: the agency checks the quote, Archstone checks the shape of the call and warns about a bad declaration

wanderlust.pay needs the agency key and a principal its policy allows (like booking, S-07), and takes the booking id, the amount and a paymentQuote the agency issued with the booking and honours for 15 minutes. paymentQuote is a required input, so a call without one never leaves: Archstone's input contract refuses it first (see S-23). A quote that is present but expired or for another booking reaches the agency, which checks it (422). That check is the agency's rule, not Archstone's. What Archstone adds is a lint on the declaration: wanderlust.pay written without safeguards (the variant below has no failures, no authenticated, and an unenforced approval token) compiles with exit 0 and three warnings. The lints warn; they do not block.

If you paste this into your own AI: Your own AI app connects without a key unless it lets you set an Authorization header, so the booking, and with it the payment, is refused for lack of a key. "Run it live" sends the published demo key instead.

Declared: CDL and binding

wanderlust.pay.capability.yaml
capability:
  id: wanderlust.pay
  description: "Pay for a booking with the demo card. This cannot be undone. Needs the agency key and a principal the manifest's policy allows: Archstone's policy refuses a missing key or a caller who is not allowed before the agency is asked. It also needs the payment quote the agency returned with the booking; the agency, not Archstone, checks it. The manifest declares that a person should approve it; this version of Archstone does not enforce that."
  effect: irreversible

  input:
    bookingId:
      type: identifier
      description: "The booking reference from wanderlust.book."
    amount:
      type: money
      description: "The amount the booking total was quoted at."
    paymentQuote:
      type: identifier
      description: "The payment quote returned with the booking, still valid."

  output:
    payment:
      type: Payment

  failures:
    payment-quote-required: No payment quote was given.
    payment-quote-expired: The payment quote has expired.
    payment-quote-mismatch: The payment quote was issued for a different booking or amount.
    booking-not-found: No booking has that reference.

  policies:
    - authenticated
    - human-approval
  provider: wanderlust-api
wanderlust.pay.policy.yaml
wanderlust.pay.policy.yaml
# Who may pay. The same principals as booking: a caller the manifest denies or does not list is
# refused by Archstone before the agency is asked. deny wins over allow.
apiVersion: archstone/v1
kind: Policy
metadata:
  id: wanderlust-pay-principals
  name: Who may pay
  scope: capability
  capabilityId: wanderlust.pay
spec:
  allow:
    - "demo:visitor"
  deny:
    - "demo:blocked"
bindings/wanderlust.pay.binding.yaml
# Implementation binding - how wanderlust.pay reaches the synthetic agency. NOT CDL.
binding:
  capabilityId: wanderlust.pay
  connector:
    type: rest
    rest:
      baseUrl: "${SHOWCASE_API_URL}"
      method: POST
      path: /v1/payments
      headers:
        Authorization: "Bearer ${caller.accessToken}"

  response:
    resource: Payment
    map:
      paymentId: "$.paymentId"
      bookingId: "$.bookingId"
      status: "$.status"
      amount: "$.amount"
      paidAt: "$.paidAt"
wanderlust.Payment.resource.yaml
wanderlust.Payment.resource.yaml
resource:
  name: wanderlust.Payment
  description: "The outcome of a payment with the demo card."
  fields:
    paymentId:
      type: identifier
      description: "The payment reference."
    bookingId:
      type: identifier
      description: "The booking paid for."
    status:
      type: enum
      values: [paid]
      description: "Always paid when the call succeeds."
    amount:
      type: money
      description: "What was charged."
    paidAt:
      type: datetime
      description: "When the agency recorded the payment."
manifest-variants/misdeclared-pay/wanderlust.pay.capability.yaml (never deployed)
manifest-variants/misdeclared-pay/wanderlust.pay.capability.yaml (never deployed)
# Written without a quote step on purpose: no paymentQuote input, no failures, no authenticated.
capability:
  id: wanderlust.pay
  description: "Pay for a booking with the demo card."
  effect: irreversible

  input:
    bookingId:
      type: identifier
    amount:
      type: money

  output:
    payment:
      type: Payment

  policies:
    - human-approval
  provider: wanderlust-api

What the backend returned, and what the model received

on a path the manifest declaresreturned by the backend, not declared: never forwarded

Backend answer, in full (POST /v1/payments)
{
  "paymentId": "P-50310cea",
  "bookingId": "B-0162cd645944",
  "status": "paid",
  "amount": {
    "amount": 354,
    "currency": "EUR"
  },
  "paidAt": "2026-10-10T23:13:57.948Z",
  "card": {
    "brand": "DEMO",
    "last4": "0000"
  },
  "processorFee": 6.73,
  "guests": [
    {
      "name": "Nina Fictiva",
      "passport": "DEMO-PASS-639983",
      "phone": "+00 000 000 983",
      "email": "nina.fictiva@guest.example"
    }
  ]
}
What the model received (structuredContent)
{
  "payment": {
    "paymentId": "P-50310cea",
    "bookingId": "B-0162cd645944",
    "status": "paid",
    "amount": {
      "amount": 354,
      "currency": "EUR"
    },
    "paidAt": "2026-10-10T23:13:56.823Z"
  }
}

13 values in the backend's answer; 6 on declared paths; 7 never forwarded (card, brand, last4, processorFee, guests, name, passport, phone, email).

Setup step (wanderlust_quote)
Request: wanderlust_quote
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_quote","arguments":{"stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2}}}}'
Response (raw JSON-RPC)
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"quote\": {\n    \"quoteId\": \"Q-mv30gku9-fe8ae556\",\n    \"stayId\": \"ws-1001\",\n    \"dates\": {\n      \"from\": \"2027-05-12\",\n      \"to\": \"2027-05-15\"\n    },\n    \"nights\": 3,\n    \"total\": {\n      \"amount\": 354,\n      \"currency\": \"EUR\"\n    },\n    \"pets\": 0,\n    \"petFee\": {\n      \"amount\": 0,\n      \"currency\": \"EUR\"\n    },\n    \"expiresAt\": \"2026-10-10T23:28:54.561Z\"\n  }\n}"
      }
    ],
    "structuredContent": {
      "quote": {
        "quoteId": "Q-mv30gku9-fe8ae556",
        "stayId": "ws-1001",
        "dates": {
          "from": "2027-05-12",
          "to": "2027-05-15"
        },
        "nights": 3,
        "total": {
          "amount": 354,
          "currency": "EUR"
        },
        "pets": 0,
        "petFee": {
          "amount": 0,
          "currency": "EUR"
        },
        "expiresAt": "2026-10-10T23:28:54.561Z"
      }
    },
    "isError": false
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 1 · credential: none

Setup step (wanderlust_book)
Request: wanderlust_book
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'authorization: Bearer demo-public-key-visitor-0000' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_book","arguments":{"quoteId":"Q-mv30gku9-fe8ae556","stayId":"ws-1001","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2},"guestName":"Ana Pop"}}}'
Response (raw JSON-RPC)
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"booking\": {\n    \"bookingId\": \"B-0162cd645944\",\n    \"status\": \"confirmed\",\n    \"stayId\": \"ws-1001\",\n    \"dates\": {\n      \"from\": \"2027-05-12\",\n      \"to\": \"2027-05-15\"\n    },\n    \"total\": {\n      \"amount\": 354,\n      \"currency\": \"EUR\"\n    },\n    \"pets\": 0,\n    \"petFee\": {\n      \"amount\": 0,\n      \"currency\": \"EUR\"\n    },\n    \"paymentQuote\": \"PQ-mv30glpp-a489787e\",\n    \"payBy\": \"2026-10-10T23:28:55.693Z\"\n  }\n}"
      }
    ],
    "structuredContent": {
      "booking": {
        "bookingId": "B-0162cd645944",
        "status": "confirmed",
        "stayId": "ws-1001",
        "dates": {
          "from": "2027-05-12",
          "to": "2027-05-15"
        },
        "total": {
          "amount": 354,
          "currency": "EUR"
        },
        "pets": 0,
        "petFee": {
          "amount": 0,
          "currency": "EUR"
        },
        "paymentQuote": "PQ-mv30glpp-a489787e",
        "payBy": "2026-10-10T23:28:55.693Z"
      }
    },
    "isError": false
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 1 · credential: public demo key A

The call for S-09
Request: wanderlust_pay
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'authorization: Bearer demo-public-key-visitor-0000' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_pay","arguments":{"bookingId":"B-0162cd645944","amount":{"amount":354,"currency":"EUR"},"paymentQuote":"PQ-mv30glpp-a489787e"}}}'
Response (raw JSON-RPC)
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"payment\": {\n    \"paymentId\": \"P-50310cea\",\n    \"bookingId\": \"B-0162cd645944\",\n    \"status\": \"paid\",\n    \"amount\": {\n      \"amount\": 354,\n      \"currency\": \"EUR\"\n    },\n    \"paidAt\": \"2026-10-10T23:13:56.823Z\"\n  }\n}"
      }
    ],
    "structuredContent": {
      "payment": {
        "paymentId": "P-50310cea",
        "bookingId": "B-0162cd645944",
        "status": "paid",
        "amount": {
          "amount": 354,
          "currency": "EUR"
        },
        "paidAt": "2026-10-10T23:13:56.823Z"
      }
    },
    "isError": false
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 1 · credential: public demo key A

What Archstone refused: the denial payload

The same payment without the payment quote
Request: wanderlust_pay
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'authorization: Bearer demo-public-key-visitor-0000' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_pay","arguments":{"bookingId":"B-0162cd645944","amount":{"amount":354,"currency":"EUR"}}}}'
Response (raw JSON-RPC)
{
  "result": {
    "_meta": {
      "dev.archstone/input_invalid": {
        "error": "input_invalid",
        "capability": "wanderlust.pay",
        "problems": [
          {
            "path": "paymentQuote",
            "expected": "required"
          }
        ]
      }
    },
    "content": [
      {
        "type": "text",
        "text": "input for capability 'wanderlust.pay' does not match its declared contract: paymentQuote (expected required)."
      }
    ],
    "isError": true
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 0 · credential: public demo key A

Reason code input_invalid under _meta["dev.archstone/input_invalid"]; problems: [{"path":"paymentQuote","expected":"required"}]. The text content is the human-readable sentence; the client reads the reason from _meta.

Declaration lint: the payment written without safeguards

Output of the published @archstone/cli 0.33.0, re-captured on every check: archstone apply manifest-variants/misdeclared-pay exits 0 with 3 warnings.

archstone apply misdeclared-pay (warnings only)
⚠ capability 'wanderlust.pay' is irreversible and declares no failures. When it fails, an agent can say only that it failed, not why, and must not retry. Name the business outcomes that stop it under failures: (for example insufficient-funds, already-refunded).
⚠ capability 'wanderlust.pay' is irreversible and does not declare policies:[authenticated]. Any caller that can reach this server can invoke it. If that is intended, nothing to change; this line stays so the decision stays visible. Otherwise add authenticated to policies:.
⚠ capability 'wanderlust.pay' is irreversible and declares policies:[human-approval], which this version does not enforce: no approval mechanism exists. An agent can run it without anyone approving. Put the approval step in the provider, or do not serve this capability to an agent unattended.
What Archstone kept outThe payment without its quote is refused by Archstone's input contract (input_invalid, naming paymentQuote as required) and the backend call count is 0. An expired or foreign payment quote is the agency's refusal and surfaces only as backend returned 422: the status code, not the agency's reason, and no _meta; that case is asserted by the test below, not reproducible with a fixed live call. The agency's own answers name the problem (payment_quote_required, 422); the runtime reports the status only (N-09, NousVigil/archstone#175).

Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.

The test behind this

  • negatives.test.ts#L359 — N-09: the mis-declared payment warns and does not block; a missing quote is refused by the input contract; the agency refuses a wrong one
  • manifests.test.ts#L227 — the mis-declared payment variant: apply exits 0 with the three warnings
  • manifests.test.ts#L289 — every live row runs end to end against the synthetic API

Locked · planned · #165

S-10Runtime-enforced human approval: locked

The step "a person approves before it happens" is not implemented. human-approval is accepted in a capability's policies and lowered, and that is all: there is no approval mechanism and no approval-related denial reason (the eight denial reasons are in the denial-reasons test; none concerns approval). Nothing runs behind this anchor.

Planned: not available yet NousVigil/archstone#165

Nothing runs behind this anchor. It is shown, not hidden, so the gap is visible.

What is declared today

wanderlust.cancel.capability.yaml
capability:
  id: wanderlust.cancel
  description: "Cancel a booking. This cannot be undone. Needs the agency key and a principal the manifest's policy allows: Archstone's policy refuses a missing key or a caller who is not allowed before the agency is asked. The manifest declares that a person should approve it; this version of Archstone does not enforce that."
  effect: irreversible

  input:
    bookingId:
      type: identifier
      description: "The booking reference from wanderlust.book."

  output:
    cancellation:
      type: Cancellation

  failures:
    booking-not-found: No booking has that reference.
    already-cancelled: The booking was already cancelled.

  policies:
    - authenticated
    - human-approval
  provider: wanderlust-api
Advertised by tools/list: wanderlust_cancel
{
  "name": "wanderlust_cancel",
  "description": "Cancel a booking. This cannot be undone. Needs the agency key and a principal the manifest's policy allows: Archstone's policy refuses a missing key or a caller who is not allowed before the agency is asked. The manifest declares that a person should approve it; this version of Archstone does not enforce that.",
  "annotations": {
    "destructiveHint": true,
    "idempotentHint": false
  }
}

The warning Archstone prints today

Output of the published @archstone/cli 0.33.0, re-captured on every check: archstone apply on the manifest exits 0 and prints:

archstone apply (the human-approval warnings)
⚠ capability 'wanderlust.cancel' is irreversible and declares policies:[human-approval], which this version does not enforce: no approval mechanism exists. An agent can run it without anyone approving. Put the approval step in the provider, or do not serve this capability to an agent unattended.
⚠ capability 'wanderlust.pay' is irreversible and declares policies:[human-approval], which this version does not enforce: no approval mechanism exists. An agent can run it without anyone approving. Put the approval step in the provider, or do not serve this capability to an agent unattended.

What is planned

What is planned (the roadmap issue, to be refined by an RFC before anything is built): an approval bound to one call (capability, canonical input, principal), single-use and expiring; a new denial reason returned when an irreversible call declared with human-approval arrives without a valid approval, with the provider receiving no request; identical in the MCP runtime and the embedded execute(); fail closed when approval state cannot be evaluated. Until it ships, the protections that exist are hints and warnings: the destructiveHint annotation, the quote → commit pattern, the client's own confirmation, and the irreversible-* lints.

Where it stands todayDeclared, not enforced: lintIR emits irreversible-unenforced-policy (severity warning, token human-approval) for wanderlust.cancel and wanderlust.pay, and the cancel tool's advertised description says so (captured above).

The test behind this

Limits, lifecycle, errors, input and absence

Live on the demo WorkerN-11

S-11The fourth availability call in a minute is refused

wanderlust.availability is the one capability with a rate limit: 3 calls per 60 seconds, declared in a policy document. The fourth call in the window is refused with rate_limit_exceeded before the agency is asked; another capability still answers. On a Node runtime with a fake clock the test asserts the exact fourth refusal. On this live Worker the limit is approximate: the counter is a fixed window held in memory per isolate, so another isolate or a restart starts a new count, silently. The Worker says so on every response in the x-showcase-rate-limit header.

Declared: CDL and binding

wanderlust.availability.policy.yaml
# The only rate limit in the Showcase: 3 calls per minute per caller. A counter is supplied by
# whoever hosts the server; a capability that declares a rate limit and runs without one is refused.
apiVersion: archstone/v1
kind: Policy
metadata:
  id: wanderlust-availability-rate
  name: Availability rate limit
  scope: capability
  capabilityId: wanderlust.availability
spec:
  rateLimit:
    maxInvocations: 3
    windowSeconds: 60
wanderlust.availability.capability.yaml
capability:
  id: wanderlust.availability
  description: "How many rooms a property has free on a date. Archstone's policy allows 3 calls per minute per caller and refuses the rest."
  effect: read

  input:
    propertyId:
      type: identifier
      description: "The stay's id: the `id` field of a row returned by wanderlust_search (it looks like `ws-1001`), never the stay's name. To find a stay the user names, first call wanderlust_search with that name as `destination`."
    date:
      type: date

  output:
    availability:
      type: Availability

  lifecycle: beta
  provider: wanderlust-api
bindings/wanderlust.availability.binding.yaml
bindings/wanderlust.availability.binding.yaml
# Implementation binding - how wanderlust.availability reaches the synthetic agency. NOT CDL.
binding:
  capabilityId: wanderlust.availability
  connector:
    type: rest
    rest:
      baseUrl: "${SHOWCASE_API_URL}"
      method: GET
      path: /v1/availability

  response:
    resource: Availability
    map:
      propertyId: "$.propertyId"
      date: "$.date"
      roomsFree: "$.roomsFree"
      pricePerNight: "$.pricePerNight"
wanderlust.Availability.resource.yaml
wanderlust.Availability.resource.yaml
resource:
  name: wanderlust.Availability
  description: "How many rooms a property has free on a date."
  fields:
    propertyId:
      type: identifier
      description: "The property."
    date:
      type: date
      description: "The date asked about."
    roomsFree:
      type: quantity
      description: "Rooms free that night."
    pricePerNight:
      type: quantity
      description: "Nightly rate in euro."

What the backend returned, and what the model received

on a path the manifest declaresreturned by the backend, not declared: never forwarded

Backend answer, in full (GET /v1/availability)
{
  "propertyId": "ws-1001",
  "date": "2027-06-05",
  "roomsFree": 3,
  "pricePerNight": 118,
  "net": 73.16,
  "margin": 44.84,
  "commission": 14.16
}
What the model received (structuredContent)
{
  "availability": {
    "propertyId": "ws-1001",
    "date": "2027-06-05",
    "roomsFree": 3,
    "pricePerNight": 118
  }
}

7 values in the backend's answer; 4 on declared paths; 3 never forwarded (net, margin, commission).

The call for S-11
Request: wanderlust_availability
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_availability","arguments":{"propertyId":"ws-1001","date":"2027-06-05"}}}'
Response (raw JSON-RPC)
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"availability\": {\n    \"propertyId\": \"ws-1001\",\n    \"date\": \"2027-06-05\",\n    \"roomsFree\": 3,\n    \"pricePerNight\": 118\n  }\n}"
      }
    ],
    "structuredContent": {
      "availability": {
        "propertyId": "ws-1001",
        "date": "2027-06-05",
        "roomsFree": 3,
        "pricePerNight": 118
      }
    },
    "isError": false
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 1 · credential: none

What Archstone refused: the denial payload

The fourth call in the window
Request: wanderlust_availability
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_availability","arguments":{"propertyId":"ws-1001","date":"2027-06-05"}}}'
Response (raw JSON-RPC)
{
  "result": {
    "_meta": {
      "dev.archstone/policy_denied": {
        "error": "policy_denied",
        "capability": "wanderlust.availability",
        "reason": "rate_limit_exceeded"
      }
    },
    "content": [
      {
        "type": "text",
        "text": "capability 'wanderlust.availability' exceeded its rate limit (3 per 60s) for this caller."
      }
    ],
    "isError": true
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 0 · credential: none

Reason code rate_limit_exceeded under _meta["dev.archstone/policy_denied"]. The text content is the human-readable sentence; the client reads the reason from _meta.

What Archstone kept outThe backend served three availability requests and never saw the fourth (x-showcase-backend-calls: 0). The margin fields the agency returns with every availability answer are not forwarded (N-11).

Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.

The test behind this

Live on the demo WorkerN-12

S-12Deprecated still works, with a note; retired is refused and never listed

tourism.search is lifecycle: deprecated: it still answers, in its original shape, and its advertised tool description carries the phasing-out note: the note lives in tools/list, not in the search result, and the live /run body returns it as evidence (the sentence of the description the card relies on, read from the same tool list). tourism.search-classic is lifecycle: retired: it is absent from tools/list and a call to it by name is refused with lifecycle_blocked, with no request to the agency.

Declared: CDL and binding

tourism.search.capability.yaml
capability:
  id: tourism.search
  description: "Find places to stay matching a traveler's destination, dates, party and budget. Deprecated: it is being phased out; prefer wanderlust.search."
  effect: read

  input:
    destination:
      type: location
      description: "The city name, e.g. Lisbon or Porto, optionally followed by its country after a comma; or the exact name of one stay, e.g. Pensão Azul, to look at that stay only."
    dates:
      type: date-range
      description: "Check-in and check-out as ISO dates (YYYY-MM-DD), 1 to 30 nights. Work out calendar dates yourself; prose such as 'next weekend' is refused."
    travelers:
      type: party
    budget:
      type: money
      required: false
      description: "Maximum nightly rate as an object {amount, currency}, currency always \"EUR\" (a per-night ceiling, not a total for the stay). Not a bare number."
    preferences:
      type: preference-set
      required: false
      description: "Optional tags, all of which a stay must satisfy: pets (also pet-friendly, cat, dog), breakfast, family. Other tags are ignored."

  output:
    stays:
      collection: Stay
    totalMatches:
      type: quantity
      description: "Total number of stays the backend found, before this response's own page/limit."

  lifecycle: deprecated
  provider: wanderlust-api
bindings/tourism.search.binding.yaml
# Implementation binding - how tourism.search reaches the synthetic agency. NOT CDL.
binding:
  capabilityId: tourism.search
  connector:
    type: rest
    rest:
      baseUrl: "${SHOWCASE_API_URL}"
      method: POST
      path: /v1/search

  response:
    collection: "$.stays[*]"
    resource: Stay
    map:
      name: "$.name"
      location: "$.location"
      pricePerNight: "$.pricePerNight"
      rating: "$.rating"

  extract:
    totalMatches: "$.totalMatches"

  # What the synthetic API last returned for the fixture's request (recorded: paths and types, never values).
  # The shape includes fields this manifest never maps (net, margin, guests, ...): drift in them is still
  # detected, and they are still never forwarded to a model.
  # contract: the recorded shape of everything the backend answered (14 paths, most of them never mapped above) is in the file
tourism.search-classic.capability.yaml
tourism.search-classic.capability.yaml
capability:
  id: tourism.search-classic
  description: "The oldest search. Retired: switched off, neither listed nor callable."
  effect: read

  input:
    destination:
      type: location
    dates:
      type: date-range
    travelers:
      type: party
    budget:
      type: money
      required: false

  output:
    stays:
      collection: Stay

  lifecycle: retired
  provider: wanderlust-api
tourism.Stay.resource.yaml
tourism.Stay.resource.yaml
# The unit the legacy tourism.search returns - identical to examples/manifests/tourism.
resource:
  name: tourism.Stay
  description: "A bookable place to stay matching a traveler's search."
  fields:
    name:
      type: text
      description: "The property's display name."
    location:
      type: location
      description: "Where the stay is - city, region, or address."
    pricePerNight:
      type: quantity
      description: "Nightly rate for the stay."
    rating:
      type: quantity
      required: false
      description: "Guest review score, when available."

What the backend returned, and what the model received

on a path the manifest declaresreturned by the backend, not declared: never forwarded

Backend answer, in full (POST /v1/search)
{
  "stays": [
    {
      "id": "ws-1002",
      "name": "Pensão Azul",
      "location": "Lisbon",
      "pricePerNight": 74,
      "rating": 4.1,
      "boardType": "ROOM_ONLY",
      "freeCancellationUntil": "2026-08-03",
      "roomDescription": "Family Room, two bedrooms",
      "net": 57.72,
      "commission": 8.88
    },
    {
      "id": "ws-1004",
      "name": "Rio Tejo Lofts",
      "location": "Lisbon",
      "pricePerNight": 96,
      "rating": 4.3,
      "boardType": "ROOM_ONLY",
      "freeCancellationUntil": "2026-07-05",
      "roomDescription": "Junior Suite, terrace",
      "net": 74.88,
      "commission": 11.52
    },
    {
      "id": "ws-1001",
      "name": "Casa Alfama",
      "location": "Lisbon",
      "pricePerNight": 118,
      "rating": 4.6,
      "boardType": "BREAKFAST",
      "freeCancellationUntil": "2026-07-02",
      "roomDescription": "Junior Suite, terrace",
      "net": 92.04,
      "commission": 14.16
    },
    {
      "id": "ws-1003",
      "name": "Miradouro Court",
      "location": "Lisbon",
      "pricePerNight": 139,
      "rating": 4.8,
      "boardType": "BREAKFAST",
      "freeCancellationUntil": "2026-06-04",
      "roomDescription": "Family Room, two bedrooms",
      "net": 108.42,
      "commission": 16.68
    }
  ],
  "totalMatches": 4
}
What the model received (structuredContent)
{
  "stays": [
    {
      "name": "Pensão Azul",
      "location": "Lisbon",
      "pricePerNight": 74,
      "rating": 4.1
    },
    {
      "name": "Rio Tejo Lofts",
      "location": "Lisbon",
      "pricePerNight": 96,
      "rating": 4.3
    },
    {
      "name": "Casa Alfama",
      "location": "Lisbon",
      "pricePerNight": 118,
      "rating": 4.6
    },
    {
      "name": "Miradouro Court",
      "location": "Lisbon",
      "pricePerNight": 139,
      "rating": 4.8
    }
  ],
  "totalMatches": 4
}

41 values in the backend's answer; 17 on declared paths; 24 never forwarded (id, boardType, freeCancellationUntil, roomDescription, net, commission).

The call for S-12
Request: tourism_search
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"tourism_search","arguments":{"destination":"Lisbon","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2}}}}'
Response (raw JSON-RPC)
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"stays\": [\n    {\n      \"name\": \"Pensão Azul\",\n      \"location\": \"Lisbon\",\n      \"pricePerNight\": 74,\n      \"rating\": 4.1\n    },\n    {\n      \"name\": \"Rio Tejo Lofts\",\n      \"location\": \"Lisbon\",\n      \"pricePerNight\": 96,\n      \"rating\": 4.3\n    },\n    {\n      \"name\": \"Casa Alfama\",\n      \"location\": \"Lisbon\",\n      \"pricePerNight\": 118,\n      \"rating\": 4.6\n    },\n    {\n      \"name\": \"Miradouro Court\",\n      \"location\": \"Lisbon\",\n      \"pricePerNight\": 139,\n      \"rating\": 4.8\n    }\n  ],\n  \"totalMatches\": 4\n}"
      }
    ],
    "structuredContent": {
      "stays": [
        {
          "name": "Pensão Azul",
          "location": "Lisbon",
          "pricePerNight": 74,
          "rating": 4.1
        },
        {
          "name": "Rio Tejo Lofts",
          "location": "Lisbon",
          "pricePerNight": 96,
          "rating": 4.3
        },
        {
          "name": "Casa Alfama",
          "location": "Lisbon",
          "pricePerNight": 118,
          "rating": 4.6
        },
        {
          "name": "Miradouro Court",
          "location": "Lisbon",
          "pricePerNight": 139,
          "rating": 4.8
        }
      ],
      "totalMatches": 4
    },
    "isError": false
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 1 · credential: none

Advertised by tools/list: tourism_search
{
  "name": "tourism_search",
  "description": "Find places to stay matching a traveler's destination, dates, party and budget. Deprecated: it is being phased out; prefer wanderlust.search. (deprecated — avoid new usage)",
  "annotations": {
    "readOnlyHint": true
  }
}

Where the note lives (evidence of the live /run body, kind tool-description): the tool description in tools/list. The sentence, from the captured tools/list: Deprecated: it is being phased out; prefer wanderlust.search.

What Archstone refused: the denial payload

A call to the retired tool, by name
Request: tourism_search-classic
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"tourism_search-classic","arguments":{"destination":"Lisbon","dates":{"from":"2027-05-12","to":"2027-05-15"},"travelers":{"adults":2}}}}'
Response (raw JSON-RPC)
{
  "result": {
    "_meta": {
      "dev.archstone/lifecycle_blocked": {
        "error": "lifecycle_blocked",
        "capability": "tourism.search-classic",
        "lifecycle": "retired"
      }
    },
    "content": [
      {
        "type": "text",
        "text": "capability 'tourism.search-classic' is retired and can no longer be invoked."
      }
    ],
    "isError": true
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 0 · credential: none

Reason code lifecycle_blocked under _meta["dev.archstone/lifecycle_blocked"]; lifecycle: retired. The text content is the human-readable sentence; the client reads the reason from _meta.

Checked on the captured tools/list (11 tools): tourism_search-classic is not listed.

What Archstone kept outThe retired capability is listed nowhere (checked on the captured tools/list) and the agency receives nothing. The S-16 recording lists it as not exposed (retired), and the exposed total equals the tool list a client receives (N-12).

Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.

The test behind this

Live on the demo WorkerN-13

S-13An error row is a row; a wrong-typed price is a contract violation

wanderlust.room-status maps normal rows and, with onError, error rows that arrive inside a 2xx answer: the busy room is returned as a row marked $row: "error" with a declared code and message. A property whose price arrives in the wrong type (ws-1003: an object with a formatted string where a number is declared) is not passed on as fine: the result is an error with _meta["dev.archstone/contract_violation"] naming the capability and listing the field under invalid ({ field: "pricePerNight", expected: "quantity" }; missing stays empty, because the field was sent, in the wrong shape), no structured content, and a text that says the raw body is withheld. The live /run call makes both requests (alsoRun holds the second), so the card's two claims are each shown by its own run. The busy row and the wrong-typed price are one property and date each (ws-1002 and ws-1003 on 2027-05-12); every other property answers normally.

Declared: CDL and binding

wanderlust.room-status.capability.yaml
capability:
  id: wanderlust.room-status
  description: "Is a property's room free on a date? Some rows may come back as errors when the agency's system is busy."
  effect: read

  input:
    propertyId:
      type: identifier
      description: "The stay's id: the `id` field of a row returned by wanderlust_search (it looks like `ws-1001`), never the stay's name. To find a stay the user names, first call wanderlust_search with that name as `destination`."
    date:
      type: date

  output:
    rooms:
      collection: RoomStatus

  provider: wanderlust-api
bindings/wanderlust.room-status.binding.yaml
# Implementation binding - how wanderlust.room-status reaches the synthetic agency. NOT CDL.
binding:
  capabilityId: wanderlust.room-status
  connector:
    type: rest
    rest:
      baseUrl: "${SHOWCASE_API_URL}"
      method: GET
      path: /v1/room-status

  response:
    collection: "$.rows[*]"
    resource: RoomStatus
    map:
      room: "$.room"
      status: "$.status"
      pricePerNight: "$.pricePerNight"
    onError:
      errorResource: RoomStatusError
      when:
        path: "$.error"
        exists: true
      map:
        code: "$.error.code"
        message: "$.error.message"

  # What the synthetic API last returned for the fixture's request (recorded: paths and types, never values).
  # The shape includes fields this manifest never maps (net, margin, guests, ...): drift in them is still
  # detected, and they are still never forwarded to a model.
  # contract: the recorded shape of everything the backend answered (8 paths, most of them never mapped above) is in the file
wanderlust.RoomStatus.resource.yaml
wanderlust.RoomStatus.resource.yaml
resource:
  name: wanderlust.RoomStatus
  description: "One room's status on a date."
  fields:
    room:
      type: text
      description: "Room type."
    status:
      type: enum
      values: [free, taken]
      description: "Whether it is free."
    pricePerNight:
      type: quantity
      description: "Nightly rate in euro."
wanderlust.RoomStatusError.resource.yaml
wanderlust.RoomStatusError.resource.yaml
resource:
  name: wanderlust.RoomStatusError
  description: "A row the agency could not answer, in place of a room status."
  fields:
    code:
      type: text
      description: "A short machine-readable reason."
    message:
      type: text
      required: false
      description: "What the agency said."

What the backend returned, and what the model received

on a path the manifest declaresreturned by the backend, not declared: never forwarded

Backend answer, in full (GET /v1/room-status)
{
  "propertyId": "ws-1002",
  "date": "2027-05-12",
  "rows": [
    {
      "room": "Double Room",
      "status": "free",
      "pricePerNight": 74
    },
    {
      "room": "Family Room",
      "error": {
        "code": "agency-busy",
        "message": "The agency's room system is busy; try again shortly."
      }
    }
  ]
}
What the model received (structuredContent)
{
  "rooms": [
    {
      "$row": "ok",
      "room": "Double Room",
      "status": "free",
      "pricePerNight": 74
    },
    {
      "$row": "error",
      "code": "agency-busy",
      "message": "The agency's room system is busy; try again shortly."
    }
  ]
}

8 values in the backend's answer; 6 on declared paths; 2 never forwarded (propertyId, date).

The call for S-13
Request: wanderlust_room-status
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_room-status","arguments":{"propertyId":"ws-1002","date":"2027-05-12"}}}'
Response (raw JSON-RPC)
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"rooms\": [\n    {\n      \"$row\": \"ok\",\n      \"room\": \"Double Room\",\n      \"status\": \"free\",\n      \"pricePerNight\": 74\n    },\n    {\n      \"$row\": \"error\",\n      \"code\": \"agency-busy\",\n      \"message\": \"The agency's room system is busy; try again shortly.\"\n    }\n  ]\n}"
      }
    ],
    "structuredContent": {
      "rooms": [
        {
          "$row": "ok",
          "room": "Double Room",
          "status": "free",
          "pricePerNight": 74
        },
        {
          "$row": "error",
          "code": "agency-busy",
          "message": "The agency's room system is busy; try again shortly."
        }
      ]
    },
    "isError": false
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 1 · credential: none

What Archstone refused: the denial payload

The wrong-format-price call: the property whose price is the wrong type (ws-1003)
Request: wanderlust_room-status
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_room-status","arguments":{"propertyId":"ws-1003","date":"2027-05-12"}}}'
Response (raw JSON-RPC)
{
  "result": {
    "_meta": {
      "dev.archstone/contract_violation": {
        "error": "contract_violation",
        "capability": "wanderlust.room-status",
        "missing": [],
        "invalid": [
          {
            "field": "pricePerNight",
            "expected": "quantity"
          }
        ]
      }
    },
    "content": [
      {
        "type": "text",
        "text": "contract violation: capability 'wanderlust.room-status' — provider response has a value of the wrong shape in field(s): pricePerNight (expected quantity). Declared output shape not met; raw body withheld."
      }
    ],
    "isError": true
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 1 · credential: none

Reason code contract_violation under _meta["dev.archstone/contract_violation"]; invalid: [{"field":"pricePerNight","expected":"quantity"}]. The text content is the human-readable sentence; the client reads the reason from _meta.

What the agency sent for this property (the price is an object with a formatted string)
{
  "propertyId": "ws-1003",
  "date": "2027-05-12",
  "rows": [
    {
      "room": "Double Room",
      "status": "taken",
      "pricePerNight": {
        "amount": "139,00",
        "currency": "EUR"
      }
    },
    {
      "room": "Family Room",
      "status": "taken",
      "pricePerNight": {
        "amount": "202,00",
        "currency": "EUR"
      }
    }
  ]
}
What Archstone kept outThe wrong-typed price appears nowhere in the result, and the violation names the field as invalid, not missing. A failing backend is reported as an error with its status (backend returned 503), not as degraded: onError maps error rows inside a 2xx answer, and a non-2xx answer never reaches the response mapping. That 503 path is not reproducible on the live Worker; the test below asserts it (N-13).

Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.

The test behind this

Live on the demo WorkerN-14

S-14Deletion is not a tool

The agency's API has DELETE /v1/guests/{name}/bookings, and it works with either demo key. No capability binds it: no capability, field, mapping or connector in the manifest names deletion. A client that asks for a tool called wanderlust_delete-guest-bookings is told it is unknown, and nothing reaches the agency.

Declared: CDL and binding

capabilities.yaml
# capabilities.yaml - the Wanderlust Agency Showcase manifest (CDL 0.2).
#
# A SYNTHETIC agency: every hotel, guest, price and id is invented, and the backend it points at
# (../api/wanderlust-api.mjs) over-exposes on purpose. The manifest is the contract; what it leaves
# unnamed never reaches a model. See ../README.md.
#
#   archstone apply examples/showcase/manifest
#   SHOWCASE_API_URL=http://localhost:8788 archstone serve examples/showcase/manifest

company:
  id: wanderlust
  name: Wanderlust Agency (synthetic)
  description: An invented travel agency that exercises every Archstone feature against a deliberately over-exposing backend.

capabilities:
  - wanderlust.search
  - wanderlust.stay-details
  - wanderlust.stay-photos
  - wanderlust.stay-page
  - wanderlust.quote
  - wanderlust.book
  - wanderlust.cancel
  - wanderlust.pay
  - wanderlust.availability
  - wanderlust.room-status
  - wanderlust.neighbourhood
  - tourism.search
  - tourism.search-classic

providers:
  - wanderlust-api

What the backend does

In-process, the vendored synthetic API: DELETE /v1/guests/Ana%20Pop/bookings (demo key A)
{
  "guest": "Ana Pop",
  "deletedBookings": 4,
  "deletedAt": "2027-05-01T10:07:00.000Z"
}

The agency answers as though it deleted. The synthetic API stores nothing, so nothing is deleted; the point is that the route exists, works, and is bound by no capability.

The call for S-14
Request: wanderlust_delete-guest-bookings
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_delete-guest-bookings","arguments":{"name":"Ana Pop"}}}'
Response (raw JSON-RPC)
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "unknown tool: wanderlust_delete-guest-bookings"
      }
    ],
    "isError": true
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 0 · credential: none

Checked on the captured tools/list (11 tools): wanderlust_delete-guest-bookings, wanderlust_delete-booking, wanderlust_delete-bookings are not listed.

What Archstone kept outThe exposed tool list contains no tool that deletes (checked on the captured tools/list); the call is answered unknown tool and x-showcase-backend-calls is 0. This is absence by construction: there is no gate to bypass because there is no capability (N-14).

Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.

The test behind this

Live on the demo WorkerN-23

S-23A wrong-shaped argument is refused before the agency is asked

wanderlust.search declares its input: destination is a location (a string), dates a date-range (an object of ISO dates), travelers a party (an object of non-negative counts). The runtime validates the arguments against that declaration after the lifecycle and policy gates and before any connector work, and it coerces nothing. This scenario sends a destination that is an object ({"$ne": 1}, the shape of a query-injection attempt), the word tomorrow where a date range belongs, and a negative party. The call is refused with input_invalid and the agency is never called: x-showcase-backend-calls is 0.

Declared: CDL and binding

wanderlust.search.capability.yaml
capability:
  id: wanderlust.search
  description: "Find places to stay matching a traveler's destination, dates, party and budget. Results come in the agency's own order; nothing here ranks them."
  effect: read

  input:
    destination:
      type: location
      description: "The city name, e.g. Lisbon or Porto, optionally followed by its country after a comma; or the exact name of one stay, e.g. Casa Alfama or Pensão Azul, to look at that stay only and get its id. Use this whenever the user names a stay; do not ask for a city first."
    dates:
      type: date-range
      description: "Check-in and check-out as ISO dates (YYYY-MM-DD), 1 to 30 nights. Work out calendar dates yourself; prose such as 'next weekend' is refused."
    travelers:
      type: party
    budget:
      type: money
      required: false
      description: "Maximum nightly rate as an object {amount, currency}, currency always \"EUR\" (a per-night ceiling, not a total for the stay). Not a bare number."
    preferences:
      type: preference-set
      required: false
      description: "Optional tags, all of which a stay must satisfy: pets (also pet-friendly, cat, dog), breakfast, family. Other tags are ignored."

  output:
    stays:
      collection: Stay
    totalMatches:
      type: quantity
      description: "How many stays the agency found."

  provider: wanderlust-api
bindings/wanderlust.search.binding.yaml
bindings/wanderlust.search.binding.yaml
# Implementation binding - how wanderlust.search reaches the synthetic agency. NOT CDL.
binding:
  capabilityId: wanderlust.search
  connector:
    type: rest
    rest:
      baseUrl: "${SHOWCASE_API_URL}"
      method: POST
      path: /v1/stays/search

  response:
    collection: "$.stays[*]"
    resource: Stay
    map:
      id: "$.id"
      name: "$.name"
      location: "$.location"
      pricePerNight: "$.pricePerNight"
      rating: "$.rating"
      petPolicy: "$.petPolicy"

  extract:
    totalMatches: "$.totalMatches"

  # What the synthetic API last returned for the fixture's request (recorded: paths and types, never values).
  # The shape includes fields this manifest never maps (net, margin, guests, ...): drift in them is still
  # detected, and they are still never forwarded to a model.
  # contract: the recorded shape of everything the backend answered (18 paths, most of them never mapped above) is in the file
The call for S-23
Request: wanderlust_search
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"wanderlust_search","arguments":{"destination":{"$ne":1},"dates":"tomorrow","travelers":-1}}}'
Response (raw JSON-RPC)
{
  "result": {
    "_meta": {
      "dev.archstone/input_invalid": {
        "error": "input_invalid",
        "capability": "wanderlust.search",
        "problems": [
          {
            "path": "destination",
            "expected": "string"
          },
          {
            "path": "dates",
            "expected": "object"
          },
          {
            "path": "travelers",
            "expected": "object"
          }
        ]
      }
    },
    "content": [
      {
        "type": "text",
        "text": "input for capability 'wanderlust.search' does not match its declared contract: destination (expected string); dates (expected object); travelers (expected object)."
      }
    ],
    "isError": true
  },
  "jsonrpc": "2.0",
  "id": 1
}

HTTP 200 · x-showcase-backend-calls: 0 · credential: none

Reason code input_invalid under _meta["dev.archstone/input_invalid"]; problems: [{"path":"destination","expected":"string"},{"path":"dates","expected":"object"},{"path":"travelers","expected":"object"}]. The text content is the human-readable sentence; the client reads the reason from _meta.

What Archstone kept outThe refusal names declared field paths and fixed expectations only (destination expected string, dates and travelers expected object, at most 16 problems); it never contains a value that was sent or the name of an undeclared key (N-23). The same arguments made well-formed reach the agency, which is the test's control. This holds because the declared input is enforced (core #195); before that fix the arguments were forwarded and only the agency's own validation stopped them.

Optional. Calls the public demo Worker from your browser; it answers pages on https://archstone.dev only. Nothing above depends on it.

The test behind this

Recorded: what runs only on a machine of your own

RecordedN-15

S-15A report from the agency's own database (local only)

The SQL provider is Node-only and needs a database, so this scenario is a recording, not a live call. The manifest under local/reporting is not part of the manifest the Worker serves. The capability is business-only (what an analyst can ask, what comes back); the binding holds the query, and no tenant filter: whose bookings these are is the caller's identity, enforced by the database (row-level security), not by the binding.

Declared

local/reporting/wanderlust.bookings-by-city.capability.yaml
# Business only: what an analyst can ask and what comes back. No table, no query, no tenant. Whose
# bookings these are is the caller's identity, enforced by the database (see fixture.sql).
capability:
  id: wanderlust.bookings-by-city
  description: How many bookings the caller's own agency took in each city in one month.
  effect: read

  input:
    month:
      type: text
      description: The month, as YYYY-MM.

  output:
    cities:
      collection: CityBookings

  provider: agency-warehouse
local/reporting/bindings/wanderlust.bookings-by-city.binding.yaml
# NOT CDL - implementation (how). The connection string is an env placeholder, never a literal.
#
# What is NOT here: any tenant. The query does not filter by whose bookings these are, and the
# grammar has no way to - the caller's identity reaches the database as a transaction-scoped setting
# that a row-level-security policy reads. Drop the policy and `archstone verify` goes red.

binding:
  capabilityId: wanderlust.bookings-by-city
  connector:
    type: sql
    sql:
      engine: postgres
      dsn: "${REPORTING_DSN}"
      statementKind: select
      query: |
        SELECT city, bookings
        FROM app.bookings_by_city_v
        WHERE month = $1
        ORDER BY bookings DESC, city
      params:
        - month

  # Row -> CityBookings. Anything the view returns and this does not name is dropped here.
  response:
    collection: "$[*]"
    resource: CityBookings
    map:
      city: "$.city"
      bookings: "$.bookings"

  # contract: the recorded shape of everything the backend answered (4 paths, most of them never mapped above) is in the file
local/reporting/wanderlust.CityBookings.resource.yaml
local/reporting/wanderlust.CityBookings.resource.yaml
resource:
  name: wanderlust.CityBookings
  description: The number of bookings one agency took in one city in one month.
  fields:
    city:
      type: text
      description: The city.
    bookings:
      type: quantity
      description: How many bookings.

Recording

Recorded with the core's recorder, CLI stamp 0.31.0 (the version when the content last changed), with a fixed clock (2027-05-01); machine-specific text is rewritten to placeholders (none needed). The file is vendored verbatim from the pinned commit: transcripts/s-15.json.

$ archstone apply examples/showcase/local/reportingexit 0

stdout (14 lines)

archstone apply examples/showcase/local/reporting

  company    Wanderlust Agency (synthetic) (wanderlust)
  providers  agency-warehouse
  declared   1 capabilities
  loaded     1 capability docs, 1 bindings
    ✓ wanderlust.bookings-by-city  [read] → agency-warehouse

  ✓ shapes valid
  semantic   0 error(s), 0 warning(s)
  registry   IR v0 — 1 capabilities, 1 invocable (bound)

  → run 'archstone serve examples/showcase/local/reporting' to expose 1 tool(s) to an AI agent over MCP

$ archstone verify examples/showcase/local/reporting --identity-map examples/showcase/local/reporting/identity-map.jsonexit 0

stdout (4 lines)

archstone verify examples/showcase/local/reporting

  🟢 wanderlust.bookings-by-city — fingerprint unchanged, mapping OK

call wanderlust_bookings-by-citydemo:analyst

Arguments
{
  "month": "2027-05"
}
Result (isError: false)
{
  "as": "demo:analyst",
  "isError": false,
  "structuredContent": {
    "cities": [
      {
        "bookings": 5,
        "city": "Lisbon"
      },
      {
        "bookings": 3,
        "city": "Porto"
      },
      {
        "bookings": 2,
        "city": "Seville"
      }
    ]
  }
}

call wanderlust_bookings-by-citydemo:outsider

Arguments
{
  "month": "2027-05"
}
Result (isError: false)
{
  "as": "demo:outsider",
  "isError": false,
  "structuredContent": {
    "cities": []
  }
}

call wanderlust_bookings-by-city(no caller)

Arguments
{
  "month": "2027-05"
}
Result (isError: true)
{
  "as": "(no caller)",
  "isError": true
}

What the recorder checked

  • `apply` compiles the local reporting manifest offline (exit 0)
  • `verify` exits 0: the recorded contract holds
  • the local manifest builds a registry with one tool
  • the analyst gets a table of bookings per city for the month: Lisbon 5, Porto 3, Seville 2
  • the reporting manifest lives outside the live one

What Archstone kept out, or did not do

  • `verify` replays the request as another tenant (the fixture names one) and is green only because that replay returned zero rows
  • the table has exactly the declared columns, city and bookings (the guest passport column is not in the view)
  • another agency's analyst asking the same question gets no rows: the database's row-level security answers, not the binding
  • a call with no caller identity is refused, not answered
  • the live manifest's tool list has no bookings-by-city tool
  • no capability in the live manifest is bound to sql
  • no file under the live manifest declares a sql connector

The test behind this

RecordedN-16

S-16What the AI can see, in one list

archstone apply --exposure prints, per capability, what a model sends, what it is shown and what it never sees: the declared fields on one side, and on the other the paths the recorded contract observed the backend returning that the manifest does not name. The recording below is the full report for the 13-capability manifest.

Recording

Recorded with the core's recorder, CLI stamp 0.32.0 (the version when the content last changed), with a fixed clock (2027-05-01); machine-specific text is rewritten to placeholders (none needed). The file is vendored verbatim from the pinned commit: transcripts/s-16.json.

$ archstone apply examples/showcase/manifest --exposureexit 0

stdout, 106 lines: show
stdout (106 lines)

archstone apply examples/showcase/manifest

  company    Wanderlust Agency (synthetic) (wanderlust)
  providers  wanderlust-api
  declared   13 capabilities
  loaded     13 capability docs, 13 bindings
    ✓ tourism.search-classic  [read] → wanderlust-api
    ✓ tourism.search  [read] → wanderlust-api
    ✓ wanderlust.availability  [read] → wanderlust-api
    ✓ wanderlust.book  [write] → wanderlust-api
    ✓ wanderlust.cancel  [irreversible] → wanderlust-api
    ✓ wanderlust.neighbourhood  [read] → wanderlust-api
    ✓ wanderlust.pay  [irreversible] → wanderlust-api
    ✓ wanderlust.quote  [write] → wanderlust-api
    ✓ wanderlust.room-status  [read] → wanderlust-api
    ✓ wanderlust.search  [read] → wanderlust-api
    ✓ wanderlust.stay-details  [read] → wanderlust-api
    ✓ wanderlust.stay-page  [read] → wanderlust-api
    ✓ wanderlust.stay-photos  [read] → wanderlust-api
  policies   4 policy document(s)
    ✓ wanderlust-availability-rate  → capability wanderlust.availability
    ✓ wanderlust-book-principals  → capability wanderlust.book
    ✓ wanderlust-cancel-principals  → capability wanderlust.cancel
    ✓ wanderlust-pay-principals  → capability wanderlust.pay

  ✓ shapes valid
  semantic   0 error(s), 2 warning(s)
    ⚠ capability 'wanderlust.cancel' is irreversible and declares policies:[human-approval], which this version does not enforce: no approval mechanism exists. An agent can run it without anyone approving. Put the approval step in the provider, or do not serve this capability to an agent unattended.
    ⚠ capability 'wanderlust.pay' is irreversible and declares policies:[human-approval], which this version does not enforce: no approval mechanism exists. An agent can run it without anyone approving. Put the approval step in the provider, or do not serve this capability to an agent unattended.
  registry   IR v0 — 13 capabilities, 13 invocable (bound)

  exposure   what a model sends, is shown, and never sees — names and types only
  totals     13 declared — 11 exposed, 1 unlisted, 1 not exposed

  tourism.search  [read]  exposed
    receives   budget (money, optional), dates (date-range), destination (location), preferences (preference-set, optional), travelers (party)
    exposes    stays[].location (location), stays[].name (text), stays[].pricePerNight (quantity), stays[].rating (quantity, optional), totalMatches (quantity, extract)
    withholds  $.stays[].boardType (string), $.stays[].commission (number), $.stays[].freeCancellationUntil (string), $.stays[].id (string), $.stays[].net (number), $.stays[].roomDescription (string)
               as observed in fixtures/tourism.search.golden.json (sha256:f5475f…)

  tourism.search-classic  [read]  not exposed (retired)
    receives   budget (money, optional), dates (date-range), destination (location), travelers (party)
    exposes    stays[].location (location), stays[].name (text), stays[].pricePerNight (quantity), stays[].rating (quantity, optional)
    withholds  unknown — no recorded contract

  wanderlust.availability  [read]  exposed
    receives   date (date), propertyId (identifier)
    exposes    availability.date (date), availability.pricePerNight (quantity), availability.propertyId (identifier), availability.roomsFree (quantity)
    withholds  unknown — no recorded contract

  wanderlust.book  [write]  exposed
    receives   dates (date-range), guestName (string), quoteId (identifier), stayId (identifier), travelers (party)
    exposes    booking.bookingId (identifier), booking.dates (date-range), booking.payBy (datetime), booking.paymentQuote (identifier), booking.petFee (money), booking.pets (quantity), booking.status (enum), booking.stayId (identifier), booking.total (money)
    withholds  unknown — no recorded contract

  wanderlust.cancel  [irreversible]  exposed
    receives   bookingId (identifier)
    exposes    cancellation.bookingId (identifier), cancellation.cancelledAt (datetime), cancellation.refund (money), cancellation.status (enum)
    withholds  unknown — no recorded contract

  wanderlust.neighbourhood  [read]  unlisted (experimental) — callable by id, not advertised
    receives   area (text)
    exposes    neighbourhood.area (text), neighbourhood.summary (text), neighbourhood.walkability (quantity, optional)
    withholds  $.localTips (array), $.localTips[] (string)
               as observed in fixtures/wanderlust.neighbourhood.golden.json (sha256:fa7430…)

  wanderlust.pay  [irreversible]  exposed
    receives   amount (money), bookingId (identifier), paymentQuote (identifier)
    exposes    payment.amount (money), payment.bookingId (identifier), payment.paidAt (datetime), payment.paymentId (identifier), payment.status (enum)
    withholds  unknown — no recorded contract

  wanderlust.quote  [write]  exposed
    receives   dates (date-range), pets (quantity, optional), stayId (identifier), travelers (party)
    exposes    quote.dates (date-range), quote.expiresAt (datetime), quote.nights (quantity), quote.petFee (money), quote.pets (quantity), quote.quoteId (identifier), quote.stayId (identifier), quote.total (money)
    withholds  unknown — no recorded contract

  wanderlust.room-status  [read]  exposed
    receives   date (date), propertyId (identifier)
    exposes    rooms[].code (text), rooms[].message (text, optional), rooms[].pricePerNight (quantity), rooms[].room (text), rooms[].status (enum)
    withholds  $.date (string), $.propertyId (string)
               as observed in fixtures/wanderlust.room-status.golden.json (sha256:d00727…)

  wanderlust.search  [read]  exposed
    receives   budget (money, optional), dates (date-range), destination (location), preferences (preference-set, optional), travelers (party)
    exposes    stays[].id (identifier), stays[].location (location), stays[].name (text), stays[].petPolicy (text, optional), stays[].pricePerNight (quantity), stays[].rating (quantity, optional), totalMatches (quantity, extract)
    withholds  $.stays[].commission (number), $.stays[].currency (string), $.stays[].description_html (string), $.stays[].hostContact (object), $.stays[].hostContact.name (string), $.stays[].hostContact.phone (string), $.stays[].margin (number), $.stays[].net (number)
               as observed in fixtures/wanderlust.search.golden.json (sha256:243302…)

  wanderlust.stay-details  [read]  exposed
    receives   stayId (identifier)
    exposes    stay.location (location), stay.name (text), stay.petPolicy (text, optional), stay.pricePerNight (quantity), stay.rating (quantity, optional), stay.rooms (wanderlust.Room[])
    withholds  $.commission (number), $.currency (string), $.description_html (string), $.guests (array), $.guests[] (object), $.guests[].email (string), $.guests[].name (string), $.guests[].passport (string), $.guests[].phone (string), $.id (string), $.margin (number), $.net (number)
               as observed in fixtures/wanderlust.stay-details.golden.json (sha256:1ed3fe…)

  wanderlust.stay-page  [read]  exposed
    receives   stayId (identifier)
    exposes    pages[].name (text), pages[].url (web-page, optional)
    withholds  unknown — no recorded contract

  wanderlust.stay-photos  [read]  exposed
    receives   stayId (identifier)
    exposes    gallery.photos (image[])
    withholds  unknown — no recorded contract

  → run 'archstone serve examples/showcase/manifest' to expose 13 tool(s) to an AI agent over MCP

What the recorder checked

  • `apply --exposure` exits 0 on the live manifest
  • the same report as JSON exits 0
  • the report covers all 13 capabilities
  • the exposed list is not empty (the check below is not vacuous)
  • `build` exits 0 on the live manifest
  • the manifest declares at least 30 resource fields in total
  • the backend was observed returning 'margin' (listed under withholds, never exposed)
  • the backend was observed returning 'passport' (listed under withholds, never exposed)
  • the backend was observed returning 'phone' (listed under withholds, never exposed)
  • the backend was observed returning 'description_html' (listed under withholds, never exposed)

What Archstone kept out, or did not do

  • no exposed field is named or nested under 'margin'
  • no exposed field is named or nested under 'passport'
  • no exposed field is named or nested under 'phone'
  • no exposed field is named or nested under 'description_html'
  • no capability in the list deletes anything
  • no resource declares a field named 'margin'
  • no resource declares a field named 'passport'
  • no resource declares a field named 'phone'
  • no resource declares a field named 'description_html'
  • no outbound connection was attempted

The same question, asked of the published CLI

The transcript above was recorded with the core's recorder (CLI stamp 0.32.0). As a cross-check, the page's check runs archstone apply --exposure --json with the published @archstone/cli 0.33.0 on the same manifest: 13 capabilities, 63 exposed paths, and none of them is named margin, passport, phone, description_html, net, commission, guests, email. For wanderlust.search it exposes stays[].id, stays[].location, stays[].name, stays[].petPolicy, stays[].pricePerNight, stays[].rating, totalMatches and lists as observed in the backend's answer but not exposed:

wanderlust.search: observed, not exposed
$.stays[].commission
$.stays[].currency
$.stays[].description_html
$.stays[].hostContact
$.stays[].hostContact.name
$.stays[].hostContact.phone
$.stays[].margin
$.stays[].net

The test behind this

  • recorded-s16.test.ts#L20 — recorded S-16: nothing named margin, passport, phone or description_html is exposed; no capability deletes

RecordedPart locked · planned · #178N-17

S-17The agency added a field: verify names it, adopt needs a person

The recorder wraps the synthetic API in its own server to add a guestEmail field (no flag exists on the API itself). archstone verify replays the recorded contract against the backend and names the gained field; archstone adopt is the interactive step where a person decides to declare it. With nobody at the keyboard it writes nothing and exits non-zero. The typed answers of the person (typed) are part of the recording because they are where the decision lives.

Recording

Recorded with the core's recorder, CLI stamp 0.32.0 (the version when the content last changed), with a fixed clock (2027-05-01); machine-specific text is rewritten to placeholders (<manifest>). The file is vendored verbatim from the pinned commit: transcripts/s-17.json.

$ archstone verify <manifest>exit 0

stdout (9 lines)

archstone verify <manifest>

  🟢 tourism.search — fingerprint unchanged, mapping OK
  🟢 wanderlust.neighbourhood — fingerprint unchanged, mapping OK
  🟢 wanderlust.room-status — fingerprint unchanged, mapping OK
  🟢 wanderlust.search — fingerprint unchanged, mapping OK
  🟢 wanderlust.stay-details — fingerprint unchanged, mapping OK
      nested keys not declared (dropped): rooms.amenities.host, rooms.amenities.lastUsedBy, rooms.amenities.history, rooms.id, rooms.net, rooms.margin, rooms.commission, rooms.guests

$ archstone verify <manifest>exit 0

stdout (9 lines)

archstone verify <manifest>

  🟢 tourism.search — fingerprint unchanged, mapping OK
  🟢 wanderlust.neighbourhood — fingerprint unchanged, mapping OK
  🟢 wanderlust.room-status — fingerprint unchanged, mapping OK
  🟢 wanderlust.search — fingerprint unchanged, mapping OK
  🟡 wanderlust.stay-details — mapping still resolves; response shape gained 1 field(s): $.guestEmail (string)
      nested keys not declared (dropped): rooms.amenities.host, rooms.amenities.lastUsedBy, rooms.amenities.history, rooms.id, rooms.net, rooms.margin, rooms.commission, rooms.guests

$ archstone adopt <manifest>exit 1

Nobody at the keyboard (typed: []).

stdout (13 lines)

archstone adopt <manifest>

  tourism.search — nothing to adopt.

  wanderlust.neighbourhood — nothing to adopt.

  wanderlust.room-status — nothing to adopt.

  wanderlust.search — nothing to adopt.

  wanderlust.stay-details — 1 field(s) the backend returns and the manifest does not declare:
    · $.guestEmail (string) → guestEmail: text
stderr (2 lines)

archstone adopt: no more input — nothing written. Adoption needs a person (PromptUnavailable).

$ archstone adopt <manifest>exit 0

A person typed: y, The lead guest's email address, as the agency holds it.

stdout (17 lines)

archstone adopt <manifest>

  tourism.search — nothing to adopt.

  wanderlust.neighbourhood — nothing to adopt.

  wanderlust.room-status — nothing to adopt.

  wanderlust.search — nothing to adopt.

  wanderlust.stay-details — 1 field(s) the backend returns and the manifest does not declare:
    · $.guestEmail (string) → guestEmail: text

  Declare guestEmail (text)? [y/N]   Describe guestEmail — an agent reads this to decide whether to use it:
  > 
  wanderlust.stay-details — declared guestEmail; contract re-recorded.

$ archstone verify <manifest>exit 0

stdout (9 lines)

archstone verify <manifest>

  🟢 tourism.search — fingerprint unchanged, mapping OK
  🟢 wanderlust.neighbourhood — fingerprint unchanged, mapping OK
  🟢 wanderlust.room-status — fingerprint unchanged, mapping OK
  🟢 wanderlust.search — fingerprint unchanged, mapping OK
  🟢 wanderlust.stay-details — fingerprint unchanged, mapping OK
      nested keys not declared (dropped): rooms.amenities.host, rooms.amenities.lastUsedBy, rooms.amenities.history, rooms.id, rooms.net, rooms.margin, rooms.commission, rooms.guests

What the recorder checked

  • before the change, `verify` exits 0 (the contract holds)
  • strict verify (exit non-zero on a gained field): not available in this version - #178
  • after the change, `verify` names the new field: a yellow reading on wanderlust.stay-details, "gained 1 field(s): $.guestEmail"
  • and the other capabilities' readings did not change (no other line is yellow or red)
  • `adopt` with nobody at the keyboard exits non-zero
  • `adopt` with a person's answers exits 0
  • it reports the field declared and the contract re-recorded
  • the field is in the exposure now that a person declared it
  • `verify` exits 0 again once the field is declared

What Archstone kept out, or did not do

  • `verify` is a read-only check: the manifest is byte-identical after it
  • the field is absent from the exposure while undeclared
  • `adopt` with nobody at the keyboard writes nothing
  • the field is still absent from the exposure
Strict verify: planned (NousVigil/archstone#178) NousVigil/archstone#178

One part of this scenario is locked; the rest above is a real recording.

Known deviation

In this version a gained field is a yellow reading: verify names it and exits 0. Only a lost field, a changed type or a missing required value are red and exit 1. An opt-in strict mode that exits non-zero on a gained field is not available; it is tracked in NousVigil/archstone#178. The transcript says so in a claim. Separately, adopt used to resolve a resource by its bare name, and this manifest has two resources called Stay (#177, fixed in 0.32.0); the recording predates the fix and still runs adopt on wanderlust.stay-details.

The test behind this

  • recorded-s17.test.ts#L17 — recorded S-17: verify names the field, adopt needs a person, the field is absent until declared

RecordedN-18

S-18What changed between two declarations, with the backend stopped

archstone diff compares two declarations, not backends. It names one added visible field (wanderlust.Stay.rating) and one added action (wanderlust.quote) and classifies each as compatible; the recorder runs it with the backend stopped and checks that it made no connection.

Recording

Recorded with the core's recorder, CLI stamp 0.32.0 (the version when the content last changed), with a fixed clock (2027-05-01); machine-specific text is rewritten to placeholders (<before>). The file is vendored verbatim from the pinned commit: transcripts/s-18.json.

$ archstone diff <before> examples/showcase/manifest --allexit 0

stdout (10 lines)
archstone diff compares declarations, not backends — run 'archstone verify' for the backend.

  before  <before> (wanderlust)
  after   examples/showcase/manifest (wanderlust)

  compatible wanderlust.Stay — field 'rating' added (optional, quantity) (reaches wanderlust.search)
  compatible wanderlust.quote — capability added (write)
  compatible wanderlust.search — binding response mapping changed

  0 breaking, 0 notable, 3 compatible — exit 0

What the recorder checked

  • the earlier declaration is itself a valid manifest
  • `diff` exits 0: nothing in the change is breaking
  • it names exactly one added visible field: wanderlust.Stay.rating
  • it names exactly one added action: wanderlust.quote

What Archstone kept out, or did not do

  • it reports no breaking change
  • the backend is stopped: the address the commands were given refuses a connection
  • no outbound connection was attempted by `diff`

The test behind this

RecordedN-19

S-19audit and doctor, sending nothing anywhere

The recorder runs five real executions through the embedded SDK (search, quote, book, cancel, pay) into an audit trail, then runs archstone doctor, archstone audit (as a table and as CSV) and apply --exposure. audit lists what ran, including the two irreversible actions; the exposed fields in the recording come from apply --exposure, not from audit. The commands run under a guard that makes any socket, DNS lookup or fetch fail and be logged, so "zero outbound requests" is checked on the process, not only on the backend's counter.

Recording

Recorded with the core's recorder, CLI stamp 0.32.0 (the version when the content last changed), with a fixed clock (2027-05-01); machine-specific text is rewritten to placeholders (<work>). The file is vendored verbatim from the pinned commit: transcripts/s-19.json.

call wanderlust.searchundefined

Arguments
{
  "dates": {
    "from": "2027-05-12",
    "to": "2027-05-15"
  },
  "destination": "Lisbon",
  "travelers": {
    "adults": 2
  }
}
Result (isError: undefined)
{
  "status": "ok"
}

call wanderlust.quoteundefined

Arguments
{
  "dates": {
    "from": "2027-05-12",
    "to": "2027-05-15"
  },
  "stayId": "ws-1001",
  "travelers": {
    "adults": 2
  }
}
Result (isError: undefined)
{
  "status": "ok"
}

call wanderlust.bookundefined

Arguments
{
  "dates": {
    "from": "2027-05-12",
    "to": "2027-05-15"
  },
  "guestName": "Ana Pop",
  "quoteId": "Q-n34aqj6o-8902ed05",
  "stayId": "ws-1001",
  "travelers": {
    "adults": 2
  }
}
Result (isError: undefined)
{
  "status": "ok"
}

call wanderlust.cancelundefined

Arguments
{
  "bookingId": "B-0000cafe"
}
Result (isError: undefined)
{
  "status": "ok"
}

call wanderlust.payundefined

Arguments
{
  "amount": {
    "amount": 354,
    "currency": "EUR"
  },
  "bookingId": "B-0162f95c1506",
  "paymentQuote": "PQ-n34aqj6o-61e32b76"
}
Result (isError: undefined)
{
  "status": "ok"
}

$ archstone doctor examples/showcase/manifestexit 0

stdout, 124 lines: show
stdout (124 lines)

archstone doctor examples/showcase/manifest

🟡 tourism.search-classic — bound, but records no contract fixture
   `archstone verify` replays a recorded fixture against the live backend. With no fixture there is nothing to replay, so backend drift is found by an agent, in front of a customer, instead of by CI.
   (no-contract)

🟡 wanderlust.availability — bound, but records no contract fixture
   `archstone verify` replays a recorded fixture against the live backend. With no fixture there is nothing to replay, so backend drift is found by an agent, in front of a customer, instead of by CI.
   (no-contract)

🟡 wanderlust.cancel — is irreversible and declares policies:[human-approval], which this version does not enforce: no approval mechanism exists.
   An agent can run it without anyone approving. Put the approval step in the provider, or do not serve this capability to an agent unattended.
   (irreversible-unenforced-policy)

🟡 wanderlust.pay — is irreversible and declares policies:[human-approval], which this version does not enforce: no approval mechanism exists.
   An agent can run it without anyone approving. Put the approval step in the provider, or do not serve this capability to an agent unattended.
   (irreversible-unenforced-policy)

🟡 wanderlust.stay-page — bound, but records no contract fixture
   `archstone verify` replays a recorded fixture against the live backend. With no fixture there is nothing to replay, so backend drift is found by an agent, in front of a customer, instead of by CI.
   (no-contract)

🟡 wanderlust.stay-photos — bound, but records no contract fixture
   `archstone verify` replays a recorded fixture against the live backend. With no fixture there is nothing to replay, so backend drift is found by an agent, in front of a customer, instead of by CI.
   (no-contract)

🔵 tourism.search-classic — baseUrl comes from the environment (${SHOWCASE_API_URL})
   Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
   (env-baseurl)

🔵 tourism.search — baseUrl comes from the environment (${SHOWCASE_API_URL})
   Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
   (env-baseurl)

🔵 wanderlust.availability — baseUrl comes from the environment (${SHOWCASE_API_URL})
   Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
   (env-baseurl)

🔵 wanderlust.availability — declares a rate limit, which needs a counter supplied at runtime
   With no counter the call is denied, fail-closed, at the first invocation. On more than one instance the counter must be shared, or a declared 100/min becomes 100/min per instance.
   (ratelimit-needs-counter)

🔵 wanderlust.book — bound and `write`, so it records no contract fixture — and should not
   `archstone verify` replays a recorded fixture as a real invocation, so a fixture here would repeat this capability's effect against the live backend on every CI run. `verify` skips it by default for that reason. Where this capability has a `read` counterpart, cover the drift with that instead — the quote half of a quote → commit pair hits the same host, auth and serialization at zero risk. Not every write has one, and Archstone cannot tell you which capability it is: nothing in CDL declares that relationship. Only if this binding's `${VAR}` genuinely resolves to a sandbox tenant is recording one worthwhile, replayed with `archstone verify --sandbox`: the flag re-includes the binding, it does not make the backend safe.
   (no-contract-non-read)

🔵 wanderlust.book — baseUrl comes from the environment (${SHOWCASE_API_URL})
   Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
   (env-baseurl)

🔵 wanderlust.book — requires an authenticated caller
   The surface serving it must carry a per-request principal — `resolveCaller` on HTTP or the embedded SDK. `archstone serve` (stdio) has one static caller for the whole process, so it cannot serve this capability to more than one identity.
   (authenticated-needs-principal)

🔵 wanderlust.cancel — bound and `irreversible`, so it records no contract fixture — and should not
   `archstone verify` replays a recorded fixture as a real invocation, so a fixture here would repeat this capability's effect against the live backend on every CI run. `verify` skips it by default for that reason. Where this capability has a `read` counterpart, cover the drift with that instead — the quote half of a quote → commit pair hits the same host, auth and serialization at zero risk. Not every write has one, and Archstone cannot tell you which capability it is: nothing in CDL declares that relationship. Only if this binding's `${VAR}` genuinely resolves to a sandbox tenant is recording one worthwhile, replayed with `archstone verify --sandbox`: the flag re-includes the binding, it does not make the backend safe.
   (no-contract-non-read)

🔵 wanderlust.cancel — baseUrl comes from the environment (${SHOWCASE_API_URL})
   Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
   (env-baseurl)

🔵 wanderlust.cancel — is declared `irreversible`
   No API description states this, so it was a human judgement: an agent must confirm explicitly and must never auto-retry. Re-read it before go-live — `irreversible` is the difference between looking up a price and charging a card. `archstone verify` applies the same judgement: it will not replay this capability's fixture against the live backend unless you assert a sandbox with --sandbox.
   (irreversible-effect)

🔵 wanderlust.cancel — requires an authenticated caller
   The surface serving it must carry a per-request principal — `resolveCaller` on HTTP or the embedded SDK. `archstone serve` (stdio) has one static caller for the whole process, so it cannot serve this capability to more than one identity.
   (authenticated-needs-principal)

🔵 wanderlust.neighbourhood — baseUrl comes from the environment (${SHOWCASE_API_URL})
   Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
   (env-baseurl)

🔵 wanderlust.neighbourhood — is `experimental`: hidden from tool listings but still invocable by id
   Deliberate behaviour, and easy to forget: an agent that knows the id can still call it. Confirm that is what you want in production.
   (experimental-capability)

🔵 wanderlust.pay — bound and `irreversible`, so it records no contract fixture — and should not
   `archstone verify` replays a recorded fixture as a real invocation, so a fixture here would repeat this capability's effect against the live backend on every CI run. `verify` skips it by default for that reason. Where this capability has a `read` counterpart, cover the drift with that instead — the quote half of a quote → commit pair hits the same host, auth and serialization at zero risk. Not every write has one, and Archstone cannot tell you which capability it is: nothing in CDL declares that relationship. Only if this binding's `${VAR}` genuinely resolves to a sandbox tenant is recording one worthwhile, replayed with `archstone verify --sandbox`: the flag re-includes the binding, it does not make the backend safe.
   (no-contract-non-read)

🔵 wanderlust.pay — baseUrl comes from the environment (${SHOWCASE_API_URL})
   Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
   (env-baseurl)

🔵 wanderlust.pay — is declared `irreversible`
   No API description states this, so it was a human judgement: an agent must confirm explicitly and must never auto-retry. Re-read it before go-live — `irreversible` is the difference between looking up a price and charging a card. `archstone verify` applies the same judgement: it will not replay this capability's fixture against the live backend unless you assert a sandbox with --sandbox.
   (irreversible-effect)

🔵 wanderlust.pay — requires an authenticated caller
   The surface serving it must carry a per-request principal — `resolveCaller` on HTTP or the embedded SDK. `archstone serve` (stdio) has one static caller for the whole process, so it cannot serve this capability to more than one identity.
   (authenticated-needs-principal)

🔵 wanderlust.quote — bound and `write`, so it records no contract fixture — and should not
   `archstone verify` replays a recorded fixture as a real invocation, so a fixture here would repeat this capability's effect against the live backend on every CI run. `verify` skips it by default for that reason. Where this capability has a `read` counterpart, cover the drift with that instead — the quote half of a quote → commit pair hits the same host, auth and serialization at zero risk. Not every write has one, and Archstone cannot tell you which capability it is: nothing in CDL declares that relationship. Only if this binding's `${VAR}` genuinely resolves to a sandbox tenant is recording one worthwhile, replayed with `archstone verify --sandbox`: the flag re-includes the binding, it does not make the backend safe.
   (no-contract-non-read)

🔵 wanderlust.quote — baseUrl comes from the environment (${SHOWCASE_API_URL})
   Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
   (env-baseurl)

🔵 wanderlust.room-status — baseUrl comes from the environment (${SHOWCASE_API_URL})
   Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
   (env-baseurl)

🔵 wanderlust.search — baseUrl comes from the environment (${SHOWCASE_API_URL})
   Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
   (env-baseurl)

🔵 wanderlust.stay-details — baseUrl comes from the environment (${SHOWCASE_API_URL})
   Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
   (env-baseurl)

🔵 wanderlust.stay-page — baseUrl comes from the environment (${SHOWCASE_API_URL})
   Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
   (env-baseurl)

🔵 wanderlust.stay-photos — baseUrl comes from the environment (${SHOWCASE_API_URL})
   Nothing wrong with it — but the deployment, not the manifest, decides where this capability points. Confirm the variable is set to the intended backend in every environment that runs it.
   (env-baseurl)

13 capabilities checked — 0 error · 6 warning · 24 advisory.

$ archstone audit <work>/trail.jsonlexit 0

stdout (16 lines)
5 records
  from  2027-05-01T10:07:00.000Z
  to    2027-05-01T10:07:04.000Z

By outcome
  succeeded       5

By capability
  wanderlust.book         1
  wanderlust.cancel       1
  wanderlust.pay          1
  wanderlust.quote        1
  wanderlust.search       1

By principal
  demo:visitor       5

$ archstone audit <work>/trail.jsonl --format csvexit 0

stdout (6 lines)
startedAt,completedAt,capabilityId,provider,phase,denialReason,principal,consumer,policyRuleIds,sessionId,id
2027-05-01T10:07:00.000Z,2027-05-01T10:07:00.001Z,wanderlust.search,wanderlust-api,succeeded,,demo:visitor,function-calling,,,exec-001
2027-05-01T10:07:01.000Z,2027-05-01T10:07:01.001Z,wanderlust.quote,wanderlust-api,succeeded,,demo:visitor,function-calling,,,exec-002
2027-05-01T10:07:02.000Z,2027-05-01T10:07:02.001Z,wanderlust.book,wanderlust-api,succeeded,,demo:visitor,function-calling,wanderlust-book-principals,,exec-003
2027-05-01T10:07:03.000Z,2027-05-01T10:07:03.001Z,wanderlust.cancel,wanderlust-api,succeeded,,demo:visitor,function-calling,wanderlust-cancel-principals,,exec-004
2027-05-01T10:07:04.000Z,2027-05-01T10:07:04.001Z,wanderlust.pay,wanderlust-api,succeeded,,demo:visitor,function-calling,wanderlust-pay-principals,,exec-005

$ archstone apply examples/showcase/manifest --exposureexit 0

stdout, 106 lines: show
stdout (106 lines)

archstone apply examples/showcase/manifest

  company    Wanderlust Agency (synthetic) (wanderlust)
  providers  wanderlust-api
  declared   13 capabilities
  loaded     13 capability docs, 13 bindings
    ✓ tourism.search-classic  [read] → wanderlust-api
    ✓ tourism.search  [read] → wanderlust-api
    ✓ wanderlust.availability  [read] → wanderlust-api
    ✓ wanderlust.book  [write] → wanderlust-api
    ✓ wanderlust.cancel  [irreversible] → wanderlust-api
    ✓ wanderlust.neighbourhood  [read] → wanderlust-api
    ✓ wanderlust.pay  [irreversible] → wanderlust-api
    ✓ wanderlust.quote  [write] → wanderlust-api
    ✓ wanderlust.room-status  [read] → wanderlust-api
    ✓ wanderlust.search  [read] → wanderlust-api
    ✓ wanderlust.stay-details  [read] → wanderlust-api
    ✓ wanderlust.stay-page  [read] → wanderlust-api
    ✓ wanderlust.stay-photos  [read] → wanderlust-api
  policies   4 policy document(s)
    ✓ wanderlust-availability-rate  → capability wanderlust.availability
    ✓ wanderlust-book-principals  → capability wanderlust.book
    ✓ wanderlust-cancel-principals  → capability wanderlust.cancel
    ✓ wanderlust-pay-principals  → capability wanderlust.pay

  ✓ shapes valid
  semantic   0 error(s), 2 warning(s)
    ⚠ capability 'wanderlust.cancel' is irreversible and declares policies:[human-approval], which this version does not enforce: no approval mechanism exists. An agent can run it without anyone approving. Put the approval step in the provider, or do not serve this capability to an agent unattended.
    ⚠ capability 'wanderlust.pay' is irreversible and declares policies:[human-approval], which this version does not enforce: no approval mechanism exists. An agent can run it without anyone approving. Put the approval step in the provider, or do not serve this capability to an agent unattended.
  registry   IR v0 — 13 capabilities, 13 invocable (bound)

  exposure   what a model sends, is shown, and never sees — names and types only
  totals     13 declared — 11 exposed, 1 unlisted, 1 not exposed

  tourism.search  [read]  exposed
    receives   budget (money, optional), dates (date-range), destination (location), preferences (preference-set, optional), travelers (party)
    exposes    stays[].location (location), stays[].name (text), stays[].pricePerNight (quantity), stays[].rating (quantity, optional), totalMatches (quantity, extract)
    withholds  $.stays[].boardType (string), $.stays[].commission (number), $.stays[].freeCancellationUntil (string), $.stays[].id (string), $.stays[].net (number), $.stays[].roomDescription (string)
               as observed in fixtures/tourism.search.golden.json (sha256:f5475f…)

  tourism.search-classic  [read]  not exposed (retired)
    receives   budget (money, optional), dates (date-range), destination (location), travelers (party)
    exposes    stays[].location (location), stays[].name (text), stays[].pricePerNight (quantity), stays[].rating (quantity, optional)
    withholds  unknown — no recorded contract

  wanderlust.availability  [read]  exposed
    receives   date (date), propertyId (identifier)
    exposes    availability.date (date), availability.pricePerNight (quantity), availability.propertyId (identifier), availability.roomsFree (quantity)
    withholds  unknown — no recorded contract

  wanderlust.book  [write]  exposed
    receives   dates (date-range), guestName (string), quoteId (identifier), stayId (identifier), travelers (party)
    exposes    booking.bookingId (identifier), booking.dates (date-range), booking.payBy (datetime), booking.paymentQuote (identifier), booking.petFee (money), booking.pets (quantity), booking.status (enum), booking.stayId (identifier), booking.total (money)
    withholds  unknown — no recorded contract

  wanderlust.cancel  [irreversible]  exposed
    receives   bookingId (identifier)
    exposes    cancellation.bookingId (identifier), cancellation.cancelledAt (datetime), cancellation.refund (money), cancellation.status (enum)
    withholds  unknown — no recorded contract

  wanderlust.neighbourhood  [read]  unlisted (experimental) — callable by id, not advertised
    receives   area (text)
    exposes    neighbourhood.area (text), neighbourhood.summary (text), neighbourhood.walkability (quantity, optional)
    withholds  $.localTips (array), $.localTips[] (string)
               as observed in fixtures/wanderlust.neighbourhood.golden.json (sha256:fa7430…)

  wanderlust.pay  [irreversible]  exposed
    receives   amount (money), bookingId (identifier), paymentQuote (identifier)
    exposes    payment.amount (money), payment.bookingId (identifier), payment.paidAt (datetime), payment.paymentId (identifier), payment.status (enum)
    withholds  unknown — no recorded contract

  wanderlust.quote  [write]  exposed
    receives   dates (date-range), pets (quantity, optional), stayId (identifier), travelers (party)
    exposes    quote.dates (date-range), quote.expiresAt (datetime), quote.nights (quantity), quote.petFee (money), quote.pets (quantity), quote.quoteId (identifier), quote.stayId (identifier), quote.total (money)
    withholds  unknown — no recorded contract

  wanderlust.room-status  [read]  exposed
    receives   date (date), propertyId (identifier)
    exposes    rooms[].code (text), rooms[].message (text, optional), rooms[].pricePerNight (quantity), rooms[].room (text), rooms[].status (enum)
    withholds  $.date (string), $.propertyId (string)
               as observed in fixtures/wanderlust.room-status.golden.json (sha256:d00727…)

  wanderlust.search  [read]  exposed
    receives   budget (money, optional), dates (date-range), destination (location), preferences (preference-set, optional), travelers (party)
    exposes    stays[].id (identifier), stays[].location (location), stays[].name (text), stays[].petPolicy (text, optional), stays[].pricePerNight (quantity), stays[].rating (quantity, optional), totalMatches (quantity, extract)
    withholds  $.stays[].commission (number), $.stays[].currency (string), $.stays[].description_html (string), $.stays[].hostContact (object), $.stays[].hostContact.name (string), $.stays[].hostContact.phone (string), $.stays[].margin (number), $.stays[].net (number)
               as observed in fixtures/wanderlust.search.golden.json (sha256:243302…)

  wanderlust.stay-details  [read]  exposed
    receives   stayId (identifier)
    exposes    stay.location (location), stay.name (text), stay.petPolicy (text, optional), stay.pricePerNight (quantity), stay.rating (quantity, optional), stay.rooms (wanderlust.Room[])
    withholds  $.commission (number), $.currency (string), $.description_html (string), $.guests (array), $.guests[] (object), $.guests[].email (string), $.guests[].name (string), $.guests[].passport (string), $.guests[].phone (string), $.id (string), $.margin (number), $.net (number)
               as observed in fixtures/wanderlust.stay-details.golden.json (sha256:1ed3fe…)

  wanderlust.stay-page  [read]  exposed
    receives   stayId (identifier)
    exposes    pages[].name (text), pages[].url (web-page, optional)
    withholds  unknown — no recorded contract

  wanderlust.stay-photos  [read]  exposed
    receives   stayId (identifier)
    exposes    gallery.photos (image[])
    withholds  unknown — no recorded contract

  → run 'archstone serve examples/showcase/manifest' to expose 13 tool(s) to an AI agent over MCP

What the recorder checked

  • `build` exits 0 (the IR the embedded run loads)
  • search ran (status ok)
  • quote ran (status ok)
  • book ran (status ok)
  • cancel (irreversible) ran (status ok)
  • pay (irreversible) ran (status ok)
  • the trail holds one record per execution
  • `doctor` exits 0: it reports health, it found no error
  • `doctor` reports a summary of what it checked (13 capabilities, 0 errors)
  • `doctor` names wanderlust.cancel as irreversible and its approval token as unenforced
  • `doctor` names wanderlust.pay as irreversible and its approval token as unenforced
  • `audit` exits 0
  • `audit` counts the five executions
  • `audit` lists the irreversible actions that ran: wanderlust.cancel and wanderlust.pay
  • `apply --exposure` lists the exposed fields (exit 0)

What Archstone kept out, or did not do

  • the backend received zero requests while these commands ran
  • no command attempted any outbound connection (socket, DNS or fetch)

The test behind this

RecordedN-20

S-20Start from the description of an existing API

archstone init reads an OpenAPI document and proposes one candidate per operation (16 here). It never defaults an effect: --non-interactive without a decisions file refuses and writes nothing. With a decisions file (the person's answers, shown below) it emits only the three confirmed capabilities; the DELETE operation was proposed and declined, and the draft mentions no passport, phone, email, margin, commission or raw HTML. It publishes nothing and makes no outbound connection.

The person's decisions

record/s20-decisions.json
{
  "version": "0",
  "company": {
    "id": "wanderlust",
    "name": "Wanderlust Agency (synthetic)",
    "description": "An invented travel agency; this draft is what `archstone init` proposes from its OpenAPI document."
  },
  "provider": "wanderlust-api",
  "baseUrlEnvVar": "SHOWCASE_API_URL",
  "decisions": [
    {
      "operation": "POST /v1/stays/search",
      "keep": true,
      "capabilityId": "wanderlust.search",
      "effect": "read"
    },
    {
      "operation": "GET /v1/stays/{stayId}",
      "keep": true,
      "capabilityId": "wanderlust.stay-details",
      "effect": "read"
    },
    {
      "operation": "GET /v1/availability",
      "keep": true,
      "capabilityId": "wanderlust.availability",
      "effect": "read"
    },
    {
      "operation": "DELETE /v1/guests/{name}/bookings",
      "keep": false,
      "note": "A delete function exists in the agency's system. A person declined it; nothing offers it to an AI."
    }
  ]
}

Recording

Recorded with the core's recorder, CLI stamp 0.32.0 (the version when the content last changed), with a fixed clock (2027-05-01); machine-specific text is rewritten to placeholders (<refused>, <draft>). The file is vendored verbatim from the pinned commit: transcripts/s-20.json.

$ archstone init examples/showcase/api/wanderlust.openapi.yaml --out <draft> --decisions examples/showcase/record/s20-decisions.json --non-interactiveexit 0

stdout, 47 lines: show
stdout (47 lines)

archstone init — examples/showcase/api/wanderlust.openapi.yaml (openapi adapter)

Wrote 7 file(s) to <draft>, and the shipped compiler compiled them.

Candidates: 16 proposed, 3 emitted, 1 not emitted.
  ✓ wanderlust.search  [read]  POST /v1/stays/search (no resource — untyped output)
  ✓ wanderlust.stay-details  [read]  GET /v1/stays/{stayId} (no resource — untyped output)
  ✓ wanderlust.availability  [read]  GET /v1/availability (no resource — untyped output)

  Declined by you (1) — most operations in a spec are not capabilities:
    - DELETE /v1/guests/{name}/bookings — A delete function exists in the agency's system. A person declined it; nothing offers it to an AI.

What did not survive the trip — read these before committing:
  no-response-shape [operation] (3) — no usable response shape — emitted without a resource, output or response mapping
    - wanderlust.search
    - wanderlust.stay-details
    - wanderlust.availability
  failures-not-emitted [operation] (1) — error responses carry no derivable business meaning — no `failures:` emitted
    - GET /v1/stays/{stayId}: error responses declared: 404
  contract-not-recorded [operation] (3) — no probe ran — no fixture, no contract
    - wanderlust.search
    - wanderlust.stay-details
    - wanderlust.availability
  semantic-type-degraded [field] (5) — semantic type fell back to string/text
    - POST /v1/stays/search: POST /v1/stays/search/requestBody/content/application/json/schema/properties/destination: a bare `type: string` implies no richer type
    - POST /v1/stays/search: #/components/schemas/Money/properties/currency: a bare `type: string` implies no richer type
    - POST /v1/stays/search: POST /v1/stays/search/requestBody/content/application/json/schema/properties/preferences/items: a bare `type: string` implies no richer type
    - GET /v1/stays/{stayId}: GET /v1/stays/{stayId}/parameters/0: a bare `type: string` implies no richer type
    - GET /v1/availability: GET /v1/availability/parameters/0: a bare `type: string` implies no richer type
  input-property-omitted [field] (3) — an optional input the connector cannot carry was left out — the agent cannot set it
    - POST /v1/stays/search#dates: object
    - POST /v1/stays/search#travelers: object
    - POST /v1/stays/search#budget: object
  identity-ref-not-inferred [field] (2) — emitted as `identifier`; this may be a `ref:` to a resource — only you know
    - wanderlust.stay-details#stayId: if this identifies a resource another capability returns, change `type: identifier` to `ref: <Resource>`
    - wanderlust.availability#propertyId: if this identifies a resource another capability returns, change `type: identifier` to `ref: <Resource>`

What no tool could infer for you, and what to check by hand:
  - Every `description:` is the source's own words. An agent reads them to decide
    whether to call you. They are the highest-leverage thing to rewrite.
  - Every `response.map` line carries the field it came from and a real example.
    A mapping can be structurally right and semantically wrong; only you can see that.
  - An input typed `identifier` may really be a `ref:` to a resource another
    capability returns. No source construct says so — only you know.

Report also written to <draft>/INIT-REPORT.md

$ archstone init examples/showcase/api/wanderlust.openapi.yaml --out <refused> --non-interactiveexit 2

stderr (1 lines)
archstone init: --non-interactive requires --decisions <file>. `init` never defaults an `effect`.

What the recorder checked

  • `init` exits 0
  • it proposed 16 candidates, one per operation in the document
  • it emitted exactly the 3 confirmed capabilities
  • `init --non-interactive` without --decisions refuses (non-zero): it never defaults an effect
  • the draft has a capability file for each confirmed capability and no other
  • the DELETE operation is in the document, so it was proposed, and the person declined it
  • it wrote only inside --out: capabilities.yaml, the capability and binding files, and its report
  • the draft compiles with the shipped compiler (`apply` exits 0)

What Archstone kept out, or did not do

  • and it wrote nothing: the output directory does not exist
  • the draft contains no delete action: no DELETE method, no capability or path that deletes
  • the draft mentions no 'passport'
  • the draft mentions no 'phone'
  • the draft mentions no 'email'
  • the draft mentions no 'margin'
  • the draft mentions no 'commission'
  • the draft mentions no 'description_html'
  • no capability was drafted with a response mapping or a recorded contract (no probe ran)
  • nothing was published: `init` made no outbound connection of any kind

The test behind this

RecordedN-21

S-21The embedded SDK: the same tools, no MCP server, no AI app

@archstone/agent loads the compiled IR with fromIR, hands the same capabilities to three vendors' tool-calling formats with tools(), and runs them with execute() through the same projection the MCP server uses. The script below is the whole example; it exits 1 and names what failed if any of its own expectations does not hold, and the page's check runs it against the published packages on every change.

Recording

Recorded with the core's recorder, CLI stamp 0.32.0 (the version when the content last changed), with a fixed clock (2027-05-01); machine-specific text is rewritten to placeholders (<api>, <work>). The file is vendored verbatim from the pinned commit: transcripts/s-21.json.

$ archstone build examples/showcase/manifest --out <work>/ir.jsonexit 0

stdout (1 lines)
archstone build examples/showcase/manifest → <work>/ir.json (13 tool(s))

$ node examples/showcase/sdk/embedded.mjs --ir <work>/ir.json --api <api>exit 0

stdout, 52 lines: show
stdout (52 lines)
{
  "vendorShapes": {
    "anthropic": {
      "tools": 11,
      "envelopeKeys": [
        "description",
        "input_schema",
        "name"
      ]
    },
    "openai-chat": {
      "tools": 11,
      "envelopeKeys": [
        "function",
        "type"
      ]
    },
    "gemini": {
      "tools": 11,
      "envelopeKeys": [
        "description",
        "name",
        "parameters"
      ]
    }
  },
  "toolNames": [
    "tourism_search",
    "wanderlust_availability",
    "wanderlust_book",
    "wanderlust_cancel",
    "wanderlust_pay",
    "wanderlust_quote",
    "wanderlust_room-status",
    "wanderlust_search",
    "wanderlust_stay-details",
    "wanderlust_stay-page",
    "wanderlust_stay-photos"
  ],
  "stayDetails": {
    "status": "ok",
    "topLevelKeys": [
      "stay"
    ],
    "forbiddenKeysFound": []
  },
  "deleteRequest": {
    "status": "error",
    "error": "unknown capability: wanderlust.delete-guest-bookings"
  },
  "failures": []
}

What the recorder checked

  • `build` exits 0 and writes the IR the SDK loads
  • the embedded script exits 0 (it exits 1 when any of its own expectations fails)
  • the same tools are offered in three vendor shapes (anthropic, openai-chat, gemini)
  • the three envelopes differ from each other, the tool list does not

What Archstone kept out, or did not do

  • S-02 holds: stay-details ran (ok) and returned none of margin, passport, phone, description_html, net, commission or the guest list
  • S-14 holds: no tool deletes, and asking for one is refused as an unknown capability
  • the refused request never reached the backend: the only request it saw was the stay lookup

Run it yourself

The script needs only the compiled IR and a base URL for the synthetic API. The public Worker serves that API on its own origin (https://demo.archstone.dev/v1/...), so a Node process can run against it directly; use pnpm --filter archstone-showcase api and --api http://localhost:8788 to run against your own copy.

From a checkout of the core at the pinned commit
git clone https://github.com/NousVigil/archstone.git && cd archstone && git checkout 6ff4bac6902190b3318ecb01f966a398d6a172a2
pnpm install && pnpm build
pnpm exec tsx packages/cli/src/index.ts build examples/showcase/manifest --out /tmp/ir.json
node examples/showcase/sdk/embedded.mjs --ir /tmp/ir.json --api https://demo.archstone.dev

The page's check also runs this exact script, copied verbatim from the pinned commit, with the published @archstone/cli and @archstone/agent (0.33.0) against the vendored synthetic API, and requires exit 0 and an empty failures list (11 tools).

sdk/embedded.mjs (verbatim)
// The embedded SDK, end to end, against the Showcase: no MCP server process, no AI app.
//
//   archstone build examples/showcase/manifest --out ir.json
//   SHOWCASE_API_URL=http://localhost:8788 node examples/showcase/sdk/embedded.mjs --ir ir.json
//
// It loads the compiled IR with `fromIR`, hands the SAME capabilities to three vendors' tool-calling
// formats with `tools()`, and runs two of them with `execute()`:
//
//   S-02  the stay-details tool returns the declared fields and none of what the backend also sends
//         (margin, passport, phone, raw HTML, the guest list);
//   S-14  there is no tool that deletes, and asking `execute()` for one is refused before any request.
//
// It prints one JSON report on stdout, and exits 1 (naming what failed on stderr) if any of that does
// not hold. `tools()` is a lowering of the IR and `execute()` a call through the same projection the
// MCP server uses; neither makes the synthetic backend safe, they only decline to forward what the
// manifest does not name.
import { readFileSync } from "node:fs";
import { fromIR } from "@archstone/agent";

const argv = process.argv.slice(2);
const flag = (name) => {
  const i = argv.indexOf(name);
  return i >= 0 ? argv[i + 1] : undefined;
};
const irPath = flag("--ir");
if (!irPath) {
  console.error("usage: node embedded.mjs --ir <ir.json> [--api <url>]  (SHOWCASE_API_URL is used when --api is absent)");
  process.exit(2);
}
const api = flag("--api") ?? process.env.SHOWCASE_API_URL;

const sdk = fromIR(JSON.parse(readFileSync(irPath, "utf8")));

const failures = [];
const expect = (claim, ok) => {
  if (!ok) failures.push(claim);
};

// --- three vendor shapes, one set of tools
const nameOf = {
  anthropic: (t) => t.name,
  "openai-chat": (t) => t.function.name,
  gemini: (t) => t.name,
};
const vendorShapes = {};
const names = {};
for (const format of Object.keys(nameOf)) {
  const defs = sdk.tools(format);
  names[format] = defs.map(nameOf[format]).sort();
  vendorShapes[format] = { tools: defs.length, envelopeKeys: Object.keys(defs[0]).sort() };
}
const toolNames = names.anthropic;
expect("the three vendor shapes expose the same tools", Object.values(names).every((n) => JSON.stringify(n) === JSON.stringify(toolNames)));
expect("every shape lists at least ten tools", toolNames.length >= 10);
expect("no shape lists a tool that deletes", !toolNames.some((n) => /delete|remove|erase/i.test(n)));
expect("the shapes differ in envelope, not in content", new Set(Object.values(vendorShapes).map((v) => v.envelopeKeys.join())).size === 3);

// --- S-02: withholding still holds
const FORBIDDEN = ["margin", "passport", "phone", "description_html", "net", "commission", "guests"];
const keysOf = (value, out = new Set()) => {
  if (Array.isArray(value)) value.forEach((v) => keysOf(v, out));
  else if (value && typeof value === "object") {
    for (const [k, v] of Object.entries(value)) {
      out.add(k);
      keysOf(v, out);
    }
  }
  return out;
};
const stay = await sdk.execute("wanderlust.stay-details", { stayId: "ws-1001" }, { env: { SHOWCASE_API_URL: api } });
expect("stay-details returns status ok", stay.status === "ok");
const seen = keysOf(stay.data);
expect("stay-details returns the declared fields (stay.name, stay.rooms)", seen.has("name") && seen.has("rooms"));
const leaked = FORBIDDEN.filter((k) => seen.has(k));
expect(`stay-details returns none of ${FORBIDDEN.join(", ")} at any depth`, leaked.length === 0);

// --- S-14: the refusal still holds
const gone = await sdk.execute("wanderlust.delete-guest-bookings", { name: "Ana Pop" }, { env: { SHOWCASE_API_URL: api } });
expect("asking for a delete capability is refused as unknown", gone.status === "error" && /unknown capability/.test(gone.error ?? ""));

const report = {
  vendorShapes,
  toolNames,
  stayDetails: { status: stay.status, topLevelKeys: Object.keys(stay.data ?? {}), forbiddenKeysFound: leaked },
  deleteRequest: { status: gone.status, error: gone.error },
  failures,
};
console.log(JSON.stringify(report, null, 2));
if (failures.length > 0) {
  console.error(`embedded.mjs: ${failures.length} expectation(s) failed:\n  - ${failures.join("\n  - ")}`);
  process.exit(1);
}

The test behind this

Planned

Locked · planned · #166

S-22A declared HTML response: designed, not built

There is no HTML output type today, and nothing runs behind this anchor. The agency's description_html is not named by any resource, so the raw description is not sent at all (S-02 shows it dropped). A declared string field cannot express "this is markup", and nothing would check the images and links inside it.

Planned: not available yet NousVigil/archstone#166

Nothing runs behind this anchor. It is shown, not hidden, so the gap is visible.

What is planned

The design is ratified (ADR-0021, from RFC-0010); the build is deferred until a real provider's API returns HTML, and none of it exists yet. The rule: a declared html output whose every image and link satisfies the binding's declared origins is passed through unmodified; otherwise the whole value is withheld through the existing withheld path, which names the field and gives a closed reason category, never the offending URL. It is never cleaned, stripped, rewritten or sanitised. A provider marked trusted gets no exception (the ADR-0008 rule). A parse error, a <form>, a <script>, an inline style, or a relative link withholds the value too, and the runtime never fetches any URL inside the HTML. When a provider needs it, this scenario becomes executable with a clean-pass and a withheld pair.

Where it stands todayToday: description_html is absent from every result and from every advertised schema (S-02).

The test behind this

Connect your own client

The Worker is a remote MCP server: https://demo.archstone.dev/mcp, Streamable HTTP, stateless, POST only (a GET is 405), JSON responses. The only credential is an optional Authorization: Bearer <key> header. There are two public demo keys; they are published here, in the core repository and in the Worker's README on purpose, and unlock nothing but invented data.

HeaderPrincipal the Worker assertsEffect
Authorization: Bearer demo-public-key-visitor-0000demo:visitoraccepted by the agency, allowed to book, pay and cancel (S-06, S-08, S-09)
Authorization: Bearer demo-public-key-blocked-0000demo:blockedaccepted by the agency, denied by the policies of book, pay and cancel: principal_denied (S-07)
any other bearernonea credential is present, no principal (S-07)
a header that is not Bearer ...cannot be resolvedrefused, fail closed: policy_unevaluatable (S-07)
no headernoneevery keyless tool works; a tool that needs a credential is refused: authenticated_no_credential (S-06)

Claude on the web, in the desktop app and on mobile can add the URL alone as a custom connector (Settings → Connectors → Add custom connector); that path has no field for a header, so it reaches every keyless tool. To send a key, use one of the blocks below. They carry the Worker's URL and header; the surrounding structure follows each client's documented configuration format, which this page does not test.

Claude Code
claude mcp add --transport http wanderlust https://demo.archstone.dev/mcp \
  --header "Authorization: Bearer demo-public-key-visitor-0000"
Claude Desktop (claude_desktop_config.json, through the mcp-remote bridge)
{
  "mcpServers": {
    "wanderlust": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://demo.archstone.dev/mcp",
        "--header",
        "Authorization:${AUTH_HEADER}"
      ],
      "env": {
        "AUTH_HEADER": "Bearer demo-public-key-visitor-0000"
      }
    }
  }
}
Cursor (~/.cursor/mcp.json)
{
  "mcpServers": {
    "wanderlust": {
      "url": "https://demo.archstone.dev/mcp",
      "headers": {
        "Authorization": "Bearer demo-public-key-visitor-0000"
      }
    }
  }
}
VS Code (.vscode/mcp.json)
{
  "servers": {
    "wanderlust": {
      "type": "http",
      "url": "https://demo.archstone.dev/mcp",
      "headers": {
        "Authorization": "Bearer demo-public-key-visitor-0000"
      }
    }
  }
}
Any HTTP client (the exchange every section shows)
curl -s -X POST https://demo.archstone.dev/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'authorization: Bearer demo-public-key-visitor-0000' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

The four limits, in technical terms

The Showcase demonstrates a compiler's guarantees on data we invented. It does not show that any real backend is safe because Archstone sits in front of it.

1. Effects are hints the client may honour or ignore

A capability's effect (read, write, irreversible) is lowered to MCP tool annotations (readOnlyHint, destructiveHint, idempotentHint). Archstone tells the client which actions cannot be undone; whether the AI asks a person first is the client's decision. Archstone enforces none of it and takes no credit for a confirmation a client chose to ask. What the Worker advertises right now (captured tools/list):

ToolAnnotations
tourism_searchreadOnlyHint: true
wanderlust_availabilityreadOnlyHint: true
wanderlust_bookdestructiveHint: false
wanderlust_canceldestructiveHint: true, idempotentHint: false
wanderlust_paydestructiveHint: true, idempotentHint: false
wanderlust_quotedestructiveHint: false
wanderlust_room-statusreadOnlyHint: true
wanderlust_searchreadOnlyHint: true
wanderlust_stay-detailsreadOnlyHint: true
wanderlust_stay-pagereadOnlyHint: true
wanderlust_stay-photosreadOnlyHint: true

2. Approval before an irreversible action is declared, not enforced

policies: [human-approval] on wanderlust.cancel and wanderlust.pay is parsed and lowered, nothing more. No approval mechanism exists, apply and doctor say so, and the lint irreversible-unenforced-policy is a warning that changes no exit code. Runtime enforcement is planned: S-10, NousVigil/archstone#165.

3. A refusal from the agency reaches the AI as a bare status

When the agency itself refuses a call that Archstone let through (an expired payment quote, for example), the result is an error whose text is backend returned 422: the status code, without the reason the agency's body declares, and no _meta. onError maps error rows inside a 2xx answer; a non-2xx answer never reaches the response mapping. Planned: NousVigil/archstone#175. Refusals by Archstone itself (a policy, the input contract, a lifecycle, a rate limit) are different: they carry a reason code in _meta.

4. The demo key is public and everything it reaches is invented

The two keys are defined in credentials.mjs and published on purpose. The principal is asserted by the Worker from a two-entry table lookup; Archstone treats it as an opaque string and verifies nothing. The agency, its hotels, guests, passport numbers (DEMO-PASS-nnnnnn), phone numbers, e-mail addresses (reserved .example domain) and payments are all invented. The backend over-exposes on purpose. Archstone stops fields the manifest does not name from being forwarded and refuses calls the manifest says to refuse; it does not fix what a backend stores, returns elsewhere, or does when asked directly. Do not copy this key pattern for a real credential.

What does not work yet, said plainly

Locked and partial items are shown, never hidden.

WhereItemWhat happens todayStatus
S-10Runtime-enforced human approvalNot implemented. human-approval is declared, not enforced; the cancel and pay calls above run at once.Locked, #165
S-22HTML response typeDesign ratified (ADR-0021), build deferred until a provider needs it. The raw description is not sent at all.Locked, #166
S-17Strict verifyA gained field is a yellow reading: named, exit 0. Only a lost field, a changed type or a missing required value exit 1.Planned, #178
S-17adopt on a manifest with two resources named StayResolved a resource by bare name. Fixed in core 0.32.0; the recording predates the fix and still runs it on wanderlust.stay-details.Fixed, #177
S-03, S-04Outcome of a withheld field over MCPA successful MCP result carries no outcome in _meta; the withheld path is a text note. status: degraded and withheld exist on the embedded SDK's execute() result.Planned, #174
S-09A refusal from the agencySurfaces only backend returned 422. An expired or foreign payment quote is the agency's rule, not Archstone's, and no _meta is attached. (A payment with no quote at all is refused earlier, by the input contract, with a reason.)Planned, #175
S-13A failing backendReported as an error with its status (backend returned 503), never as degraded: a non-2xx answer never reaches the response mapping.Planned, #175
S-11Rate limit on the live WorkerApproximate: a fixed window in memory per isolate; another isolate or a restart starts a new count. The Node test asserts the exact fourth refusal on a fake clock.By design of a demo prop

Reference

The tool a client receives

What tools/list advertises for wanderlust_stay-details, captured from the Worker: the input schema, the output schema (the declared fields only, nested to the same depth as the projection) and the annotations.

tools/list: wanderlust_stay-details
{
  "name": "wanderlust_stay-details",
  "description": "Tell me more about one stay: its rooms and what each room offers.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "stayId": {
        "description": "The stay's id: the `id` field of a row returned by wanderlust_search (it looks like `ws-1001`), never the stay's name. To find a stay the user names, first call wanderlust_search with that name as `destination`.",
        "type": "string"
      }
    },
    "required": [
      "stayId"
    ],
    "additionalProperties": false
  },
  "outputSchema": {
    "type": "object",
    "properties": {
      "stay": {
        "type": "object",
        "properties": {
          "name": {
            "description": "The property's display name.",
            "type": "string"
          },
          "location": {
            "description": "Where the stay is.",
            "type": "string"
          },
          "pricePerNight": {
            "description": "Nightly rate from, in euro.",
            "type": "number"
          },
          "rating": {
            "description": "Guest review score, when available.",
            "type": "number"
          },
          "petPolicy": {
            "description": "The stay's pet policy, e.g. cats welcome with a nightly fee, or no pets.",
            "type": "string"
          },
          "rooms": {
            "description": "The room types on offer.",
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "description": "Room type.",
                  "type": "string"
                },
                "sleeps": {
                  "description": "How many people it sleeps.",
                  "type": "number"
                },
                "pricePerNight": {
                  "description": "Nightly rate for this room in euro.",
                  "type": "number"
                },
                "amenities": {
                  "description": "What the room offers.",
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "description": "What the amenity is.",
                        "type": "string"
                      },
                      "fee": {
                        "description": "Fee in euro; absent when it is free.",
                        "type": "number"
                      }
                    },
                    "required": [
                      "name"
                    ]
                  }
                }
              },
              "required": [
                "name",
                "sleeps",
                "pricePerNight",
                "amenities"
              ]
            }
          }
        },
        "required": [
          "name",
          "location",
          "pricePerNight",
          "rooms"
        ]
      }
    },
    "required": [
      "stay"
    ]
  },
  "annotations": {
    "readOnlyHint": true
  }
}

Validating what a model extracted

The same resources that project a backend's answer also validate a model's own extraction: extractor("wanderlust.Stay", "anthropic").validate(doc) from @archstone/agent accepts a document with the declared fields, and rejects one with a missing required field, a wrong type, or a field nobody declared (an invented margin). Re-captured against the published @archstone/agent 0.33.0 on every check.

CaseInputResult
ok{"id":"ws-1001","name":"Casa Alfama","location":"Lisbon","pricePerNight":118,"rating":4.6}{"status":"degraded","data":{"id":"ws-1001","name":"Casa Alfama","location":"Lisbon","pricePerNight":118,"rating":4.6},"degraded":["petPolicy"]}
noRating{"id":"ws-1001","name":"Casa Alfama","location":"Lisbon","pricePerNight":118}{"status":"degraded","data":{"id":"ws-1001","name":"Casa Alfama","location":"Lisbon","pricePerNight":118},"degraded":["petPolicy","rating"]}
missingRequired{"id":"ws-1001","name":"Casa Alfama","location":"Lisbon","rating":4.6}{"status":"violation","missing":["pricePerNight"],"degraded":["petPolicy"]}
invented{"id":"ws-1001","name":"Casa Alfama","location":"Lisbon","pricePerNight":118,"rating":4.6,"margin":40}{"status":"degraded","data":{"id":"ws-1001","name":"Casa Alfama","location":"Lisbon","pricePerNight":118,"rating":4.6},"degraded":["petPolicy"],"undeclared":["margin"]}
wrongType{"id":"ws-1001","name":"Casa Alfama","location":"Lisbon","pricePerNight":"118","rating":4.6}{"status":"violation","invalid":["pricePerNight: expected number"],"degraded":["petPolicy"]}

Lifecycle across the manifest

A capability can be experimental, beta, stable (the default), deprecated or retired. The non-default ones in the Wanderlust manifest, read from the vendored files, and whether the Worker lists them in tools/list (captured):

CapabilityLifecycleListed to a client
tourism.searchdeprecatedyes
tourism.search-classicretiredno
wanderlust.availabilitybetayes
wanderlust.neighbourhoodexperimentalno

The experimental one, wanderlust.neighbourhood (tool name wanderlust_neighbourhood), is the one to notice: it is not in the tool list a client receives, yet a client that calls it by name is answered (the core's test for this is manifests.test.ts#L93; this page does not call it live). The core's README says an experimental capability is unlisted but still callable by name, a deprecated one answers with a phasing-out note in its tool description (S-12), and a retired one is refused (S-12).

Run it yourself, on Wanderlust

The earlier demo gave the real command under each of its sections. They are the same commands, pointed at the vendored Wanderlust manifest in a checkout of the core at the pinned commit. The manifest needs the CLI from 0.32.0 or later (the image type). What none of this shows you is your own API: that is what Verify my API is for.

Commands
npm install -g @archstone/cli
archstone apply examples/showcase/manifest/                    # validate + compile
SHOWCASE_API_URL=https://demo.archstone.dev archstone serve examples/showcase/manifest/   # MCP over stdio, against the public synthetic API
archstone serve --http examples/showcase/manifest/ --token my-bearer-token   # MCP over HTTP
archstone build examples/showcase/manifest/ --out archstone.ir.json          # portable IR
archstone init examples/showcase/api/wanderlust.openapi.yaml --out draft --decisions examples/showcase/record/s20-decisions.json --non-interactive
archstone apply examples/showcase/manifest/ --exposure     # add --json for CI
archstone adopt examples/showcase/manifest/                 # declare a field, deliberately (needs a person)
SHOWCASE_API_URL=https://demo.archstone.dev archstone verify examples/showcase/manifest/   # green / yellow / red per binding
archstone diff manifest-v1/ manifest-v2/     # what changed for an agent; exit 1 if breaking
archstone doctor examples/showcase/manifest/                # pre-production checklist, offline
archstone audit audit.log --phase denied --format csv
@archstone/agent: tools, execute, extractor, governance, MCP over HTTP
import { readFileSync } from "node:fs";
import { fromIR } from "@archstone/agent";

const archstone = fromIR(JSON.parse(readFileSync("archstone.ir.json", "utf-8")));
const myTools = archstone.tools("anthropic");   // or openai-chat | openai-responses | gemini | json-schema
const result = await archstone.execute("wanderlust.search", input, { env });

// validating what a model extracted (the other direction):
const stay = archstone.extractor("wanderlust.Stay", "anthropic");
stay.structuredOutput;                        // the closed schema, in your provider's envelope
const checked = stay.validate(modelOutput);   // ok | degraded | violation, undeclared keys named

// governance: pass identity, a counter and an audit sink on every call
await archstone.execute("wanderlust.book", input, {
  caller: { principal: "user:alice" },        // asserted by YOUR authentication
  rateLimitCounter: counter,
  auditSink: jsonLinesAuditSink(createWriteStream("audit.log", { flags: "a" })),
});

// MCP over HTTP from the same instance, on your own router:
import { mcpHandler } from "@archstone/agent/mcp";
const handler = mcpHandler(archstone, { bearerToken: process.env.ARCHSTONE_TOKEN });

Providers and proof

A binding says how a capability reaches a backend. The compiler, the tool and the declaration are the same whichever provider sits behind them.

REST is the provider every live scenario above uses: an HTTP method and path on a baseUrl that comes from your environment (${SHOWCASE_API_URL}), a JSONPath map from the response onto the resource, and optional caller credentials (${caller.accessToken}, S-06). archstone init writes it from an OpenAPI document (S-20).

SQL (Postgres): @archstone/provider-sql binds a capability directly to Postgres, read-only (SELECT). Tenant isolation is enforced by the database (row-level security, with a runtime role that is not the table owner), and a connection that is over-privileged is refused at startup. It is Node-only, so it is not part of the edge or browser build and not part of the live Worker; S-15 is a recording. Its design record is still marked a draft.

Proof: a business running on it. ArtVinci, a custom-framing business in Romania, answers customer questions today through capabilities compiled by Archstone, with real catalog and prices computed live by its own backend. It does not run archstone serve: it loads the compiled artifact with the embedded SDK (S-21). The captured conversation, with its real question and real prices, is on the homepage; the full write-up is the case study.

Where the earlier demo's technical sections went

The earlier /demo simulated ten sections on a tourism sample. Their technical content now runs on the Wanderlust agency above, not on the old sample.

Earlier sectionNow
Write the capability, get the tool (compile, optional and required fields, diff)S-01, S-18, commands in run it yourself
Start from an OpenAPI document (init)S-20
The model learns the shape before it calls (typed schemas)the tool a client receives, S-02
A field you did not name never reaches a model (exposure, adopt)S-02, S-16, S-17
When the backend breaks the contract, the answer is an error (fail closed)S-13
Your API changes; you hear about it first (verify, diff, doctor)S-17, S-18, S-19
A field the model invents never reaches your system (extractor)validating what a model extracted
Disclosed, refused, counted, recorded (effects, lifecycle, policy, rate limit, audit)S-05, S-08, S-12, lifecycle across the manifest, S-06, S-07, S-11, S-19, the audit call in run it yourself
No server process: the embedded SDKS-21
Reach the backend you have (REST, SQL), and the proofevery live scenario (REST), S-15 (SQL), providers and proof