From d045614909a56cb3626f8f6d11b9af1e1cd062eb Mon Sep 17 00:00:00 2001 From: Carl Niklas Rydberg Date: Sun, 25 Jan 2026 05:20:24 +0100 Subject: [PATCH] Fix workspace store capability reporting to reflect backend support --- README.md | 14 +- registry/amduatd-api-contract.v1.json | 53 ++++ registry/api-contract.jsonl | 2 +- src/amduatd.c | 375 +++++++++++++++++++++++++- src/amduatd_space_workspace.c | 75 +++++- src/amduatd_store.c | 27 ++ src/amduatd_store.h | 15 ++ tests/test_amduatd_space_workspace.c | 7 + 8 files changed, 552 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index f580955..a7b8ff4 100644 --- a/README.md +++ b/README.md @@ -324,8 +324,9 @@ curl --unix-socket amduatd.sock \ `/v1/space/workspace` returns a deterministic, read-only snapshot for the effective space. It aggregates the manifest, mount resolution, per-mount cursor -status, store backend metadata, federation flags, and store capabilities into -one JSON response. It performs no network I/O and does not mutate storage. +status, store backend metadata, federation flags, and store capabilities +(`capabilities.supported_ops`) into one JSON response. It performs no network +I/O and does not mutate storage. This is a local snapshot that complements: - `/v1/space/manifest` (manifest root + canonical manifest) @@ -342,9 +343,12 @@ curl --unix-socket amduatd.sock \ ## Workspace UI `/workspace` serves a minimal, human-facing page that consumes -`/v1/space/workspace` and `/v1/space/mounts/sync/until`. It is a convenience -view for inspection and manual sync control, not a stable API. For -programmatic use, call the `/v1/*` endpoints directly. +`/v1/space/workspace` and `/v1/space/mounts/sync/until`, plus read-only +health panels for `/v1/space/doctor`, `/v1/space/roots`, +`/v1/space/sync/status`, `/v1/space/mounts/resolve`, and +`/v1/space/manifest`. It is a convenience view for inspection and manual sync +control, not a stable API. For programmatic use, call the `/v1/*` endpoints +directly. ## Space mounts sync (track mounts) diff --git a/registry/amduatd-api-contract.v1.json b/registry/amduatd-api-contract.v1.json index e895acc..7d655ed 100644 --- a/registry/amduatd-api-contract.v1.json +++ b/registry/amduatd-api-contract.v1.json @@ -384,6 +384,59 @@ }, "peers": {"type": "array", "items": {"$ref": "#/schemas/space_sync_status_peer"}} } + }, + "space_workspace_response": { + "type": "object", + "required": ["effective_space", "store_backend", "federation", "capabilities", "manifest_ref", "manifest", "mounts"], + "properties": { + "effective_space": {"type": "object"}, + "store_backend": {"type": "string"}, + "federation": { + "type": "object", + "required": ["enabled", "transport"], + "properties": { + "enabled": {"type": "boolean"}, + "transport": {"type": "string"} + } + }, + "capabilities": { + "type": "object", + "required": ["supported_ops"], + "properties": { + "supported_ops": { + "type": "object", + "properties": { + "put": {"type": "boolean"}, + "get": {"type": "boolean"}, + "put_indexed": {"type": "boolean"}, + "get_indexed": {"type": "boolean"}, + "tombstone": {"type": "boolean"}, + "tombstone_lift": {"type": "boolean"}, + "log_scan": {"type": "boolean"}, + "current_state": {"type": "boolean"}, + "validate_config": {"type": "boolean"} + } + }, + "implemented_ops": { + "type": "object", + "properties": { + "put": {"type": "boolean"}, + "get": {"type": "boolean"}, + "put_indexed": {"type": "boolean"}, + "get_indexed": {"type": "boolean"}, + "tombstone": {"type": "boolean"}, + "tombstone_lift": {"type": "boolean"}, + "log_scan": {"type": "boolean"}, + "current_state": {"type": "boolean"}, + "validate_config": {"type": "boolean"} + } + } + } + }, + "manifest_ref": {"type": "string"}, + "manifest": {"type": "object"}, + "mounts": {"type": "array"} + } } } } diff --git a/registry/api-contract.jsonl b/registry/api-contract.jsonl index e688dc0..12abf41 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":"88c7e93034dacc34318b0771f1bae232bb4a34913f7274d7e99b007fe4f697c3","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":"38cb6beb6bb525d892538dad7aa584b3f2aeaaff177757fd9432fce9602f877b","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 453c4f6..7c722bc 100644 --- a/src/amduatd.c +++ b/src/amduatd.c @@ -233,7 +233,79 @@ static const char k_amduatd_workspace_html[] = "

Raw workspace JSON

\n" "
\n"
     "    \n"
-    "  \n"
+    "\n"
+    "    
\n" + "

Health / Debug

\n" + "

Fetch read-only diagnostics for the current space.

\n" + "\n" + "
\n" + "

Doctor

\n" + "
\n" + " \n" + " idle\n" + " \n" + "
\n" + " \n" + "
\n" + "\n" + "
\n" + "

Roots

\n" + "
\n" + " \n" + " idle\n" + " \n" + "
\n" + "
\n" + " \n" + "
\n" + "\n" + "
\n" + "

Sync status

\n" + "
\n" + " \n" + " idle\n" + " \n" + "
\n" + "
\n" + " \n" + "
\n" + "\n" + "
\n" + "

Mounts resolve

\n" + "
\n" + " \n" + " idle\n" + " \n" + "
\n" + " \n" + "
\n" + "\n" + "
\n" + "

Manifest

\n" + "
\n" + " \n" + " idle\n" + " \n" + "
\n" + " \n" + "
\n" + "
\n" + " \n" "\n" " \n" - "\n" - "\n"; + " \n" + "\n" + "\n"; static const char k_amduatd_contract_v1_json[] = "{" "\"contract\":\"AMDUATD/API/1\"," @@ -785,6 +1093,59 @@ static const char k_amduatd_contract_v1_json[] = "}," "\"peers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/schemas/space_sync_status_peer\"}}" "}" + "}," + "\"space_workspace_response\":{" + "\"type\":\"object\"," + "\"required\":[\"effective_space\",\"store_backend\",\"federation\",\"capabilities\",\"manifest_ref\",\"manifest\",\"mounts\"]," + "\"properties\":{" + "\"effective_space\":{\"type\":\"object\"}," + "\"store_backend\":{\"type\":\"string\"}," + "\"federation\":{" + "\"type\":\"object\"," + "\"required\":[\"enabled\",\"transport\"]," + "\"properties\":{" + "\"enabled\":{\"type\":\"boolean\"}," + "\"transport\":{\"type\":\"string\"}" + "}" + "}," + "\"capabilities\":{" + "\"type\":\"object\"," + "\"required\":[\"supported_ops\"]," + "\"properties\":{" + "\"supported_ops\":{" + "\"type\":\"object\"," + "\"properties\":{" + "\"put\":{\"type\":\"boolean\"}," + "\"get\":{\"type\":\"boolean\"}," + "\"put_indexed\":{\"type\":\"boolean\"}," + "\"get_indexed\":{\"type\":\"boolean\"}," + "\"tombstone\":{\"type\":\"boolean\"}," + "\"tombstone_lift\":{\"type\":\"boolean\"}," + "\"log_scan\":{\"type\":\"boolean\"}," + "\"current_state\":{\"type\":\"boolean\"}," + "\"validate_config\":{\"type\":\"boolean\"}" + "}" + "}," + "\"implemented_ops\":{" + "\"type\":\"object\"," + "\"properties\":{" + "\"put\":{\"type\":\"boolean\"}," + "\"get\":{\"type\":\"boolean\"}," + "\"put_indexed\":{\"type\":\"boolean\"}," + "\"get_indexed\":{\"type\":\"boolean\"}," + "\"tombstone\":{\"type\":\"boolean\"}," + "\"tombstone_lift\":{\"type\":\"boolean\"}," + "\"log_scan\":{\"type\":\"boolean\"}," + "\"current_state\":{\"type\":\"boolean\"}," + "\"validate_config\":{\"type\":\"boolean\"}" + "}" + "}" + "}" + "}," + "\"manifest_ref\":{\"type\":\"string\"}," + "\"manifest\":{\"type\":\"object\"}," + "\"mounts\":{\"type\":\"array\"}" + "}" "}" "}" "}\n"; diff --git a/src/amduatd_space_workspace.c b/src/amduatd_space_workspace.c index f7a0153..033be6f 100644 --- a/src/amduatd_space_workspace.c +++ b/src/amduatd_space_workspace.c @@ -92,12 +92,81 @@ static bool amduatd_workspace_buf_append_char(amduatd_workspace_buf_t *b, static bool amduatd_workspace_append_capabilities( amduatd_workspace_buf_t *b, - const amduat_asl_store_t *store) { + const amduat_asl_store_t *store, + amduatd_store_backend_t store_backend) { const amduat_asl_store_ops_t *ops = store != NULL ? &store->ops : NULL; + amduatd_store_caps_t caps; + if (!amduatd_store_caps_supported(store_backend, &caps)) { + memset(&caps, 0, sizeof(caps)); + } if (!amduatd_workspace_buf_append_cstr(b, ",\"capabilities\":{")) { return false; } - if (!amduatd_workspace_buf_append_cstr(b, "\"store_ops\":{")) { + if (!amduatd_workspace_buf_append_cstr(b, "\"supported_ops\":{")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr( + b, + "\"put\":") || + !amduatd_workspace_buf_append_cstr(b, caps.put ? "true" : "false")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr( + b, + ",\"get\":") || + !amduatd_workspace_buf_append_cstr(b, caps.get ? "true" : "false")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr( + b, + ",\"put_indexed\":") || + !amduatd_workspace_buf_append_cstr( + b, caps.put_indexed ? "true" : "false")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr( + b, + ",\"get_indexed\":") || + !amduatd_workspace_buf_append_cstr( + b, caps.get_indexed ? "true" : "false")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr( + b, + ",\"tombstone\":") || + !amduatd_workspace_buf_append_cstr( + b, caps.tombstone ? "true" : "false")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr( + b, + ",\"tombstone_lift\":") || + !amduatd_workspace_buf_append_cstr( + b, caps.tombstone_lift ? "true" : "false")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr( + b, + ",\"log_scan\":") || + !amduatd_workspace_buf_append_cstr( + b, caps.log_scan ? "true" : "false")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr( + b, + ",\"current_state\":") || + !amduatd_workspace_buf_append_cstr( + b, caps.current_state ? "true" : "false")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr( + b, + ",\"validate_config\":") || + !amduatd_workspace_buf_append_cstr( + b, caps.validate_config ? "true" : "false")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr(b, "},\"implemented_ops\":{")) { return false; } if (!amduatd_workspace_buf_append_cstr( @@ -428,7 +497,7 @@ amduatd_space_workspace_status_t amduatd_space_workspace_get( if (!amduatd_workspace_buf_append_cstr(&b, "}")) { goto workspace_cleanup; } - if (!amduatd_workspace_append_capabilities(&b, store)) { + if (!amduatd_workspace_append_capabilities(&b, store, store_backend)) { goto workspace_cleanup; } if (!amduatd_workspace_buf_append_cstr(&b, ",\"manifest_ref\":\"") || diff --git a/src/amduatd_store.c b/src/amduatd_store.c index 5ef3818..e24d2f3 100644 --- a/src/amduatd_store.c +++ b/src/amduatd_store.c @@ -31,6 +31,33 @@ const char *amduatd_store_backend_name(amduatd_store_backend_t backend) { } } +bool amduatd_store_caps_supported(amduatd_store_backend_t backend, + amduatd_store_caps_t *out_caps) { + if (out_caps == NULL) { + return false; + } + memset(out_caps, 0, sizeof(*out_caps)); + if (backend == AMDUATD_STORE_BACKEND_FS) { + out_caps->get = true; + out_caps->put = true; + out_caps->validate_config = true; + return true; + } + if (backend == AMDUATD_STORE_BACKEND_INDEX) { + out_caps->get = true; + out_caps->put = true; + out_caps->get_indexed = true; + out_caps->put_indexed = true; + out_caps->log_scan = true; + out_caps->current_state = true; + out_caps->tombstone = true; + out_caps->tombstone_lift = true; + out_caps->validate_config = true; + return true; + } + return true; +} + bool amduatd_store_init(amduat_asl_store_t *store, amduat_asl_store_fs_config_t *cfg, amduatd_store_ctx_t *ctx, diff --git a/src/amduatd_store.h b/src/amduatd_store.h index 574810b..43690c1 100644 --- a/src/amduatd_store.h +++ b/src/amduatd_store.h @@ -22,11 +22,26 @@ typedef struct { amduat_asl_store_index_fs_t index_fs; } amduatd_store_ctx_t; +typedef struct { + bool get; + bool put; + bool get_indexed; + bool put_indexed; + bool log_scan; + bool current_state; + bool tombstone; + bool tombstone_lift; + bool validate_config; +} amduatd_store_caps_t; + bool amduatd_store_backend_parse(const char *value, amduatd_store_backend_t *out_backend); const char *amduatd_store_backend_name(amduatd_store_backend_t backend); +bool amduatd_store_caps_supported(amduatd_store_backend_t backend, + amduatd_store_caps_t *out_caps); + bool amduatd_store_init(amduat_asl_store_t *store, amduat_asl_store_fs_config_t *cfg, amduatd_store_ctx_t *ctx, diff --git a/tests/test_amduatd_space_workspace.c b/tests/test_amduatd_space_workspace.c index a5bd1d8..457a5cf 100644 --- a/tests/test_amduatd_space_workspace.c +++ b/tests/test_amduatd_space_workspace.c @@ -375,6 +375,13 @@ static int amduatd_test_workspace_snapshot(void) { "store backend present"); expect(strstr(workspace_json, "\"transport\":\"stub\"") != NULL, "federation transport present"); + expect(strstr(workspace_json, + "\"supported_ops\":{\"put\":true,\"get\":true," + "\"put_indexed\":false,\"get_indexed\":false," + "\"tombstone\":false,\"tombstone_lift\":false," + "\"log_scan\":false,\"current_state\":false," + "\"validate_config\":true}") != NULL, + "supported ops reflect fs backend"); } free(workspace_json);