diff --git a/CMakeLists.txt b/CMakeLists.txt index 90c31ff..333ac31 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ set(amduatd_sources src/amduatd.c src/amduatd_http.c src/amduatd_caps.c src/amduatd_space_doctor.c src/amduatd_space_roots.c src/amduatd_space_manifest.c src/amduatd_space_mounts.c + src/amduatd_space_workspace.c src/amduatd_space_mounts_sync.c) if(AMDUATD_ENABLE_UI) list(APPEND amduatd_sources src/amduatd_ui.c) @@ -415,6 +416,31 @@ target_link_libraries(amduatd_test_space_mounts add_test(NAME amduatd_space_mounts COMMAND amduatd_test_space_mounts) +add_executable(amduatd_test_space_workspace + tests/test_amduatd_space_workspace.c + src/amduatd_space_workspace.c + src/amduatd_space_manifest.c + src/amduatd_http.c + src/amduatd_fed_cursor.c + src/amduatd_fed.c + src/amduatd_space.c + src/amduatd_store.c +) + +target_include_directories(amduatd_test_space_workspace + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include +) + +target_link_libraries(amduatd_test_space_workspace + PRIVATE amduat_asl_store_fs amduat_asl_pointer_fs amduat_asl_record + amduat_asl_store_index_fs amduat_asl amduat_enc amduat_util + amduat_hash_asl1 +) + +add_test(NAME amduatd_space_workspace COMMAND amduatd_test_space_workspace) + add_executable(amduatd_test_space_mounts_sync tests/test_amduatd_space_mounts_sync.c src/amduatd_space_mounts_sync.c diff --git a/README.md b/README.md index 6715fb9..f65eb44 100644 --- a/README.md +++ b/README.md @@ -320,6 +320,25 @@ curl --unix-socket amduatd.sock \ -H 'X-Amduat-Space: demo' ``` +## Space workspace snapshot + +`/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. + +This is a local snapshot that complements: +- `/v1/space/manifest` (manifest root + canonical manifest) +- `/v1/space/mounts/resolve` (resolved mounts + local tracking) +- `/v1/space/sync/status` (peer-wide cursor status) +- `/v1/space/mounts/sync/until` (active sync for track mounts) + +```sh +curl --unix-socket amduatd.sock \ + 'http://localhost/v1/space/workspace' \ + -H 'X-Amduat-Space: demo' +``` + ## Space mounts sync (track mounts) `/v1/space/mounts/sync/until` runs the federation pull/until loop for every @@ -500,6 +519,7 @@ When the daemon uses the `fs` store backend, index-only checks are reported as - `GET /v1/space/manifest` → `{effective_space, manifest_ref, manifest}` - `PUT /v1/space/manifest` → `{effective_space, manifest_ref, updated, previous_ref?, manifest}` - `GET /v1/space/mounts/resolve` → `{effective_space, manifest_ref, mounts}` +- `GET /v1/space/workspace` → `{effective_space, store_backend, federation, capabilities, manifest_ref, manifest, mounts}` - `POST /v1/space/mounts/sync/until?limit=...&max_rounds=...&max_mounts=...` → `{effective_space, manifest_ref, limit, max_rounds, max_mounts, mounts_total, mounts_synced, ok, results}` - `GET /v1/space/sync/status` → `{effective_space, store_backend, federation, peers}` - `GET /v1/ui` → browser UI for authoring/running programs diff --git a/registry/amduatd-api-contract.v1.json b/registry/amduatd-api-contract.v1.json index ea4dd3a..e895acc 100644 --- a/registry/amduatd-api-contract.v1.json +++ b/registry/amduatd-api-contract.v1.json @@ -11,6 +11,7 @@ {"method": "GET", "path": "/v1/space/manifest"}, {"method": "PUT", "path": "/v1/space/manifest"}, {"method": "GET", "path": "/v1/space/mounts/resolve"}, + {"method": "GET", "path": "/v1/space/workspace"}, {"method": "POST", "path": "/v1/space/mounts/sync/until"}, {"method": "GET", "path": "/v1/space/sync/status"}, {"method": "POST", "path": "/v1/capabilities"}, diff --git a/registry/api-contract.jsonl b/registry/api-contract.jsonl index 0505d7f..e688dc0 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":"e4f8bfbb6704576a9d90a39be6b1c484c0db1e69c6464d36a947ec3b79cbf6ba","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":"88c7e93034dacc34318b0771f1bae232bb4a34913f7274d7e99b007fe4f697c3","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 3b5d4f9..1415fac 100644 --- a/src/amduatd.c +++ b/src/amduatd.c @@ -54,6 +54,7 @@ #include "amduatd_space_manifest.h" #include "amduatd_space_mounts.h" #include "amduatd_space_mounts_sync.h" +#include "amduatd_space_workspace.h" #include #include @@ -146,6 +147,7 @@ static const char k_amduatd_contract_v1_json[] = "{\"method\":\"GET\",\"path\":\"/v1/space/manifest\"}," "{\"method\":\"PUT\",\"path\":\"/v1/space/manifest\"}," "{\"method\":\"GET\",\"path\":\"/v1/space/mounts/resolve\"}," + "{\"method\":\"GET\",\"path\":\"/v1/space/workspace\"}," "{\"method\":\"POST\",\"path\":\"/v1/space/mounts/sync/until\"}," "{\"method\":\"GET\",\"path\":\"/v1/space/sync/status\"}," "{\"method\":\"POST\",\"path\":\"/v1/capabilities\"}," @@ -1846,6 +1848,72 @@ mounts_sync_cleanup: return ok; } +static bool amduatd_handle_get_space_workspace( + int fd, + amduat_asl_store_t *store, + const amduatd_http_req_t *req, + const amduatd_cfg_t *dcfg, + const amduatd_fed_cfg_t *fed_cfg, + const amduatd_caps_t *caps, + const char *root_path, + amduatd_store_backend_t store_backend) { + amduat_asl_pointer_store_t pointer_store; + amduatd_space_workspace_status_t status; + char *workspace_json = NULL; + size_t workspace_len = 0u; + bool ok = false; + + if (store == NULL || req == NULL || dcfg == NULL || fed_cfg == NULL || + root_path == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "internal error"); + } + + if (caps != NULL && caps->enabled && req->x_capability[0] != '\0') { + const char *reason = NULL; + if (!amduatd_caps_check_space(caps, dcfg, req, &reason)) { + if (reason != NULL && strcmp(reason, "wrong-space") == 0) { + return amduatd_send_json_error(fd, 403, "Forbidden", + "space not permitted by capability"); + } + return amduatd_send_json_error(fd, 403, "Forbidden", + "invalid capability"); + } + } + + if (!amduat_asl_pointer_store_init(&pointer_store, root_path)) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "pointer store error"); + } + + status = amduatd_space_workspace_get(store, + &pointer_store, + req->effective_space, + fed_cfg, + store_backend, + &workspace_json, + &workspace_len); + if (status == AMDUATD_SPACE_WORKSPACE_ERR_NOT_FOUND) { + return amduatd_send_json_error(fd, 404, "Not Found", + "manifest not found"); + } + if (status == AMDUATD_SPACE_WORKSPACE_ERR_STORE) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "store error"); + } + if (status != AMDUATD_SPACE_WORKSPACE_OK) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "workspace decode failed"); + } + + ok = amduatd_http_send_json(fd, 200, "OK", workspace_json, false); + free(workspace_json); + if (!ok) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "error"); + } + return ok; +} + static bool amduatd_handle_put_space_manifest( int fd, amduat_asl_store_t *store, @@ -9081,6 +9149,18 @@ static bool amduatd_handle_conn(int fd, root_path); goto conn_cleanup; } + if (strcmp(req.method, "GET") == 0 && + strcmp(no_query, "/v1/space/workspace") == 0) { + ok = amduatd_handle_get_space_workspace(fd, + store, + &req, + effective_cfg, + fed_cfg, + caps, + root_path, + store_backend); + goto conn_cleanup; + } if (strcmp(req.method, "PUT") == 0 && strcmp(no_query, "/v1/space/manifest") == 0) { ok = amduatd_handle_put_space_manifest(fd, diff --git a/src/amduatd_space_workspace.c b/src/amduatd_space_workspace.c new file mode 100644 index 0000000..f7a0153 --- /dev/null +++ b/src/amduatd_space_workspace.c @@ -0,0 +1,552 @@ +#include "amduatd_space_workspace.h" + +#include "amduat/asl/ref_text.h" +#include "amduatd_fed_cursor.h" +#include "amduatd_space_manifest.h" + +#include +#include +#include +#include + +typedef struct { + char *data; + size_t len; + size_t cap; +} amduatd_workspace_buf_t; + +static void amduatd_workspace_buf_free(amduatd_workspace_buf_t *b) { + if (b == NULL) { + return; + } + free(b->data); + b->data = NULL; + b->len = 0; + b->cap = 0; +} + +static bool amduatd_workspace_buf_reserve(amduatd_workspace_buf_t *b, + size_t extra) { + size_t need; + size_t next_cap; + char *next; + + if (b == NULL) { + return false; + } + if (extra > (SIZE_MAX - b->len)) { + return false; + } + need = b->len + extra; + if (need <= b->cap) { + return true; + } + next_cap = b->cap != 0u ? b->cap : 256u; + while (next_cap < need) { + if (next_cap > (SIZE_MAX / 2u)) { + next_cap = need; + break; + } + next_cap *= 2u; + } + next = (char *)realloc(b->data, next_cap); + if (next == NULL) { + return false; + } + b->data = next; + b->cap = next_cap; + return true; +} + +static bool amduatd_workspace_buf_append(amduatd_workspace_buf_t *b, + const char *s, + size_t n) { + if (b == NULL) { + return false; + } + if (n == 0u) { + return true; + } + if (s == NULL) { + return false; + } + if (!amduatd_workspace_buf_reserve(b, n + 1u)) { + return false; + } + memcpy(b->data + b->len, s, n); + b->len += n; + b->data[b->len] = '\0'; + return true; +} + +static bool amduatd_workspace_buf_append_cstr(amduatd_workspace_buf_t *b, + const char *s) { + return amduatd_workspace_buf_append( + b, s != NULL ? s : "", s != NULL ? strlen(s) : 0u); +} + +static bool amduatd_workspace_buf_append_char(amduatd_workspace_buf_t *b, + char c) { + return amduatd_workspace_buf_append(b, &c, 1u); +} + +static bool amduatd_workspace_append_capabilities( + amduatd_workspace_buf_t *b, + const amduat_asl_store_t *store) { + const amduat_asl_store_ops_t *ops = store != NULL ? &store->ops : NULL; + if (!amduatd_workspace_buf_append_cstr(b, ",\"capabilities\":{")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr(b, "\"store_ops\":{")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr( + b, + "\"put\":") || + !amduatd_workspace_buf_append_cstr( + b, (ops != NULL && ops->put != NULL) ? "true" : "false")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr( + b, + ",\"get\":") || + !amduatd_workspace_buf_append_cstr( + b, (ops != NULL && ops->get != NULL) ? "true" : "false")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr( + b, + ",\"put_indexed\":") || + !amduatd_workspace_buf_append_cstr( + b, (ops != NULL && ops->put_indexed != NULL) ? "true" : "false")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr( + b, + ",\"get_indexed\":") || + !amduatd_workspace_buf_append_cstr( + b, (ops != NULL && ops->get_indexed != NULL) ? "true" : "false")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr( + b, + ",\"tombstone\":") || + !amduatd_workspace_buf_append_cstr( + b, (ops != NULL && ops->tombstone != NULL) ? "true" : "false")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr( + b, + ",\"tombstone_lift\":") || + !amduatd_workspace_buf_append_cstr( + b, (ops != NULL && ops->tombstone_lift != NULL) ? "true" : "false")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr( + b, + ",\"log_scan\":") || + !amduatd_workspace_buf_append_cstr( + b, (ops != NULL && ops->log_scan != NULL) ? "true" : "false")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr( + b, + ",\"current_state\":") || + !amduatd_workspace_buf_append_cstr( + b, (ops != NULL && ops->current_state != NULL) ? "true" : "false")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr( + b, + ",\"validate_config\":") || + !amduatd_workspace_buf_append_cstr( + b, + (ops != NULL && ops->validate_config != NULL) ? "true" : "false")) { + return false; + } + if (!amduatd_workspace_buf_append_cstr(b, "}}")) { + return false; + } + return true; +} + +static amduatd_space_workspace_status_t amduatd_workspace_append_tracking( + amduatd_workspace_buf_t *b, + amduat_asl_store_t *store, + amduat_asl_pointer_store_t *pointer_store, + const amduatd_space_t *effective_space, + const amduatd_space_manifest_mount_t *mount, + bool allow_cursor_lookup) { + amduatd_fed_cursor_record_t cursor; + amduat_reference_t cursor_ref; + amduatd_fed_cursor_status_t status; + bool cursor_present = false; + char *cursor_ref_hex = NULL; + const char *cursor_keying = + mount->mode == AMDUATD_SPACE_MANIFEST_MOUNT_TRACK ? "v2" : "none"; + + amduatd_fed_cursor_record_init(&cursor); + cursor_ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + + if (allow_cursor_lookup && + mount->mode == AMDUATD_SPACE_MANIFEST_MOUNT_TRACK) { + status = amduatd_fed_cursor_get_remote(store, + pointer_store, + effective_space, + mount->peer_key, + mount->space_id, + &cursor, + &cursor_ref); + if (status == AMDUATD_FED_CURSOR_ERR_NOT_FOUND) { + cursor_present = false; + } else if (status == AMDUATD_FED_CURSOR_OK) { + cursor_present = true; + } else if (status == AMDUATD_FED_CURSOR_ERR_CODEC) { + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + return AMDUATD_SPACE_WORKSPACE_ERR_CODEC; + } else { + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + return AMDUATD_SPACE_WORKSPACE_ERR_STORE; + } + } + + if (!amduatd_workspace_buf_append_cstr(b, ",\"tracking\":{") || + !amduatd_workspace_buf_append_cstr(b, "\"cursor_keying\":\"") || + !amduatd_workspace_buf_append_cstr(b, cursor_keying) || + !amduatd_workspace_buf_append_cstr(b, "\",\"pull_cursor\":{") || + !amduatd_workspace_buf_append_cstr( + b, + cursor_present ? "\"present\":true" : "\"present\":false")) { + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + return AMDUATD_SPACE_WORKSPACE_ERR_STORE; + } + + if (cursor_present && cursor.has_logseq) { + char tmp[32]; + int n = snprintf(tmp, sizeof(tmp), "%llu", + (unsigned long long)cursor.last_logseq); + if (n <= 0 || (size_t)n >= sizeof(tmp)) { + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + return AMDUATD_SPACE_WORKSPACE_ERR_STORE; + } + if (!amduatd_workspace_buf_append_cstr(b, ",\"last_logseq\":") || + !amduatd_workspace_buf_append_cstr(b, tmp)) { + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + return AMDUATD_SPACE_WORKSPACE_ERR_STORE; + } + } + + if (cursor_present && cursor.has_record_ref) { + if (!amduat_asl_ref_encode_hex(cursor.last_record_ref, &cursor_ref_hex)) { + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + return AMDUATD_SPACE_WORKSPACE_ERR_STORE; + } + if (!amduatd_workspace_buf_append_cstr(b, ",\"ref\":\"") || + !amduatd_workspace_buf_append_cstr(b, cursor_ref_hex) || + !amduatd_workspace_buf_append_cstr(b, "\"")) { + free(cursor_ref_hex); + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + return AMDUATD_SPACE_WORKSPACE_ERR_STORE; + } + free(cursor_ref_hex); + } + + if (!amduatd_workspace_buf_append_cstr(b, "}}")) { + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + return AMDUATD_SPACE_WORKSPACE_ERR_STORE; + } + + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + return AMDUATD_SPACE_WORKSPACE_OK; +} + +static bool amduatd_workspace_append_status(amduatd_workspace_buf_t *b, + bool ok, + bool note_peer, + bool note_remote, + bool note_pinned_missing, + bool note_pinned_unexpected) { + bool first_note = true; + + if (!amduatd_workspace_buf_append_cstr(b, ",\"status\":{") || + !amduatd_workspace_buf_append_cstr(b, "\"ok\":") || + !amduatd_workspace_buf_append_cstr(b, ok ? "true" : "false")) { + return false; + } + if (note_peer || note_remote || note_pinned_missing || note_pinned_unexpected) { + if (!amduatd_workspace_buf_append_cstr(b, ",\"notes\":[")) { + return false; + } + if (note_peer) { + if (!amduatd_workspace_buf_append_cstr( + b, first_note ? "\"invalid_peer_key\"" : ",\"invalid_peer_key\"")) { + return false; + } + first_note = false; + } + if (note_remote) { + if (!amduatd_workspace_buf_append_cstr( + b, + first_note ? "\"invalid_remote_space_id\"" + : ",\"invalid_remote_space_id\"")) { + return false; + } + first_note = false; + } + if (note_pinned_missing) { + if (!amduatd_workspace_buf_append_cstr( + b, + first_note ? "\"missing_pinned_root_ref\"" + : ",\"missing_pinned_root_ref\"")) { + return false; + } + first_note = false; + } + if (note_pinned_unexpected) { + if (!amduatd_workspace_buf_append_cstr( + b, + first_note ? "\"unexpected_pinned_root_ref\"" + : ",\"unexpected_pinned_root_ref\"")) { + return false; + } + first_note = false; + } + if (!amduatd_workspace_buf_append_cstr(b, "]")) { + return false; + } + } + if (!amduatd_workspace_buf_append_cstr(b, "}")) { + return false; + } + return true; +} + +amduatd_space_workspace_status_t amduatd_space_workspace_get( + amduat_asl_store_t *store, + amduat_asl_pointer_store_t *pointer_store, + const amduatd_space_t *effective_space, + const amduatd_fed_cfg_t *fed_cfg, + amduatd_store_backend_t store_backend, + char **out_json, + size_t *out_len) { + amduatd_space_manifest_t manifest; + amduat_reference_t manifest_ref; + amduatd_space_manifest_status_t status; + amduatd_workspace_buf_t b; + char *manifest_ref_hex = NULL; + char *manifest_json = NULL; + size_t manifest_len = 0u; + amduatd_space_workspace_status_t workspace_err = + AMDUATD_SPACE_WORKSPACE_ERR_STORE; + + if (out_json != NULL) { + *out_json = NULL; + } + if (out_len != NULL) { + *out_len = 0u; + } + if (store == NULL || pointer_store == NULL || fed_cfg == NULL || + out_json == NULL || out_len == NULL) { + return AMDUATD_SPACE_WORKSPACE_ERR_INVALID; + } + + memset(&manifest, 0, sizeof(manifest)); + memset(&manifest_ref, 0, sizeof(manifest_ref)); + memset(&b, 0, sizeof(b)); + + status = amduatd_space_manifest_get(store, + pointer_store, + effective_space, + &manifest_ref, + &manifest); + if (status == AMDUATD_SPACE_MANIFEST_ERR_NOT_FOUND) { + return AMDUATD_SPACE_WORKSPACE_ERR_NOT_FOUND; + } + if (status == AMDUATD_SPACE_MANIFEST_ERR_STORE) { + return AMDUATD_SPACE_WORKSPACE_ERR_STORE; + } + if (status != AMDUATD_SPACE_MANIFEST_OK) { + return AMDUATD_SPACE_WORKSPACE_ERR_CODEC; + } + + if (!amduat_asl_ref_encode_hex(manifest_ref, &manifest_ref_hex)) { + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&manifest_ref); + return AMDUATD_SPACE_WORKSPACE_ERR_STORE; + } + if (!amduatd_space_manifest_encode_json(&manifest, + &manifest_json, + &manifest_len)) { + free(manifest_ref_hex); + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&manifest_ref); + return AMDUATD_SPACE_WORKSPACE_ERR_CODEC; + } + + if (!amduatd_workspace_buf_append_cstr(&b, "{\"effective_space\":{")) { + goto workspace_cleanup; + } + if (effective_space != NULL && effective_space->enabled && + effective_space->space_id.data != NULL) { + const char *space_id = (const char *)effective_space->space_id.data; + if (!amduatd_workspace_buf_append_cstr(&b, "\"mode\":\"scoped\",") || + !amduatd_workspace_buf_append_cstr(&b, "\"space_id\":\"") || + !amduatd_workspace_buf_append_cstr(&b, space_id) || + !amduatd_workspace_buf_append_cstr(&b, "\"")) { + goto workspace_cleanup; + } + } else { + if (!amduatd_workspace_buf_append_cstr(&b, "\"mode\":\"unscoped\",") || + !amduatd_workspace_buf_append_cstr(&b, "\"space_id\":null")) { + goto workspace_cleanup; + } + } + if (!amduatd_workspace_buf_append_cstr(&b, "},\"store_backend\":\"") || + !amduatd_workspace_buf_append_cstr( + &b, amduatd_store_backend_name(store_backend)) || + !amduatd_workspace_buf_append_cstr(&b, "\",\"federation\":{")) { + goto workspace_cleanup; + } + if (!amduatd_workspace_buf_append_cstr(&b, "\"enabled\":") || + !amduatd_workspace_buf_append_cstr( + &b, fed_cfg->enabled ? "true" : "false") || + !amduatd_workspace_buf_append_cstr(&b, ",\"transport\":\"") || + !amduatd_workspace_buf_append_cstr( + &b, amduatd_fed_transport_name(fed_cfg->transport_kind)) || + !amduatd_workspace_buf_append_cstr(&b, "\"")) { + goto workspace_cleanup; + } + if (!amduatd_workspace_buf_append_cstr(&b, "}")) { + goto workspace_cleanup; + } + if (!amduatd_workspace_append_capabilities(&b, store)) { + goto workspace_cleanup; + } + if (!amduatd_workspace_buf_append_cstr(&b, ",\"manifest_ref\":\"") || + !amduatd_workspace_buf_append_cstr(&b, manifest_ref_hex) || + !amduatd_workspace_buf_append_cstr(&b, "\",\"manifest\":") || + !amduatd_workspace_buf_append(&b, manifest_json, manifest_len) || + !amduatd_workspace_buf_append_cstr(&b, ",\"mounts\":[")) { + goto workspace_cleanup; + } + + for (size_t i = 0u; i < manifest.mounts_len; ++i) { + const amduatd_space_manifest_mount_t *mount = &manifest.mounts[i]; + char *pinned_hex = NULL; + bool valid_peer = true; + bool valid_remote = true; + bool note_pinned_missing = false; + bool note_pinned_unexpected = false; + bool ok_status = true; + + if (i != 0u) { + if (!amduatd_workspace_buf_append_char(&b, ',')) { + goto workspace_cleanup; + } + } + + if (!amduatd_workspace_buf_append_cstr(&b, "{\"name\":\"") || + !amduatd_workspace_buf_append_cstr(&b, mount->name) || + !amduatd_workspace_buf_append_cstr(&b, "\",\"peer_key\":\"") || + !amduatd_workspace_buf_append_cstr(&b, mount->peer_key) || + !amduatd_workspace_buf_append_cstr(&b, "\",\"remote_space_id\":\"") || + !amduatd_workspace_buf_append_cstr(&b, mount->space_id) || + !amduatd_workspace_buf_append_cstr(&b, "\",\"mode\":\"") || + !amduatd_workspace_buf_append_cstr( + &b, + mount->mode == AMDUATD_SPACE_MANIFEST_MOUNT_PINNED ? "pinned" + : "track") || + !amduatd_workspace_buf_append_cstr(&b, "\"")) { + goto workspace_cleanup; + } + + if (mount->peer_key == NULL || + !amduat_asl_pointer_name_is_valid(mount->peer_key)) { + valid_peer = false; + } + if (mount->space_id == NULL || + !amduatd_space_space_id_is_valid(mount->space_id)) { + valid_remote = false; + } + if (mount->mode == AMDUATD_SPACE_MANIFEST_MOUNT_PINNED && + !mount->has_pinned_root_ref) { + note_pinned_missing = true; + } + if (mount->mode == AMDUATD_SPACE_MANIFEST_MOUNT_TRACK && + mount->has_pinned_root_ref) { + note_pinned_unexpected = true; + } + ok_status = valid_peer && valid_remote && !note_pinned_missing && + !note_pinned_unexpected; + + if (mount->mode == AMDUATD_SPACE_MANIFEST_MOUNT_PINNED && + mount->has_pinned_root_ref) { + if (!amduat_asl_ref_encode_hex(mount->pinned_root_ref, &pinned_hex)) { + goto workspace_cleanup; + } + if (!amduatd_workspace_buf_append_cstr(&b, ",\"pinned_root_ref\":\"") || + !amduatd_workspace_buf_append_cstr(&b, pinned_hex) || + !amduatd_workspace_buf_append_cstr(&b, "\"")) { + free(pinned_hex); + goto workspace_cleanup; + } + free(pinned_hex); + } + + { + amduatd_space_workspace_status_t track_status = + amduatd_workspace_append_tracking(&b, + store, + pointer_store, + effective_space, + mount, + ok_status); + if (track_status != AMDUATD_SPACE_WORKSPACE_OK) { + workspace_err = track_status; + goto workspace_cleanup; + } + } + + if (!amduatd_workspace_append_status(&b, + ok_status, + !valid_peer, + !valid_remote, + note_pinned_missing, + note_pinned_unexpected)) { + goto workspace_cleanup; + } + + if (!amduatd_workspace_buf_append_cstr(&b, "}")) { + goto workspace_cleanup; + } + } + + if (!amduatd_workspace_buf_append_cstr(&b, "]}\n")) { + goto workspace_cleanup; + } + + *out_json = b.data; + *out_len = b.len; + free(manifest_ref_hex); + free(manifest_json); + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&manifest_ref); + return AMDUATD_SPACE_WORKSPACE_OK; + +workspace_cleanup: + amduatd_workspace_buf_free(&b); + free(manifest_ref_hex); + free(manifest_json); + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&manifest_ref); + return workspace_err; +} diff --git a/src/amduatd_space_workspace.h b/src/amduatd_space_workspace.h new file mode 100644 index 0000000..5fc0042 --- /dev/null +++ b/src/amduatd_space_workspace.h @@ -0,0 +1,37 @@ +#ifndef AMDUATD_SPACE_WORKSPACE_H +#define AMDUATD_SPACE_WORKSPACE_H + +#include "amduat/asl/asl_pointer_fs.h" +#include "amduat/asl/store.h" +#include "amduatd_fed.h" +#include "amduatd_space.h" +#include "amduatd_store.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + AMDUATD_SPACE_WORKSPACE_OK = 0, + AMDUATD_SPACE_WORKSPACE_ERR_INVALID = 1, + AMDUATD_SPACE_WORKSPACE_ERR_NOT_FOUND = 2, + AMDUATD_SPACE_WORKSPACE_ERR_STORE = 3, + AMDUATD_SPACE_WORKSPACE_ERR_CODEC = 4 +} amduatd_space_workspace_status_t; + +amduatd_space_workspace_status_t amduatd_space_workspace_get( + amduat_asl_store_t *store, + amduat_asl_pointer_store_t *pointer_store, + const amduatd_space_t *effective_space, + const amduatd_fed_cfg_t *fed_cfg, + amduatd_store_backend_t store_backend, + char **out_json, + size_t *out_len); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* AMDUATD_SPACE_WORKSPACE_H */ diff --git a/tests/test_amduatd_space_workspace.c b/tests/test_amduatd_space_workspace.c new file mode 100644 index 0000000..a5bd1d8 --- /dev/null +++ b/tests/test_amduatd_space_workspace.c @@ -0,0 +1,398 @@ +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif + +#include "amduatd_space_workspace.h" + +#include "amduatd_fed_cursor.h" +#include "amduatd_space.h" +#include "amduatd_space_manifest.h" +#include "amduatd_store.h" + +#include "amduat/asl/asl_pointer_fs.h" +#include "amduat/asl/asl_store_fs_meta.h" +#include "amduat/asl/record.h" +#include "amduat/asl/ref_text.h" +#include "amduat/hash/asl1.h" + +#include +#include +#include +#include + +static int failures = 0; + +static void expect(bool cond, const char *msg) { + if (!cond) { + fprintf(stderr, "FAIL: %s\n", msg); + failures++; + } +} + +static char *amduatd_test_make_temp_dir(void) { + char tmpl[] = "/tmp/amduatd-space-workspace-XXXXXX"; + char *dir = mkdtemp(tmpl); + size_t len; + char *copy; + if (dir == NULL) { + perror("mkdtemp"); + return NULL; + } + len = strlen(dir); + copy = (char *)malloc(len + 1u); + if (copy == NULL) { + fprintf(stderr, "failed to allocate temp dir copy\n"); + return NULL; + } + memcpy(copy, dir, len + 1u); + return copy; +} + +static bool amduatd_make_test_ref(uint8_t fill, amduat_reference_t *out_ref) { + uint8_t digest_bytes[32]; + amduat_octets_t digest; + if (out_ref == NULL) { + return false; + } + memset(digest_bytes, fill, sizeof(digest_bytes)); + if (!amduat_octets_clone(amduat_octets(digest_bytes, sizeof(digest_bytes)), + &digest)) { + return false; + } + *out_ref = amduat_reference(AMDUAT_HASH_ASL1_ID_SHA256, digest); + return true; +} + +static int amduatd_test_workspace_missing(void) { + char *root = amduatd_test_make_temp_dir(); + amduat_asl_store_fs_config_t cfg; + amduatd_store_ctx_t store_ctx; + amduat_asl_store_t store; + amduat_asl_pointer_store_t pointer_store; + amduatd_space_t space; + amduatd_fed_cfg_t fed_cfg; + char *workspace_json = NULL; + size_t workspace_len = 0u; + amduatd_space_workspace_status_t status; + + if (root == NULL) { + return 1; + } + memset(&cfg, 0, sizeof(cfg)); + if (!amduat_asl_store_fs_init_root(root, NULL, &cfg)) { + fprintf(stderr, "failed to init store root\n"); + free(root); + return 1; + } + memset(&store_ctx, 0, sizeof(store_ctx)); + memset(&store, 0, sizeof(store)); + if (!amduatd_store_init(&store, + &cfg, + &store_ctx, + root, + AMDUATD_STORE_BACKEND_FS)) { + fprintf(stderr, "failed to init store\n"); + free(root); + return 1; + } + if (!amduat_asl_pointer_store_init(&pointer_store, root)) { + fprintf(stderr, "failed to init pointer store\n"); + free(root); + return 1; + } + if (!amduatd_space_init(&space, "alpha", false)) { + fprintf(stderr, "failed to init space\n"); + free(root); + return 1; + } + amduatd_fed_cfg_init(&fed_cfg); + + status = amduatd_space_workspace_get(&store, + &pointer_store, + &space, + &fed_cfg, + AMDUATD_STORE_BACKEND_FS, + &workspace_json, + &workspace_len); + expect(status == AMDUATD_SPACE_WORKSPACE_ERR_NOT_FOUND, + "missing manifest returns not found"); + expect(workspace_json == NULL, "workspace json unset on missing"); + free(root); + return failures == 0 ? 0 : 1; +} + +static int amduatd_test_workspace_snapshot(void) { + char *root = amduatd_test_make_temp_dir(); + amduat_asl_store_fs_config_t cfg; + amduatd_store_ctx_t store_ctx; + amduat_asl_store_t store; + amduat_asl_pointer_store_t pointer_store; + amduatd_space_t space; + amduat_reference_t pinned_ref; + char *pinned_hex = NULL; + char payload[512]; + amduat_reference_t record_ref; + amduat_octets_t pointer_name = amduat_octets(NULL, 0u); + bool swapped = false; + amduatd_fed_cursor_record_t cursor; + amduat_reference_t cursor_ref; + amduat_reference_t cursor_last_ref; + char *cursor_last_hex = NULL; + char *manifest_ref_hex = NULL; + amduatd_fed_cfg_t fed_cfg; + char *workspace_json = NULL; + size_t workspace_len = 0u; + char *workspace_json_2 = NULL; + size_t workspace_len_2 = 0u; + amduatd_space_workspace_status_t status; + const char *m0 = NULL; + const char *m1 = NULL; + const char *m2 = NULL; + + if (root == NULL) { + return 1; + } + memset(&cfg, 0, sizeof(cfg)); + if (!amduat_asl_store_fs_init_root(root, NULL, &cfg)) { + fprintf(stderr, "failed to init store root\n"); + free(root); + return 1; + } + memset(&store_ctx, 0, sizeof(store_ctx)); + memset(&store, 0, sizeof(store)); + if (!amduatd_store_init(&store, + &cfg, + &store_ctx, + root, + AMDUATD_STORE_BACKEND_FS)) { + fprintf(stderr, "failed to init store\n"); + free(root); + return 1; + } + if (!amduat_asl_pointer_store_init(&pointer_store, root)) { + fprintf(stderr, "failed to init pointer store\n"); + free(root); + return 1; + } + if (!amduatd_space_init(&space, "alpha", false)) { + fprintf(stderr, "failed to init space\n"); + free(root); + return 1; + } + + if (!amduatd_make_test_ref(0x11, &pinned_ref)) { + fprintf(stderr, "failed to make pinned ref\n"); + free(root); + return 1; + } + if (!amduat_asl_ref_encode_hex(pinned_ref, &pinned_hex)) { + fprintf(stderr, "failed to encode pinned ref\n"); + amduat_reference_free(&pinned_ref); + free(root); + return 1; + } + { + int n = snprintf( + payload, + sizeof(payload), + "{" + "\"version\":1," + "\"mounts\":[" + "{\"name\":\"beta\",\"peer_key\":\"peer-2\",\"space_id\":\"zeta\"," + "\"mode\":\"track\"}," + "{\"name\":\"alpha\",\"peer_key\":\"peer-1\",\"space_id\":\"zeta\"," + "\"mode\":\"pinned\",\"pinned_root_ref\":\"%s\"}," + "{\"name\":\"alpha\",\"peer_key\":\"peer-1\",\"space_id\":\"beta\"," + "\"mode\":\"track\"}" + "]" + "}", + pinned_hex); + if (n <= 0 || (size_t)n >= sizeof(payload)) { + fprintf(stderr, "failed to build manifest payload\n"); + free(pinned_hex); + amduat_reference_free(&pinned_ref); + free(root); + return 1; + } + } + free(pinned_hex); + + if (amduat_asl_record_store_put( + &store, + amduat_octets(AMDUATD_SPACE_MANIFEST_1, + strlen(AMDUATD_SPACE_MANIFEST_1)), + amduat_octets((const uint8_t *)payload, strlen(payload)), + &record_ref) != AMDUAT_ASL_STORE_OK) { + fprintf(stderr, "failed to store manifest record\n"); + amduat_reference_free(&pinned_ref); + free(root); + return 1; + } + + if (!amduatd_space_scope_name(&space, "manifest/head", &pointer_name)) { + fprintf(stderr, "failed to build manifest pointer name\n"); + amduat_reference_free(&record_ref); + amduat_reference_free(&pinned_ref); + free(root); + return 1; + } + + if (amduat_asl_pointer_cas(&pointer_store, + (const char *)pointer_name.data, + false, + NULL, + &record_ref, + &swapped) != AMDUAT_ASL_POINTER_OK || + !swapped) { + fprintf(stderr, "failed to set manifest pointer\n"); + amduat_octets_free(&pointer_name); + amduat_reference_free(&record_ref); + amduat_reference_free(&pinned_ref); + free(root); + return 1; + } + amduat_octets_free(&pointer_name); + + if (!amduatd_make_test_ref(0x22, &cursor_last_ref)) { + fprintf(stderr, "failed to make cursor ref\n"); + amduat_reference_free(&record_ref); + amduat_reference_free(&pinned_ref); + free(root); + return 1; + } + if (!amduat_asl_ref_encode_hex(cursor_last_ref, &cursor_last_hex)) { + fprintf(stderr, "failed to encode cursor ref\n"); + amduat_reference_free(&cursor_last_ref); + amduat_reference_free(&record_ref); + amduat_reference_free(&pinned_ref); + free(root); + return 1; + } + + amduatd_fed_cursor_record_init(&cursor); + cursor.peer_key = strdup("peer-1"); + cursor.space_id = strdup("alpha"); + cursor.has_logseq = true; + cursor.last_logseq = 42u; + cursor.has_record_ref = true; + cursor.last_record_ref = cursor_last_ref; + memset(&cursor_ref, 0, sizeof(cursor_ref)); + if (amduatd_fed_cursor_cas_set_remote(&store, + &pointer_store, + &space, + "peer-1", + "beta", + NULL, + &cursor, + &cursor_ref) != + AMDUATD_FED_CURSOR_OK) { + fprintf(stderr, "failed to set cursor\n"); + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + amduat_reference_free(&record_ref); + amduat_reference_free(&pinned_ref); + free(cursor_last_hex); + free(root); + return 1; + } + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + + if (!amduat_asl_ref_encode_hex(record_ref, &manifest_ref_hex)) { + fprintf(stderr, "failed to encode manifest ref\n"); + amduat_reference_free(&record_ref); + amduat_reference_free(&pinned_ref); + free(cursor_last_hex); + free(root); + return 1; + } + + amduatd_fed_cfg_init(&fed_cfg); + status = amduatd_space_workspace_get(&store, + &pointer_store, + &space, + &fed_cfg, + AMDUATD_STORE_BACKEND_FS, + &workspace_json, + &workspace_len); + expect(status == AMDUATD_SPACE_WORKSPACE_OK, "workspace snapshot ok"); + expect(workspace_json != NULL && workspace_len != 0u, + "workspace json populated"); + + status = amduatd_space_workspace_get(&store, + &pointer_store, + &space, + &fed_cfg, + AMDUATD_STORE_BACKEND_FS, + &workspace_json_2, + &workspace_len_2); + expect(status == AMDUATD_SPACE_WORKSPACE_OK, "workspace snapshot ok (repeat)"); + expect(workspace_json_2 != NULL && workspace_len_2 != 0u, + "workspace json populated (repeat)"); + if (workspace_json != NULL && workspace_json_2 != NULL) { + expect(workspace_len == workspace_len_2, + "workspace output length deterministic"); + if (workspace_len == workspace_len_2) { + expect(memcmp(workspace_json, workspace_json_2, workspace_len) == 0, + "workspace output deterministic"); + } + } + + if (workspace_json != NULL) { + m0 = strstr( + workspace_json, + "\"name\":\"alpha\",\"peer_key\":\"peer-1\",\"remote_space_id\":\"beta\""); + m1 = strstr( + workspace_json, + "\"name\":\"alpha\",\"peer_key\":\"peer-1\",\"remote_space_id\":\"zeta\""); + m2 = strstr( + workspace_json, + "\"name\":\"beta\",\"peer_key\":\"peer-2\",\"remote_space_id\":\"zeta\""); + expect(m0 != NULL && m1 != NULL && m2 != NULL, + "workspace mounts include all entries"); + if (m0 != NULL && m1 != NULL && m2 != NULL) { + expect(m0 < m1 && m1 < m2, "workspace mounts are in canonical order"); + } + expect(strstr(workspace_json, "\"mode\":\"pinned\"") != NULL, + "pinned mode present"); + expect(strstr(workspace_json, "\"pinned_root_ref\":\"") != NULL, + "pinned root ref present"); + expect(strstr(workspace_json, "\"cursor_keying\":\"none\"") != NULL, + "pinned cursor keying present"); + expect(strstr(workspace_json, "\"cursor_keying\":\"v2\"") != NULL, + "track cursor keying present"); + expect(strstr(workspace_json, "\"pull_cursor\":{\"present\":true") != NULL, + "track cursor present true"); + expect(strstr(workspace_json, "\"pull_cursor\":{\"present\":false") != NULL, + "pinned cursor present false"); + expect(strstr(workspace_json, "\"last_logseq\":42") != NULL, + "cursor last_logseq present"); + expect(strstr(workspace_json, cursor_last_hex) != NULL, + "cursor ref present"); + expect(strstr(workspace_json, manifest_ref_hex) != NULL, + "manifest ref present"); + expect(strstr(workspace_json, "\"store_backend\":\"fs\"") != NULL, + "store backend present"); + expect(strstr(workspace_json, "\"transport\":\"stub\"") != NULL, + "federation transport present"); + } + + free(workspace_json); + free(workspace_json_2); + free(manifest_ref_hex); + amduat_reference_free(&record_ref); + amduat_reference_free(&pinned_ref); + free(cursor_last_hex); + free(root); + return failures == 0 ? 0 : 1; +} + +int main(void) { + if (amduatd_test_workspace_missing() != 0) { + return 1; + } + if (amduatd_test_workspace_snapshot() != 0) { + return 1; + } + return failures == 0 ? 0 : 1; +}