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}
- Run
opaque --json scope plan --manifest scope.json. Save the returneddocument.round_id. - 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. - Run
opaque --json scope activate ROUND_ID. Save the scope ID. Activation issues authority and does not send a provider write. -
Prepare a change using
opaque --json scope prepare --manifest action.json: -
Review that returned round on the enrolled workstation using the same
scope-reviewcommand. The signed document binds the before state, requested status, resource version, scope, policy, reviewer, device and deadline. - Run
opaque --json scope execute --round-id ACTION_ROUND_ID --issuance-round-id ISSUANCE_ROUND_IDonce. 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.