OpsChain GA Release 5.6.1
OpsChain 5.6.1 is a generally available release, published on 2026-11-09. Every release is listed on the releases index.
Added
- What references an installed pack version is visible before a delete refuses: the v2
PackVersioncarriesreferenced_by— the template versions whose sealed set pins the version (project_code,template_code,template_id,template_version,template_version_id, so the GUI links each one) — andrun_count, the runs whose frozen set pins it; both read the same pack-pin rows the delete refusal counts, filled in one set-based query per listing page (and on the install and pin responses), and the refusal now derives from that same read. The v1 template version gainssealed_packs([{name, version}], always an array) — the installed pack versions its pinned set sealed — so a template version's page shows the relation from its side.opschain admin packs listgains theREFERENCED BYcolumn (<n> tv / <m> runs, filterable asreferenced_by) and-o jsoncarriesreferenced_byandrun_count. Reference:opschain docs pack-store,resources/pack(the newPackVersionReference),resources/template,cli/admin-packs-list; lesson 2.4. The Packs admin page and the template version page render them in the GUI half of the same train. - The CLI references shared git credentials:
opschain git-remotes create/updategain--credential-ref <id|name>— the remote points at an entry of the shared git-credential registry (the project's own or a global one) instead of carrying inline values, so rotating the credential once applies to every referencing remote on its next fetch; the flag is refused locally beside any inline credential flag (--credential-ref cannot be combined with inline credentials … — supply one or the other), the server's own refuse-both wording passes through,--credential-ref ""onupdateclears the reference and may accompany the inline values that replace it, and the strict-ffile acceptscredential_ref(a reference, not a secret — the secret keys stay refused).showprintscredential_refandlist's AUTH column readsshared-credential. A newopschain git-credentialsfamily manages the registry over the v2GitCredentialService:list [--project](a project's own plus the global credentials, or the global registry alone; masked values print as the API returns them,{{hidden}}),create --name --kind https_basic|ssh_key [--project]with the git-remotes credential contract (--username,--password-env/--password-prompt,--ssh-key-file,--passphrase-env— never a secret on argv or in a file),update <id|name>(send-only-what-is-supplied;--username ""/--clear-passphraseclear),test <id|name> --url(the server'sgit ls-remoteprobe; a failed probe exits 1 with the classified reason) anddelete <id|name> --confirm(preview without it; the in-use refusal names the remotes). Reference:opschain docs cli/git-credentials(+-list/-create/-update/-test/-delete),cli/git-remotes-create,cli/git-remotes-update,resources/git_credential; lesson 3.3 teaches the reference form as the recommended one. - Session origin in the audit trail — the proxy-aware client address, the user agent and the acting credential on every audited event and session, with a stated retention and privacy posture: (1)
server.trusted_proxies(config.yaml, a CIDR list; default empty = the TCP peer is the client) names the reverse proxies whoseX-Forwarded-For/CF-Connecting-IPthe server honours — when the peer is listed the client isCF-Connecting-IPif present, else the right-mostX-Forwarded-Forhop that is not itself listed; an unlisted peer's headers are ignored, so a direct caller can never spoof its recorded address; an invalid CIDR refusesserver startby name and the validator reports the same. One resolver (internal/server/requestorigin) runs once per request in the request-log middleware and feeds the log'sremote_ip, the per-source login throttle, the session row and the audit envelope, so all four agree. (2) Every event a request causes — theapi:*mutation audit, theapi:auth:*/warn:auth:*family, the server-emitted lifecycle events (node delete/restore/destroy, maintenance, licence, template-version, actions-refresh, artefact creation) — carries anactorobject indata:principal_id,session_id(native session) ortoken_id+token_kind,client_address,user_agent,request_id, each present only when known, never a username or email; absent on engine-originated events; an emitter's ownactoris never overwritten; it ridesdataverbatim into the SIEM forwarder. Theapi:auth:*/warn:auth:*/MFA/password family carries its client address ONLY asactor.client_address— the legacydata.sourcekey is gone (owner 2026-11-08: pre-GA, no consumers), so a SIEM rule on a login's origin keys on the envelope. (3)auth_sessionsgainsuser_agent(first seen, never rewritten) andlast_client_address(updated on every refresh) — migration00126_auth_sessions_origin(expand) — exposed on the v2Sessionresource (user_agent,last_client_address) andopschain admin sessions list(LAST ADDRESS and AGENT columns; JSONlast_client_address,user_agent), so a user recognises their own devices. (4) The retention and privacy posture is stated:opschain docs security-audit("What the audit trail holds about origin") tables what is held where and for how long —retention.events_days(events, default never), session purge,logging.max_age_days(the request log, 30 days) — with no hashing of the address (the forensic value is the point; retention is the proportionality control). Reference:opschain docs events(the audit envelope),resources/event,resources/session,config(server:),identity,cli/admin-sessions-list; runbook 7-3.
Changed
- The release keyring carries production keys only:
pkg/licence/keyring.jsonholds2026-a(issuing) and2026-registration-a(registration) alone; the2026-testand2026-registration-testentries — whose private halves are committed test data — move topkg/licence/keyring_dev.json, embedded only under-tags devkeysand appended to the release keyring at first use (a duplicate id refuses the build's keyring).make build/dev-bin,make demo-bin,make check(go test -tags devkeys ./... -race, lint with the tag) and the integration test binaries carry the dev ring;make releaseand the train never do, andopschain versionprintsdev keys: onexactly on the builds that carry it. Every2026-test-signed licence (examples/licences/*, the demo kit's, the integration harness's) therefore verifies on devkeys builds only — a released binary answerslicence: unknown signing key— so the published licence asset becomesopschain-evaluation.licence(replacingopschain-dev-full.licence): the production-signed, 90-day, every-feature licence the owner mints with2026-abefore the cut and hands the train asRELEASE_LICENCE. The train's pre-flight (make release-licence-check,scripts/release/check-licence.sh) verifies it offline against the release keyring (go run ./scripts/release/verify-licence, untagged) and refuses by name when it is unset or signed by a key the release keyring does not hold; the cut's own suite walk (3c′, which now finds a cachedopschain-evaluation.licence), the release-artifact pass (6,OPSCHAIN_IT_LICENCE— the integration harness refuses a release binary without one) and the publish step (7, re-verified; a GA tag refuses without it, an internal publish re-ships the previous release's re-verified) all apply this one licence; therelease_trainworkflow gains thelicenceinput.opschain admin packs installnow refuses a bundle signed by an unknown key naming it:unknown signing key "<key_id>": the bundle is signed with a key this build does not trust. Docs: the licensing runbook,README.release-train.md(precondition 4b, steps 3c′/7),examples/licences/README.md, the entitlements andversion/admin packs installreference pages,docs/cli.md. - The hosted platform services live on
limepoint.com:licensing.limepoint.cloud,licensing-admin.limepoint.cloud,academy.limepoint.cloudandacademy-admin.limepoint.cloudbecomelicensing.limepoint.com,licensing-admin.limepoint.com,academy.limepoint.comandacademy-admin.limepoint.com— same labels, new apex, because customers' egress allow-lists already admit.limepoint.comand the zone is already on Cloudflare. The Academy launcher's default server (DefaultServer, the embeddedrelease.json), theacademymanuals and lesson 0.3 carry the new name; nothing was ever deployed on the.cloudhosts, so there is no redirect bridge — the next binary simply carries the new default.
Added
- The release-lane keyring gates:
make release-keyring-check(amake releaseprerequisite) runs the release-lane tests untagged, exactly as the release binary builds —TestReleaseKeyringCarriesNoTestKey/TestKeyringFileCarriesNoTestKey(no-testid, no development role, at least one issuing key; the dev licence a stranger) andTestBundledPackManifestSignerIsInTheReleaseKeyring.make release-publish-selftest(inrelease-distribution-selftest) exercises the publish step's licence rule offline with a stubbedghand the real verifier — the test-key-signed dev licence as the stranger;RELEASE_LICENCE_PROOF=<file>runs the acceptance path against the owner's licence, read in place. Proven 2026-11-08 with the owner'sopschain-evaluation.licence(id5c1d7b5d…, signer2026-a): the verifier,check-licence.shand the self-test's acceptance path all green. - The public releases repository and the Homebrew tap carry the LimePoint
LICENCEnotice (owner, 2026-11-08): the OpsChain End User Licence Agreement governs the published packages and formulae; the publisher (step 7b) and the tap push (7c) write it fromscripts/release/public-repo/LICENCEon every cut, and rule 2's tree check admits it besideREADME.md,CHANGELOG.mdandinstall.sh.
Fixed
- Every local run and every template resolution failed from a directory with a space in its path (macOS:
~/Library/Application Support/opschain/work/…, the default home for local runs): the embedded JSON schemas were registered under a bare file name the compiler resolved against the working directory, percent-encoded differently on lookup, and so "failed loading" from disk —opschain actions list --localrefused, and after the server reported every action file invalid and refused change creation (embedded schema actionfile_schema.json does not compile). The schemas are registered under an absolute synthetic URL; nothing is loaded from disk. Found by the v5.6.1 cut's own suites walk. - An optional secret input left unset is absent, not an empty raw value the engine refuses: a secret input's absence is now the empty string — a secret cannot declare
default: '', so an optionaltype: secretinput left unset binds""(where it used to bind nothing and failno such keywhen a workflow step referenced it), an empty string supplied for one means "not supplied" (the declared default applies, or it binds""; a required secret left empty is stillinput <name> is required and was not supplied),${{ inputs.x }}renders empty for it andinputs.x != ''tests whether it was given; a non-empty value that is not asecret-vault://reference is refused exactly as before. The run record and the stored inputs show""for an unsupplied secret, never******. The rule applies wherever inputs bind: change and workflow-run creation (v1/v2 API, the plan lane), a workflow step'swith:pass-through into its nested change (thehosted-provisioncase —with: { x: ${{ inputs.x }} }for optional secrets the caller did not set ended the steperrorat its entry with no reason on the step), a composite action'swith:literal,opschain runs create … -i secret_input=and the local lane. Reference:opschain docs orchestration-logic,expressions,orchestration,cli/runs-create; lesson 3.5. - Why a workflow step failed reaches the operator, not only the server log: the first
hosted-provisionrun endedsystem_erroronprovision-sitewith nothing on the step — the action file's validation error (output key "output_originIp" must match …) lived in the server log alone. Every step now exposeslast_error_message, the reason recorded at its lasterror/system_error, verbatim: on the v1stepresource and the workflow step's attribute document, on the v2Step(last_error_message), inopschain runs show(printed under the failed step as↳ <reason>;-o json|yamlcarry the field) and on the GUI step tree (the GUI half of the same train). For a change step whose nested change is refused at creation — an action whose declarative file is invalid, awith:naming an input the action does not declare, a missing required input, an action absent from the catalog — the reason is the API's own refusal, exactly the textPOST /api/changeswould answer with. A broken action file is also visible before any run: resolving a template version's action catalog records its invalid files on the version (invalid_action_files: [{file, action, error}], new on the v1 template version; migration00127_template_version_invalid_action_files, expand) and emitswarn:template:action_file_invalid(source: the template version; data: file, action, error) once per template version and file — never per run or per catalog read, a changed error warns again. Reference:opschain docs resources/step,resources/workflow_run,resources/template,events,cli/runs-show; lesson 4.3. - A pack bundle's
min_platformfloor no longer refuses a development build: a git describe past a tag (v5.6.0-26-g…) or a dirty build is never placed against the floor, which protects released estates only. - The release lane's self-tests stage under their own root: the publish-public, package-managers, sign-release and write-manifest self-tests used
v5.6.0as their fixture version and read — then deleted on cleanup — the repository's realdist/release/v5.6.0stage, taking the signing facts the hosted lane had just recorded with it; on the first GA cut that failed one check ("before a signed release the README says the Windows fingerprint is pending" — the real facts were there) and removed the stage the next steps needed. The four scripts takeRELEASE_DIST_ROOT(defaultdist/release) and every self-test exports its own root under its temp dir; the wholerelease-distribution-selftestset now leaves a real stage byte-identical (checked on the cut).