Projects and the anchor

A project is to a campaign what a Git repository is to a source tree: a directory marked, at its root, by a small control directory that every command discovers by walking upward from wherever it is run. In httk that directory is .httk-project and its versioned manifest is project.json.

The anchor lives in httk.core.project, so a httk-core installation has working projects on its own. Capabilities that build on a project — httk-workflow adds a workflow workspace, signed manifests, a doctor, and remotes — layer on top of the anchor without the anchor depending on them.

The command line

Installing httk-core provides httk project, modeled on git init:

httk project init                 # make the working directory a project
httk project init PATH --name X   # or make PATH one, with an explicit name
httk project show                 # describe the nearest project
httk project show --json          # the same, machine-readable

httk project init refuses a directory that is already a project, exactly as git init refuses nothing but the anchor here is a hard error rather than a re-initialization. It creates only the anchor: project.json, the project’s Ed25519 signing key under .httk-project/keys/, and a remotes/ directory. It creates no workflow workspace; a workflow installation adds that.

httk project show describes the anchor — its metadata, whether it pins a key and which — and any rows an installed capability contributes.

Discovering and reading a project

from httk.core.project import discover_project, read_project, require_project

root = discover_project()          # the nearest project at or above the cwd, or None
root = require_project()           # the same, but raises when there is none
metadata = read_project(root)      # validated project.json as a dict

initialize_project creates the anchor; import_v1_project creates one from a legacy httk v1 ht.project directory, adopting the identities its old manifests were signed with:

from httk.core.project import initialize_project

metadata = initialize_project("campaign", name="My campaign")

Identity keys, pinning, and trust

A project owns one Ed25519 signing key, built on Extensible command line and signing’s httk.core.ed25519. Its public half is recorded in project.json and is the trust anchor a signed manifest is checked against — never the key a manifest carries in its own header.

from httk.core.project import (
    pin_project_key,        # adopt keys/project.pub as the trust anchor
    trust_project_key,      # adopt one further public key as an anchor
    pinned_project_key,     # the project's own pinned key, or None
    trusted_project_keys,   # every anchor: the pinned key and any adopted one
    key_fingerprint,        # the stable sha256: display fingerprint of a key
)

Public keys are written as ed25519:BASE64; format_public_key, parse_public_key, canonical_public_key, and read_public_key_file convert between that spelling, the raw 32 bytes, and a *.pub file.

Extending httk project

A capability adds subcommands and show rows to the umbrella command through a registry that mirrors register_cli_command(), registering lazily from its httk.handlers.* package during core plugin discovery:

from httk.core.project.cli import (
    register_project_subcommand,   # add `httk project NAME ...`
    register_project_show_section, # add rows to `httk project show`
    ProjectShowSection,            # what a show section returns
)

Registrations are strict: names are lowercase and hyphenated, the built-in init and show cannot be shadowed, and duplicates are errors. The command tree is assembled in a stable order. register_project_subcommand(name, build_parser, handler) takes a builder that declares the subcommand’s arguments and, for a leaf, the (argv, context) -> int handler that runs it; a group passes handler=None and sets the handler of each of its own nested leaves. register_project_show_section(name, section) takes a section(root, verify=...) returning a ProjectShowSection whose json is merged into show --json and whose rows are appended to the human-readable listing.