diff --git a/README.md b/README.md index e513ec5..9ca28fd 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,13 @@ Resolve the latest ref: curl --unix-socket amduatd.sock http://localhost/v1/resolve/hello ``` +Inspect concepts: + +```sh +curl --unix-socket amduatd.sock http://localhost/v1/concepts +curl --unix-socket amduatd.sock http://localhost/v1/concepts/hello +``` + ## Current endpoints - `GET /v1/meta` → `{store_id, encoding_profile_id, hash_id, api_contract_ref}` @@ -138,6 +145,8 @@ curl --unix-socket amduatd.sock http://localhost/v1/resolve/hello - request: `{name, ref?}` (`name` is lowercase; `ref` publishes an initial version) - response: `{name, concept_ref}` - `POST /v1/concepts/{name}/publish` → publishes a new version `{ref}` +- `GET /v1/concepts` → `{concepts:[{name, concept_ref}]}` +- `GET /v1/concepts/{name}` → `{name, concept_ref, latest_ref, versions[]}` - `GET /v1/resolve/{name}` → `{ref}` (latest published) - `POST /v1/pel/run` - request: `{program_ref, input_refs[], params_ref?, scheme_ref?}` (`program_ref`/`input_refs`/`params_ref` accept hex refs or concept names; omit `scheme_ref` to use `dag`) diff --git a/registry/amduatd-api-contract.v1.json b/registry/amduatd-api-contract.v1.json index c1eb70f..adfd37e 100644 --- a/registry/amduatd-api-contract.v1.json +++ b/registry/amduatd-api-contract.v1.json @@ -1 +1 @@ -{"contract":"AMDUATD/API/1","base_path":"/v1","endpoints":[{"method":"GET","path":"/v1/ui"},{"method":"GET","path":"/v1/meta"},{"method":"HEAD","path":"/v1/meta"},{"method":"GET","path":"/v1/contract"},{"method":"POST","path":"/v1/concepts"},{"method":"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":"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"}}}}} +{"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":"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"}}}}} diff --git a/registry/api-contract.jsonl b/registry/api-contract.jsonl index 5f75a0c..8117f1b 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":"e7459c83f8473e4665ae30c3ead65385bf666618500e8d782b0b0450ae55a3e0","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":"3c8ef7489b0499ebb6e8025981c6f43962568d554d63d6994d75e43812e17feb","notes":"Seeded into the ASL store at amduatd startup; ref is advertised via /v1/meta."} diff --git a/src/amduatd.c b/src/amduatd.c index 5f2a4a0..9fc9f71 100644 --- a/src/amduatd.c +++ b/src/amduatd.c @@ -35,6 +35,27 @@ #include #include +typedef struct amduatd_strbuf { + char *data; + size_t len; + size_t cap; +} amduatd_strbuf_t; + +static bool amduatd_http_send_json(int fd, + int code, + const char *reason, + const char *json, + bool head_only); + +static bool amduatd_send_json_error(int fd, + int code, + const char *reason, + const char *msg); + +static void amduatd_strbuf_free(amduatd_strbuf_t *b); +static bool amduatd_strbuf_append_cstr(amduatd_strbuf_t *b, const char *s); +static bool amduatd_strbuf_append_char(amduatd_strbuf_t *b, char c); + static const char *const AMDUATD_DEFAULT_ROOT = ".amduat-asl"; static const char *const AMDUATD_DEFAULT_SOCK = "amduatd.sock"; static const char *const AMDUATD_EDGES_FILE = ".amduatd.edges"; @@ -115,6 +136,8 @@ static const char k_amduatd_contract_v1_json[] = "{\"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\"}," @@ -228,6 +251,12 @@ static const char k_amduatd_ui_html[] = "
\n" " \n" " \n" + " \n" + "
\n" + "
Upload bytes (sets program_ref)
\n" + "
\n" + " \n" + " \n" "
\n" "
\n" " \n" @@ -307,6 +336,36 @@ static const char k_amduatd_ui_html[] = " }\n" " });\n" "\n" + " el('btnConceptLoad').addEventListener('click', async () => {\n" + " try {\n" + " const name = el('conceptName').value.trim();\n" + " const resp = await fetch(`/v1/concepts/${encodeURIComponent(name)}`);\n" + " out(await resp.text());\n" + " } catch (e) {\n" + " out(String(e));\n" + " }\n" + " });\n" + "\n" + " el('btnUpload').addEventListener('click', async () => {\n" + " try {\n" + " const file = el('uploadFile').files && el('uploadFile').files[0];\n" + " if (!file) { out('no file selected'); return; }\n" + " const resp = await fetch('/v1/artifacts', {\n" + " method: 'POST',\n" + " headers: { 'Content-Type': 'application/octet-stream' },\n" + " body: file\n" + " });\n" + " const text = await resp.text();\n" + " out(text);\n" + " if (resp.ok) {\n" + " const j = JSON.parse(text);\n" + " if (j && j.ref) el('programRef').value = j.ref;\n" + " }\n" + " } catch (e) {\n" + " out(String(e));\n" + " }\n" + " });\n" + "\n" " el('btnRun').addEventListener('click', async () => {\n" " try {\n" " const program_ref = el('programRef').value.trim();\n" @@ -873,6 +932,281 @@ static bool amduatd_concepts_resolve_latest(amduat_asl_store_t *store, return false; } +static bool amduatd_parse_name_artifact(amduat_artifact_t artifact, + char *out, + size_t cap) { + const uint8_t *bytes; + size_t len; + const char *prefix = "AMDUATD/NAME/1"; + size_t prefix_len; + size_t i; + size_t name_len; + + if (out != NULL && cap != 0) { + out[0] = '\0'; + } + if (out == NULL || cap == 0) { + return false; + } + if (artifact.bytes.len != 0 && artifact.bytes.data == NULL) { + return false; + } + bytes = artifact.bytes.data; + len = artifact.bytes.len; + prefix_len = strlen(prefix); + if (len < prefix_len + 1u) { + return false; + } + if (memcmp(bytes, prefix, prefix_len) != 0 || bytes[prefix_len] != 0) { + return false; + } + for (i = prefix_len + 1u; i < len; ++i) { + if (bytes[i] == 0) { + return false; + } + } + name_len = len - (prefix_len + 1u); + if (name_len == 0 || name_len >= cap) { + return false; + } + memcpy(out, bytes + prefix_len + 1u, name_len); + out[name_len] = '\0'; + return amduatd_name_valid(out); +} + +static bool amduatd_handle_get_concepts(int fd, + amduat_asl_store_t *store, + const amduatd_concepts_t *concepts) { + amduatd_strbuf_t b; + bool first = true; + size_t i; + + if (store == NULL || concepts == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "internal error"); + } + memset(&b, 0, sizeof(b)); + if (!amduatd_strbuf_append_cstr(&b, "{\"concepts\":[")) { + amduatd_strbuf_free(&b); + return amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + } + + for (i = 0; i < concepts->edge_refs.len; ++i) { + amduat_reference_t edge_ref = concepts->edge_refs.refs[i]; + amduat_artifact_t artifact; + amduat_tgk_edge_body_t edge; + amduat_asl_store_error_t err; + char name[128]; + char *concept_hex = NULL; + + memset(&artifact, 0, sizeof(artifact)); + err = amduat_asl_store_get(store, edge_ref, &artifact); + if (err != AMDUAT_ASL_STORE_OK) { + continue; + } + if (!artifact.has_type_tag || + artifact.type_tag.tag_id != AMDUAT_TYPE_TAG_TGK1_EDGE_V1) { + amduat_asl_artifact_free(&artifact); + continue; + } + memset(&edge, 0, sizeof(edge)); + if (!amduat_enc_tgk1_edge_decode_v1(artifact.bytes, &edge)) { + amduat_asl_artifact_free(&artifact); + continue; + } + if (!(edge.from_len == 1 && edge.to_len == 1 && + amduat_reference_eq(edge.payload, concepts->rel_aliases_ref))) { + amduat_enc_tgk1_edge_free(&edge); + amduat_asl_artifact_free(&artifact); + continue; + } + amduat_asl_artifact_free(&artifact); + + memset(&artifact, 0, sizeof(artifact)); + err = amduat_asl_store_get(store, edge.from[0], &artifact); + if (err != AMDUAT_ASL_STORE_OK || + !amduatd_parse_name_artifact(artifact, name, sizeof(name))) { + amduat_enc_tgk1_edge_free(&edge); + amduat_asl_artifact_free(&artifact); + continue; + } + amduat_asl_artifact_free(&artifact); + + if (!amduat_asl_ref_encode_hex(edge.to[0], &concept_hex)) { + amduat_enc_tgk1_edge_free(&edge); + continue; + } + + if (!first) { + (void)amduatd_strbuf_append_char(&b, ','); + } + first = false; + + (void)amduatd_strbuf_append_cstr(&b, "{\"name\":\""); + (void)amduatd_strbuf_append_cstr(&b, name); + (void)amduatd_strbuf_append_cstr(&b, "\",\"concept_ref\":\""); + (void)amduatd_strbuf_append_cstr(&b, concept_hex); + (void)amduatd_strbuf_append_cstr(&b, "\"}"); + + free(concept_hex); + amduat_enc_tgk1_edge_free(&edge); + } + + if (!amduatd_strbuf_append_cstr(&b, "]}\n")) { + amduatd_strbuf_free(&b); + return amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + } + { + bool ok = amduatd_http_send_json(fd, 200, "OK", b.data, false); + amduatd_strbuf_free(&b); + return ok; + } +} + +static bool amduatd_handle_get_concept(int fd, + amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + const amduatd_concepts_t *concepts, + const char *name) { + amduat_reference_t concept_ref; + amduat_reference_t latest_ref; + amduatd_strbuf_t b; + char *concept_hex = NULL; + char *latest_hex = NULL; + bool have_latest = false; + size_t i; + size_t version_count = 0; + + memset(&concept_ref, 0, sizeof(concept_ref)); + memset(&latest_ref, 0, sizeof(latest_ref)); + memset(&b, 0, sizeof(b)); + + if (store == NULL || cfg == NULL || concepts == NULL || name == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "internal error"); + } + if (!amduatd_name_valid(name)) { + return amduatd_send_json_error(fd, 400, "Bad Request", "invalid name"); + } + if (!amduatd_concepts_lookup_alias(store, cfg, concepts, name, &concept_ref)) { + return amduatd_send_json_error(fd, 404, "Not Found", "unknown concept"); + } + if (amduatd_concepts_resolve_latest(store, concepts, concept_ref, + &latest_ref)) { + have_latest = true; + } + + if (!amduat_asl_ref_encode_hex(concept_ref, &concept_hex)) { + amduat_reference_free(&concept_ref); + amduat_reference_free(&latest_ref); + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + } + if (have_latest) { + if (!amduat_asl_ref_encode_hex(latest_ref, &latest_hex)) { + free(concept_hex); + amduat_reference_free(&concept_ref); + amduat_reference_free(&latest_ref); + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + } + } + + if (!amduatd_strbuf_append_cstr(&b, "{")) { + free(concept_hex); + free(latest_hex); + amduat_reference_free(&concept_ref); + amduat_reference_free(&latest_ref); + return amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + } + (void)amduatd_strbuf_append_cstr(&b, "\"name\":\""); + (void)amduatd_strbuf_append_cstr(&b, name); + (void)amduatd_strbuf_append_cstr(&b, "\",\"concept_ref\":\""); + (void)amduatd_strbuf_append_cstr(&b, concept_hex); + (void)amduatd_strbuf_append_cstr(&b, "\",\"latest_ref\":"); + if (latest_hex != NULL) { + (void)amduatd_strbuf_append_cstr(&b, "\""); + (void)amduatd_strbuf_append_cstr(&b, latest_hex); + (void)amduatd_strbuf_append_cstr(&b, "\""); + } else { + (void)amduatd_strbuf_append_cstr(&b, "null"); + } + (void)amduatd_strbuf_append_cstr(&b, ",\"versions\":["); + + for (i = 0; i < concepts->edge_refs.len; ++i) { + amduat_reference_t edge_ref = concepts->edge_refs.refs[i]; + amduat_artifact_t artifact; + amduat_tgk_edge_body_t edge; + amduat_asl_store_error_t err; + char *edge_hex = NULL; + char *ref_hex = NULL; + + memset(&artifact, 0, sizeof(artifact)); + err = amduat_asl_store_get(store, edge_ref, &artifact); + if (err != AMDUAT_ASL_STORE_OK) { + continue; + } + if (!artifact.has_type_tag || + artifact.type_tag.tag_id != AMDUAT_TYPE_TAG_TGK1_EDGE_V1) { + amduat_asl_artifact_free(&artifact); + continue; + } + memset(&edge, 0, sizeof(edge)); + if (!amduat_enc_tgk1_edge_decode_v1(artifact.bytes, &edge)) { + amduat_asl_artifact_free(&artifact); + continue; + } + amduat_asl_artifact_free(&artifact); + + if (!(edge.from_len == 1 && edge.to_len == 1 && + amduat_reference_eq(edge.payload, concepts->rel_materializes_ref) && + amduat_reference_eq(edge.from[0], concept_ref))) { + amduat_enc_tgk1_edge_free(&edge); + continue; + } + + if (!amduat_asl_ref_encode_hex(edge_ref, &edge_hex) || + !amduat_asl_ref_encode_hex(edge.to[0], &ref_hex)) { + free(edge_hex); + free(ref_hex); + amduat_enc_tgk1_edge_free(&edge); + continue; + } + + if (version_count != 0) { + (void)amduatd_strbuf_append_char(&b, ','); + } + version_count++; + + (void)amduatd_strbuf_append_cstr(&b, "{\"edge_ref\":\""); + (void)amduatd_strbuf_append_cstr(&b, edge_hex); + (void)amduatd_strbuf_append_cstr(&b, "\",\"ref\":\""); + (void)amduatd_strbuf_append_cstr(&b, ref_hex); + (void)amduatd_strbuf_append_cstr(&b, "\"}"); + + free(edge_hex); + free(ref_hex); + amduat_enc_tgk1_edge_free(&edge); + + if (version_count >= 64u) { + break; + } + } + + (void)amduatd_strbuf_append_cstr(&b, "]}\n"); + + free(concept_hex); + free(latest_hex); + amduat_reference_free(&concept_ref); + amduat_reference_free(&latest_ref); + + { + bool ok = amduatd_http_send_json(fd, 200, "OK", b.data, false); + amduatd_strbuf_free(&b); + return ok; + } +} + static bool amduatd_read_exact(int fd, uint8_t *buf, size_t len) { size_t off = 0; while (off < len) { @@ -1182,11 +1516,6 @@ static const char *amduatd_query_param(const char *path, return NULL; } -typedef struct { - char *data; - size_t len; - size_t cap; -} amduatd_strbuf_t; static void amduatd_strbuf_free(amduatd_strbuf_t *b) { if (b == NULL) { @@ -3623,6 +3952,16 @@ static bool amduatd_handle_conn(int fd, if (strcmp(req.method, "POST") == 0 && strcmp(no_query, "/v1/concepts") == 0) { return amduatd_handle_post_concepts(fd, store, cfg, concepts, &req); } + if (strcmp(req.method, "GET") == 0 && strcmp(no_query, "/v1/concepts") == 0) { + return amduatd_handle_get_concepts(fd, store, concepts); + } + if (strcmp(req.method, "GET") == 0 && strncmp(no_query, "/v1/concepts/", 13) == 0) { + const char *name = no_query + 13; + if (name[0] == '\0') { + return amduatd_send_json_error(fd, 400, "Bad Request", "missing name"); + } + return amduatd_handle_get_concept(fd, store, cfg, concepts, name); + } if (strcmp(req.method, "POST") == 0 && strncmp(no_query, "/v1/concepts/", 13) == 0 && strstr(no_query, "/publish") != NULL) {