Registry stack documentation: machine-readable Markdown.
Index of all pages: https://docs.registrystack.org/preview/llms.txt
Full corpus: https://docs.registrystack.org/preview/llms-full.txt

# Verify a live registry-backed claim

> Add Registry Notary to the protected spreadsheet project, evaluate bounded project claims, and change one authored policy.

import QuickstartMeta from '../../../components/QuickstartMeta.astro';

Continue from the protected spreadsheet API in `my-first-api`. You will add
Registry Notary to the same project, evaluate claims through the live
Relay-to-Notary path, prove that caller scope is enforced, and change one
authored status policy.

<QuickstartMeta
  outcome="A live Registry Notary evaluation over the synthetic public-works workbook, with allowed, rejected, and absent-record results."
  time="About 15 minutes after completing the first API tutorial"
  level="Local single-node"
  prerequisites={['The unmodified my-first-api project from the first tutorial', 'Registryctl 0.15.0 with its matching image lock', 'Linux amd64', 'Docker Engine with Compose v2', 'curl', 'Ports 4242 and 4255 available on loopback'] }
/>

The workbook, project identifiers, and caller credentials are synthetic and
local. Do not substitute personal data or production credentials.

## Enter the project

The first tutorial stops its containers but keeps `my-first-api`. Enter that
project:

```sh
cd my-first-api
```

The command prints nothing. Run all remaining commands from this directory.

## Add Notary

Add the maintained local Notary continuation:

```sh
registryctl add notary
```

Registryctl prints this structure. The project path is absolute on your
computer:

```text
Added local Notary add-on.
  Project: <absolute-project-path>
  Files:
    registry-stack.yaml
    environments/local.yaml
    integrations/project-record-snapshot/integration.yaml
    integrations/project-record-snapshot/fixtures/match.yaml
    integrations/project-record-snapshot/fixtures/planned.yaml
    integrations/project-record-snapshot/fixtures/no-match.yaml

Next:
  registryctl test --environment local
  registryctl restart
  registryctl smoke
```

The command extends the same human-owned project. It adds a
`public-works-verification` evidence service, a bounded
`project-record-snapshot` consultation, three synthetic fixtures, and the
local caller and deployment bindings. It does not ask you to edit generated
Relay or Notary files.

Open `registry-stack.yaml` in your text editor. The added service includes:

```yaml
public-works-verification:
  kind: evidence
  version: 1
  subject_type: project
  purpose: public-works-case-management
  legal_basis: public-service-delivery
  consent: not_required
  access: { scopes: ["evidence:projects:read"] }
  consultations:
    project:
      integration: project-record-snapshot
      input: { project_id: request.target.identifiers.project_id }
  claims:
    project-record-exists: { cel: project.matched, disclosure: predicate }
    project-status-accepted: { cel: 'project.matched && project.status == "active"', disclosure: predicate }
```

Relay performs an exact lookup against the same spreadsheet used in the first
tutorial. It releases only the declared `status` output to Notary.
`project-status-accepted` is this example project's configurable policy. It
does not establish programme eligibility or make a legal decision. Both
tutorial claims return predicates. The caller does not receive the source
status value.

## Check the fixtures

Run the maintained active, planned, and absent-record cases:

```sh
registryctl test --project-dir .
```

Registryctl prints exactly:

```text
PASS: 6/6 fixtures passed
```

The fixtures prove the authored lookup and claim meanings offline. The
requests below separately prove the running Relay-to-Notary path.

## Start Relay and Notary

Start the project so Registryctl compiles and starts the combined loopback
topology:

```sh
registryctl start
```

Docker progress depends on the installed Compose version. After both products
become ready, the final lines identify the local Relay and Notary endpoints:

```text
PASS readiness: compiled workbook source is ready
PASS readiness: Registry Notary is ready
Relay API:   http://127.0.0.1:4242
Relay docs:  http://127.0.0.1:4242/docs
Notary API:  http://127.0.0.1:4255
Notary docs: http://127.0.0.1:4255/docs
```

Relay remains the only product that reads the workbook. Notary reaches Relay
through the generated private consultation binding and returns claim results.

## Load the local caller credentials

Load the generated credentials into this shell without printing them:

```sh
set -a
. .registry-stack/runtime/local/secrets/local.env
set +a
```

These commands print nothing. Do not print, paste, or commit this environment
file or either raw credential.

Prepare the active-project request:

```sh
active_request='{
  "target": {
    "type": "Project",
    "identifiers": [
      { "scheme": "project_id", "value": "pw_001" }
    ]
  },
  "claims": ["project-status-accepted"],
  "format": "application/vnd.registry-notary.claim-result+json",
  "purpose": "public-works-case-management"
}'
```

The assignment prints nothing.

## Prove scope is enforced

Send the request with the generated caller that lacks the service's required
`evidence:projects:read` scope:

```sh
curl -sS -o /dev/null -w 'HTTP %{http_code}\n' \
  -X POST \
  -H "x-api-key: $REGISTRYCTL_LOCAL_NOTARY_UNDER_SCOPED_TOKEN_RAW" \
  -H "Data-Purpose: public-works-case-management" \
  -H "Content-Type: application/json" \
  -H "Accept: application/vnd.registry-notary.claim-result+json" \
  --data "$active_request" \
  http://127.0.0.1:4255/v1/evaluations
```

Notary prints exactly:

```text
HTTP 403
```

The credential is valid, but its scope does not authorize this claim. Do not
widen a caller's scope merely to bypass this denial.

## Evaluate the active project

Repeat the request with the authorized tutorial caller:

```sh
curl -sS -w '\nHTTP %{http_code}\n' \
  -X POST \
  -H "x-api-key: $REGISTRYCTL_LOCAL_NOTARY_CALLER_TOKEN_RAW" \
  -H "Data-Purpose: public-works-case-management" \
  -H "Content-Type: application/json" \
  -H "Accept: application/vnd.registry-notary.claim-result+json" \
  --data "$active_request" \
  http://127.0.0.1:4255/v1/evaluations
```

The response contains these stable fields and ends with `HTTP 200`. Dynamic
evaluation IDs, timestamps, target handles, and provenance fields are omitted
here. JSON whitespace and object-key order may differ:

```json
{
  "results": [
    {
      "claim_id": "project-status-accepted",
      "value": true,
      "satisfied": true,
      "disclosure": "predicate",
      "format": "application/vnd.registry-notary.claim-result+json"
    }
  ]
}
```

The scoped caller is allowed to ask whether the selected project's status
meets this policy. Relay finds synthetic project `pw_001` with status
`active`, so Notary returns `true`.

## Evaluate the planned project

Prepare the same claim request for synthetic project `PW-002`:

```sh
planned_request='{
  "target": {
    "type": "Project",
    "identifiers": [
      { "scheme": "project_id", "value": "PW-002" }
    ]
  },
  "claims": ["project-status-accepted"],
  "format": "application/vnd.registry-notary.claim-result+json",
  "purpose": "public-works-case-management"
}'
```

The assignment prints nothing. Evaluate it:

```sh
curl -sS -w '\nHTTP %{http_code}\n' \
  -X POST \
  -H "x-api-key: $REGISTRYCTL_LOCAL_NOTARY_CALLER_TOKEN_RAW" \
  -H "Data-Purpose: public-works-case-management" \
  -H "Content-Type: application/json" \
  -H "Accept: application/vnd.registry-notary.claim-result+json" \
  --data "$planned_request" \
  http://127.0.0.1:4255/v1/evaluations
```

The stable claim fields are:

```json
{
  "results": [
    {
      "claim_id": "project-status-accepted",
      "value": false,
      "satisfied": false,
      "disclosure": "predicate",
      "format": "application/vnd.registry-notary.claim-result+json"
    }
  ]
}
```

The response ends with `HTTP 200`. Relay found `PW-002`, but its `planned`
status does not meet the active-only policy. This is a policy non-match, not
an absent registry record.

## Check an absent record

Use the separate existence claim for a synthetic identifier absent from the
workbook:

```sh
absent_request='{
  "target": {
    "type": "Project",
    "identifiers": [
      { "scheme": "project_id", "value": "pw_999" }
    ]
  },
  "claims": ["project-record-exists"],
  "format": "application/vnd.registry-notary.claim-result+json",
  "purpose": "public-works-case-management"
}'

curl -sS -w '\nHTTP %{http_code}\n' \
  -X POST \
  -H "x-api-key: $REGISTRYCTL_LOCAL_NOTARY_CALLER_TOKEN_RAW" \
  -H "Data-Purpose: public-works-case-management" \
  -H "Content-Type: application/json" \
  -H "Accept: application/vnd.registry-notary.claim-result+json" \
  --data "$absent_request" \
  http://127.0.0.1:4255/v1/evaluations
```

The assignment prints nothing. The stable claim fields are:

```json
{
  "results": [
    {
      "claim_id": "project-record-exists",
      "value": false,
      "satisfied": false,
      "disclosure": "predicate",
      "format": "application/vnd.registry-notary.claim-result+json"
    }
  ]
}
```

The response ends with `HTTP 200`. `false` means this bounded lookup produced
no match in the selected workbook. It does not prove global nonexistence,
fraud, ineligibility, or another legal negative.

## Change the status policy

In `registry-stack.yaml`, replace the active-only claim:

```yaml
project-status-accepted: { cel: 'project.matched && project.status == "active"', disclosure: predicate }
```

with a rule that also accepts `planned`:

```yaml
project-status-accepted:
  cel: 'project.matched && (project.status == "active" || project.status == "planned")'
  disclosure: predicate
```

The claim name remains accurate because it describes this example project's
authored acceptance policy, not an intrinsic meaning of either status.

Open
`integrations/project-record-snapshot/fixtures/planned.yaml` and change its
maintained claim expectation:

```yaml
project-status-accepted: true
```

The source interaction and `outputs: { status: planned }` remain unchanged.
You changed only the policy and its expected derived result.

Check the complete fixture set again:

```sh
registryctl test --project-dir .
```

Registryctl prints exactly:

```text
PASS: 6/6 fixtures passed
```

## Restart and see the changed result

Restart from the authored files:

```sh
registryctl restart
```

After Docker progress, Registryctl prints the same readiness and endpoint
lines shown earlier. Registryctl regenerates the Relay and Notary inputs.
You do not edit `.registry-stack/`.

Repeat the unchanged planned-project request:

```sh
curl -sS -w '\nHTTP %{http_code}\n' \
  -X POST \
  -H "x-api-key: $REGISTRYCTL_LOCAL_NOTARY_CALLER_TOKEN_RAW" \
  -H "Data-Purpose: public-works-case-management" \
  -H "Content-Type: application/json" \
  -H "Accept: application/vnd.registry-notary.claim-result+json" \
  --data "$planned_request" \
  http://127.0.0.1:4255/v1/evaluations
```

The stable fields now show the changed policy result:

```json
{
  "results": [
    {
      "claim_id": "project-status-accepted",
      "value": true,
      "satisfied": true,
      "disclosure": "predicate",
      "format": "application/vnd.registry-notary.claim-result+json"
    }
  ]
}
```

The response ends with `HTTP 200`. The source record is still `planned`; the
authored acceptance policy now includes that status.

## Stop the local services

Stop the combined runtime:

```sh
registryctl stop
```

Docker may print container shutdown progress. Registryctl prints no additional
success line after Compose exits successfully.

Remove the generated raw credentials from this shell:

```sh
unset REGISTRYCTL_LOCAL_NOTARY_CALLER_TOKEN_RAW \
  REGISTRYCTL_LOCAL_NOTARY_UNDER_SCOPED_TOKEN_RAW \
  REGISTRYCTL_LOCAL_RELAY_MATCH_KEY_RAW \
  REGISTRYCTL_LOCAL_RELAY_NO_MATCH_KEY_RAW
```

The command prints nothing. The authored workbook, service, integration,
fixtures, and policy edit remain in `my-first-api`. Generated runtime files
and local credentials remain disposable.

## What you built

You extended the first tutorial's protected spreadsheet project with a live
Registry Notary path. You proved that:

- A valid but under-scoped caller is denied.
- The scoped tutorial caller receives predicate results without receiving a
  workbook row.
- A policy non-match and an absent source record have distinct claim meanings.
- An authored policy and its maintained expectation change the live result
  after `registryctl restart`.

Relay owns the exact workbook lookup. Notary owns claim evaluation and
disclosure. Registryctl compiles and runs both from the same human-owned
project.

## Next

- [Connect your own data](../../configure/) when you are ready to replace the
  synthetic workbook and review the resulting integration and policy.
- [Understand generated files](../../generated-artifacts/) before handing
  compiled product inputs to an operator.
- [Prepare the operator handoff](../../operate/) before introducing real
  callers, identity providers, databases, or production networks.

## Troubleshooting

| Symptom | Cause | Resolution |
| --- | --- | --- |
| `registryctl add notary` reports an unsupported shape | The project differs from the maintained spreadsheet starter or already has a conflicting Notary integration. | Start from the unmodified `my-first-api` project created by Registryctl 0.15.0. |
| `registryctl add notary` prints `Verified local Notary add-on.` | The exact maintained add-on is already present. | Continue with the fixture check. The command made no further changes. |
| `registryctl test` fails after the policy edit | The authored rule and fixture expectation disagree. | Keep the planned source output unchanged and set its `project-status-accepted` expectation to `true`. |
| Notary returns `401` | The caller credential is missing or belongs to another project. | Source this project's private `local.env` again without printing it. |
| Notary returns `403` for the authorized caller | The caller lacks `evidence:projects:read`, or the purpose does not match the service. | Use the generated tutorial caller and `public-works-case-management`. Do not widen the scope merely to bypass the denial. |
| A generated artifact integrity check fails | A file under `.registry-stack/` was edited or replaced. | Rerun Registryctl from the human-owned project so it regenerates the disposable runtime. |