Unpublished SDK candidate: this package guide reflects the 13 September source snapshot. See availability and source before running candidate commands.
CPRa external worker library¶
This independently versioned Go 1.25 module runs operator-owned Go handlers in a
separate process. Every Go implementation file requires -tags externaljobs.
Importing this package without the tag fails at compile time. The separate SDK
contains the tagged low-level protocol client; normal SDK applications do not
depend on this module or bbolt.
Qualification: the tests exercise protocol fixtures and real worker-process termination. CPRa's management/external-worker server contracts are a release prerequisite; fixture success is not evidence of production-server integration, provider certification, or a published module version.
Add the module to your worker application¶
Use Go 1.25 or newer. The worker is a library, so create your own executable and
add the module with go get; importing it does not start a service. Once a
qualified version is published, replace the placeholders with the exact SDK and
worker versions listed as compatible in that release:
mkdir cpra-worker
cd cpra-worker
go mod init example.com/team/cpra-worker
GOWORK=off go get github.com/ziad-hsn/cpra/sdk/go@'REPLACE_WITH_PUBLISHED_SDK_VERSION'
GOWORK=off go get github.com/ziad-hsn/cpra/sdk/go/worker@'REPLACE_WITH_PUBLISHED_WORKER_VERSION'
go build -tags=externaljobs .
Save the complete program below as main.go before the build command. The
candidate v0.1.0-rc.1 requirements do not mean these versions are already
downloadable. In this source checkout, use the repository's temporary workspace
for SDK/worker development until the nested modules are published. Published
consumers must not need that workspace or a local replace directive.
The worker module ships its own MIT license, package overview, and
examples. Every Go file, including doc.go and
example_test.go, requires the build tag. The server separately
requires externaljobs, runtime enablement, and worker authorization.
Start a worker¶
Provision a private, 32-byte raw wrapping-key file outside the state directory.
Use resolved absolute paths for both the state directory and wrapping key:
neither the final component nor any ancestor may be a symbolic link. On macOS,
for example, resolve a /var/... path to its actual /private/var/... location
before configuring the worker. The state path must be owned by the service
identity, mode 0700 on Unix; the key must be 0600. On Windows, the state/key
DACL may grant access only to
the process identity and SYSTEM. The library creates a protected inheritable
Windows state-directory DACL and checks existing ownership/access rules. Native
Windows qualification is separate from cross compilation.
The key protects a data-key envelope, encrypted results, and replay metadata. It is never generated or replaced automatically. Back up the stopped complete worker state directory, and retain the matching key separately. The worker identity and CPRa store/restore identity must match on every restart. A restored or replaced server identity requires operator reconciliation; changing the configured identity must never silently discard an old outbox.
//go:build externaljobs
package main
import (
"context"
"log"
"os"
"os/signal"
cpra "github.com/ziad-hsn/cpra/sdk/go"
"github.com/ziad-hsn/cpra/sdk/go/api"
"github.com/ziad-hsn/cpra/sdk/go/worker"
)
func main() {
client, err := cpra.NewWorkerClient(cpra.Config{
BaseURL: "https://cpra.example.net",
AuthToken: os.Getenv("CPRA_WORKER_TOKEN"), // scoped worker principal
})
if err != nil { log.Fatal(err) }
defer client.CloseIdleConnections()
registry := worker.NewRegistry()
err = registry.Register("example/health", "1", "check",
func(ctx context.Context, job worker.Job) (api.Outcome, error) {
// Call your provider here using worker-local configuration and
// job.Credentials. This example deliberately makes no health claim.
return api.Outcome{Status: "noData", Diagnostic: "handler not configured"}, nil
})
if err != nil { log.Fatal(err) }
runner, err := worker.New(worker.Config{
Client: client, Registry: registry,
WorkerID: "checks-west", ServerID: "<verified-server-store-epoch>",
StateDir: "/var/lib/cpra-example-worker",
WrappingKeyPath: "/etc/cpra-example-worker/wrapping.key",
})
if err != nil { log.Fatal(err) }
defer runner.Close()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
if err := runner.Run(ctx); err != nil { log.Print(err) }
}
Read the program as three separate responsibilities:
NewWorkerClientauthenticates as a worker principal. It talks to the worker protocol; it does not provision JobTypes or grant itself management access.Registry.Registerbinds a descriptor to a function compiled into this executable. The three strings must match the operator-provisioned JobType. The example handler returnsnoDatadeliberately: replace its body with an actual health observation before assigning work to it.worker.Newopens and checks the encrypted journal.Run(ctx)admits work, invokes registered handlers, records outcomes, and delivers those outcomes. Cancelling the context requests shutdown. The application owns signal handling and its supervisor owns the final process deadline.
There is no success output for the placeholder handler. A compatible server may
assign a check and receive noData; that proves no target health. To test handler
registration without a server, run the executable example below. The complete
DAO/SMS integration in the repository shows real HTTP handler bodies against
explicit local fixtures before configured deployment.
Expected example output is check <nil>, recovery <nil>, and
notification <nil>. These values mean the registry accepted each handler. The
example does not obtain a start grant or invoke any external operation.
Register exact JobType ID/version/category matches before calling Run. The
registry freezes when execution begins. The SDK never downloads or executes
handler code. Versioned descriptors are separately provisioned through an
operator client; advertising a capability does not grant permission to use it.
| Handler category | Confirmed statuses | Unavailable/uncertain statuses |
|---|---|---|
check |
success, failure |
noData (unknown/rejected normalize to noData) |
recovery |
accepted, completed |
unknown, rejected |
notification |
accepted, delivered |
unknown, rejected |
The runner fills execution/grant/category identity. A returned error, panic,
invalid status, or oversized result becomes unknown (noData for checks).
Handler error text is not persisted because it may expose provider secrets.
accepted distinguishes provider acceptance from observed completion/delivery.
Rejection codes do not independently authorize retries; the server enforces its
registered classification and current policy.
Use CredentialResolver to resolve an opaque profile name locally. Resolved
values and assignment parameters are never written to the journal. Do not put
provider secrets in outcome data, diagnostics, or evidence. Such payloads are
encrypted locally but are deliberately delivered to CPRa when returned.
Execution and restart contract¶
- Reserve journal record and byte capacity before requesting a start grant.
- Persist the grant/execution marker before invoking the handler.
- Persist the resulting bounded envelope before attempting delivery.
- Resend the same envelope until an authoritative durable receipt arrives.
A lost start response never causes speculative execution. On restart, reserved or started entries are reconciled with the original execution; interrupted handlers are never run again. Uncertain recovery and notification entries stay held even after an unknown-result receipt. Only an authoritative terminal start disposition removes that hold. This relies on the server never returning an old executable grant for a terminal execution.
QueueLateEvidence appends evidence using the original receipt and a stable new
evidence identity. It does not replace the result, clear the hold, or re-enable
execution. Duplicate evidence is safe to resend; server-side idempotency remains
required if a response is lost. Evidence URLs are data, never fetched by the
worker library.
Capacity and shutdown¶
Defaults: 16 concurrent execution slots, 4,096 records, 256 MiB live-plus-reserved record bytes, 128 KiB encoded outcomes, 64 KiB diagnostic text, and 512 MiB journal allocation before admission stops. Allocation pressure never discards entries or interrupts completion writes already reserved. Status reports allocated, live, and reserved bytes separately. Deleting bbolt records does not shrink the file; offline operational compaction is not provided by this first library.
The runner uses a single bounded poller and result-delivery loop. Execution goroutines are bounded by concurrency slots, and durable record/byte capacity is reserved before requesting start permission or invoking a handler. Polls wait at most 25 seconds. Result retries default to one second and use the same persisted identity and envelope; ordinary SDK mutation retry policy stays off.
Run(ctx) installs no signal handler and never exits the process. Cancellation
stops admission, cancels cooperative handlers, and retains known results for
delivery/restart. After 45 seconds, Status().DrainDeadlineExpired becomes true.
The runner retains its journal lock and does not return while a handler is
still alive. A supervisor must terminate an uncooperative process. Lease expiry
does not fence an external process, and this library cannot sandbox hostile Go
code. Use a separate service account, process/container resource limits, network
policy, and provider credentials; never mount the CPRa server state or key paths.
Verification and versioning¶
Run tagged tests explicitly; a parent module's go test ./... does not enter a
nested module. Repository scripts create a temporary Go workspace for local
verification. Published consumers must use downloaded module versions with
GOWORK=off, without local replacements.
Initial release tags are sdk/go/worker/v0.1.0-rc.1 and, after qualification,
sdk/go/worker/v0.1.0. Journal format is 1. SDK, server API, worker protocol,
and journal versions are separate compatibility contracts. No tag is implied to
exist merely because it appears in the candidate module requirement.
Read tagged package documentation¶
The hosted pkg.go.dev service selects a limited set of OS/architecture build
contexts. Its loader does not enable the arbitrary externaljobs tag. As a
result, normal hosted indexing does not render this package or the SDK's tagged
custom-job APIs. This is a documentation limitation, not a reason to expose an
untagged stub. See the pkg.go.dev build-context policy
and its source-file selection implementation.
Read this README, the source comments, and the CPRa generated tagged API
reference. The ordinary go doc command also selects the default build context;
setting GOFLAGS=-tags=externaljobs does not make it render this package.
To check which files actually compile and run its executable examples locally:
go list -tags=externaljobs -f '{{.GoFiles}}' github.com/ziad-hsn/cpra/sdk/go/worker
go test -tags=externaljobs -run Example ./...
The module remains downloadable after publication because build constraints do not remove its source from a module archive. Package visibility, compilation with the tag, server authorization, and release qualification are separate checks.
For publication from the complete repository checkout, follow the maintainer publishing guide. The guide is a repository document outside this nested module archive; the installation and execution requirements above remain available in the downloaded module.