asolaria / cli /README.md
Jessedbrown's picture
Sync with GitHub main 85faeb1
8fb5584 verified
|
Raw History Blame Contribute Delete
17 kB

asolaria

A command-line tool for the Asolaria Aether stack. It lets you generate Ed25519 keys, sign and verify files, run the tighten-only gate verdict from gate/verdict over an envelope file, and (observe) compute the gate scores from observed graph events with the trained L0 EdgeLevelGNN, without writing Rust. Results are pipe-delimited HBP rows ending in json=0.

Install from source

Requires Rust 1.81 or newer. Clone the repository, then from its root:

cargo install --locked --path cli

--locked builds with the reviewed Cargo.lock. Without it, cargo install re-resolves dependencies to their newest versions, some of which no longer build on Rust 1.81. The binary is installed to ~/.cargo/bin, which must be on your PATH.

Walk-through

The seed file is a secret, so keep it outside the checkout (here, in your home directory):

asolaria keygen --out ~/asolaria-me.seed          # KEYGEN|public=<hex>|seed_file=...|json=0
asolaria sign --key ~/asolaria-me.seed cli/examples/envelope.hbp   # SIGN|sig=<hex>|public=<hex>|bytes=<n>|json=0
asolaria verify --pub <public> --sig <sig> cli/examples/envelope.hbp   # VERIFY|ok=1|json=0
asolaria gate cli/examples/envelope.hbp           # GATE|verdict=PROCEED|...|signature=unchecked|...
asolaria gate cli/examples/envelope.hbp --pub <public> --sig <sig>     # ...|signature=verified|...
asolaria pubkey --key ~/asolaria-me.seed          # PUBKEY|public=<hex>|json=0
asolaria observe --events model/l0-edge/tests/fixtures/events.hbp cli/examples/observe-envelope.hbp

The last line prints one GATE row; see observe below.

To capture fields from a row in a shell script, split on |:

ROW=$(asolaria sign --key ~/asolaria-me.seed cli/examples/envelope.hbp)
SIG=$(echo "$ROW" | cut -d'|' -f2 | cut -d= -f2); PUB=$(echo "$ROW" | cut -d'|' -f3 | cut -d= -f2)

- in place of a file reads stdin. Signatures cover the exact bytes of the file, so any edit, including whitespace or a comment line, invalidates them. Options also accept --opt=value, and -- ends option parsing. asolaria <command> --help (or help <command>) prints that command's usage; no arguments, help, -h or --help print the overview (exit 0); -V and version are aliases for --version.

-h or --help anywhere before -- prints help with exit 0 and no GATE row, including where a file path goes. A wrapper that runs gate or observe therefore passes -- before file paths (asolaria observe --events "$E" -- "$F") and requires the GATE row, not only exit 0.

Commands

Command Output
keygen --out <seed-file> 32 bytes of OS randomness written as 64 lowercase hex characters and a newline. Refuses to overwrite an existing file; on Unix the file is created with mode 0600. Prints the public key only, never the seed.
pubkey --key <seed-file> The public key of a seed file (exactly 64 hex characters, optional trailing newline).
sign --key <seed-file> <file|-> Ed25519 signature (128 hex) over the exact input bytes.
verify --pub <64 hex> --sig <128 hex> <file|-> VERIFY|ok=1 or VERIFY|ok=0|reason=signature_invalid / reason=key_malformed.
gate <envelope|-> [--pub <hex> --sig <hex>] GATE|verdict=…|hookwall=…|spawn_gate=…|tier_gate=…|fischer=…|input_guard=…|signature=…|reason=…|json=0
observe --events <events|-> <envelope|-> [--pub <hex> --sig <hex>] The GATE|… row of gate with score, l0_real, mark and reverse_risk after the verdict (see observe)
help, --version Usage summary; VERSION|name=asolaria|version=…|json=0.

gate with --pub/--sig first verifies the signature over the exact envelope bytes. A signature that does not verify, or a public key that is not a curve point, makes the verdict BLOCK (signature=invalid, exit 4) for any envelope that parses. It can only tighten the result, never loosen it. If the signature is invalid and the envelope does not parse, the row is still BLOCK (exit 4) with every gate reported as UNEVALUATED. A valid or absent signature over an envelope that does not parse is a usage error (exit 2, no GATE row). Without --pub/--sig the row says signature=unchecked, and the verdict is only as trustworthy as wherever the envelope came from.

signature=verified means only that the exact bytes were signed by the private key matching the --pub you passed. Take --pub from a trusted, pinned source, never from the same channel as the envelope: anyone can sign an envelope (with any caller_tier) using their own key.

In reason and seed_file, %, | and control characters are percent-encoded (%25, %7C, %0A, …), so a row is always one line with a fixed set of fields. Errors go to stderr as ERROR|kind=<usage|io>|reason=<escaped>|json=0.

Exit codes

Code Meaning
0 Success; gate or observe verdict PROCEED
1 verify failed, an I/O error, or the result row could not be written (closed pipe, full disk)
2 Usage error: bad arguments, malformed hex or seed file, keygen target already exists, envelope or events parse error, envelope over 1 MiB, or an observe envelope with a score key or without payload
3 gate or observe verdict HOLD
4 gate or observe verdict BLOCK

Only exit 0 means PROCEED. Treat every other exit code (including 1, 2 and any crash) as do-not-proceed. If the GATE row cannot be written, a HOLD or BLOCK keeps exit 3 or 4 and a PROCEED becomes exit 1. If the KEYGEN row cannot be written, the new seed file is removed.

Envelope format

One key=value per line, at most 1 MiB in total. Lines that are blank or whose first non-space character is # are ignored. The key must match exactly (no surrounding spaces); the value is everything after the first =, taken verbatim (a trailing \r is dropped). Unknown keys, duplicate keys, a missing required key, a line without =, non-UTF-8 input and malformed values are all usage errors (exit 2): nothing is guessed. Control characters (tab, CR inside a line, ESC, NUL, …), Unicode bidi controls and a leading byte order mark are rejected on every line, comments included, so an envelope cannot display differently from how it parses; save files as UTF-8 without a BOM. Integers are plain decimal digits; flags are 0, 1, false or true. Defaults are the strict values, not FischerEnvelope::default() (which pre-fills proof paths). See examples/envelope.hbp for a clean envelope that composes to PROCEED.

Key Field Type Default
slot VerifiedEnvelope::slot integer 0..=255 (64 and above makes hookwall BLOCK) required
syscall_no syscall_no integer 0..=255 required
caller_pid caller_pid integer, u64 required
hook_tier hook_tier micro, cosign, firmware required
caller_tier caller_tier public, restricted, stealth, hidden, shadow, secret, sovereignty required
target_tier target_tier same as caller_tier required
event_tags event_tags comma-separated, no empty items empty list
forward_score_q forward_score_q integer 0..=1000 required
reverse_risk_q reverse_risk_q integer 0..=1000 required
pid fischer_envelope.pid text unset (Fischer BLOCKs missing_pid)
actor fischer_envelope.actor text empty
verb fischer_envelope.verb text empty (Fischer BLOCKs missing_verb)
target fischer_envelope.target text empty
payload fischer_envelope.payload text empty
payload_json_true fischer_envelope.payload_json_true flag 0
hbp_path fischer_envelope.hbp_path text unset
sidecar_plan fischer_envelope.sidecar_plan text unset
ledger_path fischer_envelope.ledger_path text unset
tuple fischer_envelope.tuple text unset
cube_47d fischer_envelope.cube_47d text unset
cosign fischer_envelope.cosign text unset
halt_path fischer_envelope.halt_path text unset
authority_jump fischer_envelope.authority_jump flag 0
recursive_consent fischer_envelope.recursive_consent flag 0
operator_witness_required fischer_envelope.operator_witness_required flag 0
operator_witness fischer_envelope.operator_witness flag 0
composite fischer_score.composite text; accepted for completeness, ignored by every gate and not shown in output 0
l0_real fischer_score.l0_real flag 0
shannon_q fischer_score.shannon_q integer 0..=1000 (per-mille) 0
shannon_above fischer_score.shannon_above flag 0
g4_state fischer_score.g4_state UNKNOWN, EDGE_MINED, PATH_FOUND, CONVERGED, MISTAKE_FLAGGED (anything else parses, then the input guard BLOCKs it) UNKNOWN
fischer_strict VerifiedEnvelope::fischer_strict flag 0

An optional text key that is present with an empty value (cosign=) is set to the empty string, which every gate treats like unset. What each gate checks is described in ../gate/verdict/README.md.

observe

asolaria observe --events <events> <envelope> [--pub <hex> --sig <hex>] computes every GNN-derived gate input itself, from a file of observed graph events and the request in the envelope, and then runs the same five gates as gate. It takes no other options. It runs in process in Rust (gate/observed over model/l0-edge): no Python, torch, Node, JSON or network.

The envelope uses the format above with every key except the seven scores: forward_score_q, reverse_risk_q, composite, l0_real, shannon_q, shannon_above and g4_state. Any of them is a usage error (exit 2) naming the key and its line. The required keys are slot, syscall_no, caller_pid, hook_tier, caller_tier, target_tier and payload. asolaria-hookwall.mjs:70 scores envelope.payload when it is a string and JSON.stringify(envelope.payload ?? envelope.verb ?? '') otherwise; observe scores the string form only, so an envelope without payload is a usage error (exit 2). An empty payload= is scored as the empty string, as upstream scores an empty string payload. In the request, actor -> target is the edge the L0 model scores (graph node ids), and pid and payload are what asolaria-score scores (score(pid, content)). See examples/observe-envelope.hbp.

The events file holds one row per graph-runtime event, EVENT|actor=<id>|actor_type=<t>|target=<id>|target_type=<t>|risk=<integer>|status=<s>, with # comments and blank lines ignored. Every key is optional, and values are percent-escaped (%25, %7C, %3D and every control byte). The full format and the parser's refusals are in ../model/l0-edge/README.md. - reads either file from stdin (not both). The whole events file is read into memory; the parser accepts at most 1,000,000 events.

What it computes, in order:

  1. Node features for every id in the events (extractNodeFeatures, gnnDataExport.js:25-64), scaled as train_baseline.py scales them.
  2. The L0 EdgeLevelGNN forward that the running server (inference_server.py, port 4792) runs, on the request asolaria-score sends: nodes [actor, target], edge [[0, 1]]. When the actor or the target never appears in the events there is no L0 reading, as when :4792 does not answer (l0_real=false). At a toFixed(4) rounding midpoint the L0 value can be one e4 unit from what torch's float32 output gives (measured in ../model/l0-edge/README.md).
  3. The asolaria-score signals: l0 (when present), Shannon of the payload and the sha baseline of pid|payload. L4, G1, G2 and G3 have no service in this stack and are absent.
  4. The composite (asolaria-score.mjs:154-163) and reverseGain (lines 104-108).
  5. The gate envelope: forward_score_q = composite_e4 / 10 (rounded down) and reverse_risk_q = reverse_risk_e4 / 10 (rounded up) for the spawn gate, where _e4 values are the four decimals of toFixed(4) as integers (units of 1/10000), and the Fischer score (composite, l0_real, shannon_q, shannon_above, g4_state=UNKNOWN). Every other field comes from the envelope.

observe prints the GATE row of gate with four keys after the verdict, rendered as the hookwall observation row renders them (asolaria-hookwall.mjs:48-51): score is JavaScript String of the composite (sc.composite), l0_real is true or false, mark is FORWARD_GNN_MARK_GENIUS when reverseGain promotes and REVERSE_GAIN_MARK_MISTAKE otherwise (asolaria-score.mjs:107), and reverse_risk is +(1 - composite).toFixed(4). A failed signature over an envelope or events file that does not parse gives the BLOCK row of gate, every gate UNEVALUATED, without these keys. The L0 reading and the separate signals are not printed; gate/observed returns them to Rust callers. With the SYNTHETIC events fixture (model/l0-edge/tests/fixtures/events.hbp), from the repository root:

$ asolaria observe --events model/l0-edge/tests/fixtures/events.hbp cli/examples/observe-envelope.hbp
GATE|verdict=HOLD|score=0.2139|l0_real=true|mark=REVERSE_GAIN_MARK_MISTAKE|reverse_risk=0.7861|hookwall=PROCEED|spawn_gate=HOLD|tier_gate=PROCEED|fischer=PROCEED|input_guard=PROCEED|signature=unchecked|reason=HOLD by spawn_gate; hookwall=Proceed spawn_gate=Hold tier_gate=Allow fischer=PROCEED cpl=0 flags=[proof_gain_q,gnn_observable,reproducible] input_guard=[]|json=0

The exit code is 3 (HOLD). The other request pairs listed in the fixture's header give, with the same envelope otherwise:

actor -> target score l0_real mark verdict (exit)
agent:benign -> svc:ledger 0.2139 true REVERSE_GAIN_MARK_MISTAKE HOLD (3)
agent:rogue -> svc:vault 0.7593 true FORWARD_GNN_MARK_GENIUS PROCEED (0)
agent:ghost -> svc:ledger 0.4705 false REVERSE_GAIN_MARK_MISTAKE HOLD (3)

The model was trained with BCELoss on the gnnDataExport.js edge label (train_baseline.py:83-105), which is 1 for risk >= 6, status blocked or denied, or an action containing blocked or denied (gnnDataExport.js:74-77). asolaria-score adds L0 to the composite with weight 0.30 (asolaria-score.mjs:156-159). These are the upstream definitions; observe reproduces them.

--pub/--sig work as for gate and cover the exact envelope bytes only. The events file is not signed, so signature=verified says nothing about it. The events file decides the L0 term: with examples/observe-envelope.hbp, the single row EVENT|actor=agent:benign|target=svc:ledger|risk=9 gives score=0.7375 and PROCEED (exit 0), where the fixture gives score=0.2139 and HOLD (exit 3).

Scope

  • Ed25519 is real: ed25519-dalek via asolaria-hookwall, verified with verify_strict (which also rejects small-order keys and non-canonical signatures). Keys come from the operating system's random source (getrandom).

  • The gates are deterministic software policy over values you supply. For gate, scores, g4_state, l0_real and the tiers are inputs, not measurements. For observe, the scores are computed from the events file you supply (g4_state is UNKNOWN: this stack has no G4 service); the events and the tiers are still inputs. The tool enforces nothing by itself; the caller must act on the verdict and exit code.

  • signature=verified means the exact bytes were signed by the private key matching --pub, nothing more. There is no key registry or pinning, and envelopes carry no expiry or nonce, so a signed envelope can be replayed.

  • Nothing persists. No ledger, log or chain row is written; keygen writes only the seed file.

  • There is no physical attestation. The sovereignty tier is a policy label; nothing detects hardware or operator presence.

  • A seed file is a secret. Keep it mode 0600, outside any repository, and never commit it. The seed is held in hookwall's PrivateKey (zeroized on drop) and the hex buffers are wiped, but this is ordinary userspace memory, not a hardware key store. On Windows the file gets the directory's default ACL.

  • sign and verify read their whole input into memory; gate and observe cap envelopes at 1 MiB; observe reads the whole events file into memory.

Development

From the cli/ directory:

cd cli
cargo +1.81.0 test --locked
cargo +1.81.0 clippy --locked --all-targets -- -D warnings

Cargo.lock pins every crate shared with gate/hookwall/Cargo.lock to the same version. observe adds only path dependencies (gate/observed, model/l0-edge); no new crates.io crate.

License

MIT. See ../LICENSE.