Skip to main content

Pearls

PEARLS — Parallel-Execution Agentic Reconciliation and Learning Systems

Learn faster by running safer, at scale.

Pearls is an architecture for letting AI agents act autonomously over private data without surrendering control, provenance, or context to model vendors. It resolves the usual tradeoff between centralized policy (safe but slow) and local agent autonomy (fast but unauditable) by fixing the execution context of every agent run and reconciling the results afterward.

Architecture in one sentence

Fix the execution context in a versioned manifest, compile least authority into a TAJ, isolate agent writes to descendant Packages, and reconcile the resulting evidence through a branchable Pearl history.

Core ideas

  • Scene — the fixed context and capability frame for one agent execution, with four roles: Input Control, Input Content, Output Content, Output Control. Inputs are pinned and read-only; outputs are bounded and append-only. An agent cannot expand its own authority mid-run.
  • Pearl — a continuing, versioned lineage of related Scenes: branch history, accepted control revisions, evaluations, and reconciliation decisions.
  • Package revision — a sealed Scene manifest plus the immutable content it references. A Quilt Package manifest acts as a capability-addressed routing table from stable logical keys to exact S3 object versions.
  • TAJ (Translated Access JWT) — a signed capability over a pinned manifest and a logical scope, minted once by RAJA (the authorization compiler) and enforced at runtime by RAJEE (an Envoy-based data-plane gateway) — no policy database in the data path.
  • Reconciliation — TerminusDB versions the active graph of Scenes and Pearl lineages; independent branches merge structurally, then get checked against domain invariants before becoming operative.

Learning is separated from mutation: an agent's Output Control (proposed steering, exceptions, evaluations) never changes its own authority. It becomes active only through a separately authorized promotion into a later Scene's Input Control.

Why it matters

Safer isolation permits more parallel executions. More executions produce more real-world evidence. Reconciliation converts that evidence into reusable control. Better control makes later executions safer, cheaper, and more autonomous — a compounding-autonomy flywheel instead of a security/velocity tradeoff.

The same bounded-execution record also supports Phase IV monitoring: continuous, post-deployment evaluation of agents, analogous to clinical postmarketing surveillance.

Reference stack (AWS)

Plane Components
Identity & policy OIDC/IAM, Amazon Verified Permissions, KMS/Secrets Manager, CloudTrail/CloudWatch
Manifest & coordination TerminusDB, Scene sealer, RAJA/RALE
Execution EventBridge + SQS, Step Functions, Lambda/ECS/Batch, model adapters (Bedrock, local, external)
Data plane RAJEE/Envoy, pinned manifest cache, Amazon S3 (versioned)

Durable context, policy, evidence, and Package history stay in the customer's own AWS account. Model providers are replaceable compute: models can be rented; context must be owned.

Document status

The Pearls architecture distinguishes three levels of maturity:

  1. Available primitives — existing AWS services and Quilt Package semantics.
  2. Reference integration — the proposed composition of those primitives into Scene-based execution isolation.
  3. Validation frontier — mechanisms that still need implementation and stress testing (deterministic Scene sealing and cross-revision, domain-aware reconciliation).

Release 0.1.1 adds an alpha maturity-level 1→2 authority-compilation primitive: pearls compiles a pinned Quilt Package scope into an AWS STS session policy and temporary credentials. It is not a claim that the complete Pearls architecture is production-ready.

AWS-native TAJ (pearls)

pearls is a small Python 3.11+ compiler for minimum-viable Scene isolation. The trusted orchestrator loads one immutable Quilt manifest, resolves a sealed logical scope, and passes the resulting minified policy to sts:AssumeRole. AWS evaluates the temporary credential as the intersection of the administrator-created base role and this session policy. There is no runtime gateway or policy database.

This AWS-native TAJ is an STS role session, not a literal JWT. Its policy travels with the temporary credential and its RoleSessionName is the Scene ID used for audit correlation.

Install and test

# From PyPI, once the distribution is published
uv tool install pearls   # the pearls CLI on its own
uv add pearls            # as a project dependency

# From a checkout
uv venv
uv pip install .

# Development checks
uv pip install ".[dev]"
ruff format --check .
ruff check .
mypy
cfn-lint infra/*.yaml src/pearls/templates/*.yaml
pytest
uv build

Direct runtime and development dependencies are pinned in pyproject.toml; transitive resolution remains the installer/index's responsibility. There is no uv.lock, so a checkout resolves the way an ordinary install does. Normal CI runs only offline tests and never assumes a role or mutates AWS resources.

Pinned manifest and scope contract

A manifest URI must name an S3-backed Quilt registry, package, and complete 64-character top hash:

quilt+s3://manifest-bucket#package=project%2Fsealed-scene&top_hash=<64 hex characters>

The package name is URL encoded in the fragment. Floating package pointers, hash prefixes, local files, wildcard-bearing S3 keys, and unpinned manifests are rejected. The compiler identity resolves the manifest. The resulting worker credential receives one physical S3 read prefix per source registry represented by the selection, plus the exact content-addressed manifest read when the caller has not already resolved it. IAM bounds the read blast radius to those prefixes; the runner, rather than IAM, enforces each selected object's exact version and content hash.

A scope is a mapping like examples/scope.json:

{
  "readable": ["input-control/worker.json", "input-content/"],
  "writable_package": "scenes/example/output@my-project-bucket",
  "require_version_ids": true
}

A readable value selects an exact logical key when one exists. Otherwise it selects the logical directory of that name; a trailing slash makes directory intent explicit. Every selector must match at least one object. Overlapping selectors are de-duplicated deterministically. The output data prefix is <package-name>/ in the named bucket.

require_version_ids fails compilation if any selected object lacks an S3 version ID. A Scene run always requires both a version ID and a supported Quilt content hash, requests that exact version, and verifies the returned bytes before the handler executes. The prefix credential can read other objects under an admitted source prefix; those objects are not materialized or accepted as inputs by the runner.

Compile and mint

from pearls import compile, mint

manifest_uri = "quilt+s3://manifest-bucket#package=project%2Fsealed-scene&top_hash=" + "a" * 64
scope = {
    "readable": ["input-control/worker.json", "input-content/"],
    "writable_package": "scenes/run-123/output@my-project-bucket",
    "require_version_ids": True,
}

compiled = compile(manifest_uri, scope)
print(compiled.report())  # safe authority metadata; no credentials

credentials = mint(
    manifest_uri,
    scope,
    role_arn="arn:aws:iam::123456789012:role/scene-execution",
    scene_id="scene-run-123",
)

# Pass only these temporary values to the isolated worker.
worker_session_kwargs = credentials.boto3_kwargs()

CompiledPolicy.policy_json is the exact inline policy sent to STS. CompiledPolicy.report() records the single prefix-scoped mode, admitted source prefixes, selected logical and physical objects with their declared versions and hashes, output package, serialized policy size, and the integrity mechanism a runner must enforce. The low-level mint() API only creates the prefix-scoped capability; it does not download or verify selected objects. Callers using it directly must perform the same exact-VersionId and Quilt-hash checks before consuming bytes. Secret credential fields are excluded from repr, but remain bearer secrets and must never be logged or committed.

Run one bounded Scene

A sealed runner scope selects one exact Bedrock foundation-model or account-scoped inference-profile ARN. The selected ARN is the modelId passed to Bedrock; a separate exact bedrock_invoke_resources closure records every IAM resource needed for that invocation. A direct foundation model derives its one-resource closure automatically. An inference profile must explicitly include its own ARN and every foundation-model ARN returned for that profile, including the regionless support ARN used by a global profile. The public runner compiles and mints once, reads and verifies only selected object versions, passes one explicit scoped session to the handler, and publishes a Quilt output Package without listing the bucket:

from pearls import SceneHandlerResult, run_scene


def handler(inputs, session, context):
    prompt = inputs["input-content/prompt.txt"].content.decode()
    response = session.client("bedrock-runtime").converse(
        modelId=context.bedrock_model_id,
        messages=[{"role": "user", "content": [{"text": prompt}]}],
    )
    return SceneHandlerResult(
        objects={"result/response.txt": response["output"]["message"]["content"][0]["text"]},
        invocation={"request_id": response["ResponseMetadata"]["RequestId"]},
        output_meta={"stop_reason": response["stopReason"]},
    )


result = run_scene(
    manifest_uri=manifest_uri,
    scope_spec={
        **scope,
        "bedrock_model": "arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-micro-v1:0",
    },
    role_arn="arn:aws:iam::123456789012:role/scene-execution",
    scene_id="scene-run-123",
    handler=handler,
)
print(result.package_uri)

For an inference profile the routing closure is explicit and fail-closed. The caller declares the expected resources, then supplies a loader backed by GetInferenceProfile in the selected source Region:

import boto3

from pearls import load_bedrock_profile_resources

profile_arn = (
    "arn:aws:bedrock:us-east-1:123456789012:"
    "inference-profile/us.anthropic.claude-3-5-sonnet-20241022-v2:0"
)
profile_scope = {
    **scope,
    "bedrock_model": profile_arn,
    "bedrock_invoke_resources": [
        profile_arn,
        "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0",
        "arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0",
    ],
}
bedrock_control = boto3.client("bedrock", region_name="us-east-1")
result = run_scene(
    # ...the same arguments as above...
    scope_spec=profile_scope,
    bedrock_profile_loader=lambda arn: load_bedrock_profile_resources(bedrock_control, arn),
)

The compiler rejects unrelated resources, sorts the closure, and requires the declared set to exactly match GetInferenceProfile.models; missing, stale, or extra destinations fail before STS minting. The selected profile Region determines the Bedrock Runtime client Region and the selected profile ARN remains the reported modelId. Supporting ARNs are reported separately as IAM authority and are conditioned on the exact selected profile with bedrock:InferenceProfileArn, so they cannot be invoked directly. This follows AWS's inference-profile IAM guidance. Runner-generated policies omit the pinned manifest object's read permission after the trusted compiler has resolved it; handlers receive materialized, hash-checked inputs and never reread that manifest. The standalone compile()/mint() default retains the manifest read for backward compatibility. This conservative narrowing leaves enough of the STS plaintext budget for exact profile closures.

Handlers return bytes or strings, JSON-serializable invocation evidence, and optional run-derived output_meta. Caller metadata and handler metadata are secret-screened, finite-JSON validated, and merged only when their top-level keys are disjoint; a collision is a HandlerContractError, never an implicit winner. The merged document is what workflow schemas and domain invariants validate and what the manifest records. The package always includes handler objects, run/scene.json, and run/invocation.json. See examples/hello_scene.py for a thin one-Converse consumer. Prompt and response bodies remain explicit package objects rather than implicit metadata.

A caller can compose content-aware checks without adding domain vocabulary to pearls. Each checker has a stable identifier and returns all findings it can establish from the final single-revision material:

from pearls import PublicationContext


class RequiredAssessment:
    identifier = "trial-assessment/v1"

    def check(self, pending: PublicationContext, /) -> tuple[str, ...]:
        findings = []
        if "assessment/verdict.json" not in pending.objects:
            findings.append("assessment/verdict.json is required")
        if "pass_count" not in pending.user_meta:
            findings.append("pass_count metadata is required")
        return tuple(findings)


result = run_scene(
    # ...the same arguments as above...
    output_invariants=(RequiredAssessment(),),
)

Workflow validation runs first, then all invariant checkers run over detached, read-only views of the complete object set, merged metadata, message, and output package name. Findings—including a checker that raises unexpectedly—are aggregated in DomainInvariantError; either refusal occurs before any write. The hook is intentionally single-revision: checks needing a prior manifest or lineage state require separately authorized resolution outside this interface.

scene_id is also the collision-safe publication revision identity. A retry of publish_package with the same identity and byte-for-byte intent reuses the recorded revision without multiplying writes; different intent under that identity raises PublicationConflictError. A matching revision that is already latest, or that has been superseded by a newer latest, returns its durable status without consulting a changed or deleted current workflow because no write is possible. An interrupted retry whose original base was absent and remains absent revalidates the workflow and domain invariants before a create-only latest write. A retry whose recorded base was a present S3 VersionId does not replay that transition: S3 destination writes cannot be conditioned on VersionId, so it fails closed as the successful durable status published-not-latest rather than risking an ETag ABA rollback. A deliberate re-execution uses a new Scene ID and appends another revision, even in the same second. Publication writes data, the immutable manifest, and a create-only revision pointer before conditionally advancing latest: first publication uses If-None-Match, later ones use If-Match on the observed ETag. A CAS loser still returns a durable PublicationResult with status="published-not-latest"; it is not reported as a total failure. The revision records its original CAS base ETag and S3 VersionId so a retry can reject a restored, newer latest version even when its body and ETag match the old base. After a conditional conflict the publisher rereads latest, so another retry that advanced the same top hash is reported accurately. PublicationResult.write_disposition separately records whether this invocation performed no successful write, only recovered latest, or entered the package-write path; durable status and invocation-local mutation are intentionally distinct. SceneRunResult.top_hash remains a compatibility property over SceneRunResult.publication.top_hash.

Boot from one genesis package

run_scene_from_genesis() makes one pinned package the complete Scene brief. The package must contain UTF-8 README.md and outputs.json controls, the fixed protocol/requirements.json runtime declaration, and at least one entry below input/. The handler receives only the input/ entries in its inputs mapping and reads the control prompt from context.control_prompt:

{
  "quilt+s3://my-project-bucket/results/#package=scenes%2Frun-123%2Foutput": "result tree",
  "quilt+s3://my-project-bucket/summary.json#package=scenes%2Frun-123%2Foutput": "exact summary"
}

An outputs.json key is an unpinned output destination, not input authority. No URI path grants the whole package, a trailing / grants a logical subtree, and any other path grants one exact logical object. All entries must target one package and must not overlap. Exact-object and subtree selectors may not cover run/scene.json or run/invocation.json; those runner-owned keys are always rejected from returned handler objects, including under a whole-package grant.

The requirements control uses one strict versioned shape:

{
  "$schema": "https://quiltdata.com/schemas/pearls/handler-runtime-requirements/v1",
  "python": ">=3.11,<3.14",
  "imports": [
    {"module": "PIL", "requirement": "Pillow>=10,<11"},
    {"module": "packaging", "requirement": "packaging==26.3; python_version >= '3.11'"}
  ]
}

python is a nonblank PEP 440 specifier. Each imports entry maps one unique top-level Python import identifier to a PEP 508 distribution requirement, so distribution and module names may differ, as with Pillow and PIL. Direct URLs and extras are refused because preflight installs nothing; environment markers are evaluated only in ordinary requirement context, and a context-only marker such as extra is a typed refusal rather than a silent omission. An explicit empty imports array is valid, but the control itself is mandatory. The v1 protocol bounds the UTF-8 source at 65,536 bytes and 128 imports; python, module, and requirement are limited to 256, 128, and 1,024 characters respectively. Installed-version evidence is limited to 256 characters, and a refusal reports at most 20 findings of 512 characters each with an omitted-count suffix. Authoring and runtime use the same limits, while the S3 reader stops after 65,537 bytes instead of buffering an oversized control.

At execution preflight, the current Python version must match, every applicable distribution must be installed at a matching version, and each declared module must be resolvable. Resolution uses find_spec() rather than importing the module body. This proves neither that the named distribution supplied the resolved module nor that import-time Python/native initialization, transitive requirements, or the declaration's completeness will succeed. Pearls does not install or resolve dependencies, validate ABIs, lock the environment, sandbox code, or claim reproducibility.

from pearls import SceneHandlerResult, run_scene_from_genesis


def genesis_handler(inputs, session, context):
    prompt = context.control_prompt
    assert prompt is not None
    return SceneHandlerResult(
        objects={"results/answer.txt": prompt},
        invocation={"input_count": len(inputs)},
    )


result = run_scene_from_genesis(
    manifest_uri,
    role_arn="arn:aws:iam::123456789012:role/scene-execution",
    scene_id="scene-run-123",
    handler=genesis_handler,
    bedrock_model="arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-micro-v1:0",
)

The selected bedrock_model and optional bedrock_invoke_resources are deliberately supplied by the execution plane and recorded separately in the authority report; they are not derived from the sealed genesis package. A direct foundation model needs no explicit resource list. An inference profile requires the same complete exact closure shown above. The trusted bootstrap identity resolves the pinned manifest and reads README.md, outputs.json, and protocol/requirements.json by exact S3 version, verifies their Quilt hashes, and verifies local runtime compatibility before policy compilation or STS minting. scene check, scene run, and programmatic run_scene_from_genesis() share this preflight. Existing immutable Genesis packages without the requirements control fail closed and must be republished as a new pinned revision with an explicit declaration.

Handlers run in-process as trusted execution code: they receive the scoped session that publication later uses, so selector and reserved-key checks constrain returned objects but are not a sandbox against a handler that makes direct S3 calls or retains the session. The lower-level run_scene(manifest_uri, scope_spec, ...) API remains available without a Genesis requirements declaration; its SceneRunResult.runtime_requirements is therefore None.

Command line

Installing the package creates one pearls command with three authority groups:

pearls
  genesis author DIRECTORY                                  operator package authority
  scene   check | run                                       bounded Scene authority
  infra   validate | plan | deploy | status | assume-policy  administrator IAM

The split is deliberate. genesis uses the selected operator identity to author immutable packages, scene operates with bounded short-lived authority, and infra mutates persistent IAM with administrator authority. Shared conventions:

Concern Convention
Credentials Selected with --profile / --region and resolved by boto3; no command accepts key material or tokens
Output Human-readable on stdout by default, --json for a stable versioned document
Confirmation Read-only and --dry-run operations never prompt; mutating operator/admin verbs require confirmation or --yes
Exit codes 0 success, 1 typed refusal or failure, 2 usage error
Safety Receipts omit credentials, object bodies, metadata values, and tokens

Author a genesis package

pearls genesis author freezes one local tree and publishes it with the operator's boto3 identity. A valid tree contains UTF-8 README.md and outputs.json controls, a strict UTF-8 protocol/requirements.json, a selected Python handler that parses and statically binds module-level handler, and at least one regular file below input/. Authoring validates the requirements schema and PEP 440/508 syntax but deliberately does not require the declared interpreter, distributions, or modules on the operator's machine; authoring and execution environments may differ. The selected handler defaults to protocol/handler.py; --handler-key LOGICAL_KEY chooses another safe relative logical key, which must also be supplied to later pearls scene commands. The requirements key remains fixed when a custom handler key is selected. Traversal is recursive and sorted by relative POSIX logical key. Every regular file is read once into an immutable plan; symlinks, devices/FIFOs, unreadable files or directories, unsafe relative paths, and logical-key collisions are refused. Traversal pins root and child directory descriptors so concurrent symlink swaps cannot escape the selected tree. For each regular file, device, inode, mode, size, modification time, and change time must remain identical at discovery, immediately after open, and after the complete read while its descriptor remains open. Authoring fails closed on platforms without descriptor-relative filesystem APIs.

pearls genesis author ./trial001-genesis \
  --destination proj/trial001-worker@quilt-dev \
  --revision-id genesis-trial001-v1 \
  --metadata @genesis-meta.json \
  --message "Author Trial 001 worker genesis" \
  --seal-input 'quilt+s3://source-registry#package=reference%2Frubric&top_hash=<64 hex characters>&path=rubrics%2Ftrial001.json' \
  --yes --json

--destination NAME@BUCKET, --metadata, and a nonblank --message are required. Metadata uses the same inline JSON or @file convention as Scene publication. --revision-id ID optionally supplies the reusable publication identity using the same 2–64 safe-character grammar as Scene publication; latest is reserved. When omitted, a collision-resistant ID is generated and shown in approval material before the first write. Operators should save that ID, or explicitly supply one, so a command restarted after a lost response can assert the same idempotent intent instead of creating a second revision. Reusing an ID with different bytes or declarations is refused. The declaration bound to the create-only revision includes the selected handler key and canonical exact seal provenance, so equal copied bytes cannot relabel an old revision with a different source package, physical VersionId, or handler selection. The destination workflow is the required named workflow genesis by default; --workflow ID selects another named workflow, and there is deliberately no no-workflow escape for authoring.

--seal-input is repeatable and has a dedicated strict grammar:

quilt+s3://REGISTRY#package=NAME&top_hash=64HEX&path=LOGICAL_KEY

The package name and logical key are fragment values and should be percent encoded. Raw leading or trailing URI whitespace is refused; a legitimate space inside a logical key remains valid when encoded as %20. Floating references, hash prefixes, duplicate/extra fields, empty or directory-like paths, malformed encoding, duplicate sources, and destination collisions are refused. A source key X/Y becomes input/X/Y. The operator client reads .quilt/packages/<top_hash>, parses it locally without registry telemetry, recomputes the Quilt top hash, then reads the selected object by its exact S3 VersionId and verifies its declared Quilt content hash. The bytes are copied into the new package; a foreign physical S3 reference is never retained.

--dry-run performs the complete local layout, requirements syntax, selected-handler, sealed-source, destination versioning, and workflow metadata/entries/message/handle validation. It does not test local runtime compatibility. It performs zero writes, never prompts, and reports a non-durable planned_revision_id that can be saved for a later real invocation. The durable revision ID, S3 version identities, top hash, and pinned URI remain null rather than being invented. A real publication prompts only after that preparation unless --yes is present; approval material always includes the planned revision ID before any write. Publication then uses the exact frozen bytes via the same revision-safe publisher used by Scenes. New publications recheck versioning and the named workflow immediately before their first write. A durable, same-intent retry that needs no write returns its stored disposition even if the current workflow changed. An interrupted revision whose original latest base was absent revalidates before a create-only recovery; one whose base was present remains durable but does not replay an ETag-only transition that cannot be conditioned on the recorded VersionId. A durable CAS loser is still exit 0 with status published-not-latest. In JSON receipts, mutations_performed lists publish_package only when this invocation completed a package-path or latest write; completed and superseded status-only retries report an empty list. Human success prints the canonical, round-trippable pinned URI first; package-name slashes are percent encoded.

The authoring identity needs local read access; s3:GetBucketVersioning on the destination; s3:GetObject for current destination workflow/config/schema and package pointers; s3:GetObjectVersion for any workflow schema URL carrying a versionId at its configured bucket/key; s3:PutObject for <name>/*, .quilt/packages/*, and .quilt/named_packages/<name>/*; and, when sealing, s3:GetObject for the foreign manifest plus s3:GetObjectVersion for selected data. It needs no STS, Scene role, worker session, IAM, CloudFormation, bucket listing, delete, or foreign write permission. Authoring validates code identity and static shape, not handler safety: it never executes protocol/handler.py. Bucket Object Lock, bucket policies, lifecycle rules, and cleanup of unreferenced versions after a mid-publication failure remain operator responsibilities.

A sealed-genesis Scene carries its executable protocol at protocol/handler.py, alongside the README.md control prompt, outputs.json write grant, fixed protocol/requirements.json, and input/ entries. pearls scene reads the handler and requirements from the requested pinned revision by exact S3 version and verifies their bytes against the Quilt content hashes in the manifest.

Verifying and executing are deliberately separate steps, because a Python module body runs as soon as it is imported. scene check verifies handler identity and binding statically and uses metadata plus find_spec() for applicable runtime requirements; it imports neither the handler nor a declared module body. scene run performs the same shared requirements preflight before AssumeRole, then executes the handler only after the policy is compiled, the Scene credential is minted, and verified inputs are materialized. Thus malformed or incompatible requirements spend no Scene authority and run no sealed code.

scene check is therefore a read-only preflight. It validates every argument shape, resolves and hash-verifies the genesis controls and sealed handler, verifies local declared runtime compatibility, compiles the session policy, and prints the non-secret authority report. It performs no AssumeRole, no model invocation, no write, and no handler or dependency module-body execution, so a nonzero exit always precedes all of those:

pearls scene check \
  --genesis 'quilt+s3://quilt-dev#package=proj%2Ftrial001-worker&top_hash=<64 hex characters>' \
  --role-arn arn:aws:iam::123456789012:role/scene-execution-haiku \
  --model-arn arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-haiku-20240307-v1:0

For an inference profile, --model-arn remains the one selected modelId and --model-resource-arn is repeated for every foundation model in the expected IAM closure (including a global profile's regionless support ARN). The bootstrap identity calls GetInferenceProfile in the selected source Region and refuses before minting unless the returned destinations match exactly:

pearls scene check \
  --genesis 'quilt+s3://quilt-dev#package=proj%2Ftrial001-worker&top_hash=<64 hex characters>' \
  --role-arn arn:aws:iam::123456789012:role/scene-execution \
  --model-arn arn:aws:bedrock:us-east-1:123456789012:inference-profile/us.anthropic.claude-3-5-sonnet-20241022-v2:0 \
  --model-resource-arn arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0 \
  --model-resource-arn arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0

Each reported check names the identity it needed. Checks labeled trusted-bootstrap used the operator's own read access, because the genesis controls must be read before a Scene credential can exist. The command also prints what it cannot prove: Bedrock account model access, the base role's own permissions, the output registry's workflow contract, handler safety, whether the independently found module is supplied by the named distribution, or whether import-time/native initialization and undeclared transitive dependencies will succeed. Static handler binding also does not prove that the eventual value is callable.

scene run executes one Scene. It calls run_scene_from_genesis() exactly once and adds no authority, materialization, workflow, or publication logic of its own:

pearls scene run \
  --genesis 'quilt+s3://quilt-dev#package=proj%2Ftrial001-worker&top_hash=<64 hex characters>' \
  --role-arn arn:aws:iam::123456789012:role/scene-execution-haiku \
  --model-arn arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-haiku-20240307-v1:0 \
  --metadata @output-meta.json \
  --message "Trial 001 worker Scene run" \
  --json

--scene-id is accepted; when omitted a collision-resistant valid RoleSessionName is generated. It is also the output revision identity, so a new execution needs a new ID and reuse is an explicit idempotency assertion. --metadata takes inline JSON or @path, and maps to the workflow-aware caller half of output_meta; --message and --workflow / --no-workflow map to output_message and output_workflow. The scene check receipt schema is v2 and the run receipt schema is v4. Both include exact requirements-control bucket/key/VersionId, Quilt hash, source SHA-256/size, the actual and required Python versions, and bounded per-import evidence. Each import records marker applicability, distribution-version status, and module-resolution status as separate fields; marker-skipped entries are not-checked, not “compatible.” A summary reports declared, applicable, and skipped counts, and both receipt types carry explicit limitations including the unproven distribution-to-module association. The run receipt additionally carries the full non-secret compiled authority report and contains only non-secret identity: Scene ID, genesis URI/top hash, verified handler provenance, assumed-role ARN, immutable package URI/top hash, revision and S3 version identities, whether the revision was reused, latest disposition, and timestamps. Its top-level status is published or published-not-latest; either is exit 0 because the revision is durable. Metadata from both caller and handler is reported by key only. scene check cannot know handler-derived keys because it never executes the handler, and the CLI does not dynamically load programmatic output_invariants. Diagnostics go to stderr so --json stdout stays parseable.

Two boundaries survive the CLI unchanged. The trusted bootstrap identity reads the pinned manifest and the sealed control objects by exact version before minting anything; --profile and --region apply to those reads and to AssumeRole, never to the Scene session, which is built only from the minted credential and the selected model/profile ARN's region. The trusted-handler boundary is unchanged too: verifying the handler's hash proves code identity, not code safety. During a run the handler executes in-process and receives the scoped session that publication later uses. Ordering execution after the authority checks limits when sealed code runs, not what it can do once it runs. The command claims no sandboxing and no ambient credential isolation.

Mapping to the four Scene roles

Scene role AWS-native enforcement
Input Control Selected logical keys determine the descriptors the runner materializes. IAM grants one physical prefix per source registry, so it bounds blast radius but does not carry the manifest.
Input Content The runner requests every selected object with its declared VersionId, verifies the returned version, and verifies the bytes against the declared Quilt hash before invoking the handler.
Output Content s3:PutObject is allowed only on the declared data selectors in <package-name>/ plus the runner-owned records.
Output Control Quilt manifest writes are limited to .quilt/packages/*; named-package writes are limited to .quilt/named_packages/<package-name>/*, with exact reads of that package's latest pointer and the current Scene ID's revision pointer for CAS and idempotency. Sibling revision pointers remain unreadable.

The session policy uses resource-scoped Allow statements plus a standalone explicit deny for every s3:DeleteObject* and s3:ListBucket* action. Its intersection with the base role excludes all other actions and resources absent from either policy. The policy grants no IAM, STS, ACL, bucket-policy, or input-write action. Input policy size is O(source registries), not O(selected objects): each registry contributes one common physical prefix. Compilation refuses a registry whose selected objects share only the bucket root or whose common prefix overlaps .quilt/; it never widens those cases to bucket-wide or Quilt-metadata read authority. Output write isolation and exact Bedrock model/profile scoping are unchanged.

A prefix-scoped credential can read known objects under an admitted source prefix that the manifest scope did not select. This is an explicit accepted risk and is reported by scene check under unproven. It does not weaken delivered-input provenance: the runner exposes only selected bytes whose exact S3 version and Quilt hash both verify. The trusted in-process handler receives the same session, so Pearls does not claim to prove the absence of extra SDK reads.

The compiler deliberately does not add a blanket Bedrock Deny with NotResource set only to the selected ARN: that would also deny the supporting foundation resources Bedrock evaluates for a profile. Instead it emits one exact allow for the selected profile and a separate support-resource allow conditioned with StringLike on bedrock:InferenceProfileArn. The value is an exact ARN with no wildcard, so a handler cannot invoke a support model directly. The declared closure must match the source Region's GetInferenceProfile response exactly, and every statement counts toward the 2,048-character STS limit; a closure that cannot fit is refused before minting. The base-role/session-policy intersection excludes every resource outside the operator inventory and this verified per-Scene closure.

Because a session policy can only narrow its base role, the agent cannot add authority during the session. Promotion of output into a later Scene's inputs remains separately authorized.

Workflow contracts on output registries

Governed buckets register Quilt workflows (.quilt/workflows/config.yml) and can require every package to name one and satisfy its JSON Schemas. Publication honors that contract with quilt3 semantics, using only the caller-supplied S3 client (a scoped Scene session validates against exactly the registry its credentials can see):

  • workflow=... (default) resolves the registry's default_workflow and respects is_workflow_required (true by default when a config exists); workflow=None opts out explicitly and is refused when required; a named workflow must exist in the config.
  • The caller and handler metadata maps are merged with collision refusal; that final package user_meta is validated against the workflow's metadata_schema, the entry list against its entries_schema, the commit message against is_message_required, and the package name against handle_pattern.
  • Workflow validation happens first, then every caller-supplied PublicationInvariant checks the same final metadata plus object bytes, logical keys, message, and package name. Workflow refusal raises WorkflowContractError; invariant findings are aggregated in DomainInvariantError. Both leave nothing behind because they run before the first write.
  • A conforming publication stamps {id, config, schemas} (S3 URIs with version IDs) into the manifest header exactly as quilt3 writes it, so catalogs and quilt3 read the package as having passed the contract it actually passed.
  • Schema documents must be reachable by the publishing credentials; under a Scene session that means the output registry's own .quilt/workflows/* prefix (already part of the compiled read scope). $ref is rejected and only draft-07 meta-schemas are supported, for parity with quilt3's validator.
  • Contract objects must themselves be versioned: a config or schema without an S3 version ID (written before versioning was enabled, or while suspended) is refused, because the stamp could then only reference mutable latest content instead of the exact contract that was validated. This is stricter than quilt3, in the refuse direction only.

run_scene and run_scene_from_genesis expose these as output_meta, output_message, output_workflow, and output_invariants.

Policy-size behavior

STS accepts at most 2,048 plaintext characters and also applies an undocumented compressed packed-policy budget. Input authority contributes one prefix per source registry regardless of selected-object count; selected VersionIds and object ARNs are absent from the policy. Exact output and Bedrock resources remain part of the measured document, and compilation refuses plaintext above the configured limit.

The packed percentage is known only after AssumeRole. If AWS returns PackedPolicyTooLarge, Pearls raises PackedPolicyTooLargeError with the selected object count, prefix count, and plaintext size instead of presenting the plaintext preflight as proof that minting will succeed. There is no policy-mode fallback: every successful compilation uses the same prefix-scoped read model.

Base role

infra/scene-execution-role.yaml is an administrator-applied CloudFormation template. It creates no buckets and the Python module never creates IAM resources. The role is broad for reads and writes within a controlled project-bucket ARN pattern so the session policy provides the per-Scene bound, but it deliberately has no object or version delete permission. A caller that assumes the role without RAJA's session policy therefore cannot turn the append-only execution role into cleanup authority.

An administrator can deploy it with an account-specific principal and bucket pattern:

aws cloudformation deploy \
  --template-file infra/scene-execution-role.yaml \
  --stack-name pearls-scene-execution \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides \
    OrchestratorPrincipalArn=arn:aws:iam::123456789012:role/orchestrator \
    ProjectBucketArnPattern=arn:aws:s3:::my-project-* \
    BedrockInvokeModelArns=arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-micro-v1:0,arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-haiku-20240307-v1:0

BedrockInvokeModelArns is a required CommaDelimitedList. Each element must be an exact commercial-partition foundation-model or account-scoped inference-profile ARN; wildcards, custom models, other partitions, and empty entries are rejected. A profile inventory must also enumerate every exact regional foundation-model ARN that Bedrock can route it to. The base list is an operator inventory, not a Scene selection: each sealed Scene still selects one model/profile and the effective permission is the intersection of this inventory with that Scene's exact session policy. A missing inventory member therefore fails closed.

The orchestrator needs sts:AssumeRole permission as well as the role's trust. The template targets the commercial aws partition in 0.1.1. mint(external_id=...) is available for separately managed roles whose trust policy requires sts:ExternalId; the shipped same-account template does not require one. The worker must not be able to retrieve broader ambient task, instance, or execution-role credentials; otherwise it can bypass the scoped credentials entirely.

Deploying execution roles with pearls infra

The same template ships inside the wheel, so an installed Pearls can deploy it without hand-assembling the parameters, remembering the named-IAM capability, or keeping the stack name and RoleName aligned. A byte-identity test keeps the packaged copy and infra/scene-execution-role.yaml the same file, so the installed CLI cannot deploy a role that differs from the one reviewed here.

pearls infra validate is fully offline. It parses the template, checks any supplied values against the template's own AllowedPatterns and numeric bounds, and checks the consistency the template cannot express. Every selected profile or directly invocable foundation model must match the deployment Region. An out-of-Region or regionless foundation ARN is accepted only when its model ID and geographic shape can support an admitted profile in the deployment Region. Because exact destinations are source-Region and profile specific, this offline command does not claim routing membership; scene check verifies the per-Scene closure against GetInferenceProfile before STS minting:

pearls infra validate \
  --orchestrator-principal-arn arn:aws:iam::123456789012:user/orchestrator \
  --project-bucket-arn-pattern 'arn:aws:s3:::quilt-dev-*' \
  --bedrock-model-arn arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-haiku-20240307-v1:0 \
  --bedrock-model-arn arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-micro-v1:0 \
  --role-name scene-execution-model-inventory \
  --region us-east-1 \
  --lint

pearls infra plan creates a change set, prints the parameter diff, the resource changes, and the rendered trust policy and inline role policy, then deletes the change set. It never executes it and changes no stack resource. A change set that is never executed is always deleted before the command exits, whether it was planned, declined, or failed to launch; only an executed one is left in the stack's history. Planning a stack name that does not exist yet is the one visible side effect: CloudFormation creates the stack in REVIEW_IN_PROGRESS with no resources, and the command says so, because no pearls verb has delete authority. A later deploy of the same stack name proceeds normally. pearls infra deploy renders the same review material, requires confirmation unless --yes, passes CAPABILITY_NAMED_IAM explicitly, waits for a terminal stack status, and prints SceneExecutionRoleArn in a form usable directly as pearls scene run --role-arn. Re-running deploy with unchanged parameters is a reported no-op rather than an error, and a failed or rolled-back deployment exits nonzero with the CloudFormation failure reason preserved.

The Trial 001 two-model project can use one role inventory. Repeat --bedrock-model-arn; the CLI sorts and serializes the values for the CommaDelimitedList, so argument order does not create a noisy parameter diff:

pearls infra deploy \
  --orchestrator-principal-arn arn:aws:iam::123456789012:user/orchestrator \
  --project-bucket-arn-pattern 'arn:aws:s3:::quilt-dev-*' \
  --bedrock-model-arn arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-haiku-20240307-v1:0 \
  --bedrock-model-arn arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-micro-v1:0 \
  --role-name scene-execution-trial001 \
  --region us-east-1 --yes

The orchestrator now needs sts:AssumeRole on that one role. pearls infra assume-policy generates the document from deployed stacks and prints it by default; applying it requires an explicit flag and confirmation, and warns that it replaces any existing inline policy of that name:

pearls infra assume-policy \
  --from-stack scene-execution-trial001

pearls infra assume-policy \
  --from-stack scene-execution-trial001 \
  --apply-to-user orchestrator --yes

pearls infra status --stack-name scene-execution-trial001 reports the stack status, parameters, and drift-relevant identity: role ARN, complete admitted Bedrock resource list, bucket pattern, and maximum session duration. Plan, deploy, no-op, and status JSON expose the inventory as a logical list even though the CloudFormation API transports it as one comma-delimited string.

Three limits are reported by the commands themselves rather than papered over. Bedrock account-level model access cannot be provisioned by this template and remains a console prerequisite; the role alone is not sufficient. The trust policy admits the orchestrator principal with no sts:ExternalId condition, while mint(external_id=...) exists for separately managed roles. The template targets the commercial aws partition only. These commands also never delete a stack or a role: there is no cleanup authority here.

Live AWS authorization proof

tests/aws/test_authorization.py is an opt-in proof against real IAM, STS, and S3. An unrestricted session of the same base role first proves that the denied read/version and outside-write operations are otherwise allowed, and that even a bare base session cannot delete versions. The scoped session then proves an in-scope version read; versioned reads of a wrong existing version inside an admitted prefix; reads of the pinned input manifest, output workflow configuration, and exact output latest pointer; data, manifest, and named-pointer writes; and denial of current (unversioned) data reads, reads outside admitted prefixes, outside writes, listing, object deletion, and version deletion. PEARLS_DENIED_READ must name an existing version outside every admitted source prefix; the harness verifies this precondition. A separate administrator-managed cleanup role removes the exact disposable versions in finally; cleanup authority is never present on the Scene role or scoped worker. The harness requires versioned disposable buckets, does not provision infrastructure, and never runs merely because AWS credentials are present.

Configure an administrator-owned sandbox, then run:

export PEARLS_RUN_AWS_PROOF=1
export PEARLS_MANIFEST_URI='quilt+s3://...#package=...&top_hash=...'
export PEARLS_SCOPE_FILE="$PWD/examples/scope.json"
export PEARLS_ROLE_ARN='arn:aws:iam::123456789012:role/scene-execution'
export PEARLS_CLEANUP_ROLE_ARN='arn:aws:iam::123456789012:role/scene-proof-cleanup'
export PEARLS_ALLOWED_READ='s3://bucket/key?versionId=expected-version'
# A different selected key paired with the expected version ID; S3 must return NoSuchVersion.
export PEARLS_CROSS_OBJECT_VERSION_READ='s3://bucket/other-selected-key?versionId=expected-version'
export PEARLS_WRONG_VERSION_READ='s3://bucket/key?versionId=other-existing-version'
# Must reference an existing object version outside every admitted source prefix.
export PEARLS_DENIED_READ='s3://bucket/outside-prefix/sibling-control.json?versionId=existing-version'
export PEARLS_WORKFLOW_READ='s3://output-bucket/.quilt/workflows/config.yml'
export PEARLS_OUTPUT_LATEST_READ='s3://output-bucket/.quilt/named_packages/scenes/example/output/latest'
export PEARLS_ALLOWED_WRITE='s3://output-bucket/scenes/example/output/proof.txt'
export PEARLS_DENIED_WRITE='s3://output-bucket/outside-output/proof.txt'
# Runner proof: use a scope with an exact bedrock_model and a disposable output package base.
export PEARLS_RUNNER_SCOPE_FILE="$PWD/examples/scope.json"
export PEARLS_RUNNER_PROMPT_KEY='input-content/prompt.txt'
export PEARLS_ALTERNATE_MODEL_ID='arn:aws:bedrock:us-east-1::foundation-model/another-model'
export PEARLS_TRANSCRIPT="$PWD/evidence/aws-taj/aws-proof-transcript.jsonl"
pytest -m aws tests/aws/test_authorization.py

The cleanup role is sandbox-only and should grant version deletion solely for the disposable proof resources. Set PEARLS_CLEANUP_EXTERNAL_ID if its trust policy requires a distinct external ID.

The JSONL transcript preserves operation outcomes, HTTP status codes, AWS request IDs, assumed-role audit metadata, and the compilation report. It does not record object bodies, access keys, secret keys, or session tokens. See evidence/aws-taj/README.md before preserving a run.

Security and maturity boundaries

  • pearls scene run verifies the sealed handler's bytes against the pinned manifest before executing them, which establishes code identity, not code safety. The handler runs in-process with this process's authority and receives the scoped Scene session that publication later uses. Installing the CLI does not add a sandbox; a genesis package you would not run by hand is equally dangerous through the command. pearls scene check does not import the handler at all, so a preflight of an untrusted package executes none of it, but that makes check a safe inspection step rather than a safety guarantee about the subsequent run.
  • Runtime preflight checks the current Python version, installed distribution metadata, and top-level module resolvability only. It does not install or resolve packages, prove a distribution supplied the found module, execute import-time/native initialization, validate undeclared transitive dependencies, or establish a reproducible environment.
  • pearls infra verbs mutate persistent IAM with administrator authority and are kept in a separate group for that reason. validate and plan change nothing, deploy and assume-policy --apply-to-* require confirmation or --yes, and no verb can delete a stack, a role, or an object.
  • The publisher requires output bucket versioning before its first write and requires every S3 write to return a non-null version ID. Each invocation gets a create-only revision pointer; a present-base latest update performs an exact ETag-and-VersionId reread followed by a final ETag/value compare-and-swap. That reread catches a same-body generation change during package writes, but S3 cannot fence another same-body rewrite in the final read-to-write interval because destination PutObject has no VersionId condition. Quilt's raw top-hash pointer cannot carry a unique generation token, so is_latest reports current package content rather than ownership of one pointer version; exact generation fencing would require another coordination protocol. Concurrent writers can create unreferenced object versions, but both the CAS winner and loser retain manifests and create-only revision pointers that pin their exact data versions. The loser returns published-not-latest rather than disguising a durable revision as total failure. Bucket Object Lock or bucket-policy controls remain necessary to prevent unrelated principals from replacing or deleting versions.
  • The broad .quilt/packages/* output namespace is required because a new manifest's content hash is unknown before the worker builds it. Quilt3's content-addressed API contract derives that key from the manifest bytes; enforcing the contract against raw S3 callers belongs to a separate system boundary. The worker may read the exact pinned input manifest, output workflow namespace, exact output latest pointer, and only its selected revision pointer for consumer, CAS, and idempotency paths. Sibling revisions and ListBucket remain denied. Quilt 8's public Package.push() performs an optional manifest listing after the write to render a short hash, so a no-list integration must suppress or replace that presentation step; do not grant broad listing merely for display.
  • Multipart create/upload/complete operations use s3:PutObject, but s3:AbortMultipartUpload is intentionally absent. Failed multipart uploads can leave billable incomplete parts; output buckets should configure an AbortIncompleteMultipartUpload lifecycle rule.
  • The compiler emits commercial-partition (arn:aws) S3 resources in 0.1.1 and rejects GovCloud/China role ARNs rather than minting mismatched policy.
  • The compiler bounds a namespace; it does not prove that an output Package is a lineage descendant. Scene sealing/reconciliation must establish that relationship.
  • KMS permissions are intentionally absent. SSE-KMS inputs or outputs need a separately designed key policy and session-policy extension; do not add broad KMS authority casually.
  • CloudTrail records AssumeRole as a management event. S3 object operations require separately configured S3 data-event logging and may incur charges; they are not present in ordinary event history automatically.
  • Explicit read/write/list/delete denies preserve those boundaries even when a bucket policy grants directly to the role session. Administrators must still avoid granting unrelated S3 actions to sessions; permissions boundaries, service control policies, and VPC endpoint policies may further restrict access.
  • STS credentials cannot revoke already-started work immediately. Use short durations and require the trusted orchestrator to recompile before refresh.
  • This release has comprehensive offline policy/API tests. The live proof is environment-gated because CI has no AWS sandbox; do not treat a mocked test as evidence of AWS's authorization behavior.

The set-based version guard relies on Amazon S3 assigning a unique version ID to each stored object version, as documented in Retaining multiple versions of objects with S3 Versioning. Quilt manifest loading follows the public Package.browse API. Content was rephrased for compliance with licensing restrictions.

License

Apache License 2.0. See LICENSE and NOTICE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pearls-0.3.0.tar.gz (205.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pearls-0.3.0-py3-none-any.whl (114.2 kB view details)

Uploaded Python 3

File details

Details for the file pearls-0.3.0.tar.gz.

File metadata

  • Download URL: pearls-0.3.0.tar.gz
  • Upload date:
  • Size: 205.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.17 {"installer":{"name":"uv","version":"0.9.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for pearls-0.3.0.tar.gz
Algorithm Hash digest
SHA256 e23ad1054da56d3066c0b533edcfb5dd5705fc3eafd7ab0eee1b7caf679c4686
MD5 ee3ebbf34a58f9f1baeb4cb7e6afb47f
BLAKE2b-256 20cc7ecaefd98d24c6c25d2018cc3f9d4b90a261058458b4e11591af5523f74d

See more details on using hashes here.

File details

Details for the file pearls-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: pearls-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 114.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.17 {"installer":{"name":"uv","version":"0.9.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for pearls-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 54c63d2b5cabf74fc433a08125308a603ea8c677d06414c8849c41029da03dd6
MD5 b666c3b666657829ff294830b8df401b
BLAKE2b-256 910445ab97d6233d9a33335bb56f4d99474771c7268bedc577ae17b62277eaf3

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 files

0.2.1

2 files

0.2.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page