Compare commits

...

50 commits
qbits ... main

Author SHA1 Message Date
Carl Niklas Rydberg 0ae2c8d74a jhioåf 2026-02-08 09:54:50 +01:00
Carl Niklas Rydberg 4fa7c32117 Advance amduat to suppress snapshot warning flood 2026-02-08 09:01:20 +01:00
Carl Niklas Rydberg 05565f0c45 Advance amduat to index recovery and stale-head healing fixes 2026-02-08 08:55:46 +01:00
Carl Niklas Rydberg 2386449da7 Pin amduat lock-file and edge-append hardening commits 2026-02-08 08:48:43 +01:00
Carl Niklas Rydberg b6e2724c5a I dont know 2026-02-08 08:28:28 +01:00
Carl Niklas Rydberg ce7d852e71 Restore vendor/amduat pointer to available main commit 2026-02-08 08:14:14 +01:00
Carl Niklas Rydberg a54a42c0bf fixed index locck file error
(cherry picked from commit a8a2ab1efb)
2026-02-08 08:13:10 +01:00
Carl Niklas Rydberg 59be5aee7d Add index backend startup regression coverage and update test script paths
(cherry picked from commit 81c1115db5)
2026-02-08 08:13:10 +01:00
Carl Niklas Rydberg 7fab7d2e47 Merge branch 'apiv2' 2026-02-08 07:44:01 +01:00
Carl Niklas Rydberg cb91cc1569 some tests 2026-02-08 07:33:38 +01:00
Carl Niklas Rydberg cdc00dafc2 some fix 2026-02-08 07:32:20 +01:00
Carl Niklas Rydberg b8c0a6e6d0 Implement v2 graph API surface and contract/test coverage 2026-02-07 19:46:59 +01:00
Carl Niklas Rydberg d1e82e71f9 added submodule amduat 2026-02-05 15:40:59 +01:00
Carl Niklas Rydberg d045614909 Fix workspace store capability reporting to reflect backend support 2026-01-25 05:20:24 +01:00
Carl Niklas Rydberg 5a140178a1 Add minimal Workspace UI consuming /v1/space/workspace 2026-01-24 22:16:40 +01:00
Carl Niklas Rydberg 3ada8d6a71 Add /v1/space/workspace workspace snapshot endpoint 2026-01-24 21:43:40 +01:00
Carl Niklas Rydberg ee4397b0d6 Add mount-aware /v1/space/mounts/sync/until with tests and docs 2026-01-24 20:16:53 +01:00
Carl Niklas Rydberg e54a9009a6 Add GET /v1/space/mounts/resolve with local mount resolution tests
Add mount-aware v2 federation cursors with remote_space_id support
2026-01-24 19:50:13 +01:00
Carl Niklas Rydberg a870b188e9 Add GET /v1/space/mounts/resolve with local mount resolution tests 2026-01-24 19:15:23 +01:00
Carl Niklas Rydberg 8eea0cb69b Add CAS-safe PUT /v1/space/manifest with tests and docs 2026-01-24 18:57:13 +01:00
Carl Niklas Rydberg 704ea18a32 Add space manifest CAS head and read-only /v1/space/manifest endpoint 2026-01-24 18:42:01 +01:00
Carl Niklas Rydberg f99ec3ee89 Add bounded fed pull/push until endpoints with tests 2026-01-24 18:11:44 +01:00
Carl Niklas Rydberg 67c837be3c Add space sync status endpoint with cursor peer discovery 2026-01-24 17:43:51 +01:00
Carl Niklas Rydberg d74884b442 Extend fed smoke test to cover push and pull 2026-01-24 17:10:02 +01:00
Carl Niklas Rydberg 8a490ef09e Add federation ingest endpoint with tests and docs 2026-01-24 15:49:47 +01:00
Carl Niklas Rydberg 79f19213ce Add /Testing/Temporary/ to .gitignore 2026-01-24 14:06:52 +01:00
Carl Niklas Rydberg 354808635f add /build-asan/ to .gitignore 2026-01-24 14:05:21 +01:00
Carl Niklas Rydberg fd43cfaf59 Add federation cursors, pull APIs, and smoke test 2026-01-24 14:03:26 +01:00
Carl Niklas Rydberg f3a065c8ab Add space doctor endpoint with deterministic checks and tests 2026-01-24 10:59:49 +01:00
Carl Niklas Rydberg af11665a35 amduatd: add opt-in federation config, space scoping, and tests 2026-01-24 10:35:49 +01:00
Carl Niklas Rydberg db3c4b4c93 Add per-request space selection via X-Amduat-Space 2026-01-24 10:16:22 +01:00
Carl Niklas Rydberg 5ecb28c84c amduatd: optionally index pel derivations 2026-01-24 09:42:02 +01:00
Carl Niklas Rydberg ebdd37cfcd Add selectable fs/index store backend for amduatd 2026-01-24 08:44:28 +01:00
Carl Niklas Rydberg a299b6c463 amduatd: add pointer-rooted edge index and refresh loop 2026-01-24 07:22:51 +01:00
Carl Niklas Rydberg 8d7c7d93a5 http out 2026-01-24 06:50:53 +01:00
Carl Niklas Rydberg 66291f6d43 Extract concepts subsystem 2026-01-24 06:29:51 +01:00
Carl Niklas Rydberg 5e36cb6e5c Extract amduatd space scoping module 2026-01-24 03:21:14 +01:00
Carl Niklas Rydberg 578aa09860 Extract capability token module 2026-01-24 03:03:54 +01:00
Carl Niklas Rydberg d07dae5252 Extract amduatd UI module 2026-01-23 23:30:29 +01:00
Carl Niklas Rydberg 507007e865 Add read-only capability tokens 2026-01-23 23:08:41 +01:00
Carl Niklas Rydberg ffb2b1b015 Add space scoping to amduatd 2026-01-23 22:28:56 +01:00
Carl Niklas Rydberg 8e6ee13953 Bump amduat core 2026-01-23 20:58:16 +01:00
Carl Niklas Rydberg 724c1e9cd7 amduatd: add peer actor and uid allowlist 2026-01-23 20:55:41 +01:00
Carl Niklas Rydberg 37d5490316 Bump amduat core 2026-01-23 20:18:30 +01:00
Carl Niklas Rydberg 94566056bd Add asl store GC tool 2026-01-23 19:35:01 +01:00
Carl Niklas Rydberg 43428cce9c Bump amduat core 2026-01-23 19:15:48 +01:00
Carl Niklas Rydberg e1da1692d4 Bump amduat core 2026-01-23 19:05:18 +01:00
Carl Niklas Rydberg 009b53ffa5 Bump amduat core 2026-01-23 18:32:07 +01:00
Carl Niklas Rydberg ac37ce871d federation? 2026-01-21 19:54:54 +01:00
Carl Niklas Rydberg a4b501e48d federation 2026-01-21 19:51:26 +01:00
91 changed files with 53848 additions and 3129 deletions

2
.gitignore vendored
View file

@ -2,3 +2,5 @@
*.sock *.sock
.amduat-asl/ .amduat-asl/
artifact.bin artifact.bin
/build-asan/
Testing/Temporary/

2
.gitmodules vendored
View file

@ -1,3 +1,3 @@
[submodule "vendor/amduat"] [submodule "vendor/amduat"]
path = vendor/amduat path = vendor/amduat
url = niklas@blackhole.rakeroots.lan:/mnt/duat/services/git/repos/amduat.git url = /mnt/duat/services/git/repos/amduat.git

View file

@ -5,16 +5,516 @@ set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF) set(CMAKE_C_EXTENSIONS OFF)
option(AMDUATD_ENABLE_UI "Build amduatd embedded UI" ON)
add_subdirectory(vendor/amduat) add_subdirectory(vendor/amduat)
add_executable(amduatd src/amduatd.c) add_library(amduat_federation
federation/coord.c
federation/transport_stub.c
federation/transport_unix.c
)
target_include_directories(amduat_federation
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include
)
target_link_libraries(amduat_federation
PRIVATE amduat_asl amduat_enc amduat_util amduat_fed
)
set(amduatd_sources src/amduatd.c src/amduatd_http.c src/amduatd_caps.c
src/amduatd_space.c src/amduatd_concepts.c
src/amduatd_store.c src/amduatd_derivation_index.c
src/amduatd_fed.c src/amduatd_fed_cursor.c
src/amduatd_fed_until.c
src/amduatd_fed_pull_plan.c src/amduatd_fed_push_plan.c
src/amduatd_fed_pull_apply.c src/amduatd_fed_push_apply.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)
endif()
add_executable(amduatd ${amduatd_sources})
target_include_directories(amduatd target_include_directories(amduatd
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/src/internal PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/src/internal
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include
) )
target_link_libraries(amduatd target_compile_definitions(amduatd
PRIVATE amduat_tgk amduat_pel amduat_format amduat_asl_store_fs amduat_asl PRIVATE AMDUATD_ENABLE_UI=$<BOOL:${AMDUATD_ENABLE_UI}>
amduat_enc amduat_hash_asl1 amduat_util
) )
target_link_libraries(amduatd
PRIVATE amduat_tgk amduat_pel amduat_format amduat_asl_store_fs
amduat_asl_store_index_fs amduat_asl_derivation_index_fs
amduat_asl_record amduat_asl amduat_enc amduat_hash_asl1
amduat_util amduat_federation
)
add_executable(amduat_pel_gc
src/amduat_pel_gc.c
src/asl_gc_fs.c
)
set_target_properties(amduat_pel_gc PROPERTIES OUTPUT_NAME "amduat-pel")
target_include_directories(amduat_pel_gc
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include
)
target_link_libraries(amduat_pel_gc
PRIVATE amduat_asl_store_fs amduat_asl_record amduat_asl amduat_enc
amduat_hash_asl1 amduat_pel amduat_util
)
enable_testing()
add_executable(amduatd_test_store_backend
tests/test_amduatd_store_backend.c
src/amduatd_store.c
)
target_include_directories(amduatd_test_store_backend
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include
)
target_link_libraries(amduatd_test_store_backend
PRIVATE amduat_asl_store_fs amduat_asl_store_index_fs
)
add_test(NAME amduatd_store_backend COMMAND amduatd_test_store_backend)
add_executable(amduatd_test_space_resolve
tests/test_amduatd_space_resolve.c
src/amduatd_space.c
)
target_include_directories(amduatd_test_space_resolve
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_resolve
PRIVATE amduat_asl_pointer_fs
)
add_test(NAME amduatd_space_resolve COMMAND amduatd_test_space_resolve)
add_executable(amduatd_test_derivation_index
tests/test_amduatd_derivation_index.c
src/amduatd_derivation_index.c
)
target_include_directories(amduatd_test_derivation_index
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include
)
target_link_libraries(amduatd_test_derivation_index
PRIVATE amduat_asl_derivation_index_fs amduat_pel amduat_asl amduat_util
)
add_test(NAME amduatd_derivation_index COMMAND amduatd_test_derivation_index)
add_executable(amduatd_test_fed_cfg
tests/test_amduatd_fed_cfg.c
src/amduatd_fed.c
src/amduatd_space.c
)
target_include_directories(amduatd_test_fed_cfg
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include
)
target_link_libraries(amduatd_test_fed_cfg
PRIVATE amduat_asl amduat_enc amduat_util amduat_asl_pointer_fs
)
add_test(NAME amduatd_fed_cfg COMMAND amduatd_test_fed_cfg)
add_executable(amduatd_test_fed_cursor
tests/test_amduatd_fed_cursor.c
src/amduatd_fed_cursor.c
src/amduatd_fed.c
src/amduatd_space.c
src/amduatd_store.c
)
target_include_directories(amduatd_test_fed_cursor
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include
)
target_link_libraries(amduatd_test_fed_cursor
PRIVATE amduat_asl_store_fs amduat_asl_store_index_fs amduat_asl_record
amduat_asl amduat_enc amduat_asl_pointer_fs amduat_util
amduat_hash_asl1
)
add_test(NAME amduatd_fed_cursor COMMAND amduatd_test_fed_cursor)
add_executable(amduatd_test_fed_pull_plan
tests/test_amduatd_fed_pull_plan.c
src/amduatd_fed_pull_plan.c
src/amduatd_fed_cursor.c
src/amduatd_fed.c
src/amduatd_space.c
)
target_include_directories(amduatd_test_fed_pull_plan
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include
)
target_link_libraries(amduatd_test_fed_pull_plan
PRIVATE amduat_asl amduat_asl_record amduat_asl_log_store amduat_enc amduat_util amduat_hash_asl1
amduat_asl_pointer_fs
)
add_test(NAME amduatd_fed_pull_plan COMMAND amduatd_test_fed_pull_plan)
add_executable(amduatd_test_fed_push_plan
tests/test_amduatd_fed_push_plan.c
src/amduatd_fed_push_plan.c
src/amduatd_fed_cursor.c
src/amduatd_fed.c
src/amduatd_space.c
)
target_include_directories(amduatd_test_fed_push_plan
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include
)
target_link_libraries(amduatd_test_fed_push_plan
PRIVATE amduat_asl amduat_asl_record amduat_asl_log_store amduat_enc amduat_util amduat_hash_asl1
amduat_asl_pointer_fs
)
add_test(NAME amduatd_fed_push_plan COMMAND amduatd_test_fed_push_plan)
add_executable(amduatd_test_fed_push_apply
tests/test_amduatd_fed_push_apply.c
src/amduatd_fed_push_apply.c
src/amduatd_fed_push_plan.c
src/amduatd_fed_cursor.c
src/amduatd_fed.c
src/amduatd_space.c
src/amduatd_store.c
)
target_include_directories(amduatd_test_fed_push_apply
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include
)
target_link_libraries(amduatd_test_fed_push_apply
PRIVATE amduat_asl_store_fs amduat_asl_store_index_fs amduat_asl_record
amduat_asl amduat_asl_log_store amduat_enc amduat_asl_pointer_fs amduat_util
amduat_hash_asl1
)
add_test(NAME amduatd_fed_push_apply COMMAND amduatd_test_fed_push_apply)
add_executable(amduatd_test_fed_pull_until
tests/test_amduatd_fed_pull_until.c
src/amduatd_fed_until.c
src/amduatd_fed_pull_apply.c
src/amduatd_fed_pull_plan.c
src/amduatd_fed_push_apply.c
src/amduatd_fed_push_plan.c
src/amduatd_fed_cursor.c
src/amduatd_fed.c
src/amduatd_space.c
src/amduatd_store.c
)
target_include_directories(amduatd_test_fed_pull_until
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include
)
target_link_libraries(amduatd_test_fed_pull_until
PRIVATE amduat_asl_store_fs amduat_asl_store_index_fs amduat_asl_record
amduat_asl amduat_enc amduat_asl_pointer_fs amduat_util
amduat_asl_log_store
amduat_hash_asl1 amduat_fed
)
add_test(NAME amduatd_fed_pull_until COMMAND amduatd_test_fed_pull_until)
add_executable(amduatd_test_fed_push_until
tests/test_amduatd_fed_push_until.c
src/amduatd_fed_until.c
src/amduatd_fed_pull_apply.c
src/amduatd_fed_pull_plan.c
src/amduatd_fed_push_apply.c
src/amduatd_fed_push_plan.c
src/amduatd_fed_cursor.c
src/amduatd_fed.c
src/amduatd_space.c
src/amduatd_store.c
)
target_include_directories(amduatd_test_fed_push_until
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include
)
target_link_libraries(amduatd_test_fed_push_until
PRIVATE amduat_asl_store_fs amduat_asl_store_index_fs amduat_asl_record
amduat_asl amduat_enc amduat_asl_pointer_fs amduat_util
amduat_asl_log_store
amduat_hash_asl1 amduat_fed
)
add_test(NAME amduatd_fed_push_until COMMAND amduatd_test_fed_push_until)
add_executable(amduatd_test_fed_pull_apply
tests/test_amduatd_fed_pull_apply.c
src/amduatd_fed_pull_apply.c
src/amduatd_fed_pull_plan.c
src/amduatd_fed_cursor.c
src/amduatd_fed.c
src/amduatd_space.c
src/amduatd_store.c
)
target_include_directories(amduatd_test_fed_pull_apply
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include
)
target_link_libraries(amduatd_test_fed_pull_apply
PRIVATE amduat_asl_store_fs amduat_asl_store_index_fs amduat_asl_record
amduat_asl amduat_enc amduat_asl_pointer_fs amduat_util
amduat_hash_asl1 amduat_fed
)
add_test(NAME amduatd_fed_pull_apply COMMAND amduatd_test_fed_pull_apply)
add_executable(amduatd_http_unix
tests/tools/amduatd_http_unix.c
)
add_test(NAME amduatd_fed_smoke
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_fed_smoke.sh
)
set_tests_properties(amduatd_fed_smoke PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME amduatd_fed_ingest
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_fed_ingest.sh
)
set_tests_properties(amduatd_fed_ingest PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME amduatd_graph_queries
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_graph_queries.sh
)
set_tests_properties(amduatd_graph_queries PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME amduatd_graph_contract
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_graph_contract.sh
)
set_tests_properties(amduatd_graph_contract PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME amduatd_graph_index_append
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_graph_index_append.sh
)
set_tests_properties(amduatd_graph_index_append PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME amduatd_graph_index_append_stress
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_graph_index_append_stress.sh
)
set_tests_properties(amduatd_graph_index_append_stress PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME amduatd_index_two_nodes
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_index_two_nodes.sh
)
set_tests_properties(amduatd_index_two_nodes PROPERTIES SKIP_RETURN_CODE 77)
add_executable(amduatd_test_space_doctor
tests/test_amduatd_space_doctor.c
src/amduatd_space_doctor.c
src/amduatd_space.c
src/amduatd_store.c
)
target_include_directories(amduatd_test_space_doctor
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_doctor
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_doctor COMMAND amduatd_test_space_doctor)
add_executable(amduatd_test_space_roots
tests/test_amduatd_space_roots.c
src/amduatd_space_roots.c
src/amduatd_space.c
src/amduatd_fed_cursor.c
src/amduatd_store.c
)
target_include_directories(amduatd_test_space_roots
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_roots
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_roots COMMAND amduatd_test_space_roots)
add_executable(amduatd_test_space_manifest
tests/test_amduatd_space_manifest.c
src/amduatd_space_manifest.c
src/amduatd_space.c
src/amduatd_store.c
src/amduatd_http.c
)
target_include_directories(amduatd_test_space_manifest
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_manifest
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_manifest COMMAND amduatd_test_space_manifest)
add_executable(amduatd_test_space_mounts
tests/test_amduatd_space_mounts.c
src/amduatd_space_mounts.c
src/amduatd_space_manifest.c
src/amduatd_http.c
src/amduatd_fed_cursor.c
src/amduatd_space.c
src/amduatd_store.c
)
target_include_directories(amduatd_test_space_mounts
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_mounts
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_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
src/amduatd_space_manifest.c
src/amduatd_fed_cursor.c
src/amduatd_fed_pull_plan.c
src/amduatd_fed_pull_apply.c
src/amduatd_fed_push_apply.c
src/amduatd_fed_push_plan.c
src/amduatd_fed_until.c
src/amduatd_fed.c
src/amduatd_http.c
src/amduatd_space.c
src/amduatd_store.c
)
target_include_directories(amduatd_test_space_mounts_sync
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_mounts_sync
PRIVATE amduat_asl_store_fs amduat_asl_pointer_fs amduat_asl_record
amduat_asl_store_index_fs amduat_asl amduat_enc amduat_util
amduat_asl_log_store
amduat_hash_asl1 amduat_fed
)
add_test(NAME amduatd_space_mounts_sync COMMAND amduatd_test_space_mounts_sync)
add_executable(amduatd_test_space_sync_status
tests/test_amduatd_space_sync_status.c
src/amduatd_space_roots.c
src/amduatd_space.c
src/amduatd_fed_cursor.c
src/amduatd_store.c
)
target_include_directories(amduatd_test_space_sync_status
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_sync_status
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_sync_status COMMAND amduatd_test_space_sync_status)

585
README.md
View file

@ -2,6 +2,13 @@
`amduat-api` builds `amduatd`, a minimal HTTP server over a Unix domain socket that exposes Amduat substrate operations for a **single ASL store root**. `amduat-api` builds `amduatd`, a minimal HTTP server over a Unix domain socket that exposes Amduat substrate operations for a **single ASL store root**.
## App Developer Handoff
For a compact, implementation-focused guide for external app teams, use:
- `docs/v2-app-developer-guide.md`
- `registry/amduatd-api-contract.v2.json` (machine-readable contract)
## Build ## Build
```sh ```sh
@ -9,6 +16,14 @@ cmake -S . -B build
cmake --build build -j cmake --build build -j
``` ```
To build without the embedded UI:
```sh
cmake -S . -B build -DAMDUATD_ENABLE_UI=OFF
```
When the UI is enabled (default), `/v1/ui` serves the same embedded HTML as before.
## Core dependency ## Core dependency
This repo vendors the core implementation as a git submodule at `vendor/amduat`. This repo vendors the core implementation as a git submodule at `vendor/amduat`.
@ -25,18 +40,362 @@ Initialize a store:
./vendor/amduat/build/amduat-asl init --root .amduat-asl ./vendor/amduat/build/amduat-asl init --root .amduat-asl
``` ```
Run the daemon: Run the daemon (fs backend default):
```sh ```sh
./build/amduatd --root .amduat-asl --sock amduatd.sock ./build/amduatd --root .amduat-asl --sock amduatd.sock
``` ```
Run the daemon with the index-backed store:
```sh
./build/amduatd --root .amduat-asl --sock amduatd.sock --store-backend index
```
Note: `/v1/fed/records` and `/v1/fed/push/plan` require the index backend.
## Federation (dev)
Federation is opt-in and disabled by default. Enabling federation requires the
index-backed store and explicit flags:
```sh
./build/amduatd --root .amduat-asl --sock amduatd.sock --store-backend index \
--fed-enable --fed-transport unix --fed-unix-sock peer.sock \
--fed-domain-id 1 --fed-registry-ref <registry_ref_hex>
```
Flags:
- `--fed-enable` turns on the coordinator tick loop.
- `--fed-transport stub|unix` selects transport (`stub` by default).
- `--fed-unix-sock PATH` configures the unix transport socket path.
- `--fed-domain-id ID` sets the local domain id.
- `--fed-registry-ref REF` seeds the registry reference (hex ref).
- `--fed-require-space` rejects `/v1/fed/*` requests that do not resolve a space.
`X-Amduat-Space` is honored for `/v1/fed/*` requests the same way as other
endpoints. If `--space` is configured, unix transport requests will include the
same `X-Amduat-Space` header when contacting peers.
### Federation cursors
Federation cursors track deterministic, auditable sync checkpoints per
`(space, peer)`. Cursor heads are stored as ASL pointers that reference CAS
records (`fed/cursor` schema). A peer key should be a stable identifier for the
remote (for example a federation registry domain id rendered as a decimal
string).
Push cursors are separate from pull cursors and live under
`fed/push_cursor/<peer>/head` (space scoped).
To avoid cursor collisions across multiple mounts to the same peer, pass
`remote_space_id=<space_id>` on cursor-aware endpoints. When provided, cursor
heads use `fed/cursor/<peer>/<remote_space_id>/head` and
`fed/push_cursor/<peer>/<remote_space_id>/head`. When omitted, the legacy v1
cursor names remain in effect for backward compatibility.
Read the current cursor for a peer:
```sh
curl --unix-socket amduatd.sock \
'http://localhost/v1/fed/cursor?peer=domain-2' \
-H 'X-Amduat-Space: demo'
```
Scoped to a specific remote space:
```sh
curl --unix-socket amduatd.sock \
'http://localhost/v1/fed/cursor?peer=domain-2&remote_space_id=beta' \
-H 'X-Amduat-Space: demo'
```
Write a cursor update (CAS-safe; include `expected_ref` to enforce; omitting it
only succeeds when the cursor is absent):
```sh
curl --unix-socket amduatd.sock -X POST \
'http://localhost/v1/fed/cursor?peer=domain-2' \
-H 'Content-Type: application/json' \
-H 'X-Amduat-Space: demo' \
-d '{"last_logseq":123,"last_record_hash":"<ref>","expected_ref":"<ref>"}'
```
Cursor values are intended to drive incremental log/index scanning when that
infrastructure is available; the cursor endpoints themselves do not require the
index backend.
### Federation pull plan
You can ask the daemon to compute a read-only plan of which remote records would
be pulled from a peer given the current cursor:
```sh
curl --unix-socket amduatd.sock \
'http://localhost/v1/fed/pull/plan?peer=2&limit=128' \
-H 'X-Amduat-Space: demo'
```
The plan does not write artifacts, records, or cursors. It is deterministic and
returns only identifiers (logseq/ref), plus the next cursor candidate if the
plan were applied successfully.
Append `&remote_space_id=<space_id>` to use mount-specific cursor keying.
Apply a bounded batch of remote records (advances the cursor only after
success):
```sh
curl --unix-socket amduatd.sock -X POST \
'http://localhost/v1/fed/pull?peer=2&limit=128' \
-H 'X-Amduat-Space: demo'
```
`/v1/fed/pull` requires the index backend and will not advance the cursor on
partial failure.
Use `remote_space_id=<space_id>` to scope the cursor to a mount.
### Federation push plan (sender dry run)
Compute a read-only plan of what would be sent to a peer from the local log
since the push cursor (does not advance the cursor):
```sh
curl --unix-socket amduatd.sock \
'http://localhost/v1/fed/push/plan?peer=2&limit=128' \
-H 'X-Amduat-Space: demo'
```
`/v1/fed/push/plan` requires the index backend and uses a push cursor separate
from the pull cursor.
Append `&remote_space_id=<space_id>` to use mount-specific cursor keying.
### Federation push (sender apply)
Send local records to a peer (advances the push cursor only after all records
apply successfully on the peer):
```sh
curl --unix-socket amduatd.sock -X POST \
'http://localhost/v1/fed/push?peer=2&limit=128' \
-H 'X-Amduat-Space: demo'
```
`/v1/fed/push` uses `/v1/fed/ingest` on the peer and only advances the push
cursor after the batch completes. It requires the index backend.
Use `remote_space_id=<space_id>` to scope the cursor to a mount.
### Federation sync until caught up
Use the bounded helpers to repeatedly apply pull/push until no work remains,
with a hard `max_rounds` cap to keep requests bounded:
```sh
curl --unix-socket amduatd.sock -X POST \
'http://localhost/v1/fed/pull/until?peer=2&limit=128&max_rounds=10' \
-H 'X-Amduat-Space: demo'
```
```sh
curl --unix-socket amduatd.sock -X POST \
'http://localhost/v1/fed/push/until?peer=2&limit=128&max_rounds=10' \
-H 'X-Amduat-Space: demo'
```
### Federation ingest (receiver)
`/v1/fed/ingest` applies a single incoming record (push receiver). The request
is space-scoped via `X-Amduat-Space` and requires federation to be enabled;
otherwise the daemon responds with 503.
For artifact, per, and tgk_edge records, send raw bytes and provide metadata via
query params:
```sh
curl --unix-socket amduatd.sock -X POST \
'http://localhost/v1/fed/ingest?record_type=artifact&ref=<ref>' \
-H 'Content-Type: application/octet-stream' \
-H 'X-Amduat-Space: demo' \
--data-binary 'payload'
```
For tombstones, send a small JSON payload:
```sh
curl --unix-socket amduatd.sock -X POST \
'http://localhost/v1/fed/ingest' \
-H 'Content-Type: application/json' \
-H 'X-Amduat-Space: demo' \
-d '{"record_type":"tombstone","ref":"<ref>"}'
```
Notes:
- Record types: `artifact`, `per`, `tgk_edge`, `tombstone`.
- Size limit: 8 MiB per request.
- Tombstones use deterministic defaults: `scope=0`, `reason_code=0`.
Run the daemon with derivation indexing enabled:
```sh
./build/amduatd --root .amduat-asl --sock amduatd.sock --enable-derivation-index
```
## Space roots (GC)
`/v1/space/roots` enumerates the pointer heads that must be treated as GC roots
for the effective space, including federation cursor heads.
GC root sets MUST include federation cursors to avoid trimming artifacts still
reachable via replication state. Use the roots listing to build your root set
before running GC tooling.
Example:
```sh
curl --unix-socket amduatd.sock \
'http://localhost/v1/space/roots' \
-H 'X-Amduat-Space: demo'
```
## Space sync status
`/v1/space/sync/status` is a read-only summary of federation readiness and
per-peer cursor positions for the effective space. Peers are discovered from
cursor head pointers (pull and push) and returned in deterministic order.
```sh
curl --unix-socket amduatd.sock \
'http://localhost/v1/space/sync/status' \
-H 'X-Amduat-Space: demo'
```
The response groups cursor status per peer and per remote space id:
`peers:[{peer_key, remotes:[{remote_space_id, pull_cursor, push_cursor}]}]`.
`remote_space_id` is `null` for legacy v1 cursor heads.
## Space manifest
`/v1/space/manifest` returns the space manifest rooted at the deterministic
pointer head (`manifest/head` or `space/<space_id>/manifest/head`). The manifest
is stored in CAS as a record and returned with its ref plus a decoded,
deterministic JSON payload. If no manifest head is present, the endpoint
returns a 404.
```sh
curl --unix-socket amduatd.sock \
'http://localhost/v1/space/manifest' \
-H 'X-Amduat-Space: demo'
```
Create the manifest (only if no head exists yet):
```sh
curl --unix-socket amduatd.sock \
-X PUT 'http://localhost/v1/space/manifest' \
-H 'Content-Type: application/json' \
-H 'X-Amduat-Space: demo' \
--data-binary '{"version":1,"mounts":[]}'
```
Update with optimistic concurrency:
```sh
curl --unix-socket amduatd.sock \
-X PUT 'http://localhost/v1/space/manifest' \
-H 'Content-Type: application/json' \
-H 'X-Amduat-Space: demo' \
-H 'If-Match: <manifest_ref>' \
--data-binary @manifest.json
```
`If-Match` can be replaced with `?expected_ref=<manifest_ref>` if needed.
## Space mount resolution
`/v1/space/mounts/resolve` returns a deterministic, local-only view of the
space manifest mounts with their local pull cursor state. It performs no
network I/O and does not mutate storage. Track mounts indicate intent; syncing
remains a separate concern.
If no manifest head is present, the endpoint returns a 404.
Track mounts report `local_tracking.cursor_namespace` (`v2` when using
`remote_space_id`-keyed cursors).
```sh
curl --unix-socket amduatd.sock \
'http://localhost/v1/space/mounts/resolve' \
-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
(`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)
- `/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'
```
## Workspace UI
`/workspace` serves a minimal, human-facing page that consumes
`/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)
`/v1/space/mounts/sync/until` runs the federation pull/until loop for every
`track` mount in the current manifest using v2 cursor keying
(`remote_space_id = mount.space_id`). It is bounded by `limit`, `max_rounds`,
and `max_mounts`, returns per-mount status, and continues after errors.
Requires federation enabled and the index backend. If no manifest head is
present, the endpoint returns a 404.
```sh
curl --unix-socket amduatd.sock -X POST \
'http://localhost/v1/space/mounts/sync/until?limit=128&max_rounds=10&max_mounts=32' \
-H 'X-Amduat-Space: demo'
```
To fail `/v1/pel/run` if the derivation index write fails:
```sh
./build/amduatd --root .amduat-asl --sock amduatd.sock --derivation-index-strict
```
Dev loop (build + restart): Dev loop (build + restart):
```sh ```sh
./scripts/dev-restart.sh ./scripts/dev-restart.sh
``` ```
## Federation smoke test
Run the end-to-end federation smoke test (starts two local daemons, verifies
pull replication A→B and push replication B→A, and checks cursors):
```sh
./scripts/test_fed_smoke.sh
```
The test requires the index backend and either `curl` with `--unix-socket`
support or the built-in `build/amduatd_http_unix` helper.
Query store meta: Query store meta:
```sh ```sh
@ -92,6 +451,60 @@ curl --unix-socket amduatd.sock -X POST http://localhost/v1/pel/run \
-d '{"program_ref":"<program_ref>","input_refs":["<input_ref_0>"],"params_ref":"<params_ref>"}' -d '{"program_ref":"<program_ref>","input_refs":["<input_ref_0>"],"params_ref":"<params_ref>"}'
``` ```
Run a v2 PEL execute request with inline artifact ingest:
```sh
curl --unix-socket amduatd.sock -X POST http://localhost/v2/pel/execute \
-H 'Content-Type: application/json' \
-d '{
"scheme_ref":"dag",
"program_ref":"<program_ref>",
"inputs":{
"refs":["<input_ref_0>"],
"inline_artifacts":[{"body_hex":"48656c6c6f2c20763221","type_tag":"0x00000000"}]
},
"receipt":{
"input_manifest_ref":"<manifest_ref>",
"environment_ref":"<env_ref>",
"evaluator_id":"local-amduatd",
"executor_ref":"<executor_ref>",
"started_at":1731000000,
"completed_at":1731000001
}
}'
```
The v2 execute response returns `run_ref` (and `result_ref` alias),
`receipt_ref`, `stored_input_refs[]`, `output_refs[]`, and `status`.
Simplified async v2 operations (PEL-backed under the hood):
```sh
# put (returns job_id)
curl --unix-socket amduatd.sock -X POST http://localhost/v2/ops/put \
-H 'Content-Type: application/json' \
-d '{"body_hex":"48656c6c6f","type_tag":"0x00000000"}'
# concat (returns job_id)
curl --unix-socket amduatd.sock -X POST http://localhost/v2/ops/concat \
-H 'Content-Type: application/json' \
-d '{"left_ref":"<ref_a>","right_ref":"<ref_b>"}'
# slice (returns job_id)
curl --unix-socket amduatd.sock -X POST http://localhost/v2/ops/slice \
-H 'Content-Type: application/json' \
-d '{"ref":"<ref>","offset":1,"length":3}'
# poll job
curl --unix-socket amduatd.sock http://localhost/v2/jobs/1
# get bytes
curl --unix-socket amduatd.sock http://localhost/v2/get/<ref>
```
When derivation indexing is enabled, successful PEL runs record derivations under
`<root>/index/derivations/by_artifact/` keyed by output refs (plus result/trace/receipt refs).
Define a PEL/PROGRAM-DAG/1 program (store-backed): Define a PEL/PROGRAM-DAG/1 program (store-backed):
```sh ```sh
@ -135,12 +548,59 @@ Artifact info (length + type tag):
curl --unix-socket amduatd.sock 'http://localhost/v1/artifacts/<ref>?format=info' curl --unix-socket amduatd.sock 'http://localhost/v1/artifacts/<ref>?format=info'
``` ```
## Space selection
Requests can select a space via the `X-Amduat-Space` header:
```sh
curl --unix-socket amduatd.sock http://localhost/v1/concepts \
-H 'X-Amduat-Space: demo'
```
Precedence rules:
- `X-Amduat-Space` header (if present)
- daemon `--space` default (if configured)
- unscoped names (no space)
When capability tokens are used, the requested space must match the token's
space (or the token must be unscoped), otherwise the request is rejected.
## Space doctor
Check deterministic invariants for the effective space:
```sh
curl --unix-socket amduatd.sock http://localhost/v1/space/doctor
curl --unix-socket amduatd.sock http://localhost/v1/space/doctor \
-H 'X-Amduat-Space: demo'
```
When the daemon uses the `fs` store backend, index-only checks are reported as
`"skipped"`; the `index` backend runs them.
## 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}`
- `GET /v1/contract` → contract bytes (JSON) (+ `X-Amduat-Contract-Ref` header) - `GET /v1/contract` → contract bytes (JSON) (+ `X-Amduat-Contract-Ref` header)
- `GET /v1/contract?format=ref``{ref}` - `GET /v1/contract?format=ref``{ref}`
- `GET /v1/space/doctor` → deterministic space health checks
- `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 - `GET /v1/ui` → browser UI for authoring/running programs
- `GET /v1/fed/records?domain_id=...&from_logseq=...&limit=...``{domain_id, snapshot_id, log_prefix, next_logseq, records[]}` (published artifacts + tombstones + PER + TGK edges)
- `GET /v1/fed/cursor?peer=...&remote_space_id=...``{peer_key, space_id, last_logseq, last_record_hash, ref}` (`remote_space_id` optional)
- `POST /v1/fed/cursor?peer=...&remote_space_id=...``{ref}` (CAS update; `expected_ref` in body; `remote_space_id` optional)
- `GET /v1/fed/pull/plan?peer=...&limit=...&remote_space_id=...``{peer, effective_space, cursor, remote_scan, records, next_cursor_candidate, ...}`
- `GET /v1/fed/push/plan?peer=...&limit=...&remote_space_id=...``{peer, domain_id, effective_space, cursor, scan, records, required_artifacts, next_cursor_candidate}`
- `POST /v1/fed/pull?peer=...&limit=...&remote_space_id=...``{peer, effective_space, cursor_before, plan_summary, applied, cursor_after, errors}`
- `POST /v1/fed/push?peer=...&limit=...&remote_space_id=...``{peer, domain_id, effective_space, cursor_before, plan_summary, sent, cursor_after, errors}`
- `GET /v1/fed/artifacts/{ref}` → raw bytes for federation resolve
- `GET /v1/fed/status``{status, domain_id, registry_ref, last_tick_ms}`
- `POST /v1/artifacts` - `POST /v1/artifacts`
- raw bytes: `Content-Type: application/octet-stream` (+ optional `X-Amduat-Type-Tag: 0x...`) - raw bytes: `Content-Type: application/octet-stream` (+ optional `X-Amduat-Type-Tag: 0x...`)
- artifact framing: `Content-Type: application/vnd.amduat.asl.artifact+v1` - artifact framing: `Content-Type: application/vnd.amduat.asl.artifact+v1`
@ -156,10 +616,131 @@ curl --unix-socket amduatd.sock 'http://localhost/v1/artifacts/<ref>?format=info
- `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`)
- response: `{result_ref, trace_ref?, output_refs[], status}` - request receipt (optional): `{receipt:{input_manifest_ref, environment_ref, evaluator_id, executor_ref, started_at, completed_at, sbom_ref?, parity_digest_hex?, executor_fingerprint_ref?, run_id_hex?, limits?, logs?, determinism_level?, rng_seed_hex?, signature_hex?}}`
- response: `{result_ref, trace_ref?, receipt_ref?, output_refs[], status}`
- `POST /v2/pel/execute` (first v2 slice)
- request: `{program_ref, scheme_ref?, params_ref?, inputs:{refs[], inline_artifacts:[{body_hex, type_tag?}]}, receipt:{input_manifest_ref, environment_ref, evaluator_id, executor_ref, started_at, completed_at}}`
- response: `{run_ref, result_ref, trace_ref?, receipt_ref, stored_input_refs[], output_refs[], status}`
- `POST /v2/ops/put` → async enqueue `{job_id, status}`
- request: `{body_hex, type_tag?}`
- `POST /v2/ops/concat` → async enqueue `{job_id, status}`
- request: `{left_ref, right_ref}` (refs or concept names)
- `POST /v2/ops/slice` → async enqueue `{job_id, status}`
- request: `{ref, offset, length}` (`ref` accepts ref or concept name)
- `GET /v2/jobs/{id}``{job_id, kind, status, created_at_ms, started_at_ms, completed_at_ms, result_ref, error}`
- `GET /v2/get/{ref}` → artifact bytes (alias to `/v1/artifacts/{ref}`)
- `GET /v2/healthz` → liveness probe `{ok, status, time_ms}`
- `GET /v2/readyz` → readiness probe `{ok, status, components:{graph_index, federation}}` (`503` when not ready)
- `GET /v2/metrics` → Prometheus text metrics (`text/plain; version=0.0.4`)
- `POST /v2/graph/nodes` → create/seed graph node via concept `{name, ref?}`
- `POST /v2/graph/nodes/{name}/versions` → publish node version `{ref, metadata_ref?|provenance?}` (`metadata_ref` and `provenance` are mutually exclusive)
- `POST /v2/graph/nodes/{name}/versions/tombstone` → tombstone a previously published node version `{ref, metadata_ref?|provenance?}` (`metadata_ref` and `provenance` are mutually exclusive; append-only)
- `GET /v2/graph/nodes/{name}/versions?as_of=&include_tombstoned=` → get node versions (same shape as node read; defaults hide tombstoned versions from `latest_ref` and `versions[]`; set `include_tombstoned=true` to include them)
- `GET /v2/graph/nodes/{name}/neighbors?dir=&predicate=&limit=&cursor=&as_of=&provenance_ref=&include_tombstoned=&expand_names=&expand_artifacts=` → neighbor scan (paged, snapshot-bounded; tombstoned edges excluded by default unless `include_tombstoned=true`; optional provenance filter; add names/latest refs via expansion flags)
- `GET /v2/graph/search?name_prefix=&limit=&cursor=&as_of=` → search nodes by name prefix (paged, snapshot-bounded)
- `GET /v2/graph/paths?from=&to=&max_depth=&predicate=&as_of=&k=&expand_names=&expand_artifacts=&include_tombstoned=&provenance_ref=&max_fanout=&max_result_bytes=&include_stats=` → shortest directed path query (bounded BFS, snapshot-bounded, returns up to `k` paths with hop metadata and optional names/latest refs; tombstoned edges excluded by default unless `include_tombstoned=true`; optional provenance filter; supports fanout/response-size guards and optional stats)
- `GET /v2/graph/subgraph?roots[]=&max_depth=&max_fanout=&predicates[]=&dir=&as_of=&include_versions=&include_tombstoned=&limit_nodes=&limit_edges=&cursor=&provenance_ref=&max_result_bytes=&include_stats=` → bounded multi-hop subgraph retrieval from roots (snapshot-bounded, paged by opaque `next_cursor`; tombstoned edges excluded by default unless `include_tombstoned=true`; optional provenance filter; supports fanout/response-size guards and optional stats; returns `{nodes[], edges[], frontier?[], next_cursor, truncated}`)
- `POST /v2/graph/edges` → append graph edge `{subject, predicate, object, metadata_ref?|provenance?}` (`metadata_ref` and `provenance` are mutually exclusive)
- `POST /v2/graph/edges/tombstone` → append tombstone for an existing edge `{edge_ref, metadata_ref?|provenance?}` (append-only correction/retraction; `metadata_ref` and `provenance` are mutually exclusive)
- `POST /v2/graph/batch` → apply mixed graph mutations `{idempotency_key?, mode?, nodes?, versions?, edges?}` (versions/edges items support `metadata_ref?|provenance?`; mode: `fail_fast|continue_on_error`; deterministic replay for repeated `idempotency_key` + identical payload; returns `{ok, applied, results[]}` with per-item status/error)
- `POST /v2/graph/query` → unified graph query `{where?, predicates?, direction?, include_versions?, include_tombstoned?, include_stats?, max_result_bytes?, as_of?, limit?, cursor?}` returning `{nodes[], edges[], paging, stats?}` (`nodes[].versions[]` included when `include_versions=true`; `where.provenance_ref` filters edges linked via `ms.has_provenance`; tombstoned edges are excluded as-of snapshot unless `include_tombstoned=true`)
- `POST /v2/graph/retrieve` → agent-oriented bounded retrieval `{roots[], goal_predicates?[], max_depth?, as_of?, provenance_min_confidence?, include_versions?, include_tombstoned?, max_fanout?, limit_nodes?, limit_edges?, max_result_bytes?}` returning `{nodes[], edges[], explanations[], truncated, stats}` (multi-hop traversal from roots with optional predicate targeting and provenance-confidence gating; each explanation includes edge inclusion reasons and traversal depth)
- `POST /v2/graph/export` → paged graph envelope export `{as_of?, cursor?, limit?, predicates?[], roots?[], include_tombstoned?, max_result_bytes?}` returning `{items[], next_cursor, has_more, snapshot_as_of, stats}` (`items[]` preserve edge refs/order plus predicate, tombstone flag, and attached `metadata_ref` when present)
- `POST /v2/graph/import` → ordered graph envelope replay `{mode?, items[]}` (`mode=fail_fast|continue_on_error`) returning `{ok, applied, results[]}` with per-item `index/status/code/error/edge_ref`
- `GET /v2/graph/schema/predicates` → current graph governance policy `{mode, provenance_mode, predicates[]}` where `mode` is `strict|warn|off` and `provenance_mode` is `optional|required`
- `POST /v2/graph/schema/predicates` → update graph governance `{mode?, provenance_mode?, predicates?[]}` (`predicates[]` entries accept `{predicate_ref|predicate, domain?, range?}`; predicate validation mode is enforced for edge writes in `POST /v2/graph/edges`, batch edge items, and graph import edge items; `provenance_mode=required` enforces provenance attachment (`metadata_ref` or `provenance`) for version/edge/tombstone writes; policy is persisted per space root)
- `GET /v2/graph/stats` → graph/index health summary `{edges_total, aliases_total, index{...}, tombstones{edges, ratio}}`
- `GET /v2/graph/capabilities` → graph feature/version negotiation payload `{contract, graph{version, features, limits, modes}, runtime{...}}`
- `GET /v2/graph/changes?since_cursor=&since_as_of=&limit=&wait_ms=&event_types[]=&predicates[]=&roots[]=` → incremental appended graph events with strict cursor replay window (returns `410` when cursor falls outside retained window), resumable cursor tokens, optional long-poll (`wait_ms`), and event/predicate/root filters; response `{events[], next_cursor, has_more}`
- `GET /v2/graph/edges?subject=&predicate=&object=&dir=&limit=&cursor=&as_of=&expand_names=&expand_artifacts=&provenance_ref=&include_tombstoned=&max_result_bytes=&include_stats=` → filtered edge scan with paging (`dir=any|outgoing|incoming`, snapshot-bounded; tombstoned edges excluded by default unless `include_tombstoned=true`; optional provenance filter; add names/latest refs via expansion flags; supports response-size guards and optional stats)
- graph paging cursors are opaque tokens (`g1_*`); legacy numeric cursors are still accepted
- `GET /v2/graph/nodes/{name}?as_of=&include_tombstoned=``{name, concept_ref, latest_ref, versions[], outgoing[], incoming[]}` (`latest_ref` is resolved from visible versions at `as_of`; tombstoned versions excluded by default)
- `GET /v2/graph/history/{name}?as_of=&include_tombstoned=` → event timeline for node versions/edges (snapshot-bounded; `latest_ref` and tombstoned fact events both respect same visibility rules)
Graph client helper examples for agent flows are in `scripts/graph_client_helpers.sh`:
- `batch-ingest` (idempotent batch write helper)
- `sync-once` (incremental changes cursor step)
- `subgraph` (bounded retrieval helper)
- `POST /v1/pel/programs` - `POST /v1/pel/programs`
- request: authoring JSON for `PEL/PROGRAM-DAG/1` (kernel ops only; `params_hex` is raw hex bytes) - request: authoring JSON for `PEL/PROGRAM-DAG/1` (kernel ops only; `params_hex` is raw hex bytes)
- response: `{program_ref}` - response: `{program_ref}`
- `POST /v1/context_frames`
UI (human-facing, not an API contract):
- `GET /workspace` → minimal workspace snapshot + sync controls (uses `/v1/space/workspace`)
Receipt example (with v1.1 fields):
```json
{
"program_ref": "ab12...",
"input_refs": ["cd34..."],
"receipt": {
"input_manifest_ref": "ef56...",
"environment_ref": "7890...",
"evaluator_id": "local-amduatd",
"executor_ref": "1122...",
"started_at": 1712345678,
"completed_at": 1712345688,
"executor_fingerprint_ref": "3344...",
"run_id_hex": "deadbeef",
"limits": {
"cpu_ms": 12,
"wall_ms": 20,
"max_rss_kib": 1024,
"io_reads": 1,
"io_writes": 0
},
"logs": [
{"kind": 1, "log_ref": "5566...", "sha256_hex": "aabbcc"}
],
"determinism_level": 2,
"rng_seed_hex": "010203",
"signature_hex": "bead"
}
}
```
Federation records example:
```bash
curl --unix-socket amduatd.sock \
'http://localhost/v1/fed/records?domain_id=1&from_logseq=0&limit=256'
```
```json
{
"domain_id": 1,
"snapshot_id": 42,
"log_prefix": 1234,
"next_logseq": 120,
"records": [
{
"domain_id": 1,
"type": 0,
"ref": "ab12...",
"logseq": 100,
"snapshot_id": 42,
"log_prefix": 1234,
"visibility": 1,
"has_source": false,
"source_domain": 0
}
]
}
```
Response example:
```json
{
"result_ref": "aa11...",
"trace_ref": "bb22...",
"receipt_ref": "cc33...",
"output_refs": ["dd44..."],
"status": "OK"
}
```
## Notes ## Notes

View file

@ -0,0 +1,196 @@
# amduatd API v2 Design (PEL-only writes)
## Status
Draft proposal for discussion.
First server slice implemented: `POST /v2/pel/execute` with `inputs.refs`,
`inputs.inline_artifacts[{body_hex,type_tag?}]`, and required `receipt`.
## Goals
- Keep `amduatd` thin: transport, parsing, auth, and mapping to core calls.
- Make **PEL the only write primitive** in HTTP v2.
- Enforce artifact provenance for writes: adding new artifacts must go through:
1. store inputs,
2. run a PEL program,
3. persist and return receipt.
- Keep `/v1/*` behavior unchanged while v2 is introduced.
## Non-goals
- Re-implementing PEL semantics in `amduat-api`.
- Creating new substrate semantics in daemon code.
- Removing v1 immediately.
## High-level model
v2 separates read and write concerns:
- Read paths remain direct and deterministic (`GET /v2/meta`, `GET /v2/artifacts/{ref}`, federation/space read endpoints).
- All state-changing operations are executed as PEL runs.
In v2, `POST /v1/artifacts` equivalent is replaced by a mutation endpoint that always drives a PEL run.
For simplified clients, v2 also exposes async operation endpoints:
- `POST /v2/ops/put`
- `POST /v2/ops/concat`
- `POST /v2/ops/slice`
- `GET /v2/jobs/{id}`
These enqueue work and execute PEL in the daemon background loop.
Graph-oriented endpoints can be added as thin wrappers over concepts/relations:
- `POST /v2/graph/nodes` (maps to concept create/publish)
- `POST /v2/graph/edges` (maps to relation edge append)
- `GET /v2/graph/nodes/{name}` (node + versions + incoming/outgoing edges)
- `GET /v2/graph/history/{name}` (version + edge timeline)
## Versioning and contract id
- Base path: `/v2`
- Contract id: `AMDUATD/API/2`
- Keep the same store-seeded contract model used in v1.
## Endpoint shape
### Read-only endpoints (v2)
- `GET /v2/meta`
- `GET /v2/contract`
- `GET /v2/artifacts/{ref}`
- `HEAD /v2/artifacts/{ref}`
- `GET /v2/artifacts/{ref}?format=info`
- Space/federation read endpoints can be mirrored under `/v2/*` unchanged where possible.
### Write endpoints (v2)
- `POST /v2/pel/execute`
No direct artifact write endpoint in v2 (`POST /v2/artifacts` is intentionally absent).
## `POST /v2/pel/execute`
Single mutation primitive for v2.
### Request
```json
{
"program_ref": "<hex-ref-or-name>",
"scheme_ref": "dag",
"params_ref": "<optional-ref-or-name>",
"inputs": {
"refs": ["<hex-ref-or-name>"],
"inline_artifacts": [
{
"content_type": "application/octet-stream",
"type_tag": "0x00000000",
"body_hex": "48656c6c6f"
}
]
},
"receipt": {
"input_manifest_ref": "<ref-or-name>",
"environment_ref": "<ref-or-name>",
"evaluator_id": "local-amduatd",
"executor_ref": "<ref-or-name>",
"started_at": 1731000000,
"completed_at": 1731000005
},
"effects": {
"publish_outputs": true,
"append_fed_log": true
}
}
```
### Processing contract
Server behavior is deterministic and ordered:
1. Resolve `program_ref`, `scheme_ref`, `params_ref`, and `inputs.refs` to refs.
2. Decode and store `inputs.inline_artifacts` into ASL store.
3. Build effective `input_refs = refs + stored_inline_refs`.
4. Execute PEL using core `pel_surf_run_with_result`.
5. Build FER receipt from run result + receipt fields.
6. Store receipt artifact and return `receipt_ref`.
7. Apply requested side effects (`publish_outputs`, `append_fed_log`) after successful run.
If any step fails:
- return non-2xx,
- do not publish outputs,
- do not append federation publish records.
## Response
```json
{
"run_ref": "<pel1-result-ref>",
"trace_ref": "<optional-trace-ref>",
"receipt_ref": "<fer1-receipt-ref>",
"stored_input_refs": ["<ref>"],
"output_refs": ["<ref>"],
"status": "OK"
}
```
Notes:
- `stored_input_refs` are refs created from `inline_artifacts`.
- `receipt_ref` is required for successful writes in v2.
## Error model
- `400` invalid request or schema violation.
- `404` referenced input/program/params not found.
- `409` conflict in post-run side effects (e.g. cursor/update conflicts).
- `422` run completed but business policy failed (reserved for policy checks).
- `500` internal failure.
Error payload:
```json
{
"error": {
"code": "invalid_input_ref",
"message": "input ref not found",
"retryable": false
}
}
```
## Behavioral differences from v1
- Removed write path: direct `POST /artifacts`.
- New requirement: every write returns a receipt ref tied to a PEL run.
- `pel/run` semantics become the write gateway rather than an optional compute endpoint.
## Compatibility and rollout
1. Add `/v2/pel/execute` while keeping `/v1/*` unchanged.
2. Mirror core read endpoints under `/v2/*`.
3. Mark `POST /v1/artifacts` as deprecated in docs.
4. Migrate clients to v2 execute flow.
5. Remove v1 write endpoints in a later major release.
## Open questions
- Should v2 require exactly one output for write operations, or allow multiple outputs with one receipt?
- Should inline artifact body support both `body_hex` and `body_base64`, or move to multipart for large payloads?
- Should `publish_outputs` default to `true` or be explicit-only?
- Do we need async execution (`202 + run status endpoint`) for long-running programs in v2?
## Minimal implementation plan
1. Add v2 contract bytes (`registry/amduatd-api-contract.v2.json`).
2. Add `/v2/pel/execute` handler by adapting existing `POST /v1/pel/run` + artifact ingest path.
3. Factor shared receipt parsing/building from current v1 pel handler.
4. Add tests:
- inline artifact -> stored -> run -> receipt success,
- failure before run,
- failure during run,
- side effect gating (no publish on failure).
5. Document deprecation notice for `POST /v1/artifacts`.

View file

@ -0,0 +1,355 @@
# Federation Coordinator Middle Layer Spec
This document specifies the middle-layer coordinator that orchestrates federation
above the core C substrate. It remains transport- and policy-aware while keeping
core semantics unchanged.
## Scope and Goals
- Maintain per-domain replay bounds and an admitted set.
- Fetch and ingest published records from remotes.
- Build federation views via core APIs.
- Resolve missing bytes by fetching artifacts into a cache store.
- Keep storage layout private (no extents or blocks exposed).
- Align with tier1 federation semantics and replay determinism.
- Federation view is the union of admitted domains regardless of topology.
Non-goals:
- Re-implement core semantics in this layer.
- Introduce a single global snapshot ID (federation is per-domain).
## Core Dependencies
The coordinator calls into vendor core APIs (`vendor/amduat/include/amduat/fed/*`)
and MUST stay aligned with their signatures:
- `amduat_fed_registry_*` (persist per-domain admission state)
- `amduat_fed_ingest_validate` (record validation + conflict detection)
- `amduat_fed_replay_build` (deterministic replay per domain)
- `amduat_fed_view_build` + `amduat_fed_resolve` (build view and resolve ref-only)
Core expects per-domain replay bounds `{domain_id, snapshot_id, log_prefix}` and
does not handle transport, auth, caching policy, or remote fetch.
Alignment note: the daemon-layer API in this repo still needs updates to match
current vendor core types and signatures. This spec reflects vendor headers as
the source of truth.
Tier1 alignment (normative):
- `ASL/FEDERATION/1`
- `ASL/FEDERATION-REPLAY/1`
- `ASL/DOMAIN-MODEL/1`
- `ASL/POLICY-HASH/1`
- `ENC/ASL-CORE-INDEX/1` (canonical in `vendor/amduat/tier1/enc-asl-core-index-1.md`)
## Data Structures
### Registry State (per domain, persisted)
```
struct amduat_fed_domain_state {
uint32_t domain_id;
uint64_t snapshot_id;
uint64_t log_prefix;
uint64_t last_logseq;
uint8_t admitted;
uint8_t policy_ok;
uint8_t reserved[6];
amduat_hash_id_t policy_hash_id;
amduat_octets_t policy_hash; /* Empty when unknown; caller-owned bytes. */
};
```
Registry bytes are stored via `amduat_fed_registry_store_*` in the local ASL
store; the coordinator owns admission workflows and policy compatibility checks.
### Admitted Set (in-memory)
```
struct amduat_fed_admitted_set {
uint32_t *domain_ids; // sorted, unique
size_t len;
};
```
The admitted set is derived from registry entries with `admitted != 0` and
`policy_ok != 0`.
### Snapshot Vector (per view build)
```
struct amduat_fed_snapshot_vector {
amduat_fed_view_bounds_t *bounds;
size_t len;
uint64_t vector_epoch;
};
```
### Record Staging (per fetch batch)
```
struct amduat_fed_record {
amduat_fed_record_meta_t meta;
amduat_fed_record_id_t id;
uint64_t logseq;
uint64_t snapshot_id;
uint64_t log_prefix;
};
```
Records MUST carry the fields required by `ASL/FEDERATION-REPLAY/1`, and replay
ordering MUST be deterministic (sort by `logseq`, then canonical identity).
Record metadata includes visibility and optional cross-domain source identity.
### View and Policy Denies
```
struct amduat_fed_view_bounds {
uint32_t domain_id;
uint64_t snapshot_id;
uint64_t log_prefix;
};
struct amduat_fed_policy_deny {
amduat_fed_record_id_t id;
uint32_t reason_code;
};
```
### Fetch Backlog / Retry State
```
struct amduat_fed_fetch_state {
uint32_t domain_id;
uint64_t next_snapshot_id;
uint64_t next_log_prefix;
uint64_t next_logseq;
uint64_t backoff_ms;
uint64_t last_attempt_ms;
uint32_t consecutive_failures;
};
```
### Cache Store Metadata (optional)
```
struct amduat_fed_cache_policy {
bool enabled;
uint64_t max_bytes;
uint64_t used_bytes;
uint32_t ttl_seconds;
uint32_t prefetch_depth;
};
```
## Coordinator Interfaces
### Configuration
```
struct amduat_fed_coord_cfg {
const char *registry_path;
amduat_asl_store_t *authoritative_store;
amduat_asl_store_t *cache_store; // optional
amduat_asl_store_t *session_store; // optional
amduat_fed_transport transport;
amduat_fed_cache_policy cache_policy;
amduat_fed_policy_hooks policy_hooks;
};
```
### Lifecycle and Operations
```
int amduat_fed_coord_open(
const struct amduat_fed_coord_cfg *cfg,
struct amduat_fed_coord **out);
int amduat_fed_coord_close(struct amduat_fed_coord *c);
int amduat_fed_coord_load_registry(struct amduat_fed_coord *c);
int amduat_fed_coord_set_admitted(
struct amduat_fed_coord *c,
uint32_t domain_id,
bool admitted);
int amduat_fed_coord_tick(struct amduat_fed_coord *c, uint64_t now_ms);
int amduat_fed_coord_resolve(
struct amduat_fed_coord *c,
amduat_reference_t ref,
amduat_artifact_t *out);
```
## API Status
Planned coordinator surface (not yet implemented):
- `amduat_fed_coord_open`
- `amduat_fed_coord_close`
- `amduat_fed_coord_load_registry`
- `amduat_fed_coord_set_admitted`
- `amduat_fed_coord_tick`
- `amduat_fed_coord_resolve`
Implemented in core (already available):
- `amduat_fed_registry_*`
- `amduat_fed_ingest_validate`
- `amduat_fed_replay_build`
- `amduat_fed_view_build`
- `amduat_fed_resolve`
## Transport Abstraction
Minimal interface that hides protocol, auth, and topology:
```
struct amduat_fed_transport {
int (*get_records)(
void *ctx, uint32_t domain_id,
uint64_t snapshot_id, uint64_t log_prefix,
uint64_t from_logseq,
amduat_fed_record_iter *out_iter);
int (*get_artifact)(
void *ctx, amduat_reference_t ref,
amduat_sink *out_sink);
};
```
Transport MUST return records that can be validated with `amduat_fed_record_validate`
and MUST provide all fields required by `ASL/FEDERATION-REPLAY/1`.
Transport MUST NOT surface internal-only records from foreign domains.
## Storage and Encodings
- The coordinator stores records/artifacts via ASL store APIs and does not touch
segment layouts or extents directly.
- Federation metadata in index records is encoded by core per
`ENC/ASL-CORE-INDEX/1`; the coordinator must not override it.
- Cache store semantics are best-effort and do not affect authoritative state.
## Policies
- Admission is per-domain and controlled via registry entries and `policy_ok`.
- Policy compatibility uses `policy_hash_id` + `policy_hash` (ASL/POLICY-HASH/1).
- `policy_ok` is computed during admission by comparing local policy hash to the
remote domain's published policy hash.
- Admission and policy compatibility MUST be enforced before any foreign state is
admitted into the federation view.
- Per-record filtering, if used, MUST be deterministic and SHOULD be expressed
as policy denies passed to `amduat_fed_view_build` rather than by dropping
records before validation.
- Cache write policy is middle-layer only (fetch-on-miss, optional prefetch).
- Eviction is local (LRU or segmented queues) and must not leak layout.
- Conflict policy: reject on identity collision with differing metadata and keep
bounds stable until operator intervention (ASL/FEDERATION-REPLAY/1).
## Sequencing and Consistency
- Deterministic views require a stable snapshot vector per build.
- Bounds advance only after successful ingest; they never move backwards.
- Before validation, records are ordered by `(logseq, canonical identity)` and
filtered to `logseq <= log_prefix`.
- Tombstones and shadowing apply only within the source domain.
- Use `vector_epoch` to swap snapshot vectors atomically after a build.
- Persist registry updates atomically via `amduat_fed_registry_store_put` before
swapping the snapshot vector.
- If a remote retracts or regresses, keep local bounds and mark domain degraded.
## Core Flow
### Startup
- Load registry.
- Derive admitted set from `admitted != 0` and `policy_ok != 0`.
- Build initial snapshot vector.
### Periodic Tick
- For each admitted domain, fetch records up to bound and validate.
- Update `last_logseq`, and advance bounds if remote snapshot moves forward.
- Build federation view using `amduat_fed_view_build` over the staged records,
with optional policy denies derived from coordinator hooks.
- Swap snapshot vector atomically after registry updates.
### Resolve Loop
- Call `amduat_fed_resolve` against the latest built `amduat_fed_view_t`.
- If `AMDUAT_FED_RESOLVE_FOUND_REMOTE_NO_BYTES`, fetch bytes via transport into cache store.
- Retry resolve after cache write completes.
## Example Tick Pseudocode
```
int amduat_fed_coord_tick(struct amduat_fed_coord *c, uint64_t now_ms) {
amduat_fed_snapshot_vector vec = build_snapshot_vector(c);
clear(staged_records_all);
build_policy_denies(&policy_denies, &policy_denies_len);
for each domain in vec.bounds:
bound = &vec.bounds[i];
if !admitted(bound->domain_id) continue;
if backoff_active(bound->domain_id, now_ms) continue;
clear(staged_records);
iter = transport.get_records(
bound->domain_id,
bound->snapshot_id,
bound->log_prefix,
state.next_logseq);
while iter.next(record):
if !amduat_fed_record_validate(&record):
mark_domain_error(bound->domain_id);
break;
append(staged_records, record);
sort_by_logseq_then_id(staged_records);
clamp_to_log_prefix(staged_records, bound->log_prefix);
rc = amduat_fed_ingest_validate(
staged_records, staged_len, &err_index, &conflict_index);
if rc == AMDUAT_FED_INGEST_ERR_CONFLICT:
mark_domain_error(bound->domain_id);
continue;
if rc == AMDUAT_FED_INGEST_ERR_INVALID:
mark_domain_error(bound->domain_id);
continue;
update_registry_bounds(bound->domain_id, state);
append_all(staged_records_all, staged_records);
amduat_fed_view_build(
staged_records_all, staged_len_all,
local_domain_id, vec.bounds, vec.len,
policy_denies, policy_denies_len,
&view);
swap_snapshot_vector(c, build_snapshot_vector(c));
return 0;
}
```
## Example Resolve Pseudocode
```
int amduat_fed_coord_resolve(
struct amduat_fed_coord *c,
amduat_reference_t ref,
amduat_artifact_t *out) {
view = c->last_view;
rc = amduat_fed_resolve(&view, c->authoritative_store, ref, out);
if (rc == AMDUAT_FED_RESOLVE_FOUND_REMOTE_NO_BYTES && c->cache_store) {
rc = transport.get_artifact(ref, sink_for_cache(c->cache_store));
if (rc == 0) {
rc = amduat_fed_resolve(&view, c->authoritative_store, ref, out);
}
}
return rc;
}
```
## Coordinator Wiring Example
```
amduat_fed_transport_unix_t unix_transport;
amduat_fed_coord_cfg_t cfg;
amduat_fed_coord_t *coord = NULL;
amduat_fed_transport_unix_init(&unix_transport, "amduatd.sock");
memset(&cfg, 0, sizeof(cfg));
cfg.local_domain_id = 1;
cfg.authoritative_store = &store;
cfg.cache_store = &cache_store;
cfg.transport = amduat_fed_transport_unix_ops(&unix_transport);
if (amduat_fed_coord_open(&cfg, &coord) == AMDUAT_FED_COORD_OK) {
amduat_fed_coord_tick(coord, now_ms);
amduat_fed_coord_resolve(coord, some_ref, &artifact);
amduat_fed_coord_close(coord);
}
```

139
docs/state_report.md Normal file
View file

@ -0,0 +1,139 @@
# Repo State Report
## 1) Build + test entrypoints
- Tests: `ctest --test-dir build` (top-level build includes vendor tests via `add_subdirectory(vendor/amduat)` in `CMakeLists.txt`); core-only tests: `ctest --test-dir vendor/amduat/build` (binaries present in `vendor/amduat/build`).
- Main daemon: `./build/amduatd --root .amduat-asl --sock amduatd.sock` (`README.md`, `src/amduatd.c`).
- Dev loop: `./scripts/dev-restart.sh` (build + restart in `scripts/dev-restart.sh`).
- Core CLIs/tools: `./vendor/amduat/build/amduat-asl ...` (store/init/log/index commands in `vendor/amduat/src/tools/amduat_asl_cli.c`), `./vendor/amduat/build/amduat-pel ...` (PEL tooling in `vendor/amduat/src/tools/amduat_pel_cli.c`), `./build/amduat-pel gc --root .amduat-asl` (GC tool in `src/amduat_pel_gc.c`).
- Languages/toolchains: C11 + CMake (`CMakeLists.txt`, `vendor/amduat/CMakeLists.txt`), shell scripts (`scripts/*.sh`), embedded HTML/JS/CSS in C strings (`src/amduatd_ui.c`).
- CI config: none found (no `.github/workflows/*`, `.gitlab-ci.yml`, etc.).
## 2) Top-level architecture map (as implemented)
- Daemon runtime + HTTP surface: Paths `src/amduatd.c`, `src/amduatd_http.c`, `src/amduatd_http.h`, `src/amduatd_ui.c`; main types `amduatd_cfg_t`, `amduatd_http_req_t`, `amduatd_http_resp_t`; entrypoints `main`, `amduatd_handle_conn`, `amduatd_http_read_req`; wiring: initializes store fs config, concepts, caps, federation coord, then serves HTTP over Unix socket with a `select()` loop (`src/amduatd.c`).
- Capability tokens: Paths `src/amduatd_caps.c`, `src/amduatd_caps.h`; main types `amduatd_caps_t`, `amduatd_caps_token_t`; entrypoints `amduatd_caps_init`, `amduatd_caps_handle`, `amduatd_caps_check`; wiring: per-request token validation with optional space/pointer scoping (`src/amduatd_caps.c`).
- Concepts/relations + edge graph index: Paths `src/amduatd_concepts.c`, `src/amduatd_concepts.h`; main types `amduatd_concepts_t`, `amduatd_edge_index_state_t`, `amduatd_edge_list_t`; entrypoints `amduatd_concepts_init`, `amduatd_concepts_refresh_edges`, `amduatd_handle_get_relations`, `amduatd_handle_get_concepts`; wiring: edge records stored in a collection log, plus derived edge graph + index pointer state (`src/amduatd_concepts.c`).
- Space scoping: Paths `src/amduatd_space.c`, `src/amduatd_space.h`; main types `amduatd_space_t`; entrypoints `amduatd_space_init`, `amduatd_space_scope_name`, `amduatd_space_unscoped_name`; wiring: scopes pointer/collection names with prefix `space/<space_id>/` when enabled (`src/amduatd_space.c`).
- Federation coordinator: Paths `federation/coord.c`, `federation/coord.h`; main types `amduat_fed_coord_t`, `amduat_fed_coord_cfg_t`; entrypoints `amduat_fed_coord_open`, `amduat_fed_coord_tick`, `amduat_fed_coord_get_status`; wiring: daemon ticks coordinator on interval (`src/amduatd.c`).
- Federation transport: Paths `federation/transport_unix.c`, `federation/transport_stub.c`; main types `amduat_fed_transport_t`; entrypoints `amduat_fed_transport_unix_ops`, `amduat_fed_transport_stub_ops`; wiring: daemon currently uses stub transport (`src/amduatd.c`).
- CAS store (filesystem): Paths `vendor/amduat/src/adapters/asl_store_fs/*`, `vendor/amduat/include/amduat/asl/store.h`; main types `amduat_asl_store_fs_t`, `amduat_asl_store_t`; entrypoints `amduat_asl_store_fs_init`, `amduat_asl_store_put`, `amduat_asl_store_get`; wiring: used by daemon and tools as the backing store.
- Pointer store: Paths `vendor/amduat/src/adapters/asl_pointer_fs/asl_pointer_fs.c`; main types `amduat_asl_pointer_store_t`; entrypoints `amduat_asl_pointer_get`, `amduat_asl_pointer_cas`; wiring: used by log store, collection store, concept index, and caps checks.
- Log store: Paths `vendor/amduat/src/core/asl_log_store.c`, `vendor/amduat/include/amduat/asl/log_store.h`; main types `amduat_asl_log_store_t`, `amduat_asl_log_entry_t`; entrypoints `amduat_asl_log_append`, `amduat_asl_log_read`; wiring: collection store writes log chunks in CAS and advances pointer heads.
- Collection store: Paths `vendor/amduat/src/core/asl_collection.c`, `vendor/amduat/include/amduat/asl/collection.h`; main types `amduat_asl_collection_store_t`; entrypoints `amduat_asl_collection_append`, `amduat_asl_collection_snapshot`, `amduat_asl_collection_read`; wiring: concept edges are stored as records appended to a collection log.
- ASL index store: Paths `vendor/amduat/src/adapters/asl_store_index_fs/*`; main types `amduat_asl_store_index_fs_t`, `amduat_asl_index_state_t`; entrypoints `amduat_asl_store_index_fs_put_indexed`, `amduat_asl_store_index_fs_log_scan`, `amduat_asl_store_index_fs_gc`; wiring: available in core but not wired into amduatd (amduatd uses store_fs ops).
- PEL execution + materialization cache: Paths `vendor/amduat/src/pel_stack/surf/surf.c`, `vendor/amduat/src/adapters/asl_materialization_cache_fs/asl_materialization_cache_fs.c`; main types `amduat_pel_program_t`, `amduat_pel_run_result_t`; entrypoints `amduat_pel_surf_run_with_result`, `amduat_asl_materialization_cache_fs_get`, `amduat_asl_materialization_cache_fs_put`; wiring: used by `/v1/pel/run` and by the collection view implementation.
- Derivation index (core-only): Paths `vendor/amduat/src/adapters/asl_derivation_index_fs/asl_derivation_index_fs.c`, `vendor/amduat/include/amduat/asl/asl_derivation_index_fs.h`; main types `amduat_asl_derivation_index_fs_t`, `amduat_asl_derivation_record_t`; entrypoints `amduat_asl_derivation_index_fs_add`, `amduat_asl_derivation_index_fs_list`; wiring: used by CLI tools (`vendor/amduat/src/tools/amduat_asl_cli.c`, `vendor/amduat/src/tools/amduat_pel_cli.c`), not by amduatd.
## 3) “Space” concept: definition + storage layout
- Definition: “space” is a daemon-level scoping prefix for pointer/collection names, not a separate store; it rewrites names into `space/<space_id>/...` when enabled (`src/amduatd_space.c`, `amduatd_space_scope_name`, `amduatd_space_unscoped_name`).
- Storage layout: scoped names are stored in the pointer store under `root/pointers/space/<space_id>/.../head` per `amduat_asl_pointer_build_head_path` (`vendor/amduat/src/adapters/asl_pointer_fs/asl_pointer_fs.c`). No dedicated per-space directory outside pointer name paths is created by amduatd.
- Identification: `space_id` is a pointer-name-safe token without `/`, validated by `amduatd_space_space_id_is_valid` (calls `amduat_asl_pointer_name_is_valid`) (`src/amduatd_space.c`).
- Multi-space support: only one space can be active per daemon process via `--space` (`src/amduatd.c`, `amduatd_space_init`); code does not show per-request space selection.
## 4) Source of truth: pointers + logs (actual)
- Canonical “head/root” pointer for a space: there is no single global space root pointer. The main space-scoped heads used by amduatd are the edge index head (`daemon/edges/index/head` scoped by `amduatd_space_edges_index_head_name`) and collection snapshot/log heads for the edge collection (see below) (`src/amduatd_space.c`, `src/amduatd_concepts.c`).
- Pointer storage location + format:
- Location: `root/pointers/<name>/head` using path segments from the pointer name (`amduat_asl_pointer_build_head_path` in `vendor/amduat/src/adapters/asl_pointer_fs/asl_pointer_fs.c`).
- Format: `ASLPTR1` magic with version and flags, then name/ref/prev fields encoded via `amduat_enc_asl1_core_encode_reference_v1` (`amduat_asl_pointer_read_head` and `amduat_asl_pointer_write_head` in `vendor/amduat/src/adapters/asl_pointer_fs/asl_pointer_fs.c`).
- Read/write: `amduat_asl_pointer_get` and `amduat_asl_pointer_cas` (`vendor/amduat/src/adapters/asl_pointer_fs/asl_pointer_fs.c`).
- Collection heads used by amduatd concepts:
- Snapshot head pointer: `collection/<name>/head` built by `amduatd_build_collection_head_name` (`src/amduatd_concepts.c`) and also by `amduat_asl_collection_build_head_name` (`vendor/amduat/src/core/asl_collection.c`).
- Log head pointer: `log/collection/<name>/log/head` built by `amduatd_build_collection_log_head_name` (`src/amduatd_concepts.c`) and `amduat_asl_log_build_pointer_name` (`vendor/amduat/src/core/asl_log_store.c`).
- Log entry schema (ASL index log): `amduat_asl_log_record_t {logseq, record_type, payload, record_hash}` with record types defined in `vendor/amduat/include/amduat/enc/asl_log.h`.
- Log append behavior:
- Collection log: `amduat_asl_log_append` writes CAS log chunks (`ASL_LOG_CHUNK_1`) and advances the log head pointer via CAS (`vendor/amduat/src/core/asl_log_store.c`).
- Index log: `amduat_asl_store_index_fs_append_log_record` appends to `index/log.asl` (`vendor/amduat/src/adapters/asl_store_index_fs/asl_store_index_fs.c`).
- Integrity mechanisms:
- Collection log chunks form a chain via `prev_ref` and are rooted at the log head pointer (`amduat_asl_log_append`, `amduat_asl_log_chunk_t` in `vendor/amduat/src/core/asl_log_store.c`).
- Index log uses a hash chain where `record_hash = SHA256(prev_hash || logseq || type || payload_len || payload)` (`amduat_asl_store_index_fs_log_hash_record` in `vendor/amduat/src/adapters/asl_store_index_fs/asl_store_index_fs.c`).
- Log traversal:
- Collection log: `amduat_asl_log_read` walks the head pointer and `prev_ref` chain to gather chunks (`vendor/amduat/src/core/asl_log_store.c`).
- Index log: replay happens via `amduat_asl_store_index_fs_build_replay_state` using `amduat_asl_replay_apply_log` (`vendor/amduat/src/adapters/asl_store_index_fs/asl_store_index_fs.c`, `vendor/amduat/include/amduat/asl/index_replay.h`).
- Other persistent state that can diverge from logs/pointers:
- Edge index state and edge graph artifacts (`tgk/edge_index_state` record + `TGK_EDGE_GRAPH_1` artifact) are separate derived state from the edge collection log (`src/amduatd_concepts.c`).
- Legacy `.amduatd.edges` file (used only for migration) can diverge until migrated (`src/amduatd_concepts.c`).
- Materialization cache in `index/materializations` can diverge from CAS and is treated as a cache (`vendor/amduat/src/adapters/asl_materialization_cache_fs/asl_materialization_cache_fs.c`).
## 5) CAS (content-addressed store)
- Hash algorithm: SHA-256 is the only defined ASL1 hash id and default (`AMDUAT_HASH_ASL1_ID_SHA256` in `vendor/amduat/include/amduat/hash/asl1.h`, default config in `vendor/amduat/src/adapters/asl_store_fs/asl_store_fs_meta.c`).
- Object types stored: arbitrary artifacts with optional type tags (`amduat_artifact_t` in `vendor/amduat/include/amduat/asl/core.h`); records are stored via `amduat_asl_record_store_put` (schema + payload) in `vendor/amduat/src/core/asl_record.c` and used by amduatd for concept edges (`src/amduatd_concepts.c`).
- Address format: refs are `{hash_id, digest}`; on disk, objects are stored under hex-digest filenames derived from the raw digest (`amduat_asl_store_fs_layout_build_paths` in `vendor/amduat/src/adapters/asl_store_fs/asl_store_fs_layout.c`).
- Disk layout and APIs:
- Layout: `root/objects/<profile_hex>/<hash_hex>/<byte0>/<byte1>/<digest_hex>` (`amduat_asl_store_fs_layout_build_paths` in `vendor/amduat/src/adapters/asl_store_fs/asl_store_fs_layout.c`).
- APIs: `amduat_asl_store_put/get` (generic) and `amduat_asl_store_fs_ops` (filesystem implementation) (`vendor/amduat/include/amduat/asl/store.h`, `vendor/amduat/src/adapters/asl_store_fs/asl_store_fs.c`).
- Garbage collection:
- Exists as a standalone tool: `amduat-pel gc` uses `amduat_asl_gc_fs_run` to mark from pointer/log/collection roots and optionally delete artifacts (`src/amduat_pel_gc.c`, `src/asl_gc_fs.c`).
- Pinning concept: no explicit pin API found; reachability is derived from pointers/logs/snapshots (GC uses those roots) (`src/asl_gc_fs.c`).
## 6) Deterministic derivations (current reality)
- Derivation-related types exist in core: `amduat_pel_derivation_sid_input_t`, `amduat_pel_derivation_sid_compute` (`vendor/amduat/include/amduat/pel/derivation_sid.h`, `vendor/amduat/src/core/derivation_sid.c`).
- Execution model: PEL DAGs execute in-process (no external sandbox) via `amduat_pel_program_dag_exec_trace` and `amduat_pel_surf_run_with_result` (`vendor/amduat/src/pel_stack/surf/surf.c`).
- Inputs: referenced as CAS refs from the store (`amduat_pel_surf_run_with_result` loads program/input/params artifacts by ref in `vendor/amduat/src/pel_stack/surf/surf.c`).
- Outputs: stored back into CAS (`amduat_asl_store_put` in `vendor/amduat/src/pel_stack/surf/surf.c`); results/traces/receipts are separate artifacts (`amduat_enc_pel1_result`, `amduat_enc_pel_trace_dag` etc. used in `src/amduatd.c`).
- Provenance/audit: optional FER1 receipt data is accepted/serialized in `/v1/pel/run` (`src/amduatd.c`), but no daemon-side derivation index is written.
- Derivation index persistence: exists in core (`vendor/amduat/src/adapters/asl_derivation_index_fs/asl_derivation_index_fs.c`) and CLI tools (`vendor/amduat/src/tools/amduat_asl_cli.c`, `vendor/amduat/src/tools/amduat_pel_cli.c`), but amduatd does not write derivation records (no references in `src/`).
## 7) ASL index: what it is and what it depends on
- Storage location: `root/index/` with `log.asl`, `segments/`, `blocks/`, and `snapshots/` (layout in `vendor/amduat/src/adapters/asl_store_index_fs/asl_store_index_fs_layout.c`).
- Derived vs authoritative: the index log (`index/log.asl`) and segment files are the authoritative index state; higher-level state can be rebuilt by replaying the log plus snapshots (`amduat_asl_store_index_fs_build_replay_state` + `amduat_asl_replay_apply_log` in `vendor/amduat/src/adapters/asl_store_index_fs/asl_store_index_fs.c`).
- Build/update path: `amduat_asl_store_index_fs_put_indexed` appends log records, writes segments/blocks, and may create snapshots (`vendor/amduat/src/adapters/asl_store_index_fs/asl_store_index_fs.c`).
- Queries relying on it: `amduat_asl_log_scan`, tombstone operations, and `amduat_asl_index_current_state` are implemented by the index-backed store ops (`vendor/amduat/src/adapters/asl_store_index_fs/asl_store_index_fs.c`, `vendor/amduat/include/amduat/asl/store.h`).
- What breaks if deleted: if `index/` (including `log.asl`) is removed, index-backed stores cannot answer log/state/tombstone queries; recovery requires a log to replay, which lives under `index/` itself (`vendor/amduat/src/adapters/asl_store_index_fs/asl_store_index_fs.c`).
## 8) Daemon / runtime model (if any)
- Daemon process: `amduatd` in `src/amduatd.c` binds a Unix domain socket, listens, and handles one connection per `accept()` in a single-threaded loop using `select()`.
- Single-space or multi-space: single-space per daemon process via `--space` (`src/amduatd.c`, `src/amduatd_space.c`).
- Config mechanism: CLI flags `--root`, `--sock`, `--space`, `--migrate-unscoped-edges`, `--edges-refresh-ms`, `--allow-uid`, `--allow-user`, `--enable-cap-reads` (`src/amduatd.c`).
- IPC/RPC/HTTP APIs: HTTP over Unix socket, routes in `src/amduatd.c` and handlers in `src/amduatd_concepts.c` and `src/amduatd_caps.c`.
- Background workers: federation tick every 1s (`AMDUATD_FED_TICK_MS`) and optional edge refresh on `--edges-refresh-ms` interval (`src/amduatd.c`).
## 9) Projections / query layer
- Projection concept in this repo: the edge graph and edge index state are projections derived from the edge collection log (`AMDUATD_EDGE_INDEX_SCHEMA`, `amduatd_concepts_write_edge_index_state`, `amduatd_concepts_refresh_edges_internal` in `src/amduatd_concepts.c`).
- Generation + storage: edge graph stored as `TGK_EDGE_GRAPH_1` artifact plus pointer to `tgk/edge_index_state` record (`src/amduatd_concepts.c`).
- Derived from logs/CAS: refresh reads collection log entries via `amduat_asl_log_read` and rebuilds the edge list/graph (`src/amduatd_concepts.c`).
- Query APIs: HTTP endpoints for concepts/relations/resolve in `src/amduatd_concepts.c` and routing in `src/amduatd.c`; collection view is generated by a PEL program over collection snapshot + log (`amduatd_collection_view` in `src/amduatd_concepts.c`).
## 10) Federation / collaboration
- Networking code: `federation/transport_unix.c` builds HTTP requests over Unix sockets for `/v1/fed/records` and `/v1/fed/artifacts` (see `amduat_fed_transport_unix_ops`).
- Federation coordinator: `federation/coord.c` maintains registry state and a cached view (`amduat_fed_coord_t`).
- Daemon behavior: federation is wired but uses the stub transport (`amduat_fed_transport_stub_ops` in `src/amduatd.c`), so no remote sync by default.
- Remote refs/replication/merge/signatures: not implemented in daemon beyond read-only federation endpoints; no CRDT/merge logic found in this repo (coordinator logic delegates to core types in `vendor/amduat/include/amduat/fed/*`).
## 11) Invariants (explicit + implicit)
- Pointer names are path-safe and forbid `..` segments; used throughout for space IDs and pointer names (`amduat_asl_pointer_name_is_valid` in `vendor/amduat/src/adapters/asl_pointer_fs/asl_pointer_fs.c`).
- Collection log append is append-only with CAS+pointer CAS; chunks point to previous chunk to form a chain (`amduat_asl_log_append`, `amduat_asl_log_chunk_t` in `vendor/amduat/src/core/asl_log_store.c`).
- Log chunk entries must be consistent about timestamp/actor presence across the batch (`amduat_asl_log_entries_consistent` in `vendor/amduat/src/core/asl_log_store.c`).
- Collection snapshot heads and log heads are stable pointer names derived from collection name (`amduat_asl_collection_build_head_name` and `amduat_asl_collection_build_log_name` in `vendor/amduat/src/core/asl_collection.c`).
- Index log hash chain uses `prev_hash` + record fields to compute the next hash (`amduat_asl_store_index_fs_log_hash_record` in `vendor/amduat/src/adapters/asl_store_index_fs/asl_store_index_fs.c`).
- Edge index state is written via pointer CAS, implying the index head should only advance if the expected previous ref matches (`amduatd_concepts_write_edge_index_state` in `src/amduatd_concepts.c`).
## 12) Immediate risks of “parallel mechanisms”
- Edge index state vs edge collection log: the derived edge graph + `tgk/edge_index_state` can diverge from the log if refresh fails; there is a deterministic rebuild path by replaying the collection log (`amduatd_concepts_refresh_edges_internal` in `src/amduatd_concepts.c`).
- Legacy `.amduatd.edges` vs collection log: migration reads `.amduatd.edges` and writes into the collection log, so stale files can diverge until migrated (`amduatd_concepts_migrate_edges` in `src/amduatd_concepts.c`).
- Materialization cache vs CAS: cache entries are validated against CAS and are treated as a performance-only layer; missing or stale cache forces recompute (`amduat_asl_materialization_cache_fs_get` usage in `vendor/amduat/src/pel_stack/surf/surf.c`).
- ASL index files vs index log: segment/summary/snapshot files are derived from `index/log.asl`; if any are deleted, `amduat_asl_store_index_fs_build_replay_state` can rebuild from the log, but if `index/log.asl` is deleted the rebuild story is gone (`vendor/amduat/src/adapters/asl_store_index_fs/asl_store_index_fs.c`).
- Federation coordinator cached view vs store log: coordinator caches `last_view` in memory and refreshes on tick; divergence is possible across restarts or if tick stops (`federation/coord.c`, `src/amduatd.c`).
## 13) Recommendation inputs (no decision yet)
- Current capability summary:
- Local Unix-socket HTTP daemon over a single ASL store root with artifact CRUD, concepts/relations, and PEL execution (`src/amduatd.c`, `src/amduatd_concepts.c`).
- CAS store on disk with pointer and log primitives; collection logs and snapshot pointers are wired (`vendor/amduat/src/adapters/asl_store_fs`, `vendor/amduat/src/core/asl_log_store.c`, `vendor/amduat/src/core/asl_collection.c`).
- Edge graph projection maintained as derived state from collection logs (`src/amduatd_concepts.c`).
- Federation coordinator scaffolding and endpoints, but using stub transport by default (`federation/coord.c`, `federation/transport_stub.c`, `src/amduatd.c`).
- Core tooling for index/derivation/GC exists in vendor and repo tools (`vendor/amduat/src/tools/*`, `src/amduat_pel_gc.c`).
- Top 5 unknowns/blockers for the next step (grounded in code):
- Whether amduatd should switch to `amduat_asl_store_index_fs_ops` to enable `amduat_asl_log_scan` and tombstones; current store fs ops do not implement log scan (`vendor/amduat/src/adapters/asl_store_fs/asl_store_fs.c`, `vendor/amduat/src/near_core/asl/store.c`).
- The intended authoritative store for federation records: amduatds `/v1/fed/records` relies on `amduat_asl_log_scan`, which is unsupported by store_fs (`src/amduatd.c`, `vendor/amduat/src/near_core/asl/store.c`).
- How/when to persist derivation index records from daemon PEL runs (present in core but unused in `src/`).
- Whether the “space” scope should be exposed as a first-class selector in the API (only CLI flag currently applies globally).
- The intended registry flow for federation (coordinator expects registry refs, but no daemon config path for them besides code defaults in `src/amduatd.c`).
- Top 5 next commit candidates grounded in repo reality (no new architecture):
- Wire amduatd to an index-backed store (use `amduat_asl_store_index_fs_ops`) so federation record scanning and index state endpoints are meaningful.
- Add a daemon flag/config to load a federation registry ref and domain id into `amduat_fed_coord_cfg_t` (values are currently hardcoded in `src/amduatd.c`).
- Persist derivation index records for `/v1/pel/run` outputs using existing core `amduat_asl_derivation_index_fs_*` APIs.
- Expose index state or health endpoints that surface `amduat_asl_index_current_state` (available in core API, already used in `/v1/fed/records`).
- Formalize edge index rebuild tooling (e.g., CLI switch or maintenance endpoint) using existing `amduatd_concepts_refresh_edges` and pointer state.
- Method used:
- Searched for key terms with `rg -n "cas|content address|hash|log|append|pointer|head|space|derivation|projection|ASL|index|daemon|server|coordinator|sync|replication"`.
- Read key sources: `src/amduatd.c`, `src/amduatd_concepts.c`, `src/amduatd_space.c`, `vendor/amduat/src/adapters/asl_store_fs/asl_store_fs_layout.c`, `vendor/amduat/src/adapters/asl_pointer_fs/asl_pointer_fs.c`, `vendor/amduat/src/core/asl_log_store.c`, `vendor/amduat/src/adapters/asl_store_index_fs/asl_store_index_fs_layout.c`.
- Ran CLIs: `./build/amduatd --help`, `./vendor/amduat/build/amduat-asl --help`, `./vendor/amduat/build/amduat-asl index state --root .amduat-asl`, `./vendor/amduat/build/amduat-asl log inspect --root .amduat-asl`.
- Tests were not executed; enumerated `ctest` commands above.

View file

@ -0,0 +1,237 @@
# Amduat v2 App Developer Guide
This is the compact handoff guide for building an application against `amduatd` v2.
For machine-readable contracts, see `registry/amduatd-api-contract.v2.json`.
## 1) Runtime Model
- One daemon instance serves one ASL store root.
- Transport is local Unix socket HTTP.
- Auth is currently filesystem/socket permission based.
- All graph APIs are under `/v2/graph/*`.
Minimal local run:
```sh
./vendor/amduat/build/amduat-asl init --root .amduat-asl
./build/amduatd --root .amduat-asl --sock amduatd.sock --store-backend index
```
## 2) Request Conventions
- Use `X-Amduat-Space: <space_id>` for app isolation.
- Treat graph cursors as opaque tokens (`g1_*`), do not parse.
- Use `as_of` for snapshot-consistent reads.
- Use `include_tombstoned=true` only when you explicitly want retracted facts.
## 3) Core App Flows
### A. High-throughput ingest
Use `POST /v2/graph/batch` with:
- `idempotency_key` for deterministic retries
- `mode=continue_on_error` for partial-apply behavior
- per-item `metadata_ref` or `provenance` for trust/debug
Expect response:
- `ok` (overall)
- `applied` aggregate counts
- `results[]` with `{kind,index,status,code,error}`
### B. Multi-hop retrieval for agents
Primary endpoints:
- `GET /v2/graph/subgraph`
- `POST /v2/graph/retrieve`
- `POST /v2/graph/query` for declarative filtering
Use:
- `as_of` for stable reasoning snapshots
- `max_depth`, `max_fanout`, `limit_nodes`, `limit_edges`, `max_result_bytes`
- provenance filters where needed (`provenance_ref` / `provenance_min_confidence`)
### C. Incremental sync loop
Use `GET /v2/graph/changes`:
- Start with `since_cursor` (or bootstrap with `since_as_of`)
- Persist returned `next_cursor` after successful processing
- Handle `410` as replay-window expiry (full resync required)
- Optional long poll: `wait_ms`
### D. Fact correction
- Edge retraction: `POST /v2/graph/edges/tombstone`
- Node-version retraction: `POST /v2/graph/nodes/{name}/versions/tombstone`
Reads default to exclude tombstoned facts on retrieval surfaces unless `include_tombstoned=true`.
## 4) Endpoint Map (what to use when)
- Write node: `POST /v2/graph/nodes`
- Write version: `POST /v2/graph/nodes/{name}/versions`
- Write edge: `POST /v2/graph/edges`
- Batch write: `POST /v2/graph/batch`
- Point-ish read: `GET /v2/graph/nodes/{name}`
- Edge scan: `GET /v2/graph/edges`
- Neighbor scan: `GET /v2/graph/nodes/{name}/neighbors`
- Path lookup: `GET /v2/graph/paths`
- Subgraph: `GET /v2/graph/subgraph`
- Declarative query: `POST /v2/graph/query`
- Agent retrieval: `POST /v2/graph/retrieve`
- Changes feed: `GET /v2/graph/changes`
- Export: `POST /v2/graph/export`
- Import: `POST /v2/graph/import`
- Predicate policy: `GET/POST /v2/graph/schema/predicates`
- Health/readiness/metrics: `GET /v2/healthz`, `GET /v2/readyz`, `GET /v2/metrics`
- Graph runtime/capability: `GET /v2/graph/stats`, `GET /v2/graph/capabilities`
## 5) Provenance and Policy
Provenance object fields for writes:
- required: `source_uri`, `extractor`, `observed_at`, `ingested_at`, `trace_id`
- optional: `confidence`, `license`
Policy endpoint:
- `POST /v2/graph/schema/predicates`
Key modes:
- predicate validation: `strict|warn|off`
- provenance enforcement: `optional|required`
## 6) Error Handling and Retry Rules
- Retry-safe writes: only retries with same `idempotency_key` and identical payload.
- Validation failures: `400` or `422` (do not blind-retry).
- Not found for references/nodes: `404`.
- Cursor window expired: `410` on `/changes` (rebootstrap sync state).
- Result guard triggered: `422` (`max_result_bytes` or traversal/search limits).
- Internal errors: `500` (retry with backoff).
## 7) Performance and Safety Defaults
Recommended client defaults:
- Set explicit `limit` on scans.
- Always pass `max_result_bytes` on large retrieval requests.
- Keep `max_depth` conservative (start with 2-4).
- Enable `include_stats=true` in development to monitor scanned/returned counts and selected plan.
- Call `/v2/graph/capabilities` once at startup for feature/limit negotiation.
## 8) Minimal Startup Checklist (for external app)
1. Probe `GET /v2/readyz`.
2. Read `GET /v2/graph/capabilities`.
3. Configure schema/provenance policy (`POST /v2/graph/schema/predicates`) if your app owns policy.
4. Start ingest path (`/v2/graph/batch` idempotent).
5. Start change-consumer loop (`/v2/graph/changes`).
6. Serve retrieval via `/v2/graph/retrieve` and `/v2/graph/subgraph`.
7. Monitor `/v2/metrics` and `/v2/graph/stats`.
## 9) Useful Local Helpers
- `scripts/graph_client_helpers.sh` contains practical shell helpers for:
- idempotent batch ingest
- one-step changes sync
- subgraph retrieval
For integration tests/examples:
- `scripts/test_graph_queries.sh`
- `scripts/test_graph_contract.sh`
## 10) Copy/Paste Integration Skeleton
Set local defaults:
```sh
SOCK="amduatd.sock"
SPACE="app1"
BASE="http://localhost"
```
Startup probes:
```sh
curl --unix-socket "${SOCK}" -sS "${BASE}/v2/readyz"
curl --unix-socket "${SOCK}" -sS "${BASE}/v2/graph/capabilities"
```
Idempotent batch ingest:
```sh
curl --unix-socket "${SOCK}" -sS -X POST "${BASE}/v2/graph/batch" \
-H "Content-Type: application/json" \
-H "X-Amduat-Space: ${SPACE}" \
-d '{
"idempotency_key":"app1-batch-0001",
"mode":"continue_on_error",
"nodes":[{"name":"doc:1"}],
"edges":[
{"subject":"doc:1","predicate":"ms.within_domain","object":"topic:alpha",
"provenance":{"source_uri":"urn:app:seed","extractor":"app-loader","observed_at":1,"ingested_at":2,"trace_id":"trace-1"}}
]
}'
```
Incremental changes loop (bash skeleton):
```sh
cursor=""
while true; do
if [ -n "${cursor}" ]; then
path="/v2/graph/changes?since_cursor=${cursor}&limit=200&wait_ms=15000"
else
path="/v2/graph/changes?limit=200&wait_ms=15000"
fi
resp="$(curl --unix-socket "${SOCK}" -sS "${BASE}${path}" -H "X-Amduat-Space: ${SPACE}")" || break
# TODO: parse and process resp.events[] in your app.
next="$(printf '%s\n' "${resp}" | sed -n 's/.*"next_cursor":"\([^"]*\)".*/\1/p')"
[ -n "${next}" ] && cursor="${next}"
done
```
Agent retrieval call:
```sh
curl --unix-socket "${SOCK}" -sS -X POST "${BASE}/v2/graph/retrieve" \
-H "Content-Type: application/json" \
-H "X-Amduat-Space: ${SPACE}" \
-d '{
"roots":["doc:1"],
"goal_predicates":["ms.within_domain"],
"max_depth":2,
"max_fanout":1024,
"limit_nodes":200,
"limit_edges":400,
"max_result_bytes":1048576
}'
```
Subgraph snapshot read:
```sh
curl --unix-socket "${SOCK}" -sS \
"${BASE}/v2/graph/subgraph?roots[]=doc:1&max_depth=2&dir=outgoing&limit_nodes=200&limit_edges=400&include_stats=true&max_result_bytes=1048576" \
-H "X-Amduat-Space: ${SPACE}"
```
Edge correction (tombstone):
```sh
EDGE_REF="<edge_ref_to_retract>"
curl --unix-socket "${SOCK}" -sS -X POST "${BASE}/v2/graph/edges/tombstone" \
-H "Content-Type: application/json" \
-H "X-Amduat-Space: ${SPACE}" \
-d "{\"edge_ref\":\"${EDGE_REF}\"}"
```

584
federation/coord.c Normal file
View file

@ -0,0 +1,584 @@
#include "federation/coord.h"
#include "amduat/fed/ingest.h"
#include "amduat/asl/artifact_io.h"
#include <stdlib.h>
#include <string.h>
struct amduat_fed_coord {
amduat_fed_coord_cfg_t cfg;
amduat_fed_registry_value_t registry;
bool registry_loaded;
amduat_fed_view_t last_view;
bool has_view;
uint64_t last_tick_ms;
};
static bool amduat_fed_coord_has_registry_ref(amduat_reference_t ref) {
return ref.hash_id != 0 && ref.digest.data != NULL && ref.digest.len != 0;
}
static int amduat_fed_coord_ref_cmp(amduat_reference_t a, amduat_reference_t b) {
size_t min_len;
int cmp;
if (a.hash_id != b.hash_id) {
return (int)a.hash_id - (int)b.hash_id;
}
if (a.digest.len != b.digest.len) {
return a.digest.len < b.digest.len ? -1 : 1;
}
min_len = a.digest.len;
if (min_len == 0) {
return 0;
}
cmp = memcmp(a.digest.data, b.digest.data, min_len);
if (cmp != 0) {
return cmp;
}
return 0;
}
static int amduat_fed_coord_record_cmp(const void *lhs, const void *rhs) {
const amduat_fed_record_t *a = (const amduat_fed_record_t *)lhs;
const amduat_fed_record_t *b = (const amduat_fed_record_t *)rhs;
if (a->logseq != b->logseq) {
return a->logseq < b->logseq ? -1 : 1;
}
if (a->id.type != b->id.type) {
return (int)a->id.type - (int)b->id.type;
}
return amduat_fed_coord_ref_cmp(a->id.ref, b->id.ref);
}
static bool amduat_fed_coord_record_clone(const amduat_fed_record_t *src,
amduat_fed_record_t *out) {
if (src == NULL || out == NULL) {
return false;
}
*out = *src;
if (!amduat_reference_clone(src->id.ref, &out->id.ref)) {
return false;
}
return true;
}
static void amduat_fed_coord_record_free(amduat_fed_record_t *rec) {
if (rec == NULL) {
return;
}
amduat_reference_free(&rec->id.ref);
}
static void amduat_fed_coord_free_batch(amduat_fed_coord_t *coord,
amduat_fed_record_t *batch,
size_t batch_len) {
if (coord == NULL || batch == NULL) {
return;
}
if (coord->cfg.transport.free_records != NULL) {
coord->cfg.transport.free_records(coord->cfg.transport.ctx,
batch,
batch_len);
}
}
static amduat_fed_domain_state_t *amduat_fed_coord_find_state(
amduat_fed_registry_value_t *registry,
uint32_t domain_id) {
size_t i;
if (registry == NULL) {
return NULL;
}
for (i = 0; i < registry->len; ++i) {
if (registry->states[i].domain_id == domain_id) {
return &registry->states[i];
}
}
return NULL;
}
static bool amduat_fed_coord_records_push(amduat_fed_record_t **records,
size_t *len,
size_t *cap,
amduat_fed_record_t value) {
amduat_fed_record_t *next;
size_t next_cap;
if (records == NULL || len == NULL || cap == NULL) {
return false;
}
if (*len == *cap) {
next_cap = *cap != 0 ? *cap * 2u : 64u;
next = (amduat_fed_record_t *)realloc(*records,
next_cap * sizeof(*next));
if (next == NULL) {
return false;
}
*records = next;
*cap = next_cap;
}
(*records)[(*len)++] = value;
return true;
}
static bool amduat_fed_coord_denies_push(amduat_fed_policy_deny_t **denies,
size_t *len,
size_t *cap,
amduat_fed_policy_deny_t value) {
amduat_fed_policy_deny_t *next;
size_t next_cap;
if (denies == NULL || len == NULL || cap == NULL) {
return false;
}
if (*len == *cap) {
next_cap = *cap != 0 ? *cap * 2u : 32u;
next = (amduat_fed_policy_deny_t *)realloc(*denies,
next_cap * sizeof(*next));
if (next == NULL) {
return false;
}
*denies = next;
*cap = next_cap;
}
(*denies)[(*len)++] = value;
return true;
}
amduat_fed_coord_error_t amduat_fed_coord_open(
const amduat_fed_coord_cfg_t *cfg,
amduat_fed_coord_t **out_coord) {
amduat_fed_coord_t *coord = NULL;
if (out_coord == NULL) {
return AMDUAT_FED_COORD_ERR_INVALID;
}
*out_coord = NULL;
if (cfg == NULL || cfg->authoritative_store == NULL) {
return AMDUAT_FED_COORD_ERR_INVALID;
}
coord = (amduat_fed_coord_t *)calloc(1, sizeof(*coord));
if (coord == NULL) {
return AMDUAT_FED_COORD_ERR_OOM;
}
coord->cfg = *cfg;
if (amduat_fed_coord_has_registry_ref(cfg->registry_ref)) {
if (!amduat_reference_clone(cfg->registry_ref, &coord->cfg.registry_ref)) {
free(coord);
return AMDUAT_FED_COORD_ERR_OOM;
}
} else {
coord->cfg.registry_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
}
amduat_fed_registry_value_init(&coord->registry, NULL, 0);
coord->registry_loaded = false;
memset(&coord->last_view, 0, sizeof(coord->last_view));
coord->has_view = false;
coord->last_tick_ms = 0;
*out_coord = coord;
return AMDUAT_FED_COORD_OK;
}
amduat_fed_coord_error_t amduat_fed_coord_close(amduat_fed_coord_t *coord) {
if (coord == NULL) {
return AMDUAT_FED_COORD_ERR_INVALID;
}
if (coord->has_view) {
amduat_fed_view_free(&coord->last_view);
}
amduat_fed_registry_value_free(&coord->registry);
amduat_reference_free(&coord->cfg.registry_ref);
free(coord);
return AMDUAT_FED_COORD_OK;
}
amduat_fed_coord_error_t amduat_fed_coord_load_registry(
amduat_fed_coord_t *coord) {
amduat_fed_registry_store_t store;
amduat_fed_registry_value_t value;
amduat_asl_store_error_t store_err = AMDUAT_ASL_STORE_OK;
amduat_fed_registry_error_t err;
if (coord == NULL) {
return AMDUAT_FED_COORD_ERR_INVALID;
}
if (!amduat_fed_coord_has_registry_ref(coord->cfg.registry_ref)) {
return AMDUAT_FED_COORD_ERR_INVALID;
}
amduat_fed_registry_store_init(&store, coord->cfg.authoritative_store);
amduat_fed_registry_value_init(&value, NULL, 0);
err = amduat_fed_registry_store_get(&store,
coord->cfg.registry_ref,
&value,
&store_err);
if (err == AMDUAT_FED_REGISTRY_ERR_CODEC) {
amduat_fed_registry_value_free(&value);
return AMDUAT_FED_COORD_ERR_CODEC;
}
if (err != AMDUAT_FED_REGISTRY_OK || store_err != AMDUAT_ASL_STORE_OK) {
amduat_fed_registry_value_free(&value);
return AMDUAT_FED_COORD_ERR_STORE;
}
amduat_fed_registry_value_free(&coord->registry);
coord->registry = value;
coord->registry_loaded = true;
return AMDUAT_FED_COORD_OK;
}
amduat_fed_coord_error_t amduat_fed_coord_set_admitted(
amduat_fed_coord_t *coord,
uint32_t domain_id,
bool admitted) {
size_t i;
amduat_fed_domain_state_t state;
if (coord == NULL) {
return AMDUAT_FED_COORD_ERR_INVALID;
}
for (i = 0; i < coord->registry.len; ++i) {
if (coord->registry.states[i].domain_id == domain_id) {
coord->registry.states[i].admitted = admitted ? 1u : 0u;
if (admitted && coord->registry.states[i].policy_ok == 0u) {
coord->registry.states[i].policy_ok = 1u;
}
return AMDUAT_FED_COORD_OK;
}
}
memset(&state, 0, sizeof(state));
state.domain_id = domain_id;
state.admitted = admitted ? 1u : 0u;
state.policy_ok = admitted ? 1u : 0u;
state.policy_hash_id = 0;
state.policy_hash = amduat_octets(NULL, 0u);
if (!amduat_fed_registry_value_insert(&coord->registry, state)) {
return AMDUAT_FED_COORD_ERR_OOM;
}
return AMDUAT_FED_COORD_OK;
}
amduat_fed_coord_error_t amduat_fed_coord_tick(
amduat_fed_coord_t *coord,
uint64_t now_ms) {
amduat_fed_view_bounds_t *bounds = NULL;
size_t bounds_len = 0;
size_t bounds_cap = 0;
amduat_fed_record_t *records = NULL;
size_t records_len = 0;
size_t records_cap = 0;
amduat_fed_policy_deny_t *denies = NULL;
size_t denies_len = 0;
size_t denies_cap = 0;
size_t i;
amduat_fed_coord_error_t status = AMDUAT_FED_COORD_OK;
amduat_fed_registry_store_t reg_store;
amduat_fed_registry_error_t reg_err;
amduat_reference_t new_ref;
amduat_asl_store_error_t store_err = AMDUAT_ASL_STORE_OK;
bool registry_dirty = false;
if (coord == NULL) {
return AMDUAT_FED_COORD_ERR_INVALID;
}
coord->last_tick_ms = now_ms;
if (!coord->registry_loaded) {
if (amduat_fed_coord_has_registry_ref(coord->cfg.registry_ref)) {
status = amduat_fed_coord_load_registry(coord);
if (status != AMDUAT_FED_COORD_OK) {
return status;
}
} else {
coord->registry_loaded = true;
}
}
for (i = 0; i < coord->registry.len; ++i) {
const amduat_fed_domain_state_t *state = &coord->registry.states[i];
bool policy_ok = state->policy_ok != 0u ||
amduat_octets_is_empty(state->policy_hash);
if (state->admitted == 0u || !policy_ok) {
continue;
}
if (bounds_len == bounds_cap) {
size_t next_cap = bounds_cap != 0 ? bounds_cap * 2u : 8u;
amduat_fed_view_bounds_t *next =
(amduat_fed_view_bounds_t *)realloc(
bounds, next_cap * sizeof(*next));
if (next == NULL) {
status = AMDUAT_FED_COORD_ERR_OOM;
goto tick_cleanup;
}
bounds = next;
bounds_cap = next_cap;
}
bounds[bounds_len].domain_id = state->domain_id;
bounds[bounds_len].snapshot_id = state->snapshot_id;
bounds[bounds_len].log_prefix = state->log_prefix;
bounds_len++;
}
for (i = 0; i < bounds_len; ++i) {
amduat_fed_view_bounds_t *bound = &bounds[i];
amduat_fed_domain_state_t *state =
amduat_fed_coord_find_state(&coord->registry, bound->domain_id);
amduat_fed_record_t *batch = NULL;
size_t batch_len = 0;
size_t j;
uint64_t max_logseq = 0;
int transport_rc;
if (state == NULL) {
status = AMDUAT_FED_COORD_ERR_INVALID;
goto tick_cleanup;
}
if (coord->cfg.transport.get_records == NULL) {
status = AMDUAT_FED_COORD_ERR_INVALID;
goto tick_cleanup;
}
transport_rc = coord->cfg.transport.get_records(
coord->cfg.transport.ctx,
bound->domain_id,
bound->snapshot_id,
bound->log_prefix,
state->last_logseq + 1u,
&batch,
&batch_len);
if (transport_rc != 0) {
status = AMDUAT_FED_COORD_ERR_STORE;
goto tick_cleanup;
}
if (batch == NULL && batch_len != 0) {
status = AMDUAT_FED_COORD_ERR_INVALID;
goto tick_cleanup;
}
for (j = 0; j < batch_len; ++j) {
amduat_fed_record_t cloned;
amduat_fed_policy_deny_t deny;
amduat_reference_t deny_ref;
bool allowed = true;
bool ok = amduat_fed_coord_record_clone(&batch[j], &cloned);
if (!ok) {
status = AMDUAT_FED_COORD_ERR_OOM;
goto tick_cleanup;
}
if (cloned.logseq > bound->log_prefix) {
amduat_fed_coord_record_free(&cloned);
continue;
}
if (!amduat_fed_record_validate(&cloned)) {
amduat_fed_coord_record_free(&cloned);
status = AMDUAT_FED_COORD_ERR_INVALID;
amduat_fed_coord_free_batch(coord, batch, batch_len);
goto tick_cleanup;
}
if (coord->cfg.policy_hooks.record_allowed != NULL) {
memset(&deny, 0, sizeof(deny));
allowed = coord->cfg.policy_hooks.record_allowed(
coord->cfg.policy_hooks.ctx, &cloned, &deny);
if (!allowed) {
if (deny.id.ref.digest.data == NULL && deny.id.ref.hash_id == 0u) {
deny.id = cloned.id;
}
deny_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
if (!amduat_reference_clone(deny.id.ref, &deny_ref)) {
amduat_fed_coord_record_free(&cloned);
status = AMDUAT_FED_COORD_ERR_OOM;
amduat_fed_coord_free_batch(coord, batch, batch_len);
goto tick_cleanup;
}
deny.id.ref = deny_ref;
if (!amduat_fed_coord_denies_push(&denies,
&denies_len,
&denies_cap,
deny)) {
amduat_reference_free(&deny.id.ref);
amduat_fed_coord_record_free(&cloned);
status = AMDUAT_FED_COORD_ERR_OOM;
amduat_fed_coord_free_batch(coord, batch, batch_len);
goto tick_cleanup;
}
}
}
if (!amduat_fed_coord_records_push(&records,
&records_len,
&records_cap,
cloned)) {
amduat_fed_coord_record_free(&cloned);
status = AMDUAT_FED_COORD_ERR_OOM;
amduat_fed_coord_free_batch(coord, batch, batch_len);
goto tick_cleanup;
}
if (cloned.logseq > max_logseq) {
max_logseq = cloned.logseq;
}
}
amduat_fed_coord_free_batch(coord, batch, batch_len);
if (max_logseq > state->last_logseq) {
state->last_logseq = max_logseq;
registry_dirty = true;
}
}
if (records_len != 0) {
size_t err_index = 0;
size_t conflict_index = 0;
amduat_fed_ingest_error_t ingest_rc;
qsort(records, records_len, sizeof(*records), amduat_fed_coord_record_cmp);
ingest_rc = amduat_fed_ingest_validate(records,
records_len,
&err_index,
&conflict_index);
if (ingest_rc != AMDUAT_FED_INGEST_OK) {
status = AMDUAT_FED_COORD_ERR_INVALID;
goto tick_cleanup;
}
}
if (coord->has_view) {
amduat_fed_view_free(&coord->last_view);
coord->has_view = false;
}
if (bounds_len != 0) {
amduat_fed_view_error_t view_rc;
view_rc = amduat_fed_view_build(records,
records_len,
coord->cfg.local_domain_id,
bounds,
bounds_len,
denies,
denies_len,
&coord->last_view);
if (view_rc != AMDUAT_FED_VIEW_OK) {
status = AMDUAT_FED_COORD_ERR_INVALID;
goto tick_cleanup;
}
coord->has_view = true;
}
if (registry_dirty) {
amduat_fed_registry_store_init(&reg_store, coord->cfg.authoritative_store);
reg_err = amduat_fed_registry_store_put(&reg_store,
&coord->registry,
&new_ref,
&store_err);
if (reg_err != AMDUAT_FED_REGISTRY_OK ||
store_err != AMDUAT_ASL_STORE_OK) {
status = AMDUAT_FED_COORD_ERR_STORE;
goto tick_cleanup;
}
amduat_reference_free(&coord->cfg.registry_ref);
coord->cfg.registry_ref = new_ref;
}
tick_cleanup:
if (bounds != NULL) {
free(bounds);
}
if (records != NULL) {
for (i = 0; i < records_len; ++i) {
amduat_fed_coord_record_free(&records[i]);
}
free(records);
}
if (denies != NULL) {
for (i = 0; i < denies_len; ++i) {
amduat_reference_free(&denies[i].id.ref);
}
free(denies);
}
return status;
}
amduat_fed_coord_error_t amduat_fed_coord_resolve(
amduat_fed_coord_t *coord,
amduat_reference_t ref,
amduat_artifact_t *out_artifact) {
amduat_fed_resolve_error_t rc;
if (coord == NULL || out_artifact == NULL) {
return AMDUAT_FED_COORD_ERR_INVALID;
}
if (!coord->has_view) {
return AMDUAT_FED_COORD_ERR_INVALID;
}
rc = amduat_fed_resolve(&coord->last_view,
coord->cfg.authoritative_store,
ref,
out_artifact);
if (rc == AMDUAT_FED_RESOLVE_OK) {
return AMDUAT_FED_COORD_OK;
}
if (rc == AMDUAT_FED_RESOLVE_FOUND_REMOTE_NO_BYTES &&
coord->cfg.cache_store != NULL &&
coord->cfg.transport.get_artifact != NULL) {
amduat_octets_t bytes = amduat_octets(NULL, 0u);
amduat_artifact_t artifact;
amduat_asl_store_error_t store_err;
int fetch_rc = coord->cfg.transport.get_artifact(
coord->cfg.transport.ctx, ref, &bytes);
if (fetch_rc != 0) {
amduat_octets_free(&bytes);
return AMDUAT_FED_COORD_ERR_STORE;
}
if (!amduat_asl_artifact_from_bytes(bytes,
AMDUAT_ASL_IO_RAW,
false,
amduat_type_tag(0u),
&artifact)) {
amduat_octets_free(&bytes);
return AMDUAT_FED_COORD_ERR_INVALID;
}
store_err = amduat_asl_store_put(coord->cfg.cache_store,
artifact,
&ref);
amduat_asl_artifact_free(&artifact);
amduat_octets_free(&bytes);
if (store_err != AMDUAT_ASL_STORE_OK) {
return AMDUAT_FED_COORD_ERR_STORE;
}
rc = amduat_fed_resolve(&coord->last_view,
coord->cfg.authoritative_store,
ref,
out_artifact);
if (rc == AMDUAT_FED_RESOLVE_OK) {
return AMDUAT_FED_COORD_OK;
}
}
if (rc == AMDUAT_FED_RESOLVE_POLICY_DENIED) {
return AMDUAT_FED_COORD_ERR_INVALID;
}
if (rc == AMDUAT_FED_RESOLVE_NOT_FOUND) {
return AMDUAT_FED_COORD_ERR_INVALID;
}
return AMDUAT_FED_COORD_ERR_STORE;
}
void amduat_fed_coord_get_status(const amduat_fed_coord_t *coord,
amduat_fed_coord_status_t *out_status) {
if (out_status == NULL) {
return;
}
memset(out_status, 0, sizeof(*out_status));
if (coord == NULL) {
return;
}
out_status->domain_id = coord->cfg.local_domain_id;
if (!amduat_reference_clone(coord->cfg.registry_ref,
&out_status->registry_ref)) {
out_status->registry_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
}
out_status->last_tick_ms = coord->last_tick_ms;
}

95
federation/coord.h Normal file
View file

@ -0,0 +1,95 @@
#ifndef AMDUAT_FED_COORD_H
#define AMDUAT_FED_COORD_H
#include "amduat/asl/core.h"
#include "amduat/asl/store.h"
#include "amduat/fed/registry.h"
#include "amduat/fed/view.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct amduat_fed_coord amduat_fed_coord_t;
typedef enum {
AMDUAT_FED_COORD_OK = 0,
AMDUAT_FED_COORD_ERR_INVALID = 1,
AMDUAT_FED_COORD_ERR_OOM = 2,
AMDUAT_FED_COORD_ERR_STORE = 3,
AMDUAT_FED_COORD_ERR_CODEC = 4,
AMDUAT_FED_COORD_ERR_NOT_IMPLEMENTED = 5
} amduat_fed_coord_error_t;
typedef struct {
void *ctx;
int (*get_records)(void *ctx,
uint32_t domain_id,
uint64_t snapshot_id,
uint64_t log_prefix,
uint64_t from_logseq,
amduat_fed_record_t **out_records,
size_t *out_len);
void (*free_records)(void *ctx, amduat_fed_record_t *records, size_t len);
int (*get_artifact)(void *ctx,
amduat_reference_t ref,
amduat_octets_t *out_bytes);
} amduat_fed_transport_t;
typedef struct {
void *ctx;
bool (*record_allowed)(void *ctx,
const amduat_fed_record_t *record,
amduat_fed_policy_deny_t *out_deny);
} amduat_fed_policy_hooks_t;
typedef struct {
uint32_t local_domain_id;
amduat_asl_store_t *authoritative_store;
amduat_asl_store_t *cache_store;
amduat_reference_t registry_ref;
amduat_fed_transport_t transport;
amduat_fed_policy_hooks_t policy_hooks;
} amduat_fed_coord_cfg_t;
typedef struct {
uint32_t domain_id;
amduat_reference_t registry_ref;
uint64_t last_tick_ms;
} amduat_fed_coord_status_t;
amduat_fed_coord_error_t amduat_fed_coord_open(
const amduat_fed_coord_cfg_t *cfg,
amduat_fed_coord_t **out_coord);
amduat_fed_coord_error_t amduat_fed_coord_close(amduat_fed_coord_t *coord);
amduat_fed_coord_error_t amduat_fed_coord_load_registry(
amduat_fed_coord_t *coord);
amduat_fed_coord_error_t amduat_fed_coord_set_admitted(
amduat_fed_coord_t *coord,
uint32_t domain_id,
bool admitted);
amduat_fed_coord_error_t amduat_fed_coord_tick(
amduat_fed_coord_t *coord,
uint64_t now_ms);
amduat_fed_coord_error_t amduat_fed_coord_resolve(
amduat_fed_coord_t *coord,
amduat_reference_t ref,
amduat_artifact_t *out_artifact);
void amduat_fed_coord_get_status(const amduat_fed_coord_t *coord,
amduat_fed_coord_status_t *out_status);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUAT_FED_COORD_H */

View file

@ -0,0 +1,72 @@
#include "federation/transport_stub.h"
#include <stdlib.h>
#include <string.h>
static int amduat_fed_transport_stub_get_records(void *ctx,
uint32_t domain_id,
uint64_t snapshot_id,
uint64_t log_prefix,
uint64_t from_logseq,
amduat_fed_record_t **out_records,
size_t *out_len) {
(void)ctx;
(void)domain_id;
(void)snapshot_id;
(void)log_prefix;
(void)from_logseq;
if (out_records != NULL) {
*out_records = NULL;
}
if (out_len != NULL) {
*out_len = 0;
}
return 0;
}
static void amduat_fed_transport_stub_free_records(void *ctx,
amduat_fed_record_t *records,
size_t len) {
(void)ctx;
(void)len;
free(records);
}
static int amduat_fed_transport_stub_get_artifact(void *ctx,
amduat_reference_t ref,
amduat_octets_t *out_bytes) {
amduat_fed_transport_stub_t *stub = (amduat_fed_transport_stub_t *)ctx;
(void)ref;
if (out_bytes == NULL || stub == NULL) {
return -1;
}
if (!amduat_octets_clone(stub->artifact_bytes, out_bytes)) {
return -1;
}
return 0;
}
void amduat_fed_transport_stub_init(amduat_fed_transport_stub_t *stub) {
if (stub == NULL) {
return;
}
stub->artifact_bytes = amduat_octets(NULL, 0u);
}
amduat_fed_transport_t amduat_fed_transport_stub_ops(
amduat_fed_transport_stub_t *stub) {
amduat_fed_transport_t ops;
memset(&ops, 0, sizeof(ops));
ops.ctx = stub;
ops.get_records = amduat_fed_transport_stub_get_records;
ops.free_records = amduat_fed_transport_stub_free_records;
ops.get_artifact = amduat_fed_transport_stub_get_artifact;
return ops;
}
void amduat_fed_transport_stub_free(amduat_fed_transport_stub_t *stub) {
if (stub == NULL) {
return;
}
amduat_octets_free(&stub->artifact_bytes);
}

View file

@ -0,0 +1,25 @@
#ifndef AMDUAT_FED_TRANSPORT_STUB_H
#define AMDUAT_FED_TRANSPORT_STUB_H
#include "federation/coord.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
amduat_octets_t artifact_bytes;
} amduat_fed_transport_stub_t;
void amduat_fed_transport_stub_init(amduat_fed_transport_stub_t *stub);
amduat_fed_transport_t amduat_fed_transport_stub_ops(
amduat_fed_transport_stub_t *stub);
void amduat_fed_transport_stub_free(amduat_fed_transport_stub_t *stub);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUAT_FED_TRANSPORT_STUB_H */

1246
federation/transport_unix.c Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,63 @@
#ifndef AMDUAT_FED_TRANSPORT_UNIX_H
#define AMDUAT_FED_TRANSPORT_UNIX_H
#include "federation/coord.h"
#include "amduat/asl/asl_pointer_fs.h"
#include "amduat/fed/replay.h"
#ifdef __cplusplus
extern "C" {
#endif
enum { AMDUAT_FED_TRANSPORT_UNIX_PATH_MAX = 1024 };
typedef struct {
char socket_path[AMDUAT_FED_TRANSPORT_UNIX_PATH_MAX];
char space_id[AMDUAT_ASL_POINTER_NAME_MAX + 1u];
bool has_space;
} amduat_fed_transport_unix_t;
bool amduat_fed_transport_unix_init(amduat_fed_transport_unix_t *transport,
const char *socket_path);
bool amduat_fed_transport_unix_set_space(amduat_fed_transport_unix_t *transport,
const char *space_id);
amduat_fed_transport_t amduat_fed_transport_unix_ops(
amduat_fed_transport_unix_t *transport);
/* Returns true on successful HTTP exchange. Caller frees records via
* amduat_fed_transport_unix_ops(...).free_records and frees out_body with free.
*/
bool amduat_fed_transport_unix_get_records_with_limit(
amduat_fed_transport_unix_t *transport,
uint32_t domain_id,
uint64_t from_logseq,
uint64_t limit,
int *out_status,
amduat_fed_record_t **out_records,
size_t *out_len,
char **out_body);
/* Returns true on successful HTTP exchange. Caller frees out_body with free. */
bool amduat_fed_transport_unix_get_artifact_with_status(
amduat_fed_transport_unix_t *transport,
amduat_reference_t ref,
int *out_status,
amduat_octets_t *out_bytes,
char **out_body);
/* Returns true on successful HTTP exchange. Caller frees out_body with free. */
bool amduat_fed_transport_unix_post_ingest(
amduat_fed_transport_unix_t *transport,
amduat_fed_record_type_t record_type,
amduat_reference_t ref,
amduat_octets_t bytes,
int *out_status,
char **out_body);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUAT_FED_TRANSPORT_UNIX_H */

View file

@ -17,4 +17,9 @@ acts as the version identifier.
- `api-contract.schema.md` — JSONL manifest schema for API contracts. - `api-contract.schema.md` — JSONL manifest schema for API contracts.
- `api-contract.jsonl` — manifest of published contracts. - `api-contract.jsonl` — manifest of published contracts.
- `amduatd-api-contract.v1.json` — contract bytes (v1). - `amduatd-api-contract.v1.json` — contract bytes (v1).
- `amduatd-api-contract.v2.json` — draft contract bytes (v2, PEL-only writes).
Receipt note:
- `/v1/pel/run` accepts optional receipt v1.1 fields (executor fingerprint, run id,
limits, logs, determinism, rng seed, signature) and emits `receipt_ref` when
provided.

View file

@ -1 +1,442 @@
{"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":"GET","path":"/v1/artifacts/{ref}?format=info"},{"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"}}},"artifact_info_response":{"type":"object","required":["len","has_type_tag","type_tag"],"properties":{"len":{"type":"integer"},"has_type_tag":{"type":"boolean"},"type_tag":{"type":"string"}}}}} {
"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": "GET", "path": "/v1/space/doctor"},
{"method": "GET", "path": "/v1/space/roots"},
{"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"},
{"method": "GET", "path": "/v1/cap/resolve"},
{"method": "GET", "path": "/v1/fed/records"},
{"method": "GET", "path": "/v1/fed/cursor"},
{"method": "POST", "path": "/v1/fed/cursor"},
{"method": "GET", "path": "/v1/fed/pull/plan"},
{"method": "GET", "path": "/v1/fed/push/plan"},
{"method": "POST", "path": "/v1/fed/pull"},
{"method": "GET", "path": "/v1/fed/artifacts/{ref}"},
{"method": "GET", "path": "/v1/fed/status"},
{"method": "POST", "path": "/v1/fed/ingest"},
{"method": "POST", "path": "/v1/fed/pull/until"},
{"method": "POST", "path": "/v1/fed/push"},
{"method": "POST", "path": "/v1/fed/push/until"},
{"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/relations"},
{"method": "GET", "path": "/v1/artifacts/{ref}"},
{"method": "HEAD", "path": "/v1/artifacts/{ref}"},
{"method": "GET", "path": "/v1/artifacts/{ref}?format=info"},
{"method": "POST", "path": "/v1/pel/run"},
{"method": "POST", "path": "/v1/pel/programs"},
{"method": "POST", "path": "/v1/context_frames"}
],
"schemas": {
"capability_mint_request": {
"type": "object",
"required": ["kind", "target", "expiry_seconds"],
"properties": {
"kind": {"type": "string"},
"target": {"type": "object"},
"expiry_seconds": {"type": "integer"}
}
},
"capability_mint_response": {
"type": "object",
"required": ["token"],
"properties": {
"token": {"type": "string"}
}
},
"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'"},
"receipt": {
"type": "object",
"required": ["input_manifest_ref", "environment_ref", "evaluator_id", "executor_ref", "started_at", "completed_at"],
"properties": {
"input_manifest_ref": {"type": "string", "description": "hex ref or concept name"},
"environment_ref": {"type": "string", "description": "hex ref or concept name"},
"evaluator_id": {"type": "string", "description": "opaque evaluator bytes (utf-8)"},
"executor_ref": {"type": "string", "description": "hex ref or concept name"},
"sbom_ref": {"type": "string", "description": "hex ref or concept name"},
"parity_digest_hex": {"type": "string", "description": "hex bytes"},
"executor_fingerprint_ref": {"type": "string", "description": "hex ref or concept name"},
"run_id_hex": {"type": "string", "description": "hex bytes"},
"limits": {
"type": "object",
"required": ["cpu_ms", "wall_ms", "max_rss_kib", "io_reads", "io_writes"],
"properties": {
"cpu_ms": {"type": "integer"},
"wall_ms": {"type": "integer"},
"max_rss_kib": {"type": "integer"},
"io_reads": {"type": "integer"},
"io_writes": {"type": "integer"}
}
},
"logs": {
"type": "array",
"items": {
"type": "object",
"required": ["kind", "log_ref", "sha256_hex"],
"properties": {
"kind": {"type": "integer"},
"log_ref": {"type": "string", "description": "hex ref or concept name"},
"sha256_hex": {"type": "string", "description": "hex bytes"}
}
}
},
"determinism_level": {"type": "integer", "description": "0-255"},
"rng_seed_hex": {"type": "string", "description": "hex bytes"},
"signature_hex": {"type": "string", "description": "hex bytes"},
"started_at": {"type": "integer"},
"completed_at": {"type": "integer"}
}
}
}
},
"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"},
"receipt_ref": {"type": "string", "description": "hex ref"},
"output_refs": {"type": "array", "items": {"type": "string", "description": "hex ref"}},
"status": {"type": "string"}
}
},
"fed_records_response": {
"type": "object",
"required": ["domain_id", "snapshot_id", "log_prefix", "next_logseq", "records"],
"properties": {
"domain_id": {"type": "integer"},
"snapshot_id": {"type": "integer"},
"log_prefix": {"type": "integer"},
"next_logseq": {"type": "integer", "description": "Paging cursor; last emitted logseq + 1, or from_logseq if no records emitted."},
"records": {
"type": "array",
"items": {
"type": "object",
"required": ["domain_id", "type", "ref", "logseq", "snapshot_id", "log_prefix"],
"properties": {
"domain_id": {"type": "integer"},
"type": {"type": "integer"},
"ref": {"type": "string"},
"logseq": {"type": "integer"},
"snapshot_id": {"type": "integer"},
"log_prefix": {"type": "integer"},
"visibility": {"type": "integer"},
"has_source": {"type": "boolean"},
"source_domain": {"type": "integer"},
"notes": {"type": "string", "description": "Type mapping: ARTIFACT_PUBLISH -> ARTIFACT, PER when type_tag=FER1_RECEIPT_1, TGK_EDGE when type_tag=TGK1_EDGE_V1; ARTIFACT_UNPUBLISH -> TOMBSTONE."}
}
}
}
}
},
"fed_status_response": {
"type": "object",
"required": ["status", "domain_id", "registry_ref", "last_tick_ms"],
"properties": {
"status": {"type": "string"},
"domain_id": {"type": "integer"},
"registry_ref": {"type": ["string", "null"]},
"last_tick_ms": {"type": "integer"}
}
},
"context_frame_request": {
"type": "object",
"required": ["bindings"],
"properties": {
"bindings": {
"type": "array",
"items": {
"type": "object",
"required": ["key"],
"properties": {
"key": {"type": "string", "description": "concept name or hex ref"},
"value": {"type": "string", "description": "hex ref or concept name"},
"value_ref": {"type": "string", "description": "hex ref or concept name"},
"value_scalar": {
"type": "object",
"properties": {
"int": {"type": "integer"},
"enum": {"type": "string", "description": "concept name or hex ref"}
}
}
}
}
}
}
},
"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"}
}
},
"artifact_info_response": {
"type": "object",
"required": ["len", "has_type_tag", "type_tag"],
"properties": {
"len": {"type": "integer"},
"has_type_tag": {"type": "boolean"},
"type_tag": {"type": "string"}
}
},
"space_manifest_mount": {
"type": "object",
"required": ["name", "peer_key", "space_id", "mode"],
"properties": {
"name": {"type": "string"},
"peer_key": {"type": "string"},
"space_id": {"type": "string"},
"mode": {"type": "string"},
"pinned_root_ref": {"type": "string"}
}
},
"space_manifest": {
"type": "object",
"required": ["version", "mounts"],
"properties": {
"version": {"type": "integer"},
"mounts": {"type": "array", "items": {"$ref": "#/schemas/space_manifest_mount"}}
}
},
"space_manifest_response": {
"type": "object",
"required": ["effective_space", "manifest_ref", "manifest"],
"properties": {
"effective_space": {"type": "object"},
"manifest_ref": {"type": "string"},
"manifest": {"$ref": "#/schemas/space_manifest"}
}
},
"space_manifest_put_response": {
"type": "object",
"required": ["effective_space", "manifest_ref", "updated", "manifest"],
"properties": {
"effective_space": {"type": "object"},
"manifest_ref": {"type": "string"},
"updated": {"type": "boolean"},
"previous_ref": {"type": "string"},
"manifest": {"$ref": "#/schemas/space_manifest"}
}
},
"space_mounts_pull_cursor": {
"type": "object",
"required": ["present"],
"properties": {
"present": {"type": "boolean"},
"last_logseq": {"type": "integer"},
"ref": {"type": "string"}
}
},
"space_mounts_local_tracking": {
"type": "object",
"required": ["cursor_namespace", "cursor_scope", "remote_space_id", "pull_cursor"],
"properties": {
"cursor_namespace": {"type": "string"},
"cursor_scope": {"type": "string"},
"remote_space_id": {"type": "string"},
"pull_cursor": {"$ref": "#/schemas/space_mounts_pull_cursor"}
}
},
"space_mounts_resolved_mount": {
"type": "object",
"required": ["name", "peer_key", "space_id", "mode", "local_tracking"],
"properties": {
"name": {"type": "string"},
"peer_key": {"type": "string"},
"space_id": {"type": "string"},
"mode": {"type": "string"},
"pinned_root_ref": {"type": "string"},
"local_tracking": {"$ref": "#/schemas/space_mounts_local_tracking"}
}
},
"space_mounts_resolve_response": {
"type": "object",
"required": ["effective_space", "manifest_ref", "mounts"],
"properties": {
"effective_space": {"type": "object"},
"manifest_ref": {"type": "string"},
"mounts": {"type": "array", "items": {"$ref": "#/schemas/space_mounts_resolved_mount"}}
}
},
"space_mounts_sync_error": {
"type": "object",
"required": ["code", "message"],
"properties": {
"code": {"type": "string"},
"message": {"type": "string"}
}
},
"space_mounts_sync_cursor": {
"type": "object",
"properties": {
"last_logseq": {"type": "integer"},
"ref": {"type": "string"}
}
},
"space_mounts_sync_result": {
"type": "object",
"required": ["name", "peer_key", "remote_space_id", "status"],
"properties": {
"name": {"type": "string"},
"peer_key": {"type": "string"},
"remote_space_id": {"type": "string"},
"status": {"type": "string"},
"caught_up": {"type": "boolean"},
"rounds_executed": {"type": "integer"},
"applied": {
"type": "object",
"required": ["records", "artifacts"],
"properties": {
"records": {"type": "integer"},
"artifacts": {"type": "integer"}
}
},
"cursor": {"$ref": "#/schemas/space_mounts_sync_cursor"},
"error": {"$ref": "#/schemas/space_mounts_sync_error"}
}
},
"space_mounts_sync_response": {
"type": "object",
"required": ["effective_space", "manifest_ref", "limit", "max_rounds", "max_mounts", "mounts_total", "mounts_synced", "ok", "results"],
"properties": {
"effective_space": {"type": "object"},
"manifest_ref": {"type": "string"},
"limit": {"type": "integer"},
"max_rounds": {"type": "integer"},
"max_mounts": {"type": "integer"},
"mounts_total": {"type": "integer"},
"mounts_synced": {"type": "integer"},
"ok": {"type": "boolean"},
"results": {"type": "array", "items": {"$ref": "#/schemas/space_mounts_sync_result"}}
}
},
"space_sync_status_cursor": {
"type": "object",
"required": ["present"],
"properties": {
"present": {"type": "boolean"},
"last_logseq": {"type": "integer"},
"ref": {"type": "string"}
}
},
"space_sync_status_remote": {
"type": "object",
"required": ["remote_space_id", "pull_cursor", "push_cursor"],
"properties": {
"remote_space_id": {"type": ["string", "null"]},
"pull_cursor": {"$ref": "#/schemas/space_sync_status_cursor"},
"push_cursor": {"$ref": "#/schemas/space_sync_status_cursor"}
}
},
"space_sync_status_peer": {
"type": "object",
"required": ["peer_key", "remotes"],
"properties": {
"peer_key": {"type": "string"},
"remotes": {"type": "array", "items": {"$ref": "#/schemas/space_sync_status_remote"}}
}
},
"space_sync_status_response": {
"type": "object",
"required": ["effective_space", "store_backend", "federation", "peers"],
"properties": {
"effective_space": {"type": "object"},
"store_backend": {"type": "string"},
"federation": {
"type": "object",
"required": ["enabled", "transport"],
"properties": {
"enabled": {"type": "boolean"},
"transport": {"type": "string"}
}
},
"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"}
}
}
}
}

View file

@ -0,0 +1,820 @@
{
"contract": "AMDUATD/API/2",
"base_path": "/v2",
"notes": "Draft v2: PEL-only write surface. Direct artifact write endpoint removed.",
"endpoints": [
{"method": "GET", "path": "/v2/meta"},
{"method": "HEAD", "path": "/v2/meta"},
{"method": "GET", "path": "/v2/contract"},
{"method": "GET", "path": "/v2/healthz"},
{"method": "GET", "path": "/v2/readyz"},
{"method": "GET", "path": "/v2/metrics"},
{"method": "GET", "path": "/v2/artifacts/{ref}"},
{"method": "HEAD", "path": "/v2/artifacts/{ref}"},
{"method": "GET", "path": "/v2/artifacts/{ref}?format=info"},
{"method": "POST", "path": "/v2/pel/execute"},
{"method": "POST", "path": "/v2/ops/put"},
{"method": "POST", "path": "/v2/ops/concat"},
{"method": "POST", "path": "/v2/ops/slice"},
{"method": "GET", "path": "/v2/jobs/{id}"},
{"method": "GET", "path": "/v2/get/{ref}"},
{"method": "POST", "path": "/v2/graph/nodes"},
{"method": "POST", "path": "/v2/graph/nodes/{name}/versions"},
{"method": "POST", "path": "/v2/graph/nodes/{name}/versions/tombstone"},
{"method": "GET", "path": "/v2/graph/nodes/{name}/versions"},
{"method": "GET", "path": "/v2/graph/nodes/{name}/neighbors"},
{"method": "GET", "path": "/v2/graph/search"},
{"method": "GET", "path": "/v2/graph/paths"},
{"method": "GET", "path": "/v2/graph/subgraph"},
{"method": "POST", "path": "/v2/graph/edges"},
{"method": "POST", "path": "/v2/graph/edges/tombstone"},
{"method": "POST", "path": "/v2/graph/batch"},
{"method": "POST", "path": "/v2/graph/query"},
{"method": "POST", "path": "/v2/graph/retrieve"},
{"method": "POST", "path": "/v2/graph/export"},
{"method": "POST", "path": "/v2/graph/import"},
{"method": "GET", "path": "/v2/graph/schema/predicates"},
{"method": "POST", "path": "/v2/graph/schema/predicates"},
{"method": "GET", "path": "/v2/graph/stats"},
{"method": "GET", "path": "/v2/graph/capabilities"},
{"method": "GET", "path": "/v2/graph/changes"},
{"method": "GET", "path": "/v2/graph/edges"},
{"method": "GET", "path": "/v2/graph/nodes/{name}"},
{"method": "GET", "path": "/v2/graph/history/{name}"}
],
"schemas": {
"job_enqueue_response": {
"type": "object",
"required": ["job_id", "status"],
"properties": {
"job_id": {"type": "integer"},
"status": {"type": "string"}
}
},
"job_status_response": {
"type": "object",
"required": ["job_id", "kind", "status", "created_at_ms"],
"properties": {
"job_id": {"type": "integer"},
"kind": {"type": "string"},
"status": {"type": "string"},
"created_at_ms": {"type": "integer"},
"started_at_ms": {"type": ["integer", "null"]},
"completed_at_ms": {"type": ["integer", "null"]},
"result_ref": {"type": ["string", "null"]},
"error": {"type": ["string", "null"]}
}
},
"healthz_response": {
"type": "object",
"required": ["ok", "status", "time_ms"],
"properties": {
"ok": {"type": "boolean"},
"status": {"type": "string"},
"time_ms": {"type": "integer"}
}
},
"readyz_response": {
"type": "object",
"required": ["ok", "status", "components"],
"properties": {
"ok": {"type": "boolean"},
"status": {"type": "string"},
"components": {
"type": "object",
"required": ["graph_index", "federation"],
"properties": {
"graph_index": {"type": "boolean"},
"federation": {"type": "boolean"}
}
}
}
},
"put_request": {
"type": "object",
"required": ["body_hex"],
"properties": {
"body_hex": {"type": "string"},
"type_tag": {"type": "string"}
}
},
"concat_request": {
"type": "object",
"required": ["left_ref", "right_ref"],
"properties": {
"left_ref": {"type": "string"},
"right_ref": {"type": "string"}
}
},
"slice_request": {
"type": "object",
"required": ["ref", "offset", "length"],
"properties": {
"ref": {"type": "string"},
"offset": {"type": "integer"},
"length": {"type": "integer"}
}
},
"graph_node_create_request": {
"type": "object",
"required": ["name"],
"properties": {
"name": {"type": "string"},
"ref": {"type": "string", "description": "optional initial published ref"}
}
},
"graph_node_create_response": {
"type": "object",
"required": ["name", "concept_ref"],
"properties": {
"name": {"type": "string"},
"concept_ref": {"type": "string"}
}
},
"graph_provenance": {
"type": "object",
"required": ["source_uri", "extractor", "observed_at", "ingested_at", "trace_id"],
"properties": {
"source_uri": {"type": "string"},
"extractor": {"type": "string"},
"confidence": {"type": ["string", "number", "integer"]},
"observed_at": {"type": "integer"},
"ingested_at": {"type": "integer"},
"license": {"type": "string"},
"trace_id": {"type": "string"}
}
},
"graph_edge_create_request": {
"type": "object",
"required": ["subject", "predicate", "object"],
"properties": {
"subject": {"type": "string", "description": "concept name or hex ref"},
"predicate": {"type": "string", "description": "relation alias/name or hex ref"},
"object": {"type": "string", "description": "concept name or hex ref"},
"metadata_ref": {"type": "string", "description": "optional artifact ref"},
"provenance": {"$ref": "#/schemas/graph_provenance"}
}
},
"graph_edge_create_response": {
"type": "object",
"required": ["subject_ref", "predicate_ref", "object_ref", "edge_ref"],
"properties": {
"subject_ref": {"type": "string"},
"predicate_ref": {"type": "string"},
"object_ref": {"type": "string"},
"edge_ref": {"type": "string"},
"metadata_ref": {"type": "string"}
}
},
"graph_edge_tombstone_request": {
"type": "object",
"required": ["edge_ref"],
"properties": {
"edge_ref": {"type": "string"},
"metadata_ref": {"type": "string"},
"provenance": {"$ref": "#/schemas/graph_provenance"}
}
},
"graph_edge_tombstone_response": {
"type": "object",
"required": ["ok", "target_edge_ref", "tombstone_edge_ref"],
"properties": {
"ok": {"type": "boolean"},
"target_edge_ref": {"type": "string"},
"tombstone_edge_ref": {"type": "string"},
"metadata_ref": {"type": "string"}
}
},
"graph_node_version_tombstone_request": {
"type": "object",
"required": ["ref"],
"properties": {
"ref": {"type": "string"},
"metadata_ref": {"type": "string"},
"provenance": {"$ref": "#/schemas/graph_provenance"}
}
},
"graph_node_version_tombstone_response": {
"type": "object",
"required": ["ok", "name", "ref", "target_edge_ref", "tombstone_edge_ref"],
"properties": {
"ok": {"type": "boolean"},
"name": {"type": "string"},
"ref": {"type": "string"},
"target_edge_ref": {"type": "string"},
"tombstone_edge_ref": {"type": "string"},
"metadata_ref": {"type": "string"}
}
},
"graph_batch_request": {
"type": "object",
"properties": {
"idempotency_key": {"type": "string"},
"mode": {"type": "string", "enum": ["fail_fast", "continue_on_error"]},
"nodes": {
"type": "array",
"items": {
"type": "object",
"required": ["name"],
"properties": {
"name": {"type": "string"},
"ref": {"type": "string"}
}
}
},
"versions": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "ref"],
"properties": {
"name": {"type": "string"},
"ref": {"type": "string"},
"metadata_ref": {"type": "string"},
"provenance": {"$ref": "#/schemas/graph_provenance"}
}
}
},
"edges": {
"type": "array",
"items": {
"type": "object",
"required": ["subject", "predicate", "object"],
"properties": {
"subject": {"type": "string"},
"predicate": {"type": "string"},
"object": {"type": "string"},
"metadata_ref": {"type": "string"},
"provenance": {"$ref": "#/schemas/graph_provenance"}
}
}
}
}
},
"graph_batch_response": {
"type": "object",
"required": ["ok", "applied", "results"],
"properties": {
"ok": {"type": "boolean"},
"idempotency_key": {"type": "string"},
"mode": {"type": "string", "enum": ["fail_fast", "continue_on_error"]},
"applied": {
"type": "object",
"required": ["nodes", "versions", "edges"],
"properties": {
"nodes": {"type": "integer"},
"versions": {"type": "integer"},
"edges": {"type": "integer"}
}
},
"results": {
"type": "array",
"items": {
"type": "object",
"required": ["kind", "index", "status", "code", "error"],
"properties": {
"kind": {"type": "string", "enum": ["node", "version", "edge"]},
"index": {"type": "integer"},
"status": {"type": "string", "enum": ["applied", "error"]},
"code": {"type": "integer"},
"error": {"type": ["string", "null"]}
}
}
}
}
},
"graph_query_request": {
"type": "object",
"properties": {
"where": {
"type": "object",
"properties": {
"subject": {"type": "string"},
"object": {"type": "string"},
"node": {"type": "string"},
"provenance_ref": {"type": "string"}
}
},
"predicates": {"type": "array", "items": {"type": "string"}},
"direction": {"type": "string", "enum": ["any", "outgoing", "incoming"]},
"include_versions": {"type": "boolean"},
"include_tombstoned": {"type": "boolean"},
"include_stats": {"type": "boolean"},
"max_result_bytes": {"type": "integer"},
"as_of": {"type": ["string", "integer"]},
"limit": {"type": "integer"},
"cursor": {"type": ["string", "integer"]}
}
},
"graph_query_response": {
"type": "object",
"required": ["nodes", "edges", "paging"],
"properties": {
"nodes": {
"type": "array",
"items": {
"type": "object",
"required": ["concept_ref", "name", "latest_ref"],
"properties": {
"concept_ref": {"type": "string"},
"name": {"type": ["string", "null"]},
"latest_ref": {"type": ["string", "null"]},
"versions": {
"type": "array",
"items": {
"type": "object",
"required": ["edge_ref", "ref"],
"properties": {
"edge_ref": {"type": "string"},
"ref": {"type": "string"}
}
}
}
}
}
},
"edges": {
"type": "array",
"items": {
"type": "object",
"required": ["subject_ref", "predicate_ref", "object_ref", "edge_ref"],
"properties": {
"subject_ref": {"type": "string"},
"predicate_ref": {"type": "string"},
"object_ref": {"type": "string"},
"edge_ref": {"type": "string"}
}
}
},
"paging": {
"type": "object",
"required": ["next_cursor", "has_more"],
"properties": {
"next_cursor": {"type": ["string", "null"]},
"has_more": {"type": "boolean"}
}
},
"stats": {
"type": "object",
"properties": {
"scanned_edges": {"type": "integer"},
"returned_edges": {"type": "integer"}
}
}
}
},
"graph_retrieve_request": {
"type": "object",
"required": ["roots"],
"properties": {
"roots": {"type": "array", "items": {"type": "string"}},
"goal_predicates": {"type": "array", "items": {"type": "string"}},
"max_depth": {"type": "integer"},
"max_fanout": {"type": "integer"},
"include_versions": {"type": "boolean"},
"include_tombstoned": {"type": "boolean"},
"as_of": {"type": ["string", "integer"]},
"provenance_min_confidence": {"type": ["string", "number", "integer"]},
"limit_nodes": {"type": "integer"},
"limit_edges": {"type": "integer"},
"max_result_bytes": {"type": "integer"}
}
},
"graph_retrieve_response": {
"type": "object",
"required": ["nodes", "edges", "explanations", "truncated", "stats"],
"properties": {
"nodes": {
"type": "array",
"items": {
"type": "object",
"required": ["concept_ref", "name", "latest_ref"],
"properties": {
"concept_ref": {"type": "string"},
"name": {"type": ["string", "null"]},
"latest_ref": {"type": ["string", "null"]},
"versions": {
"type": "array",
"items": {
"type": "object",
"required": ["edge_ref", "ref"],
"properties": {
"edge_ref": {"type": "string"},
"ref": {"type": "string"}
}
}
}
}
}
},
"edges": {
"type": "array",
"items": {
"type": "object",
"required": ["subject_ref", "predicate_ref", "object_ref", "edge_ref"],
"properties": {
"subject_ref": {"type": "string"},
"predicate_ref": {"type": "string"},
"object_ref": {"type": "string"},
"edge_ref": {"type": "string"}
}
}
},
"explanations": {
"type": "array",
"items": {
"type": "object",
"required": ["edge_ref", "depth", "reasons"],
"properties": {
"edge_ref": {"type": "string"},
"depth": {"type": "integer"},
"reasons": {"type": "array", "items": {"type": "string"}},
"confidence": {"type": ["number", "null"]}
}
}
},
"truncated": {"type": "boolean"},
"stats": {
"type": "object",
"properties": {
"scanned_edges": {"type": "integer"},
"traversed_edges": {"type": "integer"},
"returned_nodes": {"type": "integer"},
"returned_edges": {"type": "integer"}
}
}
}
},
"graph_export_request": {
"type": "object",
"properties": {
"as_of": {"type": ["string", "integer"]},
"cursor": {"type": ["string", "integer"]},
"limit": {"type": "integer"},
"predicates": {"type": "array", "items": {"type": "string"}},
"roots": {"type": "array", "items": {"type": "string"}},
"include_tombstoned": {"type": "boolean"},
"max_result_bytes": {"type": "integer"}
}
},
"graph_export_response": {
"type": "object",
"required": ["items", "next_cursor", "has_more", "snapshot_as_of", "stats"],
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"required": ["seq", "edge_ref", "subject_ref", "predicate_ref", "predicate", "object_ref", "tombstoned"],
"properties": {
"seq": {"type": "integer"},
"edge_ref": {"type": "string"},
"subject_ref": {"type": "string"},
"predicate_ref": {"type": "string"},
"predicate": {"type": "string"},
"object_ref": {"type": "string"},
"tombstoned": {"type": "boolean"},
"metadata_ref": {"type": ["string", "null"]}
}
}
},
"next_cursor": {"type": ["string", "null"]},
"has_more": {"type": "boolean"},
"snapshot_as_of": {"type": "string"},
"stats": {
"type": "object",
"properties": {
"scanned_edges": {"type": "integer"},
"exported_items": {"type": "integer"}
}
}
}
},
"graph_import_request": {
"type": "object",
"required": ["items"],
"properties": {
"mode": {"type": "string", "enum": ["fail_fast", "continue_on_error"]},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"subject_ref": {"type": "string"},
"subject": {"type": "string"},
"predicate_ref": {"type": "string"},
"predicate": {"type": "string"},
"object_ref": {"type": "string"},
"object": {"type": "string"},
"metadata_ref": {"type": "string"}
}
}
}
}
},
"graph_import_response": {
"type": "object",
"required": ["ok", "applied", "results"],
"properties": {
"ok": {"type": "boolean"},
"applied": {"type": "integer"},
"results": {
"type": "array",
"items": {
"type": "object",
"required": ["index", "status", "code", "error", "edge_ref"],
"properties": {
"index": {"type": "integer"},
"status": {"type": "string", "enum": ["applied", "error"]},
"code": {"type": "integer"},
"error": {"type": ["string", "null"]},
"edge_ref": {"type": ["string", "null"]}
}
}
}
}
},
"graph_schema_predicates_request": {
"type": "object",
"properties": {
"mode": {"type": "string", "enum": ["strict", "warn", "off"]},
"provenance_mode": {"type": "string", "enum": ["optional", "required"]},
"predicates": {
"type": "array",
"items": {
"type": "object",
"properties": {
"predicate_ref": {"type": "string"},
"predicate": {"type": "string"},
"domain": {"type": "string"},
"range": {"type": "string"}
}
}
}
}
},
"graph_schema_predicates_response": {
"type": "object",
"required": ["mode", "provenance_mode", "predicates"],
"properties": {
"mode": {"type": "string", "enum": ["strict", "warn", "off"]},
"provenance_mode": {"type": "string", "enum": ["optional", "required"]},
"predicates": {
"type": "array",
"items": {
"type": "object",
"required": ["predicate_ref", "domain", "range"],
"properties": {
"predicate_ref": {"type": "string"},
"domain": {"type": ["string", "null"]},
"range": {"type": ["string", "null"]}
}
}
}
}
},
"graph_stats_response": {
"type": "object",
"required": ["edges_total", "aliases_total", "index", "tombstones"],
"properties": {
"edges_total": {"type": "integer"},
"aliases_total": {"type": "integer"},
"index": {
"type": "object",
"required": ["built_for_edges", "src_buckets", "dst_buckets", "predicate_buckets", "src_predicate_buckets", "dst_predicate_buckets", "healthy"],
"properties": {
"built_for_edges": {"type": "integer"},
"src_buckets": {"type": "integer"},
"dst_buckets": {"type": "integer"},
"predicate_buckets": {"type": "integer"},
"src_predicate_buckets": {"type": "integer"},
"dst_predicate_buckets": {"type": "integer"},
"healthy": {"type": "boolean"}
}
},
"tombstones": {
"type": "object",
"required": ["edges", "ratio"],
"properties": {
"edges": {"type": "integer"},
"ratio": {"type": "number"}
}
}
}
},
"graph_capabilities_response": {
"type": "object",
"required": ["contract", "graph", "runtime"],
"properties": {
"contract": {"type": "string"},
"graph": {
"type": "object",
"required": ["version", "features", "limits", "modes"],
"properties": {
"version": {"type": "string"},
"features": {"type": "array", "items": {"type": "string"}},
"limits": {"type": "object"},
"modes": {"type": "object"}
}
},
"runtime": {"type": "object"}
}
},
"graph_changes_response": {
"type": "object",
"required": ["events", "next_cursor", "has_more"],
"properties": {
"events": {
"type": "array",
"items": {
"type": "object",
"required": ["event", "cursor", "edge_ref", "subject_ref", "predicate_ref", "object_ref"],
"properties": {
"event": {"type": "string", "enum": ["edge_appended", "version_published", "tombstone_applied"]},
"cursor": {"type": "string"},
"edge_ref": {"type": "string"},
"subject_ref": {"type": "string"},
"predicate_ref": {"type": "string"},
"object_ref": {"type": "string"},
"concept_ref": {"type": "string"},
"ref": {"type": "string"},
"tombstoned_edge_ref": {"type": "string"}
}
}
},
"next_cursor": {"type": ["string", "null"]},
"has_more": {"type": "boolean"}
}
},
"graph_node_response": {
"type": "object",
"required": ["name", "concept_ref", "latest_ref", "versions", "outgoing", "incoming"],
"properties": {
"name": {"type": "string"},
"concept_ref": {"type": "string"},
"latest_ref": {"type": ["string", "null"]},
"versions": {"type": "array", "items": {"type": "string"}},
"outgoing": {
"type": "array",
"items": {
"type": "object",
"required": ["predicate_ref", "object_ref"],
"properties": {
"predicate_ref": {"type": "string"},
"object_ref": {"type": "string"},
"edge_ref": {"type": "string"},
"metadata_ref": {"type": ["string", "null"]}
}
}
},
"incoming": {
"type": "array",
"items": {
"type": "object",
"required": ["predicate_ref", "subject_ref"],
"properties": {
"predicate_ref": {"type": "string"},
"subject_ref": {"type": "string"},
"edge_ref": {"type": "string"},
"metadata_ref": {"type": ["string", "null"]}
}
}
}
}
},
"graph_history_response": {
"type": "object",
"required": ["name", "events"],
"properties": {
"name": {"type": "string"},
"events": {
"type": "array",
"items": {
"type": "object",
"required": ["event", "at_ms"],
"properties": {
"event": {"type": "string"},
"at_ms": {"type": "integer"},
"ref": {"type": ["string", "null"]},
"edge_ref": {"type": ["string", "null"]},
"details": {"type": "object"}
}
}
}
}
},
"pel_execute_request": {
"type": "object",
"required": ["program_ref", "inputs", "receipt"],
"properties": {
"program_ref": {"type": "string", "description": "hex ref or concept name"},
"scheme_ref": {"type": "string", "description": "hex ref or 'dag'"},
"params_ref": {"type": "string", "description": "hex ref or concept name"},
"inputs": {
"type": "object",
"properties": {
"refs": {
"type": "array",
"items": {"type": "string", "description": "hex ref or concept name"}
},
"inline_artifacts": {
"type": "array",
"items": {
"type": "object",
"required": ["body_hex"],
"properties": {
"content_type": {"type": "string"},
"type_tag": {"type": "string", "description": "hex tag id, optional"},
"body_hex": {"type": "string"}
}
}
}
}
},
"receipt": {
"type": "object",
"required": [
"input_manifest_ref",
"environment_ref",
"evaluator_id",
"executor_ref",
"started_at",
"completed_at"
],
"properties": {
"input_manifest_ref": {"type": "string", "description": "hex ref or concept name"},
"environment_ref": {"type": "string", "description": "hex ref or concept name"},
"evaluator_id": {"type": "string"},
"executor_ref": {"type": "string", "description": "hex ref or concept name"},
"sbom_ref": {"type": "string", "description": "hex ref or concept name"},
"parity_digest_hex": {"type": "string"},
"executor_fingerprint_ref": {"type": "string", "description": "hex ref or concept name"},
"run_id_hex": {"type": "string"},
"limits": {
"type": "object",
"required": ["cpu_ms", "wall_ms", "max_rss_kib", "io_reads", "io_writes"],
"properties": {
"cpu_ms": {"type": "integer"},
"wall_ms": {"type": "integer"},
"max_rss_kib": {"type": "integer"},
"io_reads": {"type": "integer"},
"io_writes": {"type": "integer"}
}
},
"logs": {
"type": "array",
"items": {
"type": "object",
"required": ["kind", "log_ref", "sha256_hex"],
"properties": {
"kind": {"type": "integer"},
"log_ref": {"type": "string", "description": "hex ref or concept name"},
"sha256_hex": {"type": "string"}
}
}
},
"determinism_level": {"type": "integer", "description": "0-255"},
"rng_seed_hex": {"type": "string"},
"signature_hex": {"type": "string"},
"started_at": {"type": "integer"},
"completed_at": {"type": "integer"}
}
},
"effects": {
"type": "object",
"properties": {
"publish_outputs": {"type": "boolean"},
"append_fed_log": {"type": "boolean"}
}
}
}
},
"pel_execute_response": {
"type": "object",
"required": ["run_ref", "receipt_ref", "stored_input_refs", "output_refs", "status"],
"properties": {
"run_ref": {"type": "string", "description": "hex ref"},
"trace_ref": {"type": "string", "description": "hex ref"},
"receipt_ref": {"type": "string", "description": "hex ref"},
"stored_input_refs": {"type": "array", "items": {"type": "string", "description": "hex ref"}},
"output_refs": {"type": "array", "items": {"type": "string", "description": "hex ref"}},
"status": {"type": "string"}
}
},
"error_response": {
"type": "object",
"required": ["error"],
"properties": {
"error": {
"type": "object",
"required": ["code", "message", "retryable"],
"properties": {
"code": {"type": "string"},
"message": {"type": "string"},
"retryable": {"type": "boolean"}
}
}
}
}
}
}

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

View file

@ -18,3 +18,8 @@ as stored in the corresponding `registry/*.json` file.
- `bytes_sha256` (string, required): sha256 of the bytes file. - `bytes_sha256` (string, required): sha256 of the bytes file.
- `notes` (string, optional): human notes. - `notes` (string, optional): human notes.
## Contract Notes
- `amduatd-api-contract.v1.json` includes optional receipt v1.1 fields for
`/v1/pel/run` and may emit `receipt_ref` in responses.
- `/v1/fed/records` supports `limit` for paging (default 256, max 10000).

186
scripts/graph_client_helpers.sh Executable file
View file

@ -0,0 +1,186 @@
#!/usr/bin/env bash
set -euo pipefail
# Reusable HTTP and graph client helpers for local unix-socket amduatd usage.
graph_helpers_init() {
if [[ $# -lt 1 ]]; then
echo "usage: graph_helpers_init ROOT_DIR" >&2
return 1
fi
GRAPH_HELPERS_ROOT_DIR="$1"
GRAPH_HELPERS_HTTP="${GRAPH_HELPERS_ROOT_DIR}/build/amduatd_http_unix"
GRAPH_HELPERS_USE_HTTP=0
if command -v curl >/dev/null 2>&1; then
if curl --help 2>/dev/null | grep -q -- '--unix-socket'; then
GRAPH_HELPERS_USE_HTTP=0
else
GRAPH_HELPERS_USE_HTTP=1
fi
else
GRAPH_HELPERS_USE_HTTP=1
fi
if [[ "${GRAPH_HELPERS_USE_HTTP}" -eq 1 && ! -x "${GRAPH_HELPERS_HTTP}" ]]; then
echo "missing http transport (need curl --unix-socket or build/amduatd_http_unix)" >&2
return 1
fi
}
graph_http_get() {
local sock="$1"
local path="$2"
shift 2
if [[ "${GRAPH_HELPERS_USE_HTTP}" -eq 1 ]]; then
"${GRAPH_HELPERS_HTTP}" --sock "${sock}" --method GET --path "${path}" "$@"
else
curl --silent --show-error --fail \
--unix-socket "${sock}" \
"$@" \
"http://localhost${path}"
fi
}
graph_http_get_allow() {
local sock="$1"
local path="$2"
shift 2
if [[ "${GRAPH_HELPERS_USE_HTTP}" -eq 1 ]]; then
"${GRAPH_HELPERS_HTTP}" --sock "${sock}" --method GET --path "${path}" --allow-status "$@"
else
curl --silent --show-error \
--unix-socket "${sock}" \
"$@" \
"http://localhost${path}"
fi
}
graph_http_post() {
local sock="$1"
local path="$2"
local data="$3"
shift 3
if [[ "${GRAPH_HELPERS_USE_HTTP}" -eq 1 ]]; then
"${GRAPH_HELPERS_HTTP}" --sock "${sock}" --method POST --path "${path}" --data "${data}" "$@"
else
curl --silent --show-error --fail \
--unix-socket "${sock}" \
"$@" \
--data-binary "${data}" \
"http://localhost${path}"
fi
}
graph_http_post_allow() {
local sock="$1"
local path="$2"
local data="$3"
shift 3
if [[ "${GRAPH_HELPERS_USE_HTTP}" -eq 1 ]]; then
"${GRAPH_HELPERS_HTTP}" --sock "${sock}" --method POST --path "${path}" --data "${data}" --allow-status "$@"
else
curl --silent --show-error \
--unix-socket "${sock}" \
"$@" \
--data-binary "${data}" \
"http://localhost${path}"
fi
}
graph_wait_for_ready() {
local sock="$1"
local pid="$2"
local log_path="$3"
local i
for i in $(seq 1 120); do
if ! kill -0 "${pid}" >/dev/null 2>&1; then
if [[ -f "${log_path}" ]] && grep -q "bind: Operation not permitted" "${log_path}"; then
echo "skip: bind not permitted for unix socket" >&2
return 77
fi
if [[ -f "${log_path}" ]]; then
cat "${log_path}" >&2
fi
return 1
fi
if [[ -S "${sock}" ]] && graph_http_get "${sock}" "/v1/meta" >/dev/null 2>&1; then
return 0
fi
sleep 0.1
done
return 1
}
graph_batch_ingest() {
local sock="$1"
local space="$2"
local payload="$3"
graph_http_post "${sock}" "/v2/graph/batch" "${payload}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space}"
}
graph_changes_sync_once() {
local sock="$1"
local space="$2"
local cursor="$3"
local limit="$4"
local path="/v2/graph/changes?limit=${limit}"
if [[ -n "${cursor}" ]]; then
path+="&since_cursor=${cursor}"
fi
graph_http_get "${sock}" "${path}" --header "X-Amduat-Space: ${space}"
}
graph_subgraph_fetch() {
local sock="$1"
local space="$2"
local root="$3"
local max_depth="$4"
local predicates="${5:-}"
local path="/v2/graph/subgraph?roots[]=${root}&max_depth=${max_depth}&dir=outgoing&limit_nodes=256&limit_edges=256"
if [[ -n "${predicates}" ]]; then
path+="&predicates[]=${predicates}"
fi
graph_http_get "${sock}" "${path}" --header "X-Amduat-Space: ${space}"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
if [[ $# -lt 1 ]]; then
echo "usage: $0 COMMAND ..." >&2
echo "commands: batch-ingest, sync-once, subgraph" >&2
exit 2
fi
cmd="$1"
shift
: "${AMDUATD_ROOT:?set AMDUATD_ROOT to repo root}"
graph_helpers_init "${AMDUATD_ROOT}"
case "${cmd}" in
batch-ingest)
if [[ $# -ne 3 ]]; then
echo "usage: $0 batch-ingest SOCK SPACE PAYLOAD_JSON" >&2
exit 2
fi
graph_batch_ingest "$1" "$2" "$3"
;;
sync-once)
if [[ $# -ne 4 ]]; then
echo "usage: $0 sync-once SOCK SPACE CURSOR LIMIT" >&2
exit 2
fi
graph_changes_sync_once "$1" "$2" "$3" "$4"
;;
subgraph)
if [[ $# -lt 4 || $# -gt 5 ]]; then
echo "usage: $0 subgraph SOCK SPACE ROOT MAX_DEPTH [PREDICATE]" >&2
exit 2
fi
graph_subgraph_fetch "$1" "$2" "$3" "$4" "${5:-}"
;;
*)
echo "unknown command: ${cmd}" >&2
exit 2
;;
esac
fi

392
scripts/test_fed_ingest.sh Normal file
View file

@ -0,0 +1,392 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HTTP_HELPER="${ROOT_DIR}/build/amduatd_http_unix"
USE_HTTP_HELPER=0
TMPDIR="${TMPDIR:-/tmp}"
mkdir -p "${TMPDIR}"
if ! command -v grep >/dev/null 2>&1; then
echo "skip: grep not found" >&2
exit 77
fi
if ! command -v awk >/dev/null 2>&1; then
echo "skip: awk not found" >&2
exit 77
fi
if command -v curl >/dev/null 2>&1; then
if curl --help 2>/dev/null | grep -q -- '--unix-socket'; then
USE_HTTP_HELPER=0
else
USE_HTTP_HELPER=1
fi
else
USE_HTTP_HELPER=1
fi
if [[ "${USE_HTTP_HELPER}" -eq 1 && ! -x "${HTTP_HELPER}" ]]; then
echo "skip: curl lacks --unix-socket support and helper missing" >&2
exit 77
fi
AMDUATD_BIN="${AMDUATD_BIN:-}"
if [[ -z "${AMDUATD_BIN}" ]]; then
for cand in \
"${ROOT_DIR}/build/amduatd" \
"${ROOT_DIR}/build-asan/amduatd"; do
if [[ -x "${cand}" ]]; then
AMDUATD_BIN="${cand}"
break
fi
done
fi
ASL_BIN="${ASL_BIN:-}"
if [[ -z "${ASL_BIN}" ]]; then
for cand in \
"${ROOT_DIR}/build/vendor/amduat/amduat-asl" \
"${ROOT_DIR}/vendor/amduat/build/amduat-asl"; do
if [[ -x "${cand}" ]]; then
ASL_BIN="${cand}"
break
fi
done
fi
if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then
echo "missing binaries; build amduatd and amduat-asl first" >&2
exit 1
fi
tmp_root="$(mktemp -d -p "${TMPDIR}" amduatd-fed-ingest-XXXXXX)"
tmp_keep="${AMDUATD_FED_INGEST_KEEP_TMP:-0}"
last_log="${TMPDIR}/amduatd-fed-ingest.last.log"
root_store="${tmp_root}/store"
root_ref="${tmp_root}/ref"
sock="${tmp_root}/amduatd.sock"
log="${tmp_root}/amduatd.log"
space_a="alpha"
space_b="beta"
show_log() {
if [[ -n "${log}" && -f "${log}" ]]; then
echo "daemon log: ${log}" >&2
cat "${log}" >&2
else
echo "daemon log missing: ${log}" >&2
fi
if [[ -f "${last_log}" ]]; then
echo "last log copy: ${last_log}" >&2
fi
}
die() {
echo "$1" >&2
show_log
exit 1
}
cleanup() {
if [[ -n "${pid:-}" ]]; then
kill "${pid}" >/dev/null 2>&1 || true
fi
if [[ -f "${log}" ]]; then
cp -f "${log}" "${last_log}" 2>/dev/null || true
fi
if [[ "${tmp_keep}" -eq 0 ]]; then
rm -rf "${tmp_root}"
else
echo "kept tmpdir: ${tmp_root}" >&2
fi
}
trap cleanup EXIT
mkdir -p "${root_store}" "${root_ref}"
"${ASL_BIN}" index init --root "${root_store}"
"${ASL_BIN}" index init --root "${root_ref}"
"${AMDUATD_BIN}" --root "${root_store}" --sock "${sock}" \
--store-backend index --space "${space_a}" \
--fed-enable --fed-transport stub \
>"${log}" 2>&1 &
pid=$!
http_get() {
local sock_path="$1"
local path="$2"
shift 2
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
"${HTTP_HELPER}" --sock "${sock_path}" --method GET --path "${path}" "$@"
else
curl --silent --show-error --fail \
--unix-socket "${sock_path}" \
"$@" \
"http://localhost${path}"
fi
}
http_get_allow() {
local sock_path="$1"
local path="$2"
shift 2
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
"${HTTP_HELPER}" --sock "${sock_path}" --method GET --path "${path}" \
--allow-status "$@"
else
curl --silent --show-error \
--unix-socket "${sock_path}" \
"$@" \
"http://localhost${path}"
fi
}
http_post() {
local sock_path="$1"
local path="$2"
local data="$3"
shift 3
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
"${HTTP_HELPER}" --sock "${sock_path}" --method POST --path "${path}" \
--data "${data}" \
"$@"
else
curl --silent --show-error --fail \
--unix-socket "${sock_path}" \
"$@" \
--data-binary "${data}" \
"http://localhost${path}"
fi
}
http_post_allow() {
local sock_path="$1"
local path="$2"
local data="$3"
shift 3
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
"${HTTP_HELPER}" --sock "${sock_path}" --method POST --path "${path}" \
--allow-status --data "${data}" \
"$@"
else
curl --silent --show-error \
--unix-socket "${sock_path}" \
"$@" \
--data-binary "${data}" \
"http://localhost${path}"
fi
}
http_status() {
local sock_path="$1"
local method="$2"
local path="$3"
shift 3
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
echo ""
return 0
fi
curl --silent --show-error --output /dev/null --write-out '%{http_code}' \
--unix-socket "${sock_path}" \
-X "${method}" \
"$@" \
"http://localhost${path}"
}
wait_for_ready() {
local sock_path="$1"
local pid_val="$2"
local log_path="$3"
local i
for i in $(seq 1 100); do
if ! kill -0 "${pid_val}" >/dev/null 2>&1; then
if [[ -f "${log_path}" ]] && grep -q "bind: Operation not permitted" "${log_path}"; then
echo "skip: bind not permitted for unix socket" >&2
exit 77
fi
if [[ -f "${log_path}" ]]; then
cat "${log_path}" >&2
fi
return 1
fi
if [[ -S "${sock_path}" ]]; then
if http_get "${sock_path}" "/v1/meta" >/dev/null 2>&1; then
return 0
fi
fi
sleep 0.1
done
return 1
}
if ! wait_for_ready "${sock}" "${pid}" "${log}"; then
die "daemon not ready"
fi
payload="fed-ingest"
ref="$(
printf '%s' "${payload}" | "${ASL_BIN}" put --root "${root_ref}" \
--input - --input-format raw --ref-format hex \
| tail -n 1 | tr -d '\r\n'
)"
ingest_resp="$(
http_post "${sock}" "/v1/fed/ingest?record_type=artifact&ref=${ref}" \
"${payload}" \
--header "Content-Type: application/octet-stream" \
--header "X-Amduat-Space: ${space_a}"
)" || {
die "ingest artifact failed"
}
status="$(
printf '%s' "${ingest_resp}" \
| tr -d '\r\n' \
| awk 'match($0, /"status":"[^"]+"/) {print substr($0, RSTART+10, RLENGTH-11)}'
)"
applied="$(
printf '%s' "${ingest_resp}" \
| tr -d '\r\n' \
| awk 'match($0, /"applied":[^,}]+/) {print substr($0, RSTART+10, RLENGTH-10)}'
)"
if [[ "${status}" != "ok" || "${applied}" != "true" ]]; then
die "unexpected ingest response: ${ingest_resp}"
fi
fetched="$(http_get "${sock}" "/v1/artifacts/${ref}")"
if [[ "${fetched}" != "${payload}" ]]; then
die "artifact fetch mismatch"
fi
ingest_again="$(
http_post "${sock}" "/v1/fed/ingest?record_type=artifact&ref=${ref}" \
"${payload}" \
--header "Content-Type: application/octet-stream" \
--header "X-Amduat-Space: ${space_a}"
)"
status="$(
printf '%s' "${ingest_again}" \
| tr -d '\r\n' \
| awk 'match($0, /"status":"[^"]+"/) {print substr($0, RSTART+10, RLENGTH-11)}'
)"
applied="$(
printf '%s' "${ingest_again}" \
| tr -d '\r\n' \
| awk 'match($0, /"applied":[^,}]+/) {print substr($0, RSTART+10, RLENGTH-10)}'
)"
if [[ "${status}" != "already_present" && "${applied}" != "false" ]]; then
die "unexpected re-ingest response: ${ingest_again}"
fi
tombstone_resp="$(
http_post "${sock}" "/v1/fed/ingest" \
"{\"record_type\":\"tombstone\",\"ref\":\"${ref}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_a}"
)"
status="$(
printf '%s' "${tombstone_resp}" \
| tr -d '\r\n' \
| awk 'match($0, /"status":"[^"]+"/) {print substr($0, RSTART+10, RLENGTH-11)}'
)"
if [[ "${status}" != "ok" ]]; then
die "unexpected tombstone response: ${tombstone_resp}"
fi
http_get_allow "${sock}" "/v1/artifacts/${ref}" >/dev/null 2>&1 || true
tombstone_again="$(
http_post "${sock}" "/v1/fed/ingest" \
"{\"record_type\":\"tombstone\",\"ref\":\"${ref}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_a}"
)"
status="$(
printf '%s' "${tombstone_again}" \
| tr -d '\r\n' \
| awk 'match($0, /"status":"[^"]+"/) {print substr($0, RSTART+10, RLENGTH-11)}'
)"
if [[ "${status}" != "ok" && "${status}" != "already_present" ]]; then
die "unexpected tombstone repeat response: ${tombstone_again}"
fi
cap_resp="$(
http_post "${sock}" "/v1/capabilities" \
"{\"kind\":\"pointer_name\",\"target\":{\"name\":\"space/${space_a}/fed/records\"},\"expiry_seconds\":3600}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_a}"
)"
cap_token="$(
printf '%s' "${cap_resp}" \
| tr -d '\r\n' \
| awk 'match($0, /"token":"[^"]+"/) {print substr($0, RSTART+9, RLENGTH-10)}'
)"
if [[ -z "${cap_token}" ]]; then
die "failed to mint capability: ${cap_resp}"
fi
wrong_space_resp="$(
http_post_allow "${sock}" "/v1/fed/ingest?record_type=artifact&ref=${ref}" \
"${payload}" \
--header "Content-Type: application/octet-stream" \
--header "X-Amduat-Space: ${space_b}" \
--header "X-Amduat-Capability: ${cap_token}"
)"
status="$(
printf '%s' "${wrong_space_resp}" \
| tr -d '\r\n' \
| awk 'match($0, /"status":"[^"]+"/) {print substr($0, RSTART+10, RLENGTH-11)}'
)"
if [[ "${status}" != "invalid" ]]; then
die "unexpected wrong-space response: ${wrong_space_resp}"
fi
if [[ "${USE_HTTP_HELPER}" -eq 0 ]]; then
code="$(http_status "${sock}" "POST" "/v1/fed/ingest?record_type=artifact&ref=${ref}" \
--header "Content-Type: application/octet-stream" \
--header "X-Amduat-Space: ${space_b}" \
--header "X-Amduat-Capability: ${cap_token}")"
if [[ "${code}" != "403" ]]; then
die "expected 403 for wrong-space capability, got ${code}"
fi
fi
kill "${pid}" >/dev/null 2>&1 || true
wait "${pid}" >/dev/null 2>&1 || true
pid=""
rm -f "${sock}"
"${AMDUATD_BIN}" --root "${root_store}" --sock "${sock}" \
--store-backend index --space "${space_a}" \
--fed-transport stub \
>"${log}" 2>&1 &
pid=$!
if ! wait_for_ready "${sock}" "${pid}" "${log}"; then
die "daemon (fed disabled) not ready"
fi
disabled_resp="$(
http_post_allow "${sock}" "/v1/fed/ingest?record_type=artifact&ref=${ref}" \
"${payload}" \
--header "Content-Type: application/octet-stream" \
--header "X-Amduat-Space: ${space_a}"
)"
status="$(
printf '%s' "${disabled_resp}" \
| tr -d '\r\n' \
| awk 'match($0, /"status":"[^"]+"/) {print substr($0, RSTART+10, RLENGTH-11)}'
)"
if [[ "${status}" != "error" ]]; then
die "unexpected disabled response: ${disabled_resp}"
fi
if [[ "${USE_HTTP_HELPER}" -eq 0 ]]; then
code="$(http_status "${sock}" "POST" "/v1/fed/ingest?record_type=artifact&ref=${ref}" \
--header "Content-Type: application/octet-stream" \
--header "X-Amduat-Space: ${space_a}")"
if [[ "${code}" != "503" ]]; then
die "expected 503 for federation disabled, got ${code}"
fi
fi
echo "ok"

409
scripts/test_fed_smoke.sh Normal file
View file

@ -0,0 +1,409 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HTTP_HELPER="${ROOT_DIR}/build/amduatd_http_unix"
USE_HTTP_HELPER=0
TMPDIR="${TMPDIR:-/tmp}"
mkdir -p "${TMPDIR}"
if ! command -v grep >/dev/null 2>&1; then
echo "skip: grep not found" >&2
exit 77
fi
if ! command -v awk >/dev/null 2>&1; then
echo "skip: awk not found" >&2
exit 77
fi
if command -v curl >/dev/null 2>&1; then
if curl --help 2>/dev/null | grep -q -- '--unix-socket'; then
USE_HTTP_HELPER=0
else
USE_HTTP_HELPER=1
fi
else
USE_HTTP_HELPER=1
fi
if [[ "${USE_HTTP_HELPER}" -eq 1 && ! -x "${HTTP_HELPER}" ]]; then
echo "skip: curl lacks --unix-socket support and helper missing" >&2
exit 77
fi
AMDUATD_BIN="${AMDUATD_BIN:-}"
if [[ -z "${AMDUATD_BIN}" ]]; then
for cand in \
"${ROOT_DIR}/build/amduatd" \
"${ROOT_DIR}/build-asan/amduatd"; do
if [[ -x "${cand}" ]]; then
AMDUATD_BIN="${cand}"
break
fi
done
fi
ASL_BIN="${ASL_BIN:-}"
if [[ -z "${ASL_BIN}" ]]; then
for cand in \
"${ROOT_DIR}/build/vendor/amduat/amduat-asl" \
"${ROOT_DIR}/vendor/amduat/build/amduat-asl"; do
if [[ -x "${cand}" ]]; then
ASL_BIN="${cand}"
break
fi
done
fi
if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then
echo "missing binaries; build amduatd and amduat-asl first" >&2
exit 1
fi
tmp_root="$(mktemp -d -p "${TMPDIR}" amduatd-fed-smoke-XXXXXX)"
root_a="${tmp_root}/a"
root_b="${tmp_root}/b"
sock_a="${tmp_root}/amduatd-a.sock"
sock_b="${tmp_root}/amduatd-b.sock"
space_id="smoke"
log_a="${tmp_root}/amduatd-a.log"
log_b="${tmp_root}/amduatd-b.log"
cleanup() {
if [[ -n "${pid_a:-}" ]]; then
kill "${pid_a}" >/dev/null 2>&1 || true
fi
if [[ -n "${pid_b:-}" ]]; then
kill "${pid_b}" >/dev/null 2>&1 || true
fi
rm -rf "${tmp_root}"
}
trap cleanup EXIT
mkdir -p "${root_a}" "${root_b}"
"${ASL_BIN}" init --root "${root_a}"
"${ASL_BIN}" init --root "${root_b}"
"${AMDUATD_BIN}" --root "${root_a}" --sock "${sock_a}" \
--store-backend index --space "${space_id}" \
--fed-enable --fed-transport unix \
--fed-unix-sock "${sock_b}" --fed-domain-id 1 \
>"${log_a}" 2>&1 &
pid_a=$!
"${AMDUATD_BIN}" --root "${root_b}" --sock "${sock_b}" \
--store-backend index --space "${space_id}" \
--fed-enable --fed-transport unix \
--fed-unix-sock "${sock_a}" --fed-domain-id 2 \
>"${log_b}" 2>&1 &
pid_b=$!
http_get() {
local sock="$1"
local path="$2"
shift 2
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
"${HTTP_HELPER}" --sock "${sock}" --method GET --path "${path}" "$@"
else
curl --silent --show-error --fail \
--unix-socket "${sock}" \
"$@" \
"http://localhost${path}"
fi
}
http_get_allow() {
local sock="$1"
local path="$2"
shift 2
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
"${HTTP_HELPER}" --sock "${sock}" --method GET --path "${path}" \
--allow-status "$@"
else
curl --silent --show-error \
--unix-socket "${sock}" \
"$@" \
"http://localhost${path}"
fi
}
http_post() {
local sock="$1"
local path="$2"
local data="$3"
shift 3
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
"${HTTP_HELPER}" --sock "${sock}" --method POST --path "${path}" \
--data "${data}" \
"$@"
else
curl --silent --show-error --fail \
--unix-socket "${sock}" \
"$@" \
--data-binary "${data}" \
"http://localhost${path}"
fi
}
http_post_allow() {
local sock="$1"
local path="$2"
local data="$3"
shift 3
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
"${HTTP_HELPER}" --sock "${sock}" --method POST --path "${path}" \
--data "${data}" --allow-status \
"$@"
else
curl --silent --show-error \
--unix-socket "${sock}" \
"$@" \
--data-binary "${data}" \
"http://localhost${path}"
fi
}
wait_for_ready() {
local sock="$1"
local pid="$2"
local log_path="$3"
local i
for i in $(seq 1 100); do
if ! kill -0 "${pid}" >/dev/null 2>&1; then
if [[ -f "${log_path}" ]] && grep -q "bind: Operation not permitted" "${log_path}"; then
echo "skip: bind not permitted for unix socket" >&2
exit 77
fi
if [[ -f "${log_path}" ]]; then
cat "${log_path}" >&2
fi
return 1
fi
if [[ -S "${sock}" ]]; then
if http_get "${sock}" "/v1/meta" >/dev/null 2>&1; then
return 0
fi
fi
sleep 0.1
done
return 1
}
if ! wait_for_ready "${sock_a}" "${pid_a}" "${log_a}"; then
echo "daemon A not ready" >&2
exit 1
fi
if ! wait_for_ready "${sock_b}" "${pid_b}" "${log_b}"; then
echo "daemon B not ready" >&2
exit 1
fi
payload="fed-smoke"
artifact_resp="$(
http_post "${sock_a}" "/v1/artifacts" "${payload}" \
--header "Content-Type: application/octet-stream" \
--header "X-Amduat-Space: ${space_id}"
)" || {
echo "artifact POST failed" >&2
if [[ -f "${log_a}" ]]; then
cat "${log_a}" >&2
fi
exit 1
}
ref="$(
printf '%s' "${artifact_resp}" \
| tr -d '\r\n' \
| awk 'match($0, /"ref":"[^"]+"/) {print substr($0, RSTART+7, RLENGTH-8)}'
)"
if [[ -z "${ref}" ]]; then
echo "failed to parse ref from daemon A" >&2
echo "artifact response: ${artifact_resp}" >&2
if [[ -f "${log_a}" ]]; then
cat "${log_a}" >&2
fi
exit 1
fi
plan_resp="$(
http_get_allow "${sock_b}" "/v1/fed/pull/plan?peer=1&limit=8" \
--header "X-Amduat-Space: ${space_id}"
)" || {
echo "pull plan failed" >&2
if [[ -f "${log_b}" ]]; then
cat "${log_b}" >&2
fi
exit 1
}
if ! echo "${plan_resp}" | grep -q "\"record_count\":"; then
echo "pull plan malformed" >&2
echo "plan response: ${plan_resp}" >&2
exit 1
fi
if echo "${plan_resp}" | grep -q "\"record_count\":0"; then
echo "pull plan empty" >&2
echo "plan response: ${plan_resp}" >&2
exit 1
fi
pull_resp="$(
http_post "${sock_b}" "/v1/fed/pull?peer=1&limit=8" "" \
--header "X-Amduat-Space: ${space_id}"
)" || {
echo "pull apply failed" >&2
if [[ -f "${log_b}" ]]; then
cat "${log_b}" >&2
fi
exit 1
}
if ! echo "${pull_resp}" | grep -q "\"advanced\":true"; then
echo "pull did not advance cursor" >&2
echo "pull response: ${pull_resp}" >&2
exit 1
fi
cursor_json="$(
http_get_allow "${sock_b}" "/v1/fed/cursor?peer=1" \
--header "X-Amduat-Space: ${space_id}"
)" || {
echo "cursor fetch failed" >&2
if [[ -f "${log_a}" ]]; then
cat "${log_a}" >&2
fi
if [[ -f "${log_b}" ]]; then
cat "${log_b}" >&2
fi
exit 1
}
echo "${cursor_json}" | grep -q "\"last_logseq\":" || {
echo "cursor missing last_logseq" >&2
echo "cursor response: ${cursor_json}" >&2
exit 1
}
payload_b="$(
http_get "${sock_b}" "/v1/artifacts/${ref}" \
--header "X-Amduat-Space: ${space_id}"
)" || {
echo "artifact fetch failed" >&2
if [[ -f "${log_b}" ]]; then
cat "${log_b}" >&2
fi
exit 1
}
if [[ "${payload_b}" != "${payload}" ]]; then
echo "payload mismatch after pull" >&2
exit 1
fi
payload_push="fed-smoke-push"
artifact_resp_push="$(
http_post "${sock_b}" "/v1/artifacts" "${payload_push}" \
--header "Content-Type: application/octet-stream" \
--header "X-Amduat-Space: ${space_id}"
)" || {
echo "artifact POST failed on B" >&2
if [[ -f "${log_b}" ]]; then
cat "${log_b}" >&2
fi
exit 1
}
ref_push="$(
printf '%s' "${artifact_resp_push}" \
| tr -d '\r\n' \
| awk 'match($0, /"ref":"[^"]+"/) {print substr($0, RSTART+7, RLENGTH-8)}'
)"
if [[ -z "${ref_push}" ]]; then
echo "failed to parse ref from daemon B" >&2
echo "artifact response: ${artifact_resp_push}" >&2
if [[ -f "${log_b}" ]]; then
cat "${log_b}" >&2
fi
exit 1
fi
push_plan_resp="$(
http_get_allow "${sock_b}" "/v1/fed/push/plan?peer=1&limit=8" \
--header "X-Amduat-Space: ${space_id}"
)" || {
echo "push plan failed" >&2
if [[ -f "${log_b}" ]]; then
cat "${log_b}" >&2
fi
exit 1
}
if ! echo "${push_plan_resp}" | grep -q "\"record_count\":"; then
echo "push plan malformed (missing endpoint?)" >&2
echo "push plan response: ${push_plan_resp}" >&2
exit 1
fi
if echo "${push_plan_resp}" | grep -q "\"record_count\":0"; then
echo "push plan empty" >&2
echo "push plan response: ${push_plan_resp}" >&2
exit 1
fi
push_cursor_before="$(
printf '%s' "${push_plan_resp}" \
| tr -d '\r\n' \
| awk 'match($0, /"cursor":\{[^}]*\}/) {seg=substr($0, RSTART, RLENGTH); if (match(seg, /"last_logseq":[0-9]+/)) {print substr(seg, RSTART+14, RLENGTH-14)}}'
)"
push_resp="$(
http_post_allow "${sock_b}" "/v1/fed/push?peer=1&limit=8" "" \
--header "X-Amduat-Space: ${space_id}"
)" || {
echo "push apply failed" >&2
if [[ -f "${log_b}" ]]; then
cat "${log_b}" >&2
fi
exit 1
}
if ! echo "${push_resp}" | grep -q "\"advanced\":true"; then
echo "push did not advance cursor" >&2
echo "push response: ${push_resp}" >&2
exit 1
fi
payload_a="$(
http_get "${sock_a}" "/v1/artifacts/${ref_push}" \
--header "X-Amduat-Space: ${space_id}"
)" || {
echo "artifact fetch failed on A" >&2
if [[ -f "${log_a}" ]]; then
cat "${log_a}" >&2
fi
exit 1
}
if [[ "${payload_a}" != "${payload_push}" ]]; then
echo "payload mismatch after push" >&2
exit 1
fi
push_plan_after="$(
http_get_allow "${sock_b}" "/v1/fed/push/plan?peer=1&limit=1" \
--header "X-Amduat-Space: ${space_id}"
)" || {
echo "push plan after failed" >&2
if [[ -f "${log_b}" ]]; then
cat "${log_b}" >&2
fi
exit 1
}
push_cursor_after="$(
printf '%s' "${push_plan_after}" \
| tr -d '\r\n' \
| awk 'match($0, /"cursor":\{[^}]*\}/) {seg=substr($0, RSTART, RLENGTH); if (match(seg, /"last_logseq":[0-9]+/)) {print substr(seg, RSTART+14, RLENGTH-14)}}'
)"
if [[ -n "${push_cursor_before}" && -n "${push_cursor_after}" ]]; then
if [[ "${push_cursor_after}" -lt "${push_cursor_before}" ]]; then
echo "push cursor did not advance" >&2
echo "cursor before: ${push_cursor_before}" >&2
echo "cursor after: ${push_cursor_after}" >&2
exit 1
fi
fi
echo "fed smoke ok"

382
scripts/test_graph_contract.sh Executable file
View file

@ -0,0 +1,382 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMPDIR="${TMPDIR:-/tmp}"
mkdir -p "${TMPDIR}"
if ! command -v grep >/dev/null 2>&1; then
echo "skip: grep not found" >&2
exit 77
fi
if ! command -v awk >/dev/null 2>&1; then
echo "skip: awk not found" >&2
exit 77
fi
# shellcheck source=/dev/null
source "${ROOT_DIR}/scripts/graph_client_helpers.sh"
graph_helpers_init "${ROOT_DIR}"
AMDUATD_BIN="${ROOT_DIR}/build/amduatd"
ASL_BIN="${ROOT_DIR}/build/vendor/amduat/amduat-asl"
if [[ ! -x "${ASL_BIN}" ]]; then
ASL_BIN="${ROOT_DIR}/vendor/amduat/build/amduat-asl"
fi
if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then
echo "missing binaries; build amduatd and amduat-asl first" >&2
exit 1
fi
tmp_root="$(mktemp -d -p "${TMPDIR}" amduatd-graph-contract-XXXXXX)"
root="${tmp_root}/root"
sock="${tmp_root}/amduatd.sock"
space_id="graphcontract"
log_file="${tmp_root}/amduatd.log"
cleanup() {
if [[ -n "${pid:-}" ]]; then
kill "${pid}" >/dev/null 2>&1 || true
wait "${pid}" >/dev/null 2>&1 || true
fi
rm -rf "${tmp_root}"
}
trap cleanup EXIT
extract_json_string() {
local key="$1"
awk -v k="\"${key}\":\"" '
{
pos = index($0, k);
if (pos > 0) {
rest = substr($0, pos + length(k));
end = index(rest, "\"");
if (end > 0) {
print substr(rest, 1, end - 1);
exit 0;
}
}
}
'
}
cursor_to_num() {
local token="$1"
if [[ -z "${token}" ]]; then
echo 0
return 0
fi
if [[ "${token}" == g1_* ]]; then
echo "${token#g1_}"
return 0
fi
echo "${token}"
}
cursor_plus_one() {
local token="$1"
local n
if [[ -z "${token}" ]]; then
return 1
fi
if [[ "${token}" == g1_* ]]; then
n="${token#g1_}"
printf 'g1_%s' "$((n + 1))"
return 0
fi
printf '%s' "$((token + 1))"
}
create_artifact() {
local payload="$1"
local resp
local ref
resp="$({
graph_http_post "${sock}" "/v1/artifacts" "${payload}" \
--header "Content-Type: application/octet-stream" \
--header "X-Amduat-Space: ${space_id}"
})"
ref="$(printf '%s\n' "${resp}" | extract_json_string "ref")"
if [[ -z "${ref}" ]]; then
echo "failed to parse artifact ref: ${resp}" >&2
exit 1
fi
printf '%s' "${ref}"
}
create_node() {
local name="$1"
local ref="$2"
graph_http_post "${sock}" "/v2/graph/nodes" "{\"name\":\"${name}\",\"ref\":\"${ref}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
}
create_edge() {
local s="$1"
local p="$2"
local o="$3"
graph_http_post "${sock}" "/v2/graph/edges" "{\"subject\":\"${s}\",\"predicate\":\"${p}\",\"object\":\"${o}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
}
mkdir -p "${root}"
"${ASL_BIN}" init --root "${root}" >/dev/null
"${AMDUATD_BIN}" --root "${root}" --sock "${sock}" --store-backend index --space "${space_id}" \
>"${log_file}" 2>&1 &
pid=$!
ready_rc=0
graph_wait_for_ready "${sock}" "${pid}" "${log_file}" || ready_rc=$?
if [[ ${ready_rc} -eq 77 ]]; then
exit 77
fi
if [[ ${ready_rc} -ne 0 ]]; then
echo "daemon not ready" >&2
exit 1
fi
ref_a="$(create_artifact "contract-a")"
ref_b="$(create_artifact "contract-b")"
ref_c="$(create_artifact "contract-c")"
ref_v1="$(create_artifact "contract-v1")"
ref_v2="$(create_artifact "contract-v2")"
create_node "gc-a" "${ref_a}"
create_node "gc-b" "${ref_b}"
create_node "gc-c" "${ref_c}"
create_node "gc-v" "${ref_v1}"
create_edge "gc-a" "ms.computed_by" "gc-b"
create_edge "gc-b" "ms.computed_by" "gc-c"
graph_http_post "${sock}" "/v2/graph/nodes/gc-v/versions" "{\"ref\":\"${ref_v2}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
versions_cutoff_raw="$(
graph_http_get "${sock}" "/v2/graph/changes?event_types[]=version_published&limit=100" \
--header "X-Amduat-Space: ${space_id}"
)"
versions_cutoff="$(printf '%s\n' "${versions_cutoff_raw}" | extract_json_string "next_cursor")"
if [[ -z "${versions_cutoff}" ]]; then
echo "missing version cutoff cursor: ${versions_cutoff_raw}" >&2
exit 1
fi
versions_cutoff="$(cursor_plus_one "${versions_cutoff}")"
graph_http_post "${sock}" "/v2/graph/nodes/gc-v/versions/tombstone" "{\"ref\":\"${ref_v2}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
node_default="$(
graph_http_get "${sock}" "/v2/graph/nodes/gc-v" \
--header "X-Amduat-Space: ${space_id}"
)"
node_default_latest="$(printf '%s\n' "${node_default}" | extract_json_string "latest_ref")"
if [[ "${node_default_latest}" != "${ref_v1}" ]]; then
echo "node default latest_ref mismatch (want ${ref_v1}): ${node_default}" >&2
exit 1
fi
if echo "${node_default}" | grep -q "\"ref\":\"${ref_v2}\""; then
echo "node default should hide tombstoned ${ref_v2}: ${node_default}" >&2
exit 1
fi
node_all="$(
graph_http_get "${sock}" "/v2/graph/nodes/gc-v?include_tombstoned=true" \
--header "X-Amduat-Space: ${space_id}"
)"
node_all_latest="$(printf '%s\n' "${node_all}" | extract_json_string "latest_ref")"
if [[ "${node_all_latest}" != "${ref_v2}" ]]; then
echo "node include_tombstoned latest_ref mismatch (want ${ref_v2}): ${node_all}" >&2
exit 1
fi
echo "${node_all}" | grep -q "\"ref\":\"${ref_v2}\"" || {
echo "node include_tombstoned should include ${ref_v2}: ${node_all}" >&2
exit 1
}
node_asof="$(
graph_http_get "${sock}" "/v2/graph/nodes/gc-v?as_of=${versions_cutoff}" \
--header "X-Amduat-Space: ${space_id}"
)"
node_asof_latest="$(printf '%s\n' "${node_asof}" | extract_json_string "latest_ref")"
if [[ "${node_asof_latest}" != "${ref_v2}" ]]; then
echo "node as_of before tombstone latest_ref mismatch (want ${ref_v2}): ${node_asof}" >&2
exit 1
fi
echo "${node_asof}" | grep -q "\"ref\":\"${ref_v2}\"" || {
echo "node as_of before tombstone should include ${ref_v2}: ${node_asof}" >&2
exit 1
}
history_default="$(
graph_http_get "${sock}" "/v2/graph/history/gc-v" \
--header "X-Amduat-Space: ${space_id}"
)"
history_default_latest="$(printf '%s\n' "${history_default}" | extract_json_string "latest_ref")"
if [[ "${history_default_latest}" != "${ref_v1}" ]]; then
echo "history default latest_ref mismatch (want ${ref_v1}): ${history_default}" >&2
exit 1
fi
if echo "${history_default}" | grep -q "\"ref\":\"${ref_v2}\""; then
echo "history default should hide tombstoned ${ref_v2}: ${history_default}" >&2
exit 1
fi
history_all="$(
graph_http_get "${sock}" "/v2/graph/history/gc-v?include_tombstoned=true" \
--header "X-Amduat-Space: ${space_id}"
)"
history_all_latest="$(printf '%s\n' "${history_all}" | extract_json_string "latest_ref")"
if [[ "${history_all_latest}" != "${ref_v2}" ]]; then
echo "history include_tombstoned latest_ref mismatch (want ${ref_v2}): ${history_all}" >&2
exit 1
fi
echo "${history_all}" | grep -q "\"ref\":\"${ref_v2}\"" || {
echo "history include_tombstoned should include ${ref_v2}: ${history_all}" >&2
exit 1
}
# schema strict: block predicate not in allowed list.
strict_policy='{"mode":"strict","predicates":[{"predicate":"ms.computed_by"}]}'
graph_http_post "${sock}" "/v2/graph/schema/predicates" "${strict_policy}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
strict_reject="$({
graph_http_post_allow "${sock}" "/v2/graph/edges" '{"subject":"gc-a","predicate":"ms.within_domain","object":"gc-c"}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
})"
echo "${strict_reject}" | grep -q 'predicate rejected by schema policy' || {
echo "strict mode should reject disallowed predicate: ${strict_reject}" >&2
exit 1
}
# schema warn: same write should pass.
warn_policy='{"mode":"warn","predicates":[{"predicate":"ms.computed_by"}]}'
graph_http_post "${sock}" "/v2/graph/schema/predicates" "${warn_policy}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
warn_accept="$({
graph_http_post "${sock}" "/v2/graph/edges" '{"subject":"gc-a","predicate":"ms.within_domain","object":"gc-c"}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
})"
echo "${warn_accept}" | grep -q '"edge_ref":"' || {
echo "warn mode should allow disallowed predicate: ${warn_accept}" >&2
exit 1
}
# provenance required: writes without metadata/provenance must fail with 422.
required_policy='{"mode":"warn","provenance_mode":"required","predicates":[{"predicate":"ms.computed_by"}]}'
graph_http_post "${sock}" "/v2/graph/schema/predicates" "${required_policy}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
schema_required="$(
graph_http_get "${sock}" "/v2/graph/schema/predicates" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${schema_required}" | grep -q '"provenance_mode":"required"' || {
echo "schema provenance_mode did not persist: ${schema_required}" >&2
exit 1
}
required_reject="$({
graph_http_post_allow "${sock}" "/v2/graph/edges" '{"subject":"gc-a","predicate":"ms.computed_by","object":"gc-b"}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
})"
echo "${required_reject}" | grep -q 'provenance required by schema policy' || {
echo "required provenance should reject missing attachment: ${required_reject}" >&2
exit 1
}
required_version_reject="$({
graph_http_post_allow "${sock}" "/v2/graph/nodes/gc-a/versions" "{\"ref\":\"${ref_v1}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
})"
echo "${required_version_reject}" | grep -q 'provenance required by schema policy' || {
echo "required provenance should reject version write without attachment: ${required_version_reject}" >&2
exit 1
}
required_accept="$({
graph_http_post "${sock}" "/v2/graph/edges" '{"subject":"gc-a","predicate":"ms.computed_by","object":"gc-b","provenance":{"source_uri":"urn:test","extractor":"contract-test","observed_at":1,"ingested_at":2,"trace_id":"trace-required-1"}}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
})"
echo "${required_accept}" | grep -q '"edge_ref":"' || {
echo "required provenance should allow explicit provenance payload: ${required_accept}" >&2
exit 1
}
# reset to optional so remaining tests can use minimal payloads.
optional_policy='{"mode":"warn","provenance_mode":"optional","predicates":[{"predicate":"ms.computed_by"}]}'
graph_http_post "${sock}" "/v2/graph/schema/predicates" "${optional_policy}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
# batch idempotency replay must be deterministic.
idem_payload='{"idempotency_key":"gc-idem-1","mode":"continue_on_error","edges":[{"subject":"gc-a","predicate":"ms.computed_by","object":"gc-c"},{"subject":"gc-missing","predicate":"ms.computed_by","object":"gc-c"}]}'
idem_first="$(graph_batch_ingest "${sock}" "${space_id}" "${idem_payload}")"
idem_second="$(graph_batch_ingest "${sock}" "${space_id}" "${idem_payload}")"
if [[ "${idem_first}" != "${idem_second}" ]]; then
echo "idempotent replay mismatch" >&2
echo "first=${idem_first}" >&2
echo "second=${idem_second}" >&2
exit 1
fi
# payload mismatch on same idempotency key must fail.
idem_conflict="$({
graph_http_post_allow "${sock}" "/v2/graph/batch" '{"idempotency_key":"gc-idem-1","mode":"continue_on_error","edges":[{"subject":"gc-a","predicate":"ms.computed_by","object":"gc-b"}]}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
})"
echo "${idem_conflict}" | grep -q 'idempotency_key reuse with different payload' || {
echo "idempotency conflict missing expected error: ${idem_conflict}" >&2
exit 1
}
# changes sync helper: cursor monotonic + resumable loop.
changes_1="$(graph_changes_sync_once "${sock}" "${space_id}" "" 2)"
cursor_1="$(printf '%s\n' "${changes_1}" | extract_json_string "next_cursor")"
if [[ -z "${cursor_1}" ]]; then
echo "changes first page missing next_cursor: ${changes_1}" >&2
exit 1
fi
changes_2="$(graph_changes_sync_once "${sock}" "${space_id}" "${cursor_1}" 2)"
cursor_2="$(printf '%s\n' "${changes_2}" | extract_json_string "next_cursor")"
if [[ -z "${cursor_2}" ]]; then
echo "changes second page missing next_cursor: ${changes_2}" >&2
exit 1
fi
num_1="$(cursor_to_num "${cursor_1}")"
num_2="$(cursor_to_num "${cursor_2}")"
if [[ "${num_2}" -lt "${num_1}" ]]; then
echo "changes cursor regressed: ${cursor_1} -> ${cursor_2}" >&2
exit 1
fi
# subgraph helper should return connected nodes.
subgraph_resp="$(graph_subgraph_fetch "${sock}" "${space_id}" "gc-a" 2 "ms.computed_by")"
echo "${subgraph_resp}" | grep -q '"name":"gc-a"' || {
echo "subgraph missing gc-a: ${subgraph_resp}" >&2
exit 1
}
echo "${subgraph_resp}" | grep -q '"name":"gc-b"' || {
echo "subgraph missing gc-b: ${subgraph_resp}" >&2
exit 1
}
echo "ok: v2 graph contract tests passed"

View file

@ -0,0 +1,82 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMPDIR="${TMPDIR:-/tmp}"
mkdir -p "${TMPDIR}"
# shellcheck source=/dev/null
source "${ROOT_DIR}/scripts/graph_client_helpers.sh"
graph_helpers_init "${ROOT_DIR}"
AMDUATD_BIN="${ROOT_DIR}/build/amduatd"
ASL_BIN="${ROOT_DIR}/vendor/amduat/build/amduat-asl"
if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then
echo "missing binaries; build amduatd and amduat-asl first" >&2
exit 1
fi
tmp_root="$(mktemp -d -p "${TMPDIR}" amduatd-graph-index-XXXXXX)"
root="${tmp_root}/root"
sock="${tmp_root}/amduatd.sock"
space_id="graphindex"
log_file="${tmp_root}/amduatd.log"
cleanup() {
if [[ -n "${pid:-}" ]]; then
kill "${pid}" >/dev/null 2>&1 || true
wait "${pid}" >/dev/null 2>&1 || true
fi
rm -rf "${tmp_root}"
}
trap cleanup EXIT
mkdir -p "${root}"
"${ASL_BIN}" index init --root "${root}" >/dev/null
"${AMDUATD_BIN}" --root "${root}" --sock "${sock}" --store-backend index --space "${space_id}" \
>"${log_file}" 2>&1 &
pid=$!
ready_rc=0
graph_wait_for_ready "${sock}" "${pid}" "${log_file}" || ready_rc=$?
if [[ ${ready_rc} -eq 77 ]]; then
exit 77
fi
if [[ ${ready_rc} -ne 0 ]]; then
echo "daemon not ready" >&2
exit 1
fi
node_a="$(
graph_http_post "${sock}" "/v2/graph/nodes" '{"name":"idx-a"}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${node_a}" | grep -q '"name":"idx-a"' || {
echo "first node create failed: ${node_a}" >&2
exit 1
}
node_b="$(
graph_http_post "${sock}" "/v2/graph/nodes" '{"name":"idx-b"}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${node_b}" | grep -q '"name":"idx-b"' || {
echo "second node create failed: ${node_b}" >&2
exit 1
}
edge="$(
graph_http_post "${sock}" "/v2/graph/edges" \
'{"subject":"idx-a","predicate":"ms.within_domain","object":"idx-b"}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${edge}" | grep -q '"edge_ref":"' || {
echo "edge create failed: ${edge}" >&2
exit 1
}
echo "ok: index append sequence passed"

View file

@ -0,0 +1,114 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMPDIR="${TMPDIR:-/tmp}"
mkdir -p "${TMPDIR}"
# shellcheck source=/dev/null
source "${ROOT_DIR}/scripts/graph_client_helpers.sh"
graph_helpers_init "${ROOT_DIR}"
AMDUATD_BIN="${ROOT_DIR}/build/amduatd"
ASL_BIN="${ROOT_DIR}/vendor/amduat/build/amduat-asl"
if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then
echo "missing binaries; build amduatd and amduat-asl first" >&2
exit 1
fi
iters="${AMDUATD_INDEX_STRESS_ITERS:-20}"
case "${iters}" in
''|*[!0-9]*)
echo "invalid AMDUATD_INDEX_STRESS_ITERS: ${iters}" >&2
exit 2
;;
esac
if [[ "${iters}" -le 0 ]]; then
echo "AMDUATD_INDEX_STRESS_ITERS must be > 0" >&2
exit 2
fi
run_one() {
local i="$1"
local tmp_root root sock space_id log_file pid ready_rc
tmp_root="$(mktemp -d -p "${TMPDIR}" amduatd-graph-index-stress-XXXXXX)"
root="${tmp_root}/root"
sock="${tmp_root}/amduatd.sock"
space_id="graphindex${i}"
log_file="${tmp_root}/amduatd.log"
pid=""
cleanup_one() {
if [[ -n "${pid}" ]]; then
kill "${pid}" >/dev/null 2>&1 || true
wait "${pid}" >/dev/null 2>&1 || true
fi
rm -rf "${tmp_root}"
}
trap cleanup_one RETURN
mkdir -p "${root}"
"${ASL_BIN}" index init --root "${root}" >/dev/null
"${AMDUATD_BIN}" --root "${root}" --sock "${sock}" --store-backend index --space "${space_id}" \
>"${log_file}" 2>&1 &
pid=$!
ready_rc=0
graph_wait_for_ready "${sock}" "${pid}" "${log_file}" || ready_rc=$?
if [[ ${ready_rc} -eq 77 ]]; then
return 77
fi
if [[ ${ready_rc} -ne 0 ]]; then
echo "iteration ${i}: daemon not ready" >&2
return 1
fi
node_a="$(
graph_http_post "${sock}" "/v2/graph/nodes" '{"name":"idx-a"}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${node_a}" | grep -q '"name":"idx-a"' || {
echo "iteration ${i}: first node create failed: ${node_a}" >&2
return 1
}
node_b="$(
graph_http_post "${sock}" "/v2/graph/nodes" '{"name":"idx-b"}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${node_b}" | grep -q '"name":"idx-b"' || {
echo "iteration ${i}: second node create failed: ${node_b}" >&2
return 1
}
edge="$(
graph_http_post "${sock}" "/v2/graph/edges" \
'{"subject":"idx-a","predicate":"ms.within_domain","object":"idx-b"}' \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${edge}" | grep -q '"edge_ref":"' || {
echo "iteration ${i}: edge create failed: ${edge}" >&2
return 1
}
echo "iteration ${i}/${iters}: ok"
return 0
}
for i in $(seq 1 "${iters}"); do
rc=0
run_one "${i}" || rc=$?
if [[ ${rc} -eq 77 ]]; then
exit 77
fi
if [[ ${rc} -ne 0 ]]; then
exit "${rc}"
fi
done
echo "ok: index append stress passed (${iters} iterations)"

781
scripts/test_graph_queries.sh Executable file
View file

@ -0,0 +1,781 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HTTP_HELPER="${ROOT_DIR}/build/amduatd_http_unix"
USE_HTTP_HELPER=0
TMPDIR="${TMPDIR:-/tmp}"
mkdir -p "${TMPDIR}"
if ! command -v grep >/dev/null 2>&1; then
echo "skip: grep not found" >&2
exit 77
fi
if ! command -v awk >/dev/null 2>&1; then
echo "skip: awk not found" >&2
exit 77
fi
if command -v curl >/dev/null 2>&1; then
if curl --help 2>/dev/null | grep -q -- '--unix-socket'; then
USE_HTTP_HELPER=0
else
USE_HTTP_HELPER=1
fi
else
USE_HTTP_HELPER=1
fi
if [[ "${USE_HTTP_HELPER}" -eq 1 && ! -x "${HTTP_HELPER}" ]]; then
echo "skip: curl lacks --unix-socket support and helper missing" >&2
exit 77
fi
AMDUATD_BIN="${ROOT_DIR}/build/amduatd"
ASL_BIN="${ROOT_DIR}/build/vendor/amduat/amduat-asl"
if [[ ! -x "${ASL_BIN}" ]]; then
ASL_BIN="${ROOT_DIR}/vendor/amduat/build/amduat-asl"
fi
if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then
echo "missing binaries; build amduatd and amduat-asl first" >&2
exit 1
fi
tmp_root="$(mktemp -d -p "${TMPDIR}" amduatd-graph-queries-XXXXXX)"
root="${tmp_root}/root"
sock="${tmp_root}/amduatd.sock"
space_id="graphq"
log_file="${tmp_root}/amduatd.log"
cleanup() {
if [[ -n "${pid:-}" ]]; then
kill "${pid}" >/dev/null 2>&1 || true
fi
rm -rf "${tmp_root}"
}
trap cleanup EXIT
http_get() {
local path="$1"
shift
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
"${HTTP_HELPER}" --sock "${sock}" --method GET --path "${path}" "$@"
else
curl --silent --show-error --fail \
--unix-socket "${sock}" \
"$@" \
"http://localhost${path}"
fi
}
http_post() {
local path="$1"
local data="$2"
shift 2
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
"${HTTP_HELPER}" --sock "${sock}" --method POST --path "${path}" \
--data "${data}" \
"$@"
else
curl --silent --show-error --fail \
--unix-socket "${sock}" \
"$@" \
--data-binary "${data}" \
"http://localhost${path}"
fi
}
extract_json_string() {
local key="$1"
awk -v k="\"${key}\":\"" '
{
pos = index($0, k);
if (pos > 0) {
rest = substr($0, pos + length(k));
end = index(rest, "\"");
if (end > 0) {
print substr(rest, 1, end - 1);
exit 0;
}
}
}
'
}
cursor_plus_one() {
local token="$1"
local n
if [[ -z "${token}" ]]; then
return 1
fi
if [[ "${token}" == g1_* ]]; then
n="${token#g1_}"
printf 'g1_%s' "$((n + 1))"
return 0
fi
printf '%s' "$((token + 1))"
}
wait_for_ready() {
local i
for i in $(seq 1 100); do
if ! kill -0 "${pid}" >/dev/null 2>&1; then
if [[ -f "${log_file}" ]] && grep -q "bind: Operation not permitted" "${log_file}"; then
echo "skip: bind not permitted for unix socket" >&2
exit 77
fi
[[ -f "${log_file}" ]] && cat "${log_file}" >&2
return 1
fi
if [[ -S "${sock}" ]] && http_get "/v1/meta" >/dev/null 2>&1; then
return 0
fi
sleep 0.1
done
return 1
}
create_artifact() {
local payload="$1"
local resp
local ref
resp="$(
http_post "/v1/artifacts" "${payload}" \
--header "Content-Type: application/octet-stream" \
--header "X-Amduat-Space: ${space_id}"
)"
ref="$(printf '%s\n' "${resp}" | extract_json_string "ref")"
if [[ -z "${ref}" ]]; then
echo "failed to parse artifact ref: ${resp}" >&2
exit 1
fi
printf '%s' "${ref}"
}
create_node() {
local name="$1"
local ref="$2"
http_post "/v2/graph/nodes" "{\"name\":\"${name}\",\"ref\":\"${ref}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
}
create_edge() {
local subject="$1"
local predicate="$2"
local object="$3"
http_post "/v2/graph/edges" \
"{\"subject\":\"${subject}\",\"predicate\":\"${predicate}\",\"object\":\"${object}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
}
create_edge_with_metadata() {
local subject="$1"
local predicate="$2"
local object="$3"
local metadata_ref="$4"
http_post "/v2/graph/edges" \
"{\"subject\":\"${subject}\",\"predicate\":\"${predicate}\",\"object\":\"${object}\",\"metadata_ref\":\"${metadata_ref}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
}
start_daemon() {
"${AMDUATD_BIN}" --root "${root}" --sock "${sock}" --store-backend index --space "${space_id}" \
>"${log_file}" 2>&1 &
pid=$!
if ! wait_for_ready; then
echo "daemon not ready" >&2
exit 1
fi
}
restart_daemon() {
if [[ -n "${pid:-}" ]]; then
kill "${pid}" >/dev/null 2>&1 || true
wait "${pid}" >/dev/null 2>&1 || true
fi
start_daemon
}
mkdir -p "${root}"
"${ASL_BIN}" init --root "${root}"
start_daemon
ref_a="$(create_artifact "payload-a")"
ref_b="$(create_artifact "payload-b")"
ref_c="$(create_artifact "payload-c")"
ref_d="$(create_artifact "payload-d")"
ref_e="$(create_artifact "payload-e")"
ref_e2="$(create_artifact "payload-e2")"
ref_v1="$(create_artifact "payload-v1")"
ref_v2="$(create_artifact "payload-v2")"
ref_p1="$(create_artifact "payload-provenance-1")"
ref_p2="$(create_artifact "payload-provenance-2")"
create_node "gq-a" "${ref_a}"
create_node "gq-b" "${ref_b}"
create_node "gq-c" "${ref_c}"
create_node "gq-d" "${ref_d}"
create_node "gq-e" "${ref_e}"
create_node "gq-v" "${ref_v1}"
create_node "gq-prov1" "${ref_p1}"
create_node "gq-prov2" "${ref_p2}"
# Seed path and neighbor data in a controlled edge order.
create_edge "gq-a" "ms.within_domain" "gq-b"
create_edge "gq-b" "ms.within_domain" "gq-c"
create_edge "gq-a" "ms.computed_by" "gq-b"
cutoff_resp="$(
http_get "/v2/graph/changes?event_types[]=edge_appended&limit=100" \
--header "X-Amduat-Space: ${space_id}"
)"
cutoff_cursor="$(printf '%s\n' "${cutoff_resp}" | extract_json_string "next_cursor")"
if [[ -z "${cutoff_cursor}" ]]; then
echo "failed to parse cutoff cursor: ${cutoff_resp}" >&2
exit 1
fi
cutoff_cursor="$(cursor_plus_one "${cutoff_cursor}")"
create_edge "gq-a" "ms.computed_by" "gq-d"
create_edge "gq-a" "ms.within_domain" "gq-c"
search_page1="$(
http_get "/v2/graph/search?name_prefix=gq-&limit=2" \
--header "X-Amduat-Space: ${space_id}"
)"
search_cursor="$(printf '%s\n' "${search_page1}" | extract_json_string "next_cursor")"
if [[ -z "${search_cursor}" ]]; then
echo "missing search cursor in page1: ${search_page1}" >&2
exit 1
fi
search_page2="$(
http_get "/v2/graph/search?name_prefix=gq-&limit=10&cursor=${search_cursor}" \
--header "X-Amduat-Space: ${space_id}"
)"
search_joined="${search_page1} ${search_page2}"
echo "${search_joined}" | grep -q '"name":"gq-a"' || { echo "search missing gq-a" >&2; exit 1; }
echo "${search_joined}" | grep -q '"name":"gq-b"' || { echo "search missing gq-b" >&2; exit 1; }
echo "${search_joined}" | grep -q '"name":"gq-c"' || { echo "search missing gq-c" >&2; exit 1; }
echo "${search_joined}" | grep -q '"name":"gq-d"' || { echo "search missing gq-d" >&2; exit 1; }
neighbors_page1="$(
http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&limit=1&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
neighbors_cursor="$(printf '%s\n' "${neighbors_page1}" | extract_json_string "next_cursor")"
if [[ -z "${neighbors_cursor}" ]]; then
echo "missing neighbors cursor in page1: ${neighbors_page1}" >&2
exit 1
fi
neighbors_page2="$(
http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&limit=10&cursor=${neighbors_cursor}&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
neighbors_joined="${neighbors_page1} ${neighbors_page2}"
echo "${neighbors_joined}" | grep -q '"neighbor_name":"gq-b"' || { echo "neighbors missing gq-b" >&2; exit 1; }
echo "${neighbors_joined}" | grep -q '"neighbor_name":"gq-d"' || { echo "neighbors missing gq-d" >&2; exit 1; }
neighbors_asof="$(
http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&limit=10&as_of=${cutoff_cursor}&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${neighbors_asof}" | grep -q '"neighbor_name":"gq-b"' || { echo "neighbors as_of missing gq-b" >&2; exit 1; }
if echo "${neighbors_asof}" | grep -q '"neighbor_name":"gq-d"'; then
echo "neighbors as_of unexpectedly includes gq-d" >&2
exit 1
fi
paths_latest="$(
http_get "/v2/graph/paths?from=gq-a&to=gq-c&predicate=ms.within_domain&max_depth=4&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${paths_latest}" | grep -q '"depth":1' || {
echo "paths latest expected direct depth 1: ${paths_latest}" >&2
exit 1
}
paths_asof="$(
http_get "/v2/graph/paths?from=gq-a&to=gq-c&predicate=ms.within_domain&max_depth=4&as_of=${cutoff_cursor}&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${paths_asof}" | grep -q '"depth":2' || {
echo "paths as_of expected historical depth 2: ${paths_asof}" >&2
exit 1
}
subgraph_page1="$(
http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&limit_edges=1&limit_nodes=10&include_versions=true" \
--header "X-Amduat-Space: ${space_id}"
)"
subgraph_cursor="$(printf '%s\n' "${subgraph_page1}" | extract_json_string "next_cursor")"
if [[ -z "${subgraph_cursor}" ]]; then
echo "missing subgraph cursor in page1: ${subgraph_page1}" >&2
exit 1
fi
subgraph_page2="$(
http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&limit_edges=10&limit_nodes=10&cursor=${subgraph_cursor}" \
--header "X-Amduat-Space: ${space_id}"
)"
subgraph_joined="${subgraph_page1} ${subgraph_page2}"
echo "${subgraph_joined}" | grep -q '"name":"gq-a"' || { echo "subgraph missing root node gq-a" >&2; exit 1; }
echo "${subgraph_joined}" | grep -q '"name":"gq-b"' || { echo "subgraph missing gq-b" >&2; exit 1; }
echo "${subgraph_joined}" | grep -q '"name":"gq-d"' || { echo "subgraph missing gq-d" >&2; exit 1; }
echo "${subgraph_joined}" | grep -q '"versions":\[' || { echo "subgraph include_versions missing versions" >&2; exit 1; }
subgraph_asof="$(
http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&as_of=${cutoff_cursor}&limit_edges=10&limit_nodes=10" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${subgraph_asof}" | grep -q '"name":"gq-b"' || { echo "subgraph as_of missing gq-b" >&2; exit 1; }
if echo "${subgraph_asof}" | grep -q '"name":"gq-d"'; then
echo "subgraph as_of unexpectedly includes gq-d" >&2
exit 1
fi
gqd_edge_resp="$(
http_get "/v2/graph/edges?subject=gq-a&predicate=ms.computed_by&object=gq-d&dir=outgoing&limit=1" \
--header "X-Amduat-Space: ${space_id}"
)"
gqd_edge_ref="$(printf '%s\n' "${gqd_edge_resp}" | extract_json_string "edge_ref")"
if [[ -z "${gqd_edge_ref}" ]]; then
echo "failed to parse gq-a->gq-d edge ref: ${gqd_edge_resp}" >&2
exit 1
fi
http_post "/v2/graph/edges/tombstone" \
"{\"edge_ref\":\"${gqd_edge_ref}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
subgraph_after_tombstone="$(
http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&limit_edges=10&limit_nodes=10" \
--header "X-Amduat-Space: ${space_id}"
)"
if echo "${subgraph_after_tombstone}" | grep -q '"name":"gq-d"'; then
echo "subgraph after tombstone unexpectedly includes gq-d: ${subgraph_after_tombstone}" >&2
exit 1
fi
subgraph_include_tombstoned="$(
http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&include_tombstoned=true&limit_edges=10&limit_nodes=10" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${subgraph_include_tombstoned}" | grep -q '"name":"gq-d"' || {
echo "subgraph include_tombstoned missing gq-d: ${subgraph_include_tombstoned}" >&2
exit 1
}
edges_include_tombstoned="$(
http_get "/v2/graph/edges?subject=gq-a&predicate=ms.computed_by&dir=outgoing&include_tombstoned=true&limit=10&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${edges_include_tombstoned}" | grep -q '"object_name":"gq-d"' || {
echo "edges include_tombstoned missing gq-d: ${edges_include_tombstoned}" >&2
exit 1
}
neighbors_after_tombstone="$(
http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&limit=10&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
if echo "${neighbors_after_tombstone}" | grep -q '"neighbor_name":"gq-d"'; then
echo "neighbors default should exclude tombstoned gq-d edge: ${neighbors_after_tombstone}" >&2
exit 1
fi
neighbors_include_tombstoned="$(
http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&include_tombstoned=true&limit=10&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${neighbors_include_tombstoned}" | grep -q '"neighbor_name":"gq-d"' || {
echo "neighbors include_tombstoned missing gq-d: ${neighbors_include_tombstoned}" >&2
exit 1
}
paths_excluding_tombstoned="$(
http_get "/v2/graph/paths?from=gq-a&to=gq-d&predicate=ms.computed_by&max_depth=2" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${paths_excluding_tombstoned}" | grep -q '"paths":\[\]' || {
echo "paths default should exclude tombstoned edge: ${paths_excluding_tombstoned}" >&2
exit 1
}
paths_include_tombstoned="$(
http_get "/v2/graph/paths?from=gq-a&to=gq-d&predicate=ms.computed_by&max_depth=2&include_tombstoned=true" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${paths_include_tombstoned}" | grep -q '"depth":1' || {
echo "paths include_tombstoned expected depth 1 path: ${paths_include_tombstoned}" >&2
exit 1
}
create_edge_with_metadata "gq-b" "ms.computed_by" "gq-d" "gq-prov1"
create_edge_with_metadata "gq-b" "ms.computed_by" "gq-a" "gq-prov2"
neighbors_provenance="$(
http_get "/v2/graph/nodes/gq-b/neighbors?dir=outgoing&predicate=ms.computed_by&provenance_ref=gq-prov1&limit=10&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${neighbors_provenance}" | grep -q '"neighbor_name":"gq-d"' || {
echo "neighbors provenance filter missing gq-d: ${neighbors_provenance}" >&2
exit 1
}
neighbors_provenance_missing="$(
http_get "/v2/graph/nodes/gq-b/neighbors?dir=outgoing&predicate=ms.computed_by&provenance_ref=gq-prov-missing&limit=10" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${neighbors_provenance_missing}" | grep -q '"neighbors":\[\]' || {
echo "neighbors unresolved provenance expected empty result: ${neighbors_provenance_missing}" >&2
exit 1
}
paths_provenance_match="$(
http_get "/v2/graph/paths?from=gq-b&to=gq-d&predicate=ms.computed_by&provenance_ref=gq-prov1&max_depth=2&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${paths_provenance_match}" | grep -q '"depth":1' || {
echo "paths provenance filter expected depth 1 path: ${paths_provenance_match}" >&2
exit 1
}
echo "${paths_provenance_match}" | grep -q '"object_name":"gq-d"' || {
echo "paths provenance filter missing gq-d: ${paths_provenance_match}" >&2
exit 1
}
paths_provenance_excluded="$(
http_get "/v2/graph/paths?from=gq-b&to=gq-a&predicate=ms.computed_by&provenance_ref=gq-prov1&max_depth=2" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${paths_provenance_excluded}" | grep -q '"paths":\[\]' || {
echo "paths provenance filter unexpectedly includes gq-b->gq-a path: ${paths_provenance_excluded}" >&2
exit 1
}
batch_resp="$(
http_post "/v2/graph/batch" \
"{\"edges\":[{\"subject\":\"gq-c\",\"predicate\":\"ms.computed_by\",\"object\":\"gq-d\",\"metadata_ref\":\"gq-prov1\"}]}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${batch_resp}" | grep -q '"ok":true' || {
echo "batch edge with metadata_ref failed: ${batch_resp}" >&2
exit 1
}
echo "${batch_resp}" | grep -q '"results":\[' || {
echo "batch response missing results array: ${batch_resp}" >&2
exit 1
}
echo "${batch_resp}" | grep -q '"status":"applied"' || {
echo "batch response missing applied item status: ${batch_resp}" >&2
exit 1
}
batch_version_provenance="$(
http_post "/v2/graph/batch" \
"{\"versions\":[{\"name\":\"gq-e\",\"ref\":\"${ref_e2}\",\"provenance\":{\"source_uri\":\"urn:test:gq-e-v2\",\"extractor\":\"graph-test\",\"confidence\":\"0.91\",\"observed_at\":1730000000000,\"ingested_at\":1730000000100,\"license\":\"test-only\",\"trace_id\":\"trace-gq-e-v2\"}}]}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${batch_version_provenance}" | grep -q '"ok":true' || {
echo "batch version with provenance failed: ${batch_version_provenance}" >&2
exit 1
}
echo "${batch_version_provenance}" | grep -q '"status":"applied"' || {
echo "batch version with provenance missing applied status: ${batch_version_provenance}" >&2
exit 1
}
batch_edge_provenance="$(
http_post "/v2/graph/batch" \
"{\"edges\":[{\"subject\":\"gq-c\",\"predicate\":\"ms.computed_by\",\"object\":\"gq-a\",\"provenance\":{\"source_uri\":\"urn:test:gq-c-a\",\"extractor\":\"graph-test\",\"confidence\":\"0.87\",\"observed_at\":1730000000200,\"ingested_at\":1730000000300,\"trace_id\":\"trace-gq-c-a\"}}]}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${batch_edge_provenance}" | grep -q '"ok":true' || {
echo "batch edge with provenance failed: ${batch_edge_provenance}" >&2
exit 1
}
echo "${batch_edge_provenance}" | grep -q '"status":"applied"' || {
echo "batch edge with provenance missing applied status: ${batch_edge_provenance}" >&2
exit 1
}
http_post "/v2/graph/nodes/gq-v/versions" \
"{\"ref\":\"${ref_v2}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
http_post "/v2/graph/nodes/gq-v/versions/tombstone" \
"{\"ref\":\"${ref_v2}\"}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" >/dev/null
gqv_after_tombstone="$(
http_get "/v2/graph/nodes/gq-v" \
--header "X-Amduat-Space: ${space_id}"
)"
gqv_latest="$(printf '%s\n' "${gqv_after_tombstone}" | extract_json_string "latest_ref")"
if [[ "${gqv_latest}" != "${ref_v1}" ]]; then
echo "version tombstone expected latest_ref=${ref_v1}, got ${gqv_latest}: ${gqv_after_tombstone}" >&2
exit 1
fi
if echo "${gqv_after_tombstone}" | grep -q "\"ref\":\"${ref_v2}\""; then
echo "version tombstone expected ${ref_v2} hidden by default: ${gqv_after_tombstone}" >&2
exit 1
fi
gqv_include_tombstoned="$(
http_get "/v2/graph/nodes/gq-v?include_tombstoned=true" \
--header "X-Amduat-Space: ${space_id}"
)"
gqv_latest_all="$(printf '%s\n' "${gqv_include_tombstoned}" | extract_json_string "latest_ref")"
if [[ "${gqv_latest_all}" != "${ref_v2}" ]]; then
echo "include_tombstoned expected latest_ref=${ref_v2}, got ${gqv_latest_all}: ${gqv_include_tombstoned}" >&2
exit 1
fi
echo "${gqv_include_tombstoned}" | grep -q "\"ref\":\"${ref_v2}\"" || {
echo "include_tombstoned expected historical version ${ref_v2}: ${gqv_include_tombstoned}" >&2
exit 1
}
history_default="$(
http_get "/v2/graph/history/gq-v" \
--header "X-Amduat-Space: ${space_id}"
)"
history_default_latest="$(printf '%s\n' "${history_default}" | extract_json_string "latest_ref")"
if [[ "${history_default_latest}" != "${ref_v1}" ]]; then
echo "history default expected latest_ref=${ref_v1}, got ${history_default_latest}: ${history_default}" >&2
exit 1
fi
if echo "${history_default}" | grep -q "\"ref\":\"${ref_v2}\""; then
echo "history default expected tombstoned version ${ref_v2} hidden: ${history_default}" >&2
exit 1
fi
history_include_tombstoned="$(
http_get "/v2/graph/history/gq-v?include_tombstoned=true" \
--header "X-Amduat-Space: ${space_id}"
)"
history_all_latest="$(printf '%s\n' "${history_include_tombstoned}" | extract_json_string "latest_ref")"
if [[ "${history_all_latest}" != "${ref_v2}" ]]; then
echo "history include_tombstoned expected latest_ref=${ref_v2}, got ${history_all_latest}: ${history_include_tombstoned}" >&2
exit 1
fi
echo "${history_include_tombstoned}" | grep -q "\"ref\":\"${ref_v2}\"" || {
echo "history include_tombstoned expected tombstoned version ${ref_v2}: ${history_include_tombstoned}" >&2
exit 1
}
batch_continue="$(
http_post "/v2/graph/batch" \
"{\"mode\":\"continue_on_error\",\"edges\":[{\"subject\":\"gq-a\",\"predicate\":\"ms.computed_by\",\"object\":\"gq-b\"},{\"subject\":\"gq-missing\",\"predicate\":\"ms.computed_by\",\"object\":\"gq-b\"}]}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${batch_continue}" | grep -q '"ok":false' || {
echo "batch continue_on_error expected ok=false: ${batch_continue}" >&2
exit 1
}
echo "${batch_continue}" | grep -q '"mode":"continue_on_error"' || {
echo "batch continue_on_error mode echo missing: ${batch_continue}" >&2
exit 1
}
echo "${batch_continue}" | grep -q '"status":"applied"' || {
echo "batch continue_on_error missing applied result: ${batch_continue}" >&2
exit 1
}
echo "${batch_continue}" | grep -q '"status":"error"' || {
echo "batch continue_on_error missing error result: ${batch_continue}" >&2
exit 1
}
idem_payload='{"idempotency_key":"gq-idem-1","mode":"continue_on_error","edges":[{"subject":"gq-b","predicate":"ms.computed_by","object":"gq-c"},{"subject":"gq-nope","predicate":"ms.computed_by","object":"gq-c"}]}'
idem_first="$(
http_post "/v2/graph/batch" \
"${idem_payload}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
idem_second="$(
http_post "/v2/graph/batch" \
"${idem_payload}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
if [[ "${idem_first}" != "${idem_second}" ]]; then
echo "idempotency replay response mismatch" >&2
echo "first=${idem_first}" >&2
echo "second=${idem_second}" >&2
exit 1
fi
restart_daemon
idem_third="$(
http_post "/v2/graph/batch" \
"${idem_payload}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
if [[ "${idem_first}" != "${idem_third}" ]]; then
echo "idempotency replay after restart mismatch" >&2
echo "first=${idem_first}" >&2
echo "third=${idem_third}" >&2
exit 1
fi
query_include_versions="$(
http_post "/v2/graph/query" \
"{\"where\":{\"subject\":\"gq-a\"},\"predicates\":[\"ms.within_domain\"],\"direction\":\"outgoing\",\"include_versions\":true,\"limit\":10}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${query_include_versions}" | grep -q '"versions":\[' || {
echo "graph query include_versions missing versions: ${query_include_versions}" >&2
exit 1
}
query_with_stats="$(
http_post "/v2/graph/query" \
"{\"where\":{\"subject\":\"gq-a\"},\"predicates\":[\"ms.within_domain\"],\"direction\":\"outgoing\",\"include_stats\":true,\"max_result_bytes\":1048576,\"limit\":10}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${query_with_stats}" | grep -q '"stats":{' || {
echo "graph query include_stats missing stats block: ${query_with_stats}" >&2
exit 1
}
echo "${query_with_stats}" | grep -q '"plan":"' || {
echo "graph query include_stats missing plan: ${query_with_stats}" >&2
exit 1
}
query_provenance="$(
http_post "/v2/graph/query" \
"{\"where\":{\"subject\":\"gq-b\",\"provenance_ref\":\"gq-prov1\"},\"predicates\":[\"ms.computed_by\"],\"direction\":\"outgoing\",\"limit\":10}" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${query_provenance}" | grep -q '"name":"gq-d"' || {
echo "graph query provenance filter missing expected node gq-d: ${query_provenance}" >&2
exit 1
}
if echo "${query_provenance}" | grep -q '"name":"gq-a"'; then
echo "graph query provenance filter unexpectedly includes gq-a: ${query_provenance}" >&2
exit 1
fi
query_provenance_count="$(printf '%s\n' "${query_provenance}" | grep -o '"edge_ref":"' | wc -l | awk '{print $1}')"
if [[ "${query_provenance_count}" != "1" ]]; then
echo "graph query provenance expected exactly one edge, got ${query_provenance_count}: ${query_provenance}" >&2
exit 1
fi
edges_provenance="$(
http_get "/v2/graph/edges?subject=gq-b&predicate=ms.computed_by&dir=outgoing&provenance_ref=gq-prov1&limit=10&expand_names=true" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${edges_provenance}" | grep -q '"object_name":"gq-d"' || {
echo "graph edges provenance filter missing gq-d: ${edges_provenance}" >&2
exit 1
}
if echo "${edges_provenance}" | grep -q '"object_name":"gq-a"'; then
echo "graph edges provenance filter unexpectedly includes gq-a: ${edges_provenance}" >&2
exit 1
fi
edges_with_stats="$(
http_get "/v2/graph/edges?subject=gq-b&predicate=ms.computed_by&dir=outgoing&include_stats=true&max_result_bytes=1048576&limit=10" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${edges_with_stats}" | grep -q '"stats":{' || {
echo "graph edges include_stats missing stats block: ${edges_with_stats}" >&2
exit 1
}
echo "${edges_with_stats}" | grep -q '"plan":"' || {
echo "graph edges include_stats missing plan: ${edges_with_stats}" >&2
exit 1
}
subgraph_provenance="$(
http_get "/v2/graph/subgraph?roots[]=gq-b&max_depth=1&predicates[]=ms.computed_by&dir=outgoing&provenance_ref=gq-prov1&limit_edges=10&limit_nodes=10" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${subgraph_provenance}" | grep -q '"name":"gq-d"' || {
echo "subgraph provenance filter missing gq-d: ${subgraph_provenance}" >&2
exit 1
}
if echo "${subgraph_provenance}" | grep -q '"name":"gq-a"'; then
echo "subgraph provenance filter unexpectedly includes gq-a: ${subgraph_provenance}" >&2
exit 1
fi
subgraph_with_stats="$(
http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&max_fanout=4096&include_stats=true&max_result_bytes=1048576&limit_edges=10&limit_nodes=10" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${subgraph_with_stats}" | grep -q '"stats":{' || {
echo "subgraph include_stats missing stats block: ${subgraph_with_stats}" >&2
exit 1
}
echo "${subgraph_with_stats}" | grep -q '"plan":"' || {
echo "subgraph include_stats missing plan: ${subgraph_with_stats}" >&2
exit 1
}
paths_with_stats="$(
http_get "/v2/graph/paths?from=gq-a&to=gq-c&predicate=ms.within_domain&max_depth=4&include_stats=true&max_fanout=4096&max_result_bytes=1048576" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${paths_with_stats}" | grep -q '"stats":{' || {
echo "paths include_stats missing stats block: ${paths_with_stats}" >&2
exit 1
}
echo "${paths_with_stats}" | grep -q '"plan":"' || {
echo "paths include_stats missing plan: ${paths_with_stats}" >&2
exit 1
}
gqb_node="$(
http_get "/v2/graph/nodes/gq-b" \
--header "X-Amduat-Space: ${space_id}"
)"
gqb_ref="$(printf '%s\n' "${gqb_node}" | extract_json_string "concept_ref")"
if [[ -z "${gqb_ref}" ]]; then
echo "failed to resolve gq-b concept ref: ${gqb_node}" >&2
exit 1
fi
changes_tombstone="$(
http_get "/v2/graph/changes?event_types[]=tombstone_applied&limit=100" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${changes_tombstone}" | grep -q '"event":"tombstone_applied"' || {
echo "changes tombstone filter missing tombstone event: ${changes_tombstone}" >&2
exit 1
}
changes_filtered="$(
http_get "/v2/graph/changes?since_as_of=${cutoff_cursor}&predicates[]=ms.computed_by&roots[]=gq-b&limit=100" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${changes_filtered}" | grep -q "\"${gqb_ref}\"" || {
echo "changes root/predicate filter missing gq-b involvement: ${changes_filtered}" >&2
exit 1
}
if echo "${changes_filtered}" | grep -q '"event":"version_published"'; then
echo "changes predicate filter unexpectedly includes version events: ${changes_filtered}" >&2
exit 1
fi
changes_wait_empty="$(
http_get "/v2/graph/changes?since_cursor=g1_999999&wait_ms=1&limit=1" \
--header "X-Amduat-Space: ${space_id}"
)"
echo "${changes_wait_empty}" | grep -q '"events":\[\]' || {
echo "changes wait_ms empty poll expected no events: ${changes_wait_empty}" >&2
exit 1
}
echo "ok: v2 graph query tests passed"

112
scripts/test_index_two_nodes.sh Executable file
View file

@ -0,0 +1,112 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMPDIR="${TMPDIR:-/tmp}"
mkdir -p "${TMPDIR}"
AMDUATD_BIN="${ROOT_DIR}/build/amduatd"
ASL_BIN="${ROOT_DIR}/build/vendor/amduat/amduat-asl"
HTTP_HELPER="${ROOT_DIR}/build/amduatd_http_unix"
USE_HTTP_HELPER=0
if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then
echo "missing binaries; build amduatd and amduat-asl first" >&2
exit 1
fi
if command -v curl >/dev/null 2>&1 && curl --help 2>/dev/null | grep -q -- '--unix-socket'; then
USE_HTTP_HELPER=0
else
USE_HTTP_HELPER=1
fi
if [[ "${USE_HTTP_HELPER}" -eq 1 && ! -x "${HTTP_HELPER}" ]]; then
echo "skip: curl lacks --unix-socket and helper missing" >&2
exit 77
fi
tmp_root="$(mktemp -d -p "${TMPDIR}" amduatd-index-two-nodes-XXXXXX)"
root="${tmp_root}/root"
sock="${tmp_root}/amduatd.sock"
space_id="app1"
log_file="${tmp_root}/amduatd.log"
cleanup() {
if [[ -n "${pid:-}" ]]; then
kill "${pid}" >/dev/null 2>&1 || true
wait "${pid}" >/dev/null 2>&1 || true
fi
rm -rf "${tmp_root}"
}
trap cleanup EXIT
wait_ready() {
local i
for i in $(seq 1 100); do
if ! kill -0 "${pid}" >/dev/null 2>&1; then
[[ -f "${log_file}" ]] && cat "${log_file}" >&2
return 1
fi
if [[ ! -S "${sock}" ]]; then
sleep 0.1
continue
fi
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
if "${HTTP_HELPER}" --sock "${sock}" --method GET --path "/v2/readyz" >/dev/null 2>&1; then
return 0
fi
elif curl --silent --show-error --fail --unix-socket "${sock}" \
"http://localhost/v2/readyz" >/dev/null 2>&1; then
return 0
fi
sleep 0.1
done
return 1
}
post_node() {
local name="$1"
local payload="{\"name\":\"${name}\"}"
if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then
"${HTTP_HELPER}" --sock "${sock}" --method POST --path "/v2/graph/nodes" \
--header "Content-Type: application/json" \
--header "X-Amduat-Space: ${space_id}" \
--data "${payload}"
else
curl --silent --show-error --fail \
--unix-socket "${sock}" \
-H "Content-Type: application/json" \
-H "X-Amduat-Space: ${space_id}" \
-X POST --data-binary "${payload}" \
"http://localhost/v2/graph/nodes"
fi
}
mkdir -p "${root}"
"${ASL_BIN}" index init --root "${root}" --force >/dev/null
"${AMDUATD_BIN}" --root "${root}" --sock "${sock}" --store-backend index --space "${space_id}" \
>"${log_file}" 2>&1 &
pid=$!
if ! wait_ready; then
echo "daemon not ready" >&2
exit 1
fi
resp1="$(post_node doca1)"
resp2="$(post_node topica1)"
echo "${resp1}" | grep -q '"name":"doca1"' || {
echo "first node write failed: ${resp1}" >&2
[[ -f "${log_file}" ]] && cat "${log_file}" >&2
exit 1
}
echo "${resp2}" | grep -q '"name":"topica1"' || {
echo "second node write failed: ${resp2}" >&2
[[ -f "${log_file}" ]] && cat "${log_file}" >&2
exit 1
}
echo "ok: two consecutive index-backed node writes succeeded"

75
src/amduat_pel_gc.c Normal file
View file

@ -0,0 +1,75 @@
#include "asl_gc_fs.h"
#include "amduat/util/log.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void amduat_pel_gc_usage(const char *argv0) {
fprintf(stderr,
"Usage: %s gc --root <root> [--keep-materializations] [--delete] [--dry-run]\n",
argv0);
}
int main(int argc, char **argv) {
const char *root = NULL;
bool keep_materializations = false;
bool delete_artifacts = false;
bool dry_run = true;
amduat_asl_gc_fs_options_t opts;
amduat_asl_gc_fs_stats_t stats;
if (argc < 2 || strcmp(argv[1], "gc") != 0) {
amduat_pel_gc_usage(argv[0]);
return 2;
}
for (int i = 2; i < argc; ++i) {
if (strcmp(argv[i], "--root") == 0) {
if (i + 1 >= argc) {
amduat_pel_gc_usage(argv[0]);
return 2;
}
root = argv[++i];
} else if (strcmp(argv[i], "--keep-materializations") == 0) {
keep_materializations = true;
} else if (strcmp(argv[i], "--delete") == 0) {
delete_artifacts = true;
dry_run = false;
} else if (strcmp(argv[i], "--dry-run") == 0) {
dry_run = true;
delete_artifacts = false;
} else if (strcmp(argv[i], "--help") == 0 ||
strcmp(argv[i], "-h") == 0) {
amduat_pel_gc_usage(argv[0]);
return 0;
} else {
amduat_pel_gc_usage(argv[0]);
return 2;
}
}
if (root == NULL) {
amduat_pel_gc_usage(argv[0]);
return 2;
}
opts.keep_materializations = keep_materializations;
opts.delete_artifacts = delete_artifacts;
opts.dry_run = dry_run;
if (!amduat_asl_gc_fs_run(root, &opts, &stats)) {
amduat_log(AMDUAT_LOG_ERROR, "gc failed");
return 1;
}
printf("pointer_roots=%zu\n", stats.pointer_roots);
printf("materialization_roots=%zu\n", stats.materialization_roots);
printf("marked_artifacts=%zu\n", stats.marked_artifacts);
printf("candidates=%zu\n", stats.candidates);
printf("candidate_bytes=%llu\n",
(unsigned long long)stats.candidate_bytes);
printf("mode=%s\n", delete_artifacts ? "delete" : "dry-run");
return 0;
}

File diff suppressed because it is too large Load diff

1928
src/amduatd_caps.c Normal file

File diff suppressed because it is too large Load diff

56
src/amduatd_caps.h Normal file
View file

@ -0,0 +1,56 @@
#ifndef AMDUATD_CAPS_H
#define AMDUATD_CAPS_H
#include "amduatd_ui.h"
#include "amduatd_space.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct amduatd_cfg_t {
amduatd_space_t space;
uint64_t edges_refresh_ms;
bool derivation_index_enabled;
bool derivation_index_strict;
} amduatd_cfg_t;
typedef enum {
AMDUATD_REF_OK = 0,
AMDUATD_REF_ERR_INVALID = 1,
AMDUATD_REF_ERR_NOT_FOUND = 2,
AMDUATD_REF_ERR_INTERNAL = 3
} amduatd_ref_status_t;
typedef struct amduatd_caps_t {
bool enabled;
bool enable_cap_reads;
amduat_octets_t hmac_key;
} amduatd_caps_t;
bool amduatd_caps_init(amduatd_caps_t *caps,
const amduatd_cfg_t *cfg,
const char *root_path);
void amduatd_caps_free(amduatd_caps_t *caps);
bool amduatd_caps_can_handle(const amduatd_http_req_t *req);
bool amduatd_caps_handle(amduatd_ctx_t *ctx,
const amduatd_http_req_t *req,
amduatd_http_resp_t *resp);
bool amduatd_caps_check_space(const amduatd_caps_t *caps,
const amduatd_cfg_t *dcfg,
const amduatd_http_req_t *req,
const char **out_reason);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUATD_CAPS_H */

16425
src/amduatd_concepts.c Normal file

File diff suppressed because it is too large Load diff

108
src/amduatd_concepts.h Normal file
View file

@ -0,0 +1,108 @@
#ifndef AMDUATD_CONCEPTS_H
#define AMDUATD_CONCEPTS_H
#include "amduat/asl/collection.h"
#include "amduatd_space.h"
#include "amduatd_ui.h"
#include <stdbool.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
amduat_reference_t record_ref;
amduat_reference_t src_ref;
amduat_reference_t dst_ref;
char *rel;
} amduatd_edge_entry_t;
typedef struct {
amduatd_edge_entry_t *items;
size_t len;
size_t cap;
} amduatd_edge_list_t;
typedef struct {
amduat_reference_t ref;
size_t *edge_indices;
size_t len;
size_t cap;
} amduatd_ref_edge_bucket_t;
typedef struct {
amduat_reference_t left_ref;
amduat_reference_t right_ref;
size_t *edge_indices;
size_t len;
size_t cap;
} amduatd_ref_pair_edge_bucket_t;
typedef struct {
size_t built_for_edges_len;
size_t *alias_edge_indices;
size_t alias_len;
size_t alias_cap;
amduatd_ref_edge_bucket_t *src_buckets;
size_t src_len;
size_t src_cap;
amduatd_ref_edge_bucket_t *dst_buckets;
size_t dst_len;
size_t dst_cap;
amduatd_ref_edge_bucket_t *predicate_buckets;
size_t predicate_len;
size_t predicate_cap;
amduatd_ref_pair_edge_bucket_t *src_predicate_buckets;
size_t src_predicate_len;
size_t src_predicate_cap;
amduatd_ref_pair_edge_bucket_t *dst_predicate_buckets;
size_t dst_predicate_len;
size_t dst_predicate_cap;
amduatd_ref_edge_bucket_t *tombstoned_src_buckets;
size_t tombstoned_src_len;
size_t tombstoned_src_cap;
} amduatd_query_index_t;
typedef struct amduatd_concepts_t {
const char *root_path;
char edges_path[1024];
char *edge_collection_name;
amduat_reference_t rel_aliases_ref;
amduat_reference_t rel_materializes_ref;
amduat_reference_t rel_represents_ref;
amduat_reference_t rel_requires_key_ref;
amduat_reference_t rel_within_domain_ref;
amduat_reference_t rel_computed_by_ref;
amduat_reference_t rel_has_provenance_ref;
amduat_reference_t rel_tombstones_ref;
amduat_asl_collection_store_t edge_collection;
amduatd_edge_list_t edges;
amduatd_query_index_t qindex;
} amduatd_concepts_t;
bool amduatd_concepts_init(amduatd_concepts_t *c,
amduat_asl_store_t *store,
const amduatd_space_t *space,
const char *root_path,
bool enable_migrations);
void amduatd_concepts_free(amduatd_concepts_t *c);
bool amduatd_concepts_refresh_edges(amduatd_ctx_t *ctx,
size_t max_new_entries);
bool amduatd_concepts_ensure_query_index_ready(amduatd_concepts_t *c);
bool amduatd_concepts_can_handle(const amduatd_http_req_t *req);
bool amduatd_concepts_handle(amduatd_ctx_t *ctx,
const amduatd_http_req_t *req,
amduatd_http_resp_t *resp);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUATD_CONCEPTS_H */

View file

@ -0,0 +1,197 @@
#include "amduatd_derivation_index.h"
#include "amduat/asl/asl_derivation_index_fs.h"
#include "amduat/pel/core.h"
#include "amduat/pel/derivation_sid.h"
#include <string.h>
/* Reserve output_index values for non-output artifacts (result/trace/receipt). */
enum {
AMDUATD_DERIVATION_OUTPUT_INDEX_RESULT = UINT32_MAX,
AMDUATD_DERIVATION_OUTPUT_INDEX_TRACE = UINT32_MAX - 1u,
AMDUATD_DERIVATION_OUTPUT_INDEX_RECEIPT = UINT32_MAX - 2u
};
static bool amduatd_derivation_index_add_record(
amduat_asl_derivation_index_fs_t *index,
amduat_octets_t sid,
amduat_reference_t program_ref,
const amduat_reference_t *input_refs,
size_t input_refs_len,
bool has_params_ref,
amduat_reference_t params_ref,
amduat_reference_t artifact_ref,
uint32_t output_index,
amduat_asl_store_error_t *out_err) {
amduat_asl_derivation_record_t record;
amduat_octets_t record_sid = amduat_octets(NULL, 0u);
amduat_asl_store_error_t err;
if (!amduat_octets_clone(sid, &record_sid)) {
if (out_err != NULL) {
*out_err = AMDUAT_ASL_STORE_ERR_INTEGRITY;
}
return false;
}
memset(&record, 0, sizeof(record));
record.sid = record_sid;
record.program_ref = program_ref;
record.output_index = output_index;
record.input_refs = (amduat_reference_t *)input_refs;
record.input_refs_len = input_refs_len;
record.has_params_ref = has_params_ref;
if (has_params_ref) {
record.params_ref = params_ref;
}
record.has_exec_profile = false;
record.exec_profile = amduat_octets(NULL, 0u);
err = amduat_asl_derivation_index_fs_add(index, artifact_ref, &record);
amduat_octets_free(&record.sid);
if (err != AMDUAT_ASL_STORE_OK) {
if (out_err != NULL) {
*out_err = err;
}
return false;
}
return true;
}
bool amduatd_derivation_index_pel_run(const char *root_path,
bool enabled,
amduat_reference_t program_ref,
const amduat_reference_t *input_refs,
size_t input_refs_len,
bool has_params_ref,
amduat_reference_t params_ref,
const amduat_pel_run_result_t *run_result,
bool has_receipt_ref,
amduat_reference_t receipt_ref,
amduat_asl_store_error_t *out_err) {
amduat_asl_derivation_index_fs_t index;
amduat_pel_derivation_sid_input_t sid_input;
amduat_octets_t sid = amduat_octets(NULL, 0u);
size_t i;
if (out_err != NULL) {
*out_err = AMDUAT_ASL_STORE_OK;
}
if (!enabled) {
return true;
}
if (root_path == NULL || root_path[0] == '\0' || run_result == NULL) {
if (out_err != NULL) {
*out_err = AMDUAT_ASL_STORE_ERR_INTEGRITY;
}
return false;
}
if (!run_result->has_result_value) {
return true;
}
if (run_result->result_value.has_store_failure) {
return true;
}
if (run_result->result_value.core_result.status != AMDUAT_PEL_EXEC_STATUS_OK) {
return true;
}
if (run_result->output_refs_len > UINT32_MAX) {
if (out_err != NULL) {
*out_err = AMDUAT_ASL_STORE_ERR_INTEGRITY;
}
return false;
}
if (!amduat_asl_derivation_index_fs_init(&index, root_path)) {
if (out_err != NULL) {
*out_err = AMDUAT_ASL_STORE_ERR_IO;
}
return false;
}
memset(&sid_input, 0, sizeof(sid_input));
sid_input.program_ref = program_ref;
sid_input.input_refs = input_refs;
sid_input.input_refs_len = input_refs_len;
sid_input.has_params_ref = has_params_ref;
if (has_params_ref) {
sid_input.params_ref = params_ref;
}
sid_input.has_exec_profile = false;
sid_input.exec_profile = amduat_octets(NULL, 0u);
if (!amduat_pel_derivation_sid_compute(&sid_input, &sid)) {
if (out_err != NULL) {
*out_err = AMDUAT_ASL_STORE_ERR_INTEGRITY;
}
return false;
}
for (i = 0u; i < run_result->output_refs_len; ++i) {
if (!amduatd_derivation_index_add_record(&index,
sid,
program_ref,
input_refs,
input_refs_len,
has_params_ref,
params_ref,
run_result->output_refs[i],
(uint32_t)i,
out_err)) {
amduat_octets_free(&sid);
return false;
}
}
if (!amduatd_derivation_index_add_record(&index,
sid,
program_ref,
input_refs,
input_refs_len,
has_params_ref,
params_ref,
run_result->result_ref,
AMDUATD_DERIVATION_OUTPUT_INDEX_RESULT,
out_err)) {
amduat_octets_free(&sid);
return false;
}
if (run_result->result_value.has_trace_ref) {
if (!amduatd_derivation_index_add_record(
&index,
sid,
program_ref,
input_refs,
input_refs_len,
has_params_ref,
params_ref,
run_result->result_value.trace_ref,
AMDUATD_DERIVATION_OUTPUT_INDEX_TRACE,
out_err)) {
amduat_octets_free(&sid);
return false;
}
}
if (has_receipt_ref) {
if (!amduatd_derivation_index_add_record(
&index,
sid,
program_ref,
input_refs,
input_refs_len,
has_params_ref,
params_ref,
receipt_ref,
AMDUATD_DERIVATION_OUTPUT_INDEX_RECEIPT,
out_err)) {
amduat_octets_free(&sid);
return false;
}
}
amduat_octets_free(&sid);
return true;
}

View file

@ -0,0 +1,32 @@
#ifndef AMDUATD_DERIVATION_INDEX_H
#define AMDUATD_DERIVATION_INDEX_H
#include "amduat/asl/core.h"
#include "amduat/asl/store.h"
#include "amduat/pel/run.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
bool amduatd_derivation_index_pel_run(const char *root_path,
bool enabled,
amduat_reference_t program_ref,
const amduat_reference_t *input_refs,
size_t input_refs_len,
bool has_params_ref,
amduat_reference_t params_ref,
const amduat_pel_run_result_t *run_result,
bool has_receipt_ref,
amduat_reference_t receipt_ref,
amduat_asl_store_error_t *out_err);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUATD_DERIVATION_INDEX_H */

236
src/amduatd_fed.c Normal file
View file

@ -0,0 +1,236 @@
#include "amduatd_fed.h"
#include "amduat/asl/ref_text.h"
#include <errno.h>
#include <stdlib.h>
#include <string.h>
static bool amduatd_fed_parse_u32(const char *s, uint32_t *out) {
unsigned long val;
char *endp = NULL;
if (s == NULL || out == NULL) {
return false;
}
errno = 0;
val = strtoul(s, &endp, 10);
if (errno != 0 || endp == s || *endp != '\0' || val > UINT32_MAX) {
return false;
}
*out = (uint32_t)val;
return true;
}
void amduatd_fed_cfg_init(amduatd_fed_cfg_t *cfg) {
if (cfg == NULL) {
return;
}
memset(cfg, 0, sizeof(*cfg));
cfg->transport_kind = AMDUATD_FED_TRANSPORT_STUB;
cfg->registry_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
}
void amduatd_fed_cfg_free(amduatd_fed_cfg_t *cfg) {
if (cfg == NULL) {
return;
}
if (cfg->registry_ref_set) {
amduat_reference_free(&cfg->registry_ref);
cfg->registry_ref_set = false;
}
amduatd_fed_cfg_init(cfg);
}
const char *amduatd_fed_transport_name(amduatd_fed_transport_kind_t kind) {
switch (kind) {
case AMDUATD_FED_TRANSPORT_STUB:
return "stub";
case AMDUATD_FED_TRANSPORT_UNIX:
return "unix";
default:
return "unknown";
}
}
amduatd_fed_parse_result_t amduatd_fed_cfg_parse_arg(amduatd_fed_cfg_t *cfg,
int argc,
char **argv,
int *io_index,
const char **out_err) {
const char *arg = NULL;
const char *value = NULL;
if (out_err != NULL) {
*out_err = NULL;
}
if (cfg == NULL || argv == NULL || io_index == NULL || argc <= 0) {
if (out_err != NULL) {
*out_err = "invalid fed config parse inputs";
}
return AMDUATD_FED_PARSE_ERROR;
}
if (*io_index < 0 || *io_index >= argc) {
if (out_err != NULL) {
*out_err = "fed config parse index out of range";
}
return AMDUATD_FED_PARSE_ERROR;
}
arg = argv[*io_index];
if (strcmp(arg, "--fed-enable") == 0) {
cfg->enabled = true;
return AMDUATD_FED_PARSE_OK;
}
if (strcmp(arg, "--fed-require-space") == 0) {
cfg->require_space = true;
return AMDUATD_FED_PARSE_OK;
}
if (strcmp(arg, "--fed-transport") == 0) {
if (*io_index + 1 >= argc) {
if (out_err != NULL) {
*out_err = "--fed-transport requires a value";
}
return AMDUATD_FED_PARSE_ERROR;
}
value = argv[++(*io_index)];
if (strcmp(value, "stub") == 0) {
cfg->transport_kind = AMDUATD_FED_TRANSPORT_STUB;
return AMDUATD_FED_PARSE_OK;
}
if (strcmp(value, "unix") == 0) {
cfg->transport_kind = AMDUATD_FED_TRANSPORT_UNIX;
return AMDUATD_FED_PARSE_OK;
}
if (out_err != NULL) {
*out_err = "invalid --fed-transport";
}
return AMDUATD_FED_PARSE_ERROR;
}
if (strcmp(arg, "--fed-unix-sock") == 0) {
size_t len;
if (*io_index + 1 >= argc) {
if (out_err != NULL) {
*out_err = "--fed-unix-sock requires a path";
}
return AMDUATD_FED_PARSE_ERROR;
}
value = argv[++(*io_index)];
len = strlen(value);
if (len == 0 || len >= sizeof(cfg->unix_socket_path)) {
if (out_err != NULL) {
*out_err = "invalid --fed-unix-sock";
}
return AMDUATD_FED_PARSE_ERROR;
}
memset(cfg->unix_socket_path, 0, sizeof(cfg->unix_socket_path));
memcpy(cfg->unix_socket_path, value, len);
cfg->unix_socket_set = true;
return AMDUATD_FED_PARSE_OK;
}
if (strcmp(arg, "--fed-domain-id") == 0) {
uint32_t domain_id = 0;
if (*io_index + 1 >= argc) {
if (out_err != NULL) {
*out_err = "--fed-domain-id requires a value";
}
return AMDUATD_FED_PARSE_ERROR;
}
value = argv[++(*io_index)];
if (!amduatd_fed_parse_u32(value, &domain_id)) {
if (out_err != NULL) {
*out_err = "invalid --fed-domain-id";
}
return AMDUATD_FED_PARSE_ERROR;
}
cfg->local_domain_id = domain_id;
return AMDUATD_FED_PARSE_OK;
}
if (strcmp(arg, "--fed-registry-ref") == 0) {
amduat_reference_t ref;
if (*io_index + 1 >= argc) {
if (out_err != NULL) {
*out_err = "--fed-registry-ref requires a value";
}
return AMDUATD_FED_PARSE_ERROR;
}
value = argv[++(*io_index)];
memset(&ref, 0, sizeof(ref));
if (!amduat_asl_ref_decode_hex(value, &ref)) {
if (out_err != NULL) {
*out_err = "invalid --fed-registry-ref";
}
return AMDUATD_FED_PARSE_ERROR;
}
if (cfg->registry_ref_set) {
amduat_reference_free(&cfg->registry_ref);
}
cfg->registry_ref = ref;
cfg->registry_ref_set = true;
return AMDUATD_FED_PARSE_OK;
}
return AMDUATD_FED_PARSE_NOT_HANDLED;
}
bool amduatd_fed_requirements_check(amduatd_store_backend_t backend,
const amduatd_fed_cfg_t *cfg,
const char **out_err) {
if (out_err != NULL) {
*out_err = NULL;
}
if (cfg == NULL) {
if (out_err != NULL) {
*out_err = "missing fed config";
}
return false;
}
if (!cfg->enabled) {
return true;
}
if (backend != AMDUATD_STORE_BACKEND_INDEX) {
if (out_err != NULL) {
*out_err = "federation requires --store-backend index";
}
return false;
}
if (cfg->transport_kind == AMDUATD_FED_TRANSPORT_UNIX &&
!cfg->unix_socket_set) {
if (out_err != NULL) {
*out_err = "unix transport requires --fed-unix-sock";
}
return false;
}
return true;
}
bool amduatd_fed_scope_names(const amduatd_fed_cfg_t *cfg,
const amduatd_space_t *space,
const char *name,
amduat_octets_t *out_scoped,
const char **out_err) {
if (out_err != NULL) {
*out_err = NULL;
}
if (out_scoped != NULL) {
*out_scoped = amduat_octets(NULL, 0u);
}
if (cfg == NULL || name == NULL || out_scoped == NULL) {
if (out_err != NULL) {
*out_err = "invalid fed scope inputs";
}
return false;
}
if (cfg->require_space && (space == NULL || !space->enabled)) {
if (out_err != NULL) {
*out_err = "missing X-Amduat-Space";
}
return false;
}
if (!amduatd_space_scope_name(space, name, out_scoped)) {
if (out_err != NULL) {
*out_err = "failed to scope name";
}
return false;
}
return true;
}

70
src/amduatd_fed.h Normal file
View file

@ -0,0 +1,70 @@
#ifndef AMDUATD_FED_H
#define AMDUATD_FED_H
#include "amduatd_space.h"
#include "amduatd_store.h"
#include "federation/transport_unix.h"
#include "amduat/asl/core.h"
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
AMDUATD_FED_TRANSPORT_STUB = 0,
AMDUATD_FED_TRANSPORT_UNIX = 1
} amduatd_fed_transport_kind_t;
enum {
AMDUATD_FED_LOG_KIND_ARTIFACT = 1u,
AMDUATD_FED_LOG_KIND_TOMBSTONE = 2u
};
typedef struct {
bool enabled;
bool require_space;
amduatd_fed_transport_kind_t transport_kind;
uint32_t local_domain_id;
bool registry_ref_set;
amduat_reference_t registry_ref;
bool unix_socket_set;
char unix_socket_path[AMDUAT_FED_TRANSPORT_UNIX_PATH_MAX];
} amduatd_fed_cfg_t;
typedef enum {
AMDUATD_FED_PARSE_OK = 0,
AMDUATD_FED_PARSE_NOT_HANDLED = 1,
AMDUATD_FED_PARSE_ERROR = 2
} amduatd_fed_parse_result_t;
void amduatd_fed_cfg_init(amduatd_fed_cfg_t *cfg);
void amduatd_fed_cfg_free(amduatd_fed_cfg_t *cfg);
const char *amduatd_fed_transport_name(amduatd_fed_transport_kind_t kind);
amduatd_fed_parse_result_t amduatd_fed_cfg_parse_arg(amduatd_fed_cfg_t *cfg,
int argc,
char **argv,
int *io_index,
const char **out_err);
bool amduatd_fed_requirements_check(amduatd_store_backend_t backend,
const amduatd_fed_cfg_t *cfg,
const char **out_err);
bool amduatd_fed_scope_names(const amduatd_fed_cfg_t *cfg,
const amduatd_space_t *space,
const char *name,
amduat_octets_t *out_scoped,
const char **out_err);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUATD_FED_H */

854
src/amduatd_fed_cursor.c Normal file
View file

@ -0,0 +1,854 @@
#include "amduatd_fed_cursor.h"
#include "amduat/asl/record.h"
#include "amduat/enc/asl1_core_codec.h"
#include "amduatd_space.h"
#include <stdlib.h>
#include <string.h>
enum {
AMDUATD_FED_CURSOR_MAGIC_LEN = 8,
AMDUATD_FED_CURSOR_VERSION = 1
};
static const uint8_t k_amduatd_fed_cursor_magic[AMDUATD_FED_CURSOR_MAGIC_LEN] = {
'A', 'F', 'C', 'U', 'R', '1', '\0', '\0'
};
enum {
AMDUATD_FED_CURSOR_FLAG_HAS_LOGSEQ = 1u << 0,
AMDUATD_FED_CURSOR_FLAG_HAS_RECORD_REF = 1u << 1,
AMDUATD_FED_CURSOR_FLAG_HAS_SPACE = 1u << 2
};
static void amduatd_fed_cursor_store_u32_le(uint8_t *out, uint32_t value) {
out[0] = (uint8_t)(value & 0xffu);
out[1] = (uint8_t)((value >> 8) & 0xffu);
out[2] = (uint8_t)((value >> 16) & 0xffu);
out[3] = (uint8_t)((value >> 24) & 0xffu);
}
static void amduatd_fed_cursor_store_u64_le(uint8_t *out, uint64_t value) {
out[0] = (uint8_t)(value & 0xffu);
out[1] = (uint8_t)((value >> 8) & 0xffu);
out[2] = (uint8_t)((value >> 16) & 0xffu);
out[3] = (uint8_t)((value >> 24) & 0xffu);
out[4] = (uint8_t)((value >> 32) & 0xffu);
out[5] = (uint8_t)((value >> 40) & 0xffu);
out[6] = (uint8_t)((value >> 48) & 0xffu);
out[7] = (uint8_t)((value >> 56) & 0xffu);
}
static bool amduatd_fed_cursor_read_u32_le(const uint8_t *data,
size_t len,
size_t *offset,
uint32_t *out) {
if (len - *offset < 4u) {
return false;
}
*out = (uint32_t)data[*offset] |
((uint32_t)data[*offset + 1u] << 8) |
((uint32_t)data[*offset + 2u] << 16) |
((uint32_t)data[*offset + 3u] << 24);
*offset += 4u;
return true;
}
static bool amduatd_fed_cursor_read_u64_le(const uint8_t *data,
size_t len,
size_t *offset,
uint64_t *out) {
if (len - *offset < 8u) {
return false;
}
*out = (uint64_t)data[*offset] |
((uint64_t)data[*offset + 1u] << 8) |
((uint64_t)data[*offset + 2u] << 16) |
((uint64_t)data[*offset + 3u] << 24) |
((uint64_t)data[*offset + 4u] << 32) |
((uint64_t)data[*offset + 5u] << 40) |
((uint64_t)data[*offset + 6u] << 48) |
((uint64_t)data[*offset + 7u] << 56);
*offset += 8u;
return true;
}
static bool amduatd_fed_cursor_add_size(size_t *acc, size_t add) {
if (*acc > SIZE_MAX - add) {
return false;
}
*acc += add;
return true;
}
static bool amduatd_fed_cursor_strdup(const char *s, size_t len, char **out) {
char *buf;
if (out == NULL) {
return false;
}
*out = NULL;
if (s == NULL || len == 0u) {
return false;
}
if (len > SIZE_MAX - 1u) {
return false;
}
buf = (char *)malloc(len + 1u);
if (buf == NULL) {
return false;
}
memcpy(buf, s, len);
buf[len] = '\0';
*out = buf;
return true;
}
static bool amduatd_fed_cursor_peer_key_is_valid(const char *peer_key) {
if (peer_key == NULL || peer_key[0] == '\0') {
return false;
}
return amduat_asl_pointer_name_is_valid(peer_key);
}
static bool amduatd_fed_cursor_remote_space_id_is_valid(
const char *remote_space_id) {
return amduatd_space_space_id_is_valid(remote_space_id);
}
static bool amduatd_fed_cursor_pointer_name_with_prefix(
const amduatd_space_t *space,
const char *peer_key,
const char *prefix,
amduat_octets_t *out_name) {
const char suffix[] = "/head";
size_t peer_len;
size_t total_len;
size_t prefix_len;
char *base = NULL;
bool ok;
if (out_name != NULL) {
*out_name = amduat_octets(NULL, 0u);
}
if (out_name == NULL || prefix == NULL ||
!amduatd_fed_cursor_peer_key_is_valid(peer_key)) {
return false;
}
peer_len = strlen(peer_key);
prefix_len = strlen(prefix);
if (peer_len > SIZE_MAX - prefix_len - (sizeof(suffix) - 1u)) {
return false;
}
total_len = prefix_len + peer_len + (sizeof(suffix) - 1u);
base = (char *)malloc(total_len + 1u);
if (base == NULL) {
return false;
}
memcpy(base, prefix, prefix_len);
memcpy(base + prefix_len, peer_key, peer_len);
memcpy(base + prefix_len + peer_len, suffix, sizeof(suffix) - 1u);
base[total_len] = '\0';
ok = amduatd_space_scope_name(space, base, out_name);
free(base);
return ok;
}
static bool amduatd_fed_cursor_pointer_name_with_prefix_v2(
const amduatd_space_t *space,
const char *peer_key,
const char *remote_space_id,
const char *prefix,
amduat_octets_t *out_name) {
const char suffix[] = "/head";
size_t peer_len;
size_t remote_len;
size_t total_len;
size_t prefix_len;
char *base = NULL;
bool ok;
if (out_name != NULL) {
*out_name = amduat_octets(NULL, 0u);
}
if (out_name == NULL || prefix == NULL ||
!amduatd_fed_cursor_peer_key_is_valid(peer_key) ||
!amduatd_fed_cursor_remote_space_id_is_valid(remote_space_id)) {
return false;
}
peer_len = strlen(peer_key);
remote_len = strlen(remote_space_id);
prefix_len = strlen(prefix);
if (peer_len > SIZE_MAX - prefix_len - 1u) {
return false;
}
if (remote_len > SIZE_MAX - prefix_len - peer_len - 1u -
(sizeof(suffix) - 1u)) {
return false;
}
total_len = prefix_len + peer_len + 1u + remote_len + (sizeof(suffix) - 1u);
base = (char *)malloc(total_len + 1u);
if (base == NULL) {
return false;
}
memcpy(base, prefix, prefix_len);
memcpy(base + prefix_len, peer_key, peer_len);
base[prefix_len + peer_len] = '/';
memcpy(base + prefix_len + peer_len + 1u, remote_space_id, remote_len);
memcpy(base + prefix_len + peer_len + 1u + remote_len, suffix,
sizeof(suffix) - 1u);
base[total_len] = '\0';
ok = amduatd_space_scope_name(space, base, out_name);
free(base);
return ok;
}
static bool amduatd_fed_cursor_record_encode(
const amduatd_fed_cursor_record_t *record,
amduat_octets_t *out_payload) {
size_t total = 0u;
uint32_t peer_len = 0u;
uint32_t space_len = 0u;
uint32_t flags = 0u;
amduat_octets_t ref_bytes = amduat_octets(NULL, 0u);
uint8_t *buf = NULL;
size_t offset = 0u;
if (out_payload != NULL) {
*out_payload = amduat_octets(NULL, 0u);
}
if (record == NULL || out_payload == NULL) {
return false;
}
if (record->peer_key == NULL || record->peer_key[0] == '\0') {
return false;
}
peer_len = (uint32_t)strlen(record->peer_key);
if (record->space_id != NULL && record->space_id[0] != '\0') {
space_len = (uint32_t)strlen(record->space_id);
flags |= AMDUATD_FED_CURSOR_FLAG_HAS_SPACE;
}
if (record->has_logseq) {
flags |= AMDUATD_FED_CURSOR_FLAG_HAS_LOGSEQ;
}
if (record->has_record_ref) {
if (record->last_record_ref.digest.data == NULL ||
record->last_record_ref.digest.len == 0u) {
return false;
}
if (!amduat_enc_asl1_core_encode_reference_v1(record->last_record_ref,
&ref_bytes)) {
return false;
}
flags |= AMDUATD_FED_CURSOR_FLAG_HAS_RECORD_REF;
}
if (!amduatd_fed_cursor_add_size(&total, AMDUATD_FED_CURSOR_MAGIC_LEN) ||
!amduatd_fed_cursor_add_size(&total, 4u + 4u + 4u + 4u) ||
!amduatd_fed_cursor_add_size(&total, peer_len) ||
!amduatd_fed_cursor_add_size(&total, space_len) ||
(record->has_logseq && !amduatd_fed_cursor_add_size(&total, 8u)) ||
(record->has_record_ref &&
!amduatd_fed_cursor_add_size(&total, 4u + ref_bytes.len))) {
amduat_octets_free(&ref_bytes);
return false;
}
buf = (uint8_t *)malloc(total);
if (buf == NULL) {
amduat_octets_free(&ref_bytes);
return false;
}
memcpy(buf + offset,
k_amduatd_fed_cursor_magic,
AMDUATD_FED_CURSOR_MAGIC_LEN);
offset += AMDUATD_FED_CURSOR_MAGIC_LEN;
amduatd_fed_cursor_store_u32_le(buf + offset,
(uint32_t)AMDUATD_FED_CURSOR_VERSION);
offset += 4u;
amduatd_fed_cursor_store_u32_le(buf + offset, flags);
offset += 4u;
amduatd_fed_cursor_store_u32_le(buf + offset, peer_len);
offset += 4u;
amduatd_fed_cursor_store_u32_le(buf + offset, space_len);
offset += 4u;
memcpy(buf + offset, record->peer_key, peer_len);
offset += peer_len;
if (space_len != 0u) {
memcpy(buf + offset, record->space_id, space_len);
offset += space_len;
}
if (record->has_logseq) {
amduatd_fed_cursor_store_u64_le(buf + offset, record->last_logseq);
offset += 8u;
}
if (record->has_record_ref) {
amduatd_fed_cursor_store_u32_le(buf + offset, (uint32_t)ref_bytes.len);
offset += 4u;
memcpy(buf + offset, ref_bytes.data, ref_bytes.len);
offset += ref_bytes.len;
}
amduat_octets_free(&ref_bytes);
*out_payload = amduat_octets(buf, total);
return true;
}
static bool amduatd_fed_cursor_record_decode(
amduat_octets_t payload,
amduatd_fed_cursor_record_t *out_record) {
size_t offset = 0u;
uint32_t version = 0u;
uint32_t flags = 0u;
uint32_t peer_len = 0u;
uint32_t space_len = 0u;
bool has_logseq = false;
bool has_record_ref = false;
bool has_space = false;
char *peer_key = NULL;
char *space_id = NULL;
amduat_reference_t record_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
if (out_record == NULL) {
return false;
}
amduatd_fed_cursor_record_init(out_record);
if (payload.len < AMDUATD_FED_CURSOR_MAGIC_LEN + 16u) {
return false;
}
if (memcmp(payload.data,
k_amduatd_fed_cursor_magic,
AMDUATD_FED_CURSOR_MAGIC_LEN) != 0) {
return false;
}
offset = AMDUATD_FED_CURSOR_MAGIC_LEN;
if (!amduatd_fed_cursor_read_u32_le(payload.data,
payload.len,
&offset,
&version) ||
version != AMDUATD_FED_CURSOR_VERSION ||
!amduatd_fed_cursor_read_u32_le(payload.data,
payload.len,
&offset,
&flags) ||
!amduatd_fed_cursor_read_u32_le(payload.data,
payload.len,
&offset,
&peer_len) ||
!amduatd_fed_cursor_read_u32_le(payload.data,
payload.len,
&offset,
&space_len)) {
return false;
}
has_logseq = (flags & AMDUATD_FED_CURSOR_FLAG_HAS_LOGSEQ) != 0u;
has_record_ref = (flags & AMDUATD_FED_CURSOR_FLAG_HAS_RECORD_REF) != 0u;
has_space = (flags & AMDUATD_FED_CURSOR_FLAG_HAS_SPACE) != 0u;
if (peer_len == 0u || payload.len - offset < peer_len) {
return false;
}
if (!amduatd_fed_cursor_strdup((const char *)payload.data + offset,
peer_len,
&peer_key)) {
return false;
}
offset += peer_len;
if ((space_len != 0u) != has_space) {
free(peer_key);
return false;
}
if (space_len != 0u) {
if (payload.len - offset < space_len) {
free(peer_key);
return false;
}
if (!amduatd_fed_cursor_strdup((const char *)payload.data + offset,
space_len,
&space_id)) {
free(peer_key);
return false;
}
offset += space_len;
}
if (has_logseq) {
if (!amduatd_fed_cursor_read_u64_le(payload.data,
payload.len,
&offset,
&out_record->last_logseq)) {
free(peer_key);
free(space_id);
return false;
}
out_record->has_logseq = true;
}
if (has_record_ref) {
uint32_t ref_len = 0u;
amduat_octets_t ref_bytes;
if (!amduatd_fed_cursor_read_u32_le(payload.data,
payload.len,
&offset,
&ref_len) ||
payload.len - offset < ref_len) {
free(peer_key);
free(space_id);
return false;
}
ref_bytes = amduat_octets(payload.data + offset, ref_len);
offset += ref_len;
if (!amduat_enc_asl1_core_decode_reference_v1(ref_bytes, &record_ref)) {
free(peer_key);
free(space_id);
return false;
}
out_record->last_record_ref = record_ref;
out_record->has_record_ref = true;
}
if (offset != payload.len) {
if (out_record->has_record_ref) {
amduat_reference_free(&out_record->last_record_ref);
}
free(peer_key);
free(space_id);
return false;
}
out_record->peer_key = peer_key;
out_record->space_id = space_id;
return true;
}
void amduatd_fed_cursor_record_init(amduatd_fed_cursor_record_t *record) {
if (record == NULL) {
return;
}
memset(record, 0, sizeof(*record));
record->last_record_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
}
void amduatd_fed_cursor_record_free(amduatd_fed_cursor_record_t *record) {
if (record == NULL) {
return;
}
free(record->peer_key);
free(record->space_id);
record->peer_key = NULL;
record->space_id = NULL;
if (record->has_record_ref) {
amduat_reference_free(&record->last_record_ref);
}
memset(record, 0, sizeof(*record));
}
bool amduatd_fed_cursor_pointer_name(const amduatd_space_t *space,
const char *peer_key,
amduat_octets_t *out_name) {
return amduatd_fed_cursor_pointer_name_with_prefix(space,
peer_key,
"fed/cursor/",
out_name);
}
bool amduatd_fed_push_cursor_pointer_name(const amduatd_space_t *space,
const char *peer_key,
amduat_octets_t *out_name) {
return amduatd_fed_cursor_pointer_name_with_prefix(space,
peer_key,
"fed/push_cursor/",
out_name);
}
bool amduatd_fed_cursor_pointer_name_v2(const amduatd_space_t *space,
const char *peer_key,
const char *remote_space_id,
amduat_octets_t *out_name) {
return amduatd_fed_cursor_pointer_name_with_prefix_v2(space,
peer_key,
remote_space_id,
"fed/cursor/",
out_name);
}
bool amduatd_fed_push_cursor_pointer_name_v2(const amduatd_space_t *space,
const char *peer_key,
const char *remote_space_id,
amduat_octets_t *out_name) {
return amduatd_fed_cursor_pointer_name_with_prefix_v2(space,
peer_key,
remote_space_id,
"fed/push_cursor/",
out_name);
}
amduatd_fed_cursor_status_t amduatd_fed_cursor_check_enabled(
const amduatd_fed_cfg_t *cfg) {
if (cfg == NULL) {
return AMDUATD_FED_CURSOR_ERR_INVALID;
}
if (!cfg->enabled) {
return AMDUATD_FED_CURSOR_ERR_DISABLED;
}
return AMDUATD_FED_CURSOR_OK;
}
static amduatd_fed_cursor_status_t amduatd_fed_cursor_get_with_prefix(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
const char *prefix,
amduatd_fed_cursor_record_t *out_cursor,
amduat_reference_t *out_ref) {
amduat_octets_t pointer_name = amduat_octets(NULL, 0u);
amduat_reference_t pointer_ref;
amduat_asl_pointer_error_t perr;
bool exists = false;
amduat_asl_record_t record;
amduat_asl_store_error_t store_err;
if (out_ref != NULL) {
*out_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
}
if (store == NULL || pointer_store == NULL || out_cursor == NULL ||
peer_key == NULL) {
return AMDUATD_FED_CURSOR_ERR_INVALID;
}
if (remote_space_id != NULL && remote_space_id[0] != '\0') {
if (!amduatd_fed_cursor_pointer_name_with_prefix_v2(effective_space,
peer_key,
remote_space_id,
prefix,
&pointer_name)) {
return AMDUATD_FED_CURSOR_ERR_INVALID;
}
} else if (!amduatd_fed_cursor_pointer_name_with_prefix(effective_space,
peer_key,
prefix,
&pointer_name)) {
return AMDUATD_FED_CURSOR_ERR_INVALID;
}
memset(&pointer_ref, 0, sizeof(pointer_ref));
perr = amduat_asl_pointer_get(pointer_store,
(const char *)pointer_name.data,
&exists,
&pointer_ref);
amduat_octets_free(&pointer_name);
if (perr != AMDUAT_ASL_POINTER_OK) {
return AMDUATD_FED_CURSOR_ERR_POINTER;
}
if (!exists) {
return AMDUATD_FED_CURSOR_ERR_NOT_FOUND;
}
memset(&record, 0, sizeof(record));
store_err = amduat_asl_record_store_get(store, pointer_ref, &record);
if (store_err != AMDUAT_ASL_STORE_OK) {
amduat_reference_free(&pointer_ref);
return AMDUATD_FED_CURSOR_ERR_STORE;
}
if (record.schema.len != strlen("fed/cursor") ||
memcmp(record.schema.data, "fed/cursor", record.schema.len) != 0) {
amduat_asl_record_free(&record);
amduat_reference_free(&pointer_ref);
return AMDUATD_FED_CURSOR_ERR_CODEC;
}
if (!amduatd_fed_cursor_record_decode(record.payload, out_cursor)) {
amduat_asl_record_free(&record);
amduat_reference_free(&pointer_ref);
return AMDUATD_FED_CURSOR_ERR_CODEC;
}
amduat_asl_record_free(&record);
if (strcmp(out_cursor->peer_key, peer_key) != 0) {
amduatd_fed_cursor_record_free(out_cursor);
amduat_reference_free(&pointer_ref);
return AMDUATD_FED_CURSOR_ERR_CODEC;
}
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 (out_cursor->space_id == NULL ||
strcmp(out_cursor->space_id, space_id) != 0) {
amduatd_fed_cursor_record_free(out_cursor);
amduat_reference_free(&pointer_ref);
return AMDUATD_FED_CURSOR_ERR_CODEC;
}
} else if (out_cursor->space_id != NULL && out_cursor->space_id[0] != '\0') {
amduatd_fed_cursor_record_free(out_cursor);
amduat_reference_free(&pointer_ref);
return AMDUATD_FED_CURSOR_ERR_CODEC;
}
if (out_ref != NULL) {
if (!amduat_reference_clone(pointer_ref, out_ref)) {
amduatd_fed_cursor_record_free(out_cursor);
amduat_reference_free(&pointer_ref);
return AMDUATD_FED_CURSOR_ERR_STORE;
}
}
amduat_reference_free(&pointer_ref);
return AMDUATD_FED_CURSOR_OK;
}
amduatd_fed_cursor_status_t amduatd_fed_cursor_get(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
amduatd_fed_cursor_record_t *out_cursor,
amduat_reference_t *out_ref) {
return amduatd_fed_cursor_get_with_prefix(store,
pointer_store,
effective_space,
peer_key,
NULL,
"fed/cursor/",
out_cursor,
out_ref);
}
amduatd_fed_cursor_status_t amduatd_fed_cursor_get_remote(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
amduatd_fed_cursor_record_t *out_cursor,
amduat_reference_t *out_ref) {
return amduatd_fed_cursor_get_with_prefix(store,
pointer_store,
effective_space,
peer_key,
remote_space_id,
"fed/cursor/",
out_cursor,
out_ref);
}
amduatd_fed_cursor_status_t amduatd_fed_push_cursor_get(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
amduatd_fed_cursor_record_t *out_cursor,
amduat_reference_t *out_ref) {
return amduatd_fed_cursor_get_with_prefix(store,
pointer_store,
effective_space,
peer_key,
NULL,
"fed/push_cursor/",
out_cursor,
out_ref);
}
amduatd_fed_cursor_status_t amduatd_fed_push_cursor_get_remote(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
amduatd_fed_cursor_record_t *out_cursor,
amduat_reference_t *out_ref) {
return amduatd_fed_cursor_get_with_prefix(store,
pointer_store,
effective_space,
peer_key,
remote_space_id,
"fed/push_cursor/",
out_cursor,
out_ref);
}
static amduatd_fed_cursor_status_t amduatd_fed_cursor_cas_set_with_prefix(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
const char *prefix,
const amduat_reference_t *expected_ref,
const amduatd_fed_cursor_record_t *new_cursor,
amduat_reference_t *out_new_ref) {
amduat_octets_t pointer_name = amduat_octets(NULL, 0u);
amduat_octets_t payload = amduat_octets(NULL, 0u);
amduat_reference_t record_ref;
amduat_asl_store_error_t store_err;
amduat_asl_pointer_error_t perr;
bool swapped = false;
if (out_new_ref != NULL) {
*out_new_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
}
if (store == NULL || pointer_store == NULL || peer_key == NULL ||
new_cursor == NULL) {
return AMDUATD_FED_CURSOR_ERR_INVALID;
}
if (new_cursor->peer_key == NULL ||
strcmp(new_cursor->peer_key, peer_key) != 0) {
return AMDUATD_FED_CURSOR_ERR_INVALID;
}
if (!new_cursor->has_logseq && !new_cursor->has_record_ref) {
return AMDUATD_FED_CURSOR_ERR_INVALID;
}
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 (new_cursor->space_id == NULL ||
strcmp(new_cursor->space_id, space_id) != 0) {
return AMDUATD_FED_CURSOR_ERR_INVALID;
}
} else if (new_cursor->space_id != NULL && new_cursor->space_id[0] != '\0') {
return AMDUATD_FED_CURSOR_ERR_INVALID;
}
if (remote_space_id != NULL && remote_space_id[0] != '\0') {
if (!amduatd_fed_cursor_pointer_name_with_prefix_v2(effective_space,
peer_key,
remote_space_id,
prefix,
&pointer_name)) {
return AMDUATD_FED_CURSOR_ERR_INVALID;
}
} else if (!amduatd_fed_cursor_pointer_name_with_prefix(effective_space,
peer_key,
prefix,
&pointer_name)) {
return AMDUATD_FED_CURSOR_ERR_INVALID;
}
if (!amduatd_fed_cursor_record_encode(new_cursor, &payload)) {
amduat_octets_free(&pointer_name);
return AMDUATD_FED_CURSOR_ERR_CODEC;
}
memset(&record_ref, 0, sizeof(record_ref));
store_err = amduat_asl_record_store_put(store,
amduat_octets("fed/cursor",
strlen("fed/cursor")),
payload,
&record_ref);
amduat_octets_free(&payload);
if (store_err != AMDUAT_ASL_STORE_OK) {
amduat_octets_free(&pointer_name);
return AMDUATD_FED_CURSOR_ERR_STORE;
}
perr = amduat_asl_pointer_cas(pointer_store,
(const char *)pointer_name.data,
expected_ref != NULL,
expected_ref,
&record_ref,
&swapped);
amduat_octets_free(&pointer_name);
if (perr != AMDUAT_ASL_POINTER_OK) {
amduat_reference_free(&record_ref);
return AMDUATD_FED_CURSOR_ERR_POINTER;
}
if (!swapped) {
amduat_reference_free(&record_ref);
return AMDUATD_FED_CURSOR_ERR_CONFLICT;
}
if (out_new_ref != NULL) {
if (!amduat_reference_clone(record_ref, out_new_ref)) {
amduat_reference_free(&record_ref);
return AMDUATD_FED_CURSOR_ERR_STORE;
}
}
amduat_reference_free(&record_ref);
return AMDUATD_FED_CURSOR_OK;
}
amduatd_fed_cursor_status_t amduatd_fed_cursor_cas_set(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const amduat_reference_t *expected_ref,
const amduatd_fed_cursor_record_t *new_cursor,
amduat_reference_t *out_new_ref) {
return amduatd_fed_cursor_cas_set_with_prefix(store,
pointer_store,
effective_space,
peer_key,
NULL,
"fed/cursor/",
expected_ref,
new_cursor,
out_new_ref);
}
amduatd_fed_cursor_status_t amduatd_fed_cursor_cas_set_remote(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
const amduat_reference_t *expected_ref,
const amduatd_fed_cursor_record_t *new_cursor,
amduat_reference_t *out_new_ref) {
return amduatd_fed_cursor_cas_set_with_prefix(store,
pointer_store,
effective_space,
peer_key,
remote_space_id,
"fed/cursor/",
expected_ref,
new_cursor,
out_new_ref);
}
amduatd_fed_cursor_status_t amduatd_fed_push_cursor_cas_set(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const amduat_reference_t *expected_ref,
const amduatd_fed_cursor_record_t *new_cursor,
amduat_reference_t *out_new_ref) {
return amduatd_fed_cursor_cas_set_with_prefix(store,
pointer_store,
effective_space,
peer_key,
NULL,
"fed/push_cursor/",
expected_ref,
new_cursor,
out_new_ref);
}
amduatd_fed_cursor_status_t amduatd_fed_push_cursor_cas_set_remote(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
const amduat_reference_t *expected_ref,
const amduatd_fed_cursor_record_t *new_cursor,
amduat_reference_t *out_new_ref) {
return amduatd_fed_cursor_cas_set_with_prefix(store,
pointer_store,
effective_space,
peer_key,
remote_space_id,
"fed/push_cursor/",
expected_ref,
new_cursor,
out_new_ref);
}

138
src/amduatd_fed_cursor.h Normal file
View file

@ -0,0 +1,138 @@
#ifndef AMDUATD_FED_CURSOR_H
#define AMDUATD_FED_CURSOR_H
#include "amduat/asl/asl_pointer_fs.h"
#include "amduat/asl/core.h"
#include "amduat/asl/store.h"
#include "amduatd_fed.h"
#include "amduatd_space.h"
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
AMDUATD_FED_CURSOR_OK = 0,
AMDUATD_FED_CURSOR_ERR_INVALID = 1,
AMDUATD_FED_CURSOR_ERR_NOT_FOUND = 2,
AMDUATD_FED_CURSOR_ERR_POINTER = 3,
AMDUATD_FED_CURSOR_ERR_STORE = 4,
AMDUATD_FED_CURSOR_ERR_CODEC = 5,
AMDUATD_FED_CURSOR_ERR_CONFLICT = 6,
AMDUATD_FED_CURSOR_ERR_DISABLED = 7
} amduatd_fed_cursor_status_t;
typedef struct {
char *peer_key;
char *space_id;
bool has_logseq;
uint64_t last_logseq;
bool has_record_ref;
amduat_reference_t last_record_ref;
} amduatd_fed_cursor_record_t;
void amduatd_fed_cursor_record_init(amduatd_fed_cursor_record_t *record);
void amduatd_fed_cursor_record_free(amduatd_fed_cursor_record_t *record);
bool amduatd_fed_cursor_pointer_name(const amduatd_space_t *space,
const char *peer_key,
amduat_octets_t *out_name);
bool amduatd_fed_push_cursor_pointer_name(const amduatd_space_t *space,
const char *peer_key,
amduat_octets_t *out_name);
bool amduatd_fed_cursor_pointer_name_v2(const amduatd_space_t *space,
const char *peer_key,
const char *remote_space_id,
amduat_octets_t *out_name);
bool amduatd_fed_push_cursor_pointer_name_v2(const amduatd_space_t *space,
const char *peer_key,
const char *remote_space_id,
amduat_octets_t *out_name);
amduatd_fed_cursor_status_t amduatd_fed_cursor_check_enabled(
const amduatd_fed_cfg_t *cfg);
amduatd_fed_cursor_status_t amduatd_fed_cursor_get(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
amduatd_fed_cursor_record_t *out_cursor,
amduat_reference_t *out_ref);
amduatd_fed_cursor_status_t amduatd_fed_cursor_get_remote(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
amduatd_fed_cursor_record_t *out_cursor,
amduat_reference_t *out_ref);
amduatd_fed_cursor_status_t amduatd_fed_push_cursor_get(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
amduatd_fed_cursor_record_t *out_cursor,
amduat_reference_t *out_ref);
amduatd_fed_cursor_status_t amduatd_fed_push_cursor_get_remote(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
amduatd_fed_cursor_record_t *out_cursor,
amduat_reference_t *out_ref);
amduatd_fed_cursor_status_t amduatd_fed_cursor_cas_set(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const amduat_reference_t *expected_ref,
const amduatd_fed_cursor_record_t *new_cursor,
amduat_reference_t *out_new_ref);
amduatd_fed_cursor_status_t amduatd_fed_cursor_cas_set_remote(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
const amduat_reference_t *expected_ref,
const amduatd_fed_cursor_record_t *new_cursor,
amduat_reference_t *out_new_ref);
amduatd_fed_cursor_status_t amduatd_fed_push_cursor_cas_set(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const amduat_reference_t *expected_ref,
const amduatd_fed_cursor_record_t *new_cursor,
amduat_reference_t *out_new_ref);
amduatd_fed_cursor_status_t amduatd_fed_push_cursor_cas_set_remote(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
const amduat_reference_t *expected_ref,
const amduatd_fed_cursor_record_t *new_cursor,
amduat_reference_t *out_new_ref);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUATD_FED_CURSOR_H */

View file

@ -0,0 +1,530 @@
#include "amduatd_fed_pull_apply.h"
#include "amduat/asl/artifact_io.h"
#include "amduat/asl/store.h"
#include "amduat/enc/fer1_receipt.h"
#include "amduat/enc/tgk1_edge.h"
#include "amduat/fed/ingest.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static bool amduatd_fed_pull_parse_u32(const char *s, uint32_t *out) {
char *end = NULL;
unsigned long val;
if (s == NULL || out == NULL || s[0] == '\0') {
return false;
}
val = strtoul(s, &end, 10);
if (end == s || *end != '\0' || val > UINT32_MAX) {
return false;
}
*out = (uint32_t)val;
return true;
}
static bool amduatd_fed_pull_strdup(const char *s, char **out) {
size_t len;
char *buf;
if (out == NULL) {
return false;
}
*out = NULL;
if (s == NULL) {
return false;
}
len = strlen(s);
if (len > SIZE_MAX - 1u) {
return false;
}
buf = (char *)malloc(len + 1u);
if (buf == NULL) {
return false;
}
if (len != 0u) {
memcpy(buf, s, len);
}
buf[len] = '\0';
*out = buf;
return true;
}
void amduatd_fed_pull_apply_report_init(
amduatd_fed_pull_apply_report_t *report) {
if (report == NULL) {
return;
}
memset(report, 0, sizeof(*report));
report->cursor_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
report->cursor_after_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
amduatd_fed_pull_plan_candidate_init(&report->plan_candidate);
}
void amduatd_fed_pull_apply_report_free(
amduatd_fed_pull_apply_report_t *report) {
if (report == NULL) {
return;
}
if (report->cursor_ref_set) {
amduat_reference_free(&report->cursor_ref);
}
if (report->cursor_after_ref_set) {
amduat_reference_free(&report->cursor_after_ref);
}
amduatd_fed_pull_plan_candidate_free(&report->plan_candidate);
memset(report, 0, sizeof(*report));
}
static void amduatd_fed_pull_report_error(
amduatd_fed_pull_apply_report_t *report,
const char *msg) {
if (report == NULL || msg == NULL) {
return;
}
memset(report->error, 0, sizeof(report->error));
strncpy(report->error, msg, sizeof(report->error) - 1u);
}
static bool amduatd_fed_pull_apply_record(
amduat_asl_store_t *store,
const amduatd_fed_pull_transport_t *transport,
const amduat_fed_record_t *record,
size_t *io_artifact_count,
int *out_remote_status,
char *err_buf,
size_t err_cap) {
int status = 0;
amduat_octets_t bytes = amduat_octets(NULL, 0u);
amduat_artifact_t artifact;
amduat_reference_t stored_ref;
amduat_asl_index_state_t state;
amduat_asl_store_error_t store_err;
amduat_type_tag_t type_tag = amduat_type_tag(0u);
bool has_tag = false;
char *body = NULL;
if (out_remote_status != NULL) {
*out_remote_status = 0;
}
if (record->id.type == AMDUAT_FED_REC_TOMBSTONE) {
store_err = amduat_asl_store_tombstone(store,
record->id.ref,
0u,
0u,
&state);
if (store_err != AMDUAT_ASL_STORE_OK) {
snprintf(err_buf, err_cap, "tombstone failed");
return false;
}
return true;
}
if (transport == NULL || transport->get_artifact == NULL) {
snprintf(err_buf, err_cap, "missing artifact transport");
return false;
}
if (!transport->get_artifact(transport->ctx,
record->id.ref,
&status,
&bytes,
&body)) {
snprintf(err_buf, err_cap, "artifact fetch failed");
free(body);
return false;
}
if (status != 200) {
if (out_remote_status != NULL) {
*out_remote_status = status;
}
snprintf(err_buf,
err_cap,
"artifact fetch status %d",
status);
free(body);
amduat_octets_free(&bytes);
return false;
}
free(body);
if (record->id.type == AMDUAT_FED_REC_TGK_EDGE) {
type_tag = amduat_type_tag(AMDUAT_TYPE_TAG_TGK1_EDGE_V1);
has_tag = true;
} else if (record->id.type == AMDUAT_FED_REC_PER) {
type_tag = amduat_type_tag(AMDUAT_TYPE_TAG_FER1_RECEIPT_1);
has_tag = true;
}
if (!amduat_asl_artifact_from_bytes(bytes,
AMDUAT_ASL_IO_RAW,
has_tag,
type_tag,
&artifact)) {
amduat_octets_free(&bytes);
snprintf(err_buf, err_cap, "artifact decode failed");
return false;
}
bytes = amduat_octets(NULL, 0u);
store_err = amduat_asl_store_put_indexed(store,
artifact,
&stored_ref,
&state);
amduat_asl_artifact_free(&artifact);
amduat_octets_free(&bytes);
if (store_err != AMDUAT_ASL_STORE_OK) {
snprintf(err_buf, err_cap, "artifact store failed");
return false;
}
amduat_reference_free(&stored_ref);
if (io_artifact_count != NULL) {
*io_artifact_count += 1u;
}
return true;
}
amduatd_fed_pull_apply_status_t amduatd_fed_pull_apply(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
uint64_t limit,
const amduatd_fed_cfg_t *fed_cfg,
const amduatd_fed_pull_transport_t *transport,
amduatd_fed_pull_apply_report_t *out_report) {
uint32_t domain_id = 0u;
amduatd_fed_cursor_record_t cursor;
amduat_reference_t cursor_ref;
bool cursor_present = false;
amduat_fed_record_t *records = NULL;
size_t record_len = 0;
size_t record_len_total = 0;
int remote_status = 0;
char *remote_body = NULL;
amduatd_fed_cursor_candidate_t candidate;
amduatd_fed_cursor_record_t next_cursor;
amduat_reference_t next_ref;
size_t i;
size_t applied_records = 0u;
size_t applied_artifacts = 0u;
char err_buf[128];
if (out_report == NULL) {
return AMDUATD_FED_PULL_APPLY_ERR_INVALID;
}
amduatd_fed_pull_apply_report_init(out_report);
out_report->peer_key = peer_key;
out_report->effective_space = effective_space;
out_report->limit = limit;
if (store == NULL || pointer_store == NULL || peer_key == NULL ||
fed_cfg == NULL || transport == NULL) {
amduatd_fed_pull_report_error(out_report, "invalid inputs");
return AMDUATD_FED_PULL_APPLY_ERR_INVALID;
}
if (!fed_cfg->enabled) {
amduatd_fed_pull_report_error(out_report, "federation disabled");
return AMDUATD_FED_PULL_APPLY_ERR_DISABLED;
}
if (store->ops.log_scan == NULL || store->ops.current_state == NULL ||
store->ops.put_indexed == NULL || store->ops.tombstone == NULL) {
amduatd_fed_pull_report_error(out_report, "requires index backend");
return AMDUATD_FED_PULL_APPLY_ERR_UNSUPPORTED;
}
{
amduat_octets_t scoped = amduat_octets(NULL, 0u);
if (remote_space_id != NULL && remote_space_id[0] != '\0') {
if (!amduatd_fed_cursor_pointer_name_v2(effective_space,
peer_key,
remote_space_id,
&scoped)) {
amduatd_fed_pull_report_error(out_report, "invalid peer");
return AMDUATD_FED_PULL_APPLY_ERR_INVALID;
}
} else if (!amduatd_fed_cursor_pointer_name(effective_space,
peer_key,
&scoped)) {
amduatd_fed_pull_report_error(out_report, "invalid peer");
return AMDUATD_FED_PULL_APPLY_ERR_INVALID;
}
amduat_octets_free(&scoped);
}
if (!amduatd_fed_pull_parse_u32(peer_key, &domain_id)) {
amduatd_fed_pull_report_error(out_report, "invalid peer");
return AMDUATD_FED_PULL_APPLY_ERR_INVALID;
}
if (transport->get_records == NULL || transport->free_records == NULL ||
transport->get_artifact == NULL) {
amduatd_fed_pull_report_error(out_report, "transport unavailable");
return AMDUATD_FED_PULL_APPLY_ERR_UNSUPPORTED;
}
amduatd_fed_cursor_record_init(&cursor);
memset(&cursor_ref, 0, sizeof(cursor_ref));
{
amduatd_fed_cursor_status_t cursor_status;
cursor_status = amduatd_fed_cursor_get_remote(store,
pointer_store,
effective_space,
peer_key,
remote_space_id,
&cursor,
&cursor_ref);
if (cursor_status == AMDUATD_FED_CURSOR_ERR_NOT_FOUND) {
cursor_present = false;
} else if (cursor_status == AMDUATD_FED_CURSOR_OK) {
cursor_present = true;
} else {
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
amduatd_fed_pull_report_error(out_report, "cursor read failed");
return AMDUATD_FED_PULL_APPLY_ERR_STORE;
}
}
if (cursor_present && cursor.has_logseq &&
cursor.last_logseq == UINT64_MAX) {
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
amduatd_fed_pull_report_error(out_report, "cursor overflow");
return AMDUATD_FED_PULL_APPLY_ERR_INVALID;
}
if (!transport->get_records(transport->ctx,
domain_id,
cursor_present && cursor.has_logseq
? cursor.last_logseq + 1u
: 0u,
limit,
&remote_status,
&records,
&record_len,
&remote_body)) {
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
amduatd_fed_pull_report_error(out_report, "remote fetch failed");
return AMDUATD_FED_PULL_APPLY_ERR_REMOTE;
}
out_report->remote_status = remote_status;
if (remote_status != 200) {
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
if (remote_body != NULL && remote_body[0] != '\0') {
amduatd_fed_pull_report_error(out_report, remote_body);
} else {
amduatd_fed_pull_report_error(out_report, "remote error");
}
free(remote_body);
return AMDUATD_FED_PULL_APPLY_ERR_REMOTE;
}
free(remote_body);
remote_body = NULL;
record_len_total = record_len;
if (record_len > limit) {
record_len = (size_t)limit;
}
out_report->cursor_present = cursor_present;
if (cursor_present && cursor.has_logseq) {
out_report->cursor_has_logseq = true;
out_report->cursor_logseq = cursor.last_logseq;
}
if (cursor_present) {
if (amduat_reference_clone(cursor_ref, &out_report->cursor_ref)) {
out_report->cursor_ref_set = true;
}
}
if (!amduatd_fed_pull_plan_next_cursor_candidate(cursor_present ? &cursor
: NULL,
records,
record_len,
&candidate)) {
if (records != NULL) {
transport->free_records(transport->ctx, records, record_len_total);
}
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
amduatd_fed_pull_report_error(out_report, "plan candidate failed");
return AMDUATD_FED_PULL_APPLY_ERR_INVALID;
}
out_report->plan_record_count = record_len;
out_report->plan_candidate = candidate;
if (record_len == 0u) {
if (records != NULL) {
transport->free_records(transport->ctx, records, record_len_total);
}
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
return AMDUATD_FED_PULL_APPLY_OK;
}
{
size_t err_index = 0;
size_t conflict_index = 0;
amduat_fed_ingest_error_t ingest_rc;
ingest_rc = amduat_fed_ingest_validate(records,
record_len,
&err_index,
&conflict_index);
if (ingest_rc != AMDUAT_FED_INGEST_OK) {
transport->free_records(transport->ctx, records, record_len_total);
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
amduatd_fed_pull_report_error(out_report, "invalid record batch");
return AMDUATD_FED_PULL_APPLY_ERR_INVALID;
}
}
if (cursor_present && cursor.has_logseq &&
records[0].logseq <= cursor.last_logseq) {
transport->free_records(transport->ctx, records, record_len_total);
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
amduatd_fed_pull_report_error(out_report, "cursor would move backwards");
return AMDUATD_FED_PULL_APPLY_ERR_INVALID;
}
for (i = 0; i < record_len; ++i) {
int artifact_status = 0;
if (i > 0 && records[i].logseq < records[i - 1].logseq) {
transport->free_records(transport->ctx, records, record_len_total);
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
amduatd_fed_pull_report_error(out_report, "record order invalid");
return AMDUATD_FED_PULL_APPLY_ERR_INVALID;
}
memset(err_buf, 0, sizeof(err_buf));
if (!amduatd_fed_pull_apply_record(store,
transport,
&records[i],
&applied_artifacts,
&artifact_status,
err_buf,
sizeof(err_buf))) {
transport->free_records(transport->ctx, records, record_len_total);
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
applied_records = i;
out_report->applied_record_count = applied_records;
out_report->applied_artifact_count = applied_artifacts;
if (artifact_status != 0) {
out_report->remote_status = artifact_status;
}
amduatd_fed_pull_report_error(out_report, err_buf);
return AMDUATD_FED_PULL_APPLY_ERR_STORE;
}
applied_records++;
}
out_report->applied_record_count = applied_records;
out_report->applied_artifact_count = applied_artifacts;
amduatd_fed_cursor_record_init(&next_cursor);
if (!amduatd_fed_pull_strdup(peer_key, &next_cursor.peer_key)) {
transport->free_records(transport->ctx, records, record_len_total);
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
amduatd_fed_cursor_record_free(&next_cursor);
amduatd_fed_pull_report_error(out_report, "oom");
return AMDUATD_FED_PULL_APPLY_ERR_OOM;
}
if (next_cursor.peer_key == NULL) {
transport->free_records(transport->ctx, records, record_len_total);
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
amduatd_fed_pull_report_error(out_report, "oom");
return AMDUATD_FED_PULL_APPLY_ERR_OOM;
}
if (effective_space != NULL && effective_space->enabled &&
effective_space->space_id.data != NULL) {
if (!amduatd_fed_pull_strdup(
(const char *)effective_space->space_id.data,
&next_cursor.space_id)) {
transport->free_records(transport->ctx, records, record_len_total);
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
amduatd_fed_cursor_record_free(&next_cursor);
amduatd_fed_pull_report_error(out_report, "oom");
return AMDUATD_FED_PULL_APPLY_ERR_OOM;
}
if (next_cursor.space_id == NULL) {
transport->free_records(transport->ctx, records, record_len_total);
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
amduatd_fed_cursor_record_free(&next_cursor);
amduatd_fed_pull_report_error(out_report, "oom");
return AMDUATD_FED_PULL_APPLY_ERR_OOM;
}
}
if (candidate.has_logseq) {
next_cursor.has_logseq = true;
next_cursor.last_logseq = candidate.logseq;
}
if (candidate.has_ref) {
next_cursor.has_record_ref = true;
if (!amduat_reference_clone(candidate.ref,
&next_cursor.last_record_ref)) {
transport->free_records(transport->ctx, records, record_len_total);
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
amduatd_fed_cursor_record_free(&next_cursor);
amduatd_fed_pull_report_error(out_report, "oom");
return AMDUATD_FED_PULL_APPLY_ERR_OOM;
}
}
memset(&next_ref, 0, sizeof(next_ref));
{
amduatd_fed_cursor_status_t cursor_status;
cursor_status = amduatd_fed_cursor_cas_set_remote(store,
pointer_store,
effective_space,
peer_key,
remote_space_id,
cursor_present
? &cursor_ref
: NULL,
&next_cursor,
&next_ref);
if (cursor_status == AMDUATD_FED_CURSOR_ERR_CONFLICT) {
transport->free_records(transport->ctx, records, record_len_total);
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
amduatd_fed_cursor_record_free(&next_cursor);
amduat_reference_free(&next_ref);
amduatd_fed_pull_report_error(out_report, "cursor conflict");
return AMDUATD_FED_PULL_APPLY_ERR_CONFLICT;
}
if (cursor_status != AMDUATD_FED_CURSOR_OK) {
transport->free_records(transport->ctx, records, record_len_total);
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
amduatd_fed_cursor_record_free(&next_cursor);
amduat_reference_free(&next_ref);
amduatd_fed_pull_report_error(out_report, "cursor update failed");
return AMDUATD_FED_PULL_APPLY_ERR_STORE;
}
}
out_report->cursor_advanced = true;
if (candidate.has_logseq) {
out_report->cursor_after_has_logseq = true;
out_report->cursor_after_logseq = candidate.logseq;
}
if (amduat_reference_clone(next_ref, &out_report->cursor_after_ref)) {
out_report->cursor_after_ref_set = true;
}
transport->free_records(transport->ctx, records, record_len_total);
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
amduatd_fed_cursor_record_free(&next_cursor);
amduat_reference_free(&next_ref);
return AMDUATD_FED_PULL_APPLY_OK;
}

View file

@ -0,0 +1,91 @@
#ifndef AMDUATD_FED_PULL_APPLY_H
#define AMDUATD_FED_PULL_APPLY_H
#include "amduat/asl/core.h"
#include "amduat/fed/replay.h"
#include "amduatd_fed.h"
#include "amduatd_fed_cursor.h"
#include "amduatd_fed_pull_plan.h"
#include "amduatd_space.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
void *ctx;
bool (*get_records)(void *ctx,
uint32_t domain_id,
uint64_t from_logseq,
uint64_t limit,
int *out_status,
amduat_fed_record_t **out_records,
size_t *out_len,
char **out_body);
void (*free_records)(void *ctx, amduat_fed_record_t *records, size_t len);
bool (*get_artifact)(void *ctx,
amduat_reference_t ref,
int *out_status,
amduat_octets_t *out_bytes,
char **out_body);
} amduatd_fed_pull_transport_t;
typedef enum {
AMDUATD_FED_PULL_APPLY_OK = 0,
AMDUATD_FED_PULL_APPLY_ERR_INVALID = 1,
AMDUATD_FED_PULL_APPLY_ERR_DISABLED = 2,
AMDUATD_FED_PULL_APPLY_ERR_UNSUPPORTED = 3,
AMDUATD_FED_PULL_APPLY_ERR_REMOTE = 4,
AMDUATD_FED_PULL_APPLY_ERR_STORE = 5,
AMDUATD_FED_PULL_APPLY_ERR_CONFLICT = 6,
AMDUATD_FED_PULL_APPLY_ERR_OOM = 7
} amduatd_fed_pull_apply_status_t;
typedef struct {
const char *peer_key;
const amduatd_space_t *effective_space;
uint64_t limit;
bool cursor_present;
bool cursor_has_logseq;
uint64_t cursor_logseq;
bool cursor_ref_set;
amduat_reference_t cursor_ref;
size_t plan_record_count;
amduatd_fed_cursor_candidate_t plan_candidate;
size_t applied_record_count;
size_t applied_artifact_count;
bool cursor_advanced;
bool cursor_after_has_logseq;
uint64_t cursor_after_logseq;
bool cursor_after_ref_set;
amduat_reference_t cursor_after_ref;
int remote_status;
char error[256];
} amduatd_fed_pull_apply_report_t;
void amduatd_fed_pull_apply_report_init(
amduatd_fed_pull_apply_report_t *report);
void amduatd_fed_pull_apply_report_free(
amduatd_fed_pull_apply_report_t *report);
amduatd_fed_pull_apply_status_t amduatd_fed_pull_apply(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
uint64_t limit,
const amduatd_fed_cfg_t *fed_cfg,
const amduatd_fed_pull_transport_t *transport,
amduatd_fed_pull_apply_report_t *out_report);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUATD_FED_PULL_APPLY_H */

477
src/amduatd_fed_pull_plan.c Normal file
View file

@ -0,0 +1,477 @@
#include "amduatd_fed_pull_plan.h"
#include "amduat/asl/ref_text.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char *data;
size_t len;
size_t cap;
} amduatd_fed_plan_strbuf_t;
static void amduatd_fed_plan_strbuf_free(amduatd_fed_plan_strbuf_t *b) {
if (b == NULL) {
return;
}
free(b->data);
b->data = NULL;
b->len = 0;
b->cap = 0;
}
static bool amduatd_fed_plan_strbuf_reserve(amduatd_fed_plan_strbuf_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 != 0 ? 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_fed_plan_strbuf_append(amduatd_fed_plan_strbuf_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_fed_plan_strbuf_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_fed_plan_strbuf_append_cstr(amduatd_fed_plan_strbuf_t *b,
const char *s) {
return amduatd_fed_plan_strbuf_append(
b, s != NULL ? s : "", s != NULL ? strlen(s) : 0u);
}
static const char *amduatd_fed_plan_record_type_name(
amduat_fed_record_type_t type) {
switch (type) {
case AMDUAT_FED_REC_ARTIFACT:
return "artifact";
case AMDUAT_FED_REC_PER:
return "per";
case AMDUAT_FED_REC_TGK_EDGE:
return "tgk_edge";
case AMDUAT_FED_REC_TOMBSTONE:
return "tombstone";
default:
return "unknown";
}
}
void amduatd_fed_pull_plan_candidate_init(
amduatd_fed_cursor_candidate_t *candidate) {
if (candidate == NULL) {
return;
}
memset(candidate, 0, sizeof(*candidate));
candidate->ref = amduat_reference(0u, amduat_octets(NULL, 0u));
}
void amduatd_fed_pull_plan_candidate_free(
amduatd_fed_cursor_candidate_t *candidate) {
if (candidate == NULL) {
return;
}
if (candidate->has_ref) {
amduat_reference_free(&candidate->ref);
}
memset(candidate, 0, sizeof(*candidate));
}
bool amduatd_fed_pull_plan_next_cursor_candidate(
const amduatd_fed_cursor_record_t *cursor,
const amduat_fed_record_t *records,
size_t record_count,
amduatd_fed_cursor_candidate_t *out_candidate) {
if (out_candidate == NULL) {
return false;
}
amduatd_fed_pull_plan_candidate_init(out_candidate);
if (record_count > 0u && records != NULL) {
const amduat_fed_record_t *last = &records[record_count - 1u];
out_candidate->has_logseq = true;
out_candidate->logseq = last->logseq;
out_candidate->has_ref = true;
if (!amduat_reference_clone(last->id.ref, &out_candidate->ref)) {
amduatd_fed_pull_plan_candidate_free(out_candidate);
return false;
}
return true;
}
if (cursor != NULL) {
if (cursor->has_logseq) {
out_candidate->has_logseq = true;
out_candidate->logseq = cursor->last_logseq;
}
if (cursor->has_record_ref) {
out_candidate->has_ref = true;
if (!amduat_reference_clone(cursor->last_record_ref,
&out_candidate->ref)) {
amduatd_fed_pull_plan_candidate_free(out_candidate);
return false;
}
}
}
return true;
}
amduatd_fed_pull_plan_status_t amduatd_fed_pull_plan_check(
const amduatd_fed_cfg_t *cfg,
const amduat_asl_store_t *store) {
if (cfg == NULL || store == NULL) {
return AMDUATD_FED_PULL_PLAN_ERR_INVALID;
}
if (!cfg->enabled) {
return AMDUATD_FED_PULL_PLAN_ERR_DISABLED;
}
if (store->ops.log_scan == NULL || store->ops.current_state == NULL) {
return AMDUATD_FED_PULL_PLAN_ERR_UNSUPPORTED;
}
return AMDUATD_FED_PULL_PLAN_OK;
}
amduatd_fed_pull_plan_status_t amduatd_fed_pull_plan_json(
const amduatd_fed_pull_plan_input_t *input,
char **out_json) {
amduatd_fed_plan_strbuf_t b;
size_t i;
const amduat_fed_record_t *first = NULL;
const amduat_fed_record_t *last = NULL;
amduatd_fed_cursor_candidate_t candidate;
char *ref_hex = NULL;
char *cursor_ref_hex = NULL;
char tmp[64];
if (out_json != NULL) {
*out_json = NULL;
}
if (input == NULL || out_json == NULL || input->peer_key == NULL) {
return AMDUATD_FED_PULL_PLAN_ERR_INVALID;
}
if (input->cursor_present &&
(input->cursor == NULL || input->cursor_ref == NULL)) {
return AMDUATD_FED_PULL_PLAN_ERR_INVALID;
}
if (input->record_count > 0u && input->records != NULL) {
first = &input->records[0];
last = &input->records[input->record_count - 1u];
}
if (!amduatd_fed_pull_plan_next_cursor_candidate(
input->cursor_present ? input->cursor : NULL,
input->records,
input->record_count,
&candidate)) {
return AMDUATD_FED_PULL_PLAN_ERR_OOM;
}
if (input->cursor_present &&
input->cursor_ref != NULL &&
input->cursor_ref->digest.data != NULL) {
if (!amduat_asl_ref_encode_hex(*input->cursor_ref, &cursor_ref_hex)) {
return AMDUATD_FED_PULL_PLAN_ERR_OOM;
}
}
memset(&b, 0, sizeof(b));
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "{")) {
goto plan_oom;
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "\"peer\":\"") ||
!amduatd_fed_plan_strbuf_append_cstr(&b, input->peer_key) ||
!amduatd_fed_plan_strbuf_append_cstr(&b, "\",")) {
goto plan_oom;
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "\"effective_space\":{")) {
goto plan_oom;
}
if (input->effective_space != NULL &&
input->effective_space->enabled &&
input->effective_space->space_id.data != NULL) {
const char *space_id = (const char *)input->effective_space->space_id.data;
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "\"mode\":\"scoped\",") ||
!amduatd_fed_plan_strbuf_append_cstr(&b, "\"space_id\":\"") ||
!amduatd_fed_plan_strbuf_append_cstr(&b, space_id) ||
!amduatd_fed_plan_strbuf_append_cstr(&b, "\"")) {
goto plan_oom;
}
} else {
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "\"mode\":\"unscoped\",") ||
!amduatd_fed_plan_strbuf_append_cstr(&b, "\"space_id\":null")) {
goto plan_oom;
}
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "},")) {
goto plan_oom;
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "\"cursor\":{")) {
goto plan_oom;
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "\"present\":") ||
!amduatd_fed_plan_strbuf_append_cstr(&b,
input->cursor_present ? "true"
: "false")) {
goto plan_oom;
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, ",\"last_logseq\":")) {
goto plan_oom;
}
if (input->cursor_present && input->cursor != NULL &&
input->cursor->has_logseq) {
snprintf(tmp, sizeof(tmp), "%llu",
(unsigned long long)input->cursor->last_logseq);
if (!amduatd_fed_plan_strbuf_append_cstr(&b, tmp)) {
goto plan_oom;
}
} else {
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "null")) {
goto plan_oom;
}
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, ",\"last_record_hash\":")) {
goto plan_oom;
}
if (input->cursor_present && input->cursor != NULL &&
input->cursor->has_record_ref) {
if (!amduat_asl_ref_encode_hex(input->cursor->last_record_ref, &ref_hex)) {
goto plan_oom;
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "\"") ||
!amduatd_fed_plan_strbuf_append_cstr(&b, ref_hex) ||
!amduatd_fed_plan_strbuf_append_cstr(&b, "\"")) {
goto plan_oom;
}
free(ref_hex);
ref_hex = NULL;
} else {
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "null")) {
goto plan_oom;
}
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, ",\"cursor_ref\":")) {
goto plan_oom;
}
if (cursor_ref_hex != NULL) {
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "\"") ||
!amduatd_fed_plan_strbuf_append_cstr(&b, cursor_ref_hex) ||
!amduatd_fed_plan_strbuf_append_cstr(&b, "\"")) {
goto plan_oom;
}
} else {
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "null")) {
goto plan_oom;
}
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "},")) {
goto plan_oom;
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "\"remote_scan\":{")) {
goto plan_oom;
}
snprintf(tmp, sizeof(tmp), "%zu", input->record_count);
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "\"record_count\":") ||
!amduatd_fed_plan_strbuf_append_cstr(&b, tmp)) {
goto plan_oom;
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, ",\"first_logseq\":")) {
goto plan_oom;
}
if (first != NULL) {
snprintf(tmp, sizeof(tmp), "%llu", (unsigned long long)first->logseq);
if (!amduatd_fed_plan_strbuf_append_cstr(&b, tmp)) {
goto plan_oom;
}
} else {
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "null")) {
goto plan_oom;
}
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, ",\"first_record_hash\":")) {
goto plan_oom;
}
if (first != NULL) {
if (!amduat_asl_ref_encode_hex(first->id.ref, &ref_hex)) {
goto plan_oom;
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "\"") ||
!amduatd_fed_plan_strbuf_append_cstr(&b, ref_hex) ||
!amduatd_fed_plan_strbuf_append_cstr(&b, "\"")) {
goto plan_oom;
}
free(ref_hex);
ref_hex = NULL;
} else {
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "null")) {
goto plan_oom;
}
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, ",\"last_logseq\":")) {
goto plan_oom;
}
if (last != NULL) {
snprintf(tmp, sizeof(tmp), "%llu", (unsigned long long)last->logseq);
if (!amduatd_fed_plan_strbuf_append_cstr(&b, tmp)) {
goto plan_oom;
}
} else {
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "null")) {
goto plan_oom;
}
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, ",\"last_record_hash\":")) {
goto plan_oom;
}
if (last != NULL) {
if (!amduat_asl_ref_encode_hex(last->id.ref, &ref_hex)) {
goto plan_oom;
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "\"") ||
!amduatd_fed_plan_strbuf_append_cstr(&b, ref_hex) ||
!amduatd_fed_plan_strbuf_append_cstr(&b, "\"")) {
goto plan_oom;
}
free(ref_hex);
ref_hex = NULL;
} else {
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "null")) {
goto plan_oom;
}
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "},")) {
goto plan_oom;
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "\"records\":[")) {
goto plan_oom;
}
for (i = 0; i < input->record_count; ++i) {
const amduat_fed_record_t *rec = &input->records[i];
const char *type_name = amduatd_fed_plan_record_type_name(rec->id.type);
if (i > 0) {
if (!amduatd_fed_plan_strbuf_append_cstr(&b, ",")) {
goto plan_oom;
}
}
if (!amduat_asl_ref_encode_hex(rec->id.ref, &ref_hex)) {
goto plan_oom;
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "{\"logseq\":")) {
goto plan_oom;
}
snprintf(tmp, sizeof(tmp), "%llu", (unsigned long long)rec->logseq);
if (!amduatd_fed_plan_strbuf_append_cstr(&b, tmp) ||
!amduatd_fed_plan_strbuf_append_cstr(&b, ",\"type\":\"") ||
!amduatd_fed_plan_strbuf_append_cstr(&b, type_name) ||
!amduatd_fed_plan_strbuf_append_cstr(&b, "\",\"ref\":\"") ||
!amduatd_fed_plan_strbuf_append_cstr(&b, ref_hex) ||
!amduatd_fed_plan_strbuf_append_cstr(&b, "\"}")) {
goto plan_oom;
}
free(ref_hex);
ref_hex = NULL;
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "],")) {
goto plan_oom;
}
if (!amduatd_fed_plan_strbuf_append_cstr(
&b,
"\"required_artifacts_status\":\"unknown\","
"\"required_artifacts\":[],"
"\"next_cursor_candidate\":{")) {
goto plan_oom;
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "\"last_logseq\":")) {
goto plan_oom;
}
if (candidate.has_logseq) {
snprintf(tmp, sizeof(tmp), "%llu", (unsigned long long)candidate.logseq);
if (!amduatd_fed_plan_strbuf_append_cstr(&b, tmp)) {
goto plan_oom;
}
} else {
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "null")) {
goto plan_oom;
}
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, ",\"last_record_hash\":")) {
goto plan_oom;
}
if (candidate.has_ref) {
if (!amduat_asl_ref_encode_hex(candidate.ref, &ref_hex)) {
goto plan_oom;
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "\"") ||
!amduatd_fed_plan_strbuf_append_cstr(&b, ref_hex) ||
!amduatd_fed_plan_strbuf_append_cstr(&b, "\"")) {
goto plan_oom;
}
free(ref_hex);
ref_hex = NULL;
} else {
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "null")) {
goto plan_oom;
}
}
if (!amduatd_fed_plan_strbuf_append_cstr(&b, "}}\n")) {
goto plan_oom;
}
amduatd_fed_pull_plan_candidate_free(&candidate);
free(cursor_ref_hex);
*out_json = b.data;
return AMDUATD_FED_PULL_PLAN_OK;
plan_oom:
free(ref_hex);
amduatd_fed_pull_plan_candidate_free(&candidate);
free(cursor_ref_hex);
amduatd_fed_plan_strbuf_free(&b);
return AMDUATD_FED_PULL_PLAN_ERR_OOM;
}

View file

@ -0,0 +1,65 @@
#ifndef AMDUATD_FED_PULL_PLAN_H
#define AMDUATD_FED_PULL_PLAN_H
#include "amduat/fed/replay.h"
#include "amduatd_fed.h"
#include "amduatd_fed_cursor.h"
#include "amduatd_space.h"
#include <stdbool.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
AMDUATD_FED_PULL_PLAN_OK = 0,
AMDUATD_FED_PULL_PLAN_ERR_INVALID = 1,
AMDUATD_FED_PULL_PLAN_ERR_DISABLED = 2,
AMDUATD_FED_PULL_PLAN_ERR_UNSUPPORTED = 3,
AMDUATD_FED_PULL_PLAN_ERR_OOM = 4
} amduatd_fed_pull_plan_status_t;
typedef struct {
const char *peer_key;
const amduatd_space_t *effective_space;
bool cursor_present;
const amduatd_fed_cursor_record_t *cursor;
const amduat_reference_t *cursor_ref;
const amduat_fed_record_t *records;
size_t record_count;
} amduatd_fed_pull_plan_input_t;
typedef struct {
bool has_logseq;
uint64_t logseq;
bool has_ref;
amduat_reference_t ref;
} amduatd_fed_cursor_candidate_t;
amduatd_fed_pull_plan_status_t amduatd_fed_pull_plan_check(
const amduatd_fed_cfg_t *cfg,
const amduat_asl_store_t *store);
void amduatd_fed_pull_plan_candidate_init(
amduatd_fed_cursor_candidate_t *candidate);
void amduatd_fed_pull_plan_candidate_free(
amduatd_fed_cursor_candidate_t *candidate);
bool amduatd_fed_pull_plan_next_cursor_candidate(
const amduatd_fed_cursor_record_t *cursor,
const amduat_fed_record_t *records,
size_t record_count,
amduatd_fed_cursor_candidate_t *out_candidate);
amduatd_fed_pull_plan_status_t amduatd_fed_pull_plan_json(
const amduatd_fed_pull_plan_input_t *input,
char **out_json);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUATD_FED_PULL_PLAN_H */

View file

@ -0,0 +1,353 @@
#include "amduatd_fed_push_apply.h"
#include "amduat/asl/artifact_io.h"
#include "amduat/asl/store.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static bool amduatd_fed_push_parse_u32(const char *s, uint32_t *out) {
char *end = NULL;
unsigned long val;
if (s == NULL || out == NULL || s[0] == '\0') {
return false;
}
val = strtoul(s, &end, 10);
if (end == s || *end != '\0' || val > UINT32_MAX) {
return false;
}
*out = (uint32_t)val;
return true;
}
static bool amduatd_fed_push_strdup(const char *s, char **out) {
size_t len;
char *buf;
if (out == NULL) {
return false;
}
*out = NULL;
if (s == NULL) {
return false;
}
len = strlen(s);
if (len > SIZE_MAX - 1u) {
return false;
}
buf = (char *)malloc(len + 1u);
if (buf == NULL) {
return false;
}
if (len != 0u) {
memcpy(buf, s, len);
}
buf[len] = '\0';
*out = buf;
return true;
}
static void amduatd_fed_push_report_error(
amduatd_fed_push_apply_report_t *report,
const char *msg) {
if (report == NULL || msg == NULL) {
return;
}
memset(report->error, 0, sizeof(report->error));
strncpy(report->error, msg, sizeof(report->error) - 1u);
}
static bool amduatd_fed_push_body_is_already_present(const char *body) {
if (body == NULL) {
return false;
}
return strstr(body, "\"status\":\"already_present\"") != NULL;
}
void amduatd_fed_push_apply_report_init(
amduatd_fed_push_apply_report_t *report) {
if (report == NULL) {
return;
}
memset(report, 0, sizeof(*report));
report->cursor_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
report->cursor_after_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
amduatd_fed_push_plan_candidate_init(&report->plan_candidate);
}
void amduatd_fed_push_apply_report_free(
amduatd_fed_push_apply_report_t *report) {
if (report == NULL) {
return;
}
if (report->cursor_ref_set) {
amduat_reference_free(&report->cursor_ref);
}
if (report->cursor_after_ref_set) {
amduat_reference_free(&report->cursor_after_ref);
}
amduatd_fed_push_plan_candidate_free(&report->plan_candidate);
memset(report, 0, sizeof(*report));
}
amduatd_fed_push_apply_status_t amduatd_fed_push_apply(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
uint64_t limit,
const char *root_path,
const amduatd_fed_cfg_t *fed_cfg,
const amduatd_fed_push_transport_t *transport,
amduatd_fed_push_apply_report_t *out_report) {
amduatd_fed_push_plan_scan_t scan;
amduatd_fed_push_cursor_candidate_t candidate;
amduatd_fed_cursor_record_t next_cursor;
amduat_reference_t next_ref;
size_t i;
if (out_report == NULL) {
return AMDUATD_FED_PUSH_APPLY_ERR_INVALID;
}
amduatd_fed_push_apply_report_init(out_report);
out_report->peer_key = peer_key;
out_report->effective_space = effective_space;
out_report->limit = limit;
if (store == NULL || pointer_store == NULL || peer_key == NULL ||
fed_cfg == NULL || transport == NULL || root_path == NULL) {
amduatd_fed_push_report_error(out_report, "invalid inputs");
return AMDUATD_FED_PUSH_APPLY_ERR_INVALID;
}
if (!fed_cfg->enabled) {
amduatd_fed_push_report_error(out_report, "federation disabled");
return AMDUATD_FED_PUSH_APPLY_ERR_DISABLED;
}
if (store->ops.log_scan == NULL || store->ops.current_state == NULL) {
amduatd_fed_push_report_error(out_report, "requires index backend");
return AMDUATD_FED_PUSH_APPLY_ERR_UNSUPPORTED;
}
if (transport->post_ingest == NULL) {
amduatd_fed_push_report_error(out_report, "transport unavailable");
return AMDUATD_FED_PUSH_APPLY_ERR_UNSUPPORTED;
}
{
amduat_octets_t scoped = amduat_octets(NULL, 0u);
if (remote_space_id != NULL && remote_space_id[0] != '\0') {
if (!amduatd_fed_push_cursor_pointer_name_v2(effective_space,
peer_key,
remote_space_id,
&scoped)) {
amduatd_fed_push_report_error(out_report, "invalid peer");
return AMDUATD_FED_PUSH_APPLY_ERR_INVALID;
}
} else if (!amduatd_fed_push_cursor_pointer_name(effective_space,
peer_key,
&scoped)) {
amduatd_fed_push_report_error(out_report, "invalid peer");
return AMDUATD_FED_PUSH_APPLY_ERR_INVALID;
}
amduat_octets_free(&scoped);
}
{
uint32_t domain_id = 0u;
if (!amduatd_fed_push_parse_u32(peer_key, &domain_id)) {
amduatd_fed_push_report_error(out_report, "invalid peer");
return AMDUATD_FED_PUSH_APPLY_ERR_INVALID;
}
}
if (amduatd_fed_push_plan_scan(store,
pointer_store,
effective_space,
peer_key,
remote_space_id,
limit,
root_path,
&scan) != AMDUATD_FED_PUSH_PLAN_OK) {
amduatd_fed_push_plan_scan_free(&scan);
amduatd_fed_push_report_error(out_report, "plan scan failed");
return AMDUATD_FED_PUSH_APPLY_ERR_STORE;
}
out_report->cursor_present = scan.cursor_present;
if (scan.cursor_present) {
out_report->cursor_has_logseq = scan.cursor.has_logseq;
out_report->cursor_logseq = scan.cursor.last_logseq;
if (scan.cursor_ref.digest.data != NULL) {
if (!amduat_reference_clone(scan.cursor_ref, &out_report->cursor_ref)) {
amduatd_fed_push_plan_scan_free(&scan);
amduatd_fed_push_report_error(out_report, "oom");
return AMDUATD_FED_PUSH_APPLY_ERR_OOM;
}
out_report->cursor_ref_set = true;
}
}
if (!amduatd_fed_push_plan_next_cursor_candidate(scan.cursor_present
? &scan.cursor
: NULL,
scan.records,
scan.record_count,
&candidate)) {
amduatd_fed_push_plan_scan_free(&scan);
amduatd_fed_push_report_error(out_report, "oom");
return AMDUATD_FED_PUSH_APPLY_ERR_OOM;
}
out_report->plan_record_count = scan.record_count;
out_report->plan_candidate = candidate;
if (scan.record_count == 0u) {
amduatd_fed_push_plan_scan_free(&scan);
return AMDUATD_FED_PUSH_APPLY_OK;
}
for (i = 0; i < scan.record_count; ++i) {
const amduat_fed_record_t *rec = &scan.records[i];
amduat_octets_t bytes = amduat_octets(NULL, 0u);
amduat_artifact_t artifact;
amduat_asl_store_error_t store_err;
int status = 0;
char *body = NULL;
bool already_present = false;
if (rec->id.type != AMDUAT_FED_REC_TOMBSTONE) {
memset(&artifact, 0, sizeof(artifact));
store_err = amduat_asl_store_get(store, rec->id.ref, &artifact);
if (store_err != AMDUAT_ASL_STORE_OK) {
amduatd_fed_push_plan_scan_free(&scan);
amduatd_fed_push_report_error(out_report, "artifact missing");
return AMDUATD_FED_PUSH_APPLY_ERR_STORE;
}
bytes = artifact.bytes;
out_report->sent_bytes_total += bytes.len;
}
if (!transport->post_ingest(transport->ctx,
rec->id.type,
rec->id.ref,
bytes,
&status,
&body)) {
if (rec->id.type != AMDUAT_FED_REC_TOMBSTONE) {
amduat_asl_artifact_free(&artifact);
}
free(body);
amduatd_fed_push_plan_scan_free(&scan);
amduatd_fed_push_report_error(out_report, "ingest failed");
return AMDUATD_FED_PUSH_APPLY_ERR_REMOTE;
}
if (rec->id.type != AMDUAT_FED_REC_TOMBSTONE) {
amduat_asl_artifact_free(&artifact);
}
out_report->sent_record_count++;
if (rec->id.type == AMDUAT_FED_REC_ARTIFACT) {
out_report->sent_artifact_count++;
} else if (rec->id.type == AMDUAT_FED_REC_PER) {
out_report->sent_per_count++;
} else if (rec->id.type == AMDUAT_FED_REC_TGK_EDGE) {
out_report->sent_tgk_edge_count++;
} else if (rec->id.type == AMDUAT_FED_REC_TOMBSTONE) {
out_report->sent_tombstone_count++;
}
if (status != 200) {
out_report->remote_status = status;
free(body);
amduatd_fed_push_plan_scan_free(&scan);
amduatd_fed_push_report_error(out_report, "peer error");
return AMDUATD_FED_PUSH_APPLY_ERR_REMOTE;
}
already_present = amduatd_fed_push_body_is_already_present(body);
free(body);
if (already_present) {
out_report->peer_already_present_count++;
} else {
out_report->peer_ok_count++;
}
}
amduatd_fed_cursor_record_init(&next_cursor);
if (!amduatd_fed_push_strdup(peer_key, &next_cursor.peer_key)) {
amduatd_fed_cursor_record_free(&next_cursor);
amduatd_fed_push_plan_scan_free(&scan);
amduatd_fed_push_report_error(out_report, "oom");
return AMDUATD_FED_PUSH_APPLY_ERR_OOM;
}
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_fed_push_strdup(space_id, &next_cursor.space_id)) {
amduatd_fed_cursor_record_free(&next_cursor);
amduatd_fed_push_plan_scan_free(&scan);
amduatd_fed_push_report_error(out_report, "oom");
return AMDUATD_FED_PUSH_APPLY_ERR_OOM;
}
} else {
next_cursor.space_id = NULL;
}
if (out_report->plan_candidate.has_logseq) {
next_cursor.has_logseq = true;
next_cursor.last_logseq = out_report->plan_candidate.logseq;
}
if (out_report->plan_candidate.has_ref) {
next_cursor.has_record_ref = true;
if (!amduat_reference_clone(out_report->plan_candidate.ref,
&next_cursor.last_record_ref)) {
amduatd_fed_cursor_record_free(&next_cursor);
amduatd_fed_push_plan_scan_free(&scan);
amduatd_fed_push_report_error(out_report, "oom");
return AMDUATD_FED_PUSH_APPLY_ERR_OOM;
}
}
if (!next_cursor.has_logseq && !next_cursor.has_record_ref) {
amduatd_fed_cursor_record_free(&next_cursor);
amduatd_fed_push_plan_scan_free(&scan);
amduatd_fed_push_report_error(out_report, "invalid cursor");
return AMDUATD_FED_PUSH_APPLY_ERR_INVALID;
}
memset(&next_ref, 0, sizeof(next_ref));
{
amduatd_fed_cursor_status_t st;
st = amduatd_fed_push_cursor_cas_set_remote(store,
pointer_store,
effective_space,
peer_key,
remote_space_id,
scan.cursor_present
? &scan.cursor_ref
: NULL,
&next_cursor,
&next_ref);
amduatd_fed_cursor_record_free(&next_cursor);
if (st == AMDUATD_FED_CURSOR_ERR_CONFLICT) {
amduatd_fed_push_plan_scan_free(&scan);
amduatd_fed_push_report_error(out_report, "cursor conflict");
return AMDUATD_FED_PUSH_APPLY_ERR_CONFLICT;
}
if (st != AMDUATD_FED_CURSOR_OK) {
amduatd_fed_push_plan_scan_free(&scan);
amduatd_fed_push_report_error(out_report, "cursor update failed");
return AMDUATD_FED_PUSH_APPLY_ERR_STORE;
}
}
out_report->cursor_advanced = true;
if (out_report->plan_candidate.has_logseq) {
out_report->cursor_after_has_logseq = true;
out_report->cursor_after_logseq = out_report->plan_candidate.logseq;
}
if (next_ref.digest.data != NULL) {
out_report->cursor_after_ref_set = true;
out_report->cursor_after_ref = next_ref;
} else {
amduat_reference_free(&next_ref);
}
amduatd_fed_push_plan_scan_free(&scan);
return AMDUATD_FED_PUSH_APPLY_OK;
}

View file

@ -0,0 +1,90 @@
#ifndef AMDUATD_FED_PUSH_APPLY_H
#define AMDUATD_FED_PUSH_APPLY_H
#include "amduat/asl/core.h"
#include "amduat/fed/replay.h"
#include "amduatd_fed.h"
#include "amduatd_fed_cursor.h"
#include "amduatd_fed_push_plan.h"
#include "amduatd_space.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
void *ctx;
bool (*post_ingest)(void *ctx,
amduat_fed_record_type_t record_type,
amduat_reference_t ref,
amduat_octets_t bytes,
int *out_status,
char **out_body);
} amduatd_fed_push_transport_t;
typedef enum {
AMDUATD_FED_PUSH_APPLY_OK = 0,
AMDUATD_FED_PUSH_APPLY_ERR_INVALID = 1,
AMDUATD_FED_PUSH_APPLY_ERR_DISABLED = 2,
AMDUATD_FED_PUSH_APPLY_ERR_UNSUPPORTED = 3,
AMDUATD_FED_PUSH_APPLY_ERR_REMOTE = 4,
AMDUATD_FED_PUSH_APPLY_ERR_STORE = 5,
AMDUATD_FED_PUSH_APPLY_ERR_CONFLICT = 6,
AMDUATD_FED_PUSH_APPLY_ERR_OOM = 7
} amduatd_fed_push_apply_status_t;
typedef struct {
const char *peer_key;
const amduatd_space_t *effective_space;
uint64_t limit;
bool cursor_present;
bool cursor_has_logseq;
uint64_t cursor_logseq;
bool cursor_ref_set;
amduat_reference_t cursor_ref;
size_t plan_record_count;
amduatd_fed_push_cursor_candidate_t plan_candidate;
size_t sent_record_count;
uint64_t sent_bytes_total;
size_t sent_artifact_count;
size_t sent_per_count;
size_t sent_tgk_edge_count;
size_t sent_tombstone_count;
size_t peer_ok_count;
size_t peer_already_present_count;
bool cursor_advanced;
bool cursor_after_has_logseq;
uint64_t cursor_after_logseq;
bool cursor_after_ref_set;
amduat_reference_t cursor_after_ref;
int remote_status;
char error[256];
} amduatd_fed_push_apply_report_t;
void amduatd_fed_push_apply_report_init(
amduatd_fed_push_apply_report_t *report);
void amduatd_fed_push_apply_report_free(
amduatd_fed_push_apply_report_t *report);
amduatd_fed_push_apply_status_t amduatd_fed_push_apply(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
uint64_t limit,
const char *root_path,
const amduatd_fed_cfg_t *fed_cfg,
const amduatd_fed_push_transport_t *transport,
amduatd_fed_push_apply_report_t *out_report);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUATD_FED_PUSH_APPLY_H */

647
src/amduatd_fed_push_plan.c Normal file
View file

@ -0,0 +1,647 @@
#include "amduatd_fed_push_plan.h"
#include "amduat/asl/log_store.h"
#include "amduat/asl/artifact_io.h"
#include "amduat/asl/ref_text.h"
#include "amduat/asl/store.h"
#include "amduat/enc/fer1_receipt.h"
#include "amduat/enc/tgk1_edge.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char *data;
size_t len;
size_t cap;
} amduatd_fed_push_plan_strbuf_t;
static void amduatd_fed_push_plan_strbuf_free(
amduatd_fed_push_plan_strbuf_t *b) {
if (b == NULL) {
return;
}
free(b->data);
b->data = NULL;
b->len = 0;
b->cap = 0;
}
static bool amduatd_fed_push_plan_strbuf_reserve(
amduatd_fed_push_plan_strbuf_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 != 0 ? 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_fed_push_plan_strbuf_append(
amduatd_fed_push_plan_strbuf_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_fed_push_plan_strbuf_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_fed_push_plan_strbuf_append_cstr(
amduatd_fed_push_plan_strbuf_t *b,
const char *s) {
return amduatd_fed_push_plan_strbuf_append(
b, s != NULL ? s : "", s != NULL ? strlen(s) : 0u);
}
static const char *amduatd_fed_push_plan_record_type_name(
amduat_fed_record_type_t type) {
switch (type) {
case AMDUAT_FED_REC_ARTIFACT:
return "artifact";
case AMDUAT_FED_REC_PER:
return "per";
case AMDUAT_FED_REC_TGK_EDGE:
return "tgk_edge";
case AMDUAT_FED_REC_TOMBSTONE:
return "tombstone";
default:
return "unknown";
}
}
void amduatd_fed_push_plan_candidate_init(
amduatd_fed_push_cursor_candidate_t *candidate) {
if (candidate == NULL) {
return;
}
memset(candidate, 0, sizeof(*candidate));
candidate->ref = amduat_reference(0u, amduat_octets(NULL, 0u));
}
void amduatd_fed_push_plan_candidate_free(
amduatd_fed_push_cursor_candidate_t *candidate) {
if (candidate == NULL) {
return;
}
if (candidate->has_ref) {
amduat_reference_free(&candidate->ref);
}
memset(candidate, 0, sizeof(*candidate));
}
bool amduatd_fed_push_plan_next_cursor_candidate(
const amduatd_fed_cursor_record_t *cursor,
const amduat_fed_record_t *records,
size_t record_count,
amduatd_fed_push_cursor_candidate_t *out_candidate) {
if (out_candidate == NULL) {
return false;
}
amduatd_fed_push_plan_candidate_init(out_candidate);
if (record_count > 0u && records != NULL) {
const amduat_fed_record_t *last = &records[record_count - 1u];
out_candidate->has_logseq = true;
out_candidate->logseq = last->logseq;
out_candidate->has_ref = true;
if (!amduat_reference_clone(last->id.ref, &out_candidate->ref)) {
amduatd_fed_push_plan_candidate_free(out_candidate);
return false;
}
return true;
}
(void)cursor;
return true;
}
amduatd_fed_push_plan_status_t amduatd_fed_push_plan_check(
const amduatd_fed_cfg_t *cfg,
const amduat_asl_store_t *store) {
if (cfg == NULL || store == NULL) {
return AMDUATD_FED_PUSH_PLAN_ERR_INVALID;
}
if (!cfg->enabled) {
return AMDUATD_FED_PUSH_PLAN_ERR_DISABLED;
}
if (store->ops.log_scan == NULL || store->ops.current_state == NULL) {
return AMDUATD_FED_PUSH_PLAN_ERR_UNSUPPORTED;
}
return AMDUATD_FED_PUSH_PLAN_OK;
}
static void amduatd_fed_push_plan_records_free(amduat_fed_record_t *records,
size_t record_count) {
size_t i;
if (records == NULL) {
return;
}
for (i = 0; i < record_count; ++i) {
amduat_reference_free(&records[i].id.ref);
}
free(records);
}
static bool amduatd_fed_push_entry_record_type(
amduat_asl_store_t *store,
const amduat_asl_log_entry_t *entry,
amduat_fed_record_type_t *out_type) {
amduat_fed_record_type_t rec_type = AMDUAT_FED_REC_ARTIFACT;
if (store == NULL || entry == NULL || out_type == NULL) {
return false;
}
if (entry->kind == AMDUATD_FED_LOG_KIND_ARTIFACT) {
amduat_artifact_t artifact;
amduat_asl_store_error_t store_err;
memset(&artifact, 0, sizeof(artifact));
store_err = amduat_asl_store_get(store, entry->payload_ref, &artifact);
if (store_err == AMDUAT_ASL_STORE_OK && artifact.has_type_tag) {
if (artifact.type_tag.tag_id == AMDUAT_TYPE_TAG_TGK1_EDGE_V1) {
rec_type = AMDUAT_FED_REC_TGK_EDGE;
} else if (artifact.type_tag.tag_id == AMDUAT_TYPE_TAG_FER1_RECEIPT_1) {
rec_type = AMDUAT_FED_REC_PER;
}
}
if (store_err == AMDUAT_ASL_STORE_OK) {
amduat_asl_artifact_free(&artifact);
}
} else if (entry->kind == AMDUATD_FED_LOG_KIND_TOMBSTONE) {
rec_type = AMDUAT_FED_REC_TOMBSTONE;
} else {
return false;
}
*out_type = rec_type;
return true;
}
void amduatd_fed_push_plan_scan_init(amduatd_fed_push_plan_scan_t *scan) {
if (scan == NULL) {
return;
}
memset(scan, 0, sizeof(*scan));
amduatd_fed_cursor_record_init(&scan->cursor);
scan->cursor_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
}
void amduatd_fed_push_plan_scan_free(amduatd_fed_push_plan_scan_t *scan) {
if (scan == NULL) {
return;
}
if (scan->cursor_present) {
amduatd_fed_cursor_record_free(&scan->cursor);
amduat_reference_free(&scan->cursor_ref);
}
amduatd_fed_push_plan_records_free(scan->records, scan->record_count);
memset(scan, 0, sizeof(*scan));
}
amduatd_fed_push_plan_status_t amduatd_fed_push_plan_scan(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
uint64_t limit,
const char *root_path,
amduatd_fed_push_plan_scan_t *out_scan) {
amduat_asl_log_store_t log_store;
amduat_asl_log_entry_t *entries = NULL;
size_t entry_count = 0u;
uint64_t next_offset = 0u;
bool end = false;
amduat_octets_t log_name = amduat_octets(NULL, 0u);
uint64_t from_logseq = 0u;
if (store == NULL || pointer_store == NULL || peer_key == NULL ||
root_path == NULL || out_scan == NULL) {
return AMDUATD_FED_PUSH_PLAN_ERR_INVALID;
}
amduatd_fed_push_plan_scan_init(out_scan);
{
amduatd_fed_cursor_status_t cursor_status;
cursor_status = amduatd_fed_push_cursor_get_remote(store,
pointer_store,
effective_space,
peer_key,
remote_space_id,
&out_scan->cursor,
&out_scan->cursor_ref);
if (cursor_status == AMDUATD_FED_CURSOR_ERR_NOT_FOUND) {
out_scan->cursor_present = false;
} else if (cursor_status == AMDUATD_FED_CURSOR_OK) {
out_scan->cursor_present = true;
if (out_scan->cursor.has_logseq) {
if (out_scan->cursor.last_logseq == UINT64_MAX) {
amduatd_fed_push_plan_scan_free(out_scan);
return AMDUATD_FED_PUSH_PLAN_ERR_INVALID;
}
from_logseq = out_scan->cursor.last_logseq + 1u;
}
} else {
amduatd_fed_push_plan_scan_free(out_scan);
return AMDUATD_FED_PUSH_PLAN_ERR_INVALID;
}
}
if (!amduat_asl_log_store_init(&log_store, root_path, store,
pointer_store)) {
amduatd_fed_push_plan_scan_free(out_scan);
return AMDUATD_FED_PUSH_PLAN_ERR_INVALID;
}
if (!amduatd_space_scope_name(effective_space, "fed/records", &log_name)) {
amduatd_fed_push_plan_scan_free(out_scan);
return AMDUATD_FED_PUSH_PLAN_ERR_INVALID;
}
{
amduat_asl_store_error_t read_err =
amduat_asl_log_read(&log_store,
(const char *)log_name.data,
from_logseq,
(size_t)limit,
&entries,
&entry_count,
&next_offset,
&end);
amduat_octets_free(&log_name);
if (read_err != AMDUAT_ASL_STORE_OK) {
amduatd_fed_push_plan_scan_free(out_scan);
return AMDUATD_FED_PUSH_PLAN_ERR_INVALID;
}
}
(void)next_offset;
(void)end;
if (entry_count != 0u) {
out_scan->records =
(amduat_fed_record_t *)calloc(entry_count, sizeof(*out_scan->records));
if (out_scan->records == NULL) {
amduat_asl_log_entries_free(entries, entry_count);
amduatd_fed_push_plan_scan_free(out_scan);
return AMDUATD_FED_PUSH_PLAN_ERR_OOM;
}
}
{
size_t i;
for (i = 0; i < entry_count; ++i) {
const amduat_asl_log_entry_t *entry = &entries[i];
amduat_fed_record_type_t rec_type = AMDUAT_FED_REC_ARTIFACT;
uint64_t logseq;
if (entry->payload_ref.digest.data == NULL ||
entry->payload_ref.digest.len == 0u) {
continue;
}
if (from_logseq > UINT64_MAX - (uint64_t)i) {
amduat_asl_log_entries_free(entries, entry_count);
amduatd_fed_push_plan_scan_free(out_scan);
return AMDUATD_FED_PUSH_PLAN_ERR_INVALID;
}
logseq = from_logseq + (uint64_t)i;
if (!amduatd_fed_push_entry_record_type(store, entry, &rec_type)) {
continue;
}
memset(&out_scan->records[out_scan->record_count], 0,
sizeof(out_scan->records[out_scan->record_count]));
out_scan->records[out_scan->record_count].id.type = rec_type;
out_scan->records[out_scan->record_count].logseq = logseq;
if (!amduat_reference_clone(entry->payload_ref,
&out_scan->records[out_scan->record_count]
.id.ref)) {
amduat_asl_log_entries_free(entries, entry_count);
amduatd_fed_push_plan_scan_free(out_scan);
return AMDUATD_FED_PUSH_PLAN_ERR_OOM;
}
out_scan->record_count++;
}
}
amduat_asl_log_entries_free(entries, entry_count);
return AMDUATD_FED_PUSH_PLAN_OK;
}
amduatd_fed_push_plan_status_t amduatd_fed_push_plan_json(
const amduatd_fed_push_plan_input_t *input,
char **out_json) {
amduatd_fed_push_plan_strbuf_t b;
size_t i;
const amduat_fed_record_t *first = NULL;
const amduat_fed_record_t *last = NULL;
amduatd_fed_push_cursor_candidate_t candidate;
char *ref_hex = NULL;
char *cursor_ref_hex = NULL;
char tmp[64];
if (out_json != NULL) {
*out_json = NULL;
}
if (input == NULL || out_json == NULL || input->peer_key == NULL) {
return AMDUATD_FED_PUSH_PLAN_ERR_INVALID;
}
if (input->record_count > 0u && input->records == NULL) {
return AMDUATD_FED_PUSH_PLAN_ERR_INVALID;
}
if (input->cursor_present && input->cursor == NULL) {
return AMDUATD_FED_PUSH_PLAN_ERR_INVALID;
}
if (input->record_count > 0u && input->records != NULL) {
first = &input->records[0];
last = &input->records[input->record_count - 1u];
}
if (!amduatd_fed_push_plan_next_cursor_candidate(
input->cursor_present ? input->cursor : NULL,
input->records,
input->record_count,
&candidate)) {
return AMDUATD_FED_PUSH_PLAN_ERR_OOM;
}
if (input->cursor_present &&
input->cursor_ref != NULL &&
input->cursor_ref->digest.data != NULL) {
if (!amduat_asl_ref_encode_hex(*input->cursor_ref, &cursor_ref_hex)) {
amduatd_fed_push_plan_candidate_free(&candidate);
return AMDUATD_FED_PUSH_PLAN_ERR_OOM;
}
}
memset(&b, 0, sizeof(b));
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "{")) {
goto plan_oom;
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"peer\":\"") ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, input->peer_key) ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\",")) {
goto plan_oom;
}
snprintf(tmp, sizeof(tmp), "%u", (unsigned int)input->domain_id);
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"domain_id\":") ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, tmp) ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, ",")) {
goto plan_oom;
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"effective_space\":{")) {
goto plan_oom;
}
if (input->effective_space != NULL &&
input->effective_space->enabled &&
input->effective_space->space_id.data != NULL) {
const char *space_id = (const char *)input->effective_space->space_id.data;
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"mode\":\"scoped\",") ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"space_id\":\"") ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, space_id) ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"")) {
goto plan_oom;
}
} else {
if (!amduatd_fed_push_plan_strbuf_append_cstr(
&b, "\"mode\":\"unscoped\",") ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"space_id\":null")) {
goto plan_oom;
}
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "},")) {
goto plan_oom;
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"cursor\":{")) {
goto plan_oom;
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"present\":") ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b,
input->cursor_present ? "true"
: "false")) {
goto plan_oom;
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, ",\"last_logseq\":")) {
goto plan_oom;
}
if (input->cursor_present && input->cursor != NULL &&
input->cursor->has_logseq) {
snprintf(tmp, sizeof(tmp), "%llu",
(unsigned long long)input->cursor->last_logseq);
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, tmp)) {
goto plan_oom;
}
} else {
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "null")) {
goto plan_oom;
}
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, ",\"ref\":")) {
goto plan_oom;
}
if (cursor_ref_hex != NULL) {
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"") ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, cursor_ref_hex) ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"")) {
goto plan_oom;
}
} else {
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "null")) {
goto plan_oom;
}
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "},")) {
goto plan_oom;
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"scan\":{")) {
goto plan_oom;
}
snprintf(tmp, sizeof(tmp), "%zu", input->record_count);
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"record_count\":") ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, tmp)) {
goto plan_oom;
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, ",\"first_logseq\":")) {
goto plan_oom;
}
if (first != NULL) {
snprintf(tmp, sizeof(tmp), "%llu", (unsigned long long)first->logseq);
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, tmp)) {
goto plan_oom;
}
} else {
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "null")) {
goto plan_oom;
}
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, ",\"last_logseq\":")) {
goto plan_oom;
}
if (last != NULL) {
snprintf(tmp, sizeof(tmp), "%llu", (unsigned long long)last->logseq);
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, tmp)) {
goto plan_oom;
}
} else {
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "null")) {
goto plan_oom;
}
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "},")) {
goto plan_oom;
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"records\":[")) {
goto plan_oom;
}
for (i = 0; i < input->record_count; ++i) {
const amduat_fed_record_t *rec = &input->records[i];
const char *type_name = amduatd_fed_push_plan_record_type_name(rec->id.type);
if (i > 0) {
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, ",")) {
goto plan_oom;
}
}
if (!amduat_asl_ref_encode_hex(rec->id.ref, &ref_hex)) {
goto plan_oom;
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "{\"logseq\":")) {
goto plan_oom;
}
snprintf(tmp, sizeof(tmp), "%llu", (unsigned long long)rec->logseq);
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, tmp) ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, ",\"record_type\":\"") ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, type_name) ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\",\"ref\":\"") ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, ref_hex) ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"}")) {
goto plan_oom;
}
free(ref_hex);
ref_hex = NULL;
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "],")) {
goto plan_oom;
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b,
"\"required_artifacts\":[")) {
goto plan_oom;
}
{
bool first_artifact = true;
for (i = 0; i < input->record_count; ++i) {
const amduat_fed_record_t *rec = &input->records[i];
if (rec->id.type == AMDUAT_FED_REC_TOMBSTONE) {
continue;
}
if (!amduat_asl_ref_encode_hex(rec->id.ref, &ref_hex)) {
goto plan_oom;
}
if (!first_artifact) {
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, ",")) {
goto plan_oom;
}
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"") ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, ref_hex) ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"")) {
goto plan_oom;
}
first_artifact = false;
free(ref_hex);
ref_hex = NULL;
}
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "],")) {
goto plan_oom;
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(
&b, "\"next_cursor_candidate\":{")) {
goto plan_oom;
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"last_logseq\":")) {
goto plan_oom;
}
if (candidate.has_logseq) {
snprintf(tmp, sizeof(tmp), "%llu", (unsigned long long)candidate.logseq);
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, tmp)) {
goto plan_oom;
}
} else {
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "null")) {
goto plan_oom;
}
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, ",\"ref\":")) {
goto plan_oom;
}
if (candidate.has_ref) {
if (!amduat_asl_ref_encode_hex(candidate.ref, &ref_hex)) {
goto plan_oom;
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"") ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, ref_hex) ||
!amduatd_fed_push_plan_strbuf_append_cstr(&b, "\"")) {
goto plan_oom;
}
free(ref_hex);
ref_hex = NULL;
} else {
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "null")) {
goto plan_oom;
}
}
if (!amduatd_fed_push_plan_strbuf_append_cstr(&b, "}}\n")) {
goto plan_oom;
}
amduatd_fed_push_plan_candidate_free(&candidate);
free(cursor_ref_hex);
*out_json = b.data;
return AMDUATD_FED_PUSH_PLAN_OK;
plan_oom:
free(ref_hex);
amduatd_fed_push_plan_candidate_free(&candidate);
free(cursor_ref_hex);
amduatd_fed_push_plan_strbuf_free(&b);
return AMDUATD_FED_PUSH_PLAN_ERR_OOM;
}

View file

@ -0,0 +1,89 @@
#ifndef AMDUATD_FED_PUSH_PLAN_H
#define AMDUATD_FED_PUSH_PLAN_H
#include "amduat/fed/replay.h"
#include "amduatd_fed.h"
#include "amduatd_fed_cursor.h"
#include "amduatd_space.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
AMDUATD_FED_PUSH_PLAN_OK = 0,
AMDUATD_FED_PUSH_PLAN_ERR_INVALID = 1,
AMDUATD_FED_PUSH_PLAN_ERR_DISABLED = 2,
AMDUATD_FED_PUSH_PLAN_ERR_UNSUPPORTED = 3,
AMDUATD_FED_PUSH_PLAN_ERR_OOM = 4
} amduatd_fed_push_plan_status_t;
typedef struct {
const char *peer_key;
uint32_t domain_id;
const amduatd_space_t *effective_space;
bool cursor_present;
const amduatd_fed_cursor_record_t *cursor;
const amduat_reference_t *cursor_ref;
const amduat_fed_record_t *records;
size_t record_count;
} amduatd_fed_push_plan_input_t;
typedef struct {
bool has_logseq;
uint64_t logseq;
bool has_ref;
amduat_reference_t ref;
} amduatd_fed_push_cursor_candidate_t;
typedef struct {
bool cursor_present;
amduatd_fed_cursor_record_t cursor;
amduat_reference_t cursor_ref;
amduat_fed_record_t *records;
size_t record_count;
} amduatd_fed_push_plan_scan_t;
amduatd_fed_push_plan_status_t amduatd_fed_push_plan_check(
const amduatd_fed_cfg_t *cfg,
const amduat_asl_store_t *store);
void amduatd_fed_push_plan_scan_init(amduatd_fed_push_plan_scan_t *scan);
void amduatd_fed_push_plan_scan_free(amduatd_fed_push_plan_scan_t *scan);
amduatd_fed_push_plan_status_t amduatd_fed_push_plan_scan(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
uint64_t limit,
const char *root_path,
amduatd_fed_push_plan_scan_t *out_scan);
void amduatd_fed_push_plan_candidate_init(
amduatd_fed_push_cursor_candidate_t *candidate);
void amduatd_fed_push_plan_candidate_free(
amduatd_fed_push_cursor_candidate_t *candidate);
bool amduatd_fed_push_plan_next_cursor_candidate(
const amduatd_fed_cursor_record_t *cursor,
const amduat_fed_record_t *records,
size_t record_count,
amduatd_fed_push_cursor_candidate_t *out_candidate);
amduatd_fed_push_plan_status_t amduatd_fed_push_plan_json(
const amduatd_fed_push_plan_input_t *input,
char **out_json);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUATD_FED_PUSH_PLAN_H */

240
src/amduatd_fed_until.c Normal file
View file

@ -0,0 +1,240 @@
#include "amduatd_fed_until.h"
#include <stdlib.h>
#include <string.h>
static void amduatd_fed_until_report_clear_cursor(
amduatd_fed_until_report_t *report) {
if (report == NULL) {
return;
}
if (report->cursor_ref_set) {
amduat_reference_free(&report->cursor_ref);
report->cursor_ref_set = false;
}
report->cursor_has_logseq = false;
report->cursor_logseq = 0u;
}
static void amduatd_fed_until_report_set_cursor(
amduatd_fed_until_report_t *report,
bool has_logseq,
uint64_t logseq,
bool has_ref,
amduat_reference_t ref) {
if (report == NULL) {
return;
}
amduatd_fed_until_report_clear_cursor(report);
if (has_logseq) {
report->cursor_has_logseq = true;
report->cursor_logseq = logseq;
}
if (has_ref) {
if (amduat_reference_clone(ref, &report->cursor_ref)) {
report->cursor_ref_set = true;
}
}
}
static void amduatd_fed_until_report_error(amduatd_fed_until_report_t *report,
const char *msg,
int remote_status) {
if (report == NULL || msg == NULL) {
return;
}
memset(report->error, 0, sizeof(report->error));
strncpy(report->error, msg, sizeof(report->error) - 1u);
report->remote_status = remote_status;
}
void amduatd_fed_until_report_init(amduatd_fed_until_report_t *report) {
if (report == NULL) {
return;
}
memset(report, 0, sizeof(*report));
report->cursor_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
}
void amduatd_fed_until_report_free(amduatd_fed_until_report_t *report) {
if (report == NULL) {
return;
}
if (report->cursor_ref_set) {
amduat_reference_free(&report->cursor_ref);
}
memset(report, 0, sizeof(*report));
}
amduatd_fed_pull_apply_status_t amduatd_fed_pull_until(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
uint64_t limit,
uint64_t max_rounds,
const amduatd_fed_cfg_t *fed_cfg,
const amduatd_fed_pull_transport_t *transport,
amduatd_fed_until_report_t *out_report) {
amduatd_fed_pull_apply_status_t status = AMDUATD_FED_PULL_APPLY_OK;
amduatd_fed_pull_apply_report_t round_report;
if (out_report == NULL) {
return AMDUATD_FED_PULL_APPLY_ERR_INVALID;
}
amduatd_fed_until_report_init(out_report);
out_report->peer_key = peer_key;
out_report->effective_space = effective_space;
out_report->limit = limit;
out_report->max_rounds = max_rounds;
if (store == NULL || pointer_store == NULL || peer_key == NULL ||
fed_cfg == NULL || transport == NULL || max_rounds == 0u) {
amduatd_fed_until_report_error(out_report, "invalid inputs", 0);
return AMDUATD_FED_PULL_APPLY_ERR_INVALID;
}
for (uint64_t round = 0u; round < max_rounds; ++round) {
amduatd_fed_pull_apply_report_init(&round_report);
status = amduatd_fed_pull_apply(store,
pointer_store,
effective_space,
peer_key,
remote_space_id,
limit,
fed_cfg,
transport,
&round_report);
out_report->rounds_executed++;
if (status != AMDUATD_FED_PULL_APPLY_OK) {
amduatd_fed_until_report_error(out_report,
round_report.error[0] != '\0'
? round_report.error
: "error",
round_report.remote_status);
amduatd_fed_pull_apply_report_free(&round_report);
return status;
}
out_report->total_records += round_report.applied_record_count;
out_report->total_artifacts += round_report.applied_artifact_count;
if (round_report.cursor_advanced) {
amduatd_fed_until_report_set_cursor(
out_report,
round_report.cursor_after_has_logseq,
round_report.cursor_after_logseq,
round_report.cursor_after_ref_set,
round_report.cursor_after_ref);
} else if (round_report.cursor_present) {
amduatd_fed_until_report_set_cursor(
out_report,
round_report.cursor_has_logseq,
round_report.cursor_logseq,
round_report.cursor_ref_set,
round_report.cursor_ref);
} else {
amduatd_fed_until_report_clear_cursor(out_report);
}
if (round_report.plan_record_count == 0u) {
out_report->caught_up = true;
amduatd_fed_pull_apply_report_free(&round_report);
return AMDUATD_FED_PULL_APPLY_OK;
}
amduatd_fed_pull_apply_report_free(&round_report);
}
out_report->caught_up = false;
return AMDUATD_FED_PULL_APPLY_OK;
}
amduatd_fed_push_apply_status_t amduatd_fed_push_until(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
uint64_t limit,
uint64_t max_rounds,
const char *root_path,
const amduatd_fed_cfg_t *fed_cfg,
const amduatd_fed_push_transport_t *transport,
amduatd_fed_until_report_t *out_report) {
amduatd_fed_push_apply_status_t status = AMDUATD_FED_PUSH_APPLY_OK;
amduatd_fed_push_apply_report_t round_report;
if (out_report == NULL) {
return AMDUATD_FED_PUSH_APPLY_ERR_INVALID;
}
amduatd_fed_until_report_init(out_report);
out_report->peer_key = peer_key;
out_report->effective_space = effective_space;
out_report->limit = limit;
out_report->max_rounds = max_rounds;
if (store == NULL || pointer_store == NULL || peer_key == NULL ||
root_path == NULL || fed_cfg == NULL || transport == NULL ||
max_rounds == 0u) {
amduatd_fed_until_report_error(out_report, "invalid inputs", 0);
return AMDUATD_FED_PUSH_APPLY_ERR_INVALID;
}
for (uint64_t round = 0u; round < max_rounds; ++round) {
amduatd_fed_push_apply_report_init(&round_report);
status = amduatd_fed_push_apply(store,
pointer_store,
effective_space,
peer_key,
remote_space_id,
limit,
root_path,
fed_cfg,
transport,
&round_report);
out_report->rounds_executed++;
if (status != AMDUATD_FED_PUSH_APPLY_OK) {
amduatd_fed_until_report_error(out_report,
round_report.error[0] != '\0'
? round_report.error
: "error",
round_report.remote_status);
amduatd_fed_push_apply_report_free(&round_report);
return status;
}
out_report->total_records += round_report.sent_record_count;
out_report->total_artifacts += round_report.sent_artifact_count;
if (round_report.cursor_advanced) {
amduatd_fed_until_report_set_cursor(
out_report,
round_report.cursor_after_has_logseq,
round_report.cursor_after_logseq,
round_report.cursor_after_ref_set,
round_report.cursor_after_ref);
} else if (round_report.cursor_present) {
amduatd_fed_until_report_set_cursor(
out_report,
round_report.cursor_has_logseq,
round_report.cursor_logseq,
round_report.cursor_ref_set,
round_report.cursor_ref);
} else {
amduatd_fed_until_report_clear_cursor(out_report);
}
if (round_report.plan_record_count == 0u) {
out_report->caught_up = true;
amduatd_fed_push_apply_report_free(&round_report);
return AMDUATD_FED_PUSH_APPLY_OK;
}
amduatd_fed_push_apply_report_free(&round_report);
}
out_report->caught_up = false;
return AMDUATD_FED_PUSH_APPLY_OK;
}

65
src/amduatd_fed_until.h Normal file
View file

@ -0,0 +1,65 @@
#ifndef AMDUATD_FED_UNTIL_H
#define AMDUATD_FED_UNTIL_H
#include "amduatd_fed_pull_apply.h"
#include "amduatd_fed_push_apply.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
const char *peer_key;
const amduatd_space_t *effective_space;
uint64_t limit;
uint64_t max_rounds;
uint64_t rounds_executed;
bool caught_up;
size_t total_records;
size_t total_artifacts;
bool cursor_has_logseq;
uint64_t cursor_logseq;
bool cursor_ref_set;
amduat_reference_t cursor_ref;
int remote_status;
char error[256];
} amduatd_fed_until_report_t;
void amduatd_fed_until_report_init(amduatd_fed_until_report_t *report);
void amduatd_fed_until_report_free(amduatd_fed_until_report_t *report);
amduatd_fed_pull_apply_status_t amduatd_fed_pull_until(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
uint64_t limit,
uint64_t max_rounds,
const amduatd_fed_cfg_t *fed_cfg,
const amduatd_fed_pull_transport_t *transport,
amduatd_fed_until_report_t *out_report);
amduatd_fed_push_apply_status_t amduatd_fed_push_until(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const char *peer_key,
const char *remote_space_id,
uint64_t limit,
uint64_t max_rounds,
const char *root_path,
const amduatd_fed_cfg_t *fed_cfg,
const amduatd_fed_push_transport_t *transport,
amduatd_fed_until_report_t *out_report);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUATD_FED_UNTIL_H */

833
src/amduatd_http.c Normal file
View file

@ -0,0 +1,833 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#define _POSIX_C_SOURCE 200809L
#include "amduatd_http.h"
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <string.h>
#include <unistd.h>
typedef struct amduatd_http_strbuf {
char *data;
size_t len;
size_t cap;
} amduatd_http_strbuf_t;
static void amduatd_http_strbuf_free(amduatd_http_strbuf_t *b) {
if (b == NULL) {
return;
}
free(b->data);
b->data = NULL;
b->len = 0;
b->cap = 0;
}
static bool amduatd_http_strbuf_reserve(amduatd_http_strbuf_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 != 0 ? 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_http_strbuf_append(amduatd_http_strbuf_t *b,
const char *s,
size_t n) {
if (b == NULL) {
return false;
}
if (n == 0) {
return true;
}
if (s == NULL) {
return false;
}
if (!amduatd_http_strbuf_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_http_strbuf_append_cstr(amduatd_http_strbuf_t *b,
const char *s) {
return amduatd_http_strbuf_append(
b, s != NULL ? s : "", s != NULL ? strlen(s) : 0u);
}
bool amduatd_write_all(int fd, const uint8_t *buf, size_t len) {
size_t off = 0;
while (off < len) {
ssize_t n = write(fd, buf + off, len - off);
if (n < 0) {
if (errno == EINTR) {
continue;
}
return false;
}
if (n == 0) {
return false;
}
off += (size_t)n;
}
return true;
}
bool amduatd_read_exact(int fd, uint8_t *buf, size_t len) {
size_t off = 0;
while (off < len) {
ssize_t n = read(fd, buf + off, len - off);
if (n < 0) {
if (errno == EINTR) {
continue;
}
return false;
}
if (n == 0) {
return false;
}
off += (size_t)n;
}
return true;
}
bool amduatd_read_urandom(uint8_t *out, size_t len) {
int fd;
if (out == NULL || len == 0) {
return false;
}
fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) {
return false;
}
if (!amduatd_read_exact(fd, out, len)) {
close(fd);
return false;
}
close(fd);
return true;
}
static bool amduatd_read_line(int fd, char *buf, size_t cap, size_t *out_len) {
size_t len = 0;
while (len + 1 < cap) {
char c = 0;
ssize_t n = read(fd, &c, 1);
if (n < 0) {
if (errno == EINTR) {
continue;
}
return false;
}
if (n == 0) {
return false;
}
if (c == '\n') {
break;
}
buf[len++] = c;
}
if (len == 0) {
return false;
}
if (len > 0 && buf[len - 1] == '\r') {
len--;
}
buf[len] = '\0';
if (out_len != NULL) {
*out_len = len;
}
return true;
}
static void amduatd_http_req_init(amduatd_http_req_t *req) {
memset(req, 0, sizeof(*req));
req->effective_space = NULL;
}
bool amduatd_http_parse_request(int fd, amduatd_http_req_t *out_req) {
char line[2048];
size_t line_len = 0;
char *sp1 = NULL;
char *sp2 = NULL;
if (out_req == NULL) {
return false;
}
amduatd_http_req_init(out_req);
if (!amduatd_read_line(fd, line, sizeof(line), &line_len)) {
return false;
}
sp1 = strchr(line, ' ');
if (sp1 == NULL) {
return false;
}
*sp1++ = '\0';
sp2 = strchr(sp1, ' ');
if (sp2 == NULL) {
return false;
}
*sp2++ = '\0';
if (strlen(line) >= sizeof(out_req->method)) {
return false;
}
strncpy(out_req->method, line, sizeof(out_req->method) - 1);
if (strlen(sp1) >= sizeof(out_req->path)) {
return false;
}
strncpy(out_req->path, sp1, sizeof(out_req->path) - 1);
for (;;) {
if (!amduatd_read_line(fd, line, sizeof(line), &line_len)) {
return false;
}
if (line_len == 0) {
break;
}
if (strncasecmp(line, "Content-Length:", 15) == 0) {
const char *v = line + 15;
while (*v == ' ' || *v == '\t') {
v++;
}
out_req->content_length = (size_t)strtoull(v, NULL, 10);
} else if (strncasecmp(line, "Content-Type:", 13) == 0) {
const char *v = line + 13;
while (*v == ' ' || *v == '\t') {
v++;
}
strncpy(out_req->content_type, v, sizeof(out_req->content_type) - 1);
} else if (strncasecmp(line, "Accept:", 7) == 0) {
const char *v = line + 7;
while (*v == ' ' || *v == '\t') {
v++;
}
strncpy(out_req->accept, v, sizeof(out_req->accept) - 1);
} else if (strncasecmp(line, "If-Match:", 9) == 0) {
const char *v = line + 9;
while (*v == ' ' || *v == '\t') {
v++;
}
strncpy(out_req->if_match, v, sizeof(out_req->if_match) - 1);
} else if (strncasecmp(line, "X-Amduat-Type-Tag:", 18) == 0) {
const char *v = line + 18;
while (*v == ' ' || *v == '\t') {
v++;
}
strncpy(out_req->x_type_tag, v, sizeof(out_req->x_type_tag) - 1);
} else if (strncasecmp(line, "X-Amduat-Capability:", 20) == 0) {
const char *v = line + 20;
while (*v == ' ' || *v == '\t') {
v++;
}
strncpy(out_req->x_capability, v, sizeof(out_req->x_capability) - 1);
} else if (strncasecmp(line, "X-Amduat-Space:", 15) == 0) {
const char *v = line + 15;
while (*v == ' ' || *v == '\t') {
v++;
}
strncpy(out_req->x_space, v, sizeof(out_req->x_space) - 1);
}
}
return true;
}
void amduatd_http_req_free(amduatd_http_req_t *req) {
if (req == NULL) {
return;
}
free((void *)req->actor.data);
req->actor = amduat_octets(NULL, 0u);
req->has_actor = false;
req->has_uid = false;
req->uid = 0;
}
bool amduatd_http_send_response(int fd, const amduatd_http_resp_t *resp) {
if (resp == NULL) {
return false;
}
return amduatd_http_send_status(fd,
resp->status,
resp->reason,
resp->content_type,
resp->body,
resp->body_len,
resp->head_only);
}
bool amduatd_http_send_status(int fd,
int code,
const char *reason,
const char *content_type,
const uint8_t *body,
size_t body_len,
bool head_only) {
char hdr[512];
int n = snprintf(hdr,
sizeof(hdr),
"HTTP/1.1 %d %s\r\n"
"Content-Length: %zu\r\n"
"Content-Type: %s\r\n"
"Connection: close\r\n"
"\r\n",
code,
reason != NULL ? reason : "",
body_len,
content_type != NULL ? content_type : "text/plain");
if (n <= 0 || (size_t)n >= sizeof(hdr)) {
return false;
}
if (!amduatd_write_all(fd, (const uint8_t *)hdr, (size_t)n)) {
return false;
}
if (!head_only && body != NULL && body_len != 0) {
return amduatd_write_all(fd, body, body_len);
}
return true;
}
bool amduatd_http_send_text(int fd,
int code,
const char *reason,
const char *text,
bool head_only) {
const uint8_t *body = (const uint8_t *)(text != NULL ? text : "");
size_t len = text != NULL ? strlen(text) : 0;
return amduatd_http_send_status(
fd, code, reason, "text/plain; charset=utf-8", body, len, head_only);
}
bool amduatd_http_send_json(int fd,
int code,
const char *reason,
const char *json,
bool head_only) {
const uint8_t *body = (const uint8_t *)(json != NULL ? json : "{}");
size_t len = json != NULL ? strlen(json) : 2;
return amduatd_http_send_status(
fd, code, reason, "application/json", body, len, head_only);
}
static bool amduatd_http_req_wants_html(const amduatd_http_req_t *req) {
if (req == NULL) {
return false;
}
if (req->accept[0] == '\0') {
return false;
}
return strstr(req->accept, "text/html") != NULL;
}
bool amduatd_http_send_not_found(int fd, const amduatd_http_req_t *req) {
if (amduatd_http_req_wants_html(req)) {
const char *path = (req != NULL && req->path[0] != '\0') ? req->path : "/";
const char *method =
(req != NULL && req->method[0] != '\0') ? req->method : "GET";
char html[4096];
int n = snprintf(
html,
sizeof(html),
"<!doctype html>\n"
"<html lang=\"en\">\n"
"<head>\n"
" <meta charset=\"utf-8\" />\n"
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n"
" <title>amduatd — Not Found</title>\n"
" <style>\n"
" :root{color-scheme:light dark;}\n"
" body{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Arial,sans-serif;"
" margin:0;line-height:1.4;}\n"
" .wrap{max-width:860px;margin:0 auto;padding:40px 20px;}\n"
" .card{border:1px solid rgba(127,127,127,.35);border-radius:14px;padding:22px;"
" background:rgba(127,127,127,.06);}\n"
" h1{margin:0 0 6px;font-size:22px;}\n"
" p{margin:8px 0;opacity:.9;}\n"
" code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace;"
" font-size:13px;}\n"
" ul{margin:10px 0 0;padding-left:18px;}\n"
" a{color:inherit;}\n"
" .muted{opacity:.75;}\n"
" </style>\n"
"</head>\n"
"<body>\n"
" <div class=\"wrap\">\n"
" <div class=\"card\">\n"
" <h1>404 — Not Found</h1>\n"
" <p>amduatd didnt recognize <code>%s %s</code>.</p>\n"
" <p class=\"muted\">Try one of these:</p>\n"
" <ul>\n"
" <li><a href=\"/v1/meta\">/v1/meta</a></li>\n"
" <li><a href=\"/v1/contract\">/v1/contract</a></li>\n"
" </ul>\n"
" <p class=\"muted\">Artifact bytes: <code>/v1/artifacts/&lt;ref&gt;</code></p>\n"
" </div>\n"
" </div>\n"
"</body>\n"
"</html>\n",
method,
path);
if (n <= 0 || (size_t)n >= sizeof(html)) {
return amduatd_http_send_text(fd, 404, "Not Found", "not found\n", false);
}
return amduatd_http_send_status(fd,
404,
"Not Found",
"text/html; charset=utf-8",
(const uint8_t *)html,
(size_t)n,
false);
}
return amduatd_http_send_text(fd, 404, "Not Found", "not found\n", false);
}
bool amduatd_send_json_error(int fd,
int code,
const char *reason,
const char *msg) {
amduatd_http_strbuf_t b;
memset(&b, 0, sizeof(b));
if (!amduatd_http_strbuf_append_cstr(&b, "{\"error\":\"") ||
!amduatd_http_strbuf_append_cstr(&b, msg != NULL ? msg : "error") ||
!amduatd_http_strbuf_append_cstr(&b, "\"}\n")) {
amduatd_http_strbuf_free(&b);
return amduatd_http_send_text(fd, 500, "Internal Server Error", "error\n",
false);
}
{
bool ok = amduatd_http_send_json(fd, code, reason, b.data, false);
amduatd_http_strbuf_free(&b);
return ok;
}
}
const char *amduatd_query_param(const char *path,
const char *key,
char *out_value,
size_t out_cap) {
const char *q = strchr(path, '?');
size_t key_len = 0;
if (out_value != NULL && out_cap != 0) {
out_value[0] = '\0';
}
if (q == NULL || key == NULL || out_value == NULL || out_cap == 0) {
return NULL;
}
q++;
key_len = strlen(key);
while (*q != '\0') {
const char *k = q;
const char *eq = strchr(k, '=');
const char *amp = strchr(k, '&');
size_t klen = 0;
const char *v = NULL;
size_t vlen = 0;
if (amp == NULL) {
amp = q + strlen(q);
}
if (eq == NULL || eq > amp) {
q = (*amp == '&') ? (amp + 1) : amp;
continue;
}
klen = (size_t)(eq - k);
if (klen == key_len && strncmp(k, key, key_len) == 0) {
v = eq + 1;
vlen = (size_t)(amp - v);
if (vlen >= out_cap) {
vlen = out_cap - 1;
}
memcpy(out_value, v, vlen);
out_value[vlen] = '\0';
return out_value;
}
q = (*amp == '&') ? (amp + 1) : amp;
}
return NULL;
}
bool amduatd_path_without_query(const char *path, char *out, size_t cap) {
const char *q = NULL;
size_t n = 0;
if (out == NULL || cap == 0) {
return false;
}
out[0] = '\0';
if (path == NULL) {
return false;
}
q = strchr(path, '?');
n = q != NULL ? (size_t)(q - path) : strlen(path);
if (n >= cap) {
n = cap - 1;
}
memcpy(out, path, n);
out[n] = '\0';
return true;
}
bool amduatd_path_extract_name(const char *path,
const char *prefix,
char *out,
size_t cap) {
size_t plen;
const char *p;
size_t len;
if (out != NULL && cap != 0) {
out[0] = '\0';
}
if (path == NULL || prefix == NULL || out == NULL || cap == 0) {
return false;
}
plen = strlen(prefix);
if (strncmp(path, prefix, plen) != 0) {
return false;
}
p = path + plen;
if (*p == '\0') {
return false;
}
len = strlen(p);
if (len >= cap) {
len = cap - 1;
}
memcpy(out, p, len);
out[len] = '\0';
return true;
}
const char *amduatd_json_skip_ws(const char *p, const char *end) {
while (p < end) {
char c = *p;
if (c != ' ' && c != '\t' && c != '\n' && c != '\r') {
break;
}
p++;
}
return p;
}
bool amduatd_json_expect(const char **p,
const char *end,
char expected) {
const char *cur;
if (p == NULL || *p == NULL) {
return false;
}
cur = amduatd_json_skip_ws(*p, end);
if (cur >= end || *cur != expected) {
return false;
}
*p = cur + 1;
return true;
}
bool amduatd_json_parse_string_noesc(const char **p,
const char *end,
const char **out_start,
size_t *out_len) {
const char *cur;
const char *s;
if (out_start != NULL) {
*out_start = NULL;
}
if (out_len != NULL) {
*out_len = 0;
}
if (p == NULL || *p == NULL) {
return false;
}
cur = amduatd_json_skip_ws(*p, end);
if (cur >= end || *cur != '"') {
return false;
}
cur++;
s = cur;
while (cur < end) {
unsigned char c = (unsigned char)*cur;
if (c == '"') {
if (out_start != NULL) {
*out_start = s;
}
if (out_len != NULL) {
*out_len = (size_t)(cur - s);
}
*p = cur + 1;
return true;
}
if (c == '\\' || c < 0x20u) {
return false;
}
cur++;
}
return false;
}
bool amduatd_json_parse_u64(const char **p,
const char *end,
uint64_t *out) {
const char *cur;
const char *start;
unsigned long long v;
char *tmp = NULL;
char *endp = NULL;
size_t n;
if (out != NULL) {
*out = 0;
}
if (p == NULL || *p == NULL || out == NULL) {
return false;
}
cur = amduatd_json_skip_ws(*p, end);
start = cur;
if (cur >= end) {
return false;
}
if (*cur < '0' || *cur > '9') {
return false;
}
cur++;
while (cur < end && *cur >= '0' && *cur <= '9') {
cur++;
}
n = (size_t)(cur - start);
tmp = (char *)malloc(n + 1u);
if (tmp == NULL) {
return false;
}
memcpy(tmp, start, n);
tmp[n] = '\0';
errno = 0;
v = strtoull(tmp, &endp, 10);
if (errno != 0 || endp == NULL || *endp != '\0') {
free(tmp);
return false;
}
free(tmp);
*out = (uint64_t)v;
*p = cur;
return true;
}
static bool amduatd_json_skip_string(const char **p, const char *end) {
const char *cur;
if (p == NULL || *p == NULL) {
return false;
}
cur = amduatd_json_skip_ws(*p, end);
if (cur >= end || *cur != '"') {
return false;
}
cur++;
while (cur < end) {
unsigned char c = (unsigned char)*cur++;
if (c == '"') {
*p = cur;
return true;
}
if (c == '\\') {
if (cur >= end) {
return false;
}
cur++;
} else if (c < 0x20u) {
return false;
}
}
return false;
}
bool amduatd_json_skip_value(const char **p,
const char *end,
int depth);
static bool amduatd_json_skip_array(const char **p,
const char *end,
int depth) {
const char *cur;
if (!amduatd_json_expect(p, end, '[')) {
return false;
}
cur = amduatd_json_skip_ws(*p, end);
if (cur < end && *cur == ']') {
*p = cur + 1;
return true;
}
for (;;) {
if (!amduatd_json_skip_value(p, end, depth + 1)) {
return false;
}
cur = amduatd_json_skip_ws(*p, end);
if (cur >= end) {
return false;
}
if (*cur == ',') {
*p = cur + 1;
continue;
}
if (*cur == ']') {
*p = cur + 1;
return true;
}
return false;
}
}
static bool amduatd_json_skip_object(const char **p,
const char *end,
int depth) {
const char *cur;
if (!amduatd_json_expect(p, end, '{')) {
return false;
}
cur = amduatd_json_skip_ws(*p, end);
if (cur < end && *cur == '}') {
*p = cur + 1;
return true;
}
for (;;) {
if (!amduatd_json_skip_string(p, end)) {
return false;
}
if (!amduatd_json_expect(p, end, ':')) {
return false;
}
if (!amduatd_json_skip_value(p, end, depth + 1)) {
return false;
}
cur = amduatd_json_skip_ws(*p, end);
if (cur >= end) {
return false;
}
if (*cur == ',') {
*p = cur + 1;
continue;
}
if (*cur == '}') {
*p = cur + 1;
return true;
}
return false;
}
}
bool amduatd_json_skip_value(const char **p,
const char *end,
int depth) {
const char *cur;
if (p == NULL || *p == NULL) {
return false;
}
if (depth > 32) {
return false;
}
cur = amduatd_json_skip_ws(*p, end);
if (cur >= end) {
return false;
}
if (*cur == '"') {
return amduatd_json_skip_string(p, end);
}
if (*cur == '{') {
return amduatd_json_skip_object(p, end, depth);
}
if (*cur == '[') {
return amduatd_json_skip_array(p, end, depth);
}
if (*cur == 't' && (end - cur) >= 4 && memcmp(cur, "true", 4) == 0) {
*p = cur + 4;
return true;
}
if (*cur == 'f' && (end - cur) >= 5 && memcmp(cur, "false", 5) == 0) {
*p = cur + 5;
return true;
}
if (*cur == 'n' && (end - cur) >= 4 && memcmp(cur, "null", 4) == 0) {
*p = cur + 4;
return true;
}
if (*cur == '-' || (*cur >= '0' && *cur <= '9')) {
cur++;
while (cur < end) {
char c = *cur;
if ((c >= '0' && c <= '9') || c == '.' || c == 'e' || c == 'E' ||
c == '+' || c == '-') {
cur++;
continue;
}
break;
}
*p = cur;
return true;
}
return false;
}
bool amduatd_copy_json_str(const char *s,
size_t len,
char **out) {
char *buf;
if (out == NULL) {
return false;
}
*out = NULL;
if (len > (SIZE_MAX - 1u)) {
return false;
}
buf = (char *)malloc(len + 1u);
if (buf == NULL) {
return false;
}
if (len != 0) {
memcpy(buf, s, len);
}
buf[len] = '\0';
*out = buf;
return true;
}

120
src/amduatd_http.h Normal file
View file

@ -0,0 +1,120 @@
#ifndef AMDUATD_HTTP_H
#define AMDUATD_HTTP_H
#include "amduat/asl/core.h"
#include "amduatd_space.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
char method[8];
char path[1024];
char content_type[128];
char accept[128];
char if_match[256];
char x_type_tag[64];
char x_capability[2048];
char x_space[AMDUAT_ASL_POINTER_NAME_MAX + 1u];
size_t content_length;
bool has_actor;
amduat_octets_t actor;
bool has_uid;
uid_t uid;
const amduatd_space_t *effective_space;
} amduatd_http_req_t;
typedef struct {
int fd;
int status;
const char *reason;
const char *content_type;
const uint8_t *body;
size_t body_len;
bool head_only;
bool ok;
} amduatd_http_resp_t;
bool amduatd_write_all(int fd, const uint8_t *buf, size_t len);
bool amduatd_read_exact(int fd, uint8_t *buf, size_t len);
bool amduatd_read_urandom(uint8_t *out, size_t len);
bool amduatd_http_parse_request(int fd, amduatd_http_req_t *out_req);
void amduatd_http_req_free(amduatd_http_req_t *req);
bool amduatd_http_send_response(int fd, const amduatd_http_resp_t *resp);
bool amduatd_http_send_status(int fd,
int code,
const char *reason,
const char *content_type,
const uint8_t *body,
size_t body_len,
bool head_only);
bool amduatd_http_send_text(int fd,
int code,
const char *reason,
const char *text,
bool head_only);
bool amduatd_http_send_json(int fd,
int code,
const char *reason,
const char *json,
bool head_only);
bool amduatd_http_send_not_found(int fd, const amduatd_http_req_t *req);
bool amduatd_send_json_error(int fd,
int code,
const char *reason,
const char *msg);
const char *amduatd_query_param(const char *path,
const char *key,
char *out_value,
size_t out_cap);
bool amduatd_path_without_query(const char *path,
char *out,
size_t cap);
bool amduatd_path_extract_name(const char *path,
const char *prefix,
char *out,
size_t cap);
const char *amduatd_json_skip_ws(const char *p, const char *end);
bool amduatd_json_expect(const char **p,
const char *end,
char expected);
bool amduatd_json_parse_string_noesc(const char **p,
const char *end,
const char **out_start,
size_t *out_len);
bool amduatd_json_parse_u64(const char **p, const char *end, uint64_t *out);
bool amduatd_json_skip_value(const char **p,
const char *end,
int depth);
bool amduatd_copy_json_str(const char *s, size_t len, char **out);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUATD_HTTP_H */

297
src/amduatd_space.c Normal file
View file

@ -0,0 +1,297 @@
#include "amduatd_space.h"
#include "amduat/util/log.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
static bool amduatd_space_name_valid(const char *name) {
return amduat_asl_pointer_name_is_valid(name);
}
static bool amduatd_space_strdup_cstr(const char *s, char **out) {
size_t len;
char *buf;
if (out == NULL) {
return false;
}
*out = NULL;
if (s == NULL) {
return false;
}
len = strlen(s);
if (len > SIZE_MAX - 1u) {
return false;
}
buf = (char *)malloc(len + 1u);
if (buf == NULL) {
return false;
}
if (len != 0) {
memcpy(buf, s, len);
}
buf[len] = '\0';
*out = buf;
return true;
}
bool amduatd_space_space_id_is_valid(const char *space_id) {
if (space_id == NULL || space_id[0] == '\0') {
return false;
}
if (strchr(space_id, '/') != NULL) {
return false;
}
return amduat_asl_pointer_name_is_valid(space_id);
}
bool amduatd_space_init(amduatd_space_t *sp,
const char *space_id_or_null,
bool migrate_unscoped_edges) {
size_t space_len = 0u;
if (sp == NULL) {
return false;
}
memset(sp, 0, sizeof(*sp));
sp->migrate_unscoped_edges = migrate_unscoped_edges;
if (space_id_or_null == NULL) {
return true;
}
if (!amduatd_space_space_id_is_valid(space_id_or_null)) {
return false;
}
space_len = strlen(space_id_or_null);
if (space_len > AMDUAT_ASL_POINTER_NAME_MAX) {
return false;
}
memcpy(sp->space_id_buf, space_id_or_null, space_len);
sp->space_id_buf[space_len] = '\0';
sp->space_id = amduat_octets(sp->space_id_buf, space_len);
sp->enabled = true;
return true;
}
void amduatd_space_free(amduatd_space_t *sp) {
if (sp == NULL) {
return;
}
memset(sp, 0, sizeof(*sp));
}
void amduatd_space_log_mapping(const amduatd_space_t *sp,
const char *user_name,
const char *scoped_name) {
if (sp == NULL || !sp->enabled) {
return;
}
if (user_name == NULL || scoped_name == NULL) {
return;
}
amduat_log(AMDUAT_LOG_DEBUG, "%s -> %s", user_name, scoped_name);
}
bool amduatd_space_scope_name(const amduatd_space_t *sp,
const char *user_name,
amduat_octets_t *out_scoped) {
char *buf = NULL;
size_t user_len = 0u;
size_t space_len = 0u;
size_t total_len = 0u;
size_t offset = 0u;
const char *space_id = NULL;
if (out_scoped != NULL) {
*out_scoped = amduat_octets(NULL, 0u);
}
if (user_name == NULL || out_scoped == NULL) {
return false;
}
if (sp == NULL || !sp->enabled || sp->space_id.data == NULL) {
if (!amduatd_space_name_valid(user_name)) {
return false;
}
if (!amduatd_space_strdup_cstr(user_name, &buf)) {
return false;
}
*out_scoped = amduat_octets((const uint8_t *)buf, strlen(buf));
return true;
}
if (strncmp(user_name, "space/", 6u) == 0) {
return false;
}
if (!amduat_asl_pointer_name_is_valid(user_name)) {
return false;
}
space_id = (const char *)sp->space_id.data;
if (!amduatd_space_space_id_is_valid(space_id)) {
return false;
}
user_len = strlen(user_name);
space_len = strlen(space_id);
if (space_len > SIZE_MAX - 7u ||
user_len > SIZE_MAX - (7u + space_len)) {
return false;
}
total_len = 6u + space_len + 1u + user_len;
buf = (char *)malloc(total_len + 1u);
if (buf == NULL) {
return false;
}
memcpy(buf + offset, "space/", 6u);
offset += 6u;
memcpy(buf + offset, space_id, space_len);
offset += space_len;
buf[offset++] = '/';
if (user_len != 0) {
memcpy(buf + offset, user_name, user_len);
offset += user_len;
}
buf[offset] = '\0';
if (!amduat_asl_pointer_name_is_valid(buf)) {
free(buf);
return false;
}
*out_scoped = amduat_octets((const uint8_t *)buf, total_len);
return true;
}
bool amduatd_space_unscoped_name(const amduatd_space_t *sp,
const char *scoped_name,
amduat_octets_t *out_unscoped) {
const char *suffix = NULL;
size_t suffix_len = 0u;
char *buf = NULL;
const char *space_id = NULL;
if (out_unscoped != NULL) {
*out_unscoped = amduat_octets(NULL, 0u);
}
if (out_unscoped == NULL || scoped_name == NULL) {
return false;
}
if (sp == NULL || !sp->enabled || sp->space_id.data == NULL) {
if (!amduatd_space_name_valid(scoped_name)) {
return false;
}
if (!amduatd_space_strdup_cstr(scoped_name, &buf)) {
return false;
}
*out_unscoped = amduat_octets((const uint8_t *)buf, strlen(buf));
return true;
}
space_id = (const char *)sp->space_id.data;
if (!amduatd_space_space_id_is_valid(space_id)) {
return false;
}
{
size_t space_len = strlen(space_id);
size_t prefix_len = 6u + space_len + 1u;
if (strncmp(scoped_name, "space/", 6u) != 0) {
return false;
}
if (strncmp(scoped_name + 6u, space_id, space_len) != 0) {
return false;
}
if (scoped_name[6u + space_len] != '/') {
return false;
}
suffix = scoped_name + prefix_len;
}
if (suffix == NULL || suffix[0] == '\0') {
return false;
}
if (!amduat_asl_pointer_name_is_valid(suffix)) {
return false;
}
suffix_len = strlen(suffix);
if (!amduatd_space_strdup_cstr(suffix, &buf)) {
return false;
}
*out_unscoped = amduat_octets((const uint8_t *)buf, suffix_len);
return true;
}
bool amduatd_space_is_scoped(const amduatd_space_t *sp, const char *name) {
size_t space_len;
const char *space_id = NULL;
if (sp == NULL || name == NULL) {
return false;
}
if (!amduat_asl_pointer_name_is_valid(name)) {
return false;
}
if (!sp->enabled || sp->space_id.data == NULL) {
return true;
}
space_id = (const char *)sp->space_id.data;
space_len = strlen(space_id);
if (strncmp(name, "space/", 6u) != 0) {
return false;
}
if (strncmp(name + 6u, space_id, space_len) != 0) {
return false;
}
if (name[6u + space_len] != '/') {
return false;
}
return true;
}
bool amduatd_space_edges_collection_name(const amduatd_space_t *sp,
amduat_octets_t *out_collection_name) {
return amduatd_space_scope_name(sp, "daemon/edges", out_collection_name);
}
bool amduatd_space_edges_index_head_name(const amduatd_space_t *sp,
amduat_octets_t *out_pointer_name) {
return amduatd_space_scope_name(sp, "daemon/edges/index/head",
out_pointer_name);
}
bool amduatd_space_should_migrate_unscoped_edges(const amduatd_space_t *sp) {
return sp != NULL && sp->enabled && sp->migrate_unscoped_edges;
}
amduatd_space_resolve_status_t amduatd_space_resolve_effective(
const amduatd_space_t *default_space,
const char *request_space_id,
amduatd_space_t *out_space,
const amduatd_space_t **out_effective) {
bool migrate = false;
if (out_space != NULL) {
memset(out_space, 0, sizeof(*out_space));
}
if (out_effective != NULL) {
*out_effective = NULL;
}
if (default_space != NULL) {
migrate = default_space->migrate_unscoped_edges;
}
if (request_space_id != NULL) {
if (request_space_id[0] == '\0') {
return AMDUATD_SPACE_RESOLVE_ERR_INVALID;
}
if (out_space == NULL) {
return AMDUATD_SPACE_RESOLVE_ERR_INVALID;
}
if (!amduatd_space_init(out_space, request_space_id, migrate)) {
return AMDUATD_SPACE_RESOLVE_ERR_INVALID;
}
if (out_effective != NULL) {
*out_effective = out_space;
}
return AMDUATD_SPACE_RESOLVE_OK;
}
if (out_effective != NULL) {
*out_effective = default_space;
}
return AMDUATD_SPACE_RESOLVE_OK;
}

66
src/amduatd_space.h Normal file
View file

@ -0,0 +1,66 @@
#ifndef AMDUATD_SPACE_H
#define AMDUATD_SPACE_H
#include "amduat/asl/asl_pointer_fs.h"
#include "amduat/asl/core.h"
#include <stdbool.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
bool enabled;
amduat_octets_t space_id;
bool migrate_unscoped_edges;
char space_id_buf[AMDUAT_ASL_POINTER_NAME_MAX + 1u];
} amduatd_space_t;
bool amduatd_space_init(amduatd_space_t *sp,
const char *space_id_or_null,
bool migrate_unscoped_edges);
void amduatd_space_free(amduatd_space_t *sp);
bool amduatd_space_scope_name(const amduatd_space_t *sp,
const char *user_name,
amduat_octets_t *out_scoped);
bool amduatd_space_unscoped_name(const amduatd_space_t *sp,
const char *scoped_name,
amduat_octets_t *out_unscoped);
bool amduatd_space_is_scoped(const amduatd_space_t *sp, const char *name);
bool amduatd_space_space_id_is_valid(const char *space_id);
void amduatd_space_log_mapping(const amduatd_space_t *sp,
const char *user_name,
const char *scoped_name);
bool amduatd_space_edges_collection_name(const amduatd_space_t *sp,
amduat_octets_t *out_collection_name);
bool amduatd_space_edges_index_head_name(const amduatd_space_t *sp,
amduat_octets_t *out_pointer_name);
bool amduatd_space_should_migrate_unscoped_edges(const amduatd_space_t *sp);
typedef enum {
AMDUATD_SPACE_RESOLVE_OK = 0,
AMDUATD_SPACE_RESOLVE_ERR_INVALID = 1
} amduatd_space_resolve_status_t;
amduatd_space_resolve_status_t amduatd_space_resolve_effective(
const amduatd_space_t *default_space,
const char *request_space_id,
amduatd_space_t *out_space,
const amduatd_space_t **out_effective);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUATD_SPACE_H */

860
src/amduatd_space_doctor.c Normal file
View file

@ -0,0 +1,860 @@
#include "amduatd_space_doctor.h"
#include "amduat/asl/record.h"
#include "amduat/enc/asl_log.h"
#include "amduat/enc/asl1_core_codec.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char *data;
size_t len;
size_t cap;
} amduatd_doctor_strbuf_t;
static void amduatd_doctor_strbuf_free(amduatd_doctor_strbuf_t *b) {
if (b == NULL) {
return;
}
free(b->data);
b->data = NULL;
b->len = 0u;
b->cap = 0u;
}
static bool amduatd_doctor_strbuf_reserve(amduatd_doctor_strbuf_t *b,
size_t extra) {
size_t need;
size_t next_cap;
char *next;
if (b == NULL) {
return false;
}
if (extra == 0u) {
return true;
}
if (b->len > SIZE_MAX - extra) {
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_doctor_strbuf_append(amduatd_doctor_strbuf_t *b,
const char *data,
size_t len) {
if (b == NULL) {
return false;
}
if (data == NULL) {
len = 0u;
}
if (!amduatd_doctor_strbuf_reserve(b, len + 1u)) {
return false;
}
if (len != 0u) {
memcpy(b->data + b->len, data, len);
}
b->len += len;
b->data[b->len] = '\0';
return true;
}
static bool amduatd_doctor_strbuf_append_cstr(amduatd_doctor_strbuf_t *b,
const char *s) {
return amduatd_doctor_strbuf_append(b,
s != NULL ? s : "",
s != NULL ? strlen(s) : 0u);
}
static bool amduatd_doctor_strbuf_append_char(amduatd_doctor_strbuf_t *b,
char c) {
return amduatd_doctor_strbuf_append(b, &c, 1u);
}
static char *amduatd_doctor_strdup(const char *s) {
size_t len;
char *copy;
if (s == NULL) {
return NULL;
}
len = strlen(s);
copy = (char *)malloc(len + 1u);
if (copy == NULL) {
return NULL;
}
memcpy(copy, s, len);
copy[len] = '\0';
return copy;
}
static const char *amduatd_space_doctor_status_name(
amduatd_space_doctor_status_t status) {
switch (status) {
case AMDUATD_DOCTOR_OK:
return "ok";
case AMDUATD_DOCTOR_WARN:
return "warn";
case AMDUATD_DOCTOR_FAIL:
return "fail";
case AMDUATD_DOCTOR_SKIPPED:
return "skipped";
default:
return "unknown";
}
}
static void amduatd_space_doctor_report_init(
amduatd_space_doctor_report_t *report) {
if (report == NULL) {
return;
}
memset(report, 0, sizeof(*report));
report->backend = AMDUATD_STORE_BACKEND_FS;
}
static void amduatd_space_doctor_report_clear(
amduatd_space_doctor_report_t *report) {
if (report == NULL) {
return;
}
for (size_t i = 0u; i < report->checks_len; ++i) {
free(report->checks[i].name);
free(report->checks[i].detail);
}
free(report->checks);
amduatd_space_doctor_report_init(report);
}
void amduatd_space_doctor_report_free(amduatd_space_doctor_report_t *report) {
amduatd_space_doctor_report_clear(report);
}
static bool amduatd_space_doctor_report_add(
amduatd_space_doctor_report_t *report,
const char *name,
amduatd_space_doctor_status_t status,
const char *detail) {
size_t next_len;
amduatd_space_doctor_check_t *next;
amduatd_space_doctor_check_t *entry;
if (report == NULL || name == NULL) {
return false;
}
if (report->checks_len > SIZE_MAX - 1u) {
return false;
}
next_len = report->checks_len + 1u;
next = (amduatd_space_doctor_check_t *)realloc(
report->checks, next_len * sizeof(*next));
if (next == NULL) {
return false;
}
report->checks = next;
entry = &report->checks[report->checks_len];
memset(entry, 0, sizeof(*entry));
entry->name = amduatd_doctor_strdup(name);
entry->detail = amduatd_doctor_strdup(detail != NULL ? detail : "");
if (entry->name == NULL || entry->detail == NULL) {
free(entry->name);
free(entry->detail);
return false;
}
entry->status = status;
report->checks_len = next_len;
switch (status) {
case AMDUATD_DOCTOR_OK:
report->ok_count++;
break;
case AMDUATD_DOCTOR_WARN:
report->warn_count++;
break;
case AMDUATD_DOCTOR_FAIL:
report->fail_count++;
break;
case AMDUATD_DOCTOR_SKIPPED:
report->skipped_count++;
break;
default:
break;
}
return true;
}
static bool amduatd_space_doctor_build_collection_head_name(
const char *name,
char **out_name) {
size_t name_len;
size_t total_len;
char *buffer;
size_t offset = 0u;
if (name == NULL || out_name == NULL) {
return false;
}
if (!amduat_asl_pointer_name_is_valid(name)) {
return false;
}
name_len = strlen(name);
total_len = 11u + name_len + 5u + 1u;
buffer = (char *)malloc(total_len);
if (buffer == NULL) {
return false;
}
memcpy(buffer + offset, "collection/", 11u);
offset += 11u;
memcpy(buffer + offset, name, name_len);
offset += name_len;
memcpy(buffer + offset, "/head", 5u);
offset += 5u;
buffer[offset] = '\0';
*out_name = buffer;
return true;
}
static bool amduatd_space_doctor_build_collection_log_head_name(
const char *name,
char **out_name) {
size_t name_len;
size_t log_name_len;
size_t total_len;
char *buffer;
size_t offset = 0u;
if (name == NULL || out_name == NULL) {
return false;
}
if (!amduat_asl_pointer_name_is_valid(name)) {
return false;
}
name_len = strlen(name);
log_name_len = 11u + name_len + 4u;
total_len = 4u + log_name_len + 5u + 1u;
buffer = (char *)malloc(total_len);
if (buffer == NULL) {
return false;
}
memcpy(buffer + offset, "log/", 4u);
offset += 4u;
memcpy(buffer + offset, "collection/", 11u);
offset += 11u;
memcpy(buffer + offset, name, name_len);
offset += name_len;
memcpy(buffer + offset, "/log", 4u);
offset += 4u;
memcpy(buffer + offset, "/head", 5u);
offset += 5u;
buffer[offset] = '\0';
*out_name = buffer;
return true;
}
enum {
AMDUATD_EDGE_INDEX_MAGIC_LEN = 8,
AMDUATD_EDGE_INDEX_VERSION = 1
};
static const uint8_t k_amduatd_edge_index_magic[AMDUATD_EDGE_INDEX_MAGIC_LEN] = {
'A', 'S', 'L', 'E', 'I', 'X', '1', '\0'
};
typedef struct {
bool has_graph_ref;
amduat_reference_t graph_ref;
} amduatd_edge_index_state_view_t;
static void amduatd_edge_index_state_view_free(
amduatd_edge_index_state_view_t *state) {
if (state == NULL) {
return;
}
if (state->has_graph_ref) {
amduat_reference_free(&state->graph_ref);
}
memset(state, 0, sizeof(*state));
}
static bool amduatd_doctor_read_u32_le(const uint8_t *data,
size_t len,
size_t *offset,
uint32_t *out) {
if (len - *offset < 4u) {
return false;
}
*out = (uint32_t)data[*offset] |
((uint32_t)data[*offset + 1u] << 8) |
((uint32_t)data[*offset + 2u] << 16) |
((uint32_t)data[*offset + 3u] << 24);
*offset += 4u;
return true;
}
static bool amduatd_doctor_read_u64_le(const uint8_t *data,
size_t len,
size_t *offset,
uint64_t *out) {
if (len - *offset < 8u) {
return false;
}
*out = (uint64_t)data[*offset] |
((uint64_t)data[*offset + 1u] << 8) |
((uint64_t)data[*offset + 2u] << 16) |
((uint64_t)data[*offset + 3u] << 24) |
((uint64_t)data[*offset + 4u] << 32) |
((uint64_t)data[*offset + 5u] << 40) |
((uint64_t)data[*offset + 6u] << 48) |
((uint64_t)data[*offset + 7u] << 56);
*offset += 8u;
return true;
}
static bool amduatd_edge_index_state_view_decode(
amduat_octets_t payload,
amduatd_edge_index_state_view_t *out_state) {
size_t offset = 0u;
uint32_t version = 0u;
uint32_t ref_len = 0u;
amduat_octets_t ref_bytes;
uint64_t ignored_offset = 0u;
if (out_state == NULL) {
return false;
}
memset(out_state, 0, sizeof(*out_state));
if (payload.data == NULL ||
payload.len < AMDUATD_EDGE_INDEX_MAGIC_LEN + 4u + 8u + 4u) {
return false;
}
if (memcmp(payload.data, k_amduatd_edge_index_magic,
AMDUATD_EDGE_INDEX_MAGIC_LEN) != 0) {
return false;
}
offset += AMDUATD_EDGE_INDEX_MAGIC_LEN;
if (!amduatd_doctor_read_u32_le(payload.data, payload.len, &offset, &version) ||
version != AMDUATD_EDGE_INDEX_VERSION) {
return false;
}
if (!amduatd_doctor_read_u64_le(payload.data, payload.len, &offset,
&ignored_offset) ||
!amduatd_doctor_read_u32_le(payload.data, payload.len, &offset, &ref_len)) {
return false;
}
(void)ignored_offset;
if (payload.len - offset < ref_len) {
return false;
}
if (ref_len != 0u) {
ref_bytes = amduat_octets(payload.data + offset, ref_len);
if (!amduat_enc_asl1_core_decode_reference_v1(ref_bytes,
&out_state->graph_ref)) {
amduatd_edge_index_state_view_free(out_state);
return false;
}
out_state->has_graph_ref = true;
offset += ref_len;
}
return offset == payload.len;
}
static amduatd_store_backend_t amduatd_space_doctor_backend_from_store(
const amduat_asl_store_t *store) {
if (store == NULL) {
return AMDUATD_STORE_BACKEND_FS;
}
if (store->ops.log_scan != NULL && store->ops.current_state != NULL) {
return AMDUATD_STORE_BACKEND_INDEX;
}
return AMDUATD_STORE_BACKEND_FS;
}
typedef struct {
const char *label;
char *pointer_name;
bool pointer_exists;
amduat_reference_t pointer_ref;
} amduatd_doctor_pointer_t;
static void amduatd_space_doctor_pointer_free(amduatd_doctor_pointer_t *ptr) {
if (ptr == NULL) {
return;
}
free(ptr->pointer_name);
ptr->pointer_name = NULL;
ptr->pointer_exists = false;
amduat_reference_free(&ptr->pointer_ref);
}
bool amduatd_space_doctor_run(amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const amduatd_cfg_t *cfg,
const amduatd_fed_cfg_t *fed_cfg,
amduatd_space_doctor_report_t *out_report) {
amduatd_doctor_pointer_t pointers[3];
amduat_octets_t edges_collection = amduat_octets(NULL, 0u);
amduat_octets_t edges_index_head = amduat_octets(NULL, 0u);
char *collection_head = NULL;
char *collection_log_head = NULL;
bool names_ok = true;
bool ok = false;
size_t i;
if (out_report == NULL) {
return false;
}
amduatd_space_doctor_report_init(out_report);
if (store == NULL || pointer_store == NULL || cfg == NULL) {
return false;
}
(void)cfg;
out_report->backend = amduatd_space_doctor_backend_from_store(store);
if (effective_space != NULL &&
effective_space->enabled &&
effective_space->space_id.data != NULL) {
out_report->scoped = true;
snprintf(out_report->space_id, sizeof(out_report->space_id), "%s",
(const char *)effective_space->space_id.data);
}
memset(pointers, 0, sizeof(pointers));
pointers[0].label = "edges_index_head";
pointers[1].label = "edges_collection_snapshot_head";
pointers[2].label = "edges_collection_log_head";
for (i = 0u; i < 3u; ++i) {
memset(&pointers[i].pointer_ref, 0, sizeof(pointers[i].pointer_ref));
}
if (!amduatd_space_edges_collection_name(effective_space, &edges_collection) ||
!amduatd_space_edges_index_head_name(effective_space, &edges_index_head)) {
(void)amduatd_space_doctor_report_add(out_report,
"pointer_name_validation",
AMDUATD_DOCTOR_FAIL,
"failed to scope pointer names");
names_ok = false;
goto doctor_after_names;
}
if (!amduatd_space_doctor_build_collection_head_name(
(const char *)edges_collection.data, &collection_head) ||
!amduatd_space_doctor_build_collection_log_head_name(
(const char *)edges_collection.data, &collection_log_head)) {
(void)amduatd_space_doctor_report_add(out_report,
"pointer_name_validation",
AMDUATD_DOCTOR_FAIL,
"failed to build collection heads");
names_ok = false;
goto doctor_after_names;
}
if (!amduat_asl_pointer_name_is_valid(
(const char *)edges_collection.data) ||
!amduat_asl_pointer_name_is_valid(
(const char *)edges_index_head.data) ||
!amduat_asl_pointer_name_is_valid(collection_head) ||
!amduat_asl_pointer_name_is_valid(collection_log_head)) {
(void)amduatd_space_doctor_report_add(out_report,
"pointer_name_validation",
AMDUATD_DOCTOR_FAIL,
"invalid pointer name encoding");
names_ok = false;
goto doctor_after_names;
}
(void)amduatd_space_doctor_report_add(out_report,
"pointer_name_validation",
AMDUATD_DOCTOR_OK,
"scoped pointer names valid");
doctor_after_names:
if (!names_ok) {
(void)amduatd_space_doctor_report_add(out_report,
"edges_index_head",
AMDUATD_DOCTOR_SKIPPED,
"invalid pointer names");
(void)amduatd_space_doctor_report_add(out_report,
"edges_collection_snapshot_head",
AMDUATD_DOCTOR_SKIPPED,
"invalid pointer names");
(void)amduatd_space_doctor_report_add(out_report,
"edges_collection_log_head",
AMDUATD_DOCTOR_SKIPPED,
"invalid pointer names");
(void)amduatd_space_doctor_report_add(out_report,
"cas_edges_index_head",
AMDUATD_DOCTOR_SKIPPED,
"invalid pointer names");
(void)amduatd_space_doctor_report_add(out_report,
"cas_edges_collection_snapshot_head",
AMDUATD_DOCTOR_SKIPPED,
"invalid pointer names");
(void)amduatd_space_doctor_report_add(out_report,
"cas_edges_collection_log_head",
AMDUATD_DOCTOR_SKIPPED,
"invalid pointer names");
(void)amduatd_space_doctor_report_add(out_report,
"edge_index_state_parse",
AMDUATD_DOCTOR_SKIPPED,
"invalid pointer names");
(void)amduatd_space_doctor_report_add(out_report,
"edge_index_graph_ref",
AMDUATD_DOCTOR_SKIPPED,
"invalid pointer names");
goto doctor_index_checks;
}
pointers[0].pointer_name = amduatd_doctor_strdup(
(const char *)edges_index_head.data);
pointers[1].pointer_name = amduatd_doctor_strdup(collection_head);
pointers[2].pointer_name = amduatd_doctor_strdup(collection_log_head);
if (pointers[0].pointer_name == NULL ||
pointers[1].pointer_name == NULL ||
pointers[2].pointer_name == NULL) {
(void)amduatd_space_doctor_report_add(out_report,
"pointer_store",
AMDUATD_DOCTOR_FAIL,
"oom");
goto doctor_cleanup;
}
for (i = 0u; i < 3u; ++i) {
bool exists = false;
amduat_asl_pointer_error_t perr =
amduat_asl_pointer_get(pointer_store,
pointers[i].pointer_name,
&exists,
&pointers[i].pointer_ref);
if (perr != AMDUAT_ASL_POINTER_OK) {
char detail[256];
snprintf(detail, sizeof(detail), "pointer error: %s",
pointers[i].pointer_name);
(void)amduatd_space_doctor_report_add(out_report,
pointers[i].label,
AMDUATD_DOCTOR_FAIL,
detail);
continue;
}
pointers[i].pointer_exists = exists;
if (exists) {
char detail[256];
snprintf(detail, sizeof(detail), "present: %s",
pointers[i].pointer_name);
(void)amduatd_space_doctor_report_add(out_report,
pointers[i].label,
AMDUATD_DOCTOR_OK,
detail);
} else {
char detail[256];
snprintf(detail, sizeof(detail), "missing: %s",
pointers[i].pointer_name);
(void)amduatd_space_doctor_report_add(out_report,
pointers[i].label,
AMDUATD_DOCTOR_WARN,
detail);
}
}
for (i = 0u; i < 3u; ++i) {
const char *check_name = NULL;
switch (i) {
case 0u:
check_name = "cas_edges_index_head";
break;
case 1u:
check_name = "cas_edges_collection_snapshot_head";
break;
case 2u:
check_name = "cas_edges_collection_log_head";
break;
default:
check_name = "cas_pointer_ref";
break;
}
if (!pointers[i].pointer_exists) {
(void)amduatd_space_doctor_report_add(out_report,
check_name,
AMDUATD_DOCTOR_SKIPPED,
"pointer missing");
continue;
}
{
amduat_artifact_t artifact;
memset(&artifact, 0, sizeof(artifact));
if (amduat_asl_store_get(store, pointers[i].pointer_ref, &artifact) !=
AMDUAT_ASL_STORE_OK) {
(void)amduatd_space_doctor_report_add(out_report,
check_name,
AMDUATD_DOCTOR_FAIL,
"store get failed");
} else {
(void)amduatd_space_doctor_report_add(out_report,
check_name,
AMDUATD_DOCTOR_OK,
"ref readable");
}
amduat_artifact_free(&artifact);
}
}
if (!pointers[0].pointer_exists) {
(void)amduatd_space_doctor_report_add(out_report,
"edge_index_state_parse",
AMDUATD_DOCTOR_SKIPPED,
"pointer missing");
(void)amduatd_space_doctor_report_add(out_report,
"edge_index_graph_ref",
AMDUATD_DOCTOR_SKIPPED,
"pointer missing");
} else {
amduat_asl_record_t record;
amduatd_edge_index_state_view_t state;
const char *schema = "tgk/edge_index_state";
bool parsed = false;
memset(&record, 0, sizeof(record));
memset(&state, 0, sizeof(state));
if (amduat_asl_record_store_get(store,
pointers[0].pointer_ref,
&record) != AMDUAT_ASL_STORE_OK) {
(void)amduatd_space_doctor_report_add(out_report,
"edge_index_state_parse",
AMDUATD_DOCTOR_FAIL,
"record load failed");
} else if (record.schema.len != strlen(schema) ||
memcmp(record.schema.data, schema, record.schema.len) != 0) {
(void)amduatd_space_doctor_report_add(out_report,
"edge_index_state_parse",
AMDUATD_DOCTOR_FAIL,
"unexpected schema");
} else if (!amduatd_edge_index_state_view_decode(record.payload, &state)) {
(void)amduatd_space_doctor_report_add(out_report,
"edge_index_state_parse",
AMDUATD_DOCTOR_FAIL,
"payload decode failed");
} else {
(void)amduatd_space_doctor_report_add(out_report,
"edge_index_state_parse",
AMDUATD_DOCTOR_OK,
"edge index state decoded");
parsed = true;
}
if (parsed && state.has_graph_ref) {
amduat_artifact_t graph_artifact;
memset(&graph_artifact, 0, sizeof(graph_artifact));
if (amduat_asl_store_get(store, state.graph_ref, &graph_artifact) !=
AMDUAT_ASL_STORE_OK) {
(void)amduatd_space_doctor_report_add(out_report,
"edge_index_graph_ref",
AMDUATD_DOCTOR_FAIL,
"graph ref missing");
} else {
(void)amduatd_space_doctor_report_add(out_report,
"edge_index_graph_ref",
AMDUATD_DOCTOR_OK,
"graph ref readable");
}
amduat_artifact_free(&graph_artifact);
} else if (parsed) {
(void)amduatd_space_doctor_report_add(out_report,
"edge_index_graph_ref",
AMDUATD_DOCTOR_SKIPPED,
"no graph ref");
} else {
(void)amduatd_space_doctor_report_add(out_report,
"edge_index_graph_ref",
AMDUATD_DOCTOR_SKIPPED,
"edge index state unreadable");
}
amduatd_edge_index_state_view_free(&state);
amduat_asl_record_free(&record);
}
doctor_index_checks:
if (out_report->backend == AMDUATD_STORE_BACKEND_INDEX) {
amduat_asl_index_state_t state;
amduat_asl_log_record_t *records = NULL;
size_t record_count = 0u;
if (!amduat_asl_index_current_state(store, &state)) {
(void)amduatd_space_doctor_report_add(out_report,
"index_current_state",
AMDUATD_DOCTOR_FAIL,
"current_state failed");
} else {
(void)amduatd_space_doctor_report_add(out_report,
"index_current_state",
AMDUATD_DOCTOR_OK,
"current_state ok");
}
{
amduat_asl_store_error_t scan_err =
amduat_asl_log_scan(store, &records, &record_count);
if (scan_err == AMDUAT_ASL_STORE_OK) {
(void)amduatd_space_doctor_report_add(out_report,
"index_log_scan",
AMDUATD_DOCTOR_OK,
"log_scan ok");
} else {
(void)amduatd_space_doctor_report_add(out_report,
"index_log_scan",
AMDUATD_DOCTOR_FAIL,
"log_scan failed");
}
if (records != NULL) {
amduat_enc_asl_log_free(records, record_count);
}
}
} else {
(void)amduatd_space_doctor_report_add(out_report,
"index_current_state",
AMDUATD_DOCTOR_SKIPPED,
"requires index backend");
(void)amduatd_space_doctor_report_add(out_report,
"index_log_scan",
AMDUATD_DOCTOR_SKIPPED,
"requires index backend");
}
{
char detail[256];
bool fed_enabled = fed_cfg != NULL && fed_cfg->enabled;
bool require_index_backend = fed_enabled;
snprintf(detail, sizeof(detail),
"enabled=%s require_index_backend=%s",
fed_enabled ? "true" : "false",
require_index_backend ? "true" : "false");
(void)amduatd_space_doctor_report_add(out_report,
"federation",
AMDUATD_DOCTOR_OK,
detail);
}
ok = true;
doctor_cleanup:
for (i = 0u; i < 3u; ++i) {
amduatd_space_doctor_pointer_free(&pointers[i]);
}
free((void *)edges_collection.data);
free((void *)edges_index_head.data);
free(collection_head);
free(collection_log_head);
if (!ok) {
amduatd_space_doctor_report_clear(out_report);
}
return ok;
}
bool amduatd_space_doctor_report_json(
const amduatd_space_doctor_report_t *report,
char **out_json) {
amduatd_doctor_strbuf_t b;
char header[256];
if (out_json != NULL) {
*out_json = NULL;
}
if (report == NULL || out_json == NULL) {
return false;
}
memset(&b, 0, sizeof(b));
if (!amduatd_doctor_strbuf_append_cstr(&b, "{")) {
amduatd_doctor_strbuf_free(&b);
return false;
}
if (!amduatd_doctor_strbuf_append_cstr(&b, "\"effective_space\":{")) {
amduatd_doctor_strbuf_free(&b);
return false;
}
if (report->scoped) {
if (snprintf(header, sizeof(header),
"\"mode\":\"scoped\",\"space_id\":\"%s\"",
report->space_id) <= 0 ||
!amduatd_doctor_strbuf_append_cstr(&b, header)) {
amduatd_doctor_strbuf_free(&b);
return false;
}
} else {
if (!amduatd_doctor_strbuf_append_cstr(&b, "\"mode\":\"unscoped\"")) {
amduatd_doctor_strbuf_free(&b);
return false;
}
}
if (!amduatd_doctor_strbuf_append_cstr(&b, "},")) {
amduatd_doctor_strbuf_free(&b);
return false;
}
if (snprintf(header, sizeof(header),
"\"store_backend\":\"%s\",",
amduatd_store_backend_name(report->backend)) <= 0 ||
!amduatd_doctor_strbuf_append_cstr(&b, header)) {
amduatd_doctor_strbuf_free(&b);
return false;
}
if (!amduatd_doctor_strbuf_append_cstr(&b, "\"checks\":[")) {
amduatd_doctor_strbuf_free(&b);
return false;
}
for (size_t i = 0u; i < report->checks_len; ++i) {
const amduatd_space_doctor_check_t *check = &report->checks[i];
if (i != 0u) {
(void)amduatd_doctor_strbuf_append_char(&b, ',');
}
if (!amduatd_doctor_strbuf_append_cstr(&b, "{\"name\":\"") ||
!amduatd_doctor_strbuf_append_cstr(&b, check->name) ||
!amduatd_doctor_strbuf_append_cstr(&b, "\",\"status\":\"") ||
!amduatd_doctor_strbuf_append_cstr(
&b,
amduatd_space_doctor_status_name(check->status)) ||
!amduatd_doctor_strbuf_append_cstr(&b, "\",\"detail\":\"") ||
!amduatd_doctor_strbuf_append_cstr(&b, check->detail) ||
!amduatd_doctor_strbuf_append_cstr(&b, "\"}")) {
amduatd_doctor_strbuf_free(&b);
return false;
}
}
if (!amduatd_doctor_strbuf_append_cstr(&b, "],")) {
amduatd_doctor_strbuf_free(&b);
return false;
}
if (snprintf(header, sizeof(header),
"\"summary\":{"
"\"ok_count\":%llu,"
"\"warn_count\":%llu,"
"\"fail_count\":%llu,"
"\"skipped_count\":%llu"
"}}\n",
(unsigned long long)report->ok_count,
(unsigned long long)report->warn_count,
(unsigned long long)report->fail_count,
(unsigned long long)report->skipped_count) <= 0 ||
!amduatd_doctor_strbuf_append_cstr(&b, header)) {
amduatd_doctor_strbuf_free(&b);
return false;
}
*out_json = b.data;
return true;
}

View file

@ -0,0 +1,60 @@
#ifndef AMDUATD_SPACE_DOCTOR_H
#define AMDUATD_SPACE_DOCTOR_H
#include "amduat/asl/asl_pointer_fs.h"
#include "amduat/asl/core.h"
#include "amduat/asl/store.h"
#include "amduatd_caps.h"
#include "amduatd_fed.h"
#include "amduatd_store.h"
#include <stdbool.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
AMDUATD_DOCTOR_OK = 0,
AMDUATD_DOCTOR_WARN = 1,
AMDUATD_DOCTOR_FAIL = 2,
AMDUATD_DOCTOR_SKIPPED = 3
} amduatd_space_doctor_status_t;
typedef struct {
char *name;
amduatd_space_doctor_status_t status;
char *detail;
} amduatd_space_doctor_check_t;
typedef struct {
bool scoped;
char space_id[AMDUAT_ASL_POINTER_NAME_MAX + 1u];
amduatd_store_backend_t backend;
amduatd_space_doctor_check_t *checks;
size_t checks_len;
size_t ok_count;
size_t warn_count;
size_t fail_count;
size_t skipped_count;
} amduatd_space_doctor_report_t;
bool amduatd_space_doctor_run(amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
const amduatd_cfg_t *cfg,
const amduatd_fed_cfg_t *fed_cfg,
amduatd_space_doctor_report_t *out_report);
bool amduatd_space_doctor_report_json(
const amduatd_space_doctor_report_t *report,
char **out_json);
void amduatd_space_doctor_report_free(amduatd_space_doctor_report_t *report);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUATD_SPACE_DOCTOR_H */

View file

@ -0,0 +1,739 @@
#include "amduatd_space_manifest.h"
#include "amduat/asl/ref_text.h"
#include "amduat/asl/record.h"
#include "amduatd_http.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char *data;
size_t len;
size_t cap;
} amduatd_manifest_buf_t;
static void amduatd_manifest_buf_free(amduatd_manifest_buf_t *b) {
if (b == NULL) {
return;
}
free(b->data);
b->data = NULL;
b->len = 0;
b->cap = 0;
}
static bool amduatd_manifest_buf_reserve(amduatd_manifest_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_manifest_buf_append(amduatd_manifest_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_manifest_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_manifest_buf_append_cstr(amduatd_manifest_buf_t *b,
const char *s) {
return amduatd_manifest_buf_append(
b, s != NULL ? s : "", s != NULL ? strlen(s) : 0u);
}
static bool amduatd_manifest_buf_append_char(amduatd_manifest_buf_t *b,
char c) {
return amduatd_manifest_buf_append(b, &c, 1u);
}
static bool amduatd_space_manifest_decode_ref(const char *s,
size_t len,
amduat_reference_t *out_ref) {
char *tmp = NULL;
bool ok = false;
if (out_ref == NULL) {
return false;
}
memset(out_ref, 0, sizeof(*out_ref));
if (!amduatd_copy_json_str(s, len, &tmp)) {
return false;
}
ok = amduat_asl_ref_decode_hex(tmp, out_ref);
free(tmp);
return ok;
}
static void amduatd_space_manifest_mount_free(
amduatd_space_manifest_mount_t *mount) {
if (mount == NULL) {
return;
}
free(mount->name);
free(mount->peer_key);
free(mount->space_id);
if (mount->has_pinned_root_ref) {
amduat_reference_free(&mount->pinned_root_ref);
}
memset(mount, 0, sizeof(*mount));
}
void amduatd_space_manifest_free(amduatd_space_manifest_t *manifest) {
if (manifest == NULL) {
return;
}
if (manifest->mounts != NULL) {
for (size_t i = 0u; i < manifest->mounts_len; ++i) {
amduatd_space_manifest_mount_free(&manifest->mounts[i]);
}
free(manifest->mounts);
}
memset(manifest, 0, sizeof(*manifest));
}
static bool amduatd_space_manifest_mounts_reserve(
amduatd_space_manifest_t *manifest,
size_t extra) {
size_t need;
size_t next_cap;
amduatd_space_manifest_mount_t *next;
if (manifest == NULL) {
return false;
}
if (extra > (SIZE_MAX - manifest->mounts_len)) {
return false;
}
need = manifest->mounts_len + extra;
if (need <= manifest->mounts_cap) {
return true;
}
next_cap = manifest->mounts_cap != 0u ? manifest->mounts_cap : 4u;
while (next_cap < need) {
if (next_cap > (SIZE_MAX / 2u)) {
next_cap = need;
break;
}
next_cap *= 2u;
}
next = (amduatd_space_manifest_mount_t *)realloc(
manifest->mounts, next_cap * sizeof(*next));
if (next == NULL) {
return false;
}
manifest->mounts = next;
manifest->mounts_cap = next_cap;
return true;
}
static bool amduatd_space_manifest_add_mount(
amduatd_space_manifest_t *manifest,
amduatd_space_manifest_mount_t *mount) {
if (manifest == NULL || mount == NULL) {
return false;
}
if (!amduatd_space_manifest_mounts_reserve(manifest, 1u)) {
return false;
}
manifest->mounts[manifest->mounts_len++] = *mount;
memset(mount, 0, sizeof(*mount));
return true;
}
static int amduatd_space_manifest_mount_cmp(const void *a, const void *b) {
const amduatd_space_manifest_mount_t *lhs =
(const amduatd_space_manifest_mount_t *)a;
const amduatd_space_manifest_mount_t *rhs =
(const amduatd_space_manifest_mount_t *)b;
int cmp;
if (lhs == NULL || rhs == NULL) {
return 0;
}
cmp = strcmp(lhs->name != NULL ? lhs->name : "",
rhs->name != NULL ? rhs->name : "");
if (cmp != 0) {
return cmp;
}
cmp = strcmp(lhs->peer_key != NULL ? lhs->peer_key : "",
rhs->peer_key != NULL ? rhs->peer_key : "");
if (cmp != 0) {
return cmp;
}
return strcmp(lhs->space_id != NULL ? lhs->space_id : "",
rhs->space_id != NULL ? rhs->space_id : "");
}
static bool amduatd_space_manifest_parse_mount(
const char **p,
const char *end,
amduatd_space_manifest_mount_t *out_mount) {
const char *key = NULL;
size_t key_len = 0u;
const char *sv = NULL;
size_t sv_len = 0u;
const char *cur = NULL;
bool have_name = false;
bool have_peer = false;
bool have_space = false;
bool have_mode = false;
bool have_pinned_root = false;
amduatd_space_manifest_mount_t mount;
if (p == NULL || end == NULL || out_mount == NULL) {
return false;
}
memset(&mount, 0, sizeof(mount));
if (!amduatd_json_expect(p, end, '{')) {
return false;
}
for (;;) {
cur = amduatd_json_skip_ws(*p, end);
if (cur < end && *cur == '}') {
*p = cur + 1;
break;
}
if (!amduatd_json_parse_string_noesc(p, end, &key, &key_len) ||
!amduatd_json_expect(p, end, ':')) {
amduatd_space_manifest_mount_free(&mount);
return false;
}
if (key_len == strlen("name") && memcmp(key, "name", key_len) == 0) {
if (have_name ||
!amduatd_json_parse_string_noesc(p, end, &sv, &sv_len) ||
!amduatd_copy_json_str(sv, sv_len, &mount.name)) {
amduatd_space_manifest_mount_free(&mount);
return false;
}
have_name = true;
} else if (key_len == strlen("peer_key") &&
memcmp(key, "peer_key", key_len) == 0) {
if (have_peer ||
!amduatd_json_parse_string_noesc(p, end, &sv, &sv_len) ||
!amduatd_copy_json_str(sv, sv_len, &mount.peer_key)) {
amduatd_space_manifest_mount_free(&mount);
return false;
}
have_peer = true;
} else if (key_len == strlen("space_id") &&
memcmp(key, "space_id", key_len) == 0) {
if (have_space ||
!amduatd_json_parse_string_noesc(p, end, &sv, &sv_len) ||
!amduatd_copy_json_str(sv, sv_len, &mount.space_id)) {
amduatd_space_manifest_mount_free(&mount);
return false;
}
have_space = true;
} else if (key_len == strlen("mode") &&
memcmp(key, "mode", key_len) == 0) {
if (have_mode ||
!amduatd_json_parse_string_noesc(p, end, &sv, &sv_len)) {
amduatd_space_manifest_mount_free(&mount);
return false;
}
if (sv_len == strlen("pinned") && memcmp(sv, "pinned", sv_len) == 0) {
mount.mode = AMDUATD_SPACE_MANIFEST_MOUNT_PINNED;
} else if (sv_len == strlen("track") &&
memcmp(sv, "track", sv_len) == 0) {
mount.mode = AMDUATD_SPACE_MANIFEST_MOUNT_TRACK;
} else {
amduatd_space_manifest_mount_free(&mount);
return false;
}
have_mode = true;
} else if (key_len == strlen("pinned_root_ref") &&
memcmp(key, "pinned_root_ref", key_len) == 0) {
if (have_pinned_root ||
!amduatd_json_parse_string_noesc(p, end, &sv, &sv_len) ||
!amduatd_space_manifest_decode_ref(sv, sv_len,
&mount.pinned_root_ref)) {
amduatd_space_manifest_mount_free(&mount);
return false;
}
mount.has_pinned_root_ref = true;
have_pinned_root = true;
} else {
if (!amduatd_json_skip_value(p, end, 0)) {
amduatd_space_manifest_mount_free(&mount);
return false;
}
}
cur = amduatd_json_skip_ws(*p, end);
if (cur < end && *cur == ',') {
*p = cur + 1;
continue;
}
if (cur < end && *cur == '}') {
*p = cur + 1;
break;
}
amduatd_space_manifest_mount_free(&mount);
return false;
}
if (!have_name || !have_peer || !have_space || !have_mode) {
amduatd_space_manifest_mount_free(&mount);
return false;
}
if (mount.name == NULL || !amduat_asl_pointer_name_is_valid(mount.name)) {
amduatd_space_manifest_mount_free(&mount);
return false;
}
if (mount.peer_key == NULL ||
!amduat_asl_pointer_name_is_valid(mount.peer_key)) {
amduatd_space_manifest_mount_free(&mount);
return false;
}
if (mount.space_id == NULL ||
!amduatd_space_space_id_is_valid(mount.space_id)) {
amduatd_space_manifest_mount_free(&mount);
return false;
}
if (mount.mode == AMDUATD_SPACE_MANIFEST_MOUNT_PINNED &&
!mount.has_pinned_root_ref) {
amduatd_space_manifest_mount_free(&mount);
return false;
}
if (mount.mode == AMDUATD_SPACE_MANIFEST_MOUNT_TRACK &&
mount.has_pinned_root_ref) {
amduatd_space_manifest_mount_free(&mount);
return false;
}
*out_mount = mount;
return true;
}
bool amduatd_space_manifest_encode_json(const amduatd_space_manifest_t *manifest,
char **out_json,
size_t *out_len) {
amduatd_manifest_buf_t b;
if (out_json != NULL) {
*out_json = NULL;
}
if (out_len != NULL) {
*out_len = 0u;
}
if (manifest == NULL || out_json == NULL || out_len == NULL) {
return false;
}
memset(&b, 0, sizeof(b));
if (!amduatd_manifest_buf_append_cstr(&b, "{\"version\":")) {
amduatd_manifest_buf_free(&b);
return false;
}
{
char tmp[32];
int n = snprintf(tmp, sizeof(tmp), "%u", manifest->version);
if (n <= 0 || (size_t)n >= sizeof(tmp)) {
amduatd_manifest_buf_free(&b);
return false;
}
if (!amduatd_manifest_buf_append_cstr(&b, tmp) ||
!amduatd_manifest_buf_append_cstr(&b, ",\"mounts\":[")) {
amduatd_manifest_buf_free(&b);
return false;
}
}
for (size_t i = 0u; i < manifest->mounts_len; ++i) {
const amduatd_space_manifest_mount_t *mount = &manifest->mounts[i];
char *root_ref_hex = NULL;
if (i != 0u) {
if (!amduatd_manifest_buf_append_char(&b, ',')) {
amduatd_manifest_buf_free(&b);
return false;
}
}
if (!amduatd_manifest_buf_append_cstr(&b, "{\"name\":\"") ||
!amduatd_manifest_buf_append_cstr(&b, mount->name) ||
!amduatd_manifest_buf_append_cstr(&b, "\",\"peer_key\":\"") ||
!amduatd_manifest_buf_append_cstr(&b, mount->peer_key) ||
!amduatd_manifest_buf_append_cstr(&b, "\",\"space_id\":\"") ||
!amduatd_manifest_buf_append_cstr(&b, mount->space_id) ||
!amduatd_manifest_buf_append_cstr(&b, "\",\"mode\":\"") ||
!amduatd_manifest_buf_append_cstr(
&b,
mount->mode == AMDUATD_SPACE_MANIFEST_MOUNT_PINNED ? "pinned"
: "track") ||
!amduatd_manifest_buf_append_cstr(&b, "\"")) {
amduatd_manifest_buf_free(&b);
return false;
}
if (mount->mode == AMDUATD_SPACE_MANIFEST_MOUNT_PINNED) {
if (!amduat_asl_ref_encode_hex(mount->pinned_root_ref, &root_ref_hex)) {
amduatd_manifest_buf_free(&b);
return false;
}
if (!amduatd_manifest_buf_append_cstr(&b, ",\"pinned_root_ref\":\"") ||
!amduatd_manifest_buf_append_cstr(&b, root_ref_hex) ||
!amduatd_manifest_buf_append_cstr(&b, "\"")) {
free(root_ref_hex);
amduatd_manifest_buf_free(&b);
return false;
}
free(root_ref_hex);
}
if (!amduatd_manifest_buf_append_cstr(&b, "}")) {
amduatd_manifest_buf_free(&b);
return false;
}
}
if (!amduatd_manifest_buf_append_cstr(&b, "]}")) {
amduatd_manifest_buf_free(&b);
return false;
}
*out_json = b.data;
*out_len = b.len;
return true;
}
static bool amduatd_space_manifest_parse(amduat_octets_t payload,
amduatd_space_manifest_t *manifest) {
const char *p = NULL;
const char *end = NULL;
const char *key = NULL;
size_t key_len = 0u;
const char *cur = NULL;
bool have_version = false;
bool have_mounts = false;
uint64_t version = 0u;
if (manifest == NULL) {
return false;
}
memset(manifest, 0, sizeof(*manifest));
if (payload.len != 0u && payload.data == NULL) {
return false;
}
p = (const char *)payload.data;
end = p + payload.len;
if (!amduatd_json_expect(&p, end, '{')) {
return false;
}
for (;;) {
cur = amduatd_json_skip_ws(p, end);
if (cur < end && *cur == '}') {
p = cur + 1;
break;
}
if (!amduatd_json_parse_string_noesc(&p, end, &key, &key_len) ||
!amduatd_json_expect(&p, end, ':')) {
amduatd_space_manifest_free(manifest);
return false;
}
if (key_len == strlen("version") &&
memcmp(key, "version", key_len) == 0) {
if (have_version || !amduatd_json_parse_u64(&p, end, &version)) {
amduatd_space_manifest_free(manifest);
return false;
}
have_version = true;
} else if (key_len == strlen("mounts") &&
memcmp(key, "mounts", key_len) == 0) {
if (have_mounts || !amduatd_json_expect(&p, end, '[')) {
amduatd_space_manifest_free(manifest);
return false;
}
cur = amduatd_json_skip_ws(p, end);
if (cur < end && *cur == ']') {
p = cur + 1;
have_mounts = true;
} else {
for (;;) {
amduatd_space_manifest_mount_t mount;
memset(&mount, 0, sizeof(mount));
if (!amduatd_space_manifest_parse_mount(&p, end, &mount) ||
!amduatd_space_manifest_add_mount(manifest, &mount)) {
amduatd_space_manifest_mount_free(&mount);
amduatd_space_manifest_free(manifest);
return false;
}
cur = amduatd_json_skip_ws(p, end);
if (cur < end && *cur == ',') {
p = cur + 1;
continue;
}
if (cur < end && *cur == ']') {
p = cur + 1;
have_mounts = true;
break;
}
amduatd_space_manifest_free(manifest);
return false;
}
}
} else {
if (!amduatd_json_skip_value(&p, end, 0)) {
amduatd_space_manifest_free(manifest);
return false;
}
}
cur = amduatd_json_skip_ws(p, end);
if (cur < end && *cur == ',') {
p = cur + 1;
continue;
}
if (cur < end && *cur == '}') {
p = cur + 1;
break;
}
amduatd_space_manifest_free(manifest);
return false;
}
if (!have_version || !have_mounts || version != 1u) {
amduatd_space_manifest_free(manifest);
return false;
}
if (version > UINT32_MAX) {
amduatd_space_manifest_free(manifest);
return false;
}
manifest->version = (uint32_t)version;
cur = amduatd_json_skip_ws(p, end);
if (cur != end) {
amduatd_space_manifest_free(manifest);
return false;
}
if (manifest->mounts_len > 1u) {
qsort(manifest->mounts,
manifest->mounts_len,
sizeof(*manifest->mounts),
amduatd_space_manifest_mount_cmp);
}
return true;
}
amduatd_space_manifest_status_t amduatd_space_manifest_put(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
amduat_octets_t payload,
const amduat_reference_t *expected_ref,
amduat_reference_t *out_new_ref,
amduatd_space_manifest_t *out_manifest) {
amduat_octets_t pointer_name = amduat_octets(NULL, 0u);
amduat_reference_t record_ref;
amduat_asl_store_error_t store_err;
amduat_asl_pointer_error_t perr;
bool swapped = false;
char *encoded = NULL;
size_t encoded_len = 0u;
amduatd_space_manifest_t local_manifest;
amduatd_space_manifest_t *manifest = out_manifest != NULL ? out_manifest
: &local_manifest;
if (out_new_ref != NULL) {
*out_new_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
}
if (manifest != NULL) {
memset(manifest, 0, sizeof(*manifest));
}
if (store == NULL || pointer_store == NULL || manifest == NULL) {
return AMDUATD_SPACE_MANIFEST_ERR_INVALID;
}
if (!amduatd_space_manifest_parse(payload, manifest)) {
amduatd_space_manifest_free(manifest);
return AMDUATD_SPACE_MANIFEST_ERR_CODEC;
}
if (!amduatd_space_manifest_encode_json(manifest, &encoded, &encoded_len)) {
amduatd_space_manifest_free(manifest);
return AMDUATD_SPACE_MANIFEST_ERR_CODEC;
}
memset(&record_ref, 0, sizeof(record_ref));
store_err = amduat_asl_record_store_put(
store,
amduat_octets(AMDUATD_SPACE_MANIFEST_1,
strlen(AMDUATD_SPACE_MANIFEST_1)),
amduat_octets((const uint8_t *)encoded, encoded_len),
&record_ref);
free(encoded);
encoded = NULL;
if (store_err != AMDUAT_ASL_STORE_OK) {
amduatd_space_manifest_free(manifest);
return AMDUATD_SPACE_MANIFEST_ERR_STORE;
}
if (!amduatd_space_scope_name(effective_space,
"manifest/head",
&pointer_name)) {
amduat_reference_free(&record_ref);
amduatd_space_manifest_free(manifest);
return AMDUATD_SPACE_MANIFEST_ERR_INVALID;
}
perr = amduat_asl_pointer_cas(pointer_store,
(const char *)pointer_name.data,
expected_ref != NULL,
expected_ref,
&record_ref,
&swapped);
amduat_octets_free(&pointer_name);
if (perr != AMDUAT_ASL_POINTER_OK) {
amduat_reference_free(&record_ref);
amduatd_space_manifest_free(manifest);
return AMDUATD_SPACE_MANIFEST_ERR_STORE;
}
if (!swapped) {
amduat_reference_free(&record_ref);
amduatd_space_manifest_free(manifest);
return AMDUATD_SPACE_MANIFEST_ERR_CONFLICT;
}
if (out_new_ref != NULL) {
if (!amduat_reference_clone(record_ref, out_new_ref)) {
amduat_reference_free(&record_ref);
amduatd_space_manifest_free(manifest);
return AMDUATD_SPACE_MANIFEST_ERR_STORE;
}
}
amduat_reference_free(&record_ref);
if (out_manifest == NULL) {
amduatd_space_manifest_free(manifest);
}
return AMDUATD_SPACE_MANIFEST_OK;
}
amduatd_space_manifest_status_t amduatd_space_manifest_get(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
amduat_reference_t *out_ref,
amduatd_space_manifest_t *out_manifest) {
amduat_octets_t pointer_name = amduat_octets(NULL, 0u);
amduat_reference_t pointer_ref;
amduat_asl_pointer_error_t perr;
amduat_asl_record_t record;
amduat_asl_store_error_t store_err;
bool exists = false;
if (out_ref != NULL) {
*out_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
}
if (out_manifest != NULL) {
memset(out_manifest, 0, sizeof(*out_manifest));
}
if (store == NULL || pointer_store == NULL || out_manifest == NULL) {
return AMDUATD_SPACE_MANIFEST_ERR_INVALID;
}
if (!amduatd_space_scope_name(effective_space,
"manifest/head",
&pointer_name)) {
return AMDUATD_SPACE_MANIFEST_ERR_INVALID;
}
memset(&pointer_ref, 0, sizeof(pointer_ref));
perr = amduat_asl_pointer_get(pointer_store,
(const char *)pointer_name.data,
&exists,
&pointer_ref);
amduat_octets_free(&pointer_name);
if (perr != AMDUAT_ASL_POINTER_OK) {
return AMDUATD_SPACE_MANIFEST_ERR_STORE;
}
if (!exists) {
return AMDUATD_SPACE_MANIFEST_ERR_NOT_FOUND;
}
memset(&record, 0, sizeof(record));
store_err = amduat_asl_record_store_get(store, pointer_ref, &record);
if (store_err != AMDUAT_ASL_STORE_OK) {
amduat_reference_free(&pointer_ref);
return AMDUATD_SPACE_MANIFEST_ERR_STORE;
}
if (record.schema.len != strlen(AMDUATD_SPACE_MANIFEST_1) ||
memcmp(record.schema.data,
AMDUATD_SPACE_MANIFEST_1,
record.schema.len) != 0) {
amduat_asl_record_free(&record);
amduat_reference_free(&pointer_ref);
return AMDUATD_SPACE_MANIFEST_ERR_CODEC;
}
if (!amduatd_space_manifest_parse(record.payload, out_manifest)) {
amduat_asl_record_free(&record);
amduat_reference_free(&pointer_ref);
return AMDUATD_SPACE_MANIFEST_ERR_CODEC;
}
amduat_asl_record_free(&record);
if (out_ref != NULL) {
if (!amduat_reference_clone(pointer_ref, out_ref)) {
amduatd_space_manifest_free(out_manifest);
amduat_reference_free(&pointer_ref);
return AMDUATD_SPACE_MANIFEST_ERR_STORE;
}
}
amduat_reference_free(&pointer_ref);
return AMDUATD_SPACE_MANIFEST_OK;
}

View file

@ -0,0 +1,74 @@
#ifndef AMDUATD_SPACE_MANIFEST_H
#define AMDUATD_SPACE_MANIFEST_H
#include "amduat/asl/asl_pointer_fs.h"
#include "amduat/asl/store.h"
#include "amduatd_space.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define AMDUATD_SPACE_MANIFEST_1 "space/manifest_1"
typedef enum {
AMDUATD_SPACE_MANIFEST_OK = 0,
AMDUATD_SPACE_MANIFEST_ERR_INVALID = 1,
AMDUATD_SPACE_MANIFEST_ERR_NOT_FOUND = 2,
AMDUATD_SPACE_MANIFEST_ERR_STORE = 3,
AMDUATD_SPACE_MANIFEST_ERR_CODEC = 4,
AMDUATD_SPACE_MANIFEST_ERR_CONFLICT = 5
} amduatd_space_manifest_status_t;
typedef enum {
AMDUATD_SPACE_MANIFEST_MOUNT_PINNED = 0,
AMDUATD_SPACE_MANIFEST_MOUNT_TRACK = 1
} amduatd_space_manifest_mode_t;
typedef struct {
char *name;
char *peer_key;
char *space_id;
amduatd_space_manifest_mode_t mode;
bool has_pinned_root_ref;
amduat_reference_t pinned_root_ref;
} amduatd_space_manifest_mount_t;
typedef struct {
uint32_t version;
amduatd_space_manifest_mount_t *mounts;
size_t mounts_len;
size_t mounts_cap;
} amduatd_space_manifest_t;
amduatd_space_manifest_status_t amduatd_space_manifest_get(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
amduat_reference_t *out_ref,
amduatd_space_manifest_t *out_manifest);
amduatd_space_manifest_status_t amduatd_space_manifest_put(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
amduat_octets_t payload,
const amduat_reference_t *expected_ref,
amduat_reference_t *out_new_ref,
amduatd_space_manifest_t *out_manifest);
bool amduatd_space_manifest_encode_json(const amduatd_space_manifest_t *manifest,
char **out_json,
size_t *out_len);
void amduatd_space_manifest_free(amduatd_space_manifest_t *manifest);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUATD_SPACE_MANIFEST_H */

336
src/amduatd_space_mounts.c Normal file
View file

@ -0,0 +1,336 @@
#include "amduatd_space_mounts.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_mounts_buf_t;
static void amduatd_mounts_buf_free(amduatd_mounts_buf_t *b) {
if (b == NULL) {
return;
}
free(b->data);
b->data = NULL;
b->len = 0;
b->cap = 0;
}
static bool amduatd_mounts_buf_reserve(amduatd_mounts_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_mounts_buf_append(amduatd_mounts_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_mounts_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_mounts_buf_append_cstr(amduatd_mounts_buf_t *b,
const char *s) {
return amduatd_mounts_buf_append(
b, s != NULL ? s : "", s != NULL ? strlen(s) : 0u);
}
static bool amduatd_mounts_buf_append_char(amduatd_mounts_buf_t *b, char c) {
return amduatd_mounts_buf_append(b, &c, 1u);
}
static amduatd_space_mounts_status_t amduatd_space_mounts_append_tracking(
amduatd_mounts_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) {
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_namespace = "none";
amduatd_fed_cursor_record_init(&cursor);
cursor_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
if (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_MOUNTS_ERR_CODEC;
} else {
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
return AMDUATD_SPACE_MOUNTS_ERR_STORE;
}
cursor_namespace = "v2";
}
if (!amduatd_mounts_buf_append_cstr(b, ",\"local_tracking\":{") ||
!amduatd_mounts_buf_append_cstr(b, "\"cursor_namespace\":\"") ||
!amduatd_mounts_buf_append_cstr(b, cursor_namespace) ||
!amduatd_mounts_buf_append_cstr(
b,
"\",\"cursor_scope\":\"per-peer-per-local-space\","
"\"remote_space_id\":\"") ||
!amduatd_mounts_buf_append_cstr(b, mount->space_id) ||
!amduatd_mounts_buf_append_cstr(b, "\",\"pull_cursor\":{") ||
!amduatd_mounts_buf_append_cstr(b,
cursor_present ? "\"present\":true"
: "\"present\":false")) {
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
return AMDUATD_SPACE_MOUNTS_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_MOUNTS_ERR_STORE;
}
if (!amduatd_mounts_buf_append_cstr(b, ",\"last_logseq\":") ||
!amduatd_mounts_buf_append_cstr(b, tmp)) {
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
return AMDUATD_SPACE_MOUNTS_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_MOUNTS_ERR_STORE;
}
if (!amduatd_mounts_buf_append_cstr(b, ",\"ref\":\"") ||
!amduatd_mounts_buf_append_cstr(b, cursor_ref_hex) ||
!amduatd_mounts_buf_append_cstr(b, "\"")) {
free(cursor_ref_hex);
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
return AMDUATD_SPACE_MOUNTS_ERR_STORE;
}
free(cursor_ref_hex);
}
if (!amduatd_mounts_buf_append_cstr(b, "}}")) {
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
return AMDUATD_SPACE_MOUNTS_ERR_STORE;
}
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
return AMDUATD_SPACE_MOUNTS_OK;
}
amduatd_space_mounts_status_t amduatd_space_mounts_resolve(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
amduat_reference_t *out_manifest_ref,
char **out_mounts_json,
size_t *out_mounts_len) {
amduatd_space_manifest_t manifest;
amduat_reference_t manifest_ref;
amduatd_space_manifest_status_t status;
amduatd_mounts_buf_t b;
if (out_manifest_ref != NULL) {
*out_manifest_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
}
if (out_mounts_json != NULL) {
*out_mounts_json = NULL;
}
if (out_mounts_len != NULL) {
*out_mounts_len = 0u;
}
if (store == NULL || pointer_store == NULL || out_manifest_ref == NULL ||
out_mounts_json == NULL || out_mounts_len == NULL) {
return AMDUATD_SPACE_MOUNTS_ERR_INVALID;
}
memset(&manifest, 0, sizeof(manifest));
memset(&manifest_ref, 0, sizeof(manifest_ref));
status = amduatd_space_manifest_get(store,
pointer_store,
effective_space,
&manifest_ref,
&manifest);
if (status == AMDUATD_SPACE_MANIFEST_ERR_NOT_FOUND) {
return AMDUATD_SPACE_MOUNTS_ERR_NOT_FOUND;
}
if (status == AMDUATD_SPACE_MANIFEST_ERR_STORE) {
return AMDUATD_SPACE_MOUNTS_ERR_STORE;
}
if (status != AMDUATD_SPACE_MANIFEST_OK) {
return AMDUATD_SPACE_MOUNTS_ERR_CODEC;
}
memset(&b, 0, sizeof(b));
if (!amduatd_mounts_buf_append_char(&b, '[')) {
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
return AMDUATD_SPACE_MOUNTS_ERR_STORE;
}
for (size_t i = 0u; i < manifest.mounts_len; ++i) {
const amduatd_space_manifest_mount_t *mount = &manifest.mounts[i];
char *pinned_hex = NULL;
if (i != 0u) {
if (!amduatd_mounts_buf_append_char(&b, ',')) {
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_ERR_STORE;
}
}
if (!amduatd_mounts_buf_append_cstr(&b, "{\"name\":\"") ||
!amduatd_mounts_buf_append_cstr(&b, mount->name) ||
!amduatd_mounts_buf_append_cstr(&b, "\",\"peer_key\":\"") ||
!amduatd_mounts_buf_append_cstr(&b, mount->peer_key) ||
!amduatd_mounts_buf_append_cstr(&b, "\",\"space_id\":\"") ||
!amduatd_mounts_buf_append_cstr(&b, mount->space_id) ||
!amduatd_mounts_buf_append_cstr(&b, "\",\"mode\":\"") ||
!amduatd_mounts_buf_append_cstr(
&b,
mount->mode == AMDUATD_SPACE_MANIFEST_MOUNT_PINNED ? "pinned"
: "track") ||
!amduatd_mounts_buf_append_cstr(&b, "\"")) {
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_ERR_STORE;
}
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)) {
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_ERR_STORE;
}
if (!amduatd_mounts_buf_append_cstr(&b, ",\"pinned_root_ref\":\"") ||
!amduatd_mounts_buf_append_cstr(&b, pinned_hex) ||
!amduatd_mounts_buf_append_cstr(&b, "\"")) {
free(pinned_hex);
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_ERR_STORE;
}
free(pinned_hex);
}
{
amduatd_space_mounts_status_t track_status =
amduatd_space_mounts_append_tracking(&b,
store,
pointer_store,
effective_space,
mount);
if (track_status != AMDUATD_SPACE_MOUNTS_OK) {
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_buf_free(&b);
return track_status;
}
}
if (!amduatd_mounts_buf_append_cstr(&b, "}")) {
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_ERR_STORE;
}
}
if (!amduatd_mounts_buf_append_char(&b, ']')) {
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_ERR_STORE;
}
if (!amduat_reference_clone(manifest_ref, out_manifest_ref)) {
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_ERR_STORE;
}
*out_mounts_json = b.data;
*out_mounts_len = b.len;
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
return AMDUATD_SPACE_MOUNTS_OK;
}

View file

@ -0,0 +1,35 @@
#ifndef AMDUATD_SPACE_MOUNTS_H
#define AMDUATD_SPACE_MOUNTS_H
#include "amduat/asl/asl_pointer_fs.h"
#include "amduat/asl/store.h"
#include "amduatd_space.h"
#include <stdbool.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
AMDUATD_SPACE_MOUNTS_OK = 0,
AMDUATD_SPACE_MOUNTS_ERR_INVALID = 1,
AMDUATD_SPACE_MOUNTS_ERR_NOT_FOUND = 2,
AMDUATD_SPACE_MOUNTS_ERR_STORE = 3,
AMDUATD_SPACE_MOUNTS_ERR_CODEC = 4
} amduatd_space_mounts_status_t;
amduatd_space_mounts_status_t amduatd_space_mounts_resolve(
amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
amduat_reference_t *out_manifest_ref,
char **out_mounts_json,
size_t *out_mounts_len);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUATD_SPACE_MOUNTS_H */

View file

@ -0,0 +1,484 @@
#include "amduatd_space_mounts_sync.h"
#include "amduat/asl/ref_text.h"
#include "amduatd_fed_until.h"
#include "amduatd_space_manifest.h"
#include <errno.h>
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char *data;
size_t len;
size_t cap;
} amduatd_mounts_sync_buf_t;
static void amduatd_mounts_sync_buf_free(amduatd_mounts_sync_buf_t *b) {
if (b == NULL) {
return;
}
free(b->data);
b->data = NULL;
b->len = 0;
b->cap = 0;
}
static bool amduatd_mounts_sync_buf_reserve(amduatd_mounts_sync_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_mounts_sync_buf_append(amduatd_mounts_sync_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_mounts_sync_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_mounts_sync_buf_append_cstr(amduatd_mounts_sync_buf_t *b,
const char *s) {
return amduatd_mounts_sync_buf_append(
b, s != NULL ? s : "", s != NULL ? strlen(s) : 0u);
}
static bool amduatd_mounts_sync_buf_append_char(amduatd_mounts_sync_buf_t *b,
char c) {
return amduatd_mounts_sync_buf_append(b, &c, 1u);
}
static bool amduatd_space_mounts_parse_u32(const char *s, uint32_t *out) {
unsigned long val;
char *endp = NULL;
if (s == NULL) {
return false;
}
errno = 0;
val = strtoul(s, &endp, 10);
if (errno != 0 || endp == s || *endp != '\0' || val > UINT32_MAX) {
return false;
}
if (out != NULL) {
*out = (uint32_t)val;
}
return true;
}
static const char *amduatd_space_mounts_sync_status_code(
amduatd_fed_pull_apply_status_t status) {
switch (status) {
case AMDUATD_FED_PULL_APPLY_ERR_INVALID:
return "invalid";
case AMDUATD_FED_PULL_APPLY_ERR_DISABLED:
return "disabled";
case AMDUATD_FED_PULL_APPLY_ERR_UNSUPPORTED:
return "unsupported";
case AMDUATD_FED_PULL_APPLY_ERR_REMOTE:
return "remote";
case AMDUATD_FED_PULL_APPLY_ERR_STORE:
return "store";
case AMDUATD_FED_PULL_APPLY_ERR_CONFLICT:
return "conflict";
case AMDUATD_FED_PULL_APPLY_ERR_OOM:
return "oom";
case AMDUATD_FED_PULL_APPLY_OK:
default:
return "error";
}
}
void amduatd_space_mounts_sync_report_free(
amduatd_space_mounts_sync_report_t *report) {
if (report == NULL) {
return;
}
amduat_reference_free(&report->manifest_ref);
free(report->results_json);
report->results_json = NULL;
report->results_len = 0u;
report->mounts_total = 0u;
report->mounts_synced = 0u;
report->ok = false;
}
amduatd_space_mounts_sync_status_t amduatd_space_mounts_sync_until(
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,
const amduatd_fed_pull_transport_t *transport,
uint64_t limit,
uint64_t max_rounds,
size_t max_mounts,
amduatd_space_mounts_sync_report_t *out_report) {
amduatd_space_manifest_t manifest;
amduat_reference_t manifest_ref;
amduatd_space_manifest_status_t status;
amduatd_mounts_sync_buf_t b;
size_t track_total = 0u;
size_t attempted = 0u;
bool ok_all = true;
if (out_report != NULL) {
memset(out_report, 0, sizeof(*out_report));
out_report->manifest_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
}
if (store == NULL || pointer_store == NULL || out_report == NULL ||
fed_cfg == NULL || transport == NULL || max_rounds == 0u) {
return AMDUATD_SPACE_MOUNTS_SYNC_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_MOUNTS_SYNC_ERR_NOT_FOUND;
}
if (status == AMDUATD_SPACE_MANIFEST_ERR_STORE) {
amduat_reference_free(&manifest_ref);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_STORE;
}
if (status != AMDUATD_SPACE_MANIFEST_OK) {
amduat_reference_free(&manifest_ref);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_CODEC;
}
if (!amduatd_mounts_sync_buf_append_char(&b, '[')) {
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
for (size_t i = 0u; i < manifest.mounts_len; ++i) {
const amduatd_space_manifest_mount_t *mount = &manifest.mounts[i];
bool mount_ok = true;
bool peer_ok = true;
bool remote_ok = true;
if (mount->mode != AMDUATD_SPACE_MANIFEST_MOUNT_TRACK) {
continue;
}
track_total++;
if (attempted >= max_mounts) {
continue;
}
if (attempted != 0u) {
if (!amduatd_mounts_sync_buf_append_char(&b, ',')) {
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
}
if (!amduatd_mounts_sync_buf_append_cstr(&b, "{\"name\":\"") ||
!amduatd_mounts_sync_buf_append_cstr(&b, mount->name) ||
!amduatd_mounts_sync_buf_append_cstr(&b, "\",\"peer_key\":\"") ||
!amduatd_mounts_sync_buf_append_cstr(&b, mount->peer_key) ||
!amduatd_mounts_sync_buf_append_cstr(&b, "\",\"remote_space_id\":\"") ||
!amduatd_mounts_sync_buf_append_cstr(&b, mount->space_id) ||
!amduatd_mounts_sync_buf_append_cstr(&b, "\",\"status\":\"")) {
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
peer_ok = amduatd_space_mounts_parse_u32(mount->peer_key, NULL);
remote_ok = amduatd_space_space_id_is_valid(mount->space_id);
if (!peer_ok || !remote_ok) {
mount_ok = false;
}
if (!mount_ok) {
ok_all = false;
if (!amduatd_mounts_sync_buf_append_cstr(&b, "error") ||
!amduatd_mounts_sync_buf_append_cstr(&b, "\",\"error\":{")) {
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
if (!peer_ok) {
if (!amduatd_mounts_sync_buf_append_cstr(&b,
"\"code\":\"invalid_peer\","
"\"message\":\"invalid peer\""
"}")) {
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
} else if (!remote_ok) {
if (!amduatd_mounts_sync_buf_append_cstr(
&b,
"\"code\":\"invalid_remote_space_id\","
"\"message\":\"invalid remote_space_id\""
"}")) {
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
} else if (!amduatd_mounts_sync_buf_append_cstr(
&b,
"\"code\":\"invalid_mount\","
"\"message\":\"invalid mount\""
"}")) {
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
if (!amduatd_mounts_sync_buf_append_cstr(&b, "}")) {
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
attempted++;
continue;
}
{
amduatd_fed_until_report_t report;
amduatd_fed_pull_apply_status_t sync_status;
char *cursor_ref_hex = NULL;
sync_status = amduatd_fed_pull_until(store,
pointer_store,
effective_space,
mount->peer_key,
mount->space_id,
limit,
max_rounds,
fed_cfg,
transport,
&report);
if (sync_status != AMDUATD_FED_PULL_APPLY_OK) {
const char *code = amduatd_space_mounts_sync_status_code(sync_status);
const char *message =
report.error[0] != '\0' ? report.error : "error";
ok_all = false;
if (!amduatd_mounts_sync_buf_append_cstr(&b, "error") ||
!amduatd_mounts_sync_buf_append_cstr(&b, "\",\"error\":{") ||
!amduatd_mounts_sync_buf_append_cstr(&b, "\"code\":\"") ||
!amduatd_mounts_sync_buf_append_cstr(&b, code) ||
!amduatd_mounts_sync_buf_append_cstr(&b, "\",\"message\":\"") ||
!amduatd_mounts_sync_buf_append_cstr(&b, message) ||
!amduatd_mounts_sync_buf_append_cstr(&b, "\"}}")) {
amduatd_fed_until_report_free(&report);
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
amduatd_fed_until_report_free(&report);
attempted++;
continue;
}
if (!amduatd_mounts_sync_buf_append_cstr(&b, "ok") ||
!amduatd_mounts_sync_buf_append_cstr(
&b,
"\",\"caught_up\":") ||
!amduatd_mounts_sync_buf_append_cstr(&b,
report.caught_up ? "true"
: "false") ||
!amduatd_mounts_sync_buf_append_cstr(&b, ",\"rounds_executed\":")) {
amduatd_fed_until_report_free(&report);
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
{
char tmp[32];
int n = snprintf(tmp, sizeof(tmp), "%llu",
(unsigned long long)report.rounds_executed);
if (n <= 0 || (size_t)n >= sizeof(tmp) ||
!amduatd_mounts_sync_buf_append_cstr(&b, tmp)) {
amduatd_fed_until_report_free(&report);
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
}
if (!amduatd_mounts_sync_buf_append_cstr(
&b,
",\"applied\":{\"records\":")) {
amduatd_fed_until_report_free(&report);
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
{
char tmp[32];
int n = snprintf(tmp, sizeof(tmp), "%zu", report.total_records);
if (n <= 0 || (size_t)n >= sizeof(tmp) ||
!amduatd_mounts_sync_buf_append_cstr(&b, tmp)) {
amduatd_fed_until_report_free(&report);
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
}
if (!amduatd_mounts_sync_buf_append_cstr(
&b,
",\"artifacts\":")) {
amduatd_fed_until_report_free(&report);
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
{
char tmp[32];
int n = snprintf(tmp, sizeof(tmp), "%zu", report.total_artifacts);
if (n <= 0 || (size_t)n >= sizeof(tmp) ||
!amduatd_mounts_sync_buf_append_cstr(&b, tmp)) {
amduatd_fed_until_report_free(&report);
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
}
if (!amduatd_mounts_sync_buf_append_cstr(&b, "},\"cursor\":{")) {
amduatd_fed_until_report_free(&report);
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
{
bool need_comma = false;
if (report.cursor_has_logseq) {
char tmp[32];
int n = snprintf(tmp, sizeof(tmp), "%llu",
(unsigned long long)report.cursor_logseq);
if (n <= 0 || (size_t)n >= sizeof(tmp) ||
!amduatd_mounts_sync_buf_append_cstr(&b, "\"last_logseq\":") ||
!amduatd_mounts_sync_buf_append_cstr(&b, tmp)) {
amduatd_fed_until_report_free(&report);
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
need_comma = true;
}
if (report.cursor_ref_set) {
if (!amduat_asl_ref_encode_hex(report.cursor_ref, &cursor_ref_hex)) {
amduatd_fed_until_report_free(&report);
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
if (need_comma) {
if (!amduatd_mounts_sync_buf_append_char(&b, ',')) {
free(cursor_ref_hex);
amduatd_fed_until_report_free(&report);
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
}
if (!amduatd_mounts_sync_buf_append_cstr(&b, "\"ref\":\"") ||
!amduatd_mounts_sync_buf_append_cstr(&b, cursor_ref_hex) ||
!amduatd_mounts_sync_buf_append_cstr(&b, "\"")) {
free(cursor_ref_hex);
amduatd_fed_until_report_free(&report);
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
free(cursor_ref_hex);
cursor_ref_hex = NULL;
}
}
if (!amduatd_mounts_sync_buf_append_cstr(&b, "}}")) {
amduatd_fed_until_report_free(&report);
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
amduatd_fed_until_report_free(&report);
}
attempted++;
}
if (!amduatd_mounts_sync_buf_append_char(&b, ']')) {
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&manifest_ref);
amduatd_mounts_sync_buf_free(&b);
return AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM;
}
out_report->mounts_total = track_total;
out_report->mounts_synced = attempted;
out_report->ok = ok_all;
out_report->results_json = b.data;
out_report->results_len = b.len;
out_report->manifest_ref = manifest_ref;
amduatd_space_manifest_free(&manifest);
return AMDUATD_SPACE_MOUNTS_SYNC_OK;
}

View file

@ -0,0 +1,54 @@
#ifndef AMDUATD_SPACE_MOUNTS_SYNC_H
#define AMDUATD_SPACE_MOUNTS_SYNC_H
#include "amduat/asl/asl_pointer_fs.h"
#include "amduat/asl/store.h"
#include "amduatd_fed.h"
#include "amduatd_fed_pull_apply.h"
#include "amduatd_space.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
AMDUATD_SPACE_MOUNTS_SYNC_OK = 0,
AMDUATD_SPACE_MOUNTS_SYNC_ERR_INVALID = 1,
AMDUATD_SPACE_MOUNTS_SYNC_ERR_NOT_FOUND = 2,
AMDUATD_SPACE_MOUNTS_SYNC_ERR_STORE = 3,
AMDUATD_SPACE_MOUNTS_SYNC_ERR_CODEC = 4,
AMDUATD_SPACE_MOUNTS_SYNC_ERR_OOM = 5
} amduatd_space_mounts_sync_status_t;
typedef struct {
amduat_reference_t manifest_ref;
size_t mounts_total;
size_t mounts_synced;
bool ok;
char *results_json;
size_t results_len;
} amduatd_space_mounts_sync_report_t;
void amduatd_space_mounts_sync_report_free(
amduatd_space_mounts_sync_report_t *report);
amduatd_space_mounts_sync_status_t amduatd_space_mounts_sync_until(
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,
const amduatd_fed_pull_transport_t *transport,
uint64_t limit,
uint64_t max_rounds,
size_t max_mounts,
amduatd_space_mounts_sync_report_t *out_report);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUATD_SPACE_MOUNTS_SYNC_H */

637
src/amduatd_space_roots.c Normal file
View file

@ -0,0 +1,637 @@
#include "amduatd_space_roots.h"
#include "amduat/asl/asl_pointer_fs.h"
#include <dirent.h>
#include <errno.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
static bool amduatd_space_roots_join_path(const char *base,
const char *segment,
char **out_path) {
size_t base_len;
size_t seg_len;
bool needs_sep;
size_t total_len;
char *buffer;
size_t offset = 0u;
if (base == NULL || segment == NULL || out_path == NULL) {
return false;
}
if (base[0] == '\0') {
return false;
}
base_len = strlen(base);
seg_len = strlen(segment);
needs_sep = base[base_len - 1u] != '/';
if (seg_len > SIZE_MAX - base_len - 2u) {
return false;
}
total_len = base_len + (needs_sep ? 1u : 0u) + seg_len + 1u;
buffer = (char *)malloc(total_len);
if (buffer == NULL) {
return false;
}
memcpy(buffer + offset, base, base_len);
offset += base_len;
if (needs_sep) {
buffer[offset++] = '/';
}
if (seg_len != 0u) {
memcpy(buffer + offset, segment, seg_len);
offset += seg_len;
}
buffer[offset] = '\0';
*out_path = buffer;
return true;
}
static bool amduatd_space_roots_segment_valid(const char *segment) {
size_t len;
size_t i;
if (segment == NULL) {
return false;
}
len = strlen(segment);
if (len == 0u) {
return false;
}
if (len == 1u && segment[0] == '.') {
return false;
}
if (len == 2u && segment[0] == '.' && segment[1] == '.') {
return false;
}
for (i = 0u; i < len; ++i) {
unsigned char c = (unsigned char)segment[i];
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-') {
continue;
}
return false;
}
return true;
}
static bool amduatd_space_roots_list_reserve(
amduatd_space_roots_list_t *list,
size_t extra) {
size_t need;
size_t next_cap;
char **next;
if (list == NULL) {
return false;
}
if (extra > (SIZE_MAX - list->len)) {
return false;
}
need = list->len + extra;
if (need <= list->cap) {
return true;
}
next_cap = list->cap != 0u ? list->cap : 8u;
while (next_cap < need) {
if (next_cap > (SIZE_MAX / 2u)) {
next_cap = need;
break;
}
next_cap *= 2u;
}
next = (char **)realloc(list->names, next_cap * sizeof(*next));
if (next == NULL) {
return false;
}
list->names = next;
list->cap = next_cap;
return true;
}
static bool amduatd_space_roots_list_add(amduatd_space_roots_list_t *list,
const char *name) {
size_t len;
char *copy;
if (list == NULL || name == NULL) {
return false;
}
if (!amduat_asl_pointer_name_is_valid(name)) {
return false;
}
len = strlen(name);
if (len > SIZE_MAX - 1u) {
return false;
}
if (!amduatd_space_roots_list_reserve(list, 1u)) {
return false;
}
copy = (char *)malloc(len + 1u);
if (copy == NULL) {
return false;
}
memcpy(copy, name, len);
copy[len] = '\0';
list->names[list->len++] = copy;
return true;
}
static int amduatd_space_roots_list_cmp(const void *a, const void *b) {
const char *const *lhs = (const char *const *)a;
const char *const *rhs = (const char *const *)b;
if (lhs == NULL || rhs == NULL || *lhs == NULL || *rhs == NULL) {
return 0;
}
return strcmp(*lhs, *rhs);
}
static void amduatd_space_roots_list_sort_dedupe(
amduatd_space_roots_list_t *list) {
size_t out = 0u;
if (list == NULL || list->len == 0u) {
return;
}
qsort(list->names, list->len, sizeof(*list->names),
amduatd_space_roots_list_cmp);
for (size_t i = 0u; i < list->len; ++i) {
if (out != 0u && strcmp(list->names[i], list->names[out - 1u]) == 0) {
free(list->names[i]);
continue;
}
list->names[out++] = list->names[i];
}
list->len = out;
}
static bool amduatd_space_roots_collect_dir(
const char *dir_path,
const char *rel_name,
amduatd_space_roots_list_t *list) {
DIR *dir;
struct dirent *entry;
if (dir_path == NULL || rel_name == NULL || list == NULL) {
return false;
}
dir = opendir(dir_path);
if (dir == NULL) {
if (errno == ENOENT) {
return true;
}
return false;
}
while ((entry = readdir(dir)) != NULL) {
const char *name = entry->d_name;
char *child_path = NULL;
struct stat st;
bool is_dir = false;
bool is_file = false;
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) {
continue;
}
if (!amduatd_space_roots_segment_valid(name)) {
continue;
}
if (!amduatd_space_roots_join_path(dir_path, name, &child_path)) {
closedir(dir);
return false;
}
if (stat(child_path, &st) != 0) {
free(child_path);
closedir(dir);
return false;
}
is_dir = S_ISDIR(st.st_mode);
is_file = S_ISREG(st.st_mode);
if (is_dir) {
char *next_rel = NULL;
size_t rel_len = strlen(rel_name);
size_t name_len = strlen(name);
size_t total_len = rel_len + 1u + name_len + 1u;
if (rel_len == 0u) {
total_len = name_len + 1u;
}
next_rel = (char *)malloc(total_len);
if (next_rel == NULL) {
free(child_path);
closedir(dir);
return false;
}
if (rel_len != 0u) {
memcpy(next_rel, rel_name, rel_len);
next_rel[rel_len] = '/';
memcpy(next_rel + rel_len + 1u, name, name_len);
next_rel[rel_len + 1u + name_len] = '\0';
} else {
memcpy(next_rel, name, name_len);
next_rel[name_len] = '\0';
}
if (!amduatd_space_roots_collect_dir(child_path, next_rel, list)) {
free(next_rel);
free(child_path);
closedir(dir);
return false;
}
free(next_rel);
} else if (is_file && strcmp(name, "head") == 0) {
if (rel_name[0] != '\0') {
if (!amduatd_space_roots_list_add(list, rel_name)) {
free(child_path);
closedir(dir);
return false;
}
}
}
free(child_path);
}
closedir(dir);
return true;
}
static bool amduatd_space_roots_collect_cursor_prefix(
const char *pointers_root,
const char *prefix_name,
amduatd_space_roots_list_t *list) {
char *prefix_path = NULL;
bool ok = false;
if (pointers_root == NULL || prefix_name == NULL || list == NULL) {
return false;
}
if (!amduat_asl_pointer_name_is_valid(prefix_name)) {
return false;
}
if (!amduatd_space_roots_join_path(pointers_root, prefix_name,
&prefix_path)) {
return false;
}
ok = amduatd_space_roots_collect_dir(prefix_path, prefix_name, list);
free(prefix_path);
return ok;
}
static bool amduatd_space_roots_append_cursor_heads(
const char *store_root,
const amduatd_space_t *effective_space,
bool push,
amduatd_space_roots_list_t *list) {
char *pointers_root = NULL;
amduat_octets_t prefix = amduat_octets(NULL, 0u);
bool ok = false;
if (store_root == NULL || list == NULL) {
return false;
}
if (!amduatd_space_scope_name(effective_space,
push ? "fed/push_cursor" : "fed/cursor",
&prefix)) {
return false;
}
if (!amduatd_space_roots_join_path(store_root, "pointers",
&pointers_root)) {
amduat_octets_free(&prefix);
return false;
}
ok = amduatd_space_roots_collect_cursor_prefix(
pointers_root, (const char *)prefix.data, list);
free(pointers_root);
amduat_octets_free(&prefix);
return ok;
}
bool amduatd_space_roots_cursor_parse(const char *prefix,
const char *pointer_name,
char **out_peer,
char **out_remote_space_id) {
const char suffix[] = "/head";
size_t prefix_len;
size_t name_len;
size_t name_len_full;
size_t suffix_len = sizeof(suffix) - 1u;
size_t peer_len;
size_t remote_len = 0u;
char *peer;
char *remote = NULL;
const char *segment = NULL;
const char *remote_start = NULL;
const char *name_end = NULL;
if (out_peer != NULL) {
*out_peer = NULL;
}
if (out_remote_space_id != NULL) {
*out_remote_space_id = NULL;
}
if (prefix == NULL || pointer_name == NULL || out_peer == NULL) {
return false;
}
prefix_len = strlen(prefix);
name_len_full = strlen(pointer_name);
name_len = name_len_full;
if (name_len_full >= suffix_len &&
strcmp(pointer_name + name_len_full - suffix_len, suffix) == 0) {
name_len = name_len_full - suffix_len;
}
if (name_len <= prefix_len + 1u) {
return false;
}
if (strncmp(pointer_name, prefix, prefix_len) != 0) {
return false;
}
if (pointer_name[prefix_len] != '/') {
return false;
}
segment = pointer_name + prefix_len + 1u;
name_end = pointer_name + name_len;
remote_start = memchr(segment, '/', (size_t)(name_end - segment));
if (remote_start != NULL) {
peer_len = (size_t)(remote_start - segment);
if (remote_start + 1u >= name_end) {
return false;
}
remote_len = (size_t)(name_end - (remote_start + 1u));
} else {
peer_len = (size_t)(name_end - segment);
}
if (peer_len == 0u) {
return false;
}
peer = (char *)malloc(peer_len + 1u);
if (peer == NULL) {
return false;
}
memcpy(peer, segment, peer_len);
peer[peer_len] = '\0';
if (remote_len != 0u && out_remote_space_id != NULL) {
remote = (char *)malloc(remote_len + 1u);
if (remote == NULL) {
free(peer);
return false;
}
memcpy(remote, remote_start + 1u, remote_len);
remote[remote_len] = '\0';
*out_remote_space_id = remote;
}
*out_peer = peer;
return true;
}
static bool amduatd_space_roots_build_collection_head_name(
const char *name,
char **out_name) {
size_t name_len;
size_t total_len;
char *buffer;
size_t offset = 0u;
if (name == NULL || out_name == NULL) {
return false;
}
if (!amduat_asl_pointer_name_is_valid(name)) {
return false;
}
name_len = strlen(name);
total_len = 11u + name_len + 5u + 1u;
buffer = (char *)malloc(total_len);
if (buffer == NULL) {
return false;
}
memcpy(buffer + offset, "collection/", 11u);
offset += 11u;
memcpy(buffer + offset, name, name_len);
offset += name_len;
memcpy(buffer + offset, "/head", 5u);
offset += 5u;
buffer[offset] = '\0';
*out_name = buffer;
return true;
}
static bool amduatd_space_roots_build_collection_log_head_name(
const char *name,
char **out_name) {
size_t name_len;
size_t total_len;
char *buffer;
size_t offset = 0u;
if (name == NULL || out_name == NULL) {
return false;
}
if (!amduat_asl_pointer_name_is_valid(name)) {
return false;
}
name_len = strlen(name);
total_len = 4u + 11u + name_len + 4u + 5u + 1u;
buffer = (char *)malloc(total_len);
if (buffer == NULL) {
return false;
}
memcpy(buffer + offset, "log/", 4u);
offset += 4u;
memcpy(buffer + offset, "collection/", 11u);
offset += 11u;
memcpy(buffer + offset, name, name_len);
offset += name_len;
memcpy(buffer + offset, "/log", 4u);
offset += 4u;
memcpy(buffer + offset, "/head", 5u);
offset += 5u;
buffer[offset] = '\0';
*out_name = buffer;
return true;
}
bool amduatd_space_roots_list(
const char *store_root,
const amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
amduatd_space_roots_list_t *out_list) {
amduat_octets_t edges_collection = amduat_octets(NULL, 0u);
amduat_octets_t edges_index_head = amduat_octets(NULL, 0u);
char *collection_head = NULL;
char *collection_log_head = NULL;
bool ok = false;
if (out_list != NULL) {
memset(out_list, 0, sizeof(*out_list));
}
if (store_root == NULL || pointer_store == NULL || out_list == NULL) {
return false;
}
(void)pointer_store;
if (!amduatd_space_edges_collection_name(effective_space,
&edges_collection) ||
!amduatd_space_edges_index_head_name(effective_space,
&edges_index_head)) {
goto cleanup;
}
if (!amduatd_space_roots_build_collection_head_name(
(const char *)edges_collection.data, &collection_head) ||
!amduatd_space_roots_build_collection_log_head_name(
(const char *)edges_collection.data, &collection_log_head)) {
goto cleanup;
}
if (!amduatd_space_roots_list_add(out_list,
(const char *)edges_index_head.data) ||
!amduatd_space_roots_list_add(out_list, collection_head) ||
!amduatd_space_roots_list_add(out_list, collection_log_head)) {
goto cleanup;
}
if (!amduatd_space_roots_append_cursor_heads(store_root,
effective_space,
false,
out_list) ||
!amduatd_space_roots_append_cursor_heads(store_root,
effective_space,
true,
out_list)) {
goto cleanup;
}
amduatd_space_roots_list_sort_dedupe(out_list);
ok = true;
cleanup:
free((void *)edges_collection.data);
free((void *)edges_index_head.data);
free(collection_head);
free(collection_log_head);
if (!ok) {
amduatd_space_roots_list_free(out_list);
}
return ok;
}
bool amduatd_space_roots_list_cursor_heads(
const char *store_root,
const amduatd_space_t *effective_space,
bool push,
amduatd_space_roots_list_t *out_list) {
bool ok = false;
if (out_list != NULL) {
memset(out_list, 0, sizeof(*out_list));
}
if (store_root == NULL || out_list == NULL) {
return false;
}
if (!amduatd_space_roots_append_cursor_heads(store_root,
effective_space,
push,
out_list)) {
goto cleanup;
}
amduatd_space_roots_list_sort_dedupe(out_list);
ok = true;
cleanup:
if (!ok) {
amduatd_space_roots_list_free(out_list);
}
return ok;
}
bool amduatd_space_roots_list_cursor_peers(
const char *store_root,
const amduatd_space_t *effective_space,
amduatd_space_roots_list_t *out_list) {
amduatd_space_roots_list_t pull_heads;
amduatd_space_roots_list_t push_heads;
amduat_octets_t pull_prefix = amduat_octets(NULL, 0u);
amduat_octets_t push_prefix = amduat_octets(NULL, 0u);
bool ok = false;
memset(&pull_heads, 0, sizeof(pull_heads));
memset(&push_heads, 0, sizeof(push_heads));
if (out_list != NULL) {
memset(out_list, 0, sizeof(*out_list));
}
if (store_root == NULL || out_list == NULL) {
return false;
}
if (!amduatd_space_scope_name(effective_space,
"fed/cursor",
&pull_prefix) ||
!amduatd_space_scope_name(effective_space,
"fed/push_cursor",
&push_prefix)) {
goto cleanup;
}
if (!amduatd_space_roots_list_cursor_heads(store_root,
effective_space,
false,
&pull_heads) ||
!amduatd_space_roots_list_cursor_heads(store_root,
effective_space,
true,
&push_heads)) {
goto cleanup;
}
for (size_t i = 0u; i < pull_heads.len; ++i) {
char *peer = NULL;
if (amduatd_space_roots_cursor_parse((const char *)pull_prefix.data,
pull_heads.names[i],
&peer,
NULL)) {
if (!amduatd_space_roots_list_add(out_list, peer)) {
free(peer);
goto cleanup;
}
free(peer);
}
}
for (size_t i = 0u; i < push_heads.len; ++i) {
char *peer = NULL;
if (amduatd_space_roots_cursor_parse((const char *)push_prefix.data,
push_heads.names[i],
&peer,
NULL)) {
if (!amduatd_space_roots_list_add(out_list, peer)) {
free(peer);
goto cleanup;
}
free(peer);
}
}
amduatd_space_roots_list_sort_dedupe(out_list);
ok = true;
cleanup:
amduatd_space_roots_list_free(&pull_heads);
amduatd_space_roots_list_free(&push_heads);
amduat_octets_free(&pull_prefix);
amduat_octets_free(&push_prefix);
if (!ok) {
amduatd_space_roots_list_free(out_list);
}
return ok;
}
void amduatd_space_roots_list_free(amduatd_space_roots_list_t *list) {
if (list == NULL) {
return;
}
for (size_t i = 0u; i < list->len; ++i) {
free(list->names[i]);
}
free(list->names);
memset(list, 0, sizeof(*list));
}

49
src/amduatd_space_roots.h Normal file
View file

@ -0,0 +1,49 @@
#ifndef AMDUATD_SPACE_ROOTS_H
#define AMDUATD_SPACE_ROOTS_H
#include "amduatd_space.h"
#include "amduat/asl/asl_pointer_fs.h"
#include <stddef.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
char **names;
size_t len;
size_t cap;
} amduatd_space_roots_list_t;
bool amduatd_space_roots_list(
const char *store_root,
const amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *effective_space,
amduatd_space_roots_list_t *out_list);
bool amduatd_space_roots_list_cursor_heads(
const char *store_root,
const amduatd_space_t *effective_space,
bool push,
amduatd_space_roots_list_t *out_list);
bool amduatd_space_roots_list_cursor_peers(
const char *store_root,
const amduatd_space_t *effective_space,
amduatd_space_roots_list_t *out_list);
bool amduatd_space_roots_cursor_parse(const char *prefix,
const char *pointer_name,
char **out_peer,
char **out_remote_space_id);
void amduatd_space_roots_list_free(amduatd_space_roots_list_t *list);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUATD_SPACE_ROOTS_H */

View file

@ -0,0 +1,621 @@
#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,
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, "\"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(
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, store_backend)) {
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 */

103
src/amduatd_store.c Normal file
View file

@ -0,0 +1,103 @@
#include "amduatd_store.h"
#include "amduat/asl/asl_store_fs_meta.h"
#include <string.h>
bool amduatd_store_backend_parse(const char *value,
amduatd_store_backend_t *out_backend) {
if (value == NULL || out_backend == NULL) {
return false;
}
if (strcmp(value, "fs") == 0) {
*out_backend = AMDUATD_STORE_BACKEND_FS;
return true;
}
if (strcmp(value, "index") == 0) {
*out_backend = AMDUATD_STORE_BACKEND_INDEX;
return true;
}
return false;
}
const char *amduatd_store_backend_name(amduatd_store_backend_t backend) {
switch (backend) {
case AMDUATD_STORE_BACKEND_FS:
return "fs";
case AMDUATD_STORE_BACKEND_INDEX:
return "index";
default:
return "unknown";
}
}
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,
const char *root_path,
amduatd_store_backend_t backend) {
if (store == NULL || cfg == NULL || ctx == NULL || root_path == NULL) {
return false;
}
memset(store, 0, sizeof(*store));
memset(ctx, 0, sizeof(*ctx));
memset(cfg, 0, sizeof(*cfg));
if (!amduat_asl_store_fs_load_config(root_path, cfg)) {
return false;
}
if (backend == AMDUATD_STORE_BACKEND_FS) {
if (!amduat_asl_store_fs_init(&ctx->fs, cfg->config, root_path)) {
return false;
}
amduat_asl_store_init(store,
cfg->config,
amduat_asl_store_fs_ops(),
&ctx->fs);
return true;
}
if (backend == AMDUATD_STORE_BACKEND_INDEX) {
if (!amduat_asl_store_index_fs_init(&ctx->index_fs,
cfg->config,
root_path)) {
return false;
}
amduat_asl_store_init(store,
cfg->config,
amduat_asl_store_index_fs_ops(),
&ctx->index_fs);
return true;
}
return false;
}

55
src/amduatd_store.h Normal file
View file

@ -0,0 +1,55 @@
#ifndef AMDUATD_STORE_H
#define AMDUATD_STORE_H
#include "amduat/asl/asl_store_fs.h"
#include "amduat/asl/asl_store_fs_meta.h"
#include "amduat/asl/asl_store_index_fs.h"
#include "amduat/asl/store.h"
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
AMDUATD_STORE_BACKEND_FS = 0,
AMDUATD_STORE_BACKEND_INDEX = 1
} amduatd_store_backend_t;
typedef struct {
amduat_asl_store_fs_t fs;
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,
const char *root_path,
amduatd_store_backend_t backend);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUATD_STORE_H */

427
src/amduatd_ui.c Normal file
View file

@ -0,0 +1,427 @@
#include "amduatd_ui.h"
#include "amduatd_http.h"
#include "amduat/asl/artifact_io.h"
#include "amduat/asl/asl_store_fs.h"
#include "amduat/asl/asl_store_fs_meta.h"
#include "amduat/asl/ref_derive.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
static const char k_amduatd_ui_html[] =
"<!doctype html>\n"
"<html lang=\"en\">\n"
"<head>\n"
" <meta charset=\"utf-8\" />\n"
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n"
" <title>amduatd — Concept editor</title>\n"
" <style>\n"
" :root{\n"
" --bg:#0b1220;--card:#111a2e;--text:#eaf0ff;--muted:#b7c3e6;--border:rgba(255,255,255,.10);\n"
" --shadow:0 10px 30px rgba(0,0,0,.35);--radius:18px;--max:980px;--pad:clamp(16px,3.5vw,28px);\n"
" }\n"
" *{box-sizing:border-box;}\n"
" html,body{min-height:100%;}\n"
" html{background:var(--bg);}\n"
" body{margin:0;min-height:100vh;font-family:\"Avenir Next\",\"Avenir\",\"Trebuchet MS\",\"Segoe UI\",sans-serif;color:var(--text);line-height:1.55;"
" background:radial-gradient(900px 400px at 15% 10%,rgba(95,145,255,.35),transparent 60%),"
" radial-gradient(800px 450px at 85% 20%,rgba(255,140,92,.25),transparent 60%),"
" radial-gradient(700px 500px at 50% 95%,rgba(56,220,181,.18),transparent 60%),var(--bg);}\n"
" a{color:inherit;text-decoration:none;}\n"
" a:hover{text-decoration:underline;text-underline-offset:4px;}\n"
" .wrap{max-width:var(--max);margin:0 auto;padding:26px var(--pad) 70px;min-height:100vh;}\n"
" header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 0 22px;}\n"
" .brand{display:flex;align-items:center;gap:10px;font-weight:700;letter-spacing:.2px;}\n"
" .logo{width:38px;height:38px;border-radius:12px;border:1px solid var(--border);"
" background:linear-gradient(135deg,rgba(95,145,255,.9),rgba(56,220,181,.8));box-shadow:var(--shadow);}\n"
" nav{display:flex;gap:14px;flex-wrap:wrap;color:var(--muted);font-size:14px;}\n"
" nav a{padding:6px 10px;border-radius:10px;}\n"
" nav a:hover{background:rgba(255,255,255,.06);text-decoration:none;}\n"
" .hero{border:1px solid var(--border);background:rgba(17,26,46,.72);border-radius:var(--radius);box-shadow:var(--shadow);"
" padding:clamp(22px,4.5vw,42px);backdrop-filter:blur(10px);}\n"
" h1{margin:0 0 10px;font-size:clamp(28px,3.6vw,40px);line-height:1.1;letter-spacing:-0.6px;}\n"
" .lead{margin:0 0 18px;color:var(--muted);font-size:clamp(14px,2vw,17px);max-width:70ch;}\n"
" .cta-row{display:flex;flex-wrap:wrap;gap:12px;margin-top:10px;}\n"
" .grid{display:grid;grid-template-columns:repeat(12,1fr);gap:14px;margin-top:16px;}\n"
" .card{grid-column:span 12;border:1px solid var(--border);background:rgba(17,26,46,.62);border-radius:16px;padding:16px;"
" box-shadow:0 8px 22px rgba(0,0,0,.25);backdrop-filter:blur(10px);}\n"
" .card h2{margin:2px 0 6px;font-size:16px;letter-spacing:.1px;}\n"
" .muted{color:var(--muted);font-size:13px;}\n"
" .span-7{grid-column:span 12;}\n"
" .span-5{grid-column:span 12;}\n"
" .span-6{grid-column:span 12;}\n"
" .stack{display:grid;gap:14px;}\n"
" @media (min-width: 980px){.span-7{grid-column:span 7;}.span-5{grid-column:span 5;}.span-6{grid-column:span 6;}}\n"
" textarea,input,select{width:100%;box-sizing:border-box;border-radius:12px;padding:10px;border:1px solid var(--border);"
" background:rgba(0,0,0,.12);color:var(--text);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace;"
" font-size:12.5px;}\n"
" textarea{min-height:420px;resize:vertical;}\n"
" .btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;padding:10px 14px;border-radius:12px;border:1px solid var(--border);"
" background:rgba(255,255,255,.06);color:var(--text);font-weight:600;font-size:14px;cursor:pointer;}\n"
" .btn:hover{background:rgba(255,255,255,.10);}\n"
" .btn.primary{background:linear-gradient(135deg,rgba(95,145,255,.95),rgba(56,220,181,.85));border-color:rgba(255,255,255,.18);}\n"
" .btn.primary:hover{filter:brightness(1.05);}\n"
" .row{display:flex;gap:10px;flex-wrap:wrap;align-items:center;}\n"
" .row > *{flex:1 1 auto;}\n"
" .row .btn{flex:0 0 auto;}\n"
" pre{white-space:pre-wrap;word-break:break-word;margin:0;padding:10px;border-radius:12px;border:1px solid var(--border);"
" background:rgba(0,0,0,.2);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace;"
" font-size:12.5px;min-height:120px;color:var(--text);}\n"
" footer{margin-top:26px;color:var(--muted);font-size:13px;display:flex;gap:10px;justify-content:space-between;flex-wrap:wrap;}\n"
" .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}\n"
" </style>\n"
"</head>\n"
"<body>\n"
" <div class=\"wrap\">\n"
" <header>\n"
" <div class=\"brand\" aria-label=\"Site brand\">\n"
" <div class=\"logo\" aria-hidden=\"true\"></div>\n"
" <span>amduatd</span>\n"
" </div>\n"
" <nav aria-label=\"Primary\">\n"
" <a href=\"#editor\">Editor</a>\n"
" <a href=\"#runner\">Run</a>\n"
" <a href=\"#relations\">Relations</a>\n"
" <a href=\"#about\">About</a>\n"
" </nav>\n"
" </header>\n"
" <main>\n"
" <section class=\"hero\" aria-labelledby=\"title\">\n"
" <h1 id=\"title\">Concept editor + PEL runner</h1>\n"
" <p class=\"lead\">Load shows the latest materialized bytes; Save uploads a new artifact and publishes a new version. Use the runner to execute PEL programs against stored artifacts.</p>\n"
" <div class=\"cta-row\">\n"
" <a class=\"btn primary\" href=\"#editor\" role=\"button\">Open editor</a>\n"
" <a class=\"btn\" href=\"#runner\" role=\"button\">Run program</a>\n"
" </div>\n"
" </section>\n"
" <section class=\"grid\" style=\"margin-top:16px;\">\n"
" <div class=\"card span-7\" id=\"editor\">\n"
" <div class=\"row\">\n"
" <input id=\"conceptName\" placeholder=\"concept name (e.g. hello)\" />\n"
" <button class=\"btn\" id=\"btnConceptCreate\" type=\"button\">Create</button>\n"
" <button class=\"btn\" id=\"btnLoad\" type=\"button\">Load</button>\n"
" <button class=\"btn\" id=\"btnSave\" type=\"button\">Save</button>\n"
" </div>\n"
" <div class=\"row\" style=\"margin-top:10px;\">\n"
" <select id=\"mode\">\n"
" <option value=\"text\">bytes: text (utf-8)</option>\n"
" <option value=\"base64\">bytes: base64</option>\n"
" <option value=\"hex\">bytes: hex</option>\n"
" <option value=\"pel_program\">PEL program: JSON</option>\n"
" </select>\n"
" <input id=\"typeTag\" placeholder=\"X-Amduat-Type-Tag (optional)\" />\n"
" <input id=\"latestRef\" placeholder=\"latest_ref\" readonly />\n"
" </div>\n"
" <textarea id=\"editor\" spellcheck=\"false\" placeholder=\"(bytes or PEL program authoring JSON)\"></textarea>\n"
" <div class=\"row\" style=\"margin-top:10px;\">\n"
" <button class=\"btn\" id=\"btnProgramTemplate\" type=\"button\">Insert identity program</button>\n"
" </div>\n"
" <div class=\"row\" style=\"margin-top:10px;\">\n"
" <input id=\"publishRef\" placeholder=\"publish existing ref\" />\n"
" <button class=\"btn\" id=\"btnPublishRef\" type=\"button\">Publish ref</button>\n"
" </div>\n"
" </div>\n"
"\n"
" <div class=\"stack span-5\">\n"
" <div class=\"card\" id=\"runner\">\n"
" <div class=\"muted\" style=\"margin-bottom:8px;\">Upload bytes (sets program_ref)</div>\n"
" <div class=\"row\">\n"
" <input id=\"uploadFile\" type=\"file\" />\n"
" <button class=\"btn\" id=\"btnUpload\" type=\"button\">Upload</button>\n"
" </div>\n"
" <hr style=\"border:none;border-top:1px solid rgba(255,255,255,.10);margin:14px 0;\" />\n"
" <div class=\"muted\" style=\"margin-bottom:8px;\">Run</div>\n"
" <input id=\"programRef\" placeholder=\"program_ref (hex ref or concept name)\" />\n"
" <div class=\"muted\" style=\"margin:10px 0 8px;\">input_refs (comma-separated hex refs or names)</div>\n"
" <input id=\"inputRefs\" placeholder=\"in0,in1,...\" />\n"
" <div class=\"muted\" style=\"margin:10px 0 8px;\">params_ref (optional)</div>\n"
" <input id=\"paramsRef\" placeholder=\"params\" />\n"
" <div class=\"muted\" style=\"margin:10px 0 8px;\">scheme_ref (optional, default dag)</div>\n"
" <input id=\"schemeRef\" placeholder=\"dag\" />\n"
" <div class=\"row\" style=\"margin-top:12px;\">\n"
" <button class=\"btn primary\" id=\"btnRun\" type=\"button\">Run</button>\n"
" <a class=\"muted\" href=\"/v1/contract\">/v1/contract</a>\n"
" <a class=\"muted\" href=\"/v1/meta\">/v1/meta</a>\n"
" </div>\n"
" <div class=\"muted\" style=\"margin:14px 0 8px;\">Response</div>\n"
" <pre id=\"out\"></pre>\n"
" </div>\n"
" <div class=\"card\" id=\"relations\">\n"
" <div class=\"muted\" style=\"margin-bottom:8px;\">Relations</div>\n"
" <div class=\"row\" style=\"margin-top:10px;\">\n"
" <button class=\"btn\" id=\"btnRelations\" type=\"button\">Refresh</button>\n"
" </div>\n"
" <pre id=\"relationsOut\"></pre>\n"
" </div>\n"
" </div>\n"
" </section>\n"
" <section id=\"about\" class=\"grid\" style=\"margin-top:16px;\">\n"
" <article class=\"card span-6\">\n"
" <h2>About</h2>\n"
" <p class=\"muted\">amduatd is a local-first mapping surface over a single ASL store root. This UI is a lightweight editor and runner for concepts and PEL programs.</p>\n"
" </article>\n"
" <article class=\"card span-6\">\n"
" <h2>Links</h2>\n"
" <p class=\"muted\"><a href=\"/v1/contract\">/v1/contract</a> • <a href=\"/v1/meta\">/v1/meta</a> • <a href=\"/v1/relations\">/v1/relations</a></p>\n"
" </article>\n"
" </section>\n"
" </main>\n"
" <footer>\n"
" <span>© 2025 Niklas Rydberg.</span>\n"
" <span><a href=\"#title\">Back to top</a></span>\n"
" </footer>\n"
" </div>\n"
"\n"
" <script>\n"
" const el = (id) => document.getElementById(id);\n"
" const out = (v) => { el('out').textContent = typeof v === 'string' ? v : JSON.stringify(v, null, 2); };\n"
" const td = new TextDecoder('utf-8');\n"
" const te = new TextEncoder();\n"
" const toHex = (u8) => Array.from(u8).map(b => b.toString(16).padStart(2,'0')).join('');\n"
" const fromHex = (s) => { const t=(s||'').trim(); if(t.length%2) throw new Error('hex length must be even'); const o=new Uint8Array(t.length/2); for(let i=0;i<o.length;i++){o[i]=parseInt(t.slice(i*2,i*2+2),16);} return o; };\n"
" const toB64 = (u8) => { let bin=''; for(let i=0;i<u8.length;i++) bin += String.fromCharCode(u8[i]); return btoa(bin); };\n"
" const fromB64 = (s) => { const bin=atob((s||'').trim()); const o=new Uint8Array(bin.length); for(let i=0;i<bin.length;i++) o[i]=bin.charCodeAt(i); return o; };\n"
"\n"
" async function ensureConcept(name){\n"
" const resp = await fetch('/v1/concepts',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name})});\n"
" if(resp.status === 409) return; // already exists\n"
" if(!resp.ok) throw new Error(await resp.text());\n"
" }\n"
"\n"
" async function loadConcept(){\n"
" const name = el('conceptName').value.trim();\n"
" if(!name){ out('missing concept name'); return; }\n"
" const resp = await fetch(`/v1/concepts/${encodeURIComponent(name)}`);\n"
" const text = await resp.text();\n"
" if(!resp.ok){ out(text); return; }\n"
" const j = JSON.parse(text);\n"
" el('latestRef').value = j.latest_ref || '';\n"
" el('programRef').value = name;\n"
" if(!j.latest_ref){ el('editor').value=''; out(text); return; }\n"
" const mode = el('mode').value;\n"
" const infoResp = await fetch(`/v1/artifacts/${j.latest_ref}?format=info`);\n"
" if(infoResp.ok){ const info = JSON.parse(await infoResp.text()); el('typeTag').value = info.has_type_tag ? info.type_tag : ''; }\n"
" if(mode === 'pel_program'){\n"
" if(!el('editor').value.trim()){\n"
" el('editor').value = JSON.stringify({\n"
" nodes:[{id:1,op:{name:'pel.bytes.concat',version:1},inputs:[{external:{input_index:0}}],params_hex:''}],\n"
" roots:[{node_id:1,output_index:0}]\n"
" }, null, 2);\n"
" }\n"
" out(text);\n"
" return;\n"
" }\n"
" const aResp = await fetch(`/v1/artifacts/${j.latest_ref}`);\n"
" if(!aResp.ok){ out(await aResp.text()); return; }\n"
" const u8 = new Uint8Array(await aResp.arrayBuffer());\n"
" if(mode==='hex') el('editor').value = toHex(u8);\n"
" else if(mode==='base64') el('editor').value = toB64(u8);\n"
" else el('editor').value = td.decode(u8);\n"
" out(text);\n"
" }\n"
"\n"
" async function saveConcept(){\n"
" const name = el('conceptName').value.trim();\n"
" if(!name){ out('missing concept name'); return; }\n"
" await ensureConcept(name);\n"
" const mode = el('mode').value;\n"
" if(mode === 'pel_program'){\n"
" const body = JSON.parse(el('editor').value || '{}');\n"
" const mkResp = await fetch('/v1/pel/programs',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});\n"
" const mkText = await mkResp.text();\n"
" if(!mkResp.ok){ out(mkText); return; }\n"
" const mk = JSON.parse(mkText);\n"
" const pref = mk.program_ref;\n"
" const pubResp = await fetch(`/v1/concepts/${encodeURIComponent(name)}/publish`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ref:pref})});\n"
" const pubText = await pubResp.text();\n"
" out(pubText);\n"
" if(pubResp.ok){ el('typeTag').value = '0x00000101'; await loadConcept(); }\n"
" return;\n"
" }\n"
" let u8;\n"
" if(mode==='hex') u8 = fromHex(el('editor').value);\n"
" else if(mode==='base64') u8 = fromB64(el('editor').value);\n"
" else u8 = te.encode(el('editor').value);\n"
" const headers = {'Content-Type':'application/octet-stream'};\n"
" const typeTag = el('typeTag').value.trim();\n"
" if(typeTag) headers['X-Amduat-Type-Tag'] = typeTag;\n"
" const putResp = await fetch('/v1/artifacts',{method:'POST',headers,body:u8});\n"
" const putText = await putResp.text();\n"
" if(!putResp.ok){ out(putText); return; }\n"
" const put = JSON.parse(putText);\n"
" const pubResp = await fetch(`/v1/concepts/${encodeURIComponent(name)}/publish`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ref:put.ref})});\n"
" const pubText = await pubResp.text();\n"
" out(pubText);\n"
" if(pubResp.ok) await loadConcept();\n"
" }\n"
"\n"
" el('btnConceptCreate').addEventListener('click', async () => { try{ await ensureConcept(el('conceptName').value.trim()); out('{\"ok\":true}\\n'); }catch(e){ out(String(e)); } });\n"
" el('btnLoad').addEventListener('click', () => loadConcept().catch(e => out(String(e))));\n"
" el('btnSave').addEventListener('click', () => saveConcept().catch(e => out(String(e))));\n"
" el('mode').addEventListener('change', () => {\n"
" if(el('mode').value === 'pel_program'){\n"
" el('typeTag').value = '0x00000101';\n"
" }\n"
" loadConcept().catch(() => {});\n"
" });\n"
"\n"
" el('btnProgramTemplate').addEventListener('click', () => {\n"
" el('mode').value = 'pel_program';\n"
" el('typeTag').value = '0x00000101';\n"
" el('editor').value = JSON.stringify({\n"
" nodes:[{id:1,op:{name:'pel.bytes.concat',version:1},inputs:[{external:{input_index:0}}],params_hex:''}],\n"
" roots:[{node_id:1,output_index:0}]\n"
" }, null, 2);\n"
" });\n"
"\n"
" el('btnPublishRef').addEventListener('click', async () => {\n"
" try{\n"
" const name = el('conceptName').value.trim();\n"
" const ref = el('publishRef').value.trim();\n"
" if(!name||!ref){ out('missing name/ref'); return; }\n"
" await ensureConcept(name);\n"
" const resp = await fetch(`/v1/concepts/${encodeURIComponent(name)}/publish`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ref})});\n"
" out(await resp.text());\n"
" }catch(e){ out(String(e)); }\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', { method:'POST', headers:{'Content-Type':'application/octet-stream'}, body:file });\n"
" const text = await resp.text();\n"
" out(text);\n"
" if (resp.ok) { const j = JSON.parse(text); if (j && j.ref) el('programRef').value = j.ref; }\n"
" } catch (e) { out(String(e)); }\n"
" });\n"
"\n"
" el('btnRun').addEventListener('click', async () => {\n"
" try {\n"
" const program_ref = el('programRef').value.trim();\n"
" const input_refs = (el('inputRefs').value || '').split(',').map(s => s.trim()).filter(Boolean);\n"
" const params_ref = el('paramsRef').value.trim();\n"
" const scheme_ref = el('schemeRef').value.trim();\n"
" const body = { program_ref, input_refs };\n"
" if (params_ref) body.params_ref = params_ref;\n"
" if (scheme_ref) body.scheme_ref = scheme_ref;\n"
" const resp = await fetch('/v1/pel/run', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) });\n"
" out(await resp.text());\n"
" } catch (e) { out(String(e)); }\n"
" });\n"
"\n"
" async function loadRelations(){\n"
" const resp = await fetch('/v1/relations');\n"
" const text = await resp.text();\n"
" el('relationsOut').textContent = text;\n"
" }\n"
" el('btnRelations').addEventListener('click', () => loadRelations().catch(e => out(String(e))));\n"
" loadRelations().catch(() => {});\n"
" </script>\n"
"</body>\n"
"</html>\n";
bool amduatd_ui_can_handle(const amduatd_http_req_t *req) {
char no_query[1024];
if (req == NULL) {
return false;
}
if (strcmp(req->method, "GET") != 0) {
return false;
}
amduatd_path_without_query(req->path, no_query, sizeof(no_query));
return strcmp(no_query, "/v1/ui") == 0;
}
bool amduatd_ui_handle(amduatd_ctx_t *ctx,
const amduatd_http_req_t *req,
amduatd_http_resp_t *resp) {
amduat_artifact_t artifact;
amduat_asl_store_error_t err;
if (ctx == NULL || req == NULL || resp == NULL) {
return false;
}
if (!amduatd_ui_can_handle(req)) {
return false;
}
if (ctx->store == NULL || ctx->ui_ref.hash_id == 0 ||
ctx->ui_ref.digest.data == NULL || ctx->ui_ref.digest.len == 0) {
resp->ok = amduatd_http_send_text(resp->fd,
500,
"Internal Server Error",
"ui not available\n",
false);
return true;
}
memset(&artifact, 0, sizeof(artifact));
err = amduat_asl_store_get(ctx->store, ctx->ui_ref, &artifact);
if (err == AMDUAT_ASL_STORE_ERR_NOT_FOUND) {
resp->ok = amduatd_http_send_text(resp->fd,
404,
"Not Found",
"not found\n",
false);
return true;
}
if (err != AMDUAT_ASL_STORE_OK) {
amduat_asl_artifact_free(&artifact);
resp->ok = amduatd_http_send_text(resp->fd,
500,
"Internal Server Error",
"store error\n",
false);
return true;
}
if (artifact.bytes.len != 0 && artifact.bytes.data == NULL) {
amduat_asl_artifact_free(&artifact);
resp->ok = amduatd_http_send_text(resp->fd,
500,
"Internal Server Error",
"store error\n",
false);
return true;
}
resp->ok = amduatd_http_send_status(resp->fd,
200,
"OK",
"text/html; charset=utf-8",
artifact.bytes.data,
artifact.bytes.len,
false);
amduat_asl_artifact_free(&artifact);
return true;
}
bool amduatd_seed_ui_html(amduat_asl_store_t *store,
const amduat_asl_store_fs_config_t *cfg,
amduat_reference_t *out_ref) {
amduat_artifact_t artifact;
amduat_asl_store_error_t err;
if (out_ref != NULL) {
memset(out_ref, 0, sizeof(*out_ref));
}
if (store == NULL || cfg == NULL || out_ref == NULL) {
return false;
}
artifact = amduat_artifact(amduat_octets(k_amduatd_ui_html,
strlen(k_amduatd_ui_html)));
(void)amduat_asl_ref_derive(artifact,
cfg->config.encoding_profile_id,
cfg->config.hash_id,
out_ref,
NULL);
err = amduat_asl_store_put(store, artifact, out_ref);
if (err != AMDUAT_ASL_STORE_OK) {
return false;
}
return true;
}

52
src/amduatd_ui.h Normal file
View file

@ -0,0 +1,52 @@
#ifndef AMDUATD_UI_H
#define AMDUATD_UI_H
#include "amduat/asl/asl_store_fs_meta.h"
#include "amduat/asl/store.h"
#include "amduatd_http.h"
#include <stdbool.h>
#include <stddef.h>
#include <sys/types.h>
#ifndef AMDUATD_ENABLE_UI
#define AMDUATD_ENABLE_UI 1
#endif
typedef struct amduatd_caps_t amduatd_caps_t;
typedef struct amduatd_cfg_t amduatd_cfg_t;
typedef struct amduatd_concepts_t amduatd_concepts_t;
typedef struct {
amduat_asl_store_t *store;
amduat_reference_t ui_ref;
const amduat_asl_store_fs_config_t *store_cfg;
amduatd_concepts_t *concepts;
const amduatd_cfg_t *daemon_cfg;
const char *root_path;
amduatd_caps_t *caps;
} amduatd_ctx_t;
#if AMDUATD_ENABLE_UI
bool amduatd_ui_can_handle(const amduatd_http_req_t *req);
bool amduatd_ui_handle(amduatd_ctx_t *ctx,
const amduatd_http_req_t *req,
amduatd_http_resp_t *resp);
#else
static inline bool amduatd_ui_can_handle(const amduatd_http_req_t *req) {
(void)req;
return false;
}
static inline bool amduatd_ui_handle(amduatd_ctx_t *ctx,
const amduatd_http_req_t *req,
amduatd_http_resp_t *resp) {
(void)ctx;
(void)req;
(void)resp;
return false;
}
#endif
#endif

1472
src/asl_gc_fs.c Normal file

File diff suppressed because it is too large Load diff

34
src/asl_gc_fs.h Normal file
View file

@ -0,0 +1,34 @@
#ifndef AMDUAT_API_ASL_GC_FS_H
#define AMDUAT_API_ASL_GC_FS_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
bool keep_materializations;
bool delete_artifacts;
bool dry_run;
} amduat_asl_gc_fs_options_t;
typedef struct {
size_t pointer_roots;
size_t materialization_roots;
size_t marked_artifacts;
size_t candidates;
uint64_t candidate_bytes;
} amduat_asl_gc_fs_stats_t;
bool amduat_asl_gc_fs_run(const char *root_path,
const amduat_asl_gc_fs_options_t *opts,
amduat_asl_gc_fs_stats_t *out_stats);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* AMDUAT_API_ASL_GC_FS_H */

View file

@ -0,0 +1,199 @@
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include "amduatd_derivation_index.h"
#include "amduat/asl/asl_derivation_index_fs.h"
#include "amduat/hash/asl1.h"
#include "amduat/pel/derivation_sid.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static char *amduatd_test_make_temp_dir(void) {
char tmpl[] = "/tmp/amduatd-deriv-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_test_make_ref(uint8_t seed, amduat_reference_t *out_ref) {
uint8_t *digest;
size_t i;
if (out_ref == NULL) {
return false;
}
digest = (uint8_t *)malloc(32u);
if (digest == NULL) {
return false;
}
for (i = 0u; i < 32u; ++i) {
digest[i] = (uint8_t)(seed + i);
}
*out_ref = amduat_reference(AMDUAT_HASH_ASL1_ID_SHA256,
amduat_octets(digest, 32u));
return true;
}
int main(void) {
char *root = amduatd_test_make_temp_dir();
amduat_asl_derivation_index_fs_t index;
amduat_reference_t program_ref;
amduat_reference_t input_ref;
amduat_reference_t output_ref;
amduat_reference_t result_ref;
amduat_reference_t params_ref;
amduat_reference_t input_refs[1];
amduat_reference_t output_refs[1];
amduat_pel_run_result_t run_result;
amduat_asl_derivation_record_t *records = NULL;
size_t record_count = 0u;
amduat_asl_store_error_t err;
if (root == NULL) {
return 1;
}
if (!amduatd_test_make_ref(1u, &program_ref) ||
!amduatd_test_make_ref(2u, &input_ref) ||
!amduatd_test_make_ref(3u, &output_ref) ||
!amduatd_test_make_ref(4u, &result_ref)) {
fprintf(stderr, "failed to build refs\n");
free(root);
return 1;
}
input_refs[0] = input_ref;
output_refs[0] = output_ref;
memset(&params_ref, 0, sizeof(params_ref));
memset(&run_result, 0, sizeof(run_result));
run_result.has_result_value = true;
run_result.result_value.core_result.status = AMDUAT_PEL_EXEC_STATUS_OK;
run_result.result_value.has_store_failure = false;
run_result.output_refs = output_refs;
run_result.output_refs_len = 1u;
run_result.result_ref = result_ref;
err = AMDUAT_ASL_STORE_OK;
if (!amduatd_derivation_index_pel_run(root,
false,
program_ref,
input_refs,
1u,
false,
params_ref,
&run_result,
false,
params_ref,
&err)) {
fprintf(stderr, "unexpected derivation index failure when disabled\n");
free(root);
return 1;
}
if (!amduat_asl_derivation_index_fs_init(&index, root)) {
fprintf(stderr, "failed to init derivation index\n");
free(root);
return 1;
}
err = amduat_asl_derivation_index_fs_list(&index, output_ref,
&records, &record_count);
if (err != AMDUAT_ASL_STORE_ERR_NOT_FOUND) {
fprintf(stderr, "expected no records when disabled, got %d\n", (int)err);
free(root);
return 1;
}
err = AMDUAT_ASL_STORE_OK;
if (!amduatd_derivation_index_pel_run(root,
true,
program_ref,
input_refs,
1u,
false,
params_ref,
&run_result,
false,
params_ref,
&err)) {
fprintf(stderr, "expected derivation index to succeed\n");
free(root);
return 1;
}
err = amduat_asl_derivation_index_fs_list(&index, output_ref,
&records, &record_count);
if (err != AMDUAT_ASL_STORE_OK || record_count != 1u) {
fprintf(stderr, "expected one derivation record, got %d count=%zu\n",
(int)err, record_count);
free(root);
return 1;
}
{
amduat_pel_derivation_sid_input_t sid_input;
amduat_octets_t sid = amduat_octets(NULL, 0u);
memset(&sid_input, 0, sizeof(sid_input));
sid_input.program_ref = program_ref;
sid_input.input_refs = input_refs;
sid_input.input_refs_len = 1u;
sid_input.has_params_ref = false;
sid_input.has_exec_profile = false;
sid_input.exec_profile = amduat_octets(NULL, 0u);
if (!amduat_pel_derivation_sid_compute(&sid_input, &sid)) {
fprintf(stderr, "failed to compute expected sid\n");
amduat_asl_derivation_records_free(records, record_count);
free(root);
return 1;
}
if (!amduat_octets_eq(records[0].sid, sid)) {
fprintf(stderr, "sid mismatch\n");
amduat_octets_free(&sid);
amduat_asl_derivation_records_free(records, record_count);
free(root);
return 1;
}
if (!amduat_reference_eq(records[0].program_ref, program_ref)) {
fprintf(stderr, "program_ref mismatch\n");
amduat_octets_free(&sid);
amduat_asl_derivation_records_free(records, record_count);
free(root);
return 1;
}
if (records[0].output_index != 0u) {
fprintf(stderr, "output_index mismatch\n");
amduat_octets_free(&sid);
amduat_asl_derivation_records_free(records, record_count);
free(root);
return 1;
}
amduat_octets_free(&sid);
}
amduat_asl_derivation_records_free(records, record_count);
amduat_reference_free(&program_ref);
amduat_reference_free(&input_ref);
amduat_reference_free(&output_ref);
amduat_reference_free(&result_ref);
free(root);
return 0;
}

View file

@ -0,0 +1,121 @@
#include "amduatd_fed.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++;
}
}
int main(void) {
amduatd_fed_cfg_t cfg;
amduatd_space_t space;
amduat_octets_t scoped = amduat_octets(NULL, 0u);
uint8_t digest_bytes[32];
amduat_octets_t digest;
amduat_reference_t ref;
char *ref_hex = NULL;
const char *err = NULL;
int i;
memset(digest_bytes, 0x2a, sizeof(digest_bytes));
if (!amduat_octets_clone(amduat_octets(digest_bytes, sizeof(digest_bytes)),
&digest)) {
fprintf(stderr, "FAIL: digest clone\n");
return 1;
}
ref = amduat_reference(AMDUAT_HASH_ASL1_ID_SHA256, digest);
if (!amduat_asl_ref_encode_hex(ref, &ref_hex)) {
fprintf(stderr, "FAIL: ref encode\n");
amduat_reference_free(&ref);
return 1;
}
amduatd_fed_cfg_init(&cfg);
expect(!cfg.enabled, "default disabled");
expect(cfg.transport_kind == AMDUATD_FED_TRANSPORT_STUB, "default transport");
expect(!cfg.require_space, "default require_space");
{
char *argv[] = {
"amduatd",
"--fed-enable",
"--fed-transport",
"unix",
"--fed-unix-sock",
"amduatd.sock",
"--fed-domain-id",
"42",
"--fed-registry-ref",
ref_hex,
"--fed-require-space",
};
int argc = (int)(sizeof(argv) / sizeof(argv[0]));
for (i = 1; i < argc; ++i) {
amduatd_fed_parse_result_t rc;
rc = amduatd_fed_cfg_parse_arg(&cfg, argc, argv, &i, &err);
expect(rc == AMDUATD_FED_PARSE_OK, "fed parse ok");
}
}
expect(cfg.enabled, "parsed enabled");
expect(cfg.transport_kind == AMDUATD_FED_TRANSPORT_UNIX,
"parsed transport");
expect(cfg.unix_socket_set, "parsed unix socket");
expect(cfg.local_domain_id == 42u, "parsed domain id");
expect(cfg.registry_ref_set, "parsed registry ref");
expect(cfg.require_space, "parsed require space");
expect(!amduatd_fed_requirements_check(AMDUATD_STORE_BACKEND_FS,
&cfg,
&err),
"requirements reject fs backend");
expect(amduatd_fed_requirements_check(AMDUATD_STORE_BACKEND_INDEX,
&cfg,
&err),
"requirements accept index backend");
if (!amduatd_space_init(&space, NULL, false)) {
fprintf(stderr, "FAIL: space init\n");
amduatd_fed_cfg_free(&cfg);
amduat_reference_free(&ref);
free(ref_hex);
return 1;
}
expect(!amduatd_fed_scope_names(&cfg, &space, "fed", &scoped, &err),
"scope requires space");
amduat_octets_free(&scoped);
if (!amduatd_space_init(&space, "alpha", false)) {
fprintf(stderr, "FAIL: space init alpha\n");
amduatd_fed_cfg_free(&cfg);
amduat_reference_free(&ref);
free(ref_hex);
return 1;
}
expect(amduatd_fed_scope_names(&cfg, &space, "fed", &scoped, &err),
"scope with space");
expect(scoped.data != NULL &&
scoped.len == strlen("space/alpha/fed") &&
memcmp(scoped.data,
"space/alpha/fed",
scoped.len) == 0,
"scoped name");
amduat_octets_free(&scoped);
amduatd_fed_cfg_free(&cfg);
amduat_reference_free(&ref);
free(ref_hex);
return failures == 0 ? 0 : 1;
}

View file

@ -0,0 +1,240 @@
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include "amduatd_fed_cursor.h"
#include "amduatd_store.h"
#include "amduat/asl/asl_store_fs_meta.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-fed-cursor-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;
}
int main(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;
amduatd_fed_cursor_record_t cursor;
amduatd_fed_cursor_record_t fetched;
amduat_reference_t record_ref;
amduat_reference_t wrong_ref;
amduat_reference_t new_ref;
amduat_reference_t get_ref;
amduatd_fed_cursor_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);
expect(amduatd_fed_cursor_check_enabled(&fed_cfg) ==
AMDUATD_FED_CURSOR_ERR_DISABLED,
"disabled federation status");
amduatd_fed_cursor_record_init(&fetched);
memset(&get_ref, 0, sizeof(get_ref));
status = amduatd_fed_cursor_get(&store,
&pointer_store,
&space,
"peer-a",
&fetched,
&get_ref);
expect(status == AMDUATD_FED_CURSOR_ERR_NOT_FOUND, "empty cursor not found");
amduatd_fed_cursor_record_free(&fetched);
amduatd_fed_cursor_record_init(&cursor);
cursor.peer_key = strdup("peer-a");
cursor.space_id = strdup("alpha");
if (cursor.peer_key == NULL || cursor.space_id == NULL) {
fprintf(stderr, "failed to allocate cursor identifiers\n");
amduatd_fed_cursor_record_free(&cursor);
free(root);
return 1;
}
cursor.has_logseq = true;
cursor.last_logseq = 42u;
if (!amduatd_make_test_ref(0x11, &record_ref)) {
fprintf(stderr, "failed to make record ref\n");
amduatd_fed_cursor_record_free(&cursor);
free(root);
return 1;
}
cursor.has_record_ref = true;
cursor.last_record_ref = record_ref;
memset(&new_ref, 0, sizeof(new_ref));
status = amduatd_fed_cursor_cas_set(&store,
&pointer_store,
&space,
"peer-a",
NULL,
&cursor,
&new_ref);
expect(status == AMDUATD_FED_CURSOR_OK, "cursor set ok");
amduatd_fed_cursor_record_init(&fetched);
memset(&get_ref, 0, sizeof(get_ref));
status = amduatd_fed_cursor_get(&store,
&pointer_store,
&space,
"peer-a",
&fetched,
&get_ref);
expect(status == AMDUATD_FED_CURSOR_OK, "cursor get ok");
expect(strcmp(fetched.peer_key, "peer-a") == 0, "peer key match");
expect(fetched.space_id != NULL && strcmp(fetched.space_id, "alpha") == 0,
"space match");
expect(fetched.has_logseq && fetched.last_logseq == 42u, "logseq match");
expect(fetched.has_record_ref &&
amduat_reference_eq(fetched.last_record_ref, record_ref),
"record ref match");
expect(amduat_reference_eq(get_ref, new_ref), "pointer ref match");
amduatd_fed_cursor_record_free(&fetched);
amduat_reference_free(&get_ref);
if (!amduatd_make_test_ref(0x22, &wrong_ref)) {
fprintf(stderr, "failed to make wrong ref\n");
amduat_reference_free(&new_ref);
amduatd_fed_cursor_record_free(&cursor);
free(root);
return 1;
}
status = amduatd_fed_cursor_cas_set(&store,
&pointer_store,
&space,
"peer-a",
&wrong_ref,
&cursor,
NULL);
expect(status == AMDUATD_FED_CURSOR_ERR_CONFLICT, "cursor cas conflict");
amduat_reference_free(&wrong_ref);
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&new_ref);
{
amduat_octets_t name = amduat_octets(NULL, 0u);
bool ok = amduatd_fed_cursor_pointer_name(&space,
"bad peer",
&name);
expect(!ok, "peer validation rejects invalid key");
amduat_octets_free(&name);
}
{
amduat_octets_t name = amduat_octets(NULL, 0u);
bool ok = amduatd_fed_cursor_pointer_name_v2(&space,
"peer-a",
"beta",
&name);
expect(ok, "v2 cursor pointer name ok");
expect(strcmp((const char *)name.data,
"space/alpha/fed/cursor/peer-a/beta/head") == 0,
"v2 cursor pointer name matches");
amduat_octets_free(&name);
}
{
amduat_octets_t name = amduat_octets(NULL, 0u);
bool ok = amduatd_fed_push_cursor_pointer_name_v2(&space,
"peer-a",
"beta",
&name);
expect(ok, "v2 push cursor pointer name ok");
expect(strcmp((const char *)name.data,
"space/alpha/fed/push_cursor/peer-a/beta/head") == 0,
"v2 push cursor pointer name matches");
amduat_octets_free(&name);
}
{
amduat_octets_t name = amduat_octets(NULL, 0u);
bool ok = amduatd_fed_cursor_pointer_name_v2(&space,
"peer-a",
"bad/space",
&name);
expect(!ok, "remote space validation rejects invalid id");
amduat_octets_free(&name);
}
free(root);
return failures == 0 ? 0 : 1;
}

View file

@ -0,0 +1,581 @@
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include "amduatd_fed_pull_apply.h"
#include "amduatd_fed_cursor.h"
#include "amduatd_store.h"
#include "amduat/asl/artifact_io.h"
#include "amduat/asl/asl_store_fs_meta.h"
#include "amduat/asl/ref_derive.h"
#include "amduat/hash/asl1.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
amduat_fed_record_t *records;
size_t record_count;
amduat_octets_t *artifact_bytes;
bool fail_artifact;
size_t fail_index;
int fail_status;
bool mutate_cursor;
amduat_asl_store_t *store;
amduat_asl_pointer_store_t *pointer_store;
const amduatd_space_t *space;
const char *peer_key;
} amduatd_test_pull_transport_t;
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-fed-pull-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_test_clone_record(const amduat_fed_record_t *src,
amduat_fed_record_t *dst) {
if (src == NULL || dst == NULL) {
return false;
}
*dst = *src;
if (!amduat_reference_clone(src->id.ref, &dst->id.ref)) {
return false;
}
return true;
}
static bool amduatd_test_pull_get_records(void *ctx,
uint32_t domain_id,
uint64_t from_logseq,
uint64_t limit,
int *out_status,
amduat_fed_record_t **out_records,
size_t *out_len,
char **out_body) {
amduatd_test_pull_transport_t *t = (amduatd_test_pull_transport_t *)ctx;
amduat_fed_record_t *records = NULL;
size_t i;
(void)domain_id;
(void)from_logseq;
(void)limit;
if (out_status == NULL || out_records == NULL || out_len == NULL) {
return false;
}
*out_status = 200;
*out_records = NULL;
*out_len = 0;
if (out_body != NULL) {
*out_body = NULL;
}
if (t == NULL || t->record_count == 0u) {
return true;
}
records = (amduat_fed_record_t *)calloc(t->record_count, sizeof(*records));
if (records == NULL) {
return false;
}
for (i = 0; i < t->record_count; ++i) {
if (!amduatd_test_clone_record(&t->records[i], &records[i])) {
free(records);
return false;
}
}
*out_records = records;
*out_len = t->record_count;
if (t->mutate_cursor && t->store != NULL && t->pointer_store != NULL &&
t->space != NULL && t->peer_key != NULL) {
amduatd_fed_cursor_record_t cursor;
amduat_reference_t cursor_ref;
amduatd_fed_cursor_record_t current;
amduat_reference_t current_ref;
const amduat_reference_t *expected_ref = NULL;
amduatd_fed_cursor_record_init(&cursor);
cursor.peer_key = strdup(t->peer_key);
cursor.space_id = strdup((const char *)t->space->space_id.data);
cursor.has_logseq = true;
cursor.last_logseq = 99u;
cursor.has_record_ref = true;
if (cursor.peer_key != NULL && cursor.space_id != NULL &&
amduat_reference_clone(t->records[0].id.ref,
&cursor.last_record_ref)) {
amduatd_fed_cursor_record_init(&current);
memset(&current_ref, 0, sizeof(current_ref));
if (amduatd_fed_cursor_get(t->store,
t->pointer_store,
t->space,
t->peer_key,
&current,
&current_ref) == AMDUATD_FED_CURSOR_OK) {
expected_ref = &current_ref;
}
(void)amduatd_fed_cursor_cas_set(t->store,
t->pointer_store,
t->space,
t->peer_key,
expected_ref,
&cursor,
&cursor_ref);
if (expected_ref != NULL) {
amduat_reference_free(&current_ref);
}
amduatd_fed_cursor_record_free(&current);
amduat_reference_free(&cursor_ref);
}
amduatd_fed_cursor_record_free(&cursor);
}
return true;
}
static void amduatd_test_pull_free_records(void *ctx,
amduat_fed_record_t *records,
size_t len) {
size_t i;
(void)ctx;
if (records == NULL) {
return;
}
for (i = 0; i < len; ++i) {
amduat_reference_free(&records[i].id.ref);
}
free(records);
}
static bool amduatd_test_pull_get_artifact(void *ctx,
amduat_reference_t ref,
int *out_status,
amduat_octets_t *out_bytes,
char **out_body) {
amduatd_test_pull_transport_t *t = (amduatd_test_pull_transport_t *)ctx;
size_t i;
if (out_status == NULL || out_bytes == NULL) {
return false;
}
*out_status = 404;
*out_bytes = amduat_octets(NULL, 0u);
if (out_body != NULL) {
*out_body = NULL;
}
if (t == NULL) {
return false;
}
for (i = 0; i < t->record_count; ++i) {
if (!amduat_reference_eq(ref, t->records[i].id.ref)) {
continue;
}
if (t->fail_artifact && i == t->fail_index) {
*out_status = t->fail_status;
return true;
}
if (!amduat_octets_clone(t->artifact_bytes[i], out_bytes)) {
return false;
}
*out_status = 200;
return true;
}
return true;
}
static bool amduatd_test_make_record(amduat_asl_store_t *store,
const char *payload,
uint64_t logseq,
uint32_t domain_id,
amduat_fed_record_t *out_record,
amduat_octets_t *out_bytes) {
amduat_artifact_t artifact;
amduat_reference_t ref;
amduat_octets_t artifact_bytes = amduat_octets(NULL, 0u);
amduat_octets_t payload_bytes = amduat_octets(NULL, 0u);
if (store == NULL || payload == NULL || out_record == NULL ||
out_bytes == NULL) {
return false;
}
if (!amduat_octets_clone(amduat_octets(payload, strlen(payload)),
&payload_bytes)) {
return false;
}
if (!amduat_asl_artifact_from_bytes(payload_bytes,
AMDUAT_ASL_IO_RAW,
false,
amduat_type_tag(0u),
&artifact)) {
amduat_octets_free(&payload_bytes);
return false;
}
if (!amduat_asl_ref_derive(artifact,
store->config.encoding_profile_id,
store->config.hash_id,
&ref,
&artifact_bytes)) {
amduat_asl_artifact_free(&artifact);
return false;
}
amduat_asl_artifact_free(&artifact);
amduat_octets_free(&artifact_bytes);
memset(out_record, 0, sizeof(*out_record));
out_record->id.type = AMDUAT_FED_REC_ARTIFACT;
out_record->id.ref = ref;
out_record->logseq = logseq;
out_record->snapshot_id = 0u;
out_record->log_prefix = 0u;
out_record->meta.domain_id = domain_id;
out_record->meta.visibility = 1u;
out_record->meta.has_source = 0u;
out_record->meta.source_domain = 0u;
if (!amduat_octets_clone(amduat_octets(payload, strlen(payload)),
out_bytes)) {
amduat_reference_free(&ref);
return false;
}
return true;
}
static void amduatd_test_free_transport(amduatd_test_pull_transport_t *t) {
size_t i;
if (t == NULL) {
return;
}
if (t->records != NULL) {
for (i = 0; i < t->record_count; ++i) {
amduat_reference_free(&t->records[i].id.ref);
}
free(t->records);
}
if (t->artifact_bytes != NULL) {
for (i = 0; i < t->record_count; ++i) {
amduat_octets_free(&t->artifact_bytes[i]);
}
free(t->artifact_bytes);
}
memset(t, 0, sizeof(*t));
}
int main(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;
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_INDEX)) {
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);
fed_cfg.enabled = true;
{
amduatd_test_pull_transport_t t;
amduatd_fed_pull_transport_t transport;
amduat_fed_record_t *records = NULL;
amduat_octets_t *bytes = NULL;
amduatd_fed_pull_apply_report_t report;
amduatd_fed_pull_apply_status_t rc;
amduat_reference_t cursor_ref;
amduatd_fed_cursor_record_t cursor;
size_t i;
memset(&t, 0, sizeof(t));
records = (amduat_fed_record_t *)calloc(2u, sizeof(*records));
bytes = (amduat_octets_t *)calloc(2u, sizeof(*bytes));
if (records == NULL || bytes == NULL) {
fprintf(stderr, "failed to alloc records\n");
free(root);
return 1;
}
if (!amduatd_test_make_record(&store,
"hello",
1u,
1u,
&records[0],
&bytes[0]) ||
!amduatd_test_make_record(&store,
"world",
2u,
1u,
&records[1],
&bytes[1])) {
fprintf(stderr, "failed to make records\n");
free(root);
return 1;
}
t.records = records;
t.record_count = 2u;
t.artifact_bytes = bytes;
memset(&transport, 0, sizeof(transport));
transport.ctx = &t;
transport.get_records = amduatd_test_pull_get_records;
transport.free_records = amduatd_test_pull_free_records;
transport.get_artifact = amduatd_test_pull_get_artifact;
amduatd_fed_pull_apply_report_init(&report);
rc = amduatd_fed_pull_apply(&store,
&pointer_store,
&space,
"1",
NULL,
2u,
&fed_cfg,
&transport,
&report);
expect(rc == AMDUATD_FED_PULL_APPLY_OK, "apply success");
expect(report.applied_record_count == 2u, "applied record count");
expect(report.cursor_advanced, "cursor advanced");
for (i = 0; i < 2u; ++i) {
amduat_artifact_t artifact;
memset(&artifact, 0, sizeof(artifact));
expect(amduat_asl_store_get(&store,
records[i].id.ref,
&artifact) == AMDUAT_ASL_STORE_OK,
"artifact stored");
amduat_asl_artifact_free(&artifact);
}
amduatd_fed_cursor_record_init(&cursor);
memset(&cursor_ref, 0, sizeof(cursor_ref));
expect(amduatd_fed_cursor_get(&store,
&pointer_store,
&space,
"1",
&cursor,
&cursor_ref) == AMDUATD_FED_CURSOR_OK,
"cursor get after apply");
expect(cursor.has_logseq && cursor.last_logseq == 2u,
"cursor advanced logseq");
amduat_reference_free(&cursor_ref);
amduatd_fed_cursor_record_free(&cursor);
amduatd_fed_pull_apply_report_free(&report);
amduatd_test_free_transport(&t);
}
{
amduatd_test_pull_transport_t t;
amduatd_fed_pull_transport_t transport;
amduat_fed_record_t *records = NULL;
amduat_octets_t *bytes = NULL;
amduatd_fed_pull_apply_report_t report;
amduatd_fed_pull_apply_status_t rc;
amduatd_fed_cursor_record_t cursor;
amduat_reference_t cursor_ref;
uint64_t before_logseq = 0u;
bool before_has_logseq = false;
memset(&t, 0, sizeof(t));
records = (amduat_fed_record_t *)calloc(2u, sizeof(*records));
bytes = (amduat_octets_t *)calloc(2u, sizeof(*bytes));
if (records == NULL || bytes == NULL) {
fprintf(stderr, "failed to alloc records\n");
free(root);
return 1;
}
if (!amduatd_test_make_record(&store,
"alpha",
3u,
1u,
&records[0],
&bytes[0]) ||
!amduatd_test_make_record(&store,
"beta",
4u,
1u,
&records[1],
&bytes[1])) {
fprintf(stderr, "failed to make records\n");
free(root);
return 1;
}
t.records = records;
t.record_count = 2u;
t.artifact_bytes = bytes;
t.fail_artifact = true;
t.fail_index = 1u;
t.fail_status = 503;
memset(&transport, 0, sizeof(transport));
transport.ctx = &t;
transport.get_records = amduatd_test_pull_get_records;
transport.free_records = amduatd_test_pull_free_records;
transport.get_artifact = amduatd_test_pull_get_artifact;
amduatd_fed_cursor_record_init(&cursor);
memset(&cursor_ref, 0, sizeof(cursor_ref));
expect(amduatd_fed_cursor_get(&store,
&pointer_store,
&space,
"1",
&cursor,
&cursor_ref) == AMDUATD_FED_CURSOR_OK,
"cursor present before partial");
before_has_logseq = cursor.has_logseq;
before_logseq = cursor.last_logseq;
amduat_reference_free(&cursor_ref);
amduatd_fed_cursor_record_free(&cursor);
amduatd_fed_pull_apply_report_init(&report);
rc = amduatd_fed_pull_apply(&store,
&pointer_store,
&space,
"1",
NULL,
2u,
&fed_cfg,
&transport,
&report);
expect(rc == AMDUATD_FED_PULL_APPLY_ERR_STORE, "apply partial failure");
expect(report.applied_record_count == 1u, "partial applied count");
amduatd_fed_cursor_record_init(&cursor);
memset(&cursor_ref, 0, sizeof(cursor_ref));
expect(amduatd_fed_cursor_get(&store,
&pointer_store,
&space,
"1",
&cursor,
&cursor_ref) == AMDUATD_FED_CURSOR_OK,
"cursor present (from previous test)");
expect(cursor.has_logseq == before_has_logseq, "cursor unchanged flag");
expect(cursor.last_logseq == before_logseq, "cursor unchanged logseq");
amduat_reference_free(&cursor_ref);
amduatd_fed_cursor_record_free(&cursor);
amduatd_fed_pull_apply_report_free(&report);
amduatd_test_free_transport(&t);
}
{
amduatd_test_pull_transport_t t;
amduatd_fed_pull_transport_t transport;
amduat_fed_record_t *records = NULL;
amduat_octets_t *bytes = NULL;
amduatd_fed_pull_apply_report_t report;
amduatd_fed_pull_apply_status_t rc;
amduatd_fed_cursor_record_t cursor;
amduat_reference_t cursor_ref;
memset(&t, 0, sizeof(t));
records = (amduat_fed_record_t *)calloc(1u, sizeof(*records));
bytes = (amduat_octets_t *)calloc(1u, sizeof(*bytes));
if (records == NULL || bytes == NULL) {
fprintf(stderr, "failed to alloc records\n");
free(root);
return 1;
}
if (!amduatd_test_make_record(&store,
"gamma",
5u,
1u,
&records[0],
&bytes[0])) {
fprintf(stderr, "failed to make record\n");
free(root);
return 1;
}
t.records = records;
t.record_count = 1u;
t.artifact_bytes = bytes;
t.mutate_cursor = true;
t.store = &store;
t.pointer_store = &pointer_store;
t.space = &space;
t.peer_key = "1";
memset(&transport, 0, sizeof(transport));
transport.ctx = &t;
transport.get_records = amduatd_test_pull_get_records;
transport.free_records = amduatd_test_pull_free_records;
transport.get_artifact = amduatd_test_pull_get_artifact;
amduatd_fed_pull_apply_report_init(&report);
rc = amduatd_fed_pull_apply(&store,
&pointer_store,
&space,
"1",
NULL,
1u,
&fed_cfg,
&transport,
&report);
expect(rc == AMDUATD_FED_PULL_APPLY_ERR_CONFLICT, "cursor conflict");
amduatd_fed_cursor_record_init(&cursor);
memset(&cursor_ref, 0, sizeof(cursor_ref));
expect(amduatd_fed_cursor_get(&store,
&pointer_store,
&space,
"1",
&cursor,
&cursor_ref) == AMDUATD_FED_CURSOR_OK,
"cursor present after conflict");
expect(cursor.has_logseq && cursor.last_logseq == 99u,
"cursor unchanged on conflict");
amduat_reference_free(&cursor_ref);
amduatd_fed_cursor_record_free(&cursor);
amduatd_fed_pull_apply_report_free(&report);
amduatd_test_free_transport(&t);
}
free(root);
return failures == 0 ? 0 : 1;
}

View file

@ -0,0 +1,142 @@
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include "amduatd_fed_pull_plan.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 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;
}
int main(void) {
amduatd_fed_cfg_t cfg;
amduat_asl_store_t store;
amduatd_fed_pull_plan_status_t status;
amduatd_fed_cfg_init(&cfg);
memset(&store, 0, sizeof(store));
status = amduatd_fed_pull_plan_check(&cfg, &store);
expect(status == AMDUATD_FED_PULL_PLAN_ERR_DISABLED,
"disabled federation check");
cfg.enabled = true;
status = amduatd_fed_pull_plan_check(&cfg, &store);
expect(status == AMDUATD_FED_PULL_PLAN_ERR_UNSUPPORTED,
"unsupported backend check");
{
amduatd_fed_pull_plan_input_t input;
amduat_fed_record_t records[2];
amduat_reference_t ref0;
amduat_reference_t ref1;
char *json = NULL;
if (!amduatd_make_test_ref(0x01, &ref0) ||
!amduatd_make_test_ref(0x02, &ref1)) {
fprintf(stderr, "FAIL: make refs\n");
return 1;
}
memset(records, 0, sizeof(records));
records[0].id.type = AMDUAT_FED_REC_ARTIFACT;
records[0].id.ref = ref0;
records[0].logseq = 1u;
records[1].id.type = AMDUAT_FED_REC_PER;
records[1].id.ref = ref1;
records[1].logseq = 2u;
memset(&input, 0, sizeof(input));
input.peer_key = "1";
input.cursor_present = false;
input.records = records;
input.record_count = 2u;
status = amduatd_fed_pull_plan_json(&input, &json);
expect(status == AMDUATD_FED_PULL_PLAN_OK, "plan json missing cursor");
expect(json != NULL && strstr(json, "\"present\":false") != NULL,
"cursor present false");
expect(json != NULL && strstr(json, "\"record_count\":2") != NULL,
"record count");
expect(json != NULL && strstr(json, "\"last_logseq\":2") != NULL,
"next cursor candidate");
free(json);
amduat_reference_free(&ref0);
amduat_reference_free(&ref1);
}
{
amduatd_fed_pull_plan_input_t input;
amduatd_fed_cursor_record_t cursor;
amduat_reference_t cursor_ref;
amduat_reference_t record_ref;
char *json = NULL;
amduatd_fed_cursor_record_init(&cursor);
cursor.peer_key = strdup("7");
cursor.space_id = NULL;
if (cursor.peer_key == NULL) {
fprintf(stderr, "FAIL: cursor peer allocation\n");
return 1;
}
cursor.has_logseq = true;
cursor.last_logseq = 5u;
if (!amduatd_make_test_ref(0x03, &record_ref)) {
fprintf(stderr, "FAIL: make cursor ref\n");
return 1;
}
cursor.has_record_ref = true;
cursor.last_record_ref = record_ref;
if (!amduatd_make_test_ref(0x04, &cursor_ref)) {
fprintf(stderr, "FAIL: make cursor ref\n");
amduatd_fed_cursor_record_free(&cursor);
return 1;
}
memset(&input, 0, sizeof(input));
input.peer_key = "7";
input.cursor_present = true;
input.cursor = &cursor;
input.cursor_ref = &cursor_ref;
input.records = NULL;
input.record_count = 0u;
status = amduatd_fed_pull_plan_json(&input, &json);
expect(status == AMDUATD_FED_PULL_PLAN_OK, "plan json with cursor");
expect(json != NULL && strstr(json, "\"present\":true") != NULL,
"cursor present true");
expect(json != NULL && strstr(json, "\"last_logseq\":5") != NULL,
"cursor logseq echoed");
free(json);
amduat_reference_free(&cursor_ref);
amduatd_fed_cursor_record_free(&cursor);
}
return failures == 0 ? 0 : 1;
}

View file

@ -0,0 +1,515 @@
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include "amduatd_fed_until.h"
#include "amduatd_fed_cursor.h"
#include "amduatd_store.h"
#include "amduat/asl/artifact_io.h"
#include "amduat/asl/core.h"
#include "amduat/asl/asl_store_fs_meta.h"
#include "amduat/asl/ref_derive.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
amduat_fed_record_t *records;
size_t record_count;
amduat_octets_t *artifact_bytes;
} amduatd_test_pull_round_t;
typedef struct {
amduatd_test_pull_round_t *rounds;
size_t round_count;
size_t call_index;
size_t fail_round;
int fail_status;
} amduatd_test_pull_transport_t;
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-fed-pull-until-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_test_make_record(amduat_asl_store_t *store,
const char *payload,
uint64_t logseq,
amduat_fed_record_t *out_record,
amduat_octets_t *out_bytes) {
amduat_artifact_t artifact;
amduat_reference_t ref;
amduat_octets_t artifact_bytes = amduat_octets(NULL, 0u);
amduat_octets_t payload_bytes = amduat_octets(NULL, 0u);
if (store == NULL || payload == NULL || out_record == NULL ||
out_bytes == NULL) {
return false;
}
if (!amduat_octets_clone(amduat_octets(payload, strlen(payload)),
&payload_bytes)) {
return false;
}
if (!amduat_asl_artifact_from_bytes(payload_bytes,
AMDUAT_ASL_IO_RAW,
false,
amduat_type_tag(0u),
&artifact)) {
amduat_octets_free(&payload_bytes);
return false;
}
if (!amduat_asl_ref_derive(artifact,
store->config.encoding_profile_id,
store->config.hash_id,
&ref,
&artifact_bytes)) {
amduat_asl_artifact_free(&artifact);
return false;
}
amduat_asl_artifact_free(&artifact);
memset(out_record, 0, sizeof(*out_record));
out_record->logseq = logseq;
out_record->id.type = AMDUAT_FED_REC_ARTIFACT;
out_record->id.ref = ref;
*out_bytes = artifact_bytes;
return true;
}
static bool amduatd_test_pull_get_records(void *ctx,
uint32_t domain_id,
uint64_t from_logseq,
uint64_t limit,
int *out_status,
amduat_fed_record_t **out_records,
size_t *out_len,
char **out_body) {
amduatd_test_pull_transport_t *t = (amduatd_test_pull_transport_t *)ctx;
(void)domain_id;
(void)from_logseq;
(void)limit;
if (out_status == NULL || out_records == NULL || out_len == NULL) {
return false;
}
*out_status = 200;
*out_records = NULL;
*out_len = 0;
if (out_body != NULL) {
*out_body = NULL;
}
if (t == NULL) {
return false;
}
if (t->fail_round != 0u && t->call_index + 1u == t->fail_round) {
*out_status = t->fail_status;
if (out_body != NULL) {
*out_body = strdup("fail");
}
t->call_index++;
return true;
}
if (t->call_index >= t->round_count) {
t->call_index++;
return true;
}
*out_records = t->rounds[t->call_index].records;
*out_len = t->rounds[t->call_index].record_count;
t->call_index++;
return true;
}
static void amduatd_test_pull_free_records(void *ctx,
amduat_fed_record_t *records,
size_t len) {
(void)ctx;
(void)records;
(void)len;
}
static bool amduatd_test_pull_get_artifact(void *ctx,
amduat_reference_t ref,
int *out_status,
amduat_octets_t *out_bytes,
char **out_body) {
amduatd_test_pull_transport_t *t = (amduatd_test_pull_transport_t *)ctx;
size_t i;
if (out_status == NULL || out_bytes == NULL) {
return false;
}
*out_status = 404;
*out_bytes = amduat_octets(NULL, 0u);
if (out_body != NULL) {
*out_body = NULL;
}
if (t == NULL) {
return false;
}
for (i = 0; i < t->round_count; ++i) {
size_t j;
for (j = 0; j < t->rounds[i].record_count; ++j) {
if (!amduat_reference_eq(ref, t->rounds[i].records[j].id.ref)) {
continue;
}
if (!amduat_octets_clone(t->rounds[i].artifact_bytes[j], out_bytes)) {
return false;
}
*out_status = 200;
return true;
}
}
return true;
}
static void amduatd_test_pull_round_free(amduatd_test_pull_round_t *round) {
size_t i;
if (round == NULL) {
return;
}
for (i = 0; i < round->record_count; ++i) {
amduat_reference_free(&round->records[i].id.ref);
amduat_octets_free(&round->artifact_bytes[i]);
}
free(round->records);
free(round->artifact_bytes);
round->records = NULL;
round->artifact_bytes = NULL;
round->record_count = 0u;
}
static bool amduatd_test_pull_round_init(amduat_asl_store_t *store,
amduatd_test_pull_round_t *round,
const char **payloads,
size_t payloads_len,
uint64_t base_logseq) {
size_t i;
if (store == NULL || round == NULL) {
return false;
}
memset(round, 0, sizeof(*round));
if (payloads_len == 0u) {
return true;
}
round->records = (amduat_fed_record_t *)calloc(payloads_len,
sizeof(*round->records));
round->artifact_bytes = (amduat_octets_t *)calloc(payloads_len,
sizeof(*round->artifact_bytes));
if (round->records == NULL || round->artifact_bytes == NULL) {
amduatd_test_pull_round_free(round);
return false;
}
for (i = 0; i < payloads_len; ++i) {
if (!amduatd_test_make_record(store,
payloads[i],
base_logseq + i,
&round->records[i],
&round->artifact_bytes[i])) {
amduatd_test_pull_round_free(round);
return false;
}
}
round->record_count = payloads_len;
return true;
}
static int amduatd_test_pull_until_zero(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;
amduatd_test_pull_transport_t stub;
amduatd_fed_pull_transport_t transport;
amduatd_fed_until_report_t report;
amduatd_fed_pull_apply_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_INDEX)) {
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, "demo", false)) {
fprintf(stderr, "failed to init space\n");
free(root);
return 1;
}
amduatd_fed_cfg_init(&fed_cfg);
fed_cfg.enabled = true;
memset(&stub, 0, sizeof(stub));
memset(&transport, 0, sizeof(transport));
transport.ctx = &stub;
transport.get_records = amduatd_test_pull_get_records;
transport.free_records = amduatd_test_pull_free_records;
transport.get_artifact = amduatd_test_pull_get_artifact;
status = amduatd_fed_pull_until(&store,
&pointer_store,
&space,
"1",
NULL,
16u,
3u,
&fed_cfg,
&transport,
&report);
expect(status == AMDUATD_FED_PULL_APPLY_OK, "pull until zero ok");
expect(report.caught_up, "pull until caught up");
expect(report.rounds_executed == 1u, "pull until rounds executed");
expect(report.total_records == 0u, "pull until records");
expect(report.total_artifacts == 0u, "pull until artifacts");
amduatd_fed_until_report_free(&report);
free(root);
return failures == 0 ? 0 : 1;
}
static int amduatd_test_pull_until_multi(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;
amduatd_test_pull_round_t rounds[2];
amduatd_test_pull_transport_t stub;
amduatd_fed_pull_transport_t transport;
amduatd_fed_until_report_t report;
amduatd_fed_pull_apply_status_t status;
const char *round0_payloads[] = {"a", "b"};
const char *round1_payloads[] = {"c"};
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_INDEX)) {
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, "demo", false)) {
fprintf(stderr, "failed to init space\n");
free(root);
return 1;
}
amduatd_fed_cfg_init(&fed_cfg);
fed_cfg.enabled = true;
if (!amduatd_test_pull_round_init(&store,
&rounds[0],
round0_payloads,
2u,
0u) ||
!amduatd_test_pull_round_init(&store,
&rounds[1],
round1_payloads,
1u,
2u)) {
fprintf(stderr, "failed to init rounds\n");
free(root);
return 1;
}
memset(&stub, 0, sizeof(stub));
stub.rounds = rounds;
stub.round_count = 2u;
memset(&transport, 0, sizeof(transport));
transport.ctx = &stub;
transport.get_records = amduatd_test_pull_get_records;
transport.free_records = amduatd_test_pull_free_records;
transport.get_artifact = amduatd_test_pull_get_artifact;
status = amduatd_fed_pull_until(&store,
&pointer_store,
&space,
"1",
NULL,
2u,
5u,
&fed_cfg,
&transport,
&report);
expect(status == AMDUATD_FED_PULL_APPLY_OK, "pull until multi ok");
expect(report.caught_up, "pull until multi caught up");
expect(report.rounds_executed == 3u, "pull until multi rounds");
expect(report.total_records == 3u, "pull until multi records");
expect(report.total_artifacts == 3u, "pull until multi artifacts");
amduatd_fed_until_report_free(&report);
amduatd_test_pull_round_free(&rounds[0]);
amduatd_test_pull_round_free(&rounds[1]);
free(root);
return failures == 0 ? 0 : 1;
}
static int amduatd_test_pull_until_error(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;
amduatd_test_pull_round_t rounds[1];
amduatd_test_pull_transport_t stub;
amduatd_fed_pull_transport_t transport;
amduatd_fed_until_report_t report;
amduatd_fed_pull_apply_status_t status;
const char *round0_payloads[] = {"a"};
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_INDEX)) {
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, "demo", false)) {
fprintf(stderr, "failed to init space\n");
free(root);
return 1;
}
amduatd_fed_cfg_init(&fed_cfg);
fed_cfg.enabled = true;
if (!amduatd_test_pull_round_init(&store,
&rounds[0],
round0_payloads,
1u,
0u)) {
fprintf(stderr, "failed to init rounds\n");
free(root);
return 1;
}
memset(&stub, 0, sizeof(stub));
stub.rounds = rounds;
stub.round_count = 1u;
stub.fail_round = 2u;
stub.fail_status = 500;
memset(&transport, 0, sizeof(transport));
transport.ctx = &stub;
transport.get_records = amduatd_test_pull_get_records;
transport.free_records = amduatd_test_pull_free_records;
transport.get_artifact = amduatd_test_pull_get_artifact;
status = amduatd_fed_pull_until(&store,
&pointer_store,
&space,
"1",
NULL,
1u,
4u,
&fed_cfg,
&transport,
&report);
expect(status == AMDUATD_FED_PULL_APPLY_ERR_REMOTE, "pull until error");
expect(report.rounds_executed == 2u, "pull until error rounds");
expect(report.total_records == 1u, "pull until error records");
amduatd_fed_until_report_free(&report);
amduatd_test_pull_round_free(&rounds[0]);
free(root);
return failures == 0 ? 0 : 1;
}
int main(void) {
if (amduatd_test_pull_until_zero() != 0) {
return 1;
}
if (amduatd_test_pull_until_multi() != 0) {
return 1;
}
if (amduatd_test_pull_until_error() != 0) {
return 1;
}
return failures == 0 ? 0 : 1;
}

View file

@ -0,0 +1,278 @@
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include "amduatd_fed_push_apply.h"
#include "amduatd_fed_cursor.h"
#include "amduatd_space.h"
#include "amduatd_store.h"
#include "amduat/asl/asl_store_fs_meta.h"
#include "amduat/asl/artifact_io.h"
#include "amduat/asl/log_store.h"
#include "amduat/hash/asl1.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
size_t call_count;
size_t already_present_at;
} amduatd_test_push_transport_t;
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-fed-push-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_test_store_artifact(amduat_asl_store_t *store,
const char *payload,
amduat_reference_t *out_ref) {
amduat_artifact_t artifact;
amduat_octets_t payload_bytes = amduat_octets(NULL, 0u);
amduat_asl_index_state_t state;
amduat_asl_store_error_t err;
if (store == NULL || payload == NULL || out_ref == NULL) {
return false;
}
if (!amduat_octets_clone(amduat_octets(payload, strlen(payload)),
&payload_bytes)) {
return false;
}
if (!amduat_asl_artifact_from_bytes(payload_bytes,
AMDUAT_ASL_IO_RAW,
false,
amduat_type_tag(0u),
&artifact)) {
amduat_octets_free(&payload_bytes);
return false;
}
err = amduat_asl_store_put_indexed(store, artifact, out_ref, &state);
amduat_asl_artifact_free(&artifact);
return err == AMDUAT_ASL_STORE_OK;
}
static bool amduatd_test_append_fed_log(amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *space,
const char *root_path,
amduat_reference_t ref) {
amduat_asl_log_store_t log_store;
amduat_octets_t log_name = amduat_octets(NULL, 0u);
amduat_asl_log_entry_t entry;
uint64_t offset = 0u;
amduat_asl_store_error_t err;
if (!amduat_asl_log_store_init(&log_store,
root_path,
store,
pointer_store)) {
return false;
}
if (!amduatd_space_scope_name(space, "fed/records", &log_name)) {
return false;
}
memset(&entry, 0, sizeof(entry));
entry.kind = AMDUATD_FED_LOG_KIND_ARTIFACT;
entry.has_timestamp = false;
entry.timestamp = 0u;
entry.payload_ref = ref;
entry.has_actor = false;
entry.actor = amduat_octets(NULL, 0u);
err = amduat_asl_log_append(&log_store,
(const char *)log_name.data,
&entry,
1u,
&offset);
amduat_octets_free(&log_name);
return err == AMDUAT_ASL_STORE_OK;
}
static bool amduatd_test_push_post_ingest(void *ctx,
amduat_fed_record_type_t record_type,
amduat_reference_t ref,
amduat_octets_t bytes,
int *out_status,
char **out_body) {
amduatd_test_push_transport_t *t = (amduatd_test_push_transport_t *)ctx;
const char *status = "ok";
const char *applied = "true";
char buf[256];
int n;
(void)record_type;
(void)ref;
(void)bytes;
if (out_status == NULL || out_body == NULL || t == NULL) {
return false;
}
t->call_count++;
if (t->already_present_at != 0u && t->call_count == t->already_present_at) {
status = "already_present";
applied = "false";
}
n = snprintf(buf,
sizeof(buf),
"{\"status\":\"%s\",\"applied\":%s,"
"\"ref\":null,\"effective_space\":{"
"\"mode\":\"unscoped\",\"space_id\":null}}",
status,
applied);
if (n <= 0 || (size_t)n >= sizeof(buf)) {
return false;
}
*out_status = 200;
*out_body = strdup(buf);
return *out_body != NULL;
}
int main(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;
amduat_reference_t ref0;
amduat_reference_t ref1;
amduatd_fed_push_transport_t transport;
amduatd_test_push_transport_t stub;
amduatd_fed_push_apply_report_t report;
amduatd_fed_push_apply_status_t status;
amduatd_fed_cursor_record_t cursor;
amduat_reference_t cursor_ref;
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_INDEX)) {
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, "demo", false)) {
fprintf(stderr, "failed to init space\n");
free(root);
return 1;
}
if (!amduatd_test_store_artifact(&store, "alpha", &ref0) ||
!amduatd_test_store_artifact(&store, "beta", &ref1)) {
fprintf(stderr, "failed to store artifacts\n");
free(root);
return 1;
}
if (!amduatd_test_append_fed_log(&store,
&pointer_store,
&space,
root,
ref0) ||
!amduatd_test_append_fed_log(&store,
&pointer_store,
&space,
root,
ref1)) {
fprintf(stderr, "failed to append fed log\n");
amduat_reference_free(&ref0);
amduat_reference_free(&ref1);
free(root);
return 1;
}
amduatd_fed_cfg_init(&fed_cfg);
fed_cfg.enabled = true;
memset(&stub, 0, sizeof(stub));
stub.already_present_at = 1u;
memset(&transport, 0, sizeof(transport));
transport.ctx = &stub;
transport.post_ingest = amduatd_test_push_post_ingest;
status = amduatd_fed_push_apply(&store,
&pointer_store,
&space,
"2",
NULL,
16u,
root,
&fed_cfg,
&transport,
&report);
expect(status == AMDUATD_FED_PUSH_APPLY_OK, "push apply ok");
expect(report.sent_record_count == 2u, "sent record count");
expect(report.peer_ok_count == 1u, "peer ok count");
expect(report.peer_already_present_count == 1u, "peer already present");
expect(report.cursor_advanced, "cursor advanced");
expect(report.cursor_after_has_logseq &&
report.cursor_after_logseq == 1u,
"cursor after logseq");
amduatd_fed_cursor_record_init(&cursor);
memset(&cursor_ref, 0, sizeof(cursor_ref));
{
amduatd_fed_cursor_status_t st;
st = amduatd_fed_push_cursor_get(&store,
&pointer_store,
&space,
"2",
&cursor,
&cursor_ref);
expect(st == AMDUATD_FED_CURSOR_OK, "push cursor stored");
expect(cursor.has_logseq && cursor.last_logseq == 1u,
"push cursor logseq");
}
amduatd_fed_cursor_record_free(&cursor);
amduat_reference_free(&cursor_ref);
amduatd_fed_push_apply_report_free(&report);
amduat_reference_free(&ref0);
amduat_reference_free(&ref1);
free(root);
return failures == 0 ? 0 : 1;
}

View file

@ -0,0 +1,175 @@
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include "amduatd_fed_push_plan.h"
#include "amduatd_fed_cursor.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 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;
}
int main(void) {
amduatd_fed_cfg_t cfg;
amduat_asl_store_t store;
amduatd_fed_push_plan_status_t status;
amduatd_fed_cfg_init(&cfg);
memset(&store, 0, sizeof(store));
status = amduatd_fed_push_plan_check(&cfg, &store);
expect(status == AMDUATD_FED_PUSH_PLAN_ERR_DISABLED,
"disabled federation check");
cfg.enabled = true;
status = amduatd_fed_push_plan_check(&cfg, &store);
expect(status == AMDUATD_FED_PUSH_PLAN_ERR_UNSUPPORTED,
"unsupported backend check");
{
amduatd_fed_push_plan_input_t input;
amduat_fed_record_t records[2];
amduat_reference_t ref0;
amduat_reference_t ref1;
char *json = NULL;
char *ref0_hex = NULL;
if (!amduatd_make_test_ref(0x01, &ref0) ||
!amduatd_make_test_ref(0x02, &ref1)) {
fprintf(stderr, "FAIL: make refs\n");
return 1;
}
memset(records, 0, sizeof(records));
records[0].id.type = AMDUAT_FED_REC_ARTIFACT;
records[0].id.ref = ref0;
records[0].logseq = 1u;
records[1].id.type = AMDUAT_FED_REC_PER;
records[1].id.ref = ref1;
records[1].logseq = 2u;
memset(&input, 0, sizeof(input));
input.peer_key = "1";
input.domain_id = 42u;
input.cursor_present = false;
input.records = records;
input.record_count = 2u;
status = amduatd_fed_push_plan_json(&input, &json);
expect(status == AMDUATD_FED_PUSH_PLAN_OK, "plan json missing cursor");
expect(json != NULL && strstr(json, "\"present\":false") != NULL,
"cursor present false");
expect(json != NULL && strstr(json, "\"record_count\":2") != NULL,
"record count");
expect(json != NULL && strstr(json, "\"last_logseq\":2") != NULL,
"next cursor candidate");
expect(json != NULL && strstr(json, "\"domain_id\":42") != NULL,
"domain id");
expect(json != NULL && strstr(json, "\"record_type\":\"per\"") != NULL,
"record type per");
if (amduat_asl_ref_encode_hex(ref0, &ref0_hex)) {
expect(json != NULL && strstr(json, ref0_hex) != NULL,
"required artifacts include ref");
} else {
fprintf(stderr, "FAIL: encode ref\n");
failures++;
}
free(ref0_hex);
free(json);
amduat_reference_free(&ref0);
amduat_reference_free(&ref1);
}
{
amduatd_fed_push_plan_input_t input;
amduatd_fed_cursor_record_t cursor;
amduat_reference_t cursor_ref;
amduat_reference_t record_ref;
char *json = NULL;
char *cursor_ref_hex = NULL;
amduatd_fed_cursor_record_init(&cursor);
cursor.peer_key = strdup("7");
cursor.space_id = NULL;
if (cursor.peer_key == NULL) {
fprintf(stderr, "FAIL: cursor peer allocation\n");
return 1;
}
cursor.has_logseq = true;
cursor.last_logseq = 5u;
if (!amduatd_make_test_ref(0x03, &record_ref)) {
fprintf(stderr, "FAIL: make cursor record ref\n");
return 1;
}
cursor.has_record_ref = true;
cursor.last_record_ref = record_ref;
if (!amduatd_make_test_ref(0x04, &cursor_ref)) {
fprintf(stderr, "FAIL: make cursor ref\n");
amduatd_fed_cursor_record_free(&cursor);
return 1;
}
if (!amduat_asl_ref_encode_hex(cursor_ref, &cursor_ref_hex)) {
fprintf(stderr, "FAIL: encode cursor ref\n");
amduat_reference_free(&cursor_ref);
amduatd_fed_cursor_record_free(&cursor);
return 1;
}
memset(&input, 0, sizeof(input));
input.peer_key = "7";
input.domain_id = 7u;
input.cursor_present = true;
input.cursor = &cursor;
input.cursor_ref = &cursor_ref;
input.records = NULL;
input.record_count = 0u;
status = amduatd_fed_push_plan_json(&input, &json);
expect(status == AMDUATD_FED_PUSH_PLAN_OK, "plan json with cursor");
expect(json != NULL && strstr(json, "\"present\":true") != NULL,
"cursor present true");
expect(json != NULL && strstr(json, "\"last_logseq\":5") != NULL,
"cursor logseq echoed");
expect(json != NULL &&
strstr(json, "\"next_cursor_candidate\":{\"last_logseq\":null") !=
NULL,
"next cursor candidate empty");
expect(json != NULL && strstr(json, cursor_ref_hex) != NULL,
"cursor ref echoed");
free(cursor_ref_hex);
free(json);
amduat_reference_free(&cursor_ref);
amduatd_fed_cursor_record_free(&cursor);
}
return failures == 0 ? 0 : 1;
}

View file

@ -0,0 +1,444 @@
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include "amduatd_fed_until.h"
#include "amduatd_fed_cursor.h"
#include "amduatd_space.h"
#include "amduatd_store.h"
#include "amduat/asl/artifact_io.h"
#include "amduat/asl/asl_store_fs_meta.h"
#include "amduat/asl/log_store.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
size_t call_count;
size_t fail_at;
int fail_status;
} amduatd_test_push_transport_t;
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-fed-push-until-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_test_store_artifact(amduat_asl_store_t *store,
const char *payload,
amduat_reference_t *out_ref) {
amduat_artifact_t artifact;
amduat_octets_t payload_bytes = amduat_octets(NULL, 0u);
amduat_asl_index_state_t state;
amduat_asl_store_error_t err;
if (store == NULL || payload == NULL || out_ref == NULL) {
return false;
}
if (!amduat_octets_clone(amduat_octets(payload, strlen(payload)),
&payload_bytes)) {
return false;
}
if (!amduat_asl_artifact_from_bytes(payload_bytes,
AMDUAT_ASL_IO_RAW,
false,
amduat_type_tag(0u),
&artifact)) {
amduat_octets_free(&payload_bytes);
return false;
}
err = amduat_asl_store_put_indexed(store, artifact, out_ref, &state);
amduat_asl_artifact_free(&artifact);
return err == AMDUAT_ASL_STORE_OK;
}
static bool amduatd_test_append_fed_log(amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *space,
const char *root_path,
amduat_reference_t ref) {
amduat_asl_log_store_t log_store;
amduat_octets_t log_name = amduat_octets(NULL, 0u);
amduat_asl_log_entry_t entry;
uint64_t offset = 0u;
amduat_asl_store_error_t err;
if (!amduat_asl_log_store_init(&log_store,
root_path,
store,
pointer_store)) {
return false;
}
if (!amduatd_space_scope_name(space, "fed/records", &log_name)) {
return false;
}
memset(&entry, 0, sizeof(entry));
entry.kind = AMDUATD_FED_LOG_KIND_ARTIFACT;
entry.has_timestamp = false;
entry.timestamp = 0u;
entry.payload_ref = ref;
entry.has_actor = false;
entry.actor = amduat_octets(NULL, 0u);
err = amduat_asl_log_append(&log_store,
(const char *)log_name.data,
&entry,
1u,
&offset);
amduat_octets_free(&log_name);
return err == AMDUAT_ASL_STORE_OK;
}
static bool amduatd_test_push_post_ingest(void *ctx,
amduat_fed_record_type_t record_type,
amduat_reference_t ref,
amduat_octets_t bytes,
int *out_status,
char **out_body) {
amduatd_test_push_transport_t *t = (amduatd_test_push_transport_t *)ctx;
const char *status = "ok";
const char *applied = "true";
char buf[256];
int n;
(void)record_type;
(void)ref;
(void)bytes;
if (out_status == NULL || out_body == NULL || t == NULL) {
return false;
}
t->call_count++;
if (t->fail_at != 0u && t->call_count == t->fail_at) {
*out_status = t->fail_status;
*out_body = strdup("{\"status\":\"error\"}");
return *out_body != NULL;
}
n = snprintf(buf,
sizeof(buf),
"{\"status\":\"%s\",\"applied\":%s,"
"\"ref\":null,\"effective_space\":{"
"\"mode\":\"unscoped\",\"space_id\":null}}",
status,
applied);
if (n <= 0 || (size_t)n >= sizeof(buf)) {
return false;
}
*out_status = 200;
*out_body = strdup(buf);
return *out_body != NULL;
}
static int amduatd_test_push_until_zero(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;
amduatd_test_push_transport_t stub;
amduatd_fed_push_transport_t transport;
amduatd_fed_until_report_t report;
amduatd_fed_push_apply_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_INDEX)) {
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, "demo", false)) {
fprintf(stderr, "failed to init space\n");
free(root);
return 1;
}
amduatd_fed_cfg_init(&fed_cfg);
fed_cfg.enabled = true;
memset(&stub, 0, sizeof(stub));
memset(&transport, 0, sizeof(transport));
transport.ctx = &stub;
transport.post_ingest = amduatd_test_push_post_ingest;
status = amduatd_fed_push_until(&store,
&pointer_store,
&space,
"2",
NULL,
8u,
3u,
root,
&fed_cfg,
&transport,
&report);
expect(status == AMDUATD_FED_PUSH_APPLY_OK, "push until zero ok");
expect(report.caught_up, "push until caught up");
expect(report.rounds_executed == 1u, "push until rounds executed");
expect(report.total_records == 0u, "push until records");
expect(report.total_artifacts == 0u, "push until artifacts");
amduatd_fed_until_report_free(&report);
free(root);
return failures == 0 ? 0 : 1;
}
static int amduatd_test_push_until_multi(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;
amduat_reference_t ref0;
amduat_reference_t ref1;
amduatd_test_push_transport_t stub;
amduatd_fed_push_transport_t transport;
amduatd_fed_until_report_t report;
amduatd_fed_push_apply_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_INDEX)) {
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, "demo", false)) {
fprintf(stderr, "failed to init space\n");
free(root);
return 1;
}
amduatd_fed_cfg_init(&fed_cfg);
fed_cfg.enabled = true;
if (!amduatd_test_store_artifact(&store, "alpha", &ref0) ||
!amduatd_test_store_artifact(&store, "beta", &ref1)) {
fprintf(stderr, "failed to store artifacts\n");
free(root);
return 1;
}
if (!amduatd_test_append_fed_log(&store,
&pointer_store,
&space,
root,
ref0) ||
!amduatd_test_append_fed_log(&store,
&pointer_store,
&space,
root,
ref1)) {
fprintf(stderr, "failed to append fed log\n");
amduat_reference_free(&ref0);
amduat_reference_free(&ref1);
free(root);
return 1;
}
memset(&stub, 0, sizeof(stub));
memset(&transport, 0, sizeof(transport));
transport.ctx = &stub;
transport.post_ingest = amduatd_test_push_post_ingest;
status = amduatd_fed_push_until(&store,
&pointer_store,
&space,
"2",
NULL,
1u,
5u,
root,
&fed_cfg,
&transport,
&report);
expect(status == AMDUATD_FED_PUSH_APPLY_OK, "push until multi ok");
expect(report.caught_up, "push until multi caught up");
expect(report.rounds_executed == 3u, "push until multi rounds");
expect(report.total_records == 2u, "push until multi records");
expect(report.total_artifacts == 2u, "push until multi artifacts");
amduat_reference_free(&ref0);
amduat_reference_free(&ref1);
amduatd_fed_until_report_free(&report);
free(root);
return failures == 0 ? 0 : 1;
}
static int amduatd_test_push_until_error(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;
amduat_reference_t ref0;
amduat_reference_t ref1;
amduatd_test_push_transport_t stub;
amduatd_fed_push_transport_t transport;
amduatd_fed_until_report_t report;
amduatd_fed_push_apply_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_INDEX)) {
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, "demo", false)) {
fprintf(stderr, "failed to init space\n");
free(root);
return 1;
}
amduatd_fed_cfg_init(&fed_cfg);
fed_cfg.enabled = true;
if (!amduatd_test_store_artifact(&store, "alpha", &ref0) ||
!amduatd_test_store_artifact(&store, "beta", &ref1)) {
fprintf(stderr, "failed to store artifacts\n");
free(root);
return 1;
}
if (!amduatd_test_append_fed_log(&store,
&pointer_store,
&space,
root,
ref0) ||
!amduatd_test_append_fed_log(&store,
&pointer_store,
&space,
root,
ref1)) {
fprintf(stderr, "failed to append fed log\n");
amduat_reference_free(&ref0);
amduat_reference_free(&ref1);
free(root);
return 1;
}
memset(&stub, 0, sizeof(stub));
stub.fail_at = 2u;
stub.fail_status = 500;
memset(&transport, 0, sizeof(transport));
transport.ctx = &stub;
transport.post_ingest = amduatd_test_push_post_ingest;
status = amduatd_fed_push_until(&store,
&pointer_store,
&space,
"2",
NULL,
1u,
4u,
root,
&fed_cfg,
&transport,
&report);
expect(status == AMDUATD_FED_PUSH_APPLY_ERR_REMOTE, "push until error");
expect(report.rounds_executed == 2u, "push until error rounds");
expect(report.total_records == 1u, "push until error records");
amduat_reference_free(&ref0);
amduat_reference_free(&ref1);
amduatd_fed_until_report_free(&report);
free(root);
return failures == 0 ? 0 : 1;
}
int main(void) {
if (amduatd_test_push_until_zero() != 0) {
return 1;
}
if (amduatd_test_push_until_multi() != 0) {
return 1;
}
if (amduatd_test_push_until_error() != 0) {
return 1;
}
return failures == 0 ? 0 : 1;
}

View file

@ -0,0 +1,465 @@
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include "amduatd_space_doctor.h"
#include "amduat/asl/asl_store_fs_meta.h"
#include "amduat/asl/core.h"
#include "amduat/asl/none.h"
#include "amduat/asl/record.h"
#include "amduat/enc/asl1_core_codec.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static char *amduatd_test_make_temp_dir(void) {
char tmpl[] = "/tmp/amduatd-doctor-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_check_status(const amduatd_space_doctor_report_t *report,
const char *name,
amduatd_space_doctor_status_t expected) {
if (report == NULL || name == NULL) {
return false;
}
for (size_t i = 0u; i < report->checks_len; ++i) {
if (strcmp(report->checks[i].name, name) == 0) {
return report->checks[i].status == expected;
}
}
return false;
}
static bool amduatd_build_collection_head_name(const char *name,
char **out_name) {
size_t name_len;
size_t total_len;
char *buffer;
size_t offset = 0u;
if (name == NULL || out_name == NULL) {
return false;
}
if (!amduat_asl_pointer_name_is_valid(name)) {
return false;
}
name_len = strlen(name);
total_len = 11u + name_len + 5u + 1u;
buffer = (char *)malloc(total_len);
if (buffer == NULL) {
return false;
}
memcpy(buffer + offset, "collection/", 11u);
offset += 11u;
memcpy(buffer + offset, name, name_len);
offset += name_len;
memcpy(buffer + offset, "/head", 5u);
offset += 5u;
buffer[offset] = '\0';
*out_name = buffer;
return true;
}
static bool amduatd_build_collection_log_head_name(const char *name,
char **out_name) {
size_t name_len;
size_t log_name_len;
size_t total_len;
char *buffer;
size_t offset = 0u;
if (name == NULL || out_name == NULL) {
return false;
}
if (!amduat_asl_pointer_name_is_valid(name)) {
return false;
}
name_len = strlen(name);
log_name_len = 11u + name_len + 4u;
total_len = 4u + log_name_len + 5u + 1u;
buffer = (char *)malloc(total_len);
if (buffer == NULL) {
return false;
}
memcpy(buffer + offset, "log/", 4u);
offset += 4u;
memcpy(buffer + offset, "collection/", 11u);
offset += 11u;
memcpy(buffer + offset, name, name_len);
offset += name_len;
memcpy(buffer + offset, "/log", 4u);
offset += 4u;
memcpy(buffer + offset, "/head", 5u);
offset += 5u;
buffer[offset] = '\0';
*out_name = buffer;
return true;
}
static void amduatd_store_u32_le(uint8_t *out, uint32_t value) {
out[0] = (uint8_t)(value & 0xffu);
out[1] = (uint8_t)((value >> 8) & 0xffu);
out[2] = (uint8_t)((value >> 16) & 0xffu);
out[3] = (uint8_t)((value >> 24) & 0xffu);
}
static void amduatd_store_u64_le(uint8_t *out, uint64_t value) {
out[0] = (uint8_t)(value & 0xffu);
out[1] = (uint8_t)((value >> 8) & 0xffu);
out[2] = (uint8_t)((value >> 16) & 0xffu);
out[3] = (uint8_t)((value >> 24) & 0xffu);
out[4] = (uint8_t)((value >> 32) & 0xffu);
out[5] = (uint8_t)((value >> 40) & 0xffu);
out[6] = (uint8_t)((value >> 48) & 0xffu);
out[7] = (uint8_t)((value >> 56) & 0xffu);
}
static bool amduatd_build_edge_index_state_payload(
amduat_reference_t graph_ref,
amduat_octets_t *out_payload) {
static const uint8_t k_magic[8] = {
'A', 'S', 'L', 'E', 'I', 'X', '1', '\0'
};
amduat_octets_t ref_bytes = amduat_octets(NULL, 0u);
uint8_t *payload = NULL;
size_t payload_len = 0u;
size_t offset = 0u;
if (out_payload == NULL) {
return false;
}
*out_payload = amduat_octets(NULL, 0u);
if (!amduat_enc_asl1_core_encode_reference_v1(graph_ref, &ref_bytes)) {
return false;
}
if (ref_bytes.len > UINT32_MAX) {
free((void *)ref_bytes.data);
return false;
}
payload_len = 8u + 4u + 8u + 4u + ref_bytes.len;
payload = (uint8_t *)malloc(payload_len);
if (payload == NULL) {
free((void *)ref_bytes.data);
return false;
}
memcpy(payload + offset, k_magic, 8u);
offset += 8u;
amduatd_store_u32_le(payload + offset, 1u);
offset += 4u;
amduatd_store_u64_le(payload + offset, 0u);
offset += 8u;
amduatd_store_u32_le(payload + offset, (uint32_t)ref_bytes.len);
offset += 4u;
memcpy(payload + offset, ref_bytes.data, ref_bytes.len);
offset += ref_bytes.len;
free((void *)ref_bytes.data);
*out_payload = amduat_octets(payload, payload_len);
return offset == payload_len;
}
static int amduatd_test_empty_store(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_cfg_t dcfg;
amduatd_fed_cfg_t fed_cfg;
amduatd_space_doctor_report_t report;
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 fs backend\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, NULL, false)) {
fprintf(stderr, "failed to init space\n");
free(root);
return 1;
}
memset(&dcfg, 0, sizeof(dcfg));
dcfg.space = space;
memset(&fed_cfg, 0, sizeof(fed_cfg));
if (!amduatd_space_doctor_run(&store,
&pointer_store,
&dcfg.space,
&dcfg,
&fed_cfg,
&report)) {
fprintf(stderr, "doctor run failed\n");
free(root);
return 1;
}
if (report.backend != AMDUATD_STORE_BACKEND_FS) {
fprintf(stderr, "expected fs backend\n");
amduatd_space_doctor_report_free(&report);
free(root);
return 1;
}
if (!amduatd_check_status(&report,
"pointer_name_validation",
AMDUATD_DOCTOR_OK) ||
!amduatd_check_status(&report,
"edges_index_head",
AMDUATD_DOCTOR_WARN) ||
!amduatd_check_status(&report,
"edges_collection_snapshot_head",
AMDUATD_DOCTOR_WARN) ||
!amduatd_check_status(&report,
"edges_collection_log_head",
AMDUATD_DOCTOR_WARN) ||
!amduatd_check_status(&report,
"index_current_state",
AMDUATD_DOCTOR_SKIPPED) ||
!amduatd_check_status(&report,
"index_log_scan",
AMDUATD_DOCTOR_SKIPPED)) {
fprintf(stderr, "unexpected doctor status for empty store\n");
amduatd_space_doctor_report_free(&report);
free(root);
return 1;
}
amduatd_space_doctor_report_free(&report);
free(root);
return 0;
}
static int amduatd_test_minimal_fixture(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_cfg_t dcfg;
amduatd_fed_cfg_t fed_cfg;
amduatd_space_doctor_report_t report;
amduat_reference_t none_ref;
amduat_reference_t graph_ref;
amduat_reference_t edge_index_ref;
amduat_octets_t collection_name = amduat_octets(NULL, 0u);
amduat_octets_t index_head_name = amduat_octets(NULL, 0u);
char *collection_head = NULL;
char *collection_log_head = NULL;
amduat_octets_t payload = amduat_octets(NULL, 0u);
bool swapped = false;
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 fs backend\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, NULL, false)) {
fprintf(stderr, "failed to init space\n");
free(root);
return 1;
}
memset(&dcfg, 0, sizeof(dcfg));
dcfg.space = space;
memset(&fed_cfg, 0, sizeof(fed_cfg));
{
amduat_artifact_t none_artifact;
if (!amduat_asl_none_artifact(&none_artifact)) {
fprintf(stderr, "failed to build none artifact\n");
free(root);
return 1;
}
if (amduat_asl_store_put(&store, none_artifact, &none_ref) !=
AMDUAT_ASL_STORE_OK) {
fprintf(stderr, "failed to store none artifact\n");
amduat_artifact_free(&none_artifact);
free(root);
return 1;
}
amduat_artifact_free(&none_artifact);
}
{
amduat_artifact_t graph_artifact =
amduat_artifact(amduat_octets("graph", 5u));
if (amduat_asl_store_put(&store, graph_artifact, &graph_ref) !=
AMDUAT_ASL_STORE_OK) {
fprintf(stderr, "failed to store graph artifact\n");
free(root);
return 1;
}
}
if (!amduatd_build_edge_index_state_payload(graph_ref, &payload)) {
fprintf(stderr, "failed to encode edge index state\n");
free(root);
return 1;
}
if (amduat_asl_record_store_put(&store,
amduat_octets("tgk/edge_index_state", 20u),
payload,
&edge_index_ref) != AMDUAT_ASL_STORE_OK) {
fprintf(stderr, "failed to store edge index record\n");
free((void *)payload.data);
free(root);
return 1;
}
free((void *)payload.data);
if (!amduatd_space_edges_collection_name(&dcfg.space, &collection_name) ||
!amduatd_space_edges_index_head_name(&dcfg.space, &index_head_name)) {
fprintf(stderr, "failed to build scoped names\n");
free(root);
return 1;
}
if (!amduatd_build_collection_head_name((const char *)collection_name.data,
&collection_head) ||
!amduatd_build_collection_log_head_name(
(const char *)collection_name.data, &collection_log_head)) {
fprintf(stderr, "failed to build collection heads\n");
free(root);
return 1;
}
if (amduat_asl_pointer_cas(&pointer_store,
(const char *)index_head_name.data,
false,
NULL,
&edge_index_ref,
&swapped) != AMDUAT_ASL_POINTER_OK || !swapped ||
amduat_asl_pointer_cas(&pointer_store,
collection_head,
false,
NULL,
&none_ref,
&swapped) != AMDUAT_ASL_POINTER_OK || !swapped ||
amduat_asl_pointer_cas(&pointer_store,
collection_log_head,
false,
NULL,
&none_ref,
&swapped) != AMDUAT_ASL_POINTER_OK || !swapped) {
fprintf(stderr, "failed to seed pointers\n");
free(root);
return 1;
}
if (!amduatd_space_doctor_run(&store,
&pointer_store,
&dcfg.space,
&dcfg,
&fed_cfg,
&report)) {
fprintf(stderr, "doctor run failed\n");
free(root);
return 1;
}
if (!amduatd_check_status(&report,
"edges_index_head",
AMDUATD_DOCTOR_OK) ||
!amduatd_check_status(&report,
"edges_collection_snapshot_head",
AMDUATD_DOCTOR_OK) ||
!amduatd_check_status(&report,
"edges_collection_log_head",
AMDUATD_DOCTOR_OK) ||
!amduatd_check_status(&report,
"edge_index_state_parse",
AMDUATD_DOCTOR_OK) ||
!amduatd_check_status(&report,
"edge_index_graph_ref",
AMDUATD_DOCTOR_OK) ||
!amduatd_check_status(&report,
"index_current_state",
AMDUATD_DOCTOR_SKIPPED)) {
fprintf(stderr, "unexpected doctor status for fixture\n");
amduatd_space_doctor_report_free(&report);
free(root);
return 1;
}
amduatd_space_doctor_report_free(&report);
free((void *)collection_name.data);
free((void *)index_head_name.data);
free(collection_head);
free(collection_log_head);
free(root);
return 0;
}
int main(void) {
if (amduatd_test_empty_store() != 0) {
return 1;
}
if (amduatd_test_minimal_fixture() != 0) {
return 1;
}
return 0;
}

View file

@ -0,0 +1,512 @@
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include "amduatd_space_manifest.h"
#include "amduatd_space.h"
#include "amduatd_store.h"
#include "amduat/asl/asl_store_fs_meta.h"
#include "amduat/asl/asl_pointer_fs.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-manifest-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_manifest_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_space_manifest_t manifest;
amduat_reference_t ref;
amduatd_space_manifest_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;
}
memset(&manifest, 0, sizeof(manifest));
memset(&ref, 0, sizeof(ref));
status = amduatd_space_manifest_get(&store,
&pointer_store,
&space,
&ref,
&manifest);
expect(status == AMDUATD_SPACE_MANIFEST_ERR_NOT_FOUND,
"missing manifest returns not found");
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&ref);
free(root);
return failures == 0 ? 0 : 1;
}
static int amduatd_test_manifest_decode(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_space_manifest_t manifest;
amduat_reference_t fetched_ref;
amduatd_space_manifest_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;
}
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);
memset(&manifest, 0, sizeof(manifest));
memset(&fetched_ref, 0, sizeof(fetched_ref));
status = amduatd_space_manifest_get(&store,
&pointer_store,
&space,
&fetched_ref,
&manifest);
expect(status == AMDUATD_SPACE_MANIFEST_OK, "manifest get ok");
expect(amduat_reference_eq(fetched_ref, record_ref),
"manifest ref matches pointer");
expect(manifest.version == 1u, "manifest version ok");
expect(manifest.mounts_len == 3u, "manifest mounts count");
if (manifest.mounts_len == 3u) {
expect(strcmp(manifest.mounts[0].name, "alpha") == 0,
"mount 0 name");
expect(strcmp(manifest.mounts[0].peer_key, "peer-1") == 0,
"mount 0 peer");
expect(strcmp(manifest.mounts[0].space_id, "beta") == 0,
"mount 0 space");
expect(manifest.mounts[0].mode == AMDUATD_SPACE_MANIFEST_MOUNT_TRACK,
"mount 0 mode");
expect(strcmp(manifest.mounts[1].name, "alpha") == 0,
"mount 1 name");
expect(strcmp(manifest.mounts[1].peer_key, "peer-1") == 0,
"mount 1 peer");
expect(strcmp(manifest.mounts[1].space_id, "zeta") == 0,
"mount 1 space");
expect(manifest.mounts[1].mode == AMDUATD_SPACE_MANIFEST_MOUNT_PINNED,
"mount 1 mode");
expect(manifest.mounts[1].has_pinned_root_ref,
"mount 1 pinned ref present");
expect(amduat_reference_eq(manifest.mounts[1].pinned_root_ref, pinned_ref),
"mount 1 pinned ref");
expect(strcmp(manifest.mounts[2].name, "beta") == 0,
"mount 2 name");
expect(strcmp(manifest.mounts[2].peer_key, "peer-2") == 0,
"mount 2 peer");
expect(strcmp(manifest.mounts[2].space_id, "zeta") == 0,
"mount 2 space");
expect(manifest.mounts[2].mode == AMDUATD_SPACE_MANIFEST_MOUNT_TRACK,
"mount 2 mode");
}
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&fetched_ref);
amduat_reference_free(&record_ref);
amduat_reference_free(&pinned_ref);
free(root);
return failures == 0 ? 0 : 1;
}
static int amduatd_test_manifest_put(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];
amduatd_space_manifest_t manifest;
amduat_reference_t first_ref;
amduat_reference_t second_ref;
amduat_reference_t wrong_ref;
amduatd_space_manifest_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;
}
if (!amduatd_make_test_ref(0x22, &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);
memset(&manifest, 0, sizeof(manifest));
memset(&first_ref, 0, sizeof(first_ref));
status = amduatd_space_manifest_put(&store,
&pointer_store,
&space,
amduat_octets((const uint8_t *)payload,
strlen(payload)),
NULL,
&first_ref,
&manifest);
expect(status == AMDUATD_SPACE_MANIFEST_OK, "manifest put ok");
if (status == AMDUATD_SPACE_MANIFEST_OK) {
expect(manifest.mounts_len == 3u, "manifest put mounts count");
if (manifest.mounts_len == 3u) {
expect(strcmp(manifest.mounts[0].name, "alpha") == 0,
"put mount 0 name");
expect(strcmp(manifest.mounts[0].peer_key, "peer-1") == 0,
"put mount 0 peer");
expect(strcmp(manifest.mounts[0].space_id, "beta") == 0,
"put mount 0 space");
expect(manifest.mounts[0].mode == AMDUATD_SPACE_MANIFEST_MOUNT_TRACK,
"put mount 0 mode");
expect(strcmp(manifest.mounts[1].name, "alpha") == 0,
"put mount 1 name");
expect(strcmp(manifest.mounts[1].peer_key, "peer-1") == 0,
"put mount 1 peer");
expect(strcmp(manifest.mounts[1].space_id, "zeta") == 0,
"put mount 1 space");
expect(manifest.mounts[1].mode == AMDUATD_SPACE_MANIFEST_MOUNT_PINNED,
"put mount 1 mode");
expect(manifest.mounts[1].has_pinned_root_ref,
"put mount 1 pinned ref");
expect(strcmp(manifest.mounts[2].name, "beta") == 0,
"put mount 2 name");
expect(strcmp(manifest.mounts[2].peer_key, "peer-2") == 0,
"put mount 2 peer");
expect(strcmp(manifest.mounts[2].space_id, "zeta") == 0,
"put mount 2 space");
expect(manifest.mounts[2].mode == AMDUATD_SPACE_MANIFEST_MOUNT_TRACK,
"put mount 2 mode");
}
}
amduatd_space_manifest_free(&manifest);
memset(&manifest, 0, sizeof(manifest));
memset(&second_ref, 0, sizeof(second_ref));
status = amduatd_space_manifest_get(&store,
&pointer_store,
&space,
&second_ref,
&manifest);
expect(status == AMDUATD_SPACE_MANIFEST_OK, "manifest get after put ok");
expect(amduat_reference_eq(second_ref, first_ref),
"manifest get ref matches put");
if (manifest.mounts_len == 3u) {
expect(strcmp(manifest.mounts[0].name, "alpha") == 0,
"get mount 0 name");
expect(strcmp(manifest.mounts[1].name, "alpha") == 0,
"get mount 1 name");
expect(strcmp(manifest.mounts[2].name, "beta") == 0,
"get mount 2 name");
}
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&second_ref);
memset(&manifest, 0, sizeof(manifest));
memset(&second_ref, 0, sizeof(second_ref));
status = amduatd_space_manifest_put(&store,
&pointer_store,
&space,
amduat_octets((const uint8_t *)payload,
strlen(payload)),
NULL,
&second_ref,
&manifest);
expect(status == AMDUATD_SPACE_MANIFEST_ERR_CONFLICT,
"manifest put conflict without If-Match");
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&second_ref);
memset(&manifest, 0, sizeof(manifest));
memset(&second_ref, 0, sizeof(second_ref));
status = amduatd_space_manifest_put(&store,
&pointer_store,
&space,
amduat_octets((const uint8_t *)payload,
strlen(payload)),
&first_ref,
&second_ref,
&manifest);
expect(status == AMDUATD_SPACE_MANIFEST_OK,
"manifest put ok with If-Match");
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&second_ref);
if (!amduatd_make_test_ref(0x99, &wrong_ref)) {
fprintf(stderr, "failed to make wrong ref\n");
amduat_reference_free(&first_ref);
amduat_reference_free(&pinned_ref);
free(root);
return 1;
}
memset(&manifest, 0, sizeof(manifest));
status = amduatd_space_manifest_put(&store,
&pointer_store,
&space,
amduat_octets((const uint8_t *)payload,
strlen(payload)),
&wrong_ref,
&second_ref,
&manifest);
expect(status == AMDUATD_SPACE_MANIFEST_ERR_CONFLICT,
"manifest put conflict with wrong If-Match");
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&wrong_ref);
amduat_reference_free(&first_ref);
amduat_reference_free(&pinned_ref);
free(root);
return failures == 0 ? 0 : 1;
}
int main(void) {
if (amduatd_test_manifest_missing() != 0) {
return 1;
}
if (amduatd_test_manifest_decode() != 0) {
return 1;
}
if (amduatd_test_manifest_put() != 0) {
return 1;
}
return failures == 0 ? 0 : 1;
}

View file

@ -0,0 +1,360 @@
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include "amduatd_space_mounts.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-mounts-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_mounts_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;
amduat_reference_t manifest_ref;
char *mounts_json = NULL;
size_t mounts_len = 0u;
amduatd_space_mounts_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;
}
memset(&manifest_ref, 0, sizeof(manifest_ref));
status = amduatd_space_mounts_resolve(&store,
&pointer_store,
&space,
&manifest_ref,
&mounts_json,
&mounts_len);
expect(status == AMDUATD_SPACE_MOUNTS_ERR_NOT_FOUND,
"missing manifest returns not found");
expect(mounts_json == NULL, "mounts json unset on missing");
amduat_reference_free(&manifest_ref);
free(root);
return failures == 0 ? 0 : 1;
}
static int amduatd_test_mounts_resolve(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;
amduat_reference_t manifest_ref;
char *mounts_json = NULL;
size_t mounts_len = 0u;
amduatd_space_mounts_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);
memset(&manifest_ref, 0, sizeof(manifest_ref));
status = amduatd_space_mounts_resolve(&store,
&pointer_store,
&space,
&manifest_ref,
&mounts_json,
&mounts_len);
expect(status == AMDUATD_SPACE_MOUNTS_OK, "mounts resolve ok");
expect(amduat_reference_eq(manifest_ref, record_ref),
"resolve manifest ref matches pointer");
expect(mounts_json != NULL && mounts_len != 0u,
"resolve mounts json populated");
if (mounts_json != NULL) {
m0 = strstr(mounts_json,
"\"name\":\"alpha\",\"peer_key\":\"peer-1\",\"space_id\":\"beta\"");
m1 = strstr(mounts_json,
"\"name\":\"alpha\",\"peer_key\":\"peer-1\",\"space_id\":\"zeta\"");
m2 = strstr(mounts_json,
"\"name\":\"beta\",\"peer_key\":\"peer-2\",\"space_id\":\"zeta\"");
expect(m0 != NULL && m1 != NULL && m2 != NULL,
"mounts include all entries");
if (m0 != NULL && m1 != NULL && m2 != NULL) {
expect(m0 < m1 && m1 < m2, "mounts are in canonical order");
}
expect(strstr(mounts_json, "\"mode\":\"pinned\"") != NULL,
"pinned mode present");
expect(strstr(mounts_json, "\"pinned_root_ref\":\"") != NULL,
"pinned root ref present");
expect(strstr(mounts_json, "\"cursor_namespace\":\"v2\"") != NULL,
"cursor namespace present");
expect(strstr(mounts_json, "\"cursor_scope\":\"per-peer-per-local-space\"") != NULL,
"cursor scope present");
expect(strstr(mounts_json, "\"remote_space_id\":\"beta\"") != NULL,
"remote space id present");
expect(strstr(mounts_json, "\"pull_cursor\":{\"present\":true") != NULL,
"cursor present true");
expect(strstr(mounts_json, "\"last_logseq\":42") != NULL,
"cursor last_logseq present");
expect(strstr(mounts_json, cursor_last_hex) != NULL,
"cursor ref present");
expect(strstr(mounts_json, "\"pull_cursor\":{\"present\":false") != NULL,
"cursor present false");
}
free(mounts_json);
amduat_reference_free(&manifest_ref);
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_mounts_missing() != 0) {
return 1;
}
if (amduatd_test_mounts_resolve() != 0) {
return 1;
}
return failures == 0 ? 0 : 1;
}

View file

@ -0,0 +1,582 @@
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include "amduatd_space_mounts_sync.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/ref_text.h"
#include "amduat/hash/asl1.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
size_t calls;
} amduatd_test_sync_transport_t;
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-mounts-sync-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 bool amduatd_test_get_records(void *ctx,
uint32_t domain_id,
uint64_t from_logseq,
uint64_t limit,
int *out_status,
amduat_fed_record_t **out_records,
size_t *out_len,
char **out_body) {
amduatd_test_sync_transport_t *t = (amduatd_test_sync_transport_t *)ctx;
(void)domain_id;
(void)from_logseq;
(void)limit;
if (out_status == NULL || out_records == NULL || out_len == NULL) {
return false;
}
*out_status = 200;
*out_records = NULL;
*out_len = 0u;
if (out_body != NULL) {
*out_body = NULL;
}
if (t != NULL) {
t->calls++;
}
return true;
}
static void amduatd_test_free_records(void *ctx,
amduat_fed_record_t *records,
size_t len) {
(void)ctx;
(void)records;
(void)len;
}
static bool amduatd_test_get_artifact(void *ctx,
amduat_reference_t ref,
int *out_status,
amduat_octets_t *out_bytes,
char **out_body) {
(void)ctx;
(void)ref;
if (out_status == NULL || out_bytes == NULL) {
return false;
}
*out_status = 404;
*out_bytes = amduat_octets(NULL, 0u);
if (out_body != NULL) {
*out_body = NULL;
}
return true;
}
static bool amduatd_test_seed_manifest(amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *space,
const char *json) {
amduat_reference_t ref;
amduatd_space_manifest_t manifest;
amduat_octets_t payload = amduat_octets(json, strlen(json));
amduatd_space_manifest_status_t status;
memset(&ref, 0, sizeof(ref));
memset(&manifest, 0, sizeof(manifest));
status = amduatd_space_manifest_put(store,
pointer_store,
space,
payload,
NULL,
&ref,
&manifest);
if (status != AMDUATD_SPACE_MANIFEST_OK) {
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&ref);
return false;
}
amduatd_space_manifest_free(&manifest);
amduat_reference_free(&ref);
return true;
}
static bool amduatd_test_seed_cursor(amduat_asl_store_t *store,
amduat_asl_pointer_store_t *pointer_store,
const amduatd_space_t *space,
const char *peer_key,
const char *remote_space_id,
uint64_t logseq,
uint8_t fill) {
amduatd_fed_cursor_record_t cursor;
amduat_reference_t ref;
amduatd_fed_cursor_status_t status;
const char *space_id = NULL;
bool ok = false;
amduatd_fed_cursor_record_init(&cursor);
cursor.peer_key = strdup(peer_key);
if (space != NULL && space->enabled && space->space_id.data != NULL) {
space_id = (const char *)space->space_id.data;
}
cursor.space_id = space_id != NULL ? strdup(space_id) : NULL;
if (cursor.peer_key == NULL || cursor.space_id == NULL) {
amduatd_fed_cursor_record_free(&cursor);
return false;
}
cursor.has_logseq = true;
cursor.last_logseq = logseq;
if (!amduatd_make_test_ref(fill, &ref)) {
amduatd_fed_cursor_record_free(&cursor);
return false;
}
cursor.has_record_ref = true;
cursor.last_record_ref = ref;
status = amduatd_fed_cursor_cas_set_remote(store,
pointer_store,
space,
peer_key,
remote_space_id,
NULL,
&cursor,
NULL);
ok = (status == AMDUATD_FED_CURSOR_OK);
amduatd_fed_cursor_record_free(&cursor);
return ok;
}
static int amduatd_test_sync_missing_manifest(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;
amduatd_test_sync_transport_t transport_state;
amduatd_fed_pull_transport_t transport;
amduatd_space_mounts_sync_report_t report;
amduatd_space_mounts_sync_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_INDEX)) {
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);
fed_cfg.enabled = true;
memset(&transport_state, 0, sizeof(transport_state));
memset(&transport, 0, sizeof(transport));
transport.ctx = &transport_state;
transport.get_records = amduatd_test_get_records;
transport.free_records = amduatd_test_free_records;
transport.get_artifact = amduatd_test_get_artifact;
status = amduatd_space_mounts_sync_until(&store,
&pointer_store,
&space,
&fed_cfg,
&transport,
128u,
10u,
32u,
&report);
expect(status == AMDUATD_SPACE_MOUNTS_SYNC_ERR_NOT_FOUND,
"missing manifest");
free(root);
return failures == 0 ? 0 : 1;
}
static int amduatd_test_sync_no_track_mounts(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;
amduatd_test_sync_transport_t transport_state;
amduatd_fed_pull_transport_t transport;
amduat_reference_t pinned_ref;
char *pinned_hex = NULL;
char json[512];
int n;
amduatd_space_mounts_sync_report_t report;
amduatd_space_mounts_sync_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_INDEX)) {
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);
fed_cfg.enabled = true;
if (!amduatd_make_test_ref(0x11, &pinned_ref) ||
!amduat_asl_ref_encode_hex(pinned_ref, &pinned_hex)) {
fprintf(stderr, "failed to build pinned ref\n");
amduat_reference_free(&pinned_ref);
free(root);
return 1;
}
n = snprintf(json, sizeof(json),
"{\"version\":1,\"mounts\":[{\"name\":\"p1\","
"\"peer_key\":\"1\",\"space_id\":\"beta\","
"\"mode\":\"pinned\",\"pinned_root_ref\":\"%s\"}]}",
pinned_hex);
free(pinned_hex);
amduat_reference_free(&pinned_ref);
if (n <= 0 || (size_t)n >= sizeof(json)) {
fprintf(stderr, "failed to format manifest json\n");
free(root);
return 1;
}
if (!amduatd_test_seed_manifest(&store, &pointer_store, &space, json)) {
fprintf(stderr, "failed to seed manifest\n");
free(root);
return 1;
}
memset(&transport_state, 0, sizeof(transport_state));
memset(&transport, 0, sizeof(transport));
transport.ctx = &transport_state;
transport.get_records = amduatd_test_get_records;
transport.free_records = amduatd_test_free_records;
transport.get_artifact = amduatd_test_get_artifact;
status = amduatd_space_mounts_sync_until(&store,
&pointer_store,
&space,
&fed_cfg,
&transport,
128u,
10u,
32u,
&report);
expect(status == AMDUATD_SPACE_MOUNTS_SYNC_OK, "sync no-track ok");
expect(report.mounts_total == 0u, "no-track total");
expect(report.mounts_synced == 0u, "no-track synced");
expect(report.ok, "no-track ok flag");
expect(report.results_json != NULL &&
strcmp(report.results_json, "[]") == 0,
"no-track results empty");
amduatd_space_mounts_sync_report_free(&report);
free(root);
return failures == 0 ? 0 : 1;
}
static int amduatd_test_sync_two_mounts(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;
amduatd_test_sync_transport_t transport_state;
amduatd_fed_pull_transport_t transport;
const char *json =
"{\"version\":1,\"mounts\":["
"{\"name\":\"b\",\"peer_key\":\"2\",\"space_id\":\"beta\",\"mode\":\"track\"},"
"{\"name\":\"a\",\"peer_key\":\"1\",\"space_id\":\"alpha\",\"mode\":\"track\"}"
"]}";
amduatd_space_mounts_sync_report_t report;
amduatd_space_mounts_sync_status_t status;
const char *first_a = NULL;
const char *second_b = 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_INDEX)) {
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);
fed_cfg.enabled = true;
if (!amduatd_test_seed_manifest(&store, &pointer_store, &space, json)) {
fprintf(stderr, "failed to seed manifest\n");
free(root);
return 1;
}
if (!amduatd_test_seed_cursor(&store,
&pointer_store,
&space,
"1",
"alpha",
5u,
0x22) ||
!amduatd_test_seed_cursor(&store,
&pointer_store,
&space,
"2",
"beta",
7u,
0x33)) {
fprintf(stderr, "failed to seed cursors\n");
free(root);
return 1;
}
memset(&transport_state, 0, sizeof(transport_state));
memset(&transport, 0, sizeof(transport));
transport.ctx = &transport_state;
transport.get_records = amduatd_test_get_records;
transport.free_records = amduatd_test_free_records;
transport.get_artifact = amduatd_test_get_artifact;
status = amduatd_space_mounts_sync_until(&store,
&pointer_store,
&space,
&fed_cfg,
&transport,
128u,
1u,
32u,
&report);
expect(status == AMDUATD_SPACE_MOUNTS_SYNC_OK, "sync two ok");
expect(report.mounts_total == 2u, "sync two total");
expect(report.mounts_synced == 2u, "sync two synced");
expect(report.ok, "sync two ok flag");
expect(transport_state.calls == 2u, "sync two transport calls");
expect(report.results_json != NULL, "sync two results present");
if (report.results_json != NULL) {
first_a = strstr(report.results_json, "\"name\":\"a\"");
second_b = strstr(report.results_json, "\"name\":\"b\"");
}
expect(first_a != NULL && second_b != NULL && first_a < second_b,
"sync results canonical order");
expect(strstr(report.results_json, "\"last_logseq\":5") != NULL,
"sync cursor logseq a");
expect(strstr(report.results_json, "\"last_logseq\":7") != NULL,
"sync cursor logseq b");
amduatd_space_mounts_sync_report_free(&report);
free(root);
return failures == 0 ? 0 : 1;
}
static int amduatd_test_sync_partial_error(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;
amduatd_test_sync_transport_t transport_state;
amduatd_fed_pull_transport_t transport;
const char *json =
"{\"version\":1,\"mounts\":["
"{\"name\":\"a\",\"peer_key\":\"peer-a\",\"space_id\":\"alpha\","
"\"mode\":\"track\"},"
"{\"name\":\"b\",\"peer_key\":\"2\",\"space_id\":\"beta\",\"mode\":\"track\"}"
"]}";
amduatd_space_mounts_sync_report_t report;
amduatd_space_mounts_sync_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_INDEX)) {
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);
fed_cfg.enabled = true;
if (!amduatd_test_seed_manifest(&store, &pointer_store, &space, json)) {
fprintf(stderr, "failed to seed manifest\n");
free(root);
return 1;
}
memset(&transport_state, 0, sizeof(transport_state));
memset(&transport, 0, sizeof(transport));
transport.ctx = &transport_state;
transport.get_records = amduatd_test_get_records;
transport.free_records = amduatd_test_free_records;
transport.get_artifact = amduatd_test_get_artifact;
status = amduatd_space_mounts_sync_until(&store,
&pointer_store,
&space,
&fed_cfg,
&transport,
128u,
1u,
32u,
&report);
expect(status == AMDUATD_SPACE_MOUNTS_SYNC_OK, "sync partial ok");
expect(!report.ok, "sync partial ok flag false");
expect(report.mounts_total == 2u, "sync partial total");
expect(report.mounts_synced == 2u, "sync partial synced");
expect(transport_state.calls == 1u, "sync partial transport calls");
expect(report.results_json != NULL &&
strstr(report.results_json, "\"code\":\"invalid_peer\"") != NULL,
"sync partial invalid peer");
amduatd_space_mounts_sync_report_free(&report);
free(root);
return failures == 0 ? 0 : 1;
}
int main(void) {
if (amduatd_test_sync_missing_manifest() != 0) {
return 1;
}
if (amduatd_test_sync_no_track_mounts() != 0) {
return 1;
}
if (amduatd_test_sync_two_mounts() != 0) {
return 1;
}
if (amduatd_test_sync_partial_error() != 0) {
return 1;
}
return failures == 0 ? 0 : 1;
}

View file

@ -0,0 +1,83 @@
#include "amduatd_space.h"
#include <stdbool.h>
#include <stdio.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 void expect_space_id(const amduatd_space_t *sp,
const char *expected,
const char *msg) {
if (expected == NULL) {
expect(sp == NULL || sp->space_id.data == NULL, msg);
return;
}
expect(sp != NULL && sp->space_id.data != NULL &&
strcmp((const char *)sp->space_id.data, expected) == 0,
msg);
}
int main(void) {
amduatd_space_t default_space;
amduatd_space_t resolved_space;
const amduatd_space_t *effective = NULL;
amduatd_space_resolve_status_t st;
if (!amduatd_space_init(&default_space, "alpha", false)) {
fprintf(stderr, "FAIL: default space init\n");
return 1;
}
st = amduatd_space_resolve_effective(&default_space,
NULL,
&resolved_space,
&effective);
expect(st == AMDUATD_SPACE_RESOLVE_OK, "resolve default without header");
expect(effective == &default_space, "uses default space");
expect(effective != NULL && effective->enabled, "default enabled");
expect_space_id(effective, "alpha", "default id");
st = amduatd_space_resolve_effective(&default_space,
"beta",
&resolved_space,
&effective);
expect(st == AMDUATD_SPACE_RESOLVE_OK, "resolve header override");
expect(effective == &resolved_space, "header overrides default");
expect(effective != NULL && effective->enabled, "header enabled");
expect_space_id(effective, "beta", "header id");
st = amduatd_space_resolve_effective(&default_space,
"",
&resolved_space,
&effective);
expect(st == AMDUATD_SPACE_RESOLVE_ERR_INVALID, "reject empty header");
st = amduatd_space_resolve_effective(&default_space,
"bad/space",
&resolved_space,
&effective);
expect(st == AMDUATD_SPACE_RESOLVE_ERR_INVALID, "reject invalid header");
if (!amduatd_space_init(&default_space, NULL, false)) {
fprintf(stderr, "FAIL: unscoped default init\n");
return 1;
}
st = amduatd_space_resolve_effective(&default_space,
NULL,
&resolved_space,
&effective);
expect(st == AMDUATD_SPACE_RESOLVE_OK, "resolve unscoped default");
expect(effective == &default_space, "unscoped uses default");
expect(effective != NULL && !effective->enabled, "unscoped disabled");
expect_space_id(effective, NULL, "unscoped id");
return failures == 0 ? 0 : 1;
}

View file

@ -0,0 +1,357 @@
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include "amduatd_space_roots.h"
#include "amduatd_fed_cursor.h"
#include "amduatd_space.h"
#include "amduatd_store.h"
#include "amduat/asl/asl_store_fs_meta.h"
#include "amduat/asl/asl_pointer_fs.h"
#include "amduat/asl/none.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static char *amduatd_test_make_temp_dir(void) {
char tmpl[] = "/tmp/amduatd-roots-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_list_contains(const amduatd_space_roots_list_t *list,
const char *name) {
if (list == NULL || name == NULL) {
return false;
}
for (size_t i = 0u; i < list->len; ++i) {
if (strcmp(list->names[i], name) == 0) {
return true;
}
}
return false;
}
static bool amduatd_list_sorted(const amduatd_space_roots_list_t *list) {
if (list == NULL || list->len < 2u) {
return true;
}
for (size_t i = 1u; i < list->len; ++i) {
if (strcmp(list->names[i - 1u], list->names[i]) > 0) {
return false;
}
}
return true;
}
static bool amduatd_build_collection_head_name(const char *name,
char **out_name) {
size_t name_len;
size_t total_len;
char *buffer;
size_t offset = 0u;
if (name == NULL || out_name == NULL) {
return false;
}
if (!amduat_asl_pointer_name_is_valid(name)) {
return false;
}
name_len = strlen(name);
total_len = 11u + name_len + 5u + 1u;
buffer = (char *)malloc(total_len);
if (buffer == NULL) {
return false;
}
memcpy(buffer + offset, "collection/", 11u);
offset += 11u;
memcpy(buffer + offset, name, name_len);
offset += name_len;
memcpy(buffer + offset, "/head", 5u);
offset += 5u;
buffer[offset] = '\0';
*out_name = buffer;
return true;
}
static bool amduatd_build_collection_log_head_name(const char *name,
char **out_name) {
size_t name_len;
size_t total_len;
char *buffer;
size_t offset = 0u;
if (name == NULL || out_name == NULL) {
return false;
}
if (!amduat_asl_pointer_name_is_valid(name)) {
return false;
}
name_len = strlen(name);
total_len = 4u + 11u + name_len + 4u + 5u + 1u;
buffer = (char *)malloc(total_len);
if (buffer == NULL) {
return false;
}
memcpy(buffer + offset, "log/", 4u);
offset += 4u;
memcpy(buffer + offset, "collection/", 11u);
offset += 11u;
memcpy(buffer + offset, name, name_len);
offset += name_len;
memcpy(buffer + offset, "/log", 4u);
offset += 4u;
memcpy(buffer + offset, "/head", 5u);
offset += 5u;
buffer[offset] = '\0';
*out_name = buffer;
return true;
}
static int amduatd_test_empty_store(void) {
char *root = amduatd_test_make_temp_dir();
amduat_asl_store_fs_config_t cfg;
amduat_asl_pointer_store_t pointer_store;
amduatd_space_t space;
amduatd_space_roots_list_t list;
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;
}
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, NULL, false)) {
fprintf(stderr, "failed to init space\n");
free(root);
return 1;
}
if (!amduatd_space_roots_list(root, &pointer_store, &space, &list)) {
fprintf(stderr, "roots list failed\n");
free(root);
return 1;
}
if (list.len != 3u ||
!amduatd_list_contains(&list, "daemon/edges/index/head") ||
!amduatd_list_contains(&list, "collection/daemon/edges/head") ||
!amduatd_list_contains(&list, "log/collection/daemon/edges/log/head") ||
!amduatd_list_sorted(&list)) {
fprintf(stderr, "unexpected empty-store roots list\n");
amduatd_space_roots_list_free(&list);
free(root);
return 1;
}
amduatd_space_roots_list_free(&list);
free(root);
return 0;
}
static int amduatd_test_cursor_roots(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_artifact_t none_artifact;
amduat_reference_t none_ref;
amduat_octets_t cursor_name = amduat_octets(NULL, 0u);
amduat_octets_t push_name = amduat_octets(NULL, 0u);
amduat_octets_t cursor_v2 = amduat_octets(NULL, 0u);
amduat_octets_t push_v2 = amduat_octets(NULL, 0u);
amduat_octets_t edges_collection = amduat_octets(NULL, 0u);
amduat_octets_t edges_index_head = amduat_octets(NULL, 0u);
char *collection_head = NULL;
char *collection_log_head = NULL;
amduatd_space_roots_list_t list;
bool swapped = false;
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 (!amduat_asl_none_artifact(&none_artifact)) {
fprintf(stderr, "failed to build none artifact\n");
free(root);
return 1;
}
if (amduat_asl_store_put(&store, none_artifact, &none_ref) !=
AMDUAT_ASL_STORE_OK) {
fprintf(stderr, "failed to store none artifact\n");
amduat_artifact_free(&none_artifact);
free(root);
return 1;
}
amduat_artifact_free(&none_artifact);
if (!amduatd_fed_cursor_pointer_name(&space, "peer-a", &cursor_name) ||
!amduatd_fed_push_cursor_pointer_name(&space, "peer-b", &push_name) ||
!amduatd_fed_cursor_pointer_name_v2(&space,
"peer-a",
"beta",
&cursor_v2) ||
!amduatd_fed_push_cursor_pointer_name_v2(&space,
"peer-b",
"beta",
&push_v2)) {
fprintf(stderr, "failed to build cursor names\n");
free(root);
return 1;
}
if (amduat_asl_pointer_cas(&pointer_store,
(const char *)cursor_name.data,
false,
NULL,
&none_ref,
&swapped) != AMDUAT_ASL_POINTER_OK || !swapped ||
amduat_asl_pointer_cas(&pointer_store,
(const char *)push_name.data,
false,
NULL,
&none_ref,
&swapped) != AMDUAT_ASL_POINTER_OK || !swapped ||
amduat_asl_pointer_cas(&pointer_store,
(const char *)cursor_v2.data,
false,
NULL,
&none_ref,
&swapped) != AMDUAT_ASL_POINTER_OK || !swapped ||
amduat_asl_pointer_cas(&pointer_store,
(const char *)push_v2.data,
false,
NULL,
&none_ref,
&swapped) != AMDUAT_ASL_POINTER_OK || !swapped) {
fprintf(stderr, "failed to seed cursor pointers\n");
amduat_octets_free(&cursor_name);
amduat_octets_free(&push_name);
amduat_octets_free(&cursor_v2);
amduat_octets_free(&push_v2);
free(root);
return 1;
}
if (!amduatd_space_roots_list(root, &pointer_store, &space, &list)) {
fprintf(stderr, "roots list failed\n");
amduat_octets_free(&cursor_name);
amduat_octets_free(&push_name);
amduat_octets_free(&cursor_v2);
amduat_octets_free(&push_v2);
free(root);
return 1;
}
if (!amduatd_space_edges_collection_name(&space, &edges_collection) ||
!amduatd_space_edges_index_head_name(&space, &edges_index_head) ||
!amduatd_build_collection_head_name(
(const char *)edges_collection.data, &collection_head) ||
!amduatd_build_collection_log_head_name(
(const char *)edges_collection.data, &collection_log_head)) {
fprintf(stderr, "failed to build static root names\n");
amduat_octets_free(&cursor_name);
amduat_octets_free(&push_name);
amduatd_space_roots_list_free(&list);
free(root);
return 1;
}
if (list.len != 7u ||
!amduatd_list_contains(&list, (const char *)cursor_name.data) ||
!amduatd_list_contains(&list, (const char *)push_name.data) ||
!amduatd_list_contains(&list, (const char *)cursor_v2.data) ||
!amduatd_list_contains(&list, (const char *)push_v2.data) ||
!amduatd_list_contains(&list, (const char *)edges_index_head.data) ||
!amduatd_list_contains(&list, collection_head) ||
!amduatd_list_contains(&list, collection_log_head) ||
!amduatd_list_sorted(&list)) {
fprintf(stderr, "unexpected cursor roots list\n");
amduat_octets_free(&cursor_name);
amduat_octets_free(&push_name);
amduat_octets_free(&cursor_v2);
amduat_octets_free(&push_v2);
amduat_octets_free(&edges_collection);
amduat_octets_free(&edges_index_head);
free(collection_head);
free(collection_log_head);
amduatd_space_roots_list_free(&list);
free(root);
return 1;
}
amduat_octets_free(&cursor_name);
amduat_octets_free(&push_name);
amduat_octets_free(&cursor_v2);
amduat_octets_free(&push_v2);
amduat_octets_free(&edges_collection);
amduat_octets_free(&edges_index_head);
free(collection_head);
free(collection_log_head);
amduatd_space_roots_list_free(&list);
free(root);
return 0;
}
int main(void) {
if (amduatd_test_empty_store() != 0) {
return 1;
}
if (amduatd_test_cursor_roots() != 0) {
return 1;
}
return 0;
}

View file

@ -0,0 +1,231 @@
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include "amduatd_space_roots.h"
#include "amduatd_fed_cursor.h"
#include "amduatd_space.h"
#include "amduatd_store.h"
#include "amduat/asl/asl_pointer_fs.h"
#include "amduat/asl/asl_store_fs_meta.h"
#include "amduat/asl/none.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static char *amduatd_test_make_temp_dir(void) {
char tmpl[] = "/tmp/amduatd-sync-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_list_contains(const amduatd_space_roots_list_t *list,
const char *name) {
if (list == NULL || name == NULL) {
return false;
}
for (size_t i = 0u; i < list->len; ++i) {
if (strcmp(list->names[i], name) == 0) {
return true;
}
}
return false;
}
static bool amduatd_list_sorted(const amduatd_space_roots_list_t *list) {
if (list == NULL || list->len < 2u) {
return true;
}
for (size_t i = 1u; i < list->len; ++i) {
if (strcmp(list->names[i - 1u], list->names[i]) > 0) {
return false;
}
}
return true;
}
static int amduatd_test_empty_peers(void) {
char *root = amduatd_test_make_temp_dir();
amduat_asl_store_fs_config_t cfg;
amduat_asl_pointer_store_t pointer_store;
amduatd_space_t space;
amduatd_space_roots_list_t peers;
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;
}
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_space_roots_list_cursor_peers(root, &space, &peers)) {
fprintf(stderr, "cursor peer list failed\n");
free(root);
return 1;
}
if (peers.len != 0u) {
fprintf(stderr, "expected empty peers list\n");
amduatd_space_roots_list_free(&peers);
free(root);
return 1;
}
amduatd_space_roots_list_free(&peers);
free(root);
return 0;
}
static int amduatd_test_peer_discovery(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_artifact_t none_artifact;
amduat_reference_t none_ref;
amduat_octets_t pull_name = amduat_octets(NULL, 0u);
amduat_octets_t push_name = amduat_octets(NULL, 0u);
amduatd_space_roots_list_t peers;
bool swapped = false;
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 (!amduat_asl_none_artifact(&none_artifact)) {
fprintf(stderr, "failed to build none artifact\n");
free(root);
return 1;
}
if (amduat_asl_store_put(&store, none_artifact, &none_ref) !=
AMDUAT_ASL_STORE_OK) {
fprintf(stderr, "failed to store none artifact\n");
amduat_artifact_free(&none_artifact);
free(root);
return 1;
}
amduat_artifact_free(&none_artifact);
if (!amduatd_fed_cursor_pointer_name(&space, "1", &pull_name) ||
!amduatd_fed_push_cursor_pointer_name(&space, "2", &push_name)) {
fprintf(stderr, "failed to build cursor names\n");
free(root);
return 1;
}
if (amduat_asl_pointer_cas(&pointer_store,
(const char *)pull_name.data,
false,
NULL,
&none_ref,
&swapped) != AMDUAT_ASL_POINTER_OK || !swapped ||
amduat_asl_pointer_cas(&pointer_store,
(const char *)push_name.data,
false,
NULL,
&none_ref,
&swapped) != AMDUAT_ASL_POINTER_OK || !swapped) {
fprintf(stderr, "failed to seed cursor pointers\n");
amduat_octets_free(&pull_name);
amduat_octets_free(&push_name);
free(root);
return 1;
}
if (!amduatd_space_roots_list_cursor_peers(root, &space, &peers)) {
fprintf(stderr, "cursor peer list failed\n");
amduat_octets_free(&pull_name);
amduat_octets_free(&push_name);
free(root);
return 1;
}
if (peers.len != 2u ||
!amduatd_list_contains(&peers, "1") ||
!amduatd_list_contains(&peers, "2") ||
!amduatd_list_sorted(&peers)) {
fprintf(stderr, "unexpected peers list\n");
for (size_t i = 0u; i < peers.len; ++i) {
fprintf(stderr, " peer[%zu]=%s\n", i, peers.names[i]);
}
amduatd_space_roots_list_free(&peers);
amduat_octets_free(&pull_name);
amduat_octets_free(&push_name);
free(root);
return 1;
}
amduatd_space_roots_list_free(&peers);
amduat_octets_free(&pull_name);
amduat_octets_free(&push_name);
free(root);
return 0;
}
int main(void) {
if (amduatd_test_empty_peers() != 0) {
return 1;
}
if (amduatd_test_peer_discovery() != 0) {
return 1;
}
return 0;
}

View file

@ -0,0 +1,405 @@
#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");
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);
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;
}

View file

@ -0,0 +1,109 @@
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include "amduatd_store.h"
#include "amduat/asl/asl_store_fs_meta.h"
#include "amduat/enc/asl_log.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static char *amduatd_test_make_temp_dir(void) {
char tmpl[] = "/tmp/amduatd-store-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;
}
int main(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_log_record_t *records = NULL;
size_t record_count = 0;
amduat_asl_store_error_t scan_err;
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 fs backend\n");
free(root);
return 1;
}
if (store.ops.log_scan != NULL) {
fprintf(stderr, "fs backend unexpectedly supports log_scan\n");
free(root);
return 1;
}
scan_err = amduat_asl_log_scan(&store, &records, &record_count);
if (scan_err != AMDUAT_ASL_STORE_ERR_UNSUPPORTED) {
fprintf(stderr, "fs backend log_scan returned %d\n", (int)scan_err);
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_INDEX)) {
fprintf(stderr, "failed to init index backend\n");
free(root);
return 1;
}
if (store.ops.log_scan == NULL) {
fprintf(stderr, "index backend missing log_scan\n");
free(root);
return 1;
}
scan_err = amduat_asl_log_scan(&store, &records, &record_count);
if (scan_err != AMDUAT_ASL_STORE_OK) {
fprintf(stderr, "index backend log_scan returned %d\n", (int)scan_err);
free(root);
return 1;
}
if (records != NULL) {
amduat_enc_asl_log_free(records, record_count);
}
free(root);
return 0;
}

View file

@ -0,0 +1,384 @@
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
typedef struct {
char *data;
size_t len;
size_t cap;
} amduatd_buf_t;
static void amduatd_buf_free(amduatd_buf_t *b) {
if (b == NULL) {
return;
}
free(b->data);
b->data = NULL;
b->len = 0u;
b->cap = 0u;
}
static bool amduatd_buf_reserve(amduatd_buf_t *b, size_t extra) {
size_t need;
size_t next;
char *mem;
if (b == NULL) {
return false;
}
if (extra > SIZE_MAX - b->len) {
return false;
}
need = b->len + extra;
if (need <= b->cap) {
return true;
}
next = b->cap == 0u ? 512u : b->cap;
while (next < need) {
if (next > SIZE_MAX / 2u) {
next = need;
break;
}
next *= 2u;
}
mem = (char *)realloc(b->data, next);
if (mem == NULL) {
return false;
}
b->data = mem;
b->cap = next;
return true;
}
static bool amduatd_buf_append(amduatd_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_buf_reserve(b, n)) {
return false;
}
memcpy(b->data + b->len, s, n);
b->len += n;
return true;
}
static bool amduatd_buf_append_cstr(amduatd_buf_t *b, const char *s) {
return amduatd_buf_append(b, s, s != NULL ? strlen(s) : 0u);
}
static void amduatd_usage(const char *prog) {
fprintf(stderr,
"usage: %s --sock PATH --method METHOD --path PATH "
"[--header \"K: V\"]... [--data STRING|--data-file PATH] "
"[--allow-status]\n",
prog);
}
static bool amduatd_read_file(const char *path, uint8_t **out_data,
size_t *out_len) {
FILE *fp;
long size;
uint8_t *buf;
if (out_data == NULL || out_len == NULL || path == NULL) {
return false;
}
*out_data = NULL;
*out_len = 0u;
fp = fopen(path, "rb");
if (fp == NULL) {
return false;
}
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
return false;
}
size = ftell(fp);
if (size < 0) {
fclose(fp);
return false;
}
if (fseek(fp, 0, SEEK_SET) != 0) {
fclose(fp);
return false;
}
buf = (uint8_t *)malloc((size_t)size);
if (buf == NULL) {
fclose(fp);
return false;
}
if (size != 0 &&
fread(buf, 1u, (size_t)size, fp) != (size_t)size) {
free(buf);
fclose(fp);
return false;
}
fclose(fp);
*out_data = buf;
*out_len = (size_t)size;
return true;
}
static int amduatd_parse_status(const char *line) {
const char *p;
char *end = NULL;
long code;
if (line == NULL) {
return -1;
}
p = strchr(line, ' ');
if (p == NULL) {
return -1;
}
while (*p == ' ') {
p++;
}
errno = 0;
code = strtol(p, &end, 10);
if (errno != 0 || end == p || code < 0 || code > 999) {
return -1;
}
return (int)code;
}
int main(int argc, char **argv) {
const char *sock_path = NULL;
const char *method = NULL;
const char *path = NULL;
bool allow_status = false;
char **headers = NULL;
size_t header_count = 0u;
uint8_t *body = NULL;
size_t body_len = 0u;
int fd = -1;
struct sockaddr_un addr;
amduatd_buf_t req;
amduatd_buf_t resp;
ssize_t nread;
int status;
char *header_end;
size_t body_offset;
size_t i;
memset(&req, 0, sizeof(req));
memset(&resp, 0, sizeof(resp));
for (i = 1; i < (size_t)argc; ++i) {
const char *arg = argv[i];
if (strcmp(arg, "--sock") == 0 && i + 1 < (size_t)argc) {
sock_path = argv[++i];
} else if (strcmp(arg, "--method") == 0 && i + 1 < (size_t)argc) {
method = argv[++i];
} else if (strcmp(arg, "--path") == 0 && i + 1 < (size_t)argc) {
path = argv[++i];
} else if (strcmp(arg, "--header") == 0 && i + 1 < (size_t)argc) {
char **next = (char **)realloc(headers,
(header_count + 1u) * sizeof(*headers));
if (next == NULL) {
fprintf(stderr, "oom\n");
goto fail;
}
headers = next;
headers[header_count++] = argv[++i];
} else if (strcmp(arg, "--data") == 0 && i + 1 < (size_t)argc) {
const char *data = argv[++i];
body_len = strlen(data);
if (body_len == 0u) {
body = NULL;
} else {
body = (uint8_t *)malloc(body_len);
if (body == NULL) {
fprintf(stderr, "oom\n");
goto fail;
}
memcpy(body, data, body_len);
}
} else if (strcmp(arg, "--data-file") == 0 && i + 1 < (size_t)argc) {
if (!amduatd_read_file(argv[++i], &body, &body_len)) {
fprintf(stderr, "failed to read data file\n");
goto fail;
}
} else if (strcmp(arg, "--allow-status") == 0) {
allow_status = true;
} else {
amduatd_usage(argv[0]);
goto fail;
}
}
if (sock_path == NULL || method == NULL || path == NULL) {
amduatd_usage(argv[0]);
goto fail;
}
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) {
perror("socket");
goto fail;
}
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
if (strlen(sock_path) >= sizeof(addr.sun_path)) {
fprintf(stderr, "socket path too long\n");
goto fail;
}
strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path) - 1u);
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
perror("connect");
goto fail;
}
if (!amduatd_buf_append_cstr(&req, method) ||
!amduatd_buf_append_cstr(&req, " ") ||
!amduatd_buf_append_cstr(&req, path) ||
!amduatd_buf_append_cstr(&req, " HTTP/1.1\r\n") ||
!amduatd_buf_append_cstr(&req, "Host: localhost\r\n") ||
!amduatd_buf_append_cstr(&req, "Connection: close\r\n")) {
fprintf(stderr, "oom\n");
goto fail;
}
for (i = 0; i < header_count; ++i) {
if (!amduatd_buf_append_cstr(&req, headers[i]) ||
!amduatd_buf_append_cstr(&req, "\r\n")) {
fprintf(stderr, "oom\n");
goto fail;
}
}
if (body != NULL) {
char len_buf[64];
int len;
len = snprintf(len_buf, sizeof(len_buf), "Content-Length: %zu\r\n",
body_len);
if (len < 0 || (size_t)len >= sizeof(len_buf) ||
!amduatd_buf_append(&req, len_buf, (size_t)len)) {
fprintf(stderr, "oom\n");
goto fail;
}
}
if (!amduatd_buf_append_cstr(&req, "\r\n")) {
fprintf(stderr, "oom\n");
goto fail;
}
{
size_t off = 0u;
while (off < req.len) {
ssize_t n = write(fd, req.data + off, req.len - off);
if (n <= 0) {
perror("write");
goto fail;
}
off += (size_t)n;
}
}
if (body != NULL && body_len != 0u) {
size_t off = 0u;
while (off < body_len) {
ssize_t n = write(fd, body + off, body_len - off);
if (n <= 0) {
perror("write");
goto fail;
}
off += (size_t)n;
}
}
while (true) {
char tmp[4096];
nread = read(fd, tmp, sizeof(tmp));
if (nread < 0) {
perror("read");
goto fail;
}
if (nread == 0) {
break;
}
if (!amduatd_buf_append(&resp, tmp, (size_t)nread)) {
fprintf(stderr, "oom\n");
goto fail;
}
}
if (resp.len == 0u) {
fprintf(stderr, "empty response\n");
goto fail;
}
if (!amduatd_buf_append(&resp, "\0", 1u)) {
fprintf(stderr, "oom\n");
goto fail;
}
{
char *line_end = strstr(resp.data, "\r\n");
if (line_end == NULL) {
fprintf(stderr, "invalid response\n");
goto fail;
}
*line_end = '\0';
status = amduatd_parse_status(resp.data);
*line_end = '\r';
if (status < 0) {
fprintf(stderr, "invalid status\n");
goto fail;
}
}
header_end = strstr(resp.data, "\r\n\r\n");
if (header_end == NULL) {
body_offset = resp.len;
} else {
body_offset = (size_t)(header_end - resp.data) + 4u;
}
if (body_offset < resp.len) {
size_t out_len = resp.len - body_offset;
if (resp.len > 0u && resp.data[resp.len - 1u] == '\0') {
if (out_len > 0u) {
out_len -= 1u;
}
}
if (out_len > 0u) {
fwrite(resp.data + body_offset, 1u, out_len, stdout);
}
}
fflush(stdout);
amduatd_buf_free(&req);
amduatd_buf_free(&resp);
free(body);
free(headers);
if (fd >= 0) {
close(fd);
}
if (!allow_status && (status < 200 || status >= 300)) {
return 1;
}
return 0;
fail:
amduatd_buf_free(&req);
amduatd_buf_free(&resp);
free(body);
free(headers);
if (fd >= 0) {
close(fd);
}
return 1;
}

2
vendor/amduat vendored

@ -1 +1 @@
Subproject commit 0f38165804d9af4fe8a7d2570af75161e93cb9cd Subproject commit a433f92f13be5193642cd514272ae0faba4aed08