From a4b501e48d16370bd6f4d267de9193300c31653a Mon Sep 17 00:00:00 2001 From: Carl Niklas Rydberg Date: Wed, 21 Jan 2026 19:51:26 +0100 Subject: [PATCH] federation --- CMakeLists.txt | 17 +- README.md | 80 ++- docs/federation-coordinator.md | 355 ++++++++++ federation/coord.c | 584 ++++++++++++++++ federation/coord.h | 95 +++ federation/transport_stub.c | 72 ++ federation/transport_stub.h | 25 + federation/transport_unix.c | 852 +++++++++++++++++++++++ federation/transport_unix.h | 26 + registry/README.md | 4 + registry/amduatd-api-contract.v1.json | 181 ++++- registry/api-contract.jsonl | 2 +- registry/api-contract.schema.md | 5 + src/amduatd.c | 961 +++++++++++++++++++++++++- 14 files changed, 3229 insertions(+), 30 deletions(-) create mode 100644 docs/federation-coordinator.md create mode 100644 federation/coord.c create mode 100644 federation/coord.h create mode 100644 federation/transport_stub.c create mode 100644 federation/transport_stub.h create mode 100644 federation/transport_unix.c create mode 100644 federation/transport_unix.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 730d92b..a2e7c61 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,21 @@ set(CMAKE_C_EXTENSIONS OFF) add_subdirectory(vendor/amduat) +add_library(amduat_federation + federation/coord.c + federation/transport_stub.c + federation/transport_unix.c +) + +target_include_directories(amduat_federation + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include +) + +target_link_libraries(amduat_federation + PRIVATE amduat_asl amduat_enc amduat_util amduat_fed +) + add_executable(amduatd src/amduatd.c) target_include_directories(amduatd @@ -16,5 +31,5 @@ target_include_directories(amduatd target_link_libraries(amduatd PRIVATE amduat_tgk amduat_pel amduat_format amduat_asl_store_fs amduat_asl - amduat_enc amduat_hash_asl1 amduat_util + amduat_enc amduat_hash_asl1 amduat_util amduat_federation ) diff --git a/README.md b/README.md index 87b4283..09c4535 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,9 @@ curl --unix-socket amduatd.sock 'http://localhost/v1/artifacts/?format=info - `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 +- `GET /v1/fed/records?domain_id=...&from_logseq=...&limit=...` → `{domain_id, snapshot_id, log_prefix, next_logseq, records[]}` (published artifacts + tombstones + PER + TGK edges) +- `GET /v1/fed/artifacts/{ref}` → raw bytes for federation resolve +- `GET /v1/fed/status` → `{status, domain_id, registry_ref, last_tick_ms}` - `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` @@ -156,10 +159,85 @@ curl --unix-socket amduatd.sock 'http://localhost/v1/artifacts/?format=info - `GET /v1/resolve/{name}` → `{ref}` (latest published) - `POST /v1/pel/run` - 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}` + - request receipt (optional): `{receipt:{input_manifest_ref, environment_ref, evaluator_id, executor_ref, started_at, completed_at, sbom_ref?, parity_digest_hex?, executor_fingerprint_ref?, run_id_hex?, limits?, logs?, determinism_level?, rng_seed_hex?, signature_hex?}}` + - response: `{result_ref, trace_ref?, receipt_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}` +- `POST /v1/context_frames` + +Receipt example (with v1.1 fields): + +```json +{ + "program_ref": "ab12...", + "input_refs": ["cd34..."], + "receipt": { + "input_manifest_ref": "ef56...", + "environment_ref": "7890...", + "evaluator_id": "local-amduatd", + "executor_ref": "1122...", + "started_at": 1712345678, + "completed_at": 1712345688, + "executor_fingerprint_ref": "3344...", + "run_id_hex": "deadbeef", + "limits": { + "cpu_ms": 12, + "wall_ms": 20, + "max_rss_kib": 1024, + "io_reads": 1, + "io_writes": 0 + }, + "logs": [ + {"kind": 1, "log_ref": "5566...", "sha256_hex": "aabbcc"} + ], + "determinism_level": 2, + "rng_seed_hex": "010203", + "signature_hex": "bead" + } +} +``` + +Federation records example: + +```bash +curl --unix-socket amduatd.sock \ + 'http://localhost/v1/fed/records?domain_id=1&from_logseq=0&limit=256' +``` + +```json +{ + "domain_id": 1, + "snapshot_id": 42, + "log_prefix": 1234, + "next_logseq": 120, + "records": [ + { + "domain_id": 1, + "type": 0, + "ref": "ab12...", + "logseq": 100, + "snapshot_id": 42, + "log_prefix": 1234, + "visibility": 1, + "has_source": false, + "source_domain": 0 + } + ] +} +``` + +Response example: + +```json +{ + "result_ref": "aa11...", + "trace_ref": "bb22...", + "receipt_ref": "cc33...", + "output_refs": ["dd44..."], + "status": "OK" +} +``` ## Notes diff --git a/docs/federation-coordinator.md b/docs/federation-coordinator.md new file mode 100644 index 0000000..bfbd41f --- /dev/null +++ b/docs/federation-coordinator.md @@ -0,0 +1,355 @@ +# Federation Coordinator Middle Layer Spec + +This document specifies the middle-layer coordinator that orchestrates federation +above the core C substrate. It remains transport- and policy-aware while keeping +core semantics unchanged. + +## Scope and Goals + +- Maintain per-domain replay bounds and an admitted set. +- Fetch and ingest published records from remotes. +- Build federation views via core APIs. +- Resolve missing bytes by fetching artifacts into a cache store. +- Keep storage layout private (no extents or blocks exposed). +- Align with tier1 federation semantics and replay determinism. +- Federation view is the union of admitted domains regardless of topology. + +Non-goals: +- Re-implement core semantics in this layer. +- Introduce a single global snapshot ID (federation is per-domain). + +## Core Dependencies + +The coordinator calls into vendor core APIs (`vendor/amduat/include/amduat/fed/*`) +and MUST stay aligned with their signatures: + +- `amduat_fed_registry_*` (persist per-domain admission state) +- `amduat_fed_ingest_validate` (record validation + conflict detection) +- `amduat_fed_replay_build` (deterministic replay per domain) +- `amduat_fed_view_build` + `amduat_fed_resolve` (build view and resolve ref-only) + +Core expects per-domain replay bounds `{domain_id, snapshot_id, log_prefix}` and +does not handle transport, auth, caching policy, or remote fetch. + +Alignment note: the daemon-layer API in this repo still needs updates to match +current vendor core types and signatures. This spec reflects vendor headers as +the source of truth. + +Tier1 alignment (normative): +- `ASL/FEDERATION/1` +- `ASL/FEDERATION-REPLAY/1` +- `ASL/DOMAIN-MODEL/1` +- `ASL/POLICY-HASH/1` +- `ENC/ASL-CORE-INDEX/1` (canonical in `vendor/amduat/tier1/enc-asl-core-index-1.md`) + +## Data Structures + +### Registry State (per domain, persisted) +``` +struct amduat_fed_domain_state { + uint32_t domain_id; + uint64_t snapshot_id; + uint64_t log_prefix; + uint64_t last_logseq; + uint8_t admitted; + uint8_t policy_ok; + uint8_t reserved[6]; + amduat_hash_id_t policy_hash_id; + amduat_octets_t policy_hash; /* Empty when unknown; caller-owned bytes. */ +}; +``` + +Registry bytes are stored via `amduat_fed_registry_store_*` in the local ASL +store; the coordinator owns admission workflows and policy compatibility checks. + +### Admitted Set (in-memory) +``` +struct amduat_fed_admitted_set { + uint32_t *domain_ids; // sorted, unique + size_t len; +}; +``` + +The admitted set is derived from registry entries with `admitted != 0` and +`policy_ok != 0`. + +### Snapshot Vector (per view build) +``` +struct amduat_fed_snapshot_vector { + amduat_fed_view_bounds_t *bounds; + size_t len; + uint64_t vector_epoch; +}; +``` + +### Record Staging (per fetch batch) +``` +struct amduat_fed_record { + amduat_fed_record_meta_t meta; + amduat_fed_record_id_t id; + uint64_t logseq; + uint64_t snapshot_id; + uint64_t log_prefix; +}; +``` + +Records MUST carry the fields required by `ASL/FEDERATION-REPLAY/1`, and replay +ordering MUST be deterministic (sort by `logseq`, then canonical identity). +Record metadata includes visibility and optional cross-domain source identity. + +### View and Policy Denies +``` +struct amduat_fed_view_bounds { + uint32_t domain_id; + uint64_t snapshot_id; + uint64_t log_prefix; +}; + +struct amduat_fed_policy_deny { + amduat_fed_record_id_t id; + uint32_t reason_code; +}; +``` + +### Fetch Backlog / Retry State +``` +struct amduat_fed_fetch_state { + uint32_t domain_id; + uint64_t next_snapshot_id; + uint64_t next_log_prefix; + uint64_t next_logseq; + uint64_t backoff_ms; + uint64_t last_attempt_ms; + uint32_t consecutive_failures; +}; +``` + +### Cache Store Metadata (optional) +``` +struct amduat_fed_cache_policy { + bool enabled; + uint64_t max_bytes; + uint64_t used_bytes; + uint32_t ttl_seconds; + uint32_t prefetch_depth; +}; +``` + +## Coordinator Interfaces + +### Configuration +``` +struct amduat_fed_coord_cfg { + const char *registry_path; + amduat_asl_store_t *authoritative_store; + amduat_asl_store_t *cache_store; // optional + amduat_asl_store_t *session_store; // optional + amduat_fed_transport transport; + amduat_fed_cache_policy cache_policy; + amduat_fed_policy_hooks policy_hooks; +}; +``` + +### Lifecycle and Operations +``` +int amduat_fed_coord_open( + const struct amduat_fed_coord_cfg *cfg, + struct amduat_fed_coord **out); + +int amduat_fed_coord_close(struct amduat_fed_coord *c); + +int amduat_fed_coord_load_registry(struct amduat_fed_coord *c); + +int amduat_fed_coord_set_admitted( + struct amduat_fed_coord *c, + uint32_t domain_id, + bool admitted); + +int amduat_fed_coord_tick(struct amduat_fed_coord *c, uint64_t now_ms); + +int amduat_fed_coord_resolve( + struct amduat_fed_coord *c, + amduat_reference_t ref, + amduat_artifact_t *out); +``` + +## API Status + +Planned coordinator surface (not yet implemented): +- `amduat_fed_coord_open` +- `amduat_fed_coord_close` +- `amduat_fed_coord_load_registry` +- `amduat_fed_coord_set_admitted` +- `amduat_fed_coord_tick` +- `amduat_fed_coord_resolve` + +Implemented in core (already available): +- `amduat_fed_registry_*` +- `amduat_fed_ingest_validate` +- `amduat_fed_replay_build` +- `amduat_fed_view_build` +- `amduat_fed_resolve` + +## Transport Abstraction + +Minimal interface that hides protocol, auth, and topology: +``` +struct amduat_fed_transport { + int (*get_records)( + void *ctx, uint32_t domain_id, + uint64_t snapshot_id, uint64_t log_prefix, + uint64_t from_logseq, + amduat_fed_record_iter *out_iter); + + int (*get_artifact)( + void *ctx, amduat_reference_t ref, + amduat_sink *out_sink); +}; +``` + +Transport MUST return records that can be validated with `amduat_fed_record_validate` +and MUST provide all fields required by `ASL/FEDERATION-REPLAY/1`. +Transport MUST NOT surface internal-only records from foreign domains. + +## Storage and Encodings + +- The coordinator stores records/artifacts via ASL store APIs and does not touch + segment layouts or extents directly. +- Federation metadata in index records is encoded by core per + `ENC/ASL-CORE-INDEX/1`; the coordinator must not override it. +- Cache store semantics are best-effort and do not affect authoritative state. + +## Policies + +- Admission is per-domain and controlled via registry entries and `policy_ok`. +- Policy compatibility uses `policy_hash_id` + `policy_hash` (ASL/POLICY-HASH/1). +- `policy_ok` is computed during admission by comparing local policy hash to the + remote domain's published policy hash. +- Admission and policy compatibility MUST be enforced before any foreign state is + admitted into the federation view. +- Per-record filtering, if used, MUST be deterministic and SHOULD be expressed + as policy denies passed to `amduat_fed_view_build` rather than by dropping + records before validation. +- Cache write policy is middle-layer only (fetch-on-miss, optional prefetch). +- Eviction is local (LRU or segmented queues) and must not leak layout. +- Conflict policy: reject on identity collision with differing metadata and keep + bounds stable until operator intervention (ASL/FEDERATION-REPLAY/1). + +## Sequencing and Consistency + +- Deterministic views require a stable snapshot vector per build. +- Bounds advance only after successful ingest; they never move backwards. +- Before validation, records are ordered by `(logseq, canonical identity)` and + filtered to `logseq <= log_prefix`. +- Tombstones and shadowing apply only within the source domain. +- Use `vector_epoch` to swap snapshot vectors atomically after a build. +- Persist registry updates atomically via `amduat_fed_registry_store_put` before + swapping the snapshot vector. +- If a remote retracts or regresses, keep local bounds and mark domain degraded. + +## Core Flow + +### Startup +- Load registry. +- Derive admitted set from `admitted != 0` and `policy_ok != 0`. +- Build initial snapshot vector. + +### Periodic Tick +- For each admitted domain, fetch records up to bound and validate. +- Update `last_logseq`, and advance bounds if remote snapshot moves forward. +- Build federation view using `amduat_fed_view_build` over the staged records, + with optional policy denies derived from coordinator hooks. +- Swap snapshot vector atomically after registry updates. + +### Resolve Loop +- Call `amduat_fed_resolve` against the latest built `amduat_fed_view_t`. +- If `AMDUAT_FED_RESOLVE_FOUND_REMOTE_NO_BYTES`, fetch bytes via transport into cache store. +- Retry resolve after cache write completes. + +## Example Tick Pseudocode +``` +int amduat_fed_coord_tick(struct amduat_fed_coord *c, uint64_t now_ms) { + amduat_fed_snapshot_vector vec = build_snapshot_vector(c); + clear(staged_records_all); + build_policy_denies(&policy_denies, &policy_denies_len); + + for each domain in vec.bounds: + bound = &vec.bounds[i]; + if !admitted(bound->domain_id) continue; + if backoff_active(bound->domain_id, now_ms) continue; + + clear(staged_records); + iter = transport.get_records( + bound->domain_id, + bound->snapshot_id, + bound->log_prefix, + state.next_logseq); + while iter.next(record): + if !amduat_fed_record_validate(&record): + mark_domain_error(bound->domain_id); + break; + append(staged_records, record); + + sort_by_logseq_then_id(staged_records); + clamp_to_log_prefix(staged_records, bound->log_prefix); + + rc = amduat_fed_ingest_validate( + staged_records, staged_len, &err_index, &conflict_index); + if rc == AMDUAT_FED_INGEST_ERR_CONFLICT: + mark_domain_error(bound->domain_id); + continue; + if rc == AMDUAT_FED_INGEST_ERR_INVALID: + mark_domain_error(bound->domain_id); + continue; + + update_registry_bounds(bound->domain_id, state); + append_all(staged_records_all, staged_records); + + amduat_fed_view_build( + staged_records_all, staged_len_all, + local_domain_id, vec.bounds, vec.len, + policy_denies, policy_denies_len, + &view); + swap_snapshot_vector(c, build_snapshot_vector(c)); + return 0; +} +``` + +## Example Resolve Pseudocode +``` +int amduat_fed_coord_resolve( + struct amduat_fed_coord *c, + amduat_reference_t ref, + amduat_artifact_t *out) { + view = c->last_view; + rc = amduat_fed_resolve(&view, c->authoritative_store, ref, out); + if (rc == AMDUAT_FED_RESOLVE_FOUND_REMOTE_NO_BYTES && c->cache_store) { + rc = transport.get_artifact(ref, sink_for_cache(c->cache_store)); + if (rc == 0) { + rc = amduat_fed_resolve(&view, c->authoritative_store, ref, out); + } + } + return rc; +} +``` + +## Coordinator Wiring Example + +``` +amduat_fed_transport_unix_t unix_transport; +amduat_fed_coord_cfg_t cfg; +amduat_fed_coord_t *coord = NULL; + +amduat_fed_transport_unix_init(&unix_transport, "amduatd.sock"); +memset(&cfg, 0, sizeof(cfg)); +cfg.local_domain_id = 1; +cfg.authoritative_store = &store; +cfg.cache_store = &cache_store; +cfg.transport = amduat_fed_transport_unix_ops(&unix_transport); + +if (amduat_fed_coord_open(&cfg, &coord) == AMDUAT_FED_COORD_OK) { + amduat_fed_coord_tick(coord, now_ms); + amduat_fed_coord_resolve(coord, some_ref, &artifact); + amduat_fed_coord_close(coord); +} +``` diff --git a/federation/coord.c b/federation/coord.c new file mode 100644 index 0000000..be18915 --- /dev/null +++ b/federation/coord.c @@ -0,0 +1,584 @@ +#include "federation/coord.h" + +#include "amduat/fed/ingest.h" +#include "amduat/asl/artifact_io.h" + +#include +#include + +struct amduat_fed_coord { + amduat_fed_coord_cfg_t cfg; + amduat_fed_registry_value_t registry; + bool registry_loaded; + amduat_fed_view_t last_view; + bool has_view; + uint64_t last_tick_ms; +}; + +static bool amduat_fed_coord_has_registry_ref(amduat_reference_t ref) { + return ref.hash_id != 0 && ref.digest.data != NULL && ref.digest.len != 0; +} + +static int amduat_fed_coord_ref_cmp(amduat_reference_t a, amduat_reference_t b) { + size_t min_len; + int cmp; + + if (a.hash_id != b.hash_id) { + return (int)a.hash_id - (int)b.hash_id; + } + if (a.digest.len != b.digest.len) { + return a.digest.len < b.digest.len ? -1 : 1; + } + min_len = a.digest.len; + if (min_len == 0) { + return 0; + } + cmp = memcmp(a.digest.data, b.digest.data, min_len); + if (cmp != 0) { + return cmp; + } + return 0; +} + +static int amduat_fed_coord_record_cmp(const void *lhs, const void *rhs) { + const amduat_fed_record_t *a = (const amduat_fed_record_t *)lhs; + const amduat_fed_record_t *b = (const amduat_fed_record_t *)rhs; + + if (a->logseq != b->logseq) { + return a->logseq < b->logseq ? -1 : 1; + } + if (a->id.type != b->id.type) { + return (int)a->id.type - (int)b->id.type; + } + return amduat_fed_coord_ref_cmp(a->id.ref, b->id.ref); +} + +static bool amduat_fed_coord_record_clone(const amduat_fed_record_t *src, + amduat_fed_record_t *out) { + if (src == NULL || out == NULL) { + return false; + } + *out = *src; + if (!amduat_reference_clone(src->id.ref, &out->id.ref)) { + return false; + } + return true; +} + +static void amduat_fed_coord_record_free(amduat_fed_record_t *rec) { + if (rec == NULL) { + return; + } + amduat_reference_free(&rec->id.ref); +} + +static void amduat_fed_coord_free_batch(amduat_fed_coord_t *coord, + amduat_fed_record_t *batch, + size_t batch_len) { + if (coord == NULL || batch == NULL) { + return; + } + if (coord->cfg.transport.free_records != NULL) { + coord->cfg.transport.free_records(coord->cfg.transport.ctx, + batch, + batch_len); + } +} + +static amduat_fed_domain_state_t *amduat_fed_coord_find_state( + amduat_fed_registry_value_t *registry, + uint32_t domain_id) { + size_t i; + + if (registry == NULL) { + return NULL; + } + for (i = 0; i < registry->len; ++i) { + if (registry->states[i].domain_id == domain_id) { + return ®istry->states[i]; + } + } + return NULL; +} + +static bool amduat_fed_coord_records_push(amduat_fed_record_t **records, + size_t *len, + size_t *cap, + amduat_fed_record_t value) { + amduat_fed_record_t *next; + size_t next_cap; + + if (records == NULL || len == NULL || cap == NULL) { + return false; + } + if (*len == *cap) { + next_cap = *cap != 0 ? *cap * 2u : 64u; + next = (amduat_fed_record_t *)realloc(*records, + next_cap * sizeof(*next)); + if (next == NULL) { + return false; + } + *records = next; + *cap = next_cap; + } + (*records)[(*len)++] = value; + return true; +} + +static bool amduat_fed_coord_denies_push(amduat_fed_policy_deny_t **denies, + size_t *len, + size_t *cap, + amduat_fed_policy_deny_t value) { + amduat_fed_policy_deny_t *next; + size_t next_cap; + + if (denies == NULL || len == NULL || cap == NULL) { + return false; + } + if (*len == *cap) { + next_cap = *cap != 0 ? *cap * 2u : 32u; + next = (amduat_fed_policy_deny_t *)realloc(*denies, + next_cap * sizeof(*next)); + if (next == NULL) { + return false; + } + *denies = next; + *cap = next_cap; + } + (*denies)[(*len)++] = value; + return true; +} + +amduat_fed_coord_error_t amduat_fed_coord_open( + const amduat_fed_coord_cfg_t *cfg, + amduat_fed_coord_t **out_coord) { + amduat_fed_coord_t *coord = NULL; + + if (out_coord == NULL) { + return AMDUAT_FED_COORD_ERR_INVALID; + } + *out_coord = NULL; + if (cfg == NULL || cfg->authoritative_store == NULL) { + return AMDUAT_FED_COORD_ERR_INVALID; + } + + coord = (amduat_fed_coord_t *)calloc(1, sizeof(*coord)); + if (coord == NULL) { + return AMDUAT_FED_COORD_ERR_OOM; + } + coord->cfg = *cfg; + if (amduat_fed_coord_has_registry_ref(cfg->registry_ref)) { + if (!amduat_reference_clone(cfg->registry_ref, &coord->cfg.registry_ref)) { + free(coord); + return AMDUAT_FED_COORD_ERR_OOM; + } + } else { + coord->cfg.registry_ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + } + amduat_fed_registry_value_init(&coord->registry, NULL, 0); + coord->registry_loaded = false; + memset(&coord->last_view, 0, sizeof(coord->last_view)); + coord->has_view = false; + coord->last_tick_ms = 0; + *out_coord = coord; + return AMDUAT_FED_COORD_OK; +} + +amduat_fed_coord_error_t amduat_fed_coord_close(amduat_fed_coord_t *coord) { + if (coord == NULL) { + return AMDUAT_FED_COORD_ERR_INVALID; + } + if (coord->has_view) { + amduat_fed_view_free(&coord->last_view); + } + amduat_fed_registry_value_free(&coord->registry); + amduat_reference_free(&coord->cfg.registry_ref); + free(coord); + return AMDUAT_FED_COORD_OK; +} + +amduat_fed_coord_error_t amduat_fed_coord_load_registry( + amduat_fed_coord_t *coord) { + amduat_fed_registry_store_t store; + amduat_fed_registry_value_t value; + amduat_asl_store_error_t store_err = AMDUAT_ASL_STORE_OK; + amduat_fed_registry_error_t err; + + if (coord == NULL) { + return AMDUAT_FED_COORD_ERR_INVALID; + } + if (!amduat_fed_coord_has_registry_ref(coord->cfg.registry_ref)) { + return AMDUAT_FED_COORD_ERR_INVALID; + } + + amduat_fed_registry_store_init(&store, coord->cfg.authoritative_store); + amduat_fed_registry_value_init(&value, NULL, 0); + err = amduat_fed_registry_store_get(&store, + coord->cfg.registry_ref, + &value, + &store_err); + if (err == AMDUAT_FED_REGISTRY_ERR_CODEC) { + amduat_fed_registry_value_free(&value); + return AMDUAT_FED_COORD_ERR_CODEC; + } + if (err != AMDUAT_FED_REGISTRY_OK || store_err != AMDUAT_ASL_STORE_OK) { + amduat_fed_registry_value_free(&value); + return AMDUAT_FED_COORD_ERR_STORE; + } + + amduat_fed_registry_value_free(&coord->registry); + coord->registry = value; + coord->registry_loaded = true; + return AMDUAT_FED_COORD_OK; +} + +amduat_fed_coord_error_t amduat_fed_coord_set_admitted( + amduat_fed_coord_t *coord, + uint32_t domain_id, + bool admitted) { + size_t i; + amduat_fed_domain_state_t state; + + if (coord == NULL) { + return AMDUAT_FED_COORD_ERR_INVALID; + } + for (i = 0; i < coord->registry.len; ++i) { + if (coord->registry.states[i].domain_id == domain_id) { + coord->registry.states[i].admitted = admitted ? 1u : 0u; + if (admitted && coord->registry.states[i].policy_ok == 0u) { + coord->registry.states[i].policy_ok = 1u; + } + return AMDUAT_FED_COORD_OK; + } + } + + memset(&state, 0, sizeof(state)); + state.domain_id = domain_id; + state.admitted = admitted ? 1u : 0u; + state.policy_ok = admitted ? 1u : 0u; + state.policy_hash_id = 0; + state.policy_hash = amduat_octets(NULL, 0u); + if (!amduat_fed_registry_value_insert(&coord->registry, state)) { + return AMDUAT_FED_COORD_ERR_OOM; + } + return AMDUAT_FED_COORD_OK; +} + +amduat_fed_coord_error_t amduat_fed_coord_tick( + amduat_fed_coord_t *coord, + uint64_t now_ms) { + amduat_fed_view_bounds_t *bounds = NULL; + size_t bounds_len = 0; + size_t bounds_cap = 0; + amduat_fed_record_t *records = NULL; + size_t records_len = 0; + size_t records_cap = 0; + amduat_fed_policy_deny_t *denies = NULL; + size_t denies_len = 0; + size_t denies_cap = 0; + size_t i; + amduat_fed_coord_error_t status = AMDUAT_FED_COORD_OK; + amduat_fed_registry_store_t reg_store; + amduat_fed_registry_error_t reg_err; + amduat_reference_t new_ref; + amduat_asl_store_error_t store_err = AMDUAT_ASL_STORE_OK; + bool registry_dirty = false; + + if (coord == NULL) { + return AMDUAT_FED_COORD_ERR_INVALID; + } + coord->last_tick_ms = now_ms; + + if (!coord->registry_loaded) { + if (amduat_fed_coord_has_registry_ref(coord->cfg.registry_ref)) { + status = amduat_fed_coord_load_registry(coord); + if (status != AMDUAT_FED_COORD_OK) { + return status; + } + } else { + coord->registry_loaded = true; + } + } + + for (i = 0; i < coord->registry.len; ++i) { + const amduat_fed_domain_state_t *state = &coord->registry.states[i]; + bool policy_ok = state->policy_ok != 0u || + amduat_octets_is_empty(state->policy_hash); + if (state->admitted == 0u || !policy_ok) { + continue; + } + if (bounds_len == bounds_cap) { + size_t next_cap = bounds_cap != 0 ? bounds_cap * 2u : 8u; + amduat_fed_view_bounds_t *next = + (amduat_fed_view_bounds_t *)realloc( + bounds, next_cap * sizeof(*next)); + if (next == NULL) { + status = AMDUAT_FED_COORD_ERR_OOM; + goto tick_cleanup; + } + bounds = next; + bounds_cap = next_cap; + } + bounds[bounds_len].domain_id = state->domain_id; + bounds[bounds_len].snapshot_id = state->snapshot_id; + bounds[bounds_len].log_prefix = state->log_prefix; + bounds_len++; + } + + for (i = 0; i < bounds_len; ++i) { + amduat_fed_view_bounds_t *bound = &bounds[i]; + amduat_fed_domain_state_t *state = + amduat_fed_coord_find_state(&coord->registry, bound->domain_id); + amduat_fed_record_t *batch = NULL; + size_t batch_len = 0; + size_t j; + uint64_t max_logseq = 0; + int transport_rc; + + if (state == NULL) { + status = AMDUAT_FED_COORD_ERR_INVALID; + goto tick_cleanup; + } + if (coord->cfg.transport.get_records == NULL) { + status = AMDUAT_FED_COORD_ERR_INVALID; + goto tick_cleanup; + } + transport_rc = coord->cfg.transport.get_records( + coord->cfg.transport.ctx, + bound->domain_id, + bound->snapshot_id, + bound->log_prefix, + state->last_logseq + 1u, + &batch, + &batch_len); + if (transport_rc != 0) { + status = AMDUAT_FED_COORD_ERR_STORE; + goto tick_cleanup; + } + if (batch == NULL && batch_len != 0) { + status = AMDUAT_FED_COORD_ERR_INVALID; + goto tick_cleanup; + } + + for (j = 0; j < batch_len; ++j) { + amduat_fed_record_t cloned; + amduat_fed_policy_deny_t deny; + amduat_reference_t deny_ref; + bool allowed = true; + bool ok = amduat_fed_coord_record_clone(&batch[j], &cloned); + if (!ok) { + status = AMDUAT_FED_COORD_ERR_OOM; + goto tick_cleanup; + } + if (cloned.logseq > bound->log_prefix) { + amduat_fed_coord_record_free(&cloned); + continue; + } + if (!amduat_fed_record_validate(&cloned)) { + amduat_fed_coord_record_free(&cloned); + status = AMDUAT_FED_COORD_ERR_INVALID; + amduat_fed_coord_free_batch(coord, batch, batch_len); + goto tick_cleanup; + } + if (coord->cfg.policy_hooks.record_allowed != NULL) { + memset(&deny, 0, sizeof(deny)); + allowed = coord->cfg.policy_hooks.record_allowed( + coord->cfg.policy_hooks.ctx, &cloned, &deny); + if (!allowed) { + if (deny.id.ref.digest.data == NULL && deny.id.ref.hash_id == 0u) { + deny.id = cloned.id; + } + deny_ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + if (!amduat_reference_clone(deny.id.ref, &deny_ref)) { + amduat_fed_coord_record_free(&cloned); + status = AMDUAT_FED_COORD_ERR_OOM; + amduat_fed_coord_free_batch(coord, batch, batch_len); + goto tick_cleanup; + } + deny.id.ref = deny_ref; + if (!amduat_fed_coord_denies_push(&denies, + &denies_len, + &denies_cap, + deny)) { + amduat_reference_free(&deny.id.ref); + amduat_fed_coord_record_free(&cloned); + status = AMDUAT_FED_COORD_ERR_OOM; + amduat_fed_coord_free_batch(coord, batch, batch_len); + goto tick_cleanup; + } + } + } + if (!amduat_fed_coord_records_push(&records, + &records_len, + &records_cap, + cloned)) { + amduat_fed_coord_record_free(&cloned); + status = AMDUAT_FED_COORD_ERR_OOM; + amduat_fed_coord_free_batch(coord, batch, batch_len); + goto tick_cleanup; + } + if (cloned.logseq > max_logseq) { + max_logseq = cloned.logseq; + } + } + + amduat_fed_coord_free_batch(coord, batch, batch_len); + + if (max_logseq > state->last_logseq) { + state->last_logseq = max_logseq; + registry_dirty = true; + } + } + + if (records_len != 0) { + size_t err_index = 0; + size_t conflict_index = 0; + amduat_fed_ingest_error_t ingest_rc; + qsort(records, records_len, sizeof(*records), amduat_fed_coord_record_cmp); + ingest_rc = amduat_fed_ingest_validate(records, + records_len, + &err_index, + &conflict_index); + if (ingest_rc != AMDUAT_FED_INGEST_OK) { + status = AMDUAT_FED_COORD_ERR_INVALID; + goto tick_cleanup; + } + } + + if (coord->has_view) { + amduat_fed_view_free(&coord->last_view); + coord->has_view = false; + } + + if (bounds_len != 0) { + amduat_fed_view_error_t view_rc; + view_rc = amduat_fed_view_build(records, + records_len, + coord->cfg.local_domain_id, + bounds, + bounds_len, + denies, + denies_len, + &coord->last_view); + if (view_rc != AMDUAT_FED_VIEW_OK) { + status = AMDUAT_FED_COORD_ERR_INVALID; + goto tick_cleanup; + } + coord->has_view = true; + } + + if (registry_dirty) { + amduat_fed_registry_store_init(®_store, coord->cfg.authoritative_store); + reg_err = amduat_fed_registry_store_put(®_store, + &coord->registry, + &new_ref, + &store_err); + if (reg_err != AMDUAT_FED_REGISTRY_OK || + store_err != AMDUAT_ASL_STORE_OK) { + status = AMDUAT_FED_COORD_ERR_STORE; + goto tick_cleanup; + } + amduat_reference_free(&coord->cfg.registry_ref); + coord->cfg.registry_ref = new_ref; + } + +tick_cleanup: + if (bounds != NULL) { + free(bounds); + } + if (records != NULL) { + for (i = 0; i < records_len; ++i) { + amduat_fed_coord_record_free(&records[i]); + } + free(records); + } + if (denies != NULL) { + for (i = 0; i < denies_len; ++i) { + amduat_reference_free(&denies[i].id.ref); + } + free(denies); + } + return status; +} + +amduat_fed_coord_error_t amduat_fed_coord_resolve( + amduat_fed_coord_t *coord, + amduat_reference_t ref, + amduat_artifact_t *out_artifact) { + amduat_fed_resolve_error_t rc; + + if (coord == NULL || out_artifact == NULL) { + return AMDUAT_FED_COORD_ERR_INVALID; + } + if (!coord->has_view) { + return AMDUAT_FED_COORD_ERR_INVALID; + } + + rc = amduat_fed_resolve(&coord->last_view, + coord->cfg.authoritative_store, + ref, + out_artifact); + if (rc == AMDUAT_FED_RESOLVE_OK) { + return AMDUAT_FED_COORD_OK; + } + if (rc == AMDUAT_FED_RESOLVE_FOUND_REMOTE_NO_BYTES && + coord->cfg.cache_store != NULL && + coord->cfg.transport.get_artifact != NULL) { + amduat_octets_t bytes = amduat_octets(NULL, 0u); + amduat_artifact_t artifact; + amduat_asl_store_error_t store_err; + int fetch_rc = coord->cfg.transport.get_artifact( + coord->cfg.transport.ctx, ref, &bytes); + if (fetch_rc != 0) { + amduat_octets_free(&bytes); + return AMDUAT_FED_COORD_ERR_STORE; + } + if (!amduat_asl_artifact_from_bytes(bytes, + AMDUAT_ASL_IO_RAW, + false, + amduat_type_tag(0u), + &artifact)) { + amduat_octets_free(&bytes); + return AMDUAT_FED_COORD_ERR_INVALID; + } + store_err = amduat_asl_store_put(coord->cfg.cache_store, + artifact, + &ref); + amduat_asl_artifact_free(&artifact); + amduat_octets_free(&bytes); + if (store_err != AMDUAT_ASL_STORE_OK) { + return AMDUAT_FED_COORD_ERR_STORE; + } + rc = amduat_fed_resolve(&coord->last_view, + coord->cfg.authoritative_store, + ref, + out_artifact); + if (rc == AMDUAT_FED_RESOLVE_OK) { + return AMDUAT_FED_COORD_OK; + } + } + if (rc == AMDUAT_FED_RESOLVE_POLICY_DENIED) { + return AMDUAT_FED_COORD_ERR_INVALID; + } + if (rc == AMDUAT_FED_RESOLVE_NOT_FOUND) { + return AMDUAT_FED_COORD_ERR_INVALID; + } + return AMDUAT_FED_COORD_ERR_STORE; +} + +void amduat_fed_coord_get_status(const amduat_fed_coord_t *coord, + amduat_fed_coord_status_t *out_status) { + if (out_status == NULL) { + return; + } + memset(out_status, 0, sizeof(*out_status)); + if (coord == NULL) { + return; + } + out_status->domain_id = coord->cfg.local_domain_id; + if (!amduat_reference_clone(coord->cfg.registry_ref, + &out_status->registry_ref)) { + out_status->registry_ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + } + out_status->last_tick_ms = coord->last_tick_ms; +} diff --git a/federation/coord.h b/federation/coord.h new file mode 100644 index 0000000..70f3cc1 --- /dev/null +++ b/federation/coord.h @@ -0,0 +1,95 @@ +#ifndef AMDUAT_FED_COORD_H +#define AMDUAT_FED_COORD_H + +#include "amduat/asl/core.h" +#include "amduat/asl/store.h" +#include "amduat/fed/registry.h" +#include "amduat/fed/view.h" + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct amduat_fed_coord amduat_fed_coord_t; + +typedef enum { + AMDUAT_FED_COORD_OK = 0, + AMDUAT_FED_COORD_ERR_INVALID = 1, + AMDUAT_FED_COORD_ERR_OOM = 2, + AMDUAT_FED_COORD_ERR_STORE = 3, + AMDUAT_FED_COORD_ERR_CODEC = 4, + AMDUAT_FED_COORD_ERR_NOT_IMPLEMENTED = 5 +} amduat_fed_coord_error_t; + +typedef struct { + void *ctx; + int (*get_records)(void *ctx, + uint32_t domain_id, + uint64_t snapshot_id, + uint64_t log_prefix, + uint64_t from_logseq, + amduat_fed_record_t **out_records, + size_t *out_len); + void (*free_records)(void *ctx, amduat_fed_record_t *records, size_t len); + int (*get_artifact)(void *ctx, + amduat_reference_t ref, + amduat_octets_t *out_bytes); +} amduat_fed_transport_t; + +typedef struct { + void *ctx; + bool (*record_allowed)(void *ctx, + const amduat_fed_record_t *record, + amduat_fed_policy_deny_t *out_deny); +} amduat_fed_policy_hooks_t; + +typedef struct { + uint32_t local_domain_id; + amduat_asl_store_t *authoritative_store; + amduat_asl_store_t *cache_store; + amduat_reference_t registry_ref; + amduat_fed_transport_t transport; + amduat_fed_policy_hooks_t policy_hooks; +} amduat_fed_coord_cfg_t; + +typedef struct { + uint32_t domain_id; + amduat_reference_t registry_ref; + uint64_t last_tick_ms; +} amduat_fed_coord_status_t; + +amduat_fed_coord_error_t amduat_fed_coord_open( + const amduat_fed_coord_cfg_t *cfg, + amduat_fed_coord_t **out_coord); + +amduat_fed_coord_error_t amduat_fed_coord_close(amduat_fed_coord_t *coord); + +amduat_fed_coord_error_t amduat_fed_coord_load_registry( + amduat_fed_coord_t *coord); + +amduat_fed_coord_error_t amduat_fed_coord_set_admitted( + amduat_fed_coord_t *coord, + uint32_t domain_id, + bool admitted); + +amduat_fed_coord_error_t amduat_fed_coord_tick( + amduat_fed_coord_t *coord, + uint64_t now_ms); + +amduat_fed_coord_error_t amduat_fed_coord_resolve( + amduat_fed_coord_t *coord, + amduat_reference_t ref, + amduat_artifact_t *out_artifact); + +void amduat_fed_coord_get_status(const amduat_fed_coord_t *coord, + amduat_fed_coord_status_t *out_status); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* AMDUAT_FED_COORD_H */ diff --git a/federation/transport_stub.c b/federation/transport_stub.c new file mode 100644 index 0000000..62e1327 --- /dev/null +++ b/federation/transport_stub.c @@ -0,0 +1,72 @@ +#include "federation/transport_stub.h" + +#include +#include + +static int amduat_fed_transport_stub_get_records(void *ctx, + uint32_t domain_id, + uint64_t snapshot_id, + uint64_t log_prefix, + uint64_t from_logseq, + amduat_fed_record_t **out_records, + size_t *out_len) { + (void)ctx; + (void)domain_id; + (void)snapshot_id; + (void)log_prefix; + (void)from_logseq; + if (out_records != NULL) { + *out_records = NULL; + } + if (out_len != NULL) { + *out_len = 0; + } + return 0; +} + +static void amduat_fed_transport_stub_free_records(void *ctx, + amduat_fed_record_t *records, + size_t len) { + (void)ctx; + (void)len; + free(records); +} + +static int amduat_fed_transport_stub_get_artifact(void *ctx, + amduat_reference_t ref, + amduat_octets_t *out_bytes) { + amduat_fed_transport_stub_t *stub = (amduat_fed_transport_stub_t *)ctx; + (void)ref; + if (out_bytes == NULL || stub == NULL) { + return -1; + } + if (!amduat_octets_clone(stub->artifact_bytes, out_bytes)) { + return -1; + } + return 0; +} + +void amduat_fed_transport_stub_init(amduat_fed_transport_stub_t *stub) { + if (stub == NULL) { + return; + } + stub->artifact_bytes = amduat_octets(NULL, 0u); +} + +amduat_fed_transport_t amduat_fed_transport_stub_ops( + amduat_fed_transport_stub_t *stub) { + amduat_fed_transport_t ops; + memset(&ops, 0, sizeof(ops)); + ops.ctx = stub; + ops.get_records = amduat_fed_transport_stub_get_records; + ops.free_records = amduat_fed_transport_stub_free_records; + ops.get_artifact = amduat_fed_transport_stub_get_artifact; + return ops; +} + +void amduat_fed_transport_stub_free(amduat_fed_transport_stub_t *stub) { + if (stub == NULL) { + return; + } + amduat_octets_free(&stub->artifact_bytes); +} diff --git a/federation/transport_stub.h b/federation/transport_stub.h new file mode 100644 index 0000000..534fff3 --- /dev/null +++ b/federation/transport_stub.h @@ -0,0 +1,25 @@ +#ifndef AMDUAT_FED_TRANSPORT_STUB_H +#define AMDUAT_FED_TRANSPORT_STUB_H + +#include "federation/coord.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + amduat_octets_t artifact_bytes; +} amduat_fed_transport_stub_t; + +void amduat_fed_transport_stub_init(amduat_fed_transport_stub_t *stub); + +amduat_fed_transport_t amduat_fed_transport_stub_ops( + amduat_fed_transport_stub_t *stub); + +void amduat_fed_transport_stub_free(amduat_fed_transport_stub_t *stub); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* AMDUAT_FED_TRANSPORT_STUB_H */ diff --git a/federation/transport_unix.c b/federation/transport_unix.c new file mode 100644 index 0000000..4864a42 --- /dev/null +++ b/federation/transport_unix.c @@ -0,0 +1,852 @@ +#define _POSIX_C_SOURCE 200809L + +#include "federation/transport_unix.h" + +#include "amduat/asl/ref_text.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +static const char *amduat_fed_json_skip_ws(const char *p, const char *end) { + while (p < end) { + if (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t') { + p++; + continue; + } + break; + } + return p; +} + +static bool amduat_fed_json_expect(const char **p, const char *end, char c) { + const char *cur; + + if (p == NULL || *p == NULL) { + return false; + } + cur = amduat_fed_json_skip_ws(*p, end); + if (cur >= end || *cur != c) { + return false; + } + *p = cur + 1; + return true; +} + +static bool amduat_fed_json_parse_string_noesc(const char **p, + const char *end, + const char **out_str, + size_t *out_len) { + const char *cur; + const char *start; + + if (p == NULL || *p == NULL || out_str == NULL || out_len == NULL) { + return false; + } + cur = amduat_fed_json_skip_ws(*p, end); + if (cur >= end || *cur != '"') { + return false; + } + start = ++cur; + while (cur < end && *cur != '"') { + if (*cur == '\\') { + return false; + } + cur++; + } + if (cur >= end) { + return false; + } + *out_str = start; + *out_len = (size_t)(cur - start); + *p = cur + 1; + return true; +} + +static bool amduat_fed_json_parse_u64(const char **p, + const char *end, + uint64_t *out) { + const char *cur; + char *next = NULL; + uint64_t v; + + if (p == NULL || *p == NULL || out == NULL) { + return false; + } + cur = amduat_fed_json_skip_ws(*p, end); + if (cur >= end) { + return false; + } + errno = 0; + v = (uint64_t)strtoull(cur, &next, 10); + if (errno != 0 || next == cur) { + return false; + } + *out = v; + *p = next; + return true; +} + +static bool amduat_fed_json_parse_u32(const char **p, + const char *end, + uint32_t *out) { + uint64_t tmp = 0; + if (!amduat_fed_json_parse_u64(p, end, &tmp)) { + return false; + } + if (tmp > UINT32_MAX) { + return false; + } + *out = (uint32_t)tmp; + return true; +} + +static bool amduat_fed_json_parse_bool(const char **p, + const char *end, + bool *out) { + const char *cur; + + if (p == NULL || *p == NULL || out == NULL) { + return false; + } + cur = amduat_fed_json_skip_ws(*p, end); + if (cur + 4 <= end && strncmp(cur, "true", 4) == 0) { + *out = true; + *p = cur + 4; + return true; + } + if (cur + 5 <= end && strncmp(cur, "false", 5) == 0) { + *out = false; + *p = cur + 5; + return true; + } + return false; +} + +static bool amduat_fed_json_skip_string(const char **p, const char *end) { + const char *cur; + if (p == NULL || *p == NULL) { + return false; + } + cur = amduat_fed_json_skip_ws(*p, end); + if (cur >= end || *cur != '"') { + return false; + } + cur++; + while (cur < end && *cur != '"') { + if (*cur == '\\') { + return false; + } + cur++; + } + if (cur >= end) { + return false; + } + *p = cur + 1; + return true; +} + +static bool amduat_fed_json_skip_value(const char **p, const char *end, int depth); + +static bool amduat_fed_json_skip_array(const char **p, + const char *end, + int depth) { + const char *cur; + if (!amduat_fed_json_expect(p, end, '[')) { + return false; + } + cur = amduat_fed_json_skip_ws(*p, end); + if (cur < end && *cur == ']') { + *p = cur + 1; + return true; + } + for (;;) { + if (!amduat_fed_json_skip_value(p, end, depth + 1)) { + return false; + } + cur = amduat_fed_json_skip_ws(*p, end); + if (cur >= end) { + return false; + } + if (*cur == ',') { + *p = cur + 1; + continue; + } + if (*cur == ']') { + *p = cur + 1; + return true; + } + return false; + } +} + +static bool amduat_fed_json_skip_object(const char **p, + const char *end, + int depth) { + const char *cur; + if (!amduat_fed_json_expect(p, end, '{')) { + return false; + } + cur = amduat_fed_json_skip_ws(*p, end); + if (cur < end && *cur == '}') { + *p = cur + 1; + return true; + } + for (;;) { + if (!amduat_fed_json_skip_string(p, end)) { + return false; + } + if (!amduat_fed_json_expect(p, end, ':')) { + return false; + } + if (!amduat_fed_json_skip_value(p, end, depth + 1)) { + return false; + } + cur = amduat_fed_json_skip_ws(*p, end); + if (cur >= end) { + return false; + } + if (*cur == ',') { + *p = cur + 1; + continue; + } + if (*cur == '}') { + *p = cur + 1; + return true; + } + return false; + } +} + +static bool amduat_fed_json_skip_value(const char **p, + const char *end, + int depth) { + const char *cur; + if (depth > 64) { + return false; + } + cur = amduat_fed_json_skip_ws(*p, end); + if (cur >= end) { + return false; + } + if (*cur == '"') { + return amduat_fed_json_skip_string(p, end); + } + if (*cur == '{') { + return amduat_fed_json_skip_object(p, end, depth); + } + if (*cur == '[') { + return amduat_fed_json_skip_array(p, end, depth); + } + if (strncmp(cur, "true", 4) == 0) { + *p = cur + 4; + return true; + } + if (strncmp(cur, "false", 5) == 0) { + *p = cur + 5; + return true; + } + if (strncmp(cur, "null", 4) == 0) { + *p = cur + 4; + return true; + } + if ((*cur >= '0' && *cur <= '9') || *cur == '-') { + char *next = NULL; + (void)strtoull(cur, &next, 10); + if (next == cur) { + return false; + } + *p = next; + return true; + } + return false; +} + +static bool amduat_fed_transport_parse_record(const char **p, + const char *end, + uint32_t default_domain_id, + amduat_fed_record_t *out) { + const char *key = NULL; + size_t key_len = 0; + const char *sv = NULL; + size_t sv_len = 0; + uint32_t domain_id = default_domain_id; + uint32_t type = 0; + uint32_t visibility = 0; + uint32_t source_domain = 0; + bool has_domain = false; + bool has_type = false; + bool has_ref = false; + bool has_logseq = false; + bool has_snapshot = false; + bool has_log_prefix = false; + bool has_source = false; + amduat_reference_t ref; + + memset(&ref, 0, sizeof(ref)); + if (!amduat_fed_json_expect(p, end, '{')) { + return false; + } + for (;;) { + const char *cur = amduat_fed_json_skip_ws(*p, end); + if (cur < end && *cur == '}') { + *p = cur + 1; + break; + } + if (!amduat_fed_json_parse_string_noesc(p, end, &key, &key_len) || + !amduat_fed_json_expect(p, end, ':')) { + return false; + } + if (key_len == strlen("domain_id") && + memcmp(key, "domain_id", key_len) == 0) { + if (!amduat_fed_json_parse_u32(p, end, &domain_id)) { + return false; + } + has_domain = true; + } else if (key_len == strlen("type") && + memcmp(key, "type", key_len) == 0) { + if (!amduat_fed_json_parse_u32(p, end, &type)) { + return false; + } + has_type = true; + } else if (key_len == strlen("ref") && + memcmp(key, "ref", key_len) == 0) { + if (!amduat_fed_json_parse_string_noesc(p, end, &sv, &sv_len)) { + return false; + } + { + char *tmp = (char *)malloc(sv_len + 1u); + if (tmp == NULL) { + return false; + } + memcpy(tmp, sv, sv_len); + tmp[sv_len] = '\0'; + if (!amduat_asl_ref_decode_hex(tmp, &ref)) { + free(tmp); + return false; + } + free(tmp); + } + has_ref = true; + } else if (key_len == strlen("logseq") && + memcmp(key, "logseq", key_len) == 0) { + if (!amduat_fed_json_parse_u64(p, end, &out->logseq)) { + return false; + } + has_logseq = true; + } else if (key_len == strlen("snapshot_id") && + memcmp(key, "snapshot_id", key_len) == 0) { + if (!amduat_fed_json_parse_u64(p, end, &out->snapshot_id)) { + return false; + } + has_snapshot = true; + } else if (key_len == strlen("log_prefix") && + memcmp(key, "log_prefix", key_len) == 0) { + if (!amduat_fed_json_parse_u64(p, end, &out->log_prefix)) { + return false; + } + has_log_prefix = true; + } else if (key_len == strlen("visibility") && + memcmp(key, "visibility", key_len) == 0) { + if (!amduat_fed_json_parse_u32(p, end, &visibility)) { + return false; + } + } else if (key_len == strlen("has_source") && + memcmp(key, "has_source", key_len) == 0) { + bool tmp = false; + if (!amduat_fed_json_parse_bool(p, end, &tmp)) { + return false; + } + has_source = tmp; + } else if (key_len == strlen("source_domain") && + memcmp(key, "source_domain", key_len) == 0) { + if (!amduat_fed_json_parse_u32(p, end, &source_domain)) { + return false; + } + } else { + if (!amduat_fed_json_skip_value(p, end, 0)) { + return false; + } + } + { + const char *cur = amduat_fed_json_skip_ws(*p, end); + if (cur >= end) { + return false; + } + if (*cur == ',') { + *p = cur + 1; + continue; + } + if (*cur == '}') { + *p = cur + 1; + break; + } + return false; + } + } + if (!has_ref) { + return false; + } + if (!has_type || !has_logseq || !has_snapshot || !has_log_prefix) { + amduat_reference_free(&ref); + return false; + } + out->meta.domain_id = domain_id; + out->meta.visibility = (uint8_t)visibility; + out->meta.has_source = has_source ? 1u : 0u; + out->meta.source_domain = source_domain; + out->id.type = (amduat_fed_record_type_t)type; + out->id.ref = ref; + (void)has_domain; + return true; +} + +static bool amduat_fed_transport_parse_records(const char *body, + size_t body_len, + amduat_fed_record_t **out_records, + size_t *out_len) { + const char *p = body; + const char *end = body + body_len; + const char *key = NULL; + size_t key_len = 0; + uint32_t domain_id = 0; + uint64_t snapshot_id = 0; + uint64_t log_prefix = 0; + uint64_t next_logseq = 0; + bool have_domain = false; + bool have_snapshot = false; + bool have_log_prefix = false; + bool have_next = false; + amduat_fed_record_t *records = NULL; + size_t records_len = 0; + size_t records_cap = 0; + + if (out_records == NULL || out_len == NULL) { + return false; + } + *out_records = NULL; + *out_len = 0; + + if (!amduat_fed_json_expect(&p, end, '{')) { + return false; + } + for (;;) { + const char *cur = amduat_fed_json_skip_ws(p, end); + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + if (!amduat_fed_json_parse_string_noesc(&p, end, &key, &key_len) || + !amduat_fed_json_expect(&p, end, ':')) { + goto parse_fail; + } + if (key_len == strlen("domain_id") && + memcmp(key, "domain_id", key_len) == 0) { + if (!amduat_fed_json_parse_u32(&p, end, &domain_id)) { + goto parse_fail; + } + have_domain = true; + } else if (key_len == strlen("snapshot_id") && + memcmp(key, "snapshot_id", key_len) == 0) { + if (!amduat_fed_json_parse_u64(&p, end, &snapshot_id)) { + goto parse_fail; + } + have_snapshot = true; + } else if (key_len == strlen("log_prefix") && + memcmp(key, "log_prefix", key_len) == 0) { + if (!amduat_fed_json_parse_u64(&p, end, &log_prefix)) { + goto parse_fail; + } + have_log_prefix = true; + } else if (key_len == strlen("next_logseq") && + memcmp(key, "next_logseq", key_len) == 0) { + if (!amduat_fed_json_parse_u64(&p, end, &next_logseq)) { + goto parse_fail; + } + have_next = true; + } else if (key_len == strlen("records") && + memcmp(key, "records", key_len) == 0) { + if (!amduat_fed_json_expect(&p, end, '[')) { + goto parse_fail; + } + cur = amduat_fed_json_skip_ws(p, end); + if (cur < end && *cur == ']') { + p = cur + 1; + } else { + for (;;) { + amduat_fed_record_t record; + memset(&record, 0, sizeof(record)); + if (!amduat_fed_transport_parse_record(&p, end, domain_id, &record)) { + goto parse_fail; + } + if (records_len == records_cap) { + size_t next_cap = records_cap != 0 ? records_cap * 2u : 64u; + amduat_fed_record_t *next = + (amduat_fed_record_t *)realloc( + records, next_cap * sizeof(*records)); + if (next == NULL) { + amduat_reference_free(&record.id.ref); + goto parse_fail; + } + records = next; + records_cap = next_cap; + } + records[records_len++] = record; + cur = amduat_fed_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == ']') { + p = cur + 1; + break; + } + goto parse_fail; + } + } + } else { + if (!amduat_fed_json_skip_value(&p, end, 0)) { + goto parse_fail; + } + } + cur = amduat_fed_json_skip_ws(p, end); + if (cur >= end) { + goto parse_fail; + } + if (*cur == ',') { + p = cur + 1; + continue; + } + if (*cur == '}') { + p = cur + 1; + break; + } + goto parse_fail; + } + + if (!have_domain || !have_snapshot || !have_log_prefix || !have_next) { + goto parse_fail; + } + + (void)snapshot_id; + (void)log_prefix; + (void)next_logseq; + *out_records = records; + *out_len = records_len; + return true; + +parse_fail: + if (records != NULL) { + size_t i; + for (i = 0; i < records_len; ++i) { + amduat_reference_free(&records[i].id.ref); + } + free(records); + } + return false; +} + +static int amduat_fed_transport_unix_connect(const char *path) { + int fd; + struct sockaddr_un addr; + + if (path == NULL || path[0] == '\0') { + return -1; + } + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1); + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) { + close(fd); + return -1; + } + return fd; +} + +static bool amduat_fed_transport_unix_send_all(int fd, + const char *buf, + size_t len) { + size_t off = 0; + while (off < len) { + ssize_t n = write(fd, buf + off, len - off); + if (n < 0) { + if (errno == EINTR) { + continue; + } + return false; + } + off += (size_t)n; + } + return true; +} + +static bool amduat_fed_transport_unix_read_all(int fd, + uint8_t **out_buf, + size_t *out_len) { + uint8_t *buf = NULL; + size_t len = 0; + size_t cap = 0; + + if (out_buf == NULL || out_len == NULL) { + return false; + } + for (;;) { + uint8_t tmp[4096]; + ssize_t n = read(fd, tmp, sizeof(tmp)); + if (n < 0) { + if (errno == EINTR) { + continue; + } + free(buf); + return false; + } + if (n == 0) { + break; + } + if (len + (size_t)n > cap) { + size_t next_cap = cap != 0 ? cap * 2u : 8192u; + while (next_cap < len + (size_t)n) { + next_cap *= 2u; + } + { + uint8_t *next = (uint8_t *)realloc(buf, next_cap); + if (next == NULL) { + free(buf); + return false; + } + buf = next; + cap = next_cap; + } + } + memcpy(buf + len, tmp, (size_t)n); + len += (size_t)n; + } + *out_buf = buf; + *out_len = len; + return true; +} + +static bool amduat_fed_transport_unix_split_response(const uint8_t *buf, + size_t len, + const uint8_t **out_body, + size_t *out_body_len, + int *out_status) { + size_t i; + const uint8_t *body = NULL; + if (out_body == NULL || out_body_len == NULL || out_status == NULL) { + return false; + } + *out_body = NULL; + *out_body_len = 0; + *out_status = 0; + if (buf == NULL || len < 12) { + return false; + } + if (memcmp(buf, "HTTP/1.", 7) != 0) { + return false; + } + for (i = 0; i + 3 < len; ++i) { + if (buf[i] == '\r' && buf[i + 1] == '\n' && + buf[i + 2] == '\r' && buf[i + 3] == '\n') { + body = buf + i + 4u; + *out_body = body; + *out_body_len = len - (i + 4u); + break; + } + } + if (body == NULL) { + return false; + } + { + int status = 0; + if (sscanf((const char *)buf, "HTTP/1.%*c %d", &status) != 1) { + return false; + } + *out_status = status; + } + return true; +} + +static int amduat_fed_transport_unix_get_records(void *ctx, + uint32_t domain_id, + uint64_t snapshot_id, + uint64_t log_prefix, + uint64_t from_logseq, + amduat_fed_record_t **out_records, + size_t *out_len) { + amduat_fed_transport_unix_t *transport = (amduat_fed_transport_unix_t *)ctx; + char req[512]; + int fd; + uint8_t *buf = NULL; + size_t buf_len = 0; + const uint8_t *body = NULL; + size_t body_len = 0; + int status = 0; + (void)snapshot_id; + (void)log_prefix; + + if (transport == NULL || out_records == NULL || out_len == NULL) { + return -1; + } + *out_records = NULL; + *out_len = 0; + + snprintf(req, sizeof(req), + "GET /v1/fed/records?domain_id=%u&from_logseq=%llu HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: close\r\n" + "\r\n", + (unsigned int)domain_id, + (unsigned long long)from_logseq); + + fd = amduat_fed_transport_unix_connect(transport->socket_path); + if (fd < 0) { + return -1; + } + if (!amduat_fed_transport_unix_send_all(fd, req, strlen(req))) { + close(fd); + return -1; + } + if (!amduat_fed_transport_unix_read_all(fd, &buf, &buf_len)) { + close(fd); + return -1; + } + close(fd); + + if (!amduat_fed_transport_unix_split_response(buf, + buf_len, + &body, + &body_len, + &status)) { + free(buf); + return -1; + } + if (status != 200) { + free(buf); + return -1; + } + if (!amduat_fed_transport_parse_records((const char *)body, + body_len, + out_records, + out_len)) { + free(buf); + return -1; + } + free(buf); + return 0; +} + +static void amduat_fed_transport_unix_free_records(void *ctx, + amduat_fed_record_t *records, + size_t len) { + size_t i; + (void)ctx; + if (records == NULL) { + return; + } + for (i = 0; i < len; ++i) { + amduat_reference_free(&records[i].id.ref); + } + free(records); +} + +static int amduat_fed_transport_unix_get_artifact(void *ctx, + amduat_reference_t ref, + amduat_octets_t *out_bytes) { + amduat_fed_transport_unix_t *transport = (amduat_fed_transport_unix_t *)ctx; + char *ref_hex = NULL; + char req[512]; + int fd; + uint8_t *buf = NULL; + size_t buf_len = 0; + const uint8_t *body = NULL; + size_t body_len = 0; + int status = 0; + bool ok; + + if (transport == NULL || out_bytes == NULL) { + return -1; + } + *out_bytes = amduat_octets(NULL, 0u); + if (!amduat_asl_ref_encode_hex(ref, &ref_hex)) { + return -1; + } + snprintf(req, sizeof(req), + "GET /v1/fed/artifacts/%s HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: close\r\n" + "\r\n", + ref_hex); + free(ref_hex); + + fd = amduat_fed_transport_unix_connect(transport->socket_path); + if (fd < 0) { + return -1; + } + ok = amduat_fed_transport_unix_send_all(fd, req, strlen(req)); + if (!ok) { + close(fd); + return -1; + } + if (!amduat_fed_transport_unix_read_all(fd, &buf, &buf_len)) { + close(fd); + return -1; + } + close(fd); + + if (!amduat_fed_transport_unix_split_response(buf, + buf_len, + &body, + &body_len, + &status)) { + free(buf); + return -1; + } + if (status != 200) { + free(buf); + return -1; + } + if (!amduat_octets_clone(amduat_octets(body, body_len), out_bytes)) { + free(buf); + return -1; + } + free(buf); + return 0; +} + +bool amduat_fed_transport_unix_init(amduat_fed_transport_unix_t *transport, + const char *socket_path) { + if (transport == NULL || socket_path == NULL) { + return false; + } + if (strlen(socket_path) >= AMDUAT_FED_TRANSPORT_UNIX_PATH_MAX) { + return false; + } + memset(transport->socket_path, 0, sizeof(transport->socket_path)); + strncpy(transport->socket_path, socket_path, + AMDUAT_FED_TRANSPORT_UNIX_PATH_MAX - 1u); + return true; +} + +amduat_fed_transport_t amduat_fed_transport_unix_ops( + amduat_fed_transport_unix_t *transport) { + amduat_fed_transport_t ops; + memset(&ops, 0, sizeof(ops)); + ops.ctx = transport; + ops.get_records = amduat_fed_transport_unix_get_records; + ops.free_records = amduat_fed_transport_unix_free_records; + ops.get_artifact = amduat_fed_transport_unix_get_artifact; + return ops; +} diff --git a/federation/transport_unix.h b/federation/transport_unix.h new file mode 100644 index 0000000..6e47782 --- /dev/null +++ b/federation/transport_unix.h @@ -0,0 +1,26 @@ +#ifndef AMDUAT_FED_TRANSPORT_UNIX_H +#define AMDUAT_FED_TRANSPORT_UNIX_H + +#include "federation/coord.h" + +#ifdef __cplusplus +extern "C" { +#endif + +enum { AMDUAT_FED_TRANSPORT_UNIX_PATH_MAX = 1024 }; + +typedef struct { + char socket_path[AMDUAT_FED_TRANSPORT_UNIX_PATH_MAX]; +} amduat_fed_transport_unix_t; + +bool amduat_fed_transport_unix_init(amduat_fed_transport_unix_t *transport, + const char *socket_path); + +amduat_fed_transport_t amduat_fed_transport_unix_ops( + amduat_fed_transport_unix_t *transport); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* AMDUAT_FED_TRANSPORT_UNIX_H */ diff --git a/registry/README.md b/registry/README.md index 846c70b..76b0b20 100644 --- a/registry/README.md +++ b/registry/README.md @@ -18,3 +18,7 @@ acts as the version identifier. - `api-contract.jsonl` — manifest of published contracts. - `amduatd-api-contract.v1.json` — contract bytes (v1). +Receipt note: +- `/v1/pel/run` accepts optional receipt v1.1 fields (executor fingerprint, run id, + limits, logs, determinism, rng seed, signature) and emits `receipt_ref` when + provided. diff --git a/registry/amduatd-api-contract.v1.json b/registry/amduatd-api-contract.v1.json index dd2dfcc..13ec487 100644 --- a/registry/amduatd-api-contract.v1.json +++ b/registry/amduatd-api-contract.v1.json @@ -1 +1,180 @@ -{"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"}}}}} +{ + "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": "GET", "path": "/v1/fed/records"}, + {"method": "GET", "path": "/v1/fed/artifacts/{ref}"}, + {"method": "GET", "path": "/v1/fed/status"}, + {"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"}, + "executor_fingerprint_ref": {"type": "string", "description": "hex ref or concept name"}, + "run_id_hex": {"type": "string", "description": "hex bytes"}, + "limits": { + "type": "object", + "required": ["cpu_ms", "wall_ms", "max_rss_kib", "io_reads", "io_writes"], + "properties": { + "cpu_ms": {"type": "integer"}, + "wall_ms": {"type": "integer"}, + "max_rss_kib": {"type": "integer"}, + "io_reads": {"type": "integer"}, + "io_writes": {"type": "integer"} + } + }, + "logs": { + "type": "array", + "items": { + "type": "object", + "required": ["kind", "log_ref", "sha256_hex"], + "properties": { + "kind": {"type": "integer"}, + "log_ref": {"type": "string", "description": "hex ref or concept name"}, + "sha256_hex": {"type": "string", "description": "hex bytes"} + } + } + }, + "determinism_level": {"type": "integer", "description": "0-255"}, + "rng_seed_hex": {"type": "string", "description": "hex bytes"}, + "signature_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"} + } + }, + "fed_records_response": { + "type": "object", + "required": ["domain_id", "snapshot_id", "log_prefix", "next_logseq", "records"], + "properties": { + "domain_id": {"type": "integer"}, + "snapshot_id": {"type": "integer"}, + "log_prefix": {"type": "integer"}, + "next_logseq": {"type": "integer", "description": "Paging cursor; last emitted logseq + 1, or from_logseq if no records emitted."}, + "records": { + "type": "array", + "items": { + "type": "object", + "required": ["domain_id", "type", "ref", "logseq", "snapshot_id", "log_prefix"], + "properties": { + "domain_id": {"type": "integer"}, + "type": {"type": "integer"}, + "ref": {"type": "string"}, + "logseq": {"type": "integer"}, + "snapshot_id": {"type": "integer"}, + "log_prefix": {"type": "integer"}, + "visibility": {"type": "integer"}, + "has_source": {"type": "boolean"}, + "source_domain": {"type": "integer"}, + "notes": {"type": "string", "description": "Type mapping: ARTIFACT_PUBLISH -> ARTIFACT, PER when type_tag=FER1_RECEIPT_1, TGK_EDGE when type_tag=TGK1_EDGE_V1; ARTIFACT_UNPUBLISH -> TOMBSTONE."} + } + } + } + } + }, + "fed_status_response": { + "type": "object", + "required": ["status", "domain_id", "registry_ref", "last_tick_ms"], + "properties": { + "status": {"type": "string"}, + "domain_id": {"type": "integer"}, + "registry_ref": {"type": ["string", "null"]}, + "last_tick_ms": {"type": "integer"} + } + }, + "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"} + } + }, + "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 index 64f4b71..cd66978 100644 --- a/registry/api-contract.jsonl +++ b/registry/api-contract.jsonl @@ -1 +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."} +{"registry":"AMDUATD/API","contract":"AMDUATD/API/1","handle":"amduat.api.amduatd.contract.v1@1","media_type":"application/json","status":"active","bytes_sha256":"34db2a9929eefe4c4d8d95314c0828746c0484dc178d3f63467a8c3d24c17110","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 index 02ccfa6..de0f604 100644 --- a/registry/api-contract.schema.md +++ b/registry/api-contract.schema.md @@ -18,3 +18,8 @@ as stored in the corresponding `registry/*.json` file. - `bytes_sha256` (string, required): sha256 of the bytes file. - `notes` (string, optional): human notes. +## Contract Notes + +- `amduatd-api-contract.v1.json` includes optional receipt v1.1 fields for + `/v1/pel/run` and may emit `receipt_ref` in responses. +- `/v1/fed/records` supports `limit` for paging (default 256, max 10000). diff --git a/src/amduatd.c b/src/amduatd.c index 71ab760..a69de32 100644 --- a/src/amduatd.c +++ b/src/amduatd.c @@ -7,12 +7,17 @@ #include "amduat/asl/store.h" #include "amduat/asl/ref_derive.h" #include "amduat/enc/asl1_core_codec.h" +#include "amduat/enc/asl_log.h" +#include "amduat/enc/fer1_receipt.h" +#include "amduat/fed/replay.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 "federation/coord.h" +#include "federation/transport_stub.h" #include "amduat/pel/program_dag.h" #include "amduat/pel/program_dag_desc.h" #include "amduat/pel/opreg_kernel.h" @@ -29,7 +34,9 @@ #include #include #include +#include +#include #include #include #include @@ -53,6 +60,14 @@ static bool amduatd_send_json_error(int fd, const char *reason, const char *msg); +static uint64_t amduatd_now_ms(void) { + struct timespec ts; + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) { + return 0; + } + return ((uint64_t)ts.tv_sec * 1000u) + ((uint64_t)ts.tv_nsec / 1000000u); +} + 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); @@ -60,6 +75,7 @@ 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 uint64_t AMDUATD_FED_TICK_MS = 1000u; static const uint32_t AMDUATD_TGK_EDGE_TYPE = 0u; @@ -161,6 +177,9 @@ static const char k_amduatd_contract_v1_json[] = "{\"method\":\"GET\",\"path\":\"/v1/meta\"}," "{\"method\":\"HEAD\",\"path\":\"/v1/meta\"}," "{\"method\":\"GET\",\"path\":\"/v1/contract\"}," + "{\"method\":\"GET\",\"path\":\"/v1/fed/records\"}," + "{\"method\":\"GET\",\"path\":\"/v1/fed/artifacts/{ref}\"}," + "{\"method\":\"GET\",\"path\":\"/v1/fed/status\"}," "{\"method\":\"POST\",\"path\":\"/v1/concepts\"}," "{\"method\":\"GET\",\"path\":\"/v1/concepts\"}," "{\"method\":\"GET\",\"path\":\"/v1/concepts/{name}\"}," @@ -194,6 +213,23 @@ static const char k_amduatd_contract_v1_json[] = "\"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\"}," + "\"executor_fingerprint_ref\":{\"type\":\"string\",\"description\":\"hex ref or concept name\"}," + "\"run_id_hex\":{\"type\":\"string\",\"description\":\"hex bytes\"}," + "\"limits\":{\"type\":\"object\",\"required\":[\"cpu_ms\",\"wall_ms\",\"max_rss_kib\",\"io_reads\",\"io_writes\"],\"properties\":{" + "\"cpu_ms\":{\"type\":\"integer\"}," + "\"wall_ms\":{\"type\":\"integer\"}," + "\"max_rss_kib\":{\"type\":\"integer\"}," + "\"io_reads\":{\"type\":\"integer\"}," + "\"io_writes\":{\"type\":\"integer\"}" + "}}," + "\"logs\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"kind\",\"log_ref\",\"sha256_hex\"],\"properties\":{" + "\"kind\":{\"type\":\"integer\"}," + "\"log_ref\":{\"type\":\"string\",\"description\":\"hex ref or concept name\"}," + "\"sha256_hex\":{\"type\":\"string\",\"description\":\"hex bytes\"}" + "}}}," + "\"determinism_level\":{\"type\":\"integer\",\"description\":\"0-255\"}," + "\"rng_seed_hex\":{\"type\":\"string\",\"description\":\"hex bytes\"}," + "\"signature_hex\":{\"type\":\"string\",\"description\":\"hex bytes\"}," "\"started_at\":{\"type\":\"integer\"}," "\"completed_at\":{\"type\":\"integer\"}" "}" @@ -211,6 +247,42 @@ static const char k_amduatd_contract_v1_json[] = "\"status\":{\"type\":\"string\"}" "}" "}," + "\"fed_records_response\":{" + "\"type\":\"object\"," + "\"required\":[\"domain_id\",\"snapshot_id\",\"log_prefix\",\"next_logseq\",\"records\"]," + "\"properties\":{" + "\"domain_id\":{\"type\":\"integer\"}," + "\"snapshot_id\":{\"type\":\"integer\"}," + "\"log_prefix\":{\"type\":\"integer\"}," + "\"next_logseq\":{\"type\":\"integer\",\"description\":\"Paging cursor; last emitted logseq + 1, or from_logseq if no records emitted.\"}," + "\"records\":{\"type\":\"array\",\"items\":{" + "\"type\":\"object\"," + "\"required\":[\"domain_id\",\"type\",\"ref\",\"logseq\",\"snapshot_id\",\"log_prefix\"]," + "\"properties\":{" + "\"domain_id\":{\"type\":\"integer\"}," + "\"type\":{\"type\":\"integer\"}," + "\"ref\":{\"type\":\"string\"}," + "\"logseq\":{\"type\":\"integer\"}," + "\"snapshot_id\":{\"type\":\"integer\"}," + "\"log_prefix\":{\"type\":\"integer\"}," + "\"visibility\":{\"type\":\"integer\"}," + "\"has_source\":{\"type\":\"boolean\"}," + "\"source_domain\":{\"type\":\"integer\"}," + "\"notes\":{\"type\":\"string\",\"description\":\"Type mapping: ARTIFACT_PUBLISH -> ARTIFACT, PER when type_tag=FER1_RECEIPT_1, TGK_EDGE when type_tag=TGK1_EDGE_V1; ARTIFACT_UNPUBLISH -> TOMBSTONE.\"}" + "}" + "}}" + "}" + "}," + "\"fed_status_response\":{" + "\"type\":\"object\"," + "\"required\":[\"status\",\"domain_id\",\"registry_ref\",\"last_tick_ms\"]," + "\"properties\":{" + "\"status\":{\"type\":\"string\"}," + "\"domain_id\":{\"type\":\"integer\"}," + "\"registry_ref\":{\"type\":[\"string\",\"null\"]}," + "\"last_tick_ms\":{\"type\":\"integer\"}" + "}" + "}," "\"context_frame_request\":{" "\"type\":\"object\"," "\"required\":[\"bindings\"]," @@ -3432,6 +3504,318 @@ static bool amduatd_handle_head_artifact(int fd, return amduatd_handle_get_artifact(fd, store, req, path, true); } +static bool amduatd_parse_u64_str(const char *s, uint64_t *out) { + char *end = NULL; + unsigned long long v; + + if (s == NULL || out == NULL || s[0] == '\0') { + return false; + } + errno = 0; + v = strtoull(s, &end, 10); + if (errno != 0 || end == s || *end != '\0') { + return false; + } + *out = (uint64_t)v; + return true; +} + +static bool amduatd_parse_u32_str(const char *s, uint32_t *out) { + uint64_t tmp = 0; + if (!amduatd_parse_u64_str(s, &tmp)) { + return false; + } + if (tmp > UINT32_MAX) { + return false; + } + *out = (uint32_t)tmp; + return true; +} + +static bool amduatd_handle_get_fed_records(int fd, + amduat_asl_store_t *store, + const amduatd_http_req_t *req) { + char domain_buf[32]; + char from_buf[32]; + char limit_buf[32]; + uint32_t domain_id = 0; + uint64_t from_logseq = 0; + uint64_t next_logseq = 0; + uint64_t limit = 256; + uint64_t emitted = 0; + amduat_asl_index_state_t state; + amduat_asl_log_record_t *records = NULL; + size_t record_count = 0; + size_t i; + amduatd_strbuf_t b; + bool first = true; + + if (store == NULL || req == NULL) { + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "internal error\n", false); + } + if (amduatd_query_param(req->path, "domain_id", + domain_buf, sizeof(domain_buf)) == NULL || + !amduatd_parse_u32_str(domain_buf, &domain_id)) { + return amduatd_http_send_text(fd, 400, "Bad Request", + "missing domain_id\n", false); + } + if (amduatd_query_param(req->path, "from_logseq", + from_buf, sizeof(from_buf)) != NULL) { + if (!amduatd_parse_u64_str(from_buf, &from_logseq)) { + return amduatd_http_send_text(fd, 400, "Bad Request", + "invalid from_logseq\n", false); + } + } + if (amduatd_query_param(req->path, "limit", + limit_buf, sizeof(limit_buf)) != NULL) { + if (!amduatd_parse_u64_str(limit_buf, &limit) || limit == 0u || + limit > 10000u) { + return amduatd_http_send_text(fd, 400, "Bad Request", + "invalid limit\n", false); + } + } + if (!amduat_asl_index_current_state(store, &state)) { + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "store error\n", false); + } + if (amduat_asl_log_scan(store, &records, &record_count) != + AMDUAT_ASL_STORE_OK) { + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "log scan error\n", false); + } + + memset(&b, 0, sizeof(b)); + if (!amduatd_strbuf_append_cstr(&b, "{")) { + goto fed_records_oom; + } + { + char header[256]; + int n = snprintf(header, + sizeof(header), + "\"domain_id\":%u," + "\"snapshot_id\":%llu," + "\"log_prefix\":%llu," + "\"records\":[", + (unsigned int)domain_id, + (unsigned long long)state.snapshot_id, + (unsigned long long)state.log_position); + if (n <= 0 || (size_t)n >= sizeof(header)) { + goto fed_records_oom; + } + if (!amduatd_strbuf_append_cstr(&b, header)) { + goto fed_records_oom; + } + } + + next_logseq = from_logseq; + for (i = 0; i < record_count; ++i) { + const amduat_asl_log_record_t *rec = &records[i]; + amduat_reference_t ref; + char *ref_hex = NULL; + char entry[512]; + int n; + + if (rec->logseq < from_logseq) { + continue; + } + memset(&ref, 0, sizeof(ref)); + if (rec->record_type == AMDUAT_ASL_LOG_RECORD_TOMBSTONE || + rec->record_type == AMDUAT_ASL_LOG_RECORD_ARTIFACT_PUBLISH || + rec->record_type == AMDUAT_ASL_LOG_RECORD_ARTIFACT_UNPUBLISH) { + if (!amduat_asl_log_decode_artifact_ref(rec->payload, &ref)) { + continue; + } + } else { + continue; + } + if (!amduat_asl_ref_encode_hex(ref, &ref_hex)) { + amduat_reference_free(&ref); + goto fed_records_oom; + } + + { + uint32_t rec_type = AMDUAT_FED_REC_TOMBSTONE; + amduat_artifact_t artifact; + amduat_asl_store_error_t store_err; + if (rec->record_type == AMDUAT_ASL_LOG_RECORD_ARTIFACT_PUBLISH) { + rec_type = AMDUAT_FED_REC_ARTIFACT; + memset(&artifact, 0, sizeof(artifact)); + store_err = amduat_asl_store_get(store, ref, &artifact); + if (store_err == AMDUAT_ASL_STORE_OK && artifact.has_type_tag) { + if (artifact.type_tag.tag_id == AMDUAT_TYPE_TAG_TGK1_EDGE_V1) { + rec_type = AMDUAT_FED_REC_TGK_EDGE; + } else if (artifact.type_tag.tag_id == + AMDUAT_TYPE_TAG_FER1_RECEIPT_1) { + rec_type = AMDUAT_FED_REC_PER; + } + } + if (store_err == AMDUAT_ASL_STORE_OK) { + amduat_asl_artifact_free(&artifact); + } + } else if (rec->record_type == AMDUAT_ASL_LOG_RECORD_ARTIFACT_UNPUBLISH) { + rec_type = AMDUAT_FED_REC_TOMBSTONE; + } + n = snprintf(entry, + sizeof(entry), + "%s{\"domain_id\":%u," + "\"type\":%u," + "\"ref\":\"%s\"," + "\"logseq\":%llu," + "\"snapshot_id\":%llu," + "\"log_prefix\":%llu," + "\"visibility\":1," + "\"has_source\":false," + "\"source_domain\":0}", + first ? "" : ",", + (unsigned int)domain_id, + (unsigned int)rec_type, + ref_hex, + (unsigned long long)rec->logseq, + (unsigned long long)state.snapshot_id, + (unsigned long long)state.log_position); + } + free(ref_hex); + amduat_reference_free(&ref); + if (n <= 0 || (size_t)n >= sizeof(entry)) { + goto fed_records_oom; + } + if (!amduatd_strbuf_append_cstr(&b, entry)) { + goto fed_records_oom; + } + first = false; + if (rec->logseq >= next_logseq) { + next_logseq = rec->logseq + 1u; + } + emitted++; + if (emitted >= limit) { + break; + } + } + + { + char tail[128]; + int n = snprintf(tail, + sizeof(tail), + "],\"next_logseq\":%llu}\n", + (unsigned long long)next_logseq); + if (n <= 0 || (size_t)n >= sizeof(tail)) { + goto fed_records_oom; + } + if (!amduatd_strbuf_append_cstr(&b, tail)) { + goto fed_records_oom; + } + } + + amduat_enc_asl_log_free(records, record_count); + { + bool ok = amduatd_http_send_json(fd, 200, "OK", b.data, false); + amduatd_strbuf_free(&b); + return ok; + } + +fed_records_oom: + if (records != NULL) { + amduat_enc_asl_log_free(records, record_count); + } + amduatd_strbuf_free(&b); + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "oom\n", false); +} + +static bool amduatd_handle_get_fed_artifact(int fd, + amduat_asl_store_t *store, + const amduatd_http_req_t *req, + const char *path) { + char no_query[1024]; + const char *ref_text = NULL; + amduat_reference_t ref; + amduat_artifact_t artifact; + amduat_asl_store_error_t err; + + if (store == NULL || req == NULL || path == NULL) { + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "internal error\n", false); + } + amduatd_path_without_query(path, no_query, sizeof(no_query)); + if (strncmp(no_query, "/v1/fed/artifacts/", 18) != 0) { + return amduatd_http_send_text(fd, 404, "Not Found", "not found\n", false); + } + ref_text = no_query + 18; + memset(&ref, 0, sizeof(ref)); + if (!amduat_asl_ref_decode_hex(ref_text, &ref)) { + return amduatd_http_send_text(fd, 400, "Bad Request", + "invalid ref\n", false); + } + + memset(&artifact, 0, sizeof(artifact)); + err = amduat_asl_store_get(store, ref, &artifact); + amduat_reference_free(&ref); + 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) { + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "store error\n", false); + } + + { + bool ok = amduatd_http_send_status(fd, + 200, + "OK", + "application/octet-stream", + artifact.bytes.data, + artifact.bytes.len, + false); + amduat_asl_artifact_free(&artifact); + return ok; + } +} + +static bool amduatd_handle_get_fed_status(int fd, + const amduat_fed_coord_t *coord, + const amduatd_http_req_t *req) { + amduat_fed_coord_status_t status; + char *ref_hex = NULL; + char json[512]; + int n; + + (void)req; + amduat_fed_coord_get_status(coord, &status); + if (status.registry_ref.hash_id != 0 && + status.registry_ref.digest.data != NULL) { + if (!amduat_asl_ref_encode_hex(status.registry_ref, &ref_hex)) { + amduat_reference_free(&status.registry_ref); + return amduatd_http_send_text(fd, 500, "Internal Server Error", + "encode error\n", false); + } + } + + if (ref_hex != NULL) { + n = snprintf(json, + sizeof(json), + "{\"status\":\"ok\",\"domain_id\":%u," + "\"registry_ref\":\"%s\",\"last_tick_ms\":%llu}\n", + (unsigned int)status.domain_id, + ref_hex, + (unsigned long long)status.last_tick_ms); + } else { + n = snprintf(json, + sizeof(json), + "{\"status\":\"ok\",\"domain_id\":%u," + "\"registry_ref\":null,\"last_tick_ms\":%llu}\n", + (unsigned int)status.domain_id, + (unsigned long long)status.last_tick_ms); + } + free(ref_hex); + amduat_reference_free(&status.registry_ref); + 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); +} + static bool amduatd_handle_post_artifacts(int fd, amduat_asl_store_t *store, const amduatd_http_req_t *req) { @@ -3546,15 +3930,34 @@ static bool amduatd_handle_post_pel_run(int fd, bool receipt_have_started = false; bool receipt_have_completed = false; bool receipt_has_sbom = false; + bool receipt_has_executor_fingerprint = false; + bool receipt_has_run_id = false; + bool receipt_has_limits = false; + bool receipt_has_logs = false; + bool receipt_has_determinism = false; + bool receipt_has_rng_seed = false; + bool receipt_has_signature = false; char *receipt_evaluator_id = NULL; uint8_t *receipt_parity_digest = NULL; size_t receipt_parity_digest_len = 0; + uint8_t *receipt_run_id = NULL; + size_t receipt_run_id_len = 0; + uint8_t *receipt_rng_seed = NULL; + size_t receipt_rng_seed_len = 0; + uint8_t *receipt_signature = NULL; + size_t receipt_signature_len = 0; uint64_t receipt_started_at = 0; uint64_t receipt_completed_at = 0; + uint8_t receipt_determinism_level = 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 receipt_executor_fingerprint_ref; + amduat_fer1_limits_t receipt_limits; + amduat_fer1_log_entry_t *receipt_logs = NULL; + size_t receipt_logs_len = 0; + size_t receipt_logs_cap = 0; amduat_reference_t scheme_ref; amduat_reference_t program_ref; amduat_reference_t params_ref; @@ -3576,6 +3979,9 @@ static bool amduatd_handle_post_pel_run(int fd, 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_executor_fingerprint_ref, 0, + sizeof(receipt_executor_fingerprint_ref)); + memset(&receipt_limits, 0, sizeof(receipt_limits)); memset(&receipt_artifact, 0, sizeof(receipt_artifact)); memset(&receipt_ref, 0, sizeof(receipt_ref)); @@ -3923,6 +4329,29 @@ static bool amduatd_handle_post_pel_run(int fd, goto pel_run_cleanup; } receipt_has_sbom = true; + } else if (rkey_len == strlen("executor_fingerprint_ref") && + memcmp(rkey, "executor_fingerprint_ref", rkey_len) == 0) { + amduatd_ref_status_t st; + if (receipt_has_executor_fingerprint || + !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid executor_fingerprint_ref"); + goto pel_run_cleanup; + } + st = amduatd_decode_ref_or_name_latest( + store, cfg, concepts, sv, sv_len, + &receipt_executor_fingerprint_ref); + if (st == AMDUATD_REF_ERR_NOT_FOUND) { + ok = amduatd_send_json_error(fd, 404, "Not Found", + "executor_fingerprint_ref not found"); + goto pel_run_cleanup; + } + if (st != AMDUATD_REF_OK) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid executor_fingerprint_ref"); + goto pel_run_cleanup; + } + receipt_has_executor_fingerprint = true; } else if (rkey_len == strlen("parity_digest_hex") && memcmp(rkey, "parity_digest_hex", rkey_len) == 0) { char *tmp = NULL; @@ -3945,6 +4374,350 @@ static bool amduatd_handle_post_pel_run(int fd, free(receipt_parity_digest); receipt_parity_digest = bytes; receipt_parity_digest_len = bytes_len; + } else if (rkey_len == strlen("run_id_hex") && + memcmp(rkey, "run_id_hex", rkey_len) == 0) { + char *tmp = NULL; + uint8_t *bytes = NULL; + size_t bytes_len = 0; + if (receipt_has_run_id || + !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 run_id_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 run_id_hex"); + goto pel_run_cleanup; + } + free(tmp); + free(receipt_run_id); + receipt_run_id = bytes; + receipt_run_id_len = bytes_len; + receipt_has_run_id = true; + } else if (rkey_len == strlen("limits") && + memcmp(rkey, "limits", rkey_len) == 0) { + bool have_cpu = false; + bool have_wall = false; + bool have_rss = false; + bool have_reads = false; + bool have_writes = false; + if (receipt_has_limits || !amduatd_json_expect(&p, end, '{')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid limits"); + goto pel_run_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, &sv, &sv_len) || + !amduatd_json_expect(&p, end, ':')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid limits"); + goto pel_run_cleanup; + } + if (sv_len == strlen("cpu_ms") && + memcmp(sv, "cpu_ms", sv_len) == 0) { + if (have_cpu || !amduatd_json_parse_u64(&p, end, + &receipt_limits.cpu_ms)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid limits cpu_ms"); + goto pel_run_cleanup; + } + have_cpu = true; + } else if (sv_len == strlen("wall_ms") && + memcmp(sv, "wall_ms", sv_len) == 0) { + if (have_wall || !amduatd_json_parse_u64(&p, end, + &receipt_limits.wall_ms)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid limits wall_ms"); + goto pel_run_cleanup; + } + have_wall = true; + } else if (sv_len == strlen("max_rss_kib") && + memcmp(sv, "max_rss_kib", sv_len) == 0) { + if (have_rss || !amduatd_json_parse_u64(&p, end, + &receipt_limits.max_rss_kib)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid limits max_rss_kib"); + goto pel_run_cleanup; + } + have_rss = true; + } else if (sv_len == strlen("io_reads") && + memcmp(sv, "io_reads", sv_len) == 0) { + if (have_reads || !amduatd_json_parse_u64(&p, end, + &receipt_limits.io_reads)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid limits io_reads"); + goto pel_run_cleanup; + } + have_reads = true; + } else if (sv_len == strlen("io_writes") && + memcmp(sv, "io_writes", sv_len) == 0) { + if (have_writes || !amduatd_json_parse_u64(&p, end, + &receipt_limits.io_writes)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid limits io_writes"); + goto pel_run_cleanup; + } + have_writes = true; + } else { + if (!amduatd_json_skip_value(&p, end, 0)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid limits"); + goto pel_run_cleanup; + } + } + cur = amduatd_json_skip_ws(p, end); + if (cur >= end) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid limits"); + 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 limits"); + goto pel_run_cleanup; + } + if (!have_cpu || !have_wall || !have_rss || + !have_reads || !have_writes) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "missing limits fields"); + goto pel_run_cleanup; + } + receipt_has_limits = true; + } else if (rkey_len == strlen("logs") && + memcmp(rkey, "logs", rkey_len) == 0) { + if (receipt_has_logs || !amduatd_json_expect(&p, end, '[')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid logs"); + goto pel_run_cleanup; + } + receipt_has_logs = true; + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ']') { + p = cur + 1; + } else { + for (;;) { + amduat_fer1_log_entry_t entry; + bool have_kind = false; + bool have_log_ref = false; + bool have_sha = false; + memset(&entry, 0, sizeof(entry)); + if (!amduatd_json_expect(&p, end, '{')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid log entry"); + goto pel_run_cleanup; + } + for (;;) { + const char *lkey = NULL; + size_t lkey_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, &lkey, &lkey_len) || + !amduatd_json_expect(&p, end, ':')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid log entry"); + goto pel_run_cleanup; + } + if (lkey_len == strlen("kind") && + memcmp(lkey, "kind", lkey_len) == 0) { + uint32_t kind = 0; + if (have_kind || + !amduatd_json_parse_u32_loose(&p, end, &kind)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid log kind"); + goto pel_run_cleanup; + } + entry.kind = kind; + have_kind = true; + } else if (lkey_len == strlen("log_ref") && + memcmp(lkey, "log_ref", lkey_len) == 0) { + amduatd_ref_status_t st; + if (have_log_ref || + !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid log_ref"); + goto pel_run_cleanup; + } + st = amduatd_decode_ref_or_name_latest( + store, cfg, concepts, sv, sv_len, &entry.log_ref); + if (st == AMDUATD_REF_ERR_NOT_FOUND) { + ok = amduatd_send_json_error(fd, 404, "Not Found", + "log_ref not found"); + goto pel_run_cleanup; + } + if (st != AMDUATD_REF_OK) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid log_ref"); + goto pel_run_cleanup; + } + have_log_ref = true; + } else if (lkey_len == strlen("sha256_hex") && + memcmp(lkey, "sha256_hex", lkey_len) == 0) { + char *tmp = NULL; + uint8_t *bytes = NULL; + size_t bytes_len = 0; + if (have_sha || + !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 sha256_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 sha256_hex"); + goto pel_run_cleanup; + } + free(tmp); + entry.sha256 = amduat_octets(bytes, bytes_len); + have_sha = true; + } else { + if (!amduatd_json_skip_value(&p, end, 0)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid log entry"); + goto pel_run_cleanup; + } + } + cur = amduatd_json_skip_ws(p, end); + if (cur >= end) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid log entry"); + 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 log entry"); + goto pel_run_cleanup; + } + if (!have_kind || !have_log_ref || !have_sha) { + if (have_log_ref) { + amduat_reference_free(&entry.log_ref); + } + if (have_sha) { + amduat_octets_free(&entry.sha256); + } + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "missing log fields"); + goto pel_run_cleanup; + } + if (receipt_logs_len == receipt_logs_cap) { + size_t next_cap = receipt_logs_cap != 0 ? receipt_logs_cap * 2u : 4u; + amduat_fer1_log_entry_t *next = + (amduat_fer1_log_entry_t *)realloc( + receipt_logs, next_cap * sizeof(*receipt_logs)); + if (next == NULL) { + amduat_reference_free(&entry.log_ref); + amduat_octets_free(&entry.sha256); + ok = amduatd_send_json_error(fd, 500, + "Internal Server Error", + "oom"); + goto pel_run_cleanup; + } + receipt_logs = next; + receipt_logs_cap = next_cap; + } + receipt_logs[receipt_logs_len++] = entry; + 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 logs"); + goto pel_run_cleanup; + } + } + } else if (rkey_len == strlen("determinism_level") && + memcmp(rkey, "determinism_level", rkey_len) == 0) { + uint32_t level = 0; + if (receipt_has_determinism || + !amduatd_json_parse_u32_loose(&p, end, &level) || + level > 255u) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid determinism_level"); + goto pel_run_cleanup; + } + receipt_determinism_level = (uint8_t)level; + receipt_has_determinism = true; + } else if (rkey_len == strlen("rng_seed_hex") && + memcmp(rkey, "rng_seed_hex", rkey_len) == 0) { + char *tmp = NULL; + uint8_t *bytes = NULL; + size_t bytes_len = 0; + if (receipt_has_rng_seed || + !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 rng_seed_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 rng_seed_hex"); + goto pel_run_cleanup; + } + free(tmp); + free(receipt_rng_seed); + receipt_rng_seed = bytes; + receipt_rng_seed_len = bytes_len; + receipt_has_rng_seed = true; + } else if (rkey_len == strlen("signature_hex") && + memcmp(rkey, "signature_hex", rkey_len) == 0) { + char *tmp = NULL; + uint8_t *bytes = NULL; + size_t bytes_len = 0; + if (receipt_has_signature || + !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 signature_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 signature_hex"); + goto pel_run_cleanup; + } + free(tmp); + free(receipt_signature); + receipt_signature = bytes; + receipt_signature_len = bytes_len; + receipt_has_signature = true; } else if (rkey_len == strlen("started_at") && memcmp(rkey, "started_at", rkey_len) == 0) { if (receipt_have_started || @@ -4071,6 +4844,13 @@ static bool amduatd_handle_post_pel_run(int fd, if (want_receipt) { amduat_octets_t evaluator_id; amduat_octets_t parity_digest; + bool use_receipt_v1_1 = receipt_has_executor_fingerprint || + receipt_has_run_id || + receipt_has_limits || + receipt_has_logs || + receipt_has_determinism || + receipt_has_rng_seed || + receipt_has_signature; if (!run_result.has_result_value) { ok = amduatd_send_json_error(fd, 500, "Internal Server Error", @@ -4088,21 +4868,60 @@ static bool amduatd_handle_post_pel_run(int fd, : 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 (use_receipt_v1_1) { + amduat_octets_t run_id = amduat_octets(receipt_run_id, + receipt_run_id_len); + amduat_octets_t rng_seed = amduat_octets(receipt_rng_seed, + receipt_rng_seed_len); + amduat_octets_t signature = amduat_octets(receipt_signature, + receipt_signature_len); + if (!amduat_fer1_receipt_from_pel_run_v1_1( + &run_result, + 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_has_executor_fingerprint, + receipt_executor_fingerprint_ref, + receipt_has_run_id, + run_id, + receipt_has_limits, + receipt_limits, + receipt_logs, + receipt_logs_len, + receipt_has_determinism, + receipt_determinism_level, + receipt_has_rng_seed, + rng_seed, + receipt_has_signature, + signature, + &receipt_artifact)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "receipt build failed"); + goto pel_run_cleanup; + } + } else { + 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) { @@ -4276,8 +5095,22 @@ pel_run_cleanup: if (receipt_has_sbom) { amduat_reference_free(&receipt_sbom_ref); } + if (receipt_has_executor_fingerprint) { + amduat_reference_free(&receipt_executor_fingerprint_ref); + } + if (receipt_logs != NULL) { + size_t i; + for (i = 0; i < receipt_logs_len; ++i) { + amduat_reference_free(&receipt_logs[i].log_ref); + amduat_octets_free(&receipt_logs[i].sha256); + } + free(receipt_logs); + } free(receipt_evaluator_id); free(receipt_parity_digest); + free(receipt_run_id); + free(receipt_rng_seed); + free(receipt_signature); return ok; } @@ -5975,7 +6808,8 @@ static bool amduatd_handle_conn(int fd, const amduat_asl_store_fs_config_t *cfg, amduat_reference_t api_contract_ref, amduat_reference_t ui_ref, - amduatd_concepts_t *concepts) { + amduatd_concepts_t *concepts, + const amduat_fed_coord_t *coord) { amduatd_http_req_t req; char no_query[1024]; @@ -5998,6 +6832,14 @@ static bool amduatd_handle_conn(int fd, 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, "GET") == 0 && + strcmp(no_query, "/v1/fed/records") == 0) { + return amduatd_handle_get_fed_records(fd, store, &req); + } + if (strcmp(req.method, "GET") == 0 && + strcmp(no_query, "/v1/fed/status") == 0) { + return amduatd_handle_get_fed_status(fd, coord, &req); + } if (strcmp(req.method, "POST") == 0 && strcmp(no_query, "/v1/artifacts") == 0) { return amduatd_handle_post_artifacts(fd, store, &req); @@ -6022,6 +6864,10 @@ static bool amduatd_handle_conn(int fd, 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/fed/artifacts/", 18) == 0) { + return amduatd_handle_get_fed_artifact(fd, store, &req, req.path); + } if (strcmp(req.method, "GET") == 0 && strncmp(no_query, "/v1/concepts/", 13) == 0) { const char *name = no_query + 13; if (name[0] == '\0') { @@ -6086,8 +6932,12 @@ int main(int argc, char **argv) { amduat_reference_t api_contract_ref; amduat_reference_t ui_ref; amduatd_concepts_t concepts; + amduat_fed_transport_stub_t fed_stub; + amduat_fed_coord_cfg_t fed_cfg; + amduat_fed_coord_t *fed_coord = NULL; int i; int sfd = -1; + uint64_t last_tick_ms = 0; memset(&api_contract_ref, 0, sizeof(api_contract_ref)); memset(&ui_ref, 0, sizeof(ui_ref)); @@ -6145,6 +6995,16 @@ int main(int argc, char **argv) { return 8; } + amduat_fed_transport_stub_init(&fed_stub); + memset(&fed_cfg, 0, sizeof(fed_cfg)); + fed_cfg.local_domain_id = 0; + fed_cfg.authoritative_store = &store; + fed_cfg.cache_store = NULL; + fed_cfg.transport = amduat_fed_transport_stub_ops(&fed_stub); + if (amduat_fed_coord_open(&fed_cfg, &fed_coord) != AMDUAT_FED_COORD_OK) { + fed_coord = NULL; + } + signal(SIGINT, amduatd_on_signal); signal(SIGTERM, amduatd_on_signal); @@ -6185,27 +7045,76 @@ int main(int argc, char **argv) { fprintf(stderr, "amduatd: root=%s sock=%s\n", root, sock_path); while (!amduatd_should_exit) { - int cfd = accept(sfd, NULL, NULL); - if (cfd < 0) { + fd_set rfds; + struct timeval tv; + struct timeval *tvp = NULL; + int rc; + + if (fed_coord != NULL) { + uint64_t now_ms = amduatd_now_ms(); + uint64_t due_ms = last_tick_ms == 0 + ? now_ms + : last_tick_ms + AMDUATD_FED_TICK_MS; + uint64_t wait_ms = due_ms <= now_ms ? 0 : (due_ms - now_ms); + tv.tv_sec = (time_t)(wait_ms / 1000u); + tv.tv_usec = (suseconds_t)((wait_ms % 1000u) * 1000u); + tvp = &tv; + } + + FD_ZERO(&rfds); + FD_SET(sfd, &rfds); + rc = select(sfd + 1, &rfds, NULL, NULL, tvp); + if (rc < 0) { if (errno == EINTR) { continue; } - perror("accept"); + perror("select"); break; } - (void)amduatd_handle_conn(cfd, - &store, - &cfg, - api_contract_ref, - ui_ref, - &concepts); - (void)close(cfd); + if (fed_coord != NULL) { + uint64_t now_ms = amduatd_now_ms(); + if (last_tick_ms == 0 || + now_ms - last_tick_ms >= AMDUATD_FED_TICK_MS) { + (void)amduat_fed_coord_tick(fed_coord, now_ms); + last_tick_ms = now_ms; + } + } + + if (rc == 0) { + continue; + } + if (!FD_ISSET(sfd, &rfds)) { + continue; + } + + { + int cfd = accept(sfd, NULL, NULL); + if (cfd < 0) { + if (errno == EINTR) { + continue; + } + perror("accept"); + break; + } + + (void)amduatd_handle_conn(cfd, + &store, + &cfg, + api_contract_ref, + ui_ref, + &concepts, + fed_coord); + (void)close(cfd); + } } amduat_reference_free(&api_contract_ref); amduat_reference_free(&ui_ref); amduatd_concepts_free(&concepts); + if (fed_coord != NULL) { + amduat_fed_coord_close(fed_coord); + } (void)unlink(sock_path); (void)close(sfd); return 0;