Skip to content

Run a bounded scope workflow

Operator-selected limits can also use a pinned AuthorityPolicy YAML/JSON manifest. Existing [scope_workflow] TOML remains supported for the support-case kind.

The opt-in scope workflow lets a person approve a finite set of resources, the values an agent may write to them, an expiry and an attempt budget. The broker prepares each action against the provider's current resource version, checks current authority, and records one dispatch attempt. The default also requires a separate human review for each exact action. One broker runs one operation kind: support-case status changes against the fixed REST contract below, or a GitHub Actions staging dispatch.

This workflow is available from v0.6.0 through opaque scope. The support connector implements the fixed REST contract below; it is not a Zendesk or Salesforce adapter. Automated tests use synthetic providers and synthetic reviewer signatures. They do not certify a live provider, native human presence, or production capacity.

Prerequisites

Use an isolated tenant broker with sealed configuration, enforced service-account custody, required identity, and native approval. The requester must have the operator role. Configure a distinct human approver with the approver role and an enrolled workstation whose public key and principal match the workflow config.

The provider credential belongs to the broker account in a private regular file. The agent must not have that credential or an alternate write path. Enroll the workstation using the existing pinned-TLS opaque-approver enroll flow before requesting a scope. Run the approver on that trusted workstation outside delegated agent sessions.

Add a configuration section to the broker's trusted configuration, then seal it using the deployment's normal configuration process:

[scope_workflow]
reviewer_id = "human:YOUR_ENROLLED_PRINCIPAL"
reviewer_public_key = "YOUR_64_CHARACTER_PUBLIC_KEY"
generation = 1
max_scope_seconds = 3600
max_attempts = 100
max_resources = 100
exact_action = true
allowed_statuses = ["open", "resolved", "closed"]

[scope_workflow.profile]
endpoint = "https://support-api.example.com/v1/"
token_file = "/var/lib/opaque/support-api.token"
# Optional: use only these operator-owned PEM trust roots for this connector.
# ca_certificate_file = "/var/lib/opaque/support-api-ca.pem"

Use actual principal identifiers from the deployment. The example public key and principal are placeholders, not runnable credentials. The endpoint must be HTTPS, end in /, and have no credentials, query or fragment. Proxy use, redirects and transport retries are disabled. The token file must be owned by the broker, have no group/other permissions, and not be a symbolic link or multiply linked file.

The broker loads the credential at startup and binds its value into the provider profile digest without exposing a separate credential fingerprint. After rotating the credential, restart the broker and obtain a fresh human-approved scope. Old scopes and action approvals cannot authorize work with the replacement credential, even when the endpoint, credential path and case IDs are unchanged. Editing the file alone does not change the credential already loaded by a running broker.

For a private HTTPS provider, configure ca_certificate_file in the sealed profile. The broker loads one to sixteen PEM certificates from an absolute, broker-owned mode-0600 regular file of at most 64 KiB, without symbolic or hard links. Configured certificates replace built-in public roots for this connector; hostname and certificate validation remain required. Omitting the field uses the built-in public roots.

The loaded CA file bytes and path are bound into the provider profile. Changing the file requires a broker restart and fresh scope approval; old scope/action approvals cannot authorize writes under the replacement trust. Editing the file alone does not change the running connector. This support is available from v0.6.0; local TLS fixtures exercise the configured path, not a customer PKI deployment.

Provider contract

GET {endpoint}cases/{id} returns exactly an object with id, status and version. Status is open, resolved or closed. IDs and versions are at most 128 ASCII alphanumeric, hyphen or underscore characters. Reads are bounded to 16 KiB and eight seconds.

The only write is PATCH {endpoint}cases/{id} with {"status":"resolved"}, If-Match: "version", and an Idempotency-Key equal to the broker action ID. The API must enforce the version precondition atomically. Its 409 and 412 responses must mean that no change was applied. A successful HTTP response is recorded as api_accepted; it does not establish downstream completion. All other write responses and transport uncertainty become unknown.

A production adapter must qualify this contract against its selected API before live use. No caller can select an arbitrary URL, method, header or request body.

Dispatch a GitHub Actions staging workflow

Pin a github.workflow.dispatch manifest whose workflows list names every repository, workflow file and branch this broker may ever dispatch, and point [authority_policy.profile] at the GitHub API with a broker-owned token file. The broker uses the same GitHub protocol module as the StagingRelease task family; it adds no second GitHub client. Reviewer enrollment, sealed configuration and identity requirements are unchanged.

A scope names targets as {repository}:{path}:{ref} and carries no statuses:

{"resources":["example-org/service:.github/workflows/staging.yml:main"],"expires_in_secs":900,"max_attempts":2}

A target outside the manifest's workflows list is refused at scope plan, so another repository, a production workflow or another branch never reaches the reviewer. The single approved field is ref, whose only allowed values are the branches of the approved resources; a scope prepare for a resource outside the approved scope is refused before any GitHub request:

{"scope_id":"SCOPE_ID","issuance_round_id":"ISSUANCE_ROUND_ID","resource":"example-org/service:.github/workflows/staging.yml:main","request_id":"staging-2026-09-26-1"}

Before the exact-action review the broker performs three bounded reads: the workflow must be active at exactly that path, the branch must resolve to a head commit, and no tag may share the branch name. The signed review shows head_sha, branch_protected, workflow_id and workflow_state as before state, the head commit as the resource version, and ref as the written field.

scope execute re-reads the branch head and refuses to send if it moved since review; that outcome is rejected and nothing was dispatched. GitHub does not enforce this precondition, so a head that moves between the re-read and the POST is not caught. The one write is POST /repos/{owner}/{repo}/actions/workflows/{file}/dispatches with body {"ref":"main"} and no inputs, under GitHub REST API version 2026-03-10. GitHub answers 200 with the new run's id: the broker records api_accepted and writes the run id to its log. The scope ledger does not retain the run id. api_accepted is an acknowledgment, not a deployment or a success. The older API answer, 204 with no body, is also api_accepted. A 4xx means GitHub validated and refused, recorded as rejected.

Every other answer, including a 200 without a usable run id, 5xx, a timeout and a closed connection, is unknown. The attempt stays charged and replaying the round returns the retained record without another POST. Because workflow_dispatch has no idempotency key, an unknown dispatch may or may not have started a run. opaque scope outcome states this on stderr for an unknown dispatch; inspect the repository's Actions runs before requesting a new dispatch. Correlating such a run to an action is by branch and time only, because the broker sends no inputs; the task family's run-name correlation does not apply here.

Automated tests cover this path against a synthetic HTTPS GitHub that answers as API version 2026-03-10 documents. One maintainer test of v0.6.0 against github.com, in a private repository and not public evidence, dispatched twice under one approved scope: GitHub created exactly two runs, and v0.6.0 retained both as unknown because it accepted only 204 (#148). The fix is unreleased, and no live dispatch has been performed with it yet.

Review and execute

Run the opaque scope commands inside the authenticated delegation. Create a scope manifest, for example (support kind; the dispatch kind omits statuses):

{"resources":["case-101","case-102"],"statuses":["resolved"],"expires_in_secs":900,"max_attempts":2}
  1. Run opaque --json scope plan --manifest scope.json. Save the returned document.round_id.
  2. On the enrolled workstation, run opaque-approver scope-review --state-dir /path/to/workstation-state --round-id ROUND_ID. The native prompt shows the complete signed scope. Approval and rejection both produce signed decisions.
  3. Run opaque --json scope activate ROUND_ID. Save the scope ID. Activation issues authority and does not send a provider write.
  4. Prepare a change using opaque --json scope prepare --manifest action.json:

    {"scope_id":"SCOPE_ID","issuance_round_id":"ISSUANCE_ROUND_ID","resource":"case-101","status":"resolved","request_id":"case-101-resolution-1"}
    
  5. Review that returned round on the enrolled workstation using the same scope-review command. The signed document binds the before state, requested status, resource version, scope, policy, reviewer, device and deadline.

  6. Run opaque --json scope execute --round-id ACTION_ROUND_ID --issuance-round-id ISSUANCE_ROUND_ID once. Inspect the returned action record.

Rounds last at most five minutes. A stale, rejected or missing decision never becomes approval. The approver sends one decision POST; uncertain acknowledgment is resolved with a receipt read, never another submission. scope-list returns at most one complete pending document per call; known round IDs remain directly addressable. scope-receipt retrieves a retained signed decision and does not claim execution occurred.

If the operator deliberately sets exact_action = false, the approved issuance permits opaque scope run --manifest action.json within the same fixed limits. The broker still reads the case version, checks identity and enrollment, charges capacity and claims dispatch. This setting is an explicit policy choice, not a model recommendation. Required model-evaluator contracts are not accepted by this initial workflow.

Inspect and stop work

opaque --json scope show SCOPE_ID returns the retained grant and charged budget. opaque --json scope outcome --scope-id SCOPE_ID --request-id REQUEST_ID reads a charged request's outcome. opaque scope revoke SCOPE_ID blocks subsequent claims, including when the original reviewer is no longer eligible. An already claimed provider request may still finish.

Attempts are never refunded. Replaying a consumed action cannot cause another send. A crash turns unfinished reservations or claims into unknown; startup cancels pending review rounds. Recovery uses retained outcomes, not automatic provider retries. Local dispatch accounting alone cannot promise exactly-once external effects.

opaque --json scope snapshot requires an auditor or admin delegation. It returns bounded, tenant-owned historical projections and total counts; a partial window cannot establish that an omitted action was never attempted. Enterprise consumers use this authenticated RPC and do not open the broker database.

The first runtime supports one owner, one reviewer, one fixed connector and root grants with no child delegation. The underlying library supports narrowing and shared ancestor budgets, but those delegation routes are not exposed here. There is no cross-broker budget, failover, quorum, or automatic retry. A complete retained ledger export supports offline accounting verification; signed review receipts must still be retained separately. Keep existing credential custody supported; new developer password-broker expansion remains deferred.