Add /v1/space/workspace workspace snapshot endpoint

This commit is contained in:
Carl Niklas Rydberg 2026-01-24 21:43:40 +01:00
parent ee4397b0d6
commit 3ada8d6a71
8 changed files with 1115 additions and 1 deletions

View file

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

View file

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

View file

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

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

View file

@ -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 <errno.h>
#include <signal.h>
@ -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,

View file

@ -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 <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}

View file

@ -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 <stddef.h>
#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 */

View file

@ -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 <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}