Add concept inspection endpoints and UI upload

This commit is contained in:
Carl Niklas Rydberg 2025-12-22 21:13:26 +01:00
parent bed97fc22d
commit ccfee235a9
4 changed files with 355 additions and 7 deletions

View file

@ -122,6 +122,13 @@ Resolve the latest ref:
curl --unix-socket amduatd.sock http://localhost/v1/resolve/hello 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 ## Current endpoints
- `GET /v1/meta``{store_id, encoding_profile_id, hash_id, api_contract_ref}` - `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) - request: `{name, ref?}` (`name` is lowercase; `ref` publishes an initial version)
- response: `{name, concept_ref}` - response: `{name, concept_ref}`
- `POST /v1/concepts/{name}/publish` → publishes a new version `{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) - `GET /v1/resolve/{name}``{ref}` (latest published)
- `POST /v1/pel/run` - `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`) - 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`)

View file

@ -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"}}}}}

View file

@ -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."}

View file

@ -35,6 +35,27 @@
#include <sys/un.h> #include <sys/un.h>
#include <unistd.h> #include <unistd.h>
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_ROOT = ".amduat-asl";
static const char *const AMDUATD_DEFAULT_SOCK = "amduatd.sock"; static const char *const AMDUATD_DEFAULT_SOCK = "amduatd.sock";
static const char *const AMDUATD_EDGES_FILE = ".amduatd.edges"; 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\":\"HEAD\",\"path\":\"/v1/meta\"},"
"{\"method\":\"GET\",\"path\":\"/v1/contract\"}," "{\"method\":\"GET\",\"path\":\"/v1/contract\"},"
"{\"method\":\"POST\",\"path\":\"/v1/concepts\"}," "{\"method\":\"POST\",\"path\":\"/v1/concepts\"},"
"{\"method\":\"GET\",\"path\":\"/v1/concepts\"},"
"{\"method\":\"GET\",\"path\":\"/v1/concepts/{name}\"},"
"{\"method\":\"POST\",\"path\":\"/v1/concepts/{name}/publish\"}," "{\"method\":\"POST\",\"path\":\"/v1/concepts/{name}/publish\"},"
"{\"method\":\"GET\",\"path\":\"/v1/resolve/{name}\"}," "{\"method\":\"GET\",\"path\":\"/v1/resolve/{name}\"},"
"{\"method\":\"POST\",\"path\":\"/v1/artifacts\"}," "{\"method\":\"POST\",\"path\":\"/v1/artifacts\"},"
@ -228,6 +251,12 @@ static const char k_amduatd_ui_html[] =
" <div class=\"row\" style=\"margin-top:10px;\">\n" " <div class=\"row\" style=\"margin-top:10px;\">\n"
" <button id=\"btnConceptCreate\" type=\"button\">Create concept</button>\n" " <button id=\"btnConceptCreate\" type=\"button\">Create concept</button>\n"
" <button id=\"btnConceptPublish\" type=\"button\">Publish program_ref</button>\n" " <button id=\"btnConceptPublish\" type=\"button\">Publish program_ref</button>\n"
" <button id=\"btnConceptLoad\" type=\"button\">Load</button>\n"
" </div>\n"
" <div style=\"margin:10px 0 8px;\" class=\"muted\">Upload bytes (sets program_ref)</div>\n"
" <div class=\"row\">\n"
" <input id=\"uploadFile\" type=\"file\" />\n"
" <button id=\"btnUpload\" type=\"button\">Upload</button>\n"
" </div>\n" " </div>\n"
" <div class=\"row\" style=\"margin-top:12px;\">\n" " <div class=\"row\" style=\"margin-top:12px;\">\n"
" <button id=\"btnRun\" type=\"button\">Run program</button>\n" " <button id=\"btnRun\" type=\"button\">Run program</button>\n"
@ -307,6 +336,36 @@ static const char k_amduatd_ui_html[] =
" }\n" " }\n"
" });\n" " });\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" " el('btnRun').addEventListener('click', async () => {\n"
" try {\n" " try {\n"
" const program_ref = el('programRef').value.trim();\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; 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) { static bool amduatd_read_exact(int fd, uint8_t *buf, size_t len) {
size_t off = 0; size_t off = 0;
while (off < len) { while (off < len) {
@ -1182,11 +1516,6 @@ static const char *amduatd_query_param(const char *path,
return NULL; return NULL;
} }
typedef struct {
char *data;
size_t len;
size_t cap;
} amduatd_strbuf_t;
static void amduatd_strbuf_free(amduatd_strbuf_t *b) { static void amduatd_strbuf_free(amduatd_strbuf_t *b) {
if (b == NULL) { 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) { if (strcmp(req.method, "POST") == 0 && strcmp(no_query, "/v1/concepts") == 0) {
return amduatd_handle_post_concepts(fd, store, cfg, concepts, &req); 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 && if (strcmp(req.method, "POST") == 0 &&
strncmp(no_query, "/v1/concepts/", 13) == 0 && strncmp(no_query, "/v1/concepts/", 13) == 0 &&
strstr(no_query, "/publish") != NULL) { strstr(no_query, "/publish") != NULL) {