The head and the face
The head is the edge API: the one surface people and services
reach. It knows who is calling (a session, a bearer, a socket peer),
maps them to the positions the record says they hold, and admits their
commands into the record with a receipt. It never performs work. The
operator who serves it owns it; it is built from source (dna/api) and
started for you by hale dna head (or, from a checkout, by
dna/face/start.sh). A head fails closed: with no
principal source it refuses to start, a caller it cannot name gets
nothing, and a command outside the caller’s positions is refused. It
never holds the spine’s database role, a model credential or the forge
token. The face is the browser shell over it, where the organism
shows itself to people.
| part | what it is | where it lives |
|---|---|---|
| head | a project’s API: reads over HTTP, commands as a surface (HeadCommands) over a socket and an HTTP exposure |
dna/api |
| the project head | the face’s server: a registry of projects, receipts, a proxy to each project’s head | dna/api/project_service, hale dna head, dna/face/start.sh |
| face | the browser shell | dna/face |
| surface | hale dna ui: a read-only page from the record |
dna/ui |
| the Board and Reviews | the queue, the Review, its verdicts and signers | hale dna board, hale dna review, dna/core/review.hl |
| the forge mirror | Reviews as pull requests | hale dna github sync, dna/host/forge_github.hl |
Start the head
Section titled “Start the head”With the toolchain installed, and a project made by hale dna new:
hale dna head /absolute/path/to/projectThe toolchain carries the head’s sources, the face’s assets and the
launcher, as it carries the host’s: the first hale dna head builds the
project head, the project’s API child, the stub OpenID provider and the
reflexes into the toolchain cache, side by side (a few minutes), and every
later one starts them at once, until the toolchain changes. It prints the
face’s loopback URL. With no project it starts detached, and the
browser’s Projects workspace creates, initializes or attaches one.
With a project, the head reads Knowledge from memory under the record’s
head role. hale dna head derives that DSN as hale dna memory migrate
derives it and hands it to the head alone; it is never printed. When the
project’s memory is not up it says so and starts anyway, with Knowledge
unsupported: hale dna dev brings the project’s memory and nerves up.
An operator’s own HALE_DNA_MEMORY_DSN_HEAD wins.
From a checkout of hale, the launcher starts the same head, building it from the checkout into temporary storage on every start:
dna/face/start.sh /absolute/path/to/projectBoth take the same flags; hale dna head --help and
dna/face/start.sh --help list them.
| port | what listens | flag |
|---|---|---|
| 8792 | the project head, serving the face | --port |
| 8793 | the API child’s reads | --api-port |
| 8794 | the stub OpenID provider (dna/oidc) |
--oidc-port |
| 8795 | the API child’s commands: its surface’s HTTP exposure | --commands-port |
The API child says when each part is listening. Its commands port is
given to it (HALE_DNA_COMMANDS_PORT), never derived from the reads’
port, and once its binding holds that port it prints:
hale dna api: commands http://127.0.0.1:<port>/The launcher starts no body, database or broker; those are the project head’s operations, each a CLI verb it runs on request.
Who you are
Section titled “Who you are”A head has exactly two ways in, and a caller is always a person, a service, or nobody.
On the socket, a peer is the local account the kernel vouches for, mapped to a person in the record’s own config:
git config --local --add dna.unix.member "uid:1000=alice"hale dna new and hale dna init map whoever runs them. The socket is
one per record, under $XDG_RUNTIME_DIR/hale/dna/ (else the record’s
.hale/dna), and the head names it in its capabilities. This is where hale dna work sends its
commands (Legs, hands and voice).
Over HTTP, every caller is an identity provider’s subject. Name the provider and map the subjects you trust:
git config dna.principal oidcgit config dna.oidc.issuer https://id.example.comgit config dna.oidc.client dna-acmegit config --add dna.oidc.member "<subject>=alice"hale dna secret set OIDC_CLIENT_SECRETThe client secret goes into the vault as oidc-client-<client>
(The skin). dna.oidc.redirect names the callback when it
is not the head’s own /auth/callback. The head reads the issuer’s
discovery document and signing keys once, at start, and does not start
without them. Every token is verified: ES256 under the issuer’s keys,
the issuer, this client in the audience, unexpired. An issuer on the
loopback is anyone’s who can bind its port, so the record pins its key
(dna.oidc.key), and the head trusts that key alone.
- A person signs in through the browser: the authorization-code
flow, a random
stateandnonce, a callback bound to the browser that started it. The session is anHttpOnlycookie that lasts eight hours or until/auth/logout. An unmapped subject gets no session. A program acting for a person presents its ID token asAuthorization: Bearer <token>instead; a leg gets one fromhale dna work loginwhere the issuer is the local stub, and signs in at/auth/loginwhere it is not (Legs, hands and voice). The head names its issuer and client at/.well-known/hale-dna, unauthenticated, which is what the verb asks. - A service, a program with no person behind it, gets its own token
from the issuer with the
client_credentialsgrant and presents it as a bearer. Map it beside your members, withgit config --local --add dna.oidc.service "<subject>=<service>". It reads asservice:<service>, gets no session, and sends no commands: a command is a person’s, and a service holds no position. A subject is a person or a service, never both.
What a person’s token reads is the record’s to say. The hat of a Work
(…/dna/context, what hale dna work brief prints) is read by the person
who holds that Work’s attempt lease and by no one else: the member map
names the person, and the claim row names who holds the lease
(Legs, hands and voice).
Local mode is OIDC too. hale dna head (and dna/face/start.sh) starts the stub
provider on the loopback and signs you in through it as the subject
local-sub, mapped to $USER; it configures a project it attaches for
that issuer, pins the stub’s key, and refuses a project another issuer
serves.
Trusted-local is a fixture’s mode. A head started with neither
dna.principal = oidc nor HALE_DNA_TRUSTED_LOCAL=1 refuses to start:
trusted-local is a fixture's mode (HALE_DNA_TRUSTED_LOCAL=1); a project serves its head under OIDC: `git config dna.principal oidc` with dna.oidc.issuer, dna.oidc.client and dna.oidc.member (dna/face/start.sh sets them up with the stub provider, dna/oidc)The head speaks plain HTTP. On a network, put TLS in front of it.
Commands and their receipts
Section titled “Commands and their receipts”Every command the record takes is a row of the head’s surface HeadCommands.
The record answers who holds what, so the same rows’ requires apply on
the socket and over HTTP:
| gate | who passes | rows |
|---|---|---|
owner |
a holder of position:board |
propose_organization, reassign_task, retire_person |
reviewer |
a holder of position:reviewer |
verdict |
position |
a holder of any live position | propose, the attempt rows (claim, outcome, renew, release_attempt, ask_allowance), file_friction, the *_knowledge changes, the structure (open_position, fill, apply_batch) |
| none | any person the head can name | create_task, lookup, lookup_knowledge, preview_batch |
Over HTTP a command is POST /call/<member> on the commands port, under
the caller’s bearer. A browser holds a session, not a bearer: the
head that serves the page relays its POST …/commands with the
session’s token, after checking what a cookie needs (the exact
Origin, X-Hale-Command: 1, a JSON body).
Every mutation is a head command; the UI is a client of the
generated surface. A change to the organization’s structure made from a
UI is not a second code path: open_position, fill and apply_batch
write exactly the rows hale dna position open, hale dna fill and
hale dna apply write, in the caller’s name, and refuse what those verbs
refuse, with the same words. The organization answers the rows whoever
sent them, and the Board decides through verdict. vacate is a
one-step batch (vacate <position> <holder>). Each takes the Record’s
head it was prepared against as record_head, and a moved Record is
stale_subject. preview_batch is a read: the batch planned against
the head, not proposed, as data (the org chart’s lines before and after,
and those it adds, takes out and keeps), so a UI shows the change, then
applies it at the head the preview names:
preview_batch { steps } → { ok, why, head, before, after, added, removed, unchanged, pending }apply_batch { request_id, record_head: head, steps, pack: "" } → receipt.structure { state: requested, request_id, review_id, … }The receipt’s structure reads requested until the organization
answers, then proposed (with the Review) or refused (with why); the
same steps still pending answer already_proposed, naming their Review.
The clients are generated from the surface (hale api client --surface HeadCommands); the legs’ Hale client is committed in
dna/core/legs/head_commands.
A command is one act per request id. Its receipt names the command and
request ids, the operation, the principal (principal_mode,
principal_name and the positions the person holds),
the target, and a state with its reason. Sent again under the same
id, a command that landed is found by it and answered with the same
receipt; lookup reads one back.
The project head keeps receipts of its own operations (create, init,
attach, sync, the local body, secrets, the model probe, and more), each
a CLI verb run detached, with its pid, exit code and log as files,
under a command-<digest> identity:
recorded → admitted | refused → running → succeeded | failed | outcome_unknownAn identical retry replays the receipt; a changed one conflicts. A
verb that reaches beyond this machine and passes its deadline is
outcome_unknown, never failed. The head appends nothing to any
record itself.
The Board’s queue
Section titled “The Board’s queue”hale dna board is what waits for the Board. A fresh project’s:
$ hale dna boardboard: 16 review(s) need your verdict k:05cecb18af4e ratify the design practice `design/software-delivery`: For an appendage or a product: process boundaries first (what runs, fail… k:076fd6de2420 ratify the design practice `design/standard-equipment`: Every part that supervises others is born with its architect: the positi……secret: the forge's token is not in the vault (forge-token); `hale dna secret set FORGE_TOKEN` fills itdecide with `hale dna review <id> approve|revise|reject`; `hale dna review <id>` renders oneIt lists the pending Reviews that need the Board, how many the Leader
is deciding inside its grant, handed Tasks waiting on a decision
(tasks waiting:), proposals, the last report, grant changes, a model
credential the body lacks, the secrets the vault lacks, and the latest
violations the organism absorbed. It reads the record, so it works in
any clone.
Reviews
Section titled “Reviews”A Review owns one question: the candidate’s digest, the authority or the positions it requires, and the verdicts it received.
hale dna review # the pending Reviewshale dna review <id> # render onehale dna review <id> --iris # a change's semantic diff in iris's review viewThe list groups the toolchain’s seeded proposals under their headings
(purpose, design, operating, using, the library’s families and a repository’s holes,
practices and holds, positions opened at run time and the
repository’s graph read again, ingest), in the order they were proposed: under holes,
each position is followed by its mandate (ratify the mandate of <position>: …), so hale dna review holes approve ratifies the
position and then its mandate, and the mandate is bound to the position
once ratified. A proposal renders as its question and what
it needs:
$ hale dna review k:3870910f5de4review k:3870910f5de4 [pending]: ratify the declared purpose? needs board · candidate sha256:3870910f5de44652ad0a9b91cedc50da02d81c3443181b6bed8fd50c0469bc8ddecide: hale dna review k:3870910f5de4 approve|revise|reject|abstain [--as <you>] [--comment <c>]A change renders its three views, always together, because each sees what the others cannot:
- the source diff, git’s, base to candidate: the only view that sees handler behaviour, a changed literal, a field added and unread;
- the semantic diff,
hale model difffrom its receipt: declarations added, removed, renamed, moved; contracts and effect classes that moved; claims whose result flipped; - the evidence table, one row per step the toolchain ran (
fmt,check,verify,test, …),yesorNO, with the receipt of its output.
Above them the render names the mutation, its class, its author, the
disposition under the grant, and the magnitude. The render works
offline, from the record and its receipts. The kill test behind the
three views is in dna/kill-test/WALKTHROUGH.md.
Verdicts
Section titled “Verdicts”hale dna review <id> approve --comment "fine"hale dna review <id> approve --digest <sha> # name the candidate you looked athale dna review <id> reject --no-wait # write it; the answer lands in the recordhale dna review design approve # every pending Review in a group, each its own verdictThe verdicts are approve, revise, reject and abstain. --as
names the reviewer (default: $USER), --authority the authority
claimed (default: board), --comment the reasoning, and --digest
the candidate you looked at (default: the one the Review pinned). The
verdict is a review.verdict row; a node relays it, and the Review
answers in the record. Unless you say --no-wait, the verb waits
for the answer:
review <id> settled: …review <id> signed by <who>; it awaits <positions>review <id> refused the verdict: …The Review admits a verdict only if:
- it names the pinned candidate (else
digest mismatch: …); - the reviewer stands to decide it: with signers, a holder of a
required position; with none, a claimed authority that satisfies
the required one (else
authority <a> does not satisfy <required>); - the reviewer did not author it (
reviewer <who> authored the candidate), except on a governance proposal requiring the Board (a fill, an opening, a graph proposal), which admits its author’s verdict: Positions andfill.
| authority | rank |
|---|---|
board (also maintainer) |
4 |
leader |
3 |
supervisor |
2 |
reviewer |
1 |
A claimed authority satisfies a requirement of its rank or below. revise and
reject settle the Review and leave the genome untouched; abstain is
recorded and leaves it open. An approve on a change is also its apply
(The heart and the body).
Route and signers
Section titled “Route and signers”A change set goes to the positions that must sign it, read from the
graph’s reviews edges, never guessed:
hale dna route main.hlhale dna route --diff main..HEADhale dna route --json main.hlIt lists who signs (each position with its holders and why), the gates the change is judged against, what no position reviews, and what is left to the fallback. The graph is memory’s, so without memory it says so:
$ hale dna route main.hlhale dna: route: the graph is memory's, and no memory is named (HALE_DNA_MEMORY_DSN_HEAD); `hale dna memory migrate` prints itA Review takes its signers and gates from the same computation when it
opens, and records them as review.routed. With signers, approval
settles only once every required position has approved and every gate
has a passing run at the candidate, as the forge reports it
(gate.observed); one rejection or revision from a required position
settles it.
With no signers (no memory, or no position the route names is held),
the Review keeps its required authority, which the render shows as the
fallback. The Leader never decides a routed Review. A verdict cites no
gate run: --evidence is refused.
The face and the surface
Section titled “The face and the surface”The face (dna/face) is the browser shell the project head serves:
the Projects workspace, where you create, initialize and attach
projects and run their operations, and, for an attached project, its
application controls, the organization, workflow definitions,
Knowledge, practices and Reviews. It does not poll: the head tells it when to read
again over an event stream (GET /api/hale/v1/head/events), when a
receipt is journaled, a run or child starts or ends, or the organism
lands a row.
hale dna ui [project] [--port N] serves a page from the record
alone (port 8790 by default). Every request runs one offline verb in
the project root and returns what it printed: the status, the Board’s
queue, the pending Reviews and one Review’s three views, the fleet,
pressure, and the history. It reads, and takes no command: a POST is
refused, because the record’s commands are the head’s surface.
It serves under dna.principal = oidc (with dna.oidc.issuer,
dna.oidc.client and dna.oidc.redirect set), a session in front of
every page, and otherwise refuses to start, as any head does.
The GitHub mirror
Section titled “The GitHub mirror”GitHub is a projection of the record, and the record wins:
git config dna.github acme/chatgit config dna.github.board alice,bob # whose GitHub reviews carry the Board's authorityhale dna secret set FORGE_TOKENhale dna github synchale dna github sync opens a pull request for every pending change
Review (the candidate pushed to dna/<id>, the three views as the
body, github.pr in the record), reads every GitHub review on it back
as a review.verdict in the reviewer’s login (board when
dna.github.board lists the login, reviewer otherwise, each once),
records the gate runs it finds at the candidate (gate.observed), and,
once a change is applied, comments the settlement (github.commented)
and pushes the genome on approval. A host under hale dna dev or
hale dna run does the same on its tick. gh runs under the vault’s
forge token alone; with none, nothing runs, and gh’s own login is
never used in its place.
How it breaks
Section titled “How it breaks”- The head refuses to start: no principal source (the trusted-local message above), an issuer that is not https or loopback, an issuer whose keys cannot be read, or a loopback issuer with no pinned key.
not a member: subject … maps to no one: map the subject withdna.oidc.member.- A command refused
forbidden: the caller holds no position the topic’s gate names, or is a service. commands_unavailable: the head was given no commands port.- A verdict refused: the reason is one of the three checks above;
hale dna review <id>shows it underrefused a verdict:. no forge token:hale dna secret set FORGE_TOKEN.