diff --git a/.gitignore b/.gitignore index 9e89508..a362608 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.sock .amduat-asl/ artifact.bin +tier1/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 4597847..730d92b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,6 @@ target_include_directories(amduatd ) target_link_libraries(amduatd - PRIVATE amduat_pel amduat_format amduat_asl_store_fs amduat_asl amduat_enc - amduat_hash_asl1 amduat_util + PRIVATE amduat_tgk amduat_pel amduat_format amduat_asl_store_fs amduat_asl + amduat_enc amduat_hash_asl1 amduat_util ) diff --git a/README.md b/README.md index 59de98e..87b4283 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,38 @@ Run the daemon: ./build/amduatd --root .amduat-asl --sock amduatd.sock ``` +Dev loop (build + restart): + +```sh +./scripts/dev-restart.sh +``` + Query store meta: ```sh curl --unix-socket amduatd.sock http://localhost/v1/meta ``` +Browser UI (use `socat` to forward the Unix socket): + +```sh +socat TCP-LISTEN:8080,fork,reuseaddr UNIX-CONNECT:amduatd.sock +``` + +Then open `http://localhost:8080/v1/ui` (concept editor). + +Discover the store-backed API contract ref: + +```sh +curl --unix-socket amduatd.sock 'http://localhost/v1/contract?format=ref' +``` + +Fetch the contract bytes (JSON): + +```sh +curl --unix-socket amduatd.sock http://localhost/v1/contract +``` + Upload raw bytes: ```sh @@ -66,18 +92,74 @@ curl --unix-socket amduatd.sock -X POST http://localhost/v1/pel/run \ -d '{"program_ref":"","input_refs":[""],"params_ref":""}' ``` +Define a PEL/PROGRAM-DAG/1 program (store-backed): + +```sh +curl --unix-socket amduatd.sock -X POST http://localhost/v1/pel/programs \ + -H 'Content-Type: application/json' \ + -d '{"nodes":[{"id":1,"op":{"name":"pel.bytes.concat","version":1},"inputs":[{"external":{"input_index":0}},{"external":{"input_index":1}}],"params_hex":""}],"roots":[{"node_id":1,"output_index":0}]}' +``` + +Create a named concept and publish a ref (so you can use the name instead of hex refs): + +```sh +curl --unix-socket amduatd.sock -X POST http://localhost/v1/concepts \ + -H 'Content-Type: application/json' \ + -d '{"name":"hello","ref":""}' +``` + +Publish a new version: + +```sh +curl --unix-socket amduatd.sock -X POST http://localhost/v1/concepts/hello/publish \ + -H 'Content-Type: application/json' \ + -d '{"ref":""}' +``` + +Resolve the latest ref: + +```sh +curl --unix-socket amduatd.sock http://localhost/v1/resolve/hello +``` + +Inspect concepts: + +```sh +curl --unix-socket amduatd.sock http://localhost/v1/concepts +curl --unix-socket amduatd.sock http://localhost/v1/concepts/hello +``` + +Artifact info (length + type tag): + +```sh +curl --unix-socket amduatd.sock 'http://localhost/v1/artifacts/?format=info' +``` + ## Current endpoints -- `GET /v1/meta` → `{store_id, encoding_profile_id, hash_id}` +- `GET /v1/meta` → `{store_id, encoding_profile_id, hash_id, api_contract_ref}` +- `GET /v1/contract` → contract bytes (JSON) (+ `X-Amduat-Contract-Ref` header) +- `GET /v1/contract?format=ref` → `{ref}` +- `GET /v1/ui` → browser UI for authoring/running programs - `POST /v1/artifacts` - raw bytes: `Content-Type: application/octet-stream` (+ optional `X-Amduat-Type-Tag: 0x...`) - artifact framing: `Content-Type: application/vnd.amduat.asl.artifact+v1` - `GET|HEAD /v1/artifacts/{ref}` - raw bytes default - artifact framing: `?format=artifact` +- `POST /v1/concepts` + - request: `{name, ref?}` (`name` is lowercase; `ref` publishes an initial version) + - response: `{name, concept_ref}` +- `POST /v1/concepts/{name}/publish` → publishes a new version `{ref}` +- `GET /v1/concepts` → `{concepts:[{name, concept_ref}]}` +- `GET /v1/concepts/{name}` → `{name, concept_ref, latest_ref, versions[]}` +- `GET /v1/resolve/{name}` → `{ref}` (latest published) - `POST /v1/pel/run` - - request: `{program_ref, input_refs[], params_ref?, scheme_ref?}` (refs are hex strings; omit `scheme_ref` to use `dag`) + - request: `{program_ref, input_refs[], params_ref?, scheme_ref?}` (`program_ref`/`input_refs`/`params_ref` accept hex refs or concept names; omit `scheme_ref` to use `dag`) - response: `{result_ref, trace_ref?, output_refs[], status}` +- `POST /v1/pel/programs` + - request: authoring JSON for `PEL/PROGRAM-DAG/1` (kernel ops only; `params_hex` is raw hex bytes) + - response: `{program_ref}` ## Notes diff --git a/registry/README.md b/registry/README.md new file mode 100644 index 0000000..846c70b --- /dev/null +++ b/registry/README.md @@ -0,0 +1,20 @@ +# amduat-api registry (draft) + +This directory mirrors the core `vendor/amduat/registry/` model for the +**amduatd HTTP API contract**. + +## Goal + +- Keep a stable, machine-readable API contract in-repo. +- Seed that contract into the daemon's single ASL store root at startup. +- Advertise the store-backed contract `ref` via `GET /v1/meta`. + +The daemon remains thin: the contract bytes are data, and the store-backed `ref` +acts as the version identifier. + +## Files + +- `api-contract.schema.md` — JSONL manifest schema for API contracts. +- `api-contract.jsonl` — manifest of published contracts. +- `amduatd-api-contract.v1.json` — contract bytes (v1). + diff --git a/registry/amduatd-api-contract.v1.json b/registry/amduatd-api-contract.v1.json new file mode 100644 index 0000000..dd2dfcc --- /dev/null +++ b/registry/amduatd-api-contract.v1.json @@ -0,0 +1 @@ +{"contract":"AMDUATD/API/1","base_path":"/v1","endpoints":[{"method":"GET","path":"/v1/ui"},{"method":"GET","path":"/v1/meta"},{"method":"HEAD","path":"/v1/meta"},{"method":"GET","path":"/v1/contract"},{"method":"POST","path":"/v1/concepts"},{"method":"GET","path":"/v1/concepts"},{"method":"GET","path":"/v1/concepts/{name}"},{"method":"POST","path":"/v1/concepts/{name}/publish"},{"method":"GET","path":"/v1/resolve/{name}"},{"method":"POST","path":"/v1/artifacts"},{"method":"GET","path":"/v1/artifacts/{ref}"},{"method":"HEAD","path":"/v1/artifacts/{ref}"},{"method":"GET","path":"/v1/artifacts/{ref}?format=info"},{"method":"POST","path":"/v1/pel/run"},{"method":"POST","path":"/v1/pel/programs"}],"schemas":{"pel_run_request":{"type":"object","required":["program_ref","input_refs"],"properties":{"program_ref":{"type":"string","description":"hex ref or concept name"},"input_refs":{"type":"array","items":{"type":"string","description":"hex ref or concept name"}},"params_ref":{"type":"string","description":"hex ref or concept name"},"scheme_ref":{"type":"string","description":"hex ref or 'dag'"}}},"pel_run_response":{"type":"object","required":["result_ref","output_refs","status"],"properties":{"result_ref":{"type":"string","description":"hex ref"},"trace_ref":{"type":"string","description":"hex ref"},"output_refs":{"type":"array","items":{"type":"string","description":"hex ref"}},"status":{"type":"string"}}},"pel_program_author_request":{"type":"object","required":["nodes","roots"],"properties":{"nodes":{"type":"array"},"roots":{"type":"array"}}},"concept_create_request":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"ref":{"type":"string","description":"hex ref"}}},"artifact_info_response":{"type":"object","required":["len","has_type_tag","type_tag"],"properties":{"len":{"type":"integer"},"has_type_tag":{"type":"boolean"},"type_tag":{"type":"string"}}}}} diff --git a/registry/api-contract.jsonl b/registry/api-contract.jsonl new file mode 100644 index 0000000..64f4b71 --- /dev/null +++ b/registry/api-contract.jsonl @@ -0,0 +1 @@ +{"registry":"AMDUATD/API","contract":"AMDUATD/API/1","handle":"amduat.api.amduatd.contract.v1@1","media_type":"application/json","status":"active","bytes_sha256":"0072ad1a308bfa52c7578a1ff4fbfc85b662b41f37a839f4390bdb4c24ecef0c","notes":"Seeded into the ASL store at amduatd startup; ref is advertised via /v1/meta."} diff --git a/registry/api-contract.schema.md b/registry/api-contract.schema.md new file mode 100644 index 0000000..02ccfa6 --- /dev/null +++ b/registry/api-contract.schema.md @@ -0,0 +1,20 @@ +# Schema: registry/api-contract.jsonl + +Each line is one JSON object (JSONL). This mirrors the conventions described in +`vendor/amduat/registry/README.md`. + +## Canonical descriptor digest + +`bytes_sha256` is the SHA-256 digest (lowercase hex) of the exact contract bytes +as stored in the corresponding `registry/*.json` file. + +## Fields + +- `registry` (string, required): registry name, fixed to `AMDUATD/API`. +- `contract` (string, required): contract series identifier (e.g. `AMDUATD/API/1`). +- `handle` (string, required): stable handle (e.g. `amduat.api.amduatd.contract.v1@1`). +- `media_type` (string, required): content type of the bytes (e.g. `application/json`). +- `status` (string, required): `active` | `deprecated` | `reserved`. +- `bytes_sha256` (string, required): sha256 of the bytes file. +- `notes` (string, optional): human notes. + diff --git a/scripts/dev-restart.sh b/scripts/dev-restart.sh new file mode 100755 index 0000000..e5ec8c0 --- /dev/null +++ b/scripts/dev-restart.sh @@ -0,0 +1,36 @@ +#!/bin/sh +set -eu + +ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +BUILD_DIR="$ROOT_DIR/build" +SOCK_PATH=${AMDUATD_SOCK:-"$ROOT_DIR/amduatd.sock"} +STORE_ROOT=${AMDUATD_ROOT:-"$ROOT_DIR/.amduat-asl"} + +if [ ! -d "$BUILD_DIR" ]; then + cmake -S "$ROOT_DIR" -B "$BUILD_DIR" +fi + +cmake --build "$BUILD_DIR" -j + +if [ -S "$SOCK_PATH" ]; then + pid= + if command -v lsof >/dev/null 2>&1; then + pid=$(lsof -t -- "$SOCK_PATH" 2>/dev/null | head -n 1 || true) + elif command -v fuser >/dev/null 2>&1; then + pid=$(fuser "$SOCK_PATH" 2>/dev/null | awk '{print $1}' | head -n 1 || true) + fi + + if [ -n "${pid:-}" ]; then + kill "$pid" 2>/dev/null || true + i=0 + while [ $i -lt 30 ] && kill -0 "$pid" 2>/dev/null; do + i=$((i + 1)) + sleep 0.1 + done + fi + + rm -f "$SOCK_PATH" +fi + +exec "$BUILD_DIR/amduatd" --root "$STORE_ROOT" --sock "$SOCK_PATH" + diff --git a/src/amduatd.c b/src/amduatd.c index 64e5cfc..71ab760 100644 --- a/src/amduatd.c +++ b/src/amduatd.c @@ -5,11 +5,19 @@ #include "amduat/asl/asl_store_fs_meta.h" #include "amduat/asl/ref_text.h" #include "amduat/asl/store.h" +#include "amduat/asl/ref_derive.h" #include "amduat/enc/asl1_core_codec.h" #include "amduat/enc/pel1_result.h" +#include "amduat/enc/pel_program_dag.h" +#include "amduat/enc/tgk1_edge.h" +#include "amduat/fer/receipt.h" #include "amduat/format/pel.h" +#include "amduat/tgk/core.h" +#include "amduat/pel/program_dag.h" #include "amduat/pel/program_dag_desc.h" +#include "amduat/pel/opreg_kernel.h" #include "amduat/pel/run.h" +#include "amduat/util/hex.h" #include #include @@ -20,6 +28,7 @@ #include #include #include +#include #include #include @@ -27,8 +36,526 @@ #include #include +typedef struct amduatd_strbuf { + char *data; + size_t len; + size_t cap; +} amduatd_strbuf_t; + +static bool amduatd_http_send_json(int fd, + int code, + const char *reason, + const char *json, + bool head_only); + +static bool amduatd_send_json_error(int fd, + int code, + const char *reason, + const char *msg); + +static void amduatd_strbuf_free(amduatd_strbuf_t *b); +static bool amduatd_strbuf_append_cstr(amduatd_strbuf_t *b, const char *s); +static bool amduatd_strbuf_append_char(amduatd_strbuf_t *b, char c); + static const char *const AMDUATD_DEFAULT_ROOT = ".amduat-asl"; static const char *const AMDUATD_DEFAULT_SOCK = "amduatd.sock"; +static const char *const AMDUATD_EDGES_FILE = ".amduatd.edges"; + +static const uint32_t AMDUATD_TGK_EDGE_TYPE = 0u; + +typedef struct { + amduat_reference_t *refs; + size_t len; + size_t cap; +} amduatd_ref_list_t; + +static void amduatd_ref_list_free(amduatd_ref_list_t *list) { + size_t i; + if (list == NULL) { + return; + } + for (i = 0; i < list->len; ++i) { + amduat_reference_free(&list->refs[i]); + } + free(list->refs); + list->refs = NULL; + list->len = 0; + list->cap = 0; +} + +static bool amduatd_ref_list_push(amduatd_ref_list_t *list, + amduat_reference_t ref) { + amduat_reference_t cloned; + amduat_reference_t *next; + + if (list == NULL) { + return false; + } + memset(&cloned, 0, sizeof(cloned)); + if (!amduat_reference_clone(ref, &cloned)) { + return false; + } + if (list->len == list->cap) { + size_t next_cap = list->cap != 0 ? list->cap * 2u : 64u; + next = (amduat_reference_t *)realloc(list->refs, + next_cap * sizeof(*list->refs)); + if (next == NULL) { + amduat_reference_free(&cloned); + return false; + } + list->refs = next; + list->cap = next_cap; + } + list->refs[list->len++] = cloned; + return true; +} + +typedef struct { + const char *root_path; + char edges_path[1024]; + amduat_reference_t rel_aliases_ref; + amduat_reference_t rel_materializes_ref; + amduat_reference_t rel_represents_ref; + amduat_reference_t rel_requires_key_ref; + amduat_reference_t rel_within_domain_ref; + amduat_reference_t rel_computed_by_ref; + amduat_reference_t rel_has_provenance_ref; + amduatd_ref_list_t edge_refs; +} amduatd_concepts_t; + +static bool amduatd_concepts_put_name_artifact(amduat_asl_store_t *store, + const char *name, + amduat_reference_t *out_ref); +static bool amduatd_concepts_put_edge(amduat_asl_store_t *store, + amduatd_concepts_t *c, + amduat_reference_t from, + amduat_reference_t to, + amduat_reference_t relation_concept_ref, + amduat_reference_t *out_edge_ref); +static bool amduatd_concepts_lookup_alias(amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + const amduatd_concepts_t *c, + const char *name, + amduat_reference_t *out_concept_ref); + +static void amduatd_concepts_free(amduatd_concepts_t *c) { + if (c == NULL) { + return; + } + amduat_reference_free(&c->rel_aliases_ref); + amduat_reference_free(&c->rel_materializes_ref); + amduat_reference_free(&c->rel_represents_ref); + amduat_reference_free(&c->rel_requires_key_ref); + amduat_reference_free(&c->rel_within_domain_ref); + amduat_reference_free(&c->rel_computed_by_ref); + amduat_reference_free(&c->rel_has_provenance_ref); + amduatd_ref_list_free(&c->edge_refs); +} + +static const char k_amduatd_contract_v1_json[] = + "{" + "\"contract\":\"AMDUATD/API/1\"," + "\"base_path\":\"/v1\"," + "\"endpoints\":[" + "{\"method\":\"GET\",\"path\":\"/v1/ui\"}," + "{\"method\":\"GET\",\"path\":\"/v1/meta\"}," + "{\"method\":\"HEAD\",\"path\":\"/v1/meta\"}," + "{\"method\":\"GET\",\"path\":\"/v1/contract\"}," + "{\"method\":\"POST\",\"path\":\"/v1/concepts\"}," + "{\"method\":\"GET\",\"path\":\"/v1/concepts\"}," + "{\"method\":\"GET\",\"path\":\"/v1/concepts/{name}\"}," + "{\"method\":\"POST\",\"path\":\"/v1/concepts/{name}/publish\"}," + "{\"method\":\"GET\",\"path\":\"/v1/resolve/{name}\"}," + "{\"method\":\"POST\",\"path\":\"/v1/artifacts\"}," + "{\"method\":\"GET\",\"path\":\"/v1/relations\"}," + "{\"method\":\"GET\",\"path\":\"/v1/artifacts/{ref}\"}," + "{\"method\":\"HEAD\",\"path\":\"/v1/artifacts/{ref}\"}," + "{\"method\":\"GET\",\"path\":\"/v1/artifacts/{ref}?format=info\"}," + "{\"method\":\"POST\",\"path\":\"/v1/pel/run\"}," + "{\"method\":\"POST\",\"path\":\"/v1/pel/programs\"}," + "{\"method\":\"POST\",\"path\":\"/v1/context_frames\"}" + "]," + "\"schemas\":{" + "\"pel_run_request\":{" + "\"type\":\"object\"," + "\"required\":[\"program_ref\",\"input_refs\"]," + "\"properties\":{" + "\"program_ref\":{\"type\":\"string\",\"description\":\"hex ref or concept name\"}," + "\"input_refs\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"description\":\"hex ref or concept name\"}}," + "\"params_ref\":{\"type\":\"string\",\"description\":\"hex ref or concept name\"}," + "\"scheme_ref\":{\"type\":\"string\",\"description\":\"hex ref or 'dag'\"}," + "\"receipt\":{" + "\"type\":\"object\"," + "\"required\":[\"input_manifest_ref\",\"environment_ref\",\"evaluator_id\",\"executor_ref\",\"started_at\",\"completed_at\"]," + "\"properties\":{" + "\"input_manifest_ref\":{\"type\":\"string\",\"description\":\"hex ref or concept name\"}," + "\"environment_ref\":{\"type\":\"string\",\"description\":\"hex ref or concept name\"}," + "\"evaluator_id\":{\"type\":\"string\",\"description\":\"opaque evaluator bytes (utf-8)\"}," + "\"executor_ref\":{\"type\":\"string\",\"description\":\"hex ref or concept name\"}," + "\"sbom_ref\":{\"type\":\"string\",\"description\":\"hex ref or concept name\"}," + "\"parity_digest_hex\":{\"type\":\"string\",\"description\":\"hex bytes\"}," + "\"started_at\":{\"type\":\"integer\"}," + "\"completed_at\":{\"type\":\"integer\"}" + "}" + "}" + "}" + "}," + "\"pel_run_response\":{" + "\"type\":\"object\"," + "\"required\":[\"result_ref\",\"output_refs\",\"status\"]," + "\"properties\":{" + "\"result_ref\":{\"type\":\"string\",\"description\":\"hex ref\"}," + "\"trace_ref\":{\"type\":\"string\",\"description\":\"hex ref\"}," + "\"receipt_ref\":{\"type\":\"string\",\"description\":\"hex ref\"}," + "\"output_refs\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"description\":\"hex ref\"}}," + "\"status\":{\"type\":\"string\"}" + "}" + "}," + "\"context_frame_request\":{" + "\"type\":\"object\"," + "\"required\":[\"bindings\"]," + "\"properties\":{" + "\"bindings\":{" + "\"type\":\"array\"," + "\"items\":{" + "\"type\":\"object\"," + "\"required\":[\"key\"]," + "\"properties\":{" + "\"key\":{\"type\":\"string\",\"description\":\"concept name or hex ref\"}," + "\"value\":{\"type\":\"string\",\"description\":\"hex ref or concept name\"}," + "\"value_ref\":{\"type\":\"string\",\"description\":\"hex ref or concept name\"}," + "\"value_scalar\":{\"type\":\"object\",\"properties\":{" + "\"int\":{\"type\":\"integer\"}," + "\"enum\":{\"type\":\"string\",\"description\":\"concept name or hex ref\"}" + "}}" + "}" + "}" + "}" + "}" + "}," + "\"pel_program_author_request\":{" + "\"type\":\"object\"," + "\"required\":[\"nodes\",\"roots\"]," + "\"properties\":{" + "\"nodes\":{\"type\":\"array\"}," + "\"roots\":{\"type\":\"array\"}" + "}" + "}" + "}" + "}\n"; + +static const char k_amduatd_ui_html[] = + "\n" + "\n" + "\n" + " \n" + " \n" + " amduatd — Concept editor\n" + " \n" + "\n" + "\n" + "
\n" + "
\n" + "
\n" + "
\n" + " amduatd\n" + "
\n" + " \n" + "
\n" + "
\n" + "
\n" + "

Concept editor + PEL runner

\n" + "

Load shows the latest materialized bytes; Save uploads a new artifact and publishes a new version. Use the runner to execute PEL programs against stored artifacts.

\n" + "
\n" + " Open editor\n" + " Run program\n" + "
\n" + "
\n" + "
\n" + "
\n" + "
\n" + " \n" + " \n" + " \n" + " \n" + "
\n" + "
\n" + " \n" + " \n" + " \n" + "
\n" + " \n" + "
\n" + " \n" + "
\n" + "
\n" + " \n" + " \n" + "
\n" + "
\n" + "\n" + "
\n" + "
\n" + "
Upload bytes (sets program_ref)
\n" + "
\n" + " \n" + " \n" + "
\n" + "
\n" + "
Run
\n" + " \n" + "
input_refs (comma-separated hex refs or names)
\n" + " \n" + "
params_ref (optional)
\n" + " \n" + "
scheme_ref (optional, default dag)
\n" + " \n" + "
\n" + " \n" + " /v1/contract\n" + " /v1/meta\n" + "
\n" + "
Response
\n" + "
\n"
+    "          
\n" + "
\n" + "
Relations
\n" + "
\n" + " \n" + "
\n" + "
\n"
+    "          
\n" + "
\n" + "
\n" + "
\n" + "
\n" + "

About

\n" + "

amduatd is a local-first mapping surface over a single ASL store root. This UI is a lightweight editor and runner for concepts and PEL programs.

\n" + "
\n" + " \n" + "
\n" + "
\n" + "
\n" + " © 2025 Niklas Rydberg.\n" + " Back to top\n" + "
\n" + "
\n" + "\n" + " \n" + "\n" + "\n"; static volatile sig_atomic_t amduatd_should_exit = 0; @@ -55,6 +582,950 @@ static bool amduatd_write_all(int fd, const uint8_t *buf, size_t len) { return true; } +static bool amduatd_read_exact(int fd, uint8_t *buf, size_t len); + +static bool amduatd_read_file(const char *path, uint8_t **out, size_t *out_len) { + uint8_t *buf = NULL; + size_t cap = 0; + size_t len = 0; + FILE *f; + + if (out != NULL) { + *out = NULL; + } + if (out_len != NULL) { + *out_len = 0; + } + if (path == NULL || out == NULL || out_len == NULL) { + return false; + } + f = fopen(path, "rb"); + if (f == NULL) { + return false; + } + for (;;) { + size_t n; + if (len == cap) { + size_t next_cap = cap != 0 ? cap * 2u : 4096u; + uint8_t *next = (uint8_t *)realloc(buf, next_cap); + if (next == NULL) { + free(buf); + fclose(f); + return false; + } + buf = next; + cap = next_cap; + } + n = fread(buf + len, 1, cap - len, f); + len += n; + if (n == 0) { + if (ferror(f)) { + free(buf); + fclose(f); + return false; + } + break; + } + } + fclose(f); + *out = buf; + *out_len = len; + return true; +} + +static bool amduatd_read_urandom(uint8_t *out, size_t len) { + int fd; + if (out == NULL || len == 0) { + return false; + } + fd = open("/dev/urandom", O_RDONLY); + if (fd < 0) { + return false; + } + if (!amduatd_read_exact(fd, out, len)) { + close(fd); + return false; + } + close(fd); + return true; +} + +static bool amduatd_name_valid(const char *name) { + size_t i; + size_t len; + if (name == NULL) { + return false; + } + len = strlen(name); + if (len == 0 || len > 64) { + return false; + } + for (i = 0; i < len; ++i) { + unsigned char c = (unsigned char)name[i]; + if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || + c == '-' || c == '.' || c == '/') { + continue; + } + return false; + } + return true; +} + +static bool amduatd_build_prefixed_bytes(const char *prefix, + const char *text, + uint8_t **out, + size_t *out_len) { + size_t p_len; + size_t t_len; + size_t len; + uint8_t *buf; + + if (out != NULL) { + *out = NULL; + } + if (out_len != NULL) { + *out_len = 0; + } + if (prefix == NULL || text == NULL || out == NULL || out_len == NULL) { + return false; + } + p_len = strlen(prefix); + t_len = strlen(text); + if (p_len > SIZE_MAX - 1u || t_len > SIZE_MAX - (p_len + 1u)) { + return false; + } + len = p_len + 1u + t_len; + buf = (uint8_t *)malloc(len); + if (buf == NULL) { + return false; + } + memcpy(buf, prefix, p_len); + buf[p_len] = 0; + memcpy(buf + p_len + 1u, text, t_len); + *out = buf; + *out_len = len; + return true; +} + +static bool amduatd_concepts_seed_relation(amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + const char *relation_name, + amduat_reference_t *out_ref) { + uint8_t *bytes = NULL; + size_t bytes_len = 0; + amduat_artifact_t artifact; + + if (out_ref == NULL) { + return false; + } + *out_ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + if (store == NULL || cfg == NULL || relation_name == NULL) { + return false; + } + if (!amduatd_build_prefixed_bytes("AMDUATD/RELATION/1", relation_name, &bytes, + &bytes_len)) { + return false; + } + artifact = amduat_artifact(amduat_octets(bytes, bytes_len)); + if (!amduat_asl_ref_derive(artifact, + cfg->config.encoding_profile_id, + cfg->config.hash_id, + out_ref, + NULL)) { + free(bytes); + return false; + } + (void)amduat_asl_store_put(store, artifact, out_ref); + free(bytes); + return true; +} + +static bool amduatd_concepts_ensure_alias(amduatd_concepts_t *c, + amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + const char *alias_name, + amduat_reference_t concept_ref) { + amduat_reference_t existing; + amduat_reference_t name_ref; + amduat_reference_t edge_ref; + + memset(&existing, 0, sizeof(existing)); + memset(&name_ref, 0, sizeof(name_ref)); + memset(&edge_ref, 0, sizeof(edge_ref)); + + if (c == NULL || store == NULL || cfg == NULL || alias_name == NULL) { + return false; + } + if (!amduatd_name_valid(alias_name)) { + return false; + } + if (amduatd_concepts_lookup_alias(store, cfg, c, alias_name, &existing)) { + amduat_reference_free(&existing); + return true; + } + if (!amduatd_concepts_put_name_artifact(store, alias_name, &name_ref)) { + return false; + } + if (!amduatd_concepts_put_edge(store, + c, + name_ref, + concept_ref, + c->rel_aliases_ref, + &edge_ref)) { + amduat_reference_free(&name_ref); + amduat_reference_free(&edge_ref); + return false; + } + amduat_reference_free(&name_ref); + amduat_reference_free(&edge_ref); + return true; +} + +static bool amduatd_concepts_init(amduatd_concepts_t *c, + amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + const char *root_path) { + uint8_t *file_bytes = NULL; + size_t file_len = 0; + size_t i; + + if (c == NULL || store == NULL || cfg == NULL || root_path == NULL) { + return false; + } + memset(c, 0, sizeof(*c)); + c->root_path = root_path; + (void)snprintf(c->edges_path, sizeof(c->edges_path), "%s/%s", root_path, + AMDUATD_EDGES_FILE); + + if (!amduatd_concepts_seed_relation(store, cfg, "aliases", + &c->rel_aliases_ref)) { + return false; + } + if (!amduatd_concepts_ensure_alias(c, store, cfg, "ms.aliases", + c->rel_aliases_ref)) { + return false; + } + if (!amduatd_concepts_seed_relation(store, cfg, "materializesAs", + &c->rel_materializes_ref)) { + return false; + } + if (!amduatd_concepts_ensure_alias(c, store, cfg, "ms.materializes_as", + c->rel_materializes_ref)) { + return false; + } + if (!amduatd_concepts_seed_relation(store, cfg, "represents", + &c->rel_represents_ref)) { + return false; + } + if (!amduatd_concepts_ensure_alias(c, store, cfg, "ms.represents", + c->rel_represents_ref)) { + return false; + } + if (!amduatd_concepts_seed_relation(store, cfg, "requiresKey", + &c->rel_requires_key_ref)) { + return false; + } + if (!amduatd_concepts_ensure_alias(c, store, cfg, "ms.requires_key", + c->rel_requires_key_ref)) { + return false; + } + if (!amduatd_concepts_seed_relation(store, cfg, "withinDomain", + &c->rel_within_domain_ref)) { + return false; + } + if (!amduatd_concepts_ensure_alias(c, store, cfg, "ms.within_domain", + c->rel_within_domain_ref)) { + return false; + } + if (!amduatd_concepts_seed_relation(store, cfg, "computedBy", + &c->rel_computed_by_ref)) { + return false; + } + if (!amduatd_concepts_ensure_alias(c, store, cfg, "ms.computed_by", + c->rel_computed_by_ref)) { + return false; + } + if (!amduatd_concepts_seed_relation(store, cfg, "hasProvenance", + &c->rel_has_provenance_ref)) { + return false; + } + if (!amduatd_concepts_ensure_alias(c, store, cfg, "ms.has_provenance", + c->rel_has_provenance_ref)) { + return false; + } + + if (!amduatd_read_file(c->edges_path, &file_bytes, &file_len)) { + return true; + } + + { + char *text = (char *)file_bytes; + char *line = text; + char *end = text + file_len; + while (line < end) { + char *nl = memchr(line, '\n', (size_t)(end - line)); + size_t n = nl != NULL ? (size_t)(nl - line) : (size_t)(end - line); + char *tmp; + amduat_reference_t ref; + bool ok; + + while (n != 0 && (line[n - 1] == '\r' || line[n - 1] == ' ' || + line[n - 1] == '\t')) { + n--; + } + while (n != 0 && (*line == ' ' || *line == '\t')) { + line++; + n--; + } + if (n != 0) { + tmp = (char *)malloc(n + 1u); + if (tmp == NULL) { + free(file_bytes); + return false; + } + memcpy(tmp, line, n); + tmp[n] = '\0'; + memset(&ref, 0, sizeof(ref)); + ok = amduat_asl_ref_decode_hex(tmp, &ref); + free(tmp); + if (ok) { + (void)amduatd_ref_list_push(&c->edge_refs, ref); + amduat_reference_free(&ref); + } + } + if (nl == NULL) { + break; + } + line = nl + 1; + } + } + + free(file_bytes); + + for (i = 0; i < c->edge_refs.len; ++i) { + if (c->edge_refs.refs[i].hash_id == 0 || + c->edge_refs.refs[i].digest.data == NULL || + c->edge_refs.refs[i].digest.len == 0) { + continue; + } + } + + return true; +} + +static bool amduatd_concepts_append_edge_ref(amduatd_concepts_t *c, + amduat_reference_t edge_ref) { + char *hex = NULL; + FILE *f; + bool ok; + + if (c == NULL) { + return false; + } + if (!amduat_asl_ref_encode_hex(edge_ref, &hex)) { + return false; + } + f = fopen(c->edges_path, "ab"); + if (f == NULL) { + free(hex); + return false; + } + ok = fprintf(f, "%s\n", hex) > 0; + fclose(f); + if (!ok) { + free(hex); + return false; + } + free(hex); + return amduatd_ref_list_push(&c->edge_refs, edge_ref); +} + +static bool amduatd_concepts_derive_name_ref( + const amduat_asl_store_fs_config_t *cfg, + const char *name, + amduat_reference_t *out_ref) { + uint8_t *bytes = NULL; + size_t bytes_len = 0; + amduat_artifact_t artifact; + + if (out_ref == NULL) { + return false; + } + *out_ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + if (cfg == NULL || name == NULL) { + return false; + } + if (!amduatd_build_prefixed_bytes("AMDUATD/NAME/1", name, &bytes, + &bytes_len)) { + return false; + } + artifact = amduat_artifact(amduat_octets(bytes, bytes_len)); + if (!amduat_asl_ref_derive(artifact, + cfg->config.encoding_profile_id, + cfg->config.hash_id, + out_ref, + NULL)) { + free(bytes); + return false; + } + free(bytes); + return true; +} + +static bool amduatd_concepts_put_name_artifact(amduat_asl_store_t *store, + const char *name, + amduat_reference_t *out_ref) { + uint8_t *bytes = NULL; + size_t bytes_len = 0; + amduat_artifact_t artifact; + + if (out_ref != NULL) { + *out_ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + } + if (store == NULL || name == NULL || out_ref == NULL) { + return false; + } + if (!amduatd_build_prefixed_bytes("AMDUATD/NAME/1", name, &bytes, + &bytes_len)) { + return false; + } + artifact = amduat_artifact(amduat_octets(bytes, bytes_len)); + if (amduat_asl_store_put(store, artifact, out_ref) != AMDUAT_ASL_STORE_OK) { + free(bytes); + return false; + } + free(bytes); + return true; +} + +static bool amduatd_concepts_put_concept_id(amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + amduat_reference_t *out_ref) { + uint8_t rnd[16]; + uint8_t *bytes = NULL; + size_t bytes_len = 0; + amduat_artifact_t artifact; + + if (out_ref != NULL) { + *out_ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + } + if (store == NULL || cfg == NULL || out_ref == NULL) { + return false; + } + if (!amduatd_read_urandom(rnd, sizeof(rnd))) { + return false; + } + bytes_len = strlen("AMDUATD/CONCEPT-ID/1") + 1u + sizeof(rnd); + bytes = (uint8_t *)malloc(bytes_len); + if (bytes == NULL) { + return false; + } + memcpy(bytes, "AMDUATD/CONCEPT-ID/1", strlen("AMDUATD/CONCEPT-ID/1")); + bytes[strlen("AMDUATD/CONCEPT-ID/1")] = 0; + memcpy(bytes + strlen("AMDUATD/CONCEPT-ID/1") + 1u, rnd, sizeof(rnd)); + artifact = amduat_artifact(amduat_octets(bytes, bytes_len)); + if (!amduat_asl_ref_derive(artifact, + cfg->config.encoding_profile_id, + cfg->config.hash_id, + out_ref, + NULL)) { + free(bytes); + return false; + } + if (amduat_asl_store_put(store, artifact, out_ref) != AMDUAT_ASL_STORE_OK) { + free(bytes); + return false; + } + free(bytes); + return true; +} + +static bool amduatd_concepts_put_edge(amduat_asl_store_t *store, + amduatd_concepts_t *c, + amduat_reference_t from, + amduat_reference_t to, + amduat_reference_t relation_concept_ref, + amduat_reference_t *out_edge_ref) { + amduat_tgk_edge_body_t edge; + amduat_reference_t from_arr[1]; + amduat_reference_t to_arr[1]; + amduat_octets_t bytes; + amduat_artifact_t artifact; + + if (out_edge_ref != NULL) { + *out_edge_ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + } + if (store == NULL || c == NULL || out_edge_ref == NULL) { + return false; + } + + memset(&edge, 0, sizeof(edge)); + edge.type = AMDUATD_TGK_EDGE_TYPE; + from_arr[0] = from; + to_arr[0] = to; + edge.from = from_arr; + edge.from_len = 1; + edge.to = to_arr; + edge.to_len = 1; + edge.payload = relation_concept_ref; + + bytes = amduat_octets(NULL, 0); + if (!amduat_enc_tgk1_edge_encode_v1(&edge, &bytes)) { + return false; + } + artifact = amduat_artifact_with_type(bytes, + amduat_type_tag(AMDUAT_TYPE_TAG_TGK1_EDGE_V1)); + if (amduat_asl_store_put(store, artifact, out_edge_ref) != AMDUAT_ASL_STORE_OK) { + free((void *)bytes.data); + return false; + } + free((void *)bytes.data); + return amduatd_concepts_append_edge_ref(c, *out_edge_ref); +} + +static bool amduatd_concepts_lookup_alias(amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + const amduatd_concepts_t *c, + const char *name, + amduat_reference_t *out_concept_ref) { + amduat_reference_t name_ref; + size_t i; + + if (out_concept_ref != NULL) { + *out_concept_ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + } + if (store == NULL || cfg == NULL || c == NULL || name == NULL || + out_concept_ref == NULL) { + return false; + } + if (!amduatd_concepts_derive_name_ref(cfg, name, &name_ref)) { + return false; + } + + for (i = c->edge_refs.len; i > 0; --i) { + amduat_artifact_t artifact; + amduat_tgk_edge_body_t edge; + amduat_asl_store_error_t err; + + memset(&artifact, 0, sizeof(artifact)); + err = amduat_asl_store_get(store, c->edge_refs.refs[i - 1u], &artifact); + if (err != AMDUAT_ASL_STORE_OK) { + continue; + } + if (!artifact.has_type_tag || + artifact.type_tag.tag_id != AMDUAT_TYPE_TAG_TGK1_EDGE_V1) { + amduat_asl_artifact_free(&artifact); + continue; + } + memset(&edge, 0, sizeof(edge)); + if (!amduat_enc_tgk1_edge_decode_v1(artifact.bytes, &edge)) { + amduat_asl_artifact_free(&artifact); + continue; + } + if (edge.from_len == 1 && edge.to_len == 1 && + amduat_reference_eq(edge.payload, c->rel_aliases_ref) && + amduat_reference_eq(edge.from[0], name_ref)) { + amduat_reference_clone(edge.to[0], out_concept_ref); + amduat_enc_tgk1_edge_free(&edge); + amduat_asl_artifact_free(&artifact); + amduat_reference_free(&name_ref); + return true; + } + amduat_enc_tgk1_edge_free(&edge); + amduat_asl_artifact_free(&artifact); + } + + amduat_reference_free(&name_ref); + return false; +} + +static bool amduatd_concepts_resolve_latest(amduat_asl_store_t *store, + const amduatd_concepts_t *c, + amduat_reference_t concept_ref, + amduat_reference_t *out_ref) { + size_t i; + + if (out_ref != NULL) { + *out_ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + } + if (store == NULL || c == NULL || out_ref == NULL) { + return false; + } + + for (i = c->edge_refs.len; i > 0; --i) { + amduat_artifact_t artifact; + amduat_tgk_edge_body_t edge; + amduat_asl_store_error_t err; + + memset(&artifact, 0, sizeof(artifact)); + err = amduat_asl_store_get(store, c->edge_refs.refs[i - 1u], &artifact); + if (err != AMDUAT_ASL_STORE_OK) { + continue; + } + if (!artifact.has_type_tag || + artifact.type_tag.tag_id != AMDUAT_TYPE_TAG_TGK1_EDGE_V1) { + amduat_asl_artifact_free(&artifact); + continue; + } + memset(&edge, 0, sizeof(edge)); + if (!amduat_enc_tgk1_edge_decode_v1(artifact.bytes, &edge)) { + amduat_asl_artifact_free(&artifact); + continue; + } + if (edge.from_len == 1 && edge.to_len == 1 && + amduat_reference_eq(edge.payload, c->rel_materializes_ref) && + amduat_reference_eq(edge.from[0], concept_ref)) { + amduat_reference_clone(edge.to[0], out_ref); + amduat_enc_tgk1_edge_free(&edge); + amduat_asl_artifact_free(&artifact); + return true; + } + amduat_enc_tgk1_edge_free(&edge); + amduat_asl_artifact_free(&artifact); + } + + return false; +} + +static bool amduatd_parse_name_artifact(amduat_artifact_t artifact, + char *out, + size_t cap) { + const uint8_t *bytes; + size_t len; + const char *prefix = "AMDUATD/NAME/1"; + size_t prefix_len; + size_t i; + size_t name_len; + + if (out != NULL && cap != 0) { + out[0] = '\0'; + } + if (out == NULL || cap == 0) { + return false; + } + if (artifact.bytes.len != 0 && artifact.bytes.data == NULL) { + return false; + } + bytes = artifact.bytes.data; + len = artifact.bytes.len; + prefix_len = strlen(prefix); + if (len < prefix_len + 1u) { + return false; + } + if (memcmp(bytes, prefix, prefix_len) != 0 || bytes[prefix_len] != 0) { + return false; + } + for (i = prefix_len + 1u; i < len; ++i) { + if (bytes[i] == 0) { + return false; + } + } + name_len = len - (prefix_len + 1u); + if (name_len == 0 || name_len >= cap) { + return false; + } + memcpy(out, bytes + prefix_len + 1u, name_len); + out[name_len] = '\0'; + return amduatd_name_valid(out); +} + +static bool amduatd_handle_get_concepts(int fd, + amduat_asl_store_t *store, + const amduatd_concepts_t *concepts) { + amduatd_strbuf_t b; + bool first = true; + size_t i; + + if (store == NULL || concepts == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "internal error"); + } + memset(&b, 0, sizeof(b)); + if (!amduatd_strbuf_append_cstr(&b, "{\"concepts\":[")) { + amduatd_strbuf_free(&b); + return amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + } + + for (i = 0; i < concepts->edge_refs.len; ++i) { + amduat_reference_t edge_ref = concepts->edge_refs.refs[i]; + amduat_artifact_t artifact; + amduat_tgk_edge_body_t edge; + amduat_asl_store_error_t err; + char name[128]; + char *concept_hex = NULL; + + memset(&artifact, 0, sizeof(artifact)); + err = amduat_asl_store_get(store, edge_ref, &artifact); + if (err != AMDUAT_ASL_STORE_OK) { + continue; + } + if (!artifact.has_type_tag || + artifact.type_tag.tag_id != AMDUAT_TYPE_TAG_TGK1_EDGE_V1) { + amduat_asl_artifact_free(&artifact); + continue; + } + memset(&edge, 0, sizeof(edge)); + if (!amduat_enc_tgk1_edge_decode_v1(artifact.bytes, &edge)) { + amduat_asl_artifact_free(&artifact); + continue; + } + if (!(edge.from_len == 1 && edge.to_len == 1 && + amduat_reference_eq(edge.payload, concepts->rel_aliases_ref))) { + amduat_enc_tgk1_edge_free(&edge); + amduat_asl_artifact_free(&artifact); + continue; + } + amduat_asl_artifact_free(&artifact); + + memset(&artifact, 0, sizeof(artifact)); + err = amduat_asl_store_get(store, edge.from[0], &artifact); + if (err != AMDUAT_ASL_STORE_OK || + !amduatd_parse_name_artifact(artifact, name, sizeof(name))) { + amduat_enc_tgk1_edge_free(&edge); + amduat_asl_artifact_free(&artifact); + continue; + } + amduat_asl_artifact_free(&artifact); + + if (!amduat_asl_ref_encode_hex(edge.to[0], &concept_hex)) { + amduat_enc_tgk1_edge_free(&edge); + continue; + } + + if (!first) { + (void)amduatd_strbuf_append_char(&b, ','); + } + first = false; + + (void)amduatd_strbuf_append_cstr(&b, "{\"name\":\""); + (void)amduatd_strbuf_append_cstr(&b, name); + (void)amduatd_strbuf_append_cstr(&b, "\",\"concept_ref\":\""); + (void)amduatd_strbuf_append_cstr(&b, concept_hex); + (void)amduatd_strbuf_append_cstr(&b, "\"}"); + + free(concept_hex); + amduat_enc_tgk1_edge_free(&edge); + } + + if (!amduatd_strbuf_append_cstr(&b, "]}\n")) { + amduatd_strbuf_free(&b); + return amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + } + { + bool ok = amduatd_http_send_json(fd, 200, "OK", b.data, false); + amduatd_strbuf_free(&b); + return ok; + } +} + +static bool amduatd_append_relation_entry(amduatd_strbuf_t *b, + bool *first, + const char *name, + amduat_reference_t ref) { + char *hex = NULL; + if (b == NULL || first == NULL || name == NULL) { + return false; + } + if (!amduat_asl_ref_encode_hex(ref, &hex)) { + return false; + } + if (!*first) { + (void)amduatd_strbuf_append_char(b, ','); + } + *first = false; + (void)amduatd_strbuf_append_cstr(b, "{\"name\":\""); + (void)amduatd_strbuf_append_cstr(b, name); + (void)amduatd_strbuf_append_cstr(b, "\",\"concept_ref\":\""); + (void)amduatd_strbuf_append_cstr(b, hex); + (void)amduatd_strbuf_append_cstr(b, "\"}"); + free(hex); + return true; +} + +static bool amduatd_handle_get_relations(int fd, + const amduatd_concepts_t *concepts) { + amduatd_strbuf_t b; + bool first = true; + + if (concepts == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "internal error"); + } + memset(&b, 0, sizeof(b)); + if (!amduatd_strbuf_append_cstr(&b, "{\"relations\":[")) { + amduatd_strbuf_free(&b); + return amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + } + if (!amduatd_append_relation_entry(&b, &first, "ms.aliases", + concepts->rel_aliases_ref) || + !amduatd_append_relation_entry(&b, &first, "ms.materializes_as", + concepts->rel_materializes_ref) || + !amduatd_append_relation_entry(&b, &first, "ms.represents", + concepts->rel_represents_ref) || + !amduatd_append_relation_entry(&b, &first, "ms.requires_key", + concepts->rel_requires_key_ref) || + !amduatd_append_relation_entry(&b, &first, "ms.within_domain", + concepts->rel_within_domain_ref) || + !amduatd_append_relation_entry(&b, &first, "ms.computed_by", + concepts->rel_computed_by_ref) || + !amduatd_append_relation_entry(&b, &first, "ms.has_provenance", + concepts->rel_has_provenance_ref)) { + amduatd_strbuf_free(&b); + return amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + } + (void)amduatd_strbuf_append_cstr(&b, "]}\n"); + + { + bool ok = amduatd_http_send_json(fd, 200, "OK", b.data, false); + amduatd_strbuf_free(&b); + return ok; + } +} + +static bool amduatd_handle_get_concept(int fd, + amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + const amduatd_concepts_t *concepts, + const char *name) { + amduat_reference_t concept_ref; + amduat_reference_t latest_ref; + amduatd_strbuf_t b; + char *concept_hex = NULL; + char *latest_hex = NULL; + bool have_latest = false; + size_t i; + size_t version_count = 0; + + memset(&concept_ref, 0, sizeof(concept_ref)); + memset(&latest_ref, 0, sizeof(latest_ref)); + memset(&b, 0, sizeof(b)); + + if (store == NULL || cfg == NULL || concepts == NULL || name == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "internal error"); + } + if (!amduatd_name_valid(name)) { + return amduatd_send_json_error(fd, 400, "Bad Request", "invalid name"); + } + if (!amduatd_concepts_lookup_alias(store, cfg, concepts, name, &concept_ref)) { + return amduatd_send_json_error(fd, 404, "Not Found", "unknown concept"); + } + if (amduatd_concepts_resolve_latest(store, concepts, concept_ref, + &latest_ref)) { + have_latest = true; + } + + if (!amduat_asl_ref_encode_hex(concept_ref, &concept_hex)) { + amduat_reference_free(&concept_ref); + amduat_reference_free(&latest_ref); + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + } + if (have_latest) { + if (!amduat_asl_ref_encode_hex(latest_ref, &latest_hex)) { + free(concept_hex); + amduat_reference_free(&concept_ref); + amduat_reference_free(&latest_ref); + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + } + } + + if (!amduatd_strbuf_append_cstr(&b, "{")) { + free(concept_hex); + free(latest_hex); + amduat_reference_free(&concept_ref); + amduat_reference_free(&latest_ref); + return amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + } + (void)amduatd_strbuf_append_cstr(&b, "\"name\":\""); + (void)amduatd_strbuf_append_cstr(&b, name); + (void)amduatd_strbuf_append_cstr(&b, "\",\"concept_ref\":\""); + (void)amduatd_strbuf_append_cstr(&b, concept_hex); + (void)amduatd_strbuf_append_cstr(&b, "\",\"latest_ref\":"); + if (latest_hex != NULL) { + (void)amduatd_strbuf_append_cstr(&b, "\""); + (void)amduatd_strbuf_append_cstr(&b, latest_hex); + (void)amduatd_strbuf_append_cstr(&b, "\""); + } else { + (void)amduatd_strbuf_append_cstr(&b, "null"); + } + (void)amduatd_strbuf_append_cstr(&b, ",\"versions\":["); + + for (i = 0; i < concepts->edge_refs.len; ++i) { + amduat_reference_t edge_ref = concepts->edge_refs.refs[i]; + amduat_artifact_t artifact; + amduat_tgk_edge_body_t edge; + amduat_asl_store_error_t err; + char *edge_hex = NULL; + char *ref_hex = NULL; + + memset(&artifact, 0, sizeof(artifact)); + err = amduat_asl_store_get(store, edge_ref, &artifact); + if (err != AMDUAT_ASL_STORE_OK) { + continue; + } + if (!artifact.has_type_tag || + artifact.type_tag.tag_id != AMDUAT_TYPE_TAG_TGK1_EDGE_V1) { + amduat_asl_artifact_free(&artifact); + continue; + } + memset(&edge, 0, sizeof(edge)); + if (!amduat_enc_tgk1_edge_decode_v1(artifact.bytes, &edge)) { + amduat_asl_artifact_free(&artifact); + continue; + } + amduat_asl_artifact_free(&artifact); + + if (!(edge.from_len == 1 && edge.to_len == 1 && + amduat_reference_eq(edge.payload, concepts->rel_materializes_ref) && + amduat_reference_eq(edge.from[0], concept_ref))) { + amduat_enc_tgk1_edge_free(&edge); + continue; + } + + if (!amduat_asl_ref_encode_hex(edge_ref, &edge_hex) || + !amduat_asl_ref_encode_hex(edge.to[0], &ref_hex)) { + free(edge_hex); + free(ref_hex); + amduat_enc_tgk1_edge_free(&edge); + continue; + } + + if (version_count != 0) { + (void)amduatd_strbuf_append_char(&b, ','); + } + version_count++; + + (void)amduatd_strbuf_append_cstr(&b, "{\"edge_ref\":\""); + (void)amduatd_strbuf_append_cstr(&b, edge_hex); + (void)amduatd_strbuf_append_cstr(&b, "\",\"ref\":\""); + (void)amduatd_strbuf_append_cstr(&b, ref_hex); + (void)amduatd_strbuf_append_cstr(&b, "\"}"); + + free(edge_hex); + free(ref_hex); + amduat_enc_tgk1_edge_free(&edge); + + if (version_count >= 64u) { + break; + } + } + + (void)amduatd_strbuf_append_cstr(&b, "]}\n"); + + free(concept_hex); + free(latest_hex); + amduat_reference_free(&concept_ref); + amduat_reference_free(&latest_ref); + + { + bool ok = amduatd_http_send_json(fd, 200, "OK", b.data, false); + amduatd_strbuf_free(&b); + return ok; + } +} + static bool amduatd_read_exact(int fd, uint8_t *buf, size_t len) { size_t off = 0; while (off < len) { @@ -245,6 +1716,78 @@ static bool amduatd_http_send_json(int fd, fd, code, reason, "application/json", body, len, head_only); } +static bool amduatd_http_req_wants_html(const amduatd_http_req_t *req) { + if (req == NULL) { + return false; + } + if (req->accept[0] == '\0') { + return false; + } + return strstr(req->accept, "text/html") != NULL; +} + +static bool amduatd_http_send_not_found(int fd, const amduatd_http_req_t *req) { + if (amduatd_http_req_wants_html(req)) { + const char *path = (req != NULL && req->path[0] != '\0') ? req->path : "/"; + const char *method = + (req != NULL && req->method[0] != '\0') ? req->method : "GET"; + char html[4096]; + int n = snprintf( + html, + sizeof(html), + "\n" + "\n" + "\n" + " \n" + " \n" + " amduatd — Not Found\n" + " \n" + "\n" + "\n" + "
\n" + "
\n" + "

404 — Not Found

\n" + "

amduatd didn’t recognize %s %s.

\n" + "

Try one of these:

\n" + " \n" + "

Artifact bytes: /v1/artifacts/<ref>

\n" + "
\n" + "
\n" + "\n" + "\n", + method, + path); + if (n <= 0 || (size_t)n >= sizeof(html)) { + return amduatd_http_send_text(fd, 404, "Not Found", "not found\n", false); + } + return amduatd_http_send_status(fd, + 404, + "Not Found", + "text/html; charset=utf-8", + (const uint8_t *)html, + (size_t)n, + false); + } + return amduatd_http_send_text(fd, 404, "Not Found", "not found\n", false); +} + static const char *amduatd_query_param(const char *path, const char *key, char *out_value, @@ -292,11 +1835,27 @@ static const char *amduatd_query_param(const char *path, return NULL; } -typedef struct { - char *data; - size_t len; - size_t cap; -} amduatd_strbuf_t; +static bool amduatd_bytes_contains(const uint8_t *data, + size_t len, + const char *needle) { + size_t i; + size_t nlen; + + if (data == NULL || needle == NULL) { + return false; + } + nlen = strlen(needle); + if (nlen == 0 || nlen > len) { + return false; + } + for (i = 0; i + nlen <= len; ++i) { + if (memcmp(data + i, needle, nlen) == 0) { + return true; + } + } + return false; +} + static void amduatd_strbuf_free(amduatd_strbuf_t *b) { if (b == NULL) { @@ -436,6 +1995,193 @@ static bool amduatd_json_parse_string_noesc(const char **p, return false; } +static bool amduatd_json_parse_u32(const char **p, + const char *end, + uint32_t *out) { + const char *cur; + const char *start; + unsigned long long v; + char *tmp = NULL; + char *endp = NULL; + size_t n; + + if (out != NULL) { + *out = 0; + } + if (p == NULL || *p == NULL || out == NULL) { + return false; + } + + cur = amduatd_json_skip_ws(*p, end); + start = cur; + if (cur >= end) { + return false; + } + if (*cur < '0' || *cur > '9') { + return false; + } + cur++; + while (cur < end && *cur >= '0' && *cur <= '9') { + cur++; + } + n = (size_t)(cur - start); + tmp = (char *)malloc(n + 1u); + if (tmp == NULL) { + return false; + } + memcpy(tmp, start, n); + tmp[n] = '\0'; + errno = 0; + v = strtoull(tmp, &endp, 10); + free(tmp); + if (errno != 0 || endp == NULL || *endp != '\0' || v > 0xffffffffULL) { + return false; + } + *out = (uint32_t)v; + *p = cur; + return true; +} + +static bool amduatd_json_parse_u64(const char **p, + const char *end, + uint64_t *out) { + const char *cur; + const char *start; + unsigned long long v; + char *tmp = NULL; + char *endp = NULL; + size_t n; + + if (out != NULL) { + *out = 0; + } + if (p == NULL || *p == NULL || out == NULL) { + return false; + } + + cur = amduatd_json_skip_ws(*p, end); + start = cur; + if (cur >= end) { + return false; + } + if (*cur < '0' || *cur > '9') { + return false; + } + cur++; + while (cur < end && *cur >= '0' && *cur <= '9') { + cur++; + } + n = (size_t)(cur - start); + tmp = (char *)malloc(n + 1u); + if (tmp == NULL) { + return false; + } + memcpy(tmp, start, n); + tmp[n] = '\0'; + errno = 0; + v = strtoull(tmp, &endp, 10); + free(tmp); + if (errno != 0 || endp == NULL || *endp != '\0') { + return false; + } + *out = (uint64_t)v; + *p = cur; + return true; +} + +static bool amduatd_json_parse_u32_loose(const char **p, + const char *end, + uint32_t *out) { + const char *cur; + const char *sv = NULL; + size_t sv_len = 0; + uint64_t v = 0; + size_t i; + + if (out != NULL) { + *out = 0; + } + if (p == NULL || *p == NULL || out == NULL) { + return false; + } + + cur = amduatd_json_skip_ws(*p, end); + if (cur >= end) { + return false; + } + if (*cur == '"') { + if (!amduatd_json_parse_string_noesc(p, end, &sv, &sv_len)) { + return false; + } + if (sv_len == 0) { + return false; + } + for (i = 0; i < sv_len; ++i) { + unsigned char c = (unsigned char)sv[i]; + if (c < '0' || c > '9') { + return false; + } + v = (v * 10u) + (uint64_t)(c - '0'); + if (v > 0xffffffffULL) { + return false; + } + } + *out = (uint32_t)v; + return true; + } + if (*cur < '0' || *cur > '9') { + return false; + } + for (;;) { + unsigned char c = (unsigned char)*cur; + if (c < '0' || c > '9') { + break; + } + v = (v * 10u) + (uint64_t)(c - '0'); + if (v > 0xffffffffULL) { + return false; + } + cur++; + if (cur >= end) { + break; + } + } + *out = (uint32_t)v; + *p = cur; + return true; +} + +static void amduatd_json_peek_token(const char *p, + const char *end, + char *out, + size_t cap) { + const char *cur; + size_t n = 0; + if (out != NULL && cap != 0) { + out[0] = '\0'; + } + if (out == NULL || cap == 0) { + return; + } + cur = amduatd_json_skip_ws(p, end); + while (cur < end && n + 1 < cap) { + unsigned char c = (unsigned char)*cur; + if (c == ',' || c == '}' || c == ']' || c == '\n' || c == '\r') { + break; + } + if (c < 0x20u || c > 0x7eu) { + out[n++] = '?'; + } else { + out[n++] = (char)c; + } + cur++; + if (n >= 24u) { + break; + } + } + out[n] = '\0'; +} + static bool amduatd_json_skip_string(const char **p, const char *end) { const char *cur; if (p == NULL || *p == NULL) { @@ -630,6 +2376,57 @@ static bool amduatd_decode_ref_hex_str(const char *s, return ok; } +typedef enum { + AMDUATD_REF_OK = 0, + AMDUATD_REF_ERR_INVALID = 1, + AMDUATD_REF_ERR_NOT_FOUND = 2, + AMDUATD_REF_ERR_INTERNAL = 3 +} amduatd_ref_status_t; + +static amduatd_ref_status_t amduatd_decode_ref_or_name_latest( + amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + const amduatd_concepts_t *concepts, + const char *s, + size_t len, + amduat_reference_t *out_ref) { + amduat_reference_t concept_ref; + char *tmp = NULL; + + if (out_ref != NULL) { + memset(out_ref, 0, sizeof(*out_ref)); + } + if (store == NULL || cfg == NULL || concepts == NULL || s == NULL || + out_ref == NULL) { + return AMDUATD_REF_ERR_INTERNAL; + } + + if (amduatd_decode_ref_hex_str(s, len, out_ref)) { + return AMDUATD_REF_OK; + } + + if (!amduatd_copy_json_str(s, len, &tmp)) { + return AMDUATD_REF_ERR_INTERNAL; + } + if (!amduatd_name_valid(tmp)) { + free(tmp); + return AMDUATD_REF_ERR_INVALID; + } + memset(&concept_ref, 0, sizeof(concept_ref)); + if (!amduatd_concepts_lookup_alias(store, cfg, concepts, tmp, &concept_ref)) { + free(tmp); + return AMDUATD_REF_ERR_NOT_FOUND; + } + free(tmp); + + if (!amduatd_concepts_resolve_latest(store, concepts, concept_ref, out_ref)) { + amduat_reference_free(&concept_ref); + return AMDUATD_REF_ERR_NOT_FOUND; + } + amduat_reference_free(&concept_ref); + return AMDUATD_REF_OK; +} + static bool amduatd_send_json_error(int fd, int code, const char *reason, @@ -695,19 +2492,47 @@ static bool amduatd_parse_type_tag_hex(const char *text, static bool amduatd_handle_meta(int fd, const amduat_asl_store_fs_config_t *cfg, + amduat_reference_t api_contract_ref, bool head_only) { - char json[512]; - int n = snprintf(json, - sizeof(json), - "{" - "\"store_id\":\"%s\"," - "\"encoding_profile_id\":\"0x%04x\"," - "\"hash_id\":\"0x%04x\"" - "}\n", - cfg != NULL ? cfg->store_id : "", - cfg != NULL ? (unsigned int)cfg->config.encoding_profile_id - : 0u, - cfg != NULL ? (unsigned int)cfg->config.hash_id : 0u); + char json[1024]; + char *contract_hex = NULL; + int n; + + if (api_contract_ref.hash_id != 0 && api_contract_ref.digest.data != NULL && + api_contract_ref.digest.len != 0) { + (void)amduat_asl_ref_encode_hex(api_contract_ref, &contract_hex); + } + + if (contract_hex != NULL) { + n = snprintf(json, + sizeof(json), + "{" + "\"store_id\":\"%s\"," + "\"encoding_profile_id\":\"0x%04x\"," + "\"hash_id\":\"0x%04x\"," + "\"api_contract_ref\":\"%s\"" + "}\n", + cfg != NULL ? cfg->store_id : "", + cfg != NULL ? (unsigned int)cfg->config.encoding_profile_id + : 0u, + cfg != NULL ? (unsigned int)cfg->config.hash_id : 0u, + contract_hex); + } else { + n = snprintf(json, + sizeof(json), + "{" + "\"store_id\":\"%s\"," + "\"encoding_profile_id\":\"0x%04x\"," + "\"hash_id\":\"0x%04x\"," + "\"api_contract_ref\":null" + "}\n", + cfg != NULL ? cfg->store_id : "", + cfg != NULL ? (unsigned int)cfg->config.encoding_profile_id + : 0u, + cfg != NULL ? (unsigned int)cfg->config.hash_id : 0u); + } + + free(contract_hex); if (n <= 0 || (size_t)n >= sizeof(json)) { return amduatd_http_send_text(fd, 500, "Internal Server Error", "error\n", head_only); @@ -715,8 +2540,766 @@ static bool amduatd_handle_meta(int fd, return amduatd_http_send_json(fd, 200, "OK", json, head_only); } +static bool amduatd_handle_get_contract(int fd, + amduat_asl_store_t *store, + const amduatd_http_req_t *req, + amduat_reference_t api_contract_ref) { + char *hex = NULL; + char format[32]; + + if (api_contract_ref.hash_id == 0 || api_contract_ref.digest.data == NULL || + api_contract_ref.digest.len == 0) { + return amduatd_http_send_not_found(fd, req); + } + + memset(format, 0, sizeof(format)); + if (req != NULL && + amduatd_query_param(req->path, "format", format, sizeof(format)) != + NULL && + strcmp(format, "ref") == 0) { + char json[2048]; + int n; + if (!amduat_asl_ref_encode_hex(api_contract_ref, &hex)) { + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "encode error\n", false); + } + n = snprintf(json, sizeof(json), "{\"ref\":\"%s\"}\n", hex); + free(hex); + if (n <= 0 || (size_t)n >= sizeof(json)) { + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "error\n", false); + } + return amduatd_http_send_json(fd, 200, "OK", json, false); + } + + { + amduat_artifact_t artifact; + amduat_asl_store_error_t err; + + memset(&artifact, 0, sizeof(artifact)); + err = amduat_asl_store_get(store, api_contract_ref, &artifact); + if (err == AMDUAT_ASL_STORE_ERR_NOT_FOUND) { + return amduatd_http_send_not_found(fd, req); + } + if (err != AMDUAT_ASL_STORE_OK) { + amduat_asl_artifact_free(&artifact); + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "store error\n", false); + } + if (artifact.bytes.len != 0 && artifact.bytes.data == NULL) { + amduat_asl_artifact_free(&artifact); + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "store error\n", false); + } + + if (!amduat_asl_ref_encode_hex(api_contract_ref, &hex)) { + amduat_asl_artifact_free(&artifact); + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "encode error\n", false); + } + + { + char hdr[600]; + int n = snprintf(hdr, + sizeof(hdr), + "HTTP/1.1 200 OK\r\n" + "Content-Length: %zu\r\n" + "Content-Type: application/json\r\n" + "X-Amduat-Contract-Ref: %s\r\n" + "Connection: close\r\n" + "\r\n", + artifact.bytes.len, + hex); + free(hex); + if (n <= 0 || (size_t)n >= sizeof(hdr)) { + amduat_asl_artifact_free(&artifact); + return false; + } + if (!amduatd_write_all(fd, (const uint8_t *)hdr, (size_t)n)) { + amduat_asl_artifact_free(&artifact); + return false; + } + if (artifact.bytes.len != 0) { + bool ok = amduatd_write_all(fd, artifact.bytes.data, artifact.bytes.len); + amduat_asl_artifact_free(&artifact); + return ok; + } + amduat_asl_artifact_free(&artifact); + return true; + } + } +} + +static bool amduatd_seed_api_contract(amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + amduat_reference_t *out_ref) { + amduat_artifact_t artifact; + amduat_asl_store_error_t err; + + if (out_ref != NULL) { + memset(out_ref, 0, sizeof(*out_ref)); + } + if (store == NULL || cfg == NULL || out_ref == NULL) { + return false; + } + + artifact = amduat_artifact(amduat_octets(k_amduatd_contract_v1_json, + strlen(k_amduatd_contract_v1_json))); + + (void)amduat_asl_ref_derive(artifact, + cfg->config.encoding_profile_id, + cfg->config.hash_id, + out_ref, + NULL); + err = amduat_asl_store_put(store, artifact, out_ref); + if (err != AMDUAT_ASL_STORE_OK) { + return false; + } + return true; +} + +typedef struct { + char *key_hex; + char *type_hex; + char *key_text; + char *value_hex; +} amduatd_seed_entry_t; + +static int amduatd_seed_entry_cmp(const void *a, const void *b) { + const amduatd_seed_entry_t *x = (const amduatd_seed_entry_t *)a; + const amduatd_seed_entry_t *y = (const amduatd_seed_entry_t *)b; + if (x == NULL || y == NULL) { + return 0; + } + return strcmp(x->key_hex != NULL ? x->key_hex : "", + y->key_hex != NULL ? y->key_hex : ""); +} + +static bool amduatd_seed_concept_if_missing( + amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + amduatd_concepts_t *concepts, + const char *name, + amduat_reference_t *out_concept_ref) { + amduat_reference_t name_ref; + amduat_reference_t concept_ref; + amduat_reference_t edge_ref; + + if (out_concept_ref != NULL) { + *out_concept_ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + } + if (store == NULL || cfg == NULL || concepts == NULL || name == NULL || + out_concept_ref == NULL) { + return false; + } + if (amduatd_concepts_lookup_alias(store, cfg, concepts, name, + out_concept_ref)) { + return true; + } + + memset(&name_ref, 0, sizeof(name_ref)); + memset(&concept_ref, 0, sizeof(concept_ref)); + memset(&edge_ref, 0, sizeof(edge_ref)); + + if (!amduatd_concepts_put_name_artifact(store, name, &name_ref)) { + return false; + } + if (!amduatd_concepts_put_concept_id(store, cfg, &concept_ref)) { + amduat_reference_free(&name_ref); + return false; + } + if (!amduatd_concepts_put_edge(store, + concepts, + name_ref, + concept_ref, + concepts->rel_aliases_ref, + &edge_ref)) { + amduat_reference_free(&name_ref); + amduat_reference_free(&concept_ref); + return false; + } + amduat_reference_free(&name_ref); + amduat_reference_free(&edge_ref); + *out_concept_ref = concept_ref; + return true; +} + +static bool amduatd_seed_store_artifact(amduat_asl_store_t *store, + amduat_artifact_t artifact, + amduat_reference_t *out_ref) { + if (out_ref != NULL) { + *out_ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + } + if (store == NULL || out_ref == NULL) { + return false; + } + if (amduat_asl_store_put(store, artifact, out_ref) != AMDUAT_ASL_STORE_OK) { + return false; + } + return true; +} + +static bool amduatd_seed_pel_identity_program( + amduat_asl_store_t *store, + amduat_reference_t *out_program_ref) { + amduat_pel_program_t program; + amduat_pel_node_t node; + amduat_pel_dag_input_t inputs[1]; + amduat_pel_root_ref_t root; + amduat_octets_t program_bytes; + amduat_artifact_t artifact; + + if (out_program_ref != NULL) { + *out_program_ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + } + if (store == NULL || out_program_ref == NULL) { + return false; + } + + memset(&program, 0, sizeof(program)); + memset(&node, 0, sizeof(node)); + memset(inputs, 0, sizeof(inputs)); + memset(&root, 0, sizeof(root)); + program_bytes = amduat_octets(NULL, 0u); + + inputs[0].kind = AMDUAT_PEL_DAG_INPUT_EXTERNAL; + inputs[0].value.external.input_index = 0; + + node.id = 1; + node.op.name = amduat_octets("pel.bytes.concat", + strlen("pel.bytes.concat")); + node.op.version = 1; + node.inputs = inputs; + node.inputs_len = 1; + node.params = amduat_octets(NULL, 0u); + + root.node_id = 1; + root.output_index = 0; + + program.nodes = &node; + program.nodes_len = 1; + program.roots = &root; + program.roots_len = 1; + + if (!amduat_enc_pel_program_dag_encode_v1(&program, &program_bytes)) { + return false; + } + artifact = amduat_artifact_with_type(program_bytes, + amduat_type_tag(0x00000101u)); + if (!amduatd_seed_store_artifact(store, artifact, out_program_ref)) { + free((void *)program_bytes.data); + return false; + } + free((void *)program_bytes.data); + return true; +} + +static bool amduatd_seed_materializes_if_missing( + amduat_asl_store_t *store, + amduatd_concepts_t *concepts, + amduat_reference_t concept_ref, + amduat_reference_t target_ref) { + amduat_reference_t latest; + amduat_reference_t edge_ref; + + memset(&latest, 0, sizeof(latest)); + if (amduatd_concepts_resolve_latest(store, concepts, concept_ref, &latest)) { + amduat_reference_free(&latest); + return true; + } + if (!amduatd_concepts_put_edge(store, + concepts, + concept_ref, + target_ref, + concepts->rel_materializes_ref, + &edge_ref)) { + return false; + } + amduat_reference_free(&edge_ref); + return true; +} + +static bool amduatd_seed_ms_ui_state(amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + amduatd_concepts_t *concepts) { + amduat_reference_t type_string_ref; + amduat_reference_t type_ref_ref; + amduat_reference_t key_title_ref; + amduat_reference_t key_status_ref; + amduat_reference_t key_latest_ref; + amduat_reference_t schema_concept_ref; + amduat_reference_t registry_concept_ref; + amduat_reference_t identity_program_ref; + amduat_reference_t schema_input_ref; + amduat_reference_t registry_input_ref; + amduat_reference_t schema_output_ref; + amduat_reference_t registry_output_ref; + amduat_reference_t seed_manifest_ref; + amduat_reference_t seed_environment_ref; + amduat_reference_t seed_executor_ref; + amduat_reference_t receipt_ref; + amduat_pel_run_result_t run_result; + amduatd_seed_entry_t fields[3]; + amduatd_seed_entry_t entries[3]; + amduatd_strbuf_t b; + amduat_artifact_t artifact; + char *seed_latest_hex = NULL; + size_t i; + bool ok = false; + + memset(&type_string_ref, 0, sizeof(type_string_ref)); + memset(&type_ref_ref, 0, sizeof(type_ref_ref)); + memset(&key_title_ref, 0, sizeof(key_title_ref)); + memset(&key_status_ref, 0, sizeof(key_status_ref)); + memset(&key_latest_ref, 0, sizeof(key_latest_ref)); + memset(&schema_concept_ref, 0, sizeof(schema_concept_ref)); + memset(®istry_concept_ref, 0, sizeof(registry_concept_ref)); + memset(&identity_program_ref, 0, sizeof(identity_program_ref)); + memset(&schema_input_ref, 0, sizeof(schema_input_ref)); + memset(®istry_input_ref, 0, sizeof(registry_input_ref)); + memset(&schema_output_ref, 0, sizeof(schema_output_ref)); + memset(®istry_output_ref, 0, sizeof(registry_output_ref)); + memset(&seed_manifest_ref, 0, sizeof(seed_manifest_ref)); + memset(&seed_environment_ref, 0, sizeof(seed_environment_ref)); + memset(&seed_executor_ref, 0, sizeof(seed_executor_ref)); + memset(&receipt_ref, 0, sizeof(receipt_ref)); + memset(&run_result, 0, sizeof(run_result)); + memset(fields, 0, sizeof(fields)); + memset(entries, 0, sizeof(entries)); + memset(&b, 0, sizeof(b)); + memset(&artifact, 0, sizeof(artifact)); + + if (!amduatd_seed_concept_if_missing(store, cfg, concepts, "ms.type.string", + &type_string_ref) || + !amduatd_seed_concept_if_missing(store, cfg, concepts, "ms.type.ref", + &type_ref_ref) || + !amduatd_seed_concept_if_missing(store, cfg, concepts, "ms.ui.field.title", + &key_title_ref) || + !amduatd_seed_concept_if_missing(store, cfg, concepts, "ms.ui.field.status", + &key_status_ref) || + !amduatd_seed_concept_if_missing(store, cfg, concepts, + "ms.ui.field.latest_ref", + &key_latest_ref) || + !amduatd_seed_concept_if_missing(store, cfg, concepts, + "ms.ui.state.schema.v1", + &schema_concept_ref) || + !amduatd_seed_concept_if_missing(store, cfg, concepts, + "ms.ui.state.registry.v1", + ®istry_concept_ref)) { + goto seed_cleanup; + } + + if (!amduatd_seed_pel_identity_program(store, &identity_program_ref)) { + goto seed_cleanup; + } + + { + amduat_reference_t latest_schema; + amduat_reference_t latest_registry; + bool have_schema = false; + bool have_registry = false; + bool schema_ok = false; + bool registry_ok = false; + + memset(&latest_schema, 0, sizeof(latest_schema)); + memset(&latest_registry, 0, sizeof(latest_registry)); + have_schema = amduatd_concepts_resolve_latest(store, + concepts, + schema_concept_ref, + &latest_schema); + have_registry = amduatd_concepts_resolve_latest(store, + concepts, + registry_concept_ref, + &latest_registry); + if (have_schema && have_registry) { + amduat_artifact_t schema_artifact; + amduat_artifact_t registry_artifact; + amduat_asl_store_error_t err; + + memset(&schema_artifact, 0, sizeof(schema_artifact)); + memset(®istry_artifact, 0, sizeof(registry_artifact)); + err = amduat_asl_store_get(store, latest_schema, &schema_artifact); + if (err == AMDUAT_ASL_STORE_OK && + schema_artifact.bytes.len != 0 && + schema_artifact.bytes.data != NULL && + amduatd_bytes_contains(schema_artifact.bytes.data, + schema_artifact.bytes.len, + "\"key_text\"")) { + schema_ok = true; + } + err = amduat_asl_store_get(store, latest_registry, ®istry_artifact); + if (err == AMDUAT_ASL_STORE_OK && + registry_artifact.bytes.len != 0 && + registry_artifact.bytes.data != NULL && + amduatd_bytes_contains(registry_artifact.bytes.data, + registry_artifact.bytes.len, + "\"value_text\"")) { + registry_ok = true; + } + amduat_asl_artifact_free(&schema_artifact); + amduat_asl_artifact_free(®istry_artifact); + } + amduat_reference_free(&latest_schema); + amduat_reference_free(&latest_registry); + if (have_schema && have_registry && schema_ok && registry_ok) { + ok = true; + goto seed_cleanup; + } + } + + { + const char *payload = "{\"seed\":\"manifest\"}"; + artifact = amduat_artifact(amduat_octets(payload, strlen(payload))); + if (!amduatd_seed_store_artifact(store, artifact, &seed_manifest_ref)) { + goto seed_cleanup; + } + } + { + const char *payload = "{\"seed\":\"environment\"}"; + artifact = amduat_artifact(amduat_octets(payload, strlen(payload))); + if (!amduatd_seed_store_artifact(store, artifact, &seed_environment_ref)) { + goto seed_cleanup; + } + } + { + const char *payload = "{\"seed\":\"executor\"}"; + artifact = amduat_artifact(amduat_octets(payload, strlen(payload))); + if (!amduatd_seed_store_artifact(store, artifact, &seed_executor_ref)) { + goto seed_cleanup; + } + } + + { + amduat_reference_t hello_ref; + amduat_reference_t title_ref; + amduat_reference_t status_ref; + char *hello_hex = NULL; + const char *payload = "hello"; + + memset(&hello_ref, 0, sizeof(hello_ref)); + memset(&title_ref, 0, sizeof(title_ref)); + memset(&status_ref, 0, sizeof(status_ref)); + + artifact = amduat_artifact(amduat_octets(payload, strlen(payload))); + if (!amduatd_seed_store_artifact(store, artifact, &hello_ref)) { + goto seed_cleanup; + } + if (!amduat_asl_ref_encode_hex(hello_ref, &hello_hex)) { + goto seed_cleanup; + } + seed_latest_hex = hello_hex; + + artifact = amduat_artifact(amduat_octets("Amduat UI", + strlen("Amduat UI"))); + if (!amduatd_seed_store_artifact(store, artifact, &title_ref)) { + free(hello_hex); + goto seed_cleanup; + } + + artifact = amduat_artifact(amduat_octets("ready", strlen("ready"))); + if (!amduatd_seed_store_artifact(store, artifact, &status_ref)) { + free(hello_hex); + goto seed_cleanup; + } + + fields[0].key_hex = NULL; + fields[1].key_hex = NULL; + fields[2].key_hex = NULL; + if (!amduat_asl_ref_encode_hex(key_title_ref, &fields[0].key_hex) || + !amduat_asl_ref_encode_hex(key_status_ref, &fields[1].key_hex) || + !amduat_asl_ref_encode_hex(key_latest_ref, &fields[2].key_hex)) { + goto seed_cleanup; + } + fields[0].type_hex = strdup("ms.type.string"); + fields[1].type_hex = strdup("ms.type.string"); + fields[2].type_hex = strdup("ms.type.ref"); + if (fields[0].type_hex == NULL || fields[1].type_hex == NULL || + fields[2].type_hex == NULL) { + goto seed_cleanup; + } + fields[0].key_text = strdup("title"); + fields[1].key_text = strdup("status"); + fields[2].key_text = strdup("latest_ref"); + if (fields[0].key_text == NULL || fields[1].key_text == NULL || + fields[2].key_text == NULL) { + goto seed_cleanup; + } + + entries[0].key_hex = fields[0].key_hex; + entries[1].key_hex = fields[1].key_hex; + entries[2].key_hex = fields[2].key_hex; + + entries[0].value_hex = strdup("Amduat UI"); + entries[1].value_hex = strdup("ready"); + entries[2].value_hex = hello_hex; + if (entries[0].value_hex == NULL || entries[1].value_hex == NULL || + entries[2].value_hex == NULL) { + goto seed_cleanup; + } + hello_hex = NULL; + seed_latest_hex = NULL; + + qsort(fields, 3, sizeof(fields[0]), amduatd_seed_entry_cmp); + qsort(entries, 3, sizeof(entries[0]), amduatd_seed_entry_cmp); + + if (!amduatd_strbuf_append_cstr(&b, + "{\"schema_version\":1,\"fields\":[")) { + goto seed_cleanup; + } + for (i = 0; i < 3; ++i) { + if (i != 0 && !amduatd_strbuf_append_char(&b, ',')) { + goto seed_cleanup; + } + if (!amduatd_strbuf_append_cstr(&b, "{\"key_ref\":\"") || + !amduatd_strbuf_append_cstr(&b, fields[i].key_hex) || + !amduatd_strbuf_append_cstr(&b, "\",\"key_text\":\"") || + !amduatd_strbuf_append_cstr(&b, fields[i].key_text) || + !amduatd_strbuf_append_cstr(&b, "\",\"type_ref\":\"") || + !amduatd_strbuf_append_cstr(&b, fields[i].type_hex) || + !amduatd_strbuf_append_cstr(&b, "\"}")) { + goto seed_cleanup; + } + } + if (!amduatd_strbuf_append_cstr(&b, "]}")) { + goto seed_cleanup; + } + artifact = amduat_artifact(amduat_octets(b.data, b.len)); + if (!amduatd_seed_store_artifact(store, artifact, &schema_input_ref)) { + goto seed_cleanup; + } + amduatd_strbuf_free(&b); + + memset(&b, 0, sizeof(b)); + if (!amduatd_strbuf_append_cstr(&b, + "{\"registry_version\":1,\"entries\":[")) { + goto seed_cleanup; + } + for (i = 0; i < 3; ++i) { + if (i != 0 && !amduatd_strbuf_append_char(&b, ',')) { + goto seed_cleanup; + } + if (!amduatd_strbuf_append_cstr(&b, "{\"key_ref\":\"") || + !amduatd_strbuf_append_cstr(&b, entries[i].key_hex) || + !amduatd_strbuf_append_cstr(&b, + i == 2 ? "\",\"value_ref\":\"" + : "\",\"value_text\":\"") || + !amduatd_strbuf_append_cstr(&b, entries[i].value_hex) || + !amduatd_strbuf_append_cstr(&b, "\"}")) { + goto seed_cleanup; + } + } + if (!amduatd_strbuf_append_cstr(&b, "]}")) { + goto seed_cleanup; + } + artifact = amduat_artifact(amduat_octets(b.data, b.len)); + if (!amduatd_seed_store_artifact(store, artifact, ®istry_input_ref)) { + goto seed_cleanup; + } + } + + { + amduat_reference_t inputs[1]; + amduat_reference_t scheme_ref = amduat_pel_program_dag_scheme_ref(); + amduat_artifact_t receipt_artifact; + amduat_reference_t edge_ref; + amduat_octets_t evaluator_id = amduat_octets("amduatd-seed", + strlen("amduatd-seed")); + + inputs[0] = schema_input_ref; + if (!amduat_pel_surf_run_with_result(store, + scheme_ref, + identity_program_ref, + inputs, + 1, + false, + amduat_reference(0u, + amduat_octets(NULL, 0u)), + &run_result)) { + goto seed_cleanup; + } + if (!run_result.has_result_value || run_result.output_refs_len != 1) { + goto seed_cleanup; + } + if (!amduat_reference_clone(run_result.output_refs[0], + &schema_output_ref)) { + goto seed_cleanup; + } + + if (!amduat_fer1_receipt_from_pel_result(&run_result.result_value, + seed_manifest_ref, + seed_environment_ref, + evaluator_id, + seed_executor_ref, + false, + amduat_reference(0u, + amduat_octets(NULL, 0u)), + amduat_octets(NULL, 0u), + 0, + 0, + &receipt_artifact)) { + goto seed_cleanup; + } + if (!amduatd_seed_store_artifact(store, receipt_artifact, &receipt_ref)) { + amduat_asl_artifact_free(&receipt_artifact); + goto seed_cleanup; + } + amduat_asl_artifact_free(&receipt_artifact); + if (!amduatd_concepts_put_edge(store, + concepts, + schema_output_ref, + receipt_ref, + concepts->rel_has_provenance_ref, + &edge_ref)) { + goto seed_cleanup; + } + amduat_reference_free(&edge_ref); + amduat_reference_free(&receipt_ref); + amduat_enc_pel1_result_free(&run_result.result_value); + amduat_pel_surf_free_refs(run_result.output_refs, run_result.output_refs_len); + amduat_pel_surf_free_ref(&run_result.result_ref); + memset(&run_result, 0, sizeof(run_result)); + } + + { + amduat_reference_t inputs[1]; + amduat_reference_t scheme_ref = amduat_pel_program_dag_scheme_ref(); + amduat_artifact_t receipt_artifact; + amduat_reference_t edge_ref; + amduat_octets_t evaluator_id = amduat_octets("amduatd-seed", + strlen("amduatd-seed")); + + inputs[0] = registry_input_ref; + if (!amduat_pel_surf_run_with_result(store, + scheme_ref, + identity_program_ref, + inputs, + 1, + false, + amduat_reference(0u, + amduat_octets(NULL, 0u)), + &run_result)) { + goto seed_cleanup; + } + if (!run_result.has_result_value || run_result.output_refs_len != 1) { + goto seed_cleanup; + } + if (!amduat_reference_clone(run_result.output_refs[0], + ®istry_output_ref)) { + goto seed_cleanup; + } + + if (!amduat_fer1_receipt_from_pel_result(&run_result.result_value, + seed_manifest_ref, + seed_environment_ref, + evaluator_id, + seed_executor_ref, + false, + amduat_reference(0u, + amduat_octets(NULL, 0u)), + amduat_octets(NULL, 0u), + 0, + 0, + &receipt_artifact)) { + goto seed_cleanup; + } + if (!amduatd_seed_store_artifact(store, receipt_artifact, &receipt_ref)) { + amduat_asl_artifact_free(&receipt_artifact); + goto seed_cleanup; + } + amduat_asl_artifact_free(&receipt_artifact); + if (!amduatd_concepts_put_edge(store, + concepts, + registry_output_ref, + receipt_ref, + concepts->rel_has_provenance_ref, + &edge_ref)) { + goto seed_cleanup; + } + amduat_reference_free(&edge_ref); + amduat_reference_free(&receipt_ref); + amduat_enc_pel1_result_free(&run_result.result_value); + amduat_pel_surf_free_refs(run_result.output_refs, run_result.output_refs_len); + amduat_pel_surf_free_ref(&run_result.result_ref); + memset(&run_result, 0, sizeof(run_result)); + } + + if (!amduatd_seed_materializes_if_missing(store, concepts, + schema_concept_ref, + schema_output_ref) || + !amduatd_seed_materializes_if_missing(store, concepts, + registry_concept_ref, + registry_output_ref)) { + goto seed_cleanup; + } + + ok = true; + +seed_cleanup: + if (run_result.has_result_value) { + amduat_enc_pel1_result_free(&run_result.result_value); + } + if (run_result.output_refs != NULL) { + amduat_pel_surf_free_refs(run_result.output_refs, run_result.output_refs_len); + } + amduat_pel_surf_free_ref(&run_result.result_ref); + + amduat_reference_free(&type_string_ref); + amduat_reference_free(&type_ref_ref); + amduat_reference_free(&key_title_ref); + amduat_reference_free(&key_status_ref); + amduat_reference_free(&key_latest_ref); + amduat_reference_free(&schema_concept_ref); + amduat_reference_free(®istry_concept_ref); + amduat_reference_free(&identity_program_ref); + amduat_reference_free(&schema_input_ref); + amduat_reference_free(®istry_input_ref); + amduat_reference_free(&schema_output_ref); + amduat_reference_free(®istry_output_ref); + amduat_reference_free(&seed_manifest_ref); + amduat_reference_free(&seed_environment_ref); + amduat_reference_free(&seed_executor_ref); + + for (i = 0; i < 3; ++i) { + free(fields[i].key_hex); + free(fields[i].type_hex); + free(fields[i].key_text); + free(entries[i].value_hex); + } + amduatd_strbuf_free(&b); + free(seed_latest_hex); + return ok; +} + +static bool amduatd_seed_ui_html(amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + amduat_reference_t *out_ref) { + amduat_artifact_t artifact; + amduat_asl_store_error_t err; + + if (out_ref != NULL) { + memset(out_ref, 0, sizeof(*out_ref)); + } + if (store == NULL || cfg == NULL || out_ref == NULL) { + return false; + } + + artifact = amduat_artifact(amduat_octets(k_amduatd_ui_html, + strlen(k_amduatd_ui_html))); + (void)amduat_asl_ref_derive(artifact, + cfg->config.encoding_profile_id, + cfg->config.hash_id, + out_ref, + NULL); + err = amduat_asl_store_put(store, artifact, out_ref); + if (err != AMDUAT_ASL_STORE_OK) { + return false; + } + return true; +} + static bool amduatd_handle_get_artifact(int fd, amduat_asl_store_t *store, + const amduatd_http_req_t *req, const char *path, bool head_only) { char no_query[1024]; @@ -726,14 +3309,15 @@ static bool amduatd_handle_get_artifact(int fd, amduat_artifact_t artifact; amduat_asl_store_error_t err; bool want_artifact = false; + bool want_info = false; memset(&ref, 0, sizeof(ref)); memset(&artifact, 0, sizeof(artifact)); amduatd_path_without_query(path, no_query, sizeof(no_query)); if (strncmp(no_query, "/v1/artifacts/", 14) != 0) { - return amduatd_http_send_text(fd, 404, "Not Found", "not found\n", - head_only); + (void)head_only; + return amduatd_http_send_not_found(fd, req); } ref_text = no_query + 14; if (ref_text[0] == '\0') { @@ -750,8 +3334,13 @@ static bool amduatd_handle_get_artifact(int fd, if (amduatd_query_param(path, "format", format, sizeof(format)) != NULL) { if (strcmp(format, "artifact") == 0) { want_artifact = true; + want_info = false; + } else if (strcmp(format, "info") == 0) { + want_artifact = false; + want_info = true; } else if (strcmp(format, "raw") == 0 || format[0] == '\0') { want_artifact = false; + want_info = false; } else { free((void *)ref.digest.data); return amduatd_http_send_text(fd, 400, "Bad Request", @@ -762,8 +3351,10 @@ static bool amduatd_handle_get_artifact(int fd, err = amduat_asl_store_get(store, ref, &artifact); free((void *)ref.digest.data); if (err == AMDUAT_ASL_STORE_ERR_NOT_FOUND) { - return amduatd_http_send_text(fd, 404, "Not Found", "not found\n", - head_only); + if (!head_only) { + return amduatd_http_send_not_found(fd, req); + } + return amduatd_http_send_text(fd, 404, "Not Found", "not found\n", true); } if (err != AMDUAT_ASL_STORE_OK) { amduat_asl_artifact_free(&artifact); @@ -772,6 +3363,36 @@ static bool amduatd_handle_get_artifact(int fd, } if (!want_artifact) { + if (want_info) { + char json[256]; + int n; + if (artifact.has_type_tag) { + n = snprintf(json, + sizeof(json), + "{" + "\"len\":%zu," + "\"has_type_tag\":true," + "\"type_tag\":\"0x%08x\"" + "}\n", + artifact.bytes.len, + (unsigned int)artifact.type_tag.tag_id); + } else { + n = snprintf(json, + sizeof(json), + "{" + "\"len\":%zu," + "\"has_type_tag\":false," + "\"type_tag\":null" + "}\n", + artifact.bytes.len); + } + amduat_asl_artifact_free(&artifact); + if (n <= 0 || (size_t)n >= sizeof(json)) { + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "error\n", head_only); + } + return amduatd_http_send_json(fd, 200, "OK", json, head_only); + } bool ok = amduatd_http_send_status( fd, 200, @@ -806,8 +3427,9 @@ static bool amduatd_handle_get_artifact(int fd, static bool amduatd_handle_head_artifact(int fd, amduat_asl_store_t *store, + const amduatd_http_req_t *req, const char *path) { - return amduatd_handle_get_artifact(fd, store, path, true); + return amduatd_handle_get_artifact(fd, store, req, path, true); } static bool amduatd_handle_post_artifacts(int fd, @@ -905,6 +3527,8 @@ static bool amduatd_handle_post_artifacts(int fd, static bool amduatd_handle_post_pel_run(int fd, amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + const amduatd_concepts_t *concepts, const amduatd_http_req_t *req) { uint8_t *body = NULL; const char *p = NULL; @@ -914,12 +3538,32 @@ static bool amduatd_handle_post_pel_run(int fd, bool have_scheme_ref = false; bool scheme_is_dag = false; bool has_params_ref = false; + bool want_receipt = false; + bool receipt_have_input_manifest = false; + bool receipt_have_environment = false; + bool receipt_have_evaluator = false; + bool receipt_have_executor = false; + bool receipt_have_started = false; + bool receipt_have_completed = false; + bool receipt_has_sbom = false; + char *receipt_evaluator_id = NULL; + uint8_t *receipt_parity_digest = NULL; + size_t receipt_parity_digest_len = 0; + uint64_t receipt_started_at = 0; + uint64_t receipt_completed_at = 0; + amduat_reference_t receipt_input_manifest_ref; + amduat_reference_t receipt_environment_ref; + amduat_reference_t receipt_executor_ref; + amduat_reference_t receipt_sbom_ref; amduat_reference_t scheme_ref; amduat_reference_t program_ref; amduat_reference_t params_ref; amduat_reference_t *input_refs = NULL; size_t input_refs_len = 0; amduat_pel_run_result_t run_result; + amduat_artifact_t receipt_artifact; + amduat_reference_t receipt_ref; + bool receipt_emitted = false; int status_code = 200; const char *status_reason = "OK"; bool ok = false; @@ -928,11 +3572,21 @@ static bool amduatd_handle_post_pel_run(int fd, memset(&program_ref, 0, sizeof(program_ref)); memset(¶ms_ref, 0, sizeof(params_ref)); memset(&run_result, 0, sizeof(run_result)); + memset(&receipt_input_manifest_ref, 0, sizeof(receipt_input_manifest_ref)); + memset(&receipt_environment_ref, 0, sizeof(receipt_environment_ref)); + memset(&receipt_executor_ref, 0, sizeof(receipt_executor_ref)); + memset(&receipt_sbom_ref, 0, sizeof(receipt_sbom_ref)); + memset(&receipt_artifact, 0, sizeof(receipt_artifact)); + memset(&receipt_ref, 0, sizeof(receipt_ref)); if (store == NULL || req == NULL) { return amduatd_send_json_error(fd, 500, "Internal Server Error", "internal error"); } + if (cfg == NULL || concepts == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "internal error"); + } if (req->content_length > (1u * 1024u * 1024u)) { return amduatd_send_json_error(fd, 413, "Payload Too Large", @@ -987,13 +3641,25 @@ static bool amduatd_handle_post_pel_run(int fd, if (key_len == strlen("program_ref") && memcmp(key, "program_ref", key_len) == 0) { + amduatd_ref_status_t st; if (have_program_ref) { ok = amduatd_send_json_error(fd, 400, "Bad Request", "duplicate program_ref"); goto pel_run_cleanup; } - if (!amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len) || - !amduatd_decode_ref_hex_str(sv, sv_len, &program_ref)) { + if (!amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid program_ref"); + goto pel_run_cleanup; + } + st = amduatd_decode_ref_or_name_latest(store, cfg, concepts, sv, sv_len, + &program_ref); + if (st == AMDUATD_REF_ERR_NOT_FOUND) { + ok = amduatd_send_json_error(fd, 404, "Not Found", + "program_ref not found"); + goto pel_run_cleanup; + } + if (st != AMDUATD_REF_OK) { ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid program_ref"); goto pel_run_cleanup; @@ -1022,13 +3688,25 @@ static bool amduatd_handle_post_pel_run(int fd, have_scheme_ref = true; } else if (key_len == strlen("params_ref") && memcmp(key, "params_ref", key_len) == 0) { + amduatd_ref_status_t st; if (has_params_ref) { ok = amduatd_send_json_error(fd, 400, "Bad Request", "duplicate params_ref"); goto pel_run_cleanup; } - if (!amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len) || - !amduatd_decode_ref_hex_str(sv, sv_len, ¶ms_ref)) { + if (!amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid params_ref"); + goto pel_run_cleanup; + } + st = amduatd_decode_ref_or_name_latest(store, cfg, concepts, sv, sv_len, + ¶ms_ref); + if (st == AMDUATD_REF_ERR_NOT_FOUND) { + ok = amduatd_send_json_error(fd, 404, "Not Found", + "params_ref not found"); + goto pel_run_cleanup; + } + if (st != AMDUATD_REF_OK) { ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid params_ref"); goto pel_run_cleanup; @@ -1055,12 +3733,25 @@ static bool amduatd_handle_post_pel_run(int fd, for (;;) { amduat_reference_t ref; memset(&ref, 0, sizeof(ref)); - if (!amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len) || - !amduatd_decode_ref_hex_str(sv, sv_len, &ref)) { + if (!amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len)) { ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid input_ref"); goto pel_run_cleanup; } + { + amduatd_ref_status_t st = amduatd_decode_ref_or_name_latest( + store, cfg, concepts, sv, sv_len, &ref); + if (st == AMDUATD_REF_ERR_NOT_FOUND) { + ok = amduatd_send_json_error(fd, 404, "Not Found", + "input_ref not found"); + goto pel_run_cleanup; + } + if (st != AMDUATD_REF_OK) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid input_ref"); + goto pel_run_cleanup; + } + } if (input_refs_len == cap) { size_t next_cap = cap != 0 ? cap * 2u : 4u; amduat_reference_t *next; @@ -1103,6 +3794,201 @@ static bool amduatd_handle_post_pel_run(int fd, goto pel_run_cleanup; } } + } else if (key_len == strlen("receipt") && + memcmp(key, "receipt", key_len) == 0) { + const char *rkey = NULL; + size_t rkey_len = 0; + if (want_receipt) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "duplicate receipt"); + goto pel_run_cleanup; + } + if (!amduatd_json_expect(&p, end, '{')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid receipt"); + goto pel_run_cleanup; + } + want_receipt = true; + for (;;) { + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + if (!amduatd_json_parse_string_noesc(&p, end, &rkey, &rkey_len) || + !amduatd_json_expect(&p, end, ':')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid receipt"); + goto pel_run_cleanup; + } + if (rkey_len == strlen("input_manifest_ref") && + memcmp(rkey, "input_manifest_ref", rkey_len) == 0) { + amduatd_ref_status_t st; + if (receipt_have_input_manifest || + !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid input_manifest_ref"); + goto pel_run_cleanup; + } + st = amduatd_decode_ref_or_name_latest(store, cfg, concepts, sv, + sv_len, &receipt_input_manifest_ref); + if (st == AMDUATD_REF_ERR_NOT_FOUND) { + ok = amduatd_send_json_error(fd, 404, "Not Found", + "input_manifest_ref not found"); + goto pel_run_cleanup; + } + if (st != AMDUATD_REF_OK) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid input_manifest_ref"); + goto pel_run_cleanup; + } + receipt_have_input_manifest = true; + } else if (rkey_len == strlen("environment_ref") && + memcmp(rkey, "environment_ref", rkey_len) == 0) { + amduatd_ref_status_t st; + if (receipt_have_environment || + !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid environment_ref"); + goto pel_run_cleanup; + } + st = amduatd_decode_ref_or_name_latest(store, cfg, concepts, sv, + sv_len, &receipt_environment_ref); + if (st == AMDUATD_REF_ERR_NOT_FOUND) { + ok = amduatd_send_json_error(fd, 404, "Not Found", + "environment_ref not found"); + goto pel_run_cleanup; + } + if (st != AMDUATD_REF_OK) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid environment_ref"); + goto pel_run_cleanup; + } + receipt_have_environment = true; + } else if (rkey_len == strlen("evaluator_id") && + memcmp(rkey, "evaluator_id", rkey_len) == 0) { + char *tmp = NULL; + if (receipt_have_evaluator || + !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len) || + !amduatd_copy_json_str(sv, sv_len, &tmp)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid evaluator_id"); + free(tmp); + goto pel_run_cleanup; + } + free(receipt_evaluator_id); + receipt_evaluator_id = tmp; + receipt_have_evaluator = true; + } else if (rkey_len == strlen("executor_ref") && + memcmp(rkey, "executor_ref", rkey_len) == 0) { + amduatd_ref_status_t st; + if (receipt_have_executor || + !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid executor_ref"); + goto pel_run_cleanup; + } + st = amduatd_decode_ref_or_name_latest(store, cfg, concepts, sv, + sv_len, &receipt_executor_ref); + if (st == AMDUATD_REF_ERR_NOT_FOUND) { + ok = amduatd_send_json_error(fd, 404, "Not Found", + "executor_ref not found"); + goto pel_run_cleanup; + } + if (st != AMDUATD_REF_OK) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid executor_ref"); + goto pel_run_cleanup; + } + receipt_have_executor = true; + } else if (rkey_len == strlen("sbom_ref") && + memcmp(rkey, "sbom_ref", rkey_len) == 0) { + amduatd_ref_status_t st; + if (receipt_has_sbom || + !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid sbom_ref"); + goto pel_run_cleanup; + } + st = amduatd_decode_ref_or_name_latest(store, cfg, concepts, sv, + sv_len, &receipt_sbom_ref); + if (st == AMDUATD_REF_ERR_NOT_FOUND) { + ok = amduatd_send_json_error(fd, 404, "Not Found", + "sbom_ref not found"); + goto pel_run_cleanup; + } + if (st != AMDUATD_REF_OK) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid sbom_ref"); + goto pel_run_cleanup; + } + receipt_has_sbom = true; + } else if (rkey_len == strlen("parity_digest_hex") && + memcmp(rkey, "parity_digest_hex", rkey_len) == 0) { + char *tmp = NULL; + uint8_t *bytes = NULL; + size_t bytes_len = 0; + if (!amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len) || + !amduatd_copy_json_str(sv, sv_len, &tmp)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid parity_digest_hex"); + free(tmp); + goto pel_run_cleanup; + } + if (!amduat_hex_decode_alloc(tmp, &bytes, &bytes_len)) { + free(tmp); + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid parity_digest_hex"); + goto pel_run_cleanup; + } + free(tmp); + free(receipt_parity_digest); + receipt_parity_digest = bytes; + receipt_parity_digest_len = bytes_len; + } else if (rkey_len == strlen("started_at") && + memcmp(rkey, "started_at", rkey_len) == 0) { + if (receipt_have_started || + !amduatd_json_parse_u64(&p, end, &receipt_started_at)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid started_at"); + goto pel_run_cleanup; + } + receipt_have_started = true; + } else if (rkey_len == strlen("completed_at") && + memcmp(rkey, "completed_at", rkey_len) == 0) { + if (receipt_have_completed || + !amduatd_json_parse_u64(&p, end, &receipt_completed_at)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid completed_at"); + goto pel_run_cleanup; + } + receipt_have_completed = true; + } else { + if (!amduatd_json_skip_value(&p, end, 0)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid receipt"); + goto pel_run_cleanup; + } + } + + cur = amduatd_json_skip_ws(p, end); + if (cur >= end) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid receipt"); + goto pel_run_cleanup; + } + if (*cur == ',') { + p = cur + 1; + continue; + } + if (*cur == '}') { + p = cur + 1; + break; + } + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid receipt"); + goto pel_run_cleanup; + } } else { if (!amduatd_json_skip_value(&p, end, 0)) { ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); @@ -1149,6 +4035,14 @@ static bool amduatd_handle_post_pel_run(int fd, scheme_ref = amduat_pel_program_dag_scheme_ref(); scheme_is_dag = true; } + if (want_receipt && + (!receipt_have_input_manifest || !receipt_have_environment || + !receipt_have_evaluator || !receipt_have_executor || + !receipt_have_started || !receipt_have_completed)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "missing receipt fields"); + goto pel_run_cleanup; + } if (!amduat_pel_surf_run_with_result(store, scheme_ref, @@ -1174,10 +4068,58 @@ static bool amduatd_handle_post_pel_run(int fd, } } + if (want_receipt) { + amduat_octets_t evaluator_id; + amduat_octets_t parity_digest; + + if (!run_result.has_result_value) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "receipt unavailable"); + goto pel_run_cleanup; + } + if (run_result.result_value.output_refs_len != 1) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "receipt requires single output"); + goto pel_run_cleanup; + } + evaluator_id = amduat_octets(receipt_evaluator_id, + receipt_evaluator_id != NULL + ? strlen(receipt_evaluator_id) + : 0u); + parity_digest = amduat_octets(receipt_parity_digest, + receipt_parity_digest_len); + if (!amduat_fer1_receipt_from_pel_result( + &run_result.result_value, + receipt_input_manifest_ref, + receipt_environment_ref, + evaluator_id, + receipt_executor_ref, + receipt_has_sbom, + receipt_sbom_ref, + parity_digest, + receipt_started_at, + receipt_completed_at, + &receipt_artifact)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "receipt build failed"); + goto pel_run_cleanup; + } + if (amduat_asl_store_put(store, receipt_artifact, &receipt_ref) != + AMDUAT_ASL_STORE_OK) { + amduat_asl_artifact_free(&receipt_artifact); + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "receipt store failed"); + goto pel_run_cleanup; + } + amduat_asl_artifact_free(&receipt_artifact); + receipt_emitted = true; + } + { amduatd_strbuf_t resp; char *result_hex = NULL; char *trace_hex = NULL; + char *receipt_hex = NULL; const char *status = "UNKNOWN"; size_t i; @@ -1228,6 +4170,24 @@ static bool amduatd_handle_post_pel_run(int fd, free(trace_hex); } + if (receipt_emitted) { + if (!amduat_asl_ref_encode_hex(receipt_ref, &receipt_hex)) { + amduatd_strbuf_free(&resp); + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + goto pel_run_cleanup; + } + if (!amduatd_strbuf_append_cstr(&resp, ",\"receipt_ref\":\"") || + !amduatd_strbuf_append_cstr(&resp, receipt_hex) || + !amduatd_strbuf_append_cstr(&resp, "\"")) { + free(receipt_hex); + amduatd_strbuf_free(&resp); + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + goto pel_run_cleanup; + } + free(receipt_hex); + } + if (!amduatd_strbuf_append_cstr(&resp, ",\"output_refs\":[")) { amduatd_strbuf_free(&resp); ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); @@ -1301,12 +4261,1721 @@ pel_run_cleanup: if (have_scheme_ref && !scheme_is_dag) { amduat_reference_free(&scheme_ref); } + if (receipt_emitted) { + amduat_reference_free(&receipt_ref); + } + if (receipt_have_input_manifest) { + amduat_reference_free(&receipt_input_manifest_ref); + } + if (receipt_have_environment) { + amduat_reference_free(&receipt_environment_ref); + } + if (receipt_have_executor) { + amduat_reference_free(&receipt_executor_ref); + } + if (receipt_has_sbom) { + amduat_reference_free(&receipt_sbom_ref); + } + free(receipt_evaluator_id); + free(receipt_parity_digest); return ok; } +typedef struct { + amduat_pel_node_t *nodes; + size_t nodes_len; + amduat_pel_root_ref_t *roots; + size_t roots_len; +} amduatd_pel_program_tmp_t; + +static void amduatd_pel_program_tmp_free(amduatd_pel_program_tmp_t *tmp) { + size_t i; + + if (tmp == NULL) { + return; + } + if (tmp->nodes != NULL) { + for (i = 0; i < tmp->nodes_len; ++i) { + free(tmp->nodes[i].inputs); + tmp->nodes[i].inputs = NULL; + tmp->nodes[i].inputs_len = 0; + free((void *)tmp->nodes[i].params.data); + tmp->nodes[i].params.data = NULL; + tmp->nodes[i].params.len = 0; + } + } + free(tmp->nodes); + tmp->nodes = NULL; + tmp->nodes_len = 0; + free(tmp->roots); + tmp->roots = NULL; + tmp->roots_len = 0; +} + +static bool amduatd_parse_params_hex(const char *s, + size_t len, + amduat_octets_t *out) { + char *tmp = NULL; + uint8_t *bytes = NULL; + size_t out_len = 0; + + if (out != NULL) { + *out = amduat_octets(NULL, 0); + } + if (s == NULL || out == NULL) { + return false; + } + if (!amduatd_copy_json_str(s, len, &tmp)) { + return false; + } + if (tmp[0] == '\0') { + free(tmp); + *out = amduat_octets(NULL, 0); + return true; + } + if (!amduat_hex_decode_alloc(tmp, &bytes, &out_len)) { + free(tmp); + return false; + } + free(tmp); + *out = amduat_octets(bytes, out_len); + return true; +} + +static bool amduatd_handle_post_pel_programs(int fd, + amduat_asl_store_t *store, + const amduatd_http_req_t *req) { + uint8_t *body = NULL; + const char *p = NULL; + const char *end = NULL; + amduatd_pel_program_tmp_t tmp; + amduat_pel_program_t program; + amduat_octets_t program_bytes; + amduat_artifact_t artifact; + amduat_reference_t program_ref; + char *ref_hex = NULL; + char json[2048]; + bool ok = false; + + memset(&tmp, 0, sizeof(tmp)); + memset(&program, 0, sizeof(program)); + program_bytes = amduat_octets(NULL, 0); + memset(&artifact, 0, sizeof(artifact)); + memset(&program_ref, 0, sizeof(program_ref)); + + if (store == NULL || req == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "internal error"); + } + if (req->content_length == 0) { + return amduatd_send_json_error(fd, 400, "Bad Request", "missing body"); + } + if (req->content_length > (2u * 1024u * 1024u)) { + return amduatd_send_json_error(fd, 413, "Payload Too Large", + "payload too large"); + } + + body = (uint8_t *)malloc(req->content_length); + if (body == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + } + if (!amduatd_read_exact(fd, body, req->content_length)) { + free(body); + return false; + } + + p = (const char *)body; + end = (const char *)body + req->content_length; + if (!amduatd_json_expect(&p, end, '{')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto pel_programs_cleanup; + } + + { + bool have_nodes = false; + bool have_roots = false; + + for (;;) { + const char *key = NULL; + size_t key_len = 0; + const char *cur = NULL; + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + + if (!amduatd_json_parse_string_noesc(&p, end, &key, &key_len) || + !amduatd_json_expect(&p, end, ':')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto pel_programs_cleanup; + } + + if (key_len == strlen("nodes") && memcmp(key, "nodes", key_len) == 0) { + size_t cap = 0; + if (have_nodes) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "duplicate nodes"); + goto pel_programs_cleanup; + } + if (!amduatd_json_expect(&p, end, '[')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid nodes"); + goto pel_programs_cleanup; + } + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ']') { + p = cur + 1; + have_nodes = true; + } else { + for (;;) { + amduat_pel_node_t node; + bool have_id = false; + bool have_op = false; + bool have_inputs = false; + bool have_params_hex = false; + amduat_octets_t op_name = amduat_octets(NULL, 0); + uint32_t op_ver = 0; + + memset(&node, 0, sizeof(node)); + + if (!amduatd_json_expect(&p, end, '{')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid node"); + goto pel_programs_cleanup; + } + for (;;) { + const char *nk = NULL; + size_t nk_len = 0; + const char *sv = NULL; + size_t sv_len = 0; + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + if (!amduatd_json_parse_string_noesc(&p, end, &nk, &nk_len) || + !amduatd_json_expect(&p, end, ':')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid node"); + goto pel_programs_cleanup; + } + + if (nk_len == strlen("id") && memcmp(nk, "id", nk_len) == 0) { + uint32_t id = 0; + if (have_id) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "duplicate node id"); + goto pel_programs_cleanup; + } + if (!amduatd_json_parse_u32_loose(&p, end, &id)) { + char near[32]; + char msg[96]; + int n; + amduatd_json_peek_token(p, end, near, sizeof(near)); + n = snprintf(msg, + sizeof(msg), + "invalid node id near '%s'", + near[0] != '\0' ? near : "?"); + if (n > 0 && (size_t)n < sizeof(msg)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", msg); + } else { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid node id"); + } + goto pel_programs_cleanup; + } + node.id = (amduat_pel_node_id_t)id; + have_id = true; + } else if (nk_len == strlen("op") && + memcmp(nk, "op", nk_len) == 0) { + bool have_name = false; + bool have_version = false; + if (have_op || !amduatd_json_expect(&p, end, '{')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid op"); + goto pel_programs_cleanup; + } + for (;;) { + const char *okey = NULL; + size_t okey_len = 0; + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + if (!amduatd_json_parse_string_noesc(&p, end, &okey, + &okey_len) || + !amduatd_json_expect(&p, end, ':')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid op"); + goto pel_programs_cleanup; + } + if (okey_len == strlen("name") && + memcmp(okey, "name", okey_len) == 0) { + if (have_name || + !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid op name"); + goto pel_programs_cleanup; + } + op_name = amduat_octets(sv, sv_len); + have_name = true; + } else if (okey_len == strlen("version") && + memcmp(okey, "version", okey_len) == 0) { + if (have_version || + !amduatd_json_parse_u32_loose(&p, end, &op_ver)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid op version"); + goto pel_programs_cleanup; + } + have_version = true; + } else { + if (!amduatd_json_skip_value(&p, end, 0)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid op"); + goto pel_programs_cleanup; + } + } + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid op"); + goto pel_programs_cleanup; + } + if (!have_name || !have_version) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "missing op fields"); + goto pel_programs_cleanup; + } + node.op.name = op_name; + node.op.version = op_ver; + have_op = true; + } else if (nk_len == strlen("inputs") && + memcmp(nk, "inputs", nk_len) == 0) { + size_t icap = 0; + if (have_inputs || !amduatd_json_expect(&p, end, '[')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid inputs"); + goto pel_programs_cleanup; + } + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ']') { + p = cur + 1; + have_inputs = true; + } else { + for (;;) { + amduat_pel_dag_input_t in; + memset(&in, 0, sizeof(in)); + if (!amduatd_json_expect(&p, end, '{')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid input"); + goto pel_programs_cleanup; + } + { + const char *ik = NULL; + size_t ik_len = 0; + if (!amduatd_json_parse_string_noesc(&p, end, &ik, + &ik_len) || + !amduatd_json_expect(&p, end, ':') || + !amduatd_json_expect(&p, end, '{')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid input"); + goto pel_programs_cleanup; + } + if (ik_len == strlen("external") && + memcmp(ik, "external", ik_len) == 0) { + uint32_t idx = 0; + const char *k2 = NULL; + size_t k2_len = 0; + if (!amduatd_json_parse_string_noesc(&p, end, &k2, + &k2_len) || + k2_len != strlen("input_index") || + memcmp(k2, "input_index", k2_len) != 0 || + !amduatd_json_expect(&p, end, ':') || + !amduatd_json_parse_u32_loose(&p, end, &idx) || + !amduatd_json_expect(&p, end, '}')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid external input"); + goto pel_programs_cleanup; + } + in.kind = AMDUAT_PEL_DAG_INPUT_EXTERNAL; + in.value.external.input_index = idx; + } else if (ik_len == strlen("node") && + memcmp(ik, "node", ik_len) == 0) { + bool have_node_id = false; + bool have_output_index = false; + uint32_t nid = 0; + uint32_t oidx = 0; + for (;;) { + const char *k2 = NULL; + size_t k2_len = 0; + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + if (!amduatd_json_parse_string_noesc(&p, end, &k2, + &k2_len) || + !amduatd_json_expect(&p, end, ':')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid node input"); + goto pel_programs_cleanup; + } + if (k2_len == strlen("node_id") && + memcmp(k2, "node_id", k2_len) == 0) { + if (have_node_id || + !amduatd_json_parse_u32_loose(&p, end, &nid)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid node_id"); + goto pel_programs_cleanup; + } + have_node_id = true; + } else if (k2_len == strlen("output_index") && + memcmp(k2, "output_index", k2_len) == 0) { + if (have_output_index || + !amduatd_json_parse_u32_loose(&p, end, &oidx)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid output_index"); + goto pel_programs_cleanup; + } + have_output_index = true; + } else { + if (!amduatd_json_skip_value(&p, end, 0)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid node input"); + goto pel_programs_cleanup; + } + } + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid node input"); + goto pel_programs_cleanup; + } + if (!have_node_id || !have_output_index) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "missing node input fields"); + goto pel_programs_cleanup; + } + in.kind = AMDUAT_PEL_DAG_INPUT_NODE; + in.value.node.node_id = (amduat_pel_node_id_t)nid; + in.value.node.output_index = oidx; + } else { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid input kind"); + goto pel_programs_cleanup; + } + if (!amduatd_json_expect(&p, end, '}')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid input"); + goto pel_programs_cleanup; + } + } + + if (node.inputs_len == icap) { + size_t next_cap = icap != 0 ? icap * 2u : 4u; + amduat_pel_dag_input_t *next = + (amduat_pel_dag_input_t *)realloc( + node.inputs, next_cap * sizeof(*node.inputs)); + if (next == NULL) { + ok = amduatd_send_json_error(fd, 500, + "Internal Server Error", + "oom"); + goto pel_programs_cleanup; + } + node.inputs = next; + icap = next_cap; + } + node.inputs[node.inputs_len++] = in; + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == ']') { + p = cur + 1; + have_inputs = true; + break; + } + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid inputs"); + goto pel_programs_cleanup; + } + } + } else if (nk_len == strlen("params_hex") && + memcmp(nk, "params_hex", nk_len) == 0) { + if (have_params_hex || + !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len) || + !amduatd_parse_params_hex(sv, sv_len, &node.params)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid params_hex"); + goto pel_programs_cleanup; + } + have_params_hex = true; + } else { + if (!amduatd_json_skip_value(&p, end, 0)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid node"); + goto pel_programs_cleanup; + } + } + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + } + + if (!have_id || !have_op || !have_inputs || !have_params_hex) { + free(node.inputs); + free((void *)node.params.data); + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "missing node fields"); + goto pel_programs_cleanup; + } + if (amduat_pel_kernel_op_lookup(node.op.name, node.op.version) == + NULL) { + free(node.inputs); + free((void *)node.params.data); + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "unknown op"); + goto pel_programs_cleanup; + } + + if (tmp.nodes_len == cap) { + size_t next_cap = cap != 0 ? cap * 2u : 4u; + amduat_pel_node_t *next = + (amduat_pel_node_t *)realloc(tmp.nodes, + next_cap * sizeof(*tmp.nodes)); + if (next == NULL) { + free(node.inputs); + free((void *)node.params.data); + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "oom"); + goto pel_programs_cleanup; + } + tmp.nodes = next; + cap = next_cap; + } + tmp.nodes[tmp.nodes_len++] = node; + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == ']') { + p = cur + 1; + have_nodes = true; + break; + } + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid nodes"); + goto pel_programs_cleanup; + } + } + } else if (key_len == strlen("roots") && + memcmp(key, "roots", key_len) == 0) { + size_t cap = 0; + if (have_roots) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "duplicate roots"); + goto pel_programs_cleanup; + } + if (!amduatd_json_expect(&p, end, '[')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid roots"); + goto pel_programs_cleanup; + } + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ']') { + p = cur + 1; + have_roots = true; + } else { + for (;;) { + amduat_pel_root_ref_t root; + bool have_node_id = false; + bool have_output_index = false; + + memset(&root, 0, sizeof(root)); + if (!amduatd_json_expect(&p, end, '{')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid root"); + goto pel_programs_cleanup; + } + for (;;) { + const char *rk = NULL; + size_t rk_len = 0; + uint32_t v = 0; + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + if (!amduatd_json_parse_string_noesc(&p, end, &rk, &rk_len) || + !amduatd_json_expect(&p, end, ':')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid root"); + goto pel_programs_cleanup; + } + if (rk_len == strlen("node_id") && + memcmp(rk, "node_id", rk_len) == 0) { + if (have_node_id || + !amduatd_json_parse_u32_loose(&p, end, &v)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid root node_id"); + goto pel_programs_cleanup; + } + root.node_id = (amduat_pel_node_id_t)v; + have_node_id = true; + } else if (rk_len == strlen("output_index") && + memcmp(rk, "output_index", rk_len) == 0) { + if (have_output_index || + !amduatd_json_parse_u32_loose(&p, end, &v)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid root output_index"); + goto pel_programs_cleanup; + } + root.output_index = v; + have_output_index = true; + } else { + if (!amduatd_json_skip_value(&p, end, 0)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid root"); + goto pel_programs_cleanup; + } + } + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid root"); + goto pel_programs_cleanup; + } + if (!have_node_id || !have_output_index) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "missing root fields"); + goto pel_programs_cleanup; + } + if (tmp.roots_len == cap) { + size_t next_cap = cap != 0 ? cap * 2u : 2u; + amduat_pel_root_ref_t *next = + (amduat_pel_root_ref_t *)realloc( + tmp.roots, next_cap * sizeof(*tmp.roots)); + if (next == NULL) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "oom"); + goto pel_programs_cleanup; + } + tmp.roots = next; + cap = next_cap; + } + tmp.roots[tmp.roots_len++] = root; + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == ']') { + p = cur + 1; + have_roots = true; + break; + } + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid roots"); + goto pel_programs_cleanup; + } + } + } else { + if (!amduatd_json_skip_value(&p, end, 0)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto pel_programs_cleanup; + } + } + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto pel_programs_cleanup; + } + + p = amduatd_json_skip_ws(p, end); + if (p != end) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto pel_programs_cleanup; + } + if (!have_nodes || !have_roots) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "missing nodes/roots"); + goto pel_programs_cleanup; + } + } + + program.nodes = tmp.nodes; + program.nodes_len = tmp.nodes_len; + program.roots = tmp.roots; + program.roots_len = tmp.roots_len; + if (!amduat_pel_program_dag_validate(&program)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid program"); + goto pel_programs_cleanup; + } + + if (!amduat_enc_pel_program_dag_encode_v1(&program, &program_bytes)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + goto pel_programs_cleanup; + } + + artifact = amduat_artifact_with_type(program_bytes, + amduat_type_tag(AMDUAT_PEL_TYPE_TAG_PROGRAM_DAG_1)); + if (amduat_asl_store_put(store, artifact, &program_ref) != AMDUAT_ASL_STORE_OK) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "store error"); + goto pel_programs_cleanup; + } + + if (!amduat_asl_ref_encode_hex(program_ref, &ref_hex)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode ref error"); + goto pel_programs_cleanup; + } + { + int n = snprintf(json, sizeof(json), "{\"program_ref\":\"%s\"}\n", ref_hex); + free(ref_hex); + if (n <= 0 || (size_t)n >= sizeof(json)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "error"); + goto pel_programs_cleanup; + } + ok = amduatd_http_send_json(fd, 200, "OK", json, false); + goto pel_programs_cleanup; + } + +pel_programs_cleanup: + free(body); + free((void *)program_bytes.data); + amduat_reference_free(&program_ref); + amduatd_pel_program_tmp_free(&tmp); + return ok; +} + +static bool amduatd_handle_get_ui(int fd, + amduat_asl_store_t *store, + amduat_reference_t ui_ref) { + amduat_artifact_t artifact; + amduat_asl_store_error_t err; + + if (store == NULL || ui_ref.hash_id == 0 || ui_ref.digest.data == NULL || + ui_ref.digest.len == 0) { + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "ui not available\n", false); + } + memset(&artifact, 0, sizeof(artifact)); + err = amduat_asl_store_get(store, ui_ref, &artifact); + if (err == AMDUAT_ASL_STORE_ERR_NOT_FOUND) { + return amduatd_http_send_text(fd, 404, "Not Found", "not found\n", false); + } + if (err != AMDUAT_ASL_STORE_OK) { + amduat_asl_artifact_free(&artifact); + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "store error\n", false); + } + if (artifact.bytes.len != 0 && artifact.bytes.data == NULL) { + amduat_asl_artifact_free(&artifact); + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "store error\n", false); + } + { + bool ok = amduatd_http_send_status(fd, + 200, + "OK", + "text/html; charset=utf-8", + artifact.bytes.data, + artifact.bytes.len, + false); + amduat_asl_artifact_free(&artifact); + return ok; + } +} + +static bool amduatd_handle_post_concepts(int fd, + amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + amduatd_concepts_t *concepts, + const amduatd_http_req_t *req) { + uint8_t *body = NULL; + const char *p = NULL; + const char *end = NULL; + char *name = NULL; + bool have_name = false; + bool have_ref = false; + amduat_reference_t target_ref; + amduat_reference_t name_ref; + amduat_reference_t concept_ref; + amduat_reference_t edge_ref; + char *concept_hex = NULL; + char json[4096]; + bool ok = false; + + memset(&target_ref, 0, sizeof(target_ref)); + memset(&name_ref, 0, sizeof(name_ref)); + memset(&concept_ref, 0, sizeof(concept_ref)); + memset(&edge_ref, 0, sizeof(edge_ref)); + + if (store == NULL || cfg == NULL || concepts == NULL || req == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "internal error"); + } + if (req->content_length == 0) { + return amduatd_send_json_error(fd, 400, "Bad Request", "missing body"); + } + if (req->content_length > (256u * 1024u)) { + return amduatd_send_json_error(fd, 413, "Payload Too Large", + "payload too large"); + } + body = (uint8_t *)malloc(req->content_length); + if (body == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + } + if (!amduatd_read_exact(fd, body, req->content_length)) { + free(body); + return false; + } + p = (const char *)body; + end = (const char *)body + req->content_length; + if (!amduatd_json_expect(&p, end, '{')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto concepts_cleanup; + } + + for (;;) { + const char *key = NULL; + size_t key_len = 0; + const char *sv = NULL; + size_t sv_len = 0; + const char *cur = NULL; + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + if (!amduatd_json_parse_string_noesc(&p, end, &key, &key_len) || + !amduatd_json_expect(&p, end, ':')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto concepts_cleanup; + } + + if (key_len == strlen("name") && memcmp(key, "name", key_len) == 0) { + if (have_name || !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len) || + !amduatd_copy_json_str(sv, sv_len, &name)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid name"); + goto concepts_cleanup; + } + if (!amduatd_name_valid(name)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid name"); + goto concepts_cleanup; + } + have_name = true; + } else if (key_len == strlen("ref") && memcmp(key, "ref", key_len) == 0) { + if (have_ref || !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len) || + !amduatd_decode_ref_hex_str(sv, sv_len, &target_ref)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid ref"); + goto concepts_cleanup; + } + have_ref = true; + } else { + if (!amduatd_json_skip_value(&p, end, 0)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto concepts_cleanup; + } + } + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto concepts_cleanup; + } + + if (!have_name) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "missing name"); + goto concepts_cleanup; + } + + if (!amduatd_concepts_put_name_artifact(store, name, &name_ref)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "store error"); + goto concepts_cleanup; + } + + { + amduat_reference_t existing; + memset(&existing, 0, sizeof(existing)); + if (amduatd_concepts_lookup_alias(store, cfg, concepts, name, &existing)) { + amduat_reference_free(&existing); + ok = amduatd_send_json_error(fd, 409, "Conflict", "name exists"); + goto concepts_cleanup; + } + } + + if (!amduatd_concepts_put_concept_id(store, cfg, &concept_ref)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "store error"); + goto concepts_cleanup; + } + + if (!amduatd_concepts_put_edge(store, + concepts, + name_ref, + concept_ref, + concepts->rel_aliases_ref, + &edge_ref)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "store error"); + goto concepts_cleanup; + } + + if (have_ref) { + if (!amduatd_concepts_put_edge(store, + concepts, + concept_ref, + target_ref, + concepts->rel_materializes_ref, + &edge_ref)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "store error"); + goto concepts_cleanup; + } + } + + if (!amduat_asl_ref_encode_hex(concept_ref, &concept_hex)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + goto concepts_cleanup; + } + + { + int n = snprintf(json, + sizeof(json), + "{" + "\"name\":\"%s\"," + "\"concept_ref\":\"%s\"" + "}\n", + name, + concept_hex); + free(concept_hex); + if (n <= 0 || (size_t)n >= sizeof(json)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "error"); + goto concepts_cleanup; + } + ok = amduatd_http_send_json(fd, 200, "OK", json, false); + } + +concepts_cleanup: + free(body); + free(name); + amduat_reference_free(&target_ref); + amduat_reference_free(&name_ref); + amduat_reference_free(&concept_ref); + amduat_reference_free(&edge_ref); + return ok; +} + +typedef struct { + char *key_hex; + char *value_hex; + uint64_t value_int; + int value_kind; +} amduatd_cf_binding_t; + +static void amduatd_cf_binding_free(amduatd_cf_binding_t *b) { + if (b == NULL) { + return; + } + free(b->key_hex); + free(b->value_hex); + b->key_hex = NULL; + b->value_hex = NULL; + b->value_int = 0; + b->value_kind = 0; +} + +static int amduatd_cf_binding_cmp(const void *a, const void *b) { + const amduatd_cf_binding_t *x = (const amduatd_cf_binding_t *)a; + const amduatd_cf_binding_t *y = (const amduatd_cf_binding_t *)b; + int c; + if (x == NULL || y == NULL) { + return 0; + } + c = strcmp(x->key_hex != NULL ? x->key_hex : "", + y->key_hex != NULL ? y->key_hex : ""); + if (c != 0) { + return c; + } + if (x->value_kind != y->value_kind) { + return x->value_kind < y->value_kind ? -1 : 1; + } + if (x->value_kind == 1) { + if (x->value_int < y->value_int) { + return -1; + } + if (x->value_int > y->value_int) { + return 1; + } + return 0; + } + return strcmp(x->value_hex != NULL ? x->value_hex : "", + y->value_hex != NULL ? y->value_hex : ""); +} + +static bool amduatd_handle_post_context_frames( + int fd, + amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + const amduatd_concepts_t *concepts, + const amduatd_http_req_t *req) { + uint8_t *body = NULL; + const char *p = NULL; + const char *end = NULL; + amduatd_cf_binding_t *bindings = NULL; + size_t bindings_len = 0; + size_t bindings_cap = 0; + amduatd_strbuf_t b; + amduat_artifact_t artifact; + amduat_reference_t ref; + char *ref_hex = NULL; + bool ok = false; + size_t i; + + memset(&b, 0, sizeof(b)); + memset(&artifact, 0, sizeof(artifact)); + memset(&ref, 0, sizeof(ref)); + + if (store == NULL || cfg == NULL || concepts == NULL || req == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "internal error"); + } + if (req->content_length == 0) { + return amduatd_send_json_error(fd, 400, "Bad Request", "missing body"); + } + if (req->content_length > (256u * 1024u)) { + return amduatd_send_json_error(fd, 413, "Payload Too Large", + "payload too large"); + } + + body = (uint8_t *)malloc(req->content_length); + if (body == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + } + if (!amduatd_read_exact(fd, body, req->content_length)) { + free(body); + return false; + } + + p = (const char *)body; + end = (const char *)body + req->content_length; + if (!amduatd_json_expect(&p, end, '{')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto cf_cleanup; + } + + for (;;) { + const char *key = NULL; + size_t key_len = 0; + const char *cur = NULL; + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + if (!amduatd_json_parse_string_noesc(&p, end, &key, &key_len) || + !amduatd_json_expect(&p, end, ':')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto cf_cleanup; + } + + if (key_len == strlen("bindings") && memcmp(key, "bindings", key_len) == 0) { + if (!amduatd_json_expect(&p, end, '[')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid bindings"); + goto cf_cleanup; + } + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ']') { + p = cur + 1; + } else { + for (;;) { + const char *bk = NULL; + size_t bk_len = 0; + const char *sv = NULL; + size_t sv_len = 0; + char *key_text = NULL; + char *value_text = NULL; + char *value_ref_text = NULL; + char *value_enum_text = NULL; + amduat_reference_t key_ref; + amduat_reference_t value_ref; + amduat_reference_t enum_ref; + bool have_key = false; + bool have_value = false; + bool have_value_ref = false; + bool have_value_scalar = false; + bool have_value_int = false; + bool have_value_enum = false; + uint64_t value_int = 0; + + memset(&key_ref, 0, sizeof(key_ref)); + memset(&value_ref, 0, sizeof(value_ref)); + memset(&enum_ref, 0, sizeof(enum_ref)); + + if (!amduatd_json_expect(&p, end, '{')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid binding"); + goto cf_cleanup; + } + for (;;) { + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + if (!amduatd_json_parse_string_noesc(&p, end, &bk, &bk_len) || + !amduatd_json_expect(&p, end, ':')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid binding"); + goto cf_cleanup; + } + if (bk_len == strlen("key") && memcmp(bk, "key", bk_len) == 0) { + if (have_key || + !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len) || + !amduatd_copy_json_str(sv, sv_len, &key_text)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid key"); + goto cf_cleanup; + } + have_key = true; + } else if (bk_len == strlen("value") && + memcmp(bk, "value", bk_len) == 0) { + if (have_value || have_value_ref || have_value_scalar || + !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len) || + !amduatd_copy_json_str(sv, sv_len, &value_text)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid value"); + goto cf_cleanup; + } + have_value = true; + } else if (bk_len == strlen("value_ref") && + memcmp(bk, "value_ref", bk_len) == 0) { + if (have_value || have_value_ref || have_value_scalar || + !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len) || + !amduatd_copy_json_str(sv, sv_len, &value_ref_text)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid value_ref"); + goto cf_cleanup; + } + have_value_ref = true; + } else if (bk_len == strlen("value_scalar") && + memcmp(bk, "value_scalar", bk_len) == 0) { + if (have_value || have_value_ref || have_value_scalar || + !amduatd_json_expect(&p, end, '{')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid value_scalar"); + goto cf_cleanup; + } + have_value_scalar = true; + for (;;) { + const char *sk = NULL; + size_t sk_len = 0; + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + if (!amduatd_json_parse_string_noesc(&p, end, &sk, &sk_len) || + !amduatd_json_expect(&p, end, ':')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid value_scalar"); + goto cf_cleanup; + } + if (sk_len == strlen("int") && memcmp(sk, "int", sk_len) == 0) { + if (have_value_int || have_value_enum || + !amduatd_json_parse_u64(&p, end, &value_int)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid int"); + goto cf_cleanup; + } + have_value_int = true; + } else if (sk_len == strlen("enum") && + memcmp(sk, "enum", sk_len) == 0) { + if (have_value_int || have_value_enum || + !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len) || + !amduatd_copy_json_str(sv, sv_len, &value_enum_text)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid enum"); + goto cf_cleanup; + } + have_value_enum = true; + } else { + if (!amduatd_json_skip_value(&p, end, 0)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid value_scalar"); + goto cf_cleanup; + } + } + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + } + } else { + if (!amduatd_json_skip_value(&p, end, 0)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid binding"); + goto cf_cleanup; + } + } + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + } + if (!have_key || + (!have_value && !have_value_ref && !have_value_scalar)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "missing binding fields"); + free(key_text); + free(value_text); + free(value_ref_text); + free(value_enum_text); + goto cf_cleanup; + } + if (amduatd_decode_ref_or_name_latest(store, + cfg, + concepts, + key_text, + strlen(key_text), + &key_ref) != AMDUATD_REF_OK) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid key"); + free(key_text); + free(value_text); + free(value_ref_text); + free(value_enum_text); + goto cf_cleanup; + } + if (have_value || have_value_ref) { + const char *vtext = have_value ? value_text : value_ref_text; + if (amduatd_decode_ref_or_name_latest(store, + cfg, + concepts, + vtext, + strlen(vtext), + &value_ref) != AMDUATD_REF_OK) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid value"); + free(key_text); + free(value_text); + free(value_ref_text); + free(value_enum_text); + amduat_reference_free(&key_ref); + goto cf_cleanup; + } + } else if (have_value_scalar) { + if (have_value_enum) { + if (amduatd_decode_ref_or_name_latest(store, + cfg, + concepts, + value_enum_text, + strlen(value_enum_text), + &enum_ref) != AMDUATD_REF_OK) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid enum"); + free(key_text); + free(value_text); + free(value_ref_text); + free(value_enum_text); + amduat_reference_free(&key_ref); + goto cf_cleanup; + } + } else if (!have_value_int) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid value_scalar"); + free(key_text); + free(value_text); + free(value_ref_text); + free(value_enum_text); + amduat_reference_free(&key_ref); + goto cf_cleanup; + } + } + + if (bindings_len == bindings_cap) { + size_t next_cap = bindings_cap != 0 ? bindings_cap * 2u : 4u; + amduatd_cf_binding_t *next = + (amduatd_cf_binding_t *)realloc(bindings, + next_cap * sizeof(*bindings)); + if (next == NULL) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "oom"); + free(key_text); + free(value_text); + amduat_reference_free(&key_ref); + amduat_reference_free(&value_ref); + goto cf_cleanup; + } + bindings = next; + bindings_cap = next_cap; + } + + { + char *key_hex = NULL; + char *value_hex = NULL; + if (!amduat_asl_ref_encode_hex(key_ref, &key_hex)) { + free(key_hex); + free(value_hex); + free(key_text); + free(value_text); + free(value_ref_text); + free(value_enum_text); + amduat_reference_free(&key_ref); + amduat_reference_free(&value_ref); + amduat_reference_free(&enum_ref); + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + goto cf_cleanup; + } + if (have_value || have_value_ref) { + if (!amduat_asl_ref_encode_hex(value_ref, &value_hex)) { + free(key_hex); + free(value_hex); + free(key_text); + free(value_text); + free(value_ref_text); + free(value_enum_text); + amduat_reference_free(&key_ref); + amduat_reference_free(&value_ref); + amduat_reference_free(&enum_ref); + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + goto cf_cleanup; + } + bindings[bindings_len].value_kind = 0; + bindings[bindings_len].value_hex = value_hex; + } else if (have_value_int) { + bindings[bindings_len].value_kind = 1; + bindings[bindings_len].value_int = value_int; + } else if (have_value_enum) { + if (!amduat_asl_ref_encode_hex(enum_ref, &value_hex)) { + free(key_hex); + free(value_hex); + free(key_text); + free(value_text); + free(value_ref_text); + free(value_enum_text); + amduat_reference_free(&key_ref); + amduat_reference_free(&value_ref); + amduat_reference_free(&enum_ref); + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + goto cf_cleanup; + } + bindings[bindings_len].value_kind = 2; + bindings[bindings_len].value_hex = value_hex; + } else { + free(key_hex); + free(value_hex); + free(key_text); + free(value_text); + free(value_ref_text); + free(value_enum_text); + amduat_reference_free(&key_ref); + amduat_reference_free(&value_ref); + amduat_reference_free(&enum_ref); + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid value_scalar"); + goto cf_cleanup; + } + bindings[bindings_len].key_hex = key_hex; + bindings_len++; + } + + free(key_text); + free(value_text); + free(value_ref_text); + free(value_enum_text); + amduat_reference_free(&key_ref); + amduat_reference_free(&value_ref); + amduat_reference_free(&enum_ref); + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == ']') { + p = cur + 1; + break; + } + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid bindings"); + goto cf_cleanup; + } + } + } else { + if (!amduatd_json_skip_value(&p, end, 0)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto cf_cleanup; + } + } + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + } + + if (bindings_len == 0) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "missing bindings"); + goto cf_cleanup; + } + + qsort(bindings, bindings_len, sizeof(*bindings), amduatd_cf_binding_cmp); + + if (!amduatd_strbuf_append_cstr(&b, "{\"bindings\":[")) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + goto cf_cleanup; + } + for (i = 0; i < bindings_len; ++i) { + if (i != 0) { + (void)amduatd_strbuf_append_char(&b, ','); + } + (void)amduatd_strbuf_append_cstr(&b, "{\"key_ref\":\""); + (void)amduatd_strbuf_append_cstr(&b, bindings[i].key_hex); + if (bindings[i].value_kind == 0) { + (void)amduatd_strbuf_append_cstr(&b, "\",\"value_ref\":\""); + (void)amduatd_strbuf_append_cstr(&b, bindings[i].value_hex); + (void)amduatd_strbuf_append_cstr(&b, "\"}"); + } else if (bindings[i].value_kind == 1) { + char tmp[32]; + int n = snprintf(tmp, sizeof(tmp), "%llu", + (unsigned long long)bindings[i].value_int); + if (n <= 0 || (size_t)n >= sizeof(tmp)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "error"); + goto cf_cleanup; + } + (void)amduatd_strbuf_append_cstr(&b, "\",\"value_int\":"); + (void)amduatd_strbuf_append_cstr(&b, tmp); + (void)amduatd_strbuf_append_cstr(&b, "}"); + } else if (bindings[i].value_kind == 2) { + (void)amduatd_strbuf_append_cstr(&b, "\",\"value_enum_ref\":\""); + (void)amduatd_strbuf_append_cstr(&b, bindings[i].value_hex); + (void)amduatd_strbuf_append_cstr(&b, "\"}"); + } else { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "error"); + goto cf_cleanup; + } + } + (void)amduatd_strbuf_append_cstr(&b, "]}\n"); + + artifact = amduat_artifact(amduat_octets(b.data, b.len)); + if (amduat_asl_store_put(store, artifact, &ref) != AMDUAT_ASL_STORE_OK) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "store error"); + goto cf_cleanup; + } + if (!amduat_asl_ref_encode_hex(ref, &ref_hex)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + goto cf_cleanup; + } + { + char json[2048]; + int n = snprintf(json, sizeof(json), "{\"ref\":\"%s\"}\n", ref_hex); + free(ref_hex); + if (n <= 0 || (size_t)n >= sizeof(json)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "error"); + goto cf_cleanup; + } + ok = amduatd_http_send_json(fd, 200, "OK", json, false); + } + +cf_cleanup: + free(body); + for (i = 0; i < bindings_len; ++i) { + amduatd_cf_binding_free(&bindings[i]); + } + free(bindings); + amduatd_strbuf_free(&b); + amduat_reference_free(&ref); + return ok; +} + +static bool amduatd_path_extract_name(const char *path, + const char *prefix, + char *out, + size_t cap) { + size_t plen; + const char *p; + size_t len; + + if (out != NULL && cap != 0) { + out[0] = '\0'; + } + if (path == NULL || prefix == NULL || out == NULL || cap == 0) { + return false; + } + plen = strlen(prefix); + if (strncmp(path, prefix, plen) != 0) { + return false; + } + p = path + plen; + if (*p == '\0') { + return false; + } + len = strlen(p); + if (len >= cap) { + len = cap - 1; + } + memcpy(out, p, len); + out[len] = '\0'; + return true; +} + +static bool amduatd_handle_post_concept_publish(int fd, + amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + amduatd_concepts_t *concepts, + const char *name, + const amduatd_http_req_t *req) { + uint8_t *body = NULL; + const char *p = NULL; + const char *end = NULL; + amduat_reference_t target_ref; + amduat_reference_t concept_ref; + amduat_reference_t edge_ref; + bool have_ref = false; + bool ok = false; + + memset(&target_ref, 0, sizeof(target_ref)); + memset(&concept_ref, 0, sizeof(concept_ref)); + memset(&edge_ref, 0, sizeof(edge_ref)); + + if (store == NULL || cfg == NULL || concepts == NULL || name == NULL || + req == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "internal error"); + } + if (!amduatd_name_valid(name)) { + return amduatd_send_json_error(fd, 400, "Bad Request", "invalid name"); + } + if (req->content_length == 0) { + return amduatd_send_json_error(fd, 400, "Bad Request", "missing body"); + } + if (req->content_length > (256u * 1024u)) { + return amduatd_send_json_error(fd, 413, "Payload Too Large", + "payload too large"); + } + + if (!amduatd_concepts_lookup_alias(store, cfg, concepts, name, &concept_ref)) { + return amduatd_send_json_error(fd, 404, "Not Found", "unknown concept"); + } + + body = (uint8_t *)malloc(req->content_length); + if (body == NULL) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + goto publish_cleanup; + } + if (!amduatd_read_exact(fd, body, req->content_length)) { + free(body); + amduat_reference_free(&concept_ref); + return false; + } + + p = (const char *)body; + end = (const char *)body + req->content_length; + if (!amduatd_json_expect(&p, end, '{')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto publish_cleanup; + } + for (;;) { + const char *key = NULL; + size_t key_len = 0; + const char *sv = NULL; + size_t sv_len = 0; + const char *cur = NULL; + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + if (!amduatd_json_parse_string_noesc(&p, end, &key, &key_len) || + !amduatd_json_expect(&p, end, ':')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto publish_cleanup; + } + if (key_len == strlen("ref") && memcmp(key, "ref", key_len) == 0) { + if (have_ref || !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len) || + !amduatd_decode_ref_hex_str(sv, sv_len, &target_ref)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid ref"); + goto publish_cleanup; + } + have_ref = true; + } else { + if (!amduatd_json_skip_value(&p, end, 0)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto publish_cleanup; + } + } + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto publish_cleanup; + } + if (!have_ref) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "missing ref"); + goto publish_cleanup; + } + + if (!amduatd_concepts_put_edge(store, + concepts, + concept_ref, + target_ref, + concepts->rel_materializes_ref, + &edge_ref)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "store error"); + goto publish_cleanup; + } + + ok = amduatd_http_send_json(fd, 200, "OK", "{\"ok\":true}\n", false); + +publish_cleanup: + free(body); + amduat_reference_free(&target_ref); + amduat_reference_free(&concept_ref); + amduat_reference_free(&edge_ref); + return ok; +} + +static bool amduatd_handle_get_resolve(int fd, + amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + const amduatd_concepts_t *concepts, + const char *name) { + amduat_reference_t concept_ref; + amduat_reference_t latest_ref; + char *hex = NULL; + char json[2048]; + int n; + + memset(&concept_ref, 0, sizeof(concept_ref)); + memset(&latest_ref, 0, sizeof(latest_ref)); + + if (store == NULL || cfg == NULL || concepts == NULL || name == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "internal error"); + } + if (!amduatd_name_valid(name)) { + return amduatd_send_json_error(fd, 400, "Bad Request", "invalid name"); + } + if (!amduatd_concepts_lookup_alias(store, cfg, concepts, name, &concept_ref)) { + return amduatd_send_json_error(fd, 404, "Not Found", "unknown concept"); + } + if (!amduatd_concepts_resolve_latest(store, concepts, concept_ref, + &latest_ref)) { + amduat_reference_free(&concept_ref); + return amduatd_send_json_error(fd, 404, "Not Found", "no versions"); + } + amduat_reference_free(&concept_ref); + + if (!amduat_asl_ref_encode_hex(latest_ref, &hex)) { + amduat_reference_free(&latest_ref); + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + } + amduat_reference_free(&latest_ref); + n = snprintf(json, sizeof(json), "{\"ref\":\"%s\"}\n", hex); + free(hex); + if (n <= 0 || (size_t)n >= sizeof(json)) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", "error"); + } + return amduatd_http_send_json(fd, 200, "OK", json, false); +} + static bool amduatd_handle_conn(int fd, amduat_asl_store_t *store, - const amduat_asl_store_fs_config_t *cfg) { + const amduat_asl_store_fs_config_t *cfg, + amduat_reference_t api_contract_ref, + amduat_reference_t ui_ref, + amduatd_concepts_t *concepts) { amduatd_http_req_t req; char no_query[1024]; @@ -1316,29 +5985,84 @@ static bool amduatd_handle_conn(int fd, amduatd_path_without_query(req.path, no_query, sizeof(no_query)); + if (strcmp(req.method, "GET") == 0 && strcmp(no_query, "/v1/ui") == 0) { + return amduatd_handle_get_ui(fd, store, ui_ref); + } + if (strcmp(req.method, "GET") == 0 && strcmp(no_query, "/v1/meta") == 0) { - return amduatd_handle_meta(fd, cfg, false); + return amduatd_handle_meta(fd, cfg, api_contract_ref, false); } if (strcmp(req.method, "HEAD") == 0 && strcmp(no_query, "/v1/meta") == 0) { - return amduatd_handle_meta(fd, cfg, true); + return amduatd_handle_meta(fd, cfg, api_contract_ref, true); + } + if (strcmp(req.method, "GET") == 0 && strcmp(no_query, "/v1/contract") == 0) { + return amduatd_handle_get_contract(fd, store, &req, api_contract_ref); } if (strcmp(req.method, "POST") == 0 && strcmp(no_query, "/v1/artifacts") == 0) { return amduatd_handle_post_artifacts(fd, store, &req); } if (strcmp(req.method, "POST") == 0 && strcmp(no_query, "/v1/pel/run") == 0) { - return amduatd_handle_post_pel_run(fd, store, &req); + return amduatd_handle_post_pel_run(fd, store, cfg, concepts, &req); + } + if (strcmp(req.method, "POST") == 0 && + strcmp(no_query, "/v1/pel/programs") == 0) { + return amduatd_handle_post_pel_programs(fd, store, &req); + } + if (strcmp(req.method, "POST") == 0 && + strcmp(no_query, "/v1/context_frames") == 0) { + return amduatd_handle_post_context_frames(fd, store, cfg, concepts, &req); + } + if (strcmp(req.method, "POST") == 0 && strcmp(no_query, "/v1/concepts") == 0) { + return amduatd_handle_post_concepts(fd, store, cfg, concepts, &req); + } + if (strcmp(req.method, "GET") == 0 && strcmp(no_query, "/v1/concepts") == 0) { + return amduatd_handle_get_concepts(fd, store, concepts); + } + if (strcmp(req.method, "GET") == 0 && strcmp(no_query, "/v1/relations") == 0) { + return amduatd_handle_get_relations(fd, concepts); + } + if (strcmp(req.method, "GET") == 0 && strncmp(no_query, "/v1/concepts/", 13) == 0) { + const char *name = no_query + 13; + if (name[0] == '\0') { + return amduatd_send_json_error(fd, 400, "Bad Request", "missing name"); + } + return amduatd_handle_get_concept(fd, store, cfg, concepts, name); + } + if (strcmp(req.method, "POST") == 0 && + strncmp(no_query, "/v1/concepts/", 13) == 0 && + strstr(no_query, "/publish") != NULL) { + char name[256]; + char *slash; + if (!amduatd_path_extract_name(no_query, "/v1/concepts/", name, + sizeof(name))) { + return amduatd_send_json_error(fd, 400, "Bad Request", "invalid path"); + } + slash = strstr(name, "/publish"); + if (slash == NULL || strcmp(slash, "/publish") != 0) { + return amduatd_send_json_error(fd, 400, "Bad Request", "invalid path"); + } + *slash = '\0'; + return amduatd_handle_post_concept_publish(fd, store, cfg, concepts, name, + &req); + } + if (strcmp(req.method, "GET") == 0 && strncmp(no_query, "/v1/resolve/", 12) == 0) { + const char *name = no_query + 12; + if (name[0] == '\0') { + return amduatd_send_json_error(fd, 400, "Bad Request", "missing name"); + } + return amduatd_handle_get_resolve(fd, store, cfg, concepts, name); } if (strcmp(req.method, "GET") == 0 && strncmp(no_query, "/v1/artifacts/", 14) == 0) { - return amduatd_handle_get_artifact(fd, store, req.path, false); + return amduatd_handle_get_artifact(fd, store, &req, req.path, false); } if (strcmp(req.method, "HEAD") == 0 && strncmp(no_query, "/v1/artifacts/", 14) == 0) { - return amduatd_handle_head_artifact(fd, store, req.path); + return amduatd_handle_head_artifact(fd, store, &req, req.path); } - return amduatd_http_send_text(fd, 404, "Not Found", "not found\n", false); + return amduatd_http_send_not_found(fd, &req); } static void amduatd_print_usage(FILE *stream) { @@ -1359,9 +6083,16 @@ int main(int argc, char **argv) { amduat_asl_store_fs_config_t cfg; amduat_asl_store_fs_t fs; amduat_asl_store_t store; + amduat_reference_t api_contract_ref; + amduat_reference_t ui_ref; + amduatd_concepts_t concepts; int i; int sfd = -1; + memset(&api_contract_ref, 0, sizeof(api_contract_ref)); + memset(&ui_ref, 0, sizeof(ui_ref)); + memset(&concepts, 0, sizeof(concepts)); + for (i = 1; i < argc; ++i) { if (strcmp(argv[i], "--root") == 0) { if (i + 1 >= argc) { @@ -1397,6 +6128,23 @@ int main(int argc, char **argv) { } amduat_asl_store_init(&store, cfg.config, amduat_asl_store_fs_ops(), &fs); + if (!amduatd_seed_api_contract(&store, &cfg, &api_contract_ref)) { + fprintf(stderr, "error: failed to seed api contract\n"); + return 8; + } + if (!amduatd_seed_ui_html(&store, &cfg, &ui_ref)) { + fprintf(stderr, "error: failed to seed ui html\n"); + return 8; + } + if (!amduatd_concepts_init(&concepts, &store, &cfg, root)) { + fprintf(stderr, "error: failed to init concept edges\n"); + return 8; + } + if (!amduatd_seed_ms_ui_state(&store, &cfg, &concepts)) { + fprintf(stderr, "error: failed to seed ms ui state\n"); + return 8; + } + signal(SIGINT, amduatd_on_signal); signal(SIGTERM, amduatd_on_signal); @@ -1446,10 +6194,18 @@ int main(int argc, char **argv) { break; } - (void)amduatd_handle_conn(cfd, &store, &cfg); + (void)amduatd_handle_conn(cfd, + &store, + &cfg, + api_contract_ref, + ui_ref, + &concepts); (void)close(cfd); } + amduat_reference_free(&api_contract_ref); + amduat_reference_free(&ui_ref); + amduatd_concepts_free(&concepts); (void)unlink(sock_path); (void)close(sfd); return 0; diff --git a/tier1/ms.md b/tier1/ms.md new file mode 100644 index 0000000..279624b --- /dev/null +++ b/tier1/ms.md @@ -0,0 +1,505 @@ +# AMDUAT-MS/1 — Mapping Surface Specification + +Status: Draft +Owner: Architecture +Version: 0.2.2 +SoT: Yes +Last Updated: 2025-11-30 +Linked Phase Pack: PH07 +Tags: [composition, execution, deterministic] + +identity_authority: amduat.programme +lineage_id: L-PENDING-MS1 +doc_code: AMDUAT-MS/1 +code_status: tentative +doc_code_aliases: [] + +location: /amduat/tier1/ms.md +surface: developer +internal_revision: 3 +provenance_mode: full + +--- + +## Overview + +**AMDUAT-MS/1** standardises the executable mapping surface that turns a +**Concept** plus a **Context Frame** into deterministic **Data** bytes. The +surface orchestrates **FPS/1 primitives** through **FCS/1 recipes** and records +runs through **FER/1**; certification, provenance, and policy decisions remain +with **FCT/1** and phase evidence packs. MS/1 governs observable mapping +behaviour and context binding rules without authorising new governance or +relation taxonomies. + +--- + +## Core Model + +MS/1 retains Amduat's two primitive node kinds and treats all other structure as +concept-typed relations. + +* **Concept node (C):** Abstract identifier that can describe, type, or govern + any graph element. A concept can own multiple materialisations without losing + identity. +* **Data node (D):** Immutable byte sequence addressed by CID (SHA-256). All + executions produce Data nodes. +* **Relation instances:** Every edge is annotated with a `relation_concept` that + identifies its semantics. Implementations MUST NOT embed a fixed relation + enumeration; relation concepts are first-class concepts. + +Non-normative relation concept examples include `represents` (C→D), +`materializesAs` (C→D), `requiresKey` (C→C), `withinDomain` (C→C), `computedBy` +(D→C), and `hasProvenance` (D→D). Ecosystems MAY register additional concepts and +MUST treat each registered concept as a normal graph node. + +--- + +## Context Frames + +A **Context Frame (CF)** is a deterministic multimap of `{ key_concept → value +}` that constrains execution. + +* **Keys** are concept identifiers; strings are optional. +* **Values** are canonical scalars (integers, enums keyed by concept ID) or CIDs + to Data nodes for larger payloads. +* **Context CID:** `CID_context = sha256(canonical_encode(CF))`. The canonical + encoding MUST order keys by concept ID and normalise values so identical frames + generate identical CIDs. + +### Scoping and Refinement + +* Frames form a tree during pipeline execution. Each mapping step receives one + frame. +* Child frames inherit bindings from the parent. A child MAY add new bindings or + narrow an existing binding but MUST NOT contradict established bindings. +* Branch communication is explicit. Publishing a refinement to siblings requires + constructing a new **published frame** and referencing it; no sideways state is + implicit. + +### Gaps and Ambiguity + +* Missing required bindings yield a **Gap Artifact** (Data) that records the + missing keys and the decision point. No payload bytes are emitted. +* When multiple admissible bindings remain, the step returns an **Ambiguity + Artifact** (Data) enumerating admissible alternatives and the rule needed to + disambiguate. Resolution demands a refined frame. + +Gap and Ambiguity artifacts are ordinary Data nodes with CIDs and MAY be audited +like any other output. + +--- + +## Mapping Surface Semantics + +The surface defines the total function: + +``` +MS_map : (Concept C, ContextFrame CF) -> Result +Result = Produced(Data bytes) | Gap(Data) | Ambiguity(Data) +``` + +### Executable Form + +* Every mapping MUST be realised as an **FCS/1 recipe** parameterised by **PCB1** + blocks. +* Recipes MAY only invoke **FPS/1 primitives** (`put`, `get`, `slice`, + `concatenate`, `reverse`, `splice`) and compositions thereof. + +### Determinism and Replayability + +For fixed `(C, CF)` inputs and fixed referenced Data CIDs, implementations MUST +produce identical bytes. + +Each run MUST emit a **FER/1 run record** (Data) that declares: + +* the recipe concept ID (FCS/1), +* the PCB1 parameter block (as Data CID), +* all input CIDs (including `CID_context`), +* the output CID, and +* relevant environment hashes (e.g., FPS/1 library surfaces) that affect + determinism. + +Replaying the FCS/1 recipe with the same inputs MUST yield the same CID. + +### Fidelity Predicates + +Each mapping declares (via relation concepts) an **equivalence predicate** that +states when the produced bytes faithfully represent the target concept. Fidelity +predicates are themselves concepts and MAY be certified through **FCT/1**. + +### Domain Keys + +Disambiguation relies on **Domain concepts** expressed as context keys (e.g., +`withinDomain`). Recipes MAY require domain bindings such as classical vs. +quantum state spaces to guarantee deterministic interpretation. + +### Function Interface Patterns + +Implementations SHOULD expose a narrow callable surface so concept inputs and +Determinism guarantees remain auditable. + +1. **Primary signature:** + * `Result ms_map(concept_id, context_frame)` is the canonical entry point. + * `concept_id` MUST be a stable concept node reference (e.g., CID or registry + handle). Implementations MUST NOT infer the target concept from global + mutable state. + * `context_frame` MUST be the complete set of bindings used for the run. Any + binding derived from other inputs MUST be reflected back into the frame + before execution. +2. **Auxiliary parameters:** + * Additional parameters are only permitted when they can be canonicalised to + deterministic Data CIDs and recorded in the FER/1 run record. + * Preferred pattern: `attachments: Sequence[DataCID] = ()`. Each attachment + MUST already exist as a Data node, and the mapping MUST reference the CIDs + explicitly in the FER/1 log. + * Alternative pattern: `concept_inputs: Sequence[ConceptID] = ()` for + secondary concept handles. Each element MUST also appear in the effective + context frame (e.g., under a relation-specific key). Passing the concept ID + without updating the frame is a violation. +3. **Rejected pattern:** Opaque keyword arguments or process-level environment + variables MUST NOT influence execution. Implementations discovering such usage + MUST emit an Ambiguity artifact describing the missing context binding + instead. + +The following pseudocode illustrates a compliant wrapper: + +``` +def ms_map(concept_id: ConceptID, + context_frame: ContextFrame, + *, + attachments: Sequence[DataCID] = (), + concept_inputs: Sequence[ConceptID] = ()) -> Result: + frame = context_frame.with_bindings({ + relation_for(ci): ci for ci in concept_inputs + }) + frame = frame.with_bindings({ + relation_for(cid): cid for cid in attachments + }) + record = run_fcs_recipe(concept_id, frame, attachments) + return normalise_result(record) +``` + +`relation_for` denotes a deterministic lookup from relation concept to the key +under which the binding is stored. Implementations MAY inline more efficient +mechanisms, but the observable effect MUST match first updating the frame and +then invoking the recipe. + +### Media (MIME) Types vs. MS Context + +**MIME/media types** label already-produced Data so downstream systems can +interpret byte strings (e.g., `text/plain; charset=utf-8`, `image/png`). An +**MS context frame** captures the *pre-execution* bindings that shape which bytes +will be emitted. They relate but are not interchangeable: + +* **Where they live:** Context bindings exist before execution and are hashed + into `CID_context`. MIME labels are attached after Data exists (e.g., CIL + payload metadata, HTTP headers, or relation edges such as + `ms.produces_media_type`). +* **What they encode:** Context keys describe decision levers (encoding, domain, + fidelity policy) that an FCS recipe uses to deterministically produce bytes. + MIME types describe how consumers should parse already-materialised bytes. A + context MAY carry a desired media-type concept ("emit this as + `application/pdf`") but it is still a binding that constrains execution, not a + replacement for Content-Type headers. +* **Interoperability:** When MS outputs feed MIME-governed ecosystems, recipes + SHOULD register a concept such as `ms.media_type` and declare it via + `ms.requires_key` if the choice affects determinism (e.g., choosing between + `text/csv` vs. `application/json`). FER/1 receipts then bind the produced Data + both to the governing context frame and to a media-type relation so downstream + MIME routers, storage overlays, and catalogues reconcile the two perspectives. + +MIME types are one example of **interpretation contracts** that MS contexts can +align with. The same pattern applies to CAD kernels with STEP schemas, audio +encoders with bitstream levels, or ML artefacts with `model.format` +descriptors. MS/1 keeps them deterministic by requiring the selection to be a +context binding (recorded before execution) while also allowing publication +surfaces to mirror the choice via their native metadata channels. + +--- + +## Pipelines and Composition + +A **Pipeline** is a concept that composes ordered mapping steps. + +* Each step executes `MS_map(C_i, CF_i)`. +* Pipelines return either Produced(Data) or an Artifact (Gap/Ambiguity). +* Given identical inputs, pipelines MUST be byte-stable. +* **Branch isolation:** each branch operates on a forked frame. Publishing + updates requires emitting a new frame with a fresh `CID_context`, preserving + immutability. + +--- + +## Conformance Criteria + +Implementations conform to MS/1 when they satisfy all of the following: + +1. Accept `(C, CF)` and emit either Produced(Data) or a Gap/Ambiguity artifact. +2. Produce outputs reproducibly via FCS/1 + FPS/1, and record each run via FER/1. +3. Encode mutable decisions through concept-typed context keys only; no hidden + flags. +4. Address all bytes by SHA-256 CIDs and guarantee identical inputs replay to + identical CIDs. +5. Treat relation types as concepts without hard-coded enumerations. + +## Dependencies (pinned drafts) + +MS/1 relies on upstream drafts; consumers MUST treat these versions as pinned +for this specification and handle later changes as upstream drift: + +* FCS/1 v0.2.1 (Draft) +* FPS/1 v0.4.3 (Draft) +* FLS/1 v0.1.0 (Draft) + +Any adoption in PH10+ MUST re-pin consciously if these upstream specs change. + +## Evidence (PH08 runtime) + +PH08 reference executions demonstrate MS/1 behaviour; cite these when asserting +readiness or approval: + +* `logs/ph08/evidence/ms/PH08-EV-MS-RUNTIME-001/` +* `logs/ph08/evidence/ms/PH08-EV-MS-ACCEPT-001/` +* `logs/ph08/evidence/ms/PH08-EV-MS-BIND-001/` +* `logs/ph08/evidence/ms/PH08-EV-MS-CORE-001/` +* `logs/ph08/evidence/ms/PH08-EV-MS-GATES-001/` +* `logs/ph08/evidence/ms/PH08-EV-MS-LADDER-001/` +* `logs/ph08/evidence/ms/PH08-EV-MS-PIPE-001/` +* `logs/ph08/evidence/ms/PH08-EV-MS-ML-EVAL-001/` + +--- + +## Worked Ladder Example (Zero → Byte) + +The following non-normative ladder illustrates how MS/1 composes mappings from +abstract numbers to textual bytes. + +1. Concepts: `C_Number65`, `C_CodePoint(U+0041)`, `C_LetterA`, `C_WordA`. +2. Context frame bindings: `withinDomain → C_Unicode15`, + `C_TextEncoding → C_UTF8`. +3. Steps: + * Interpret `C_Number65` as `C_CodePoint(U+0041)` by applying a mapping rule + concept. + * Map `C_CodePoint(U+0041)` within the Unicode/UTF-8 frame to Data `0x41`. + * Map `C_WordA` under the same frame to Data `0x41`. + +Each run records its FER/1 linkage so the byte `0x41` can be replayed. + +--- + +## Context Evolution and Replay Discipline + +### Context Revisions During Pipelines + +When a branch detects missing bindings (e.g., absent `C_TextEncoding`), it MUST +emit an Ambiguity artifact detailing admissible encodings. Progress resumes by +creating a refined frame `CF' = CF ∪ { C_TextEncoding → C_UTF8 }` and re-running +only the affected branch. Publishing the refinement to siblings is explicit and +produces a new parent frame with a distinct `CID_context`. + +### Replay-First Evolution + +MS/1 guards against semantic drift when the knowledge base or registry grows: + +* **Frame immutability:** Every FER/1 record captures the `CID_context` that was + in force at execution time. New bindings (e.g., alternative encodings) produce + new frame hashes and therefore new provenance edges. Historical runs replay + byte-for-byte because their frames never mutate. +* **Required bindings:** Recipes MUST register mandatory keys via + `ms.requires_key`. When governance tightens (for example, by requiring a + `string.encoding` key), existing frames lacking the key yield deterministic + Gap/Ambiguity artifacts instead of silently adopting defaults. +* **Replay-first migration:** If an ecosystem needs the new semantics, it reruns + the recorded FCS/1 recipe with a refined frame. The new FER/1 record cites the + refined `CID_context`, making comparisons between legacy and refreshed runs + explicit and auditable. +* **Artifact parity:** Gap and Ambiguity outputs are stored as first-class Data + nodes. Audits can therefore demonstrate exactly where a richer knowledge base + demanded additional context, keeping provenance complete even when execution + paused. + +Collectively, these rules let operators expand the registry or tighten policies +without jeopardising determinism or traceability. + +--- + +## Hashing Bits and Abstract Values + +MS/1 hashes bytes, not abstract concepts. A bit becomes hashable only once it is +materialised (e.g., via `C_BitAsOctet`). When domain or packing decisions remain +unbound, the mapping MUST emit an Ambiguity or Gap artifact rather than guess. + +--- + +## Risk Controls + +MS/1 aligns with existing Amduat controls: + +* **Semantic drift:** Context keys are concepts with versioned materialisations; + frame hashes reveal mismatches. +* **Provenance loss:** Every Produced(Data) links to a FER/1 record via + `hasProvenance`. +* **Undocumented mutation:** Frames are immutable; refinements create new + contexts. +* **Rights obligations:** Licences and attribution obligations appear as context + keys referencing policy Data. Recipes MAY decline to execute when obligations + are unmet, provided the decision is deterministic. + +--- + +## Acceptance Checks + +An MS/1 implementation SHOULD ship the following self-tests: + +1. **Replay test:** identical `(C, CF)` inputs produce the same CID and FER/1 log. +2. **Gap test:** missing required keys yield deterministic Gap artifacts. +3. **Ambiguity test:** admissible alternatives are enumerated stably. +4. **Scope test:** sibling branches remain unchanged unless a published frame is + adopted. +5. **Fidelity test:** the declared equivalence predicate is machine-verifiable. + +--- + +## Registry Alignment + +MS/1 is production-registered in the CRS/1 concept registry. Implementations +MUST treat the following handles and digests as canonical when emitting or +validating graphs: + +| Symbol | Registry Handle | Kind | SHA-256 Digest | Notes | +| --- | --- | --- | --- | --- | +| `MS/1` | `crs:concept/amduat.ms.surface@1` | Concept | `d140ac54367a88fa2459e3fedf0b2fde934f9ac73568f8a159e2b0c1c1828c70` | Primary mapping surface concept anchoring `MS_map` executions. | +| `ms.produces` | `crs:concept/amduat.ms.relation.produces@1` | Relation concept | `447a9f454d78f5b2ee300fe416138a864789e133b2eb9a84e32592aa9dd47965` | Annotates `Concept → Data` edges that capture Produced(Data) bytes. | +| `ms.requires_key` | `crs:concept/amduat.ms.relation.requires_key@1` | Relation concept | `a90295a8ca3006e062a5a1d5a6220330e53ba00677736b6f4a18efcec1169f6a` | Declares mandatory context key bindings for deterministic execution. | +| `ms.within_domain` | `crs:concept/amduat.ms.relation.within_domain@1` | Relation concept | `0993dff2531dd32ea32b98925bf8a5cbc88c88ed28cd3c3575e8affc84d7fa2d` | Expresses domain refinements used to disambiguate mappings. | +| `ms.fidelity_predicate` | `crs:concept/amduat.ms.relation.fidelity_predicate@1` | Relation concept | `7e159182789d89269b743c19da58d34acc3279d86650cfef83efd4f2c210c66a` | Binds outputs to the declared fidelity predicate concept. | +| `ms.byteValue` | `crs:concept/amduat.ms.relation.byte_value@1` | Relation concept | `4c43dd3a37ae695bac476e4dc62d8d8c2abda6c555668d4d863810d0053056c3` | Records byte concepts and their literal value bindings within the ladder corpus. | +| `ms.codePoint` | `crs:concept/amduat.ms.relation.code_point@1` | Relation concept | `bb7301d2fa0c5058cbd53019625bfd38ecb59e42010b64a9f0b0ada7dc494117` | Links textual concepts to Unicode code points under registered contexts. | +| `ms.symbolSequence` | `crs:concept/amduat.ms.relation.symbol_sequence@1` | Relation concept | `115035e5dc2db7e9ab2e4255f50aee56d222b04f7b7434dbbec76620cf36aa6d` | Declares ordered relations between code points/bytes when emitting strings. | +| `ms.upperCasePolicy` | `crs:concept/amduat.ms.relation.upper_case_policy@1` | Relation concept | `6f5165648c1069178c2e8a615bd24bd02a3a367df6f8e6ac21050f42eeea484c` | Encodes casing policies that constrain textual ladders. | +| `ms.titleCasePolicy` | `crs:concept/amduat.ms.relation.title_case_policy@1` | Relation concept | `7f10f109b578b69b079fc9f284ed73c69b82434b31db0a999a872d2b47958ae5` | Encodes title-casing policies enforced during deterministic mapping. | + +The registry sidecar at `/amduat/registry/predicates.jsonl` mirrors these +entries, allowing auditors to verify digests independent of this specification. +Implementations MUST fail closed when encountering unregistered aliases for +these handles. + +--- + +## Phase 07 Cross-Stream Integration + +Phase 07 workstreams bind their certificates, receipts, facts, overlays, and +domain dossiers to MS/1 contexts through a shared TypeTag grid: + +* `/amduat/phases/ph07/notes/PH07-CIL-XMAP-001.md` (`CIL-X1`) and + `vectors/ph07/cil/PH07-CIL-XMAP-001.json` declare the authoritative + cross-stream mapping entries and TypeTag ranges that all semantic surfaces + must cite before reaching Draft Ready gates. The JSON registry is logged + under `PH07-EV-CIL-ATTEST-001` so downstream profiles can dereference the + same identifiers without ambiguity. +* `/amduat/phases/ph07/notes/PH07-CROSS-CHECK-001.md` records the harness that + enforces these bindings. The checklist stored in + `logs/ph07/evidence/cil/PH07-EV-CIL-ATTEST-001/PH07-CROSS-TV-001.md` + validates every FER/FCT/OI manifest against the refreshed XMAP IDs before the + `*-5` ledger exits (`FER-5`, `FCT-5`, `OI-5`) can advance. +* `/amduat/phases/ph07/notes/PH07-FER-SCHEMA-001.md` (ledger `FER-1`, evidence + `PH07-EV-FER-RUN-001`) pins `xmap_refs[]`, certificate anchors, and replay + bundles to the XMAP rows so every receipt explicitly declares which MS/1 + context governed execution. +* `/amduat/phases/ph07/notes/PH07-FCT-SCHEMA-001.md` (ledger `FCT-1`, evidence + `PH07-EV-FCT-FACTS-001`) introduces the `trust_spine` block that carries the + required `xmap_ref`, `receipt_refs[]`, and `anchor_certs[]`, keeping fact + acceptance policies tied to the same mapping IDs. +* `/amduat/phases/ph07/notes/PH07-OI-HARNESS-001.md` (ledger `OI-5`, evidence + `PH07-EV-OI-VIEWS-001`) proves overlay descriptors and workspace views publish + `mapping_profile.xmap_refs[]` plus TGK edge expectations aligned with + `XMAP-CIL-CUSTOM-V1`. +* `/amduat/phases/ph07/notes/PH07-DOM-HARNESS-001.md` (ledger `DOM-5`, evidence + `PH07-EV-DOM-APPS-001`) extends the same guarantees to domain pilot dossiers, + confirming their overlays, facts, and receipts cite approved TypeTags and TGK + edge sets. + +MS/1 implementations participating in PH07 MUST therefore emit the same mapping +handles and evidence references recorded in these notes so provenance can be +validated across CIL/FER/FCT/OI boundaries. + +--- + +## Phase 05 Textual Ladder Scope and Evidence + +Phase 05 extends MS/1 from abstract examples to a production ladder that binds +textual concepts to deterministic bytes. The ladder introduces: + +* A `byte` concept family with 256 child concepts (`byte/0x00` … `byte/0xFF`) + whose CRS/1 relations emit single-octet Data nodes and optionally record the + radix context via `ms.byteValue` predicates. +* UTF-8 code point concepts that sequence byte concepts through + `ms.symbolSequence` relations while asserting `ms.within_domain` bindings to + Unicode 15 and UTF-8 domain concepts. +* Casing policy concepts (e.g., `allCaps`, `titleCase`) that require the + `string.casingPolicy` context key and advertise fidelity predicates so + downstream tooling can reject ambiguous casing decisions via + `ms.upperCasePolicy` and `ms.titleCasePolicy` handles. +* Dictionary word concepts implemented as FCS/1 recipes that concatenate code + points into byte strings, emitting FER/1 receipts that cite the governing + casing policy and context frame. + +Predicate registries gain canonical handles (`ms.byteValue`, `ms.codePoint`, +`ms.symbolSequence`, `ms.produces`, `ms.requires_key`, `ms.upperCasePolicy`, +`ms.titleCasePolicy`) so tooling can resolve ladder edges without bespoke +enumerations. Missing casing bindings MUST yield Ambiguity artifacts +(`ERR_MS_POLICY_MISSING`/`ERR_MS_AMBIGUITY`); absent code points produce Gap +artifacts (`ERR_MS_GAP`); undeclared predicates raise +`ERR_MS_UNDECLARED_PREDICATE`. + +Evidence for the ladder is captured under +`/amduat/vectors/ph05/ms1-text/manifest.json` and the reserved +`/amduat/logs/ph05/evidence/ms1/` surfaces: + +* `PH05-EV-MS-CTX-001/` — CTX/1 context frames with predicate registry vectors + (domain separator `AMDUAT:CTX\0`, reject `ERR_CTX_UNKNOWN_KEY`). +* `PH05-EV-MS-LADDER-001/` — Dual-run FER/1-backed positive ladders for bytes, + code points, and dictionary outputs with SA/PA guardrails. +* `PH05-EV-MS-ERRORS-001/` — Gap/ambiguity/missing policy/undeclared predicate + receipts mapped to ADR-006. + +--- + +## Phase Alignment and Readiness + +* **PH07 (Semantic Surfaces):** MS/1 is authoritative for every PH07 semantic + surface (CIL/FER/FCT/OI) and must be cited wherever mapping semantics are + referenced. PH07 workstreams MAY extend context vocabularies or registry rows + so long as they remain MS/1-conformant; no runtime commits are expected in + this phase beyond harness stubs and governance evidence. +* **PH08 (Reference Implementation):** The reference `ms_map` runtime, parity + harnesses, and subsystem wiring reside in the `KRN-2` campaign slotted for + Phase 08. PH08 SHALL use this specification verbatim, emitting FER/1 records + and exercising the acceptance checks in §Acceptance Checks. +* **Downstream pilots (PH09+):** Reproducible ML, CI/CD, data mesh, and notebook + pilots inherit the MS/1 contract by invoking the PH08 reference surface. Those + phases MAY add domain-specific context keys or ladders, but they MUST register + them through CRS/1 and capture evidence via the surfaces reserved in + §Phase 05 Textual Ladder Scope and Evidence. + +Declaring these boundaries keeps the approval pathway clear: PH07 completes the +specification, PH08 proves the executable substrate, and later phases consume it +without reopening MS/1 fundamentals. + +--- + +## Document History + + + +* **0.1.0 (2025-11-10):** Initial draft capturing deterministic concept-to-data mapping surface. +* **0.1.1 (2025-11-11):** Add interface patterns for mapping functions and constrain auxiliary parameters. +* **0.1.2 (2025-11-12):** Formalize registry concept handles and digests for MS/1. +* **0.1.3 (2025-11-14):** Documented PH05 textual ladder scope, predicate handles, and evidence surfaces. +* **0.1.4 (2025-11-15):** Aligned MS/1 evidence references with CTX/1, ladder, and error reservations. +* **0.1.5 (2025-11-18):** Added ms.* predicate handles, CTX/1 domain separator, ADR-006 error mapping, and dual-run evidence guardrails. +* **0.1.6 (2025-11-18):** Added context-evolution guardrails and PH07→PH08 readiness boundaries. +* **0.1.7 (2025-11-19):** Clarified MIME/media-type relationship to MS context frames and execution bindings. +* **0.2.0 (2025-11-19):** Standardized metadata, headings, and evidence alignment per DOCSTD. +* **0.2.1 (2025-11-19):** Synced registry handles and documented PH07 XMAP/harness integration. +* **0.2.2 (2025-11-30):** Added DOCID header, pinned upstream draft dependencies, and referenced PH08 MS/1 evidence bundles.