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

# Configure a script source adapter

> Add bounded Rhai adaptation for pagination, branching, and normalization without granting ambient source or credential access.

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

Use the script capability when one fixed HTTP request is not enough. Rhai is the v1 script
runtime, but the capability is product-neutral. A DHIS2 label or version does not enable it, and
an unknown source product can use exactly the same host API.

The worker has no network, filesystem, environment, subprocess, clock, random, credential, or
production logging access. It asks the Relay parent to make each bounded source call. Relay
validates the complete request against the authored same-origin authority, commits the request
before dispatch, consumes the next durable call permit, and returns only bounded JSON or text.

## Try the bundled script starter

The DHIS2 Tracker starter is one concrete script example. Its product name and version are review
evidence only; any reviewed source system can use the same script capability and host API.

<ProjectStarterSequence starter="dhis2-tracker" />

## Declare source authority

The public integration declares input schemas, authentication interface, allowed source paths,
the script file, outputs, and only the limits that differ from the defaults:

~~~yaml
version: 1
id: person-status
revision: 1

input:
  person_id:
    role: selector
    type: string
    maxLength: 64

source:
  product: fictional-registry
  versions: { unverified: [project-api-v1] }
  auth: { type: static_bearer }
  allow:
    - method: GET
      path: /v1/people/**
  response:
    format: json
  response_headers: [Link]

capability:
  script:
    file: adapter.rhai

outputs:
  active: { type: boolean }
  programme_code: { type: [string, "null"], maxLength: 32 }
~~~

Source product and version are interoperability evidence only. Runtime dispatch uses the generic
source, capability, and environment contracts.

Every input lives in one typed map and declares `role: selector` or `role: parameter`. An
integration has one to eight non-null selectors and at most sixteen total inputs. The canonical
selector values share a fixed 4096-byte ceiling. A nullable parameter is still present in the
consultation request as explicit JSON null; the adapter receives the validated value beneath
`ctx.input`.

An asterisk admits one non-empty path segment. A terminal double asterisk admits zero or more
complete non-empty segments. Matching is case-sensitive. Dot segments, encoded separators,
fragments, user information, and another origin fail before credentials are resolved. A source
POST also requires `semantics: read_only` and evidence that the admitted operation is
non-mutating.

The defaults are five calls, 64 KiB of aggregate author-controlled request bytes, a 512 KiB
per-response body, 2 MiB of aggregate source-response bytes, and a 15 second deadline. The hard
ceilings are 16 calls, 1 MiB of request bytes, 8 MiB per response, 16 MiB of source responses, and
60 seconds. Deployment settings may lower these limits but cannot widen the reviewed integration.

## Write the adapter

Create adapter.rhai with one consult(ctx) function:

~~~rhai
fn consult(ctx) {
    let target = source.path(
        "/v1/people/{person_id}",
        #{ person_id: ctx.input.person_id }
    );
    let response = source.get(target, #{
        query: #{ fields: "id,active,programmeCode" }
    });

    if response.status == 404 {
        return result.no_match();
    }
    if response.status != 200 {
        return result.fail(failure.source_rejected);
    }
    if response.body.id != ctx.input.person_id {
        return result.fail(failure.subject_mismatch);
    }

    result.match(#{
        active: response.body.active,
        programme_code: response.body.programmeCode
    })
}
~~~

The host API provides `source.path`, `source.get`, `source.post_json`, `source.post_form`, the three
successful result constructors, and fixed source failure constants. Every response has `status`,
`body`, and the safe headers named by `source.response_headers`. The body is bounded decoded JSON
by default. Set `source.response.format: text` when the reviewed endpoint returns text. Non-2xx
statuses return normally except `401`, `403`, and `429`, which remain host-owned failures. JSON
null is Rhai unit. A match returns every declared output with its exact type and nullability.

An adapter may branch, transform collections, repeat a request, and follow a bounded same-origin
pagination link. Relay canonicalizes an absolute returned link back to a relative source reference,
rejects another origin, and reapplies the path, method, header, request, response, call, and deadline
checks before dispatch. Each call is independently authorized and counts against the same
cumulative budgets.

## Split an adapter into local modules

For a larger adapter, list up to 32 project-local Rhai files in their deterministic evaluation
order:

~~~yaml
capability:
  script:
    file: adapter.rhai
    modules:
      - lib/normalization.rhai
      - lib/cardinality.rhai
~~~

Local modules contain ordinary top-level Rhai functions. They cannot add source authority or load
another file at runtime. `registryctl` resolves each normalized `.rhai` path beneath the integration
directory without following symlinks, combines the declared modules with the entrypoint, and
enforces a 64 KiB compiled-script ceiling. Module paths, order, and contents are hash-covered by the
authored and generated closures, so a module edit changes the review and deployment inputs. There
is no ambient module search path, remote package resolution, or runtime `import` fallback.

## Add interaction fixtures

Committed fixtures must be wholly synthetic and prove the rendered request as well as response:

~~~yaml
name: match
classification: synthetic
input:
  person_id: SYNTHETIC_PERSON_001
interactions:
  - expect:
      method: GET
      path: /v1/people/SYNTHETIC_PERSON_001
      query:
        fields: id,active,programmeCode
    respond:
      status: 200
      body:
        id: SYNTHETIC_PERSON_001
        active: true
        programmeCode: DEMO
expect:
  outcome: match
  outputs:
    active: true
    programme_code: DEMO
~~~

Keep match, no-match, ambiguity, identifier-mismatch, and meaningful project variations. The
harness derives malformed decoding, limits, timeout, authorization-before-source, output
minimization, and protocol-verification negatives. Large synthetic bodies belong in
fixtures/bodies/.

## Verify

~~~sh
registryctl test --project-dir <project> --integration person-status --fixture match --trace
registryctl test --project-dir <project> --integration person-status --watch
registryctl check --project-dir <project> --environment <environment> --explain
~~~

Offline tests are portable and use no real credentials or network. Production activation remains
Linux-only while the process controls are code-owned. Fixture success proves the adapter against
synthetic observations, not production source interoperability or deployment approval.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| A source call fails before dispatch | The method, path, target, header, query, body, or remaining budget is outside authored authority. | Narrowly admit the real read operation or correct the adapter. |
| A terminal result is rejected | The adapter omitted an output or returned a wrong name, type, nullability, or size. | Return the complete declared output map. |
| A fixture response matches but its request proof does not | Its expected interaction differs from the production canonical request. | Update the synthetic request expectation or fix the adapter. |
| Production reports the worker unavailable | The adjacent code-owned worker or required Linux isolation is absent. | Correct the deployment. Do not configure an in-process fallback. |

Return to [Author an HTTP Registry Stack project](../author-registry-project/) to review and build
the project.