# daedal: a worked example **Lachlan Douglas** Governed Data Service is a small, complete system: a confined step seeds a key/value store, and a read-only HTTP service serves the store's contents. It runs on a laptop with the daedal binary, python3 and podman. An engineer can read every grant in the system on one screen, resolve the system before anything runs and swap the store's substrate without editing a consumer. ## The smallest solution A solution is a directory whose root manifest declares `root:`. Components go in `daedal.components/` beneath the root, each declared in a short manifest that names its shape and relationships. The `hello-world` solution in full: ```yaml # daedal.component.yaml (identifiers omitted) label: Hello World description: Minimal "Hello, world", with hard-coded message. root: {} ``` ```yaml # daedal.components/printer/daedal.component.yaml (identifiers omitted) label: Printer description: Prints "Hello, world!". step: {} ``` The only code in the solution is the printer's step. ## The system The root declares the system's grants and binds the system's one aspect, `audit`. The root grants the `seed` step a container executor, one host program and one secret, and configures the store's log level: ```yaml # daedal.component.yaml root: bind: aspects: audit: audit grant: seed: executor: container # confined: --network=none, read-only fs, fd 3 the sole egress commands: python3: # the one program it may proxy to the host secrets: [SEED_TOKEN] # the one secret daedal may inject into that program emit: configurations: store: log_level: warn ``` The store registers on the host and emits an `endpoint` provision to its consumers: ```yaml # daedal.components/store/daedal.component.yaml step: register: $ emit: provisions: - endpoint ``` The service consumes the store's endpoint and accepts the audit offer: ```yaml # daedal.components/service/daedal.component.yaml step: register: $ absorb: offers: audit: {} provisions: store: endpoint: {} ``` The seed data is a `content:` leaf. It runs no code and contributes its records at resolve time: ```yaml # daedal.components/catalog/daedal.component.yaml content: emit: contributions: seed: records: - {sku: A-100, name: Widget, price: 9.99} - {sku: B-200, name: Gadget, price: 19.95} - {sku: C-300, name: Sprocket, price: 4.50} ``` A second leaf, `catalog-extra`, contributes one more record to the same kind, and the seed step receives both sets. The `seed` step's code writes to the store whatever records it receives. The step absorbs the records, the store's endpoint and the audit offer, and requires `python3`: ```yaml # daedal.components/seed/daedal.component.yaml step: require: - python3 absorb: offers: audit: {} contributions: - records provisions: store: endpoint: {} ``` The `audit` aspect emits one offer. The root binds the aspect, and daedal matches the aspect's offer to every step that accepts the offer: ```yaml # daedal.components/audit/daedal.component.yaml aspect: emit: offers: - audit ``` ## Resolution `daedal show` resolves the tree and draws each relationship. The glyphs mark `━` a requirement, `◇` a binding, `△` a registration, `○` a configuration, `▷` a contribution, `◯` an offer, `□` a consumed provision and `◌` an accepted offer: ``` # component descriptions omitted governed-data-service authored Governed Data Service │ ├◇ audit – audit │ └○ store │ ├ audit authored │ Audit │ └◯ audit │ ├ catalog authored │ Catalog │ └▷ seed │ ├ catalog-extra authored │ Catalog (supplementary) │ └▷ seed │ ├ seed authored │ Seed │ ├━ python3 │ ├□ store (endpoint) │ └◌ audit │ ├ service authored │ Service │ ├△ $ │ ├□ store (endpoint) │ └◌ audit │ └ store authored Store └△ $ ``` `daedal steps` derives the apply order from those relationships: ``` governed-data-service (4 steps, apply order) 1 audit aspect 2 store step 3 seed step after: catalog, catalog-extra, store, audit 4 service step after: store, audit ``` The two catalogs appear as dependencies of `seed` and have no step of their own. daedal resolves each content leaf at compile time and adds its records to the consumer's input. `daedal compile` shows the records in the seed step's input, attributed to their source, before anything runs: ``` ├ seed │ ├─ node_name: seed │ ├─ deps: [catalog, catalog-extra, store, audit] │ ├─ manifest: │ │ code: step.py │ └─ input: │ contributions: │ records: │ - attributed_instance: catalog │ value: │ - {name: Widget, price: 9.99, sku: A-100} │ - {name: Gadget, price: 19.95, sku: B-200} │ - {name: Sprocket, price: 4.5, sku: C-300} │ - attributed_instance: catalog-extra │ value: │ - {name: Flange, price: 12.0, sku: D-400} ``` That output is the normal form Free Assembly describes. ## Apply Set `DAEDAL_RUNNER_PYTHON` to the Python step SDK, `DAEDAL_CONTAINER_RUNTIME` to `podman` and `DAEDAL_SANDBOX_IMAGE` to the confinement image, then run: ```sh SEED_TOKEN=choose-a-secret daedal apply curl localhost:8080/data ``` The service returns the seeded catalog. `daedal apply --down` runs each step's recorded destroyers in reverse order and takes the system down. ## Governance The `seed` step is the one privileged component. The root's `grant:` declares the step's authority. With `executor: container`, daedal confines the step: no network, a read-only filesystem and one control channel on file descriptor 3 as its route to the host. Over that channel the step asks daedal to run a program, and daedal runs only the programs the operator has granted. The step's code tests this by asking daedal to run `echo`, which the operator has not granted: ```python # daedal.components/seed/step.py (excerpt) denied = daedal.run(["echo", "unreachable"]) if denied.get("status") is None: daedal.log("default-deny confirmed: proxying ungranted 'echo' was refused") else: return ["error", "governance breach: ungranted 'echo' was NOT refused (status {})".format(denied.get("status"))] reply = daedal.run(["python3", "-c", poster_source(url, rows)], secrets=["SEED_TOKEN"]) ``` The refused call returns with no exit status. For the granted call, daedal runs `python3` on the host. daedal resolves `SEED_TOKEN` in memory and places the value in that process's environment. The confined step writes only the variable's name into the source it passes across the channel, so the token's value stays outside the step, outside every argument list and off the disk. ## The substrate swap The store emits an `endpoint`, and the root's binding decides which registrant realises the store. By default the store's own step launches `kvstore.py` as a host process. The solution also contains `store-container`, a registrant that runs the same `kvstore.py` in a podman container through an imported `podman-run` component. The swap is one binding and one grant in the root: ```yaml root: bind: aspects: audit: audit registrants: store: store-container # the swap grant: seed: executor: container commands: python3: secrets: [SEED_TOKEN] store-container/podman-run: # the container runner may run podman and forward the secret commands: podman: secrets: [SEED_TOKEN] ``` With `store-container` bound, `daedal steps` lists the container runner before the store. `seed` and `service` keep their dependencies: ``` governed-data-service (5 steps, apply order) 1 audit aspect 2 podman-run step after: store-container/container-spec 3 store step after: store-container/podman-run 4 seed step after: catalog, catalog-extra, store, audit 5 service step after: store, audit ``` The service declares its need for `store.endpoint` and names neither the host process nor the container. ## Reuse `podman-run` is a published component in the catalogue, and this solution imports it. Its interface: ```yaml # podman-run step: register: $ require: - podman emit: provisions: - running absorb: contributions: - container ``` A publisher marks a component as a package and projects it into the local bundle store. A consumer fetches the bundle and imports the package by name into a scope, where daedal mints a new identity and derives the wiring: ```sh # publisher daedal pack mybundle podman-run daedal export podman-run # consumer daedal download "$SRC_URL" daedal import podman-run / ```