Sealing jobs, workspaces, and projects¶
For operators and campaign owners who need finished work to stay provably unchanged.
A seal is a signed statement of what one level of the workflow tree contained at a moment in time. Once a payload is sealed, any later change to a covered byte becomes a discrepancy the moment the seal is verified, and the protocol refuses the operations that would silently invalidate it.
Sealing answers a narrow, practical question: has this finished result been touched since it succeeded? It is not encryption and not access control — a seal is public and detached; anyone can read the payload, and anyone with the signing key can re-seal it. What a seal buys is detection.
The three levels¶
Each level records the level below it, so a project seal transitively pins whole payloads without re-hashing them:
A job seal records the file hashes of one payload — and each file’s owner execute bit, so a runner cannot be quietly made (un)runnable. It lives at
.httk-workspace/seals/jobs/<job_key>.json. It covers the payload’s own files, not the payload-private scratch directoriesattempts/,logs/, and.httk-job/: those are working state a job legitimately rewrites, so they are excluded and may change without breaking the seal.A workspace seal records, for every job, the digest of that job’s seal. It lives at
.httk-workspace/seal.json. Every job must be sealed before the workspace can be.A project seal records the project’s loose files and, for every registered member, the digest of that member’s own seal. It lives at
httk_project/seal.json, and the project level — seal, manifest, repair, verify — is owned by httk-core. A workflow workspace is a project member: it is recorded inhttk_project/members.json(registered when the workspace is created, unregistered when it is deleted or forgotten, its path followed on move), and httk-workflow teaches core’s verbs how to seal, exclude, verify, and check it. Every member must be sealed before the project can be.
httk project seal | unseal | verify-seal | repair | manifest are core commands;
this guide describes what a workflow workspace contributes to them. The signature
is over a domain-separated digest of the document body, exactly as a signed
project manifest is signed, so a seal digest can never be replayed as a
manifest or any other httk artifact. Verification answers two independent
questions: does the seal still describe this tree, and was it made by a key this
project trusts.
Auto-sealing succeeded jobs¶
By default a manager seals each job the moment it succeeds, so a finished result is protected without an operator remembering to. Sealing is a convenience, never part of the job’s success: a missing key, a conflicting existing seal, or a filesystem error is logged and swallowed, and the job stays succeeded.
Two workspace application settings control it:
seal.succeeded— whether to auto-seal succeeded jobs. Default on; set it tofalse(also0,no,off) to turn it off.seal.keys— the comma-separated key refs to sign with. Defaultproject,identity.
httk workspace settings set --key seal.succeeded --value false default
httk workspace settings set --key seal.keys --value project,identity default
Key refs¶
A seal is signed by one or more keys, each named by a ref:
Ref |
Signs with |
|---|---|
|
the project’s own signing seed, discovered from the tree |
|
the default operator identity |
|
a named operator identity |
a path |
a base64 Ed25519 seed file |
The --keys REFS option on job seal, workspace seal, and project seal
overrides the setting (or the project’s seal_keys member) for that one call. A
ref that cannot be resolved is skipped with a warning rather than failing the
seal; only resolving no key at all is an error.
What a seal refuses, and what still works¶
While an entity is sealed, the protocol refuses anything that would change what a seal commits to:
Refused on a sealed job: state changes (submit, transitions, requests),
job delete, and runner publish that would alter its payload; unsealing it while its workspace is still sealed.Refused on a sealed workspace: the same, plus unsealing it while its project is still sealed.
Refused under a sealed project:
workspace initthat would add a workspace the project seal does not cover.Refused generally: re-sealing a job whose recorded contents differ (unseal it first), and changing a policy or setting that a seal depends on.
What still works unchanged: every read-only command (status, show, log,
why, seal verify), gc and fsck, unlock, workflow postprocess —
it writes outside the payload (see below), so a sealed job can be postprocessed;
its output is excluded from the job seal and, when it lives inside the project
tree, from the project seal too — and transfers: the seal travels with the
payload, so a job sealed here stays sealed, and verifiable, on the machine it
moves to.
Sealing and unsealing in order¶
Seals nest downward, so they are written bottom-up and removed top-down.
# Seal: jobs, then the workspace, then the project.
httk job seal <JOB>...
httk workspace seal # or: httk workspace seal --force (seals unsealed jobs first)
httk project seal
# Unseal: project first, which frees the workspaces, which free the jobs.
httk project unseal
httk workspace unseal
httk job unseal <JOB>...
workspace seal runs inside the maintenance guard, so the workspace must be
quiescent. Without --force it lists the still-unsealed jobs and refuses;
--force seals each of them first (any quiescent kind, not just succeeded) and
then the workspace. job unseal, workspace unseal, and project unseal prompt
for confirmation, which --force skips; without a terminal and without --force
they refuse rather than block.
Verifying¶
httk workflow seal verify [PATH] verifies the seal at PATH — a project root,
a workspace root, or a job payload — and, unless --shallow, every seal it
references:
httk workflow seal verify
httk workflow seal verify --json
httk workflow seal verify --trusted-key keys/collaborator.pub some/workspace
Text output is one line per entry — <level> <subject> <verdict> <reason> —
with indented <kind> <path> discrepancy lines beneath any failing entry, then a
final status line. The exit code and that word mirror a signed manifest’s
verdicts:
Every entry |
Final line |
Exit |
|---|---|---|
|
|
0 |
valid, but at least one |
|
3 |
any |
|
1 |
--json prints { "entries": [...], "ok": <no discrepancy or invalid>, "trusted": <every signer is a trust anchor> }.
A verdict is one of valid_trusted (a signer is a pinned trust anchor),
valid_unknown_key (the signature verifies but nothing pins the signer), or
invalid (the seal no longer describes the tree, or a signature does not
verify). By default the project’s pinned keys and the local identities’ public
keys are trusted, so a tree sealed by its own project or identity verifies as
valid_trusted without naming a key; --trusted-key adds more, as an
ed25519: key, a sha256: fingerprint, or a *.pub file.
Not to be confused with¶
A transfer’s “sealed bundle.” A transfer bundle is sealed in the sense of being a finalized, checksummed archive ready to move between machines. That is a property of the transport envelope, unrelated to the signed seal documents described here (though a sealed payload keeps its seal inside the bundle and stays verifiable on arrival).
httk project export. The core command that packages a project for distribution as a signed ZIP is an export; the word seal means only the integrity seal described in this document.