Fix workspace store capability reporting to reflect backend support

This commit is contained in:
Carl Niklas Rydberg 2026-01-25 05:20:24 +01:00
parent 5a140178a1
commit d045614909
8 changed files with 552 additions and 16 deletions

View file

@ -324,8 +324,9 @@ curl --unix-socket amduatd.sock \
`/v1/space/workspace` returns a deterministic, read-only snapshot for the `/v1/space/workspace` returns a deterministic, read-only snapshot for the
effective space. It aggregates the manifest, mount resolution, per-mount cursor effective space. It aggregates the manifest, mount resolution, per-mount cursor
status, store backend metadata, federation flags, and store capabilities into status, store backend metadata, federation flags, and store capabilities
one JSON response. It performs no network I/O and does not mutate storage. (`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: This is a local snapshot that complements:
- `/v1/space/manifest` (manifest root + canonical manifest) - `/v1/space/manifest` (manifest root + canonical manifest)
@ -342,9 +343,12 @@ curl --unix-socket amduatd.sock \
## Workspace UI ## Workspace UI
`/workspace` serves a minimal, human-facing page that consumes `/workspace` serves a minimal, human-facing page that consumes
`/v1/space/workspace` and `/v1/space/mounts/sync/until`. It is a convenience `/v1/space/workspace` and `/v1/space/mounts/sync/until`, plus read-only
view for inspection and manual sync control, not a stable API. For health panels for `/v1/space/doctor`, `/v1/space/roots`,
programmatic use, call the `/v1/*` endpoints directly. `/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) ## Space mounts sync (track mounts)

View file

@ -384,6 +384,59 @@
}, },
"peers": {"type": "array", "items": {"$ref": "#/schemas/space_sync_status_peer"}} "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

@ -1 +1 @@
{"registry":"AMDUATD/API","contract":"AMDUATD/API/1","handle":"amduat.api.amduatd.contract.v1@1","media_type":"application/json","status":"active","bytes_sha256":"88c7e93034dacc34318b0771f1bae232bb4a34913f7274d7e99b007fe4f697c3","notes":"Seeded into the ASL store at amduatd startup; ref is advertised via /v1/meta."} {"registry":"AMDUATD/API","contract":"AMDUATD/API/1","handle":"amduat.api.amduatd.contract.v1@1","media_type":"application/json","status":"active","bytes_sha256":"38cb6beb6bb525d892538dad7aa584b3f2aeaaff177757fd9432fce9602f877b","notes":"Seeded into the ASL store at amduatd startup; ref is advertised via /v1/meta."}

View file

@ -233,7 +233,79 @@ static const char k_amduatd_workspace_html[] =
" <h2>Raw workspace JSON</h2>\n" " <h2>Raw workspace JSON</h2>\n"
" <pre id=\"workspace_raw\"></pre>\n" " <pre id=\"workspace_raw\"></pre>\n"
" </div>\n" " </div>\n"
" </div>\n" "\n"
" <div class=\"card\" style=\"margin-top:16px;\">\n"
" <h2>Health / Debug</h2>\n"
" <p class=\"muted\">Fetch read-only diagnostics for the current space.</p>\n"
"\n"
" <div class=\"card\" style=\"margin-top:12px;\">\n"
" <h2>Doctor</h2>\n"
" <div class=\"bar\">\n"
" <button id=\"doctor_fetch\">Fetch</button>\n"
" <span id=\"doctor_status\" class=\"muted\">idle</span>\n"
" <span id=\"doctor_summary\" class=\"mono\"></span>\n"
" </div>\n"
" <details id=\"doctor_details\" hidden>\n"
" <summary>Raw JSON</summary>\n"
" <pre id=\"doctor_raw\"></pre>\n"
" </details>\n"
" </div>\n"
"\n"
" <div class=\"card\" style=\"margin-top:12px;\">\n"
" <h2>Roots</h2>\n"
" <div class=\"bar\">\n"
" <button id=\"roots_fetch\">Fetch</button>\n"
" <span id=\"roots_status\" class=\"muted\">idle</span>\n"
" <span id=\"roots_summary\" class=\"mono\"></span>\n"
" </div>\n"
" <div id=\"roots_list\" class=\"mono\" style=\"margin-top:8px;\"></div>\n"
" <details id=\"roots_details\" hidden>\n"
" <summary>Raw JSON</summary>\n"
" <pre id=\"roots_raw\"></pre>\n"
" </details>\n"
" </div>\n"
"\n"
" <div class=\"card\" style=\"margin-top:12px;\">\n"
" <h2>Sync status</h2>\n"
" <div class=\"bar\">\n"
" <button id=\"sync_status_fetch\">Fetch</button>\n"
" <span id=\"sync_status\" class=\"muted\">idle</span>\n"
" <span id=\"sync_status_summary\" class=\"mono\"></span>\n"
" </div>\n"
" <div id=\"sync_status_table\" style=\"margin-top:8px;\"></div>\n"
" <details id=\"sync_status_details\" hidden>\n"
" <summary>Raw JSON</summary>\n"
" <pre id=\"sync_status_raw\"></pre>\n"
" </details>\n"
" </div>\n"
"\n"
" <div class=\"card\" style=\"margin-top:12px;\">\n"
" <h2>Mounts resolve</h2>\n"
" <div class=\"bar\">\n"
" <button id=\"mounts_resolve_fetch\">Fetch</button>\n"
" <span id=\"mounts_resolve_status\" class=\"muted\">idle</span>\n"
" <span id=\"mounts_resolve_summary\" class=\"mono\"></span>\n"
" </div>\n"
" <details id=\"mounts_resolve_details\" hidden>\n"
" <summary>Raw JSON</summary>\n"
" <pre id=\"mounts_resolve_raw\"></pre>\n"
" </details>\n"
" </div>\n"
"\n"
" <div class=\"card\" style=\"margin-top:12px;\">\n"
" <h2>Manifest</h2>\n"
" <div class=\"bar\">\n"
" <button id=\"manifest_fetch\">Fetch</button>\n"
" <span id=\"manifest_status\" class=\"muted\">idle</span>\n"
" <span id=\"manifest_summary\" class=\"mono\"></span>\n"
" </div>\n"
" <details id=\"manifest_details\" hidden>\n"
" <summary>Raw JSON</summary>\n"
" <pre id=\"manifest_raw\"></pre>\n"
" </details>\n"
" </div>\n"
" </div>\n"
" </div>\n"
"\n" "\n"
" <script>\n" " <script>\n"
" const spaceInput = document.getElementById('space');\n" " const spaceInput = document.getElementById('space');\n"
@ -252,6 +324,33 @@ static const char k_amduatd_workspace_html[] =
" const syncSummary = document.getElementById('sync_summary');\n" " const syncSummary = document.getElementById('sync_summary');\n"
" const syncDetails = document.getElementById('sync_details');\n" " const syncDetails = document.getElementById('sync_details');\n"
" const syncRaw = document.getElementById('sync_raw');\n" " const syncRaw = document.getElementById('sync_raw');\n"
" const doctorFetch = document.getElementById('doctor_fetch');\n"
" const doctorStatus = document.getElementById('doctor_status');\n"
" const doctorSummary = document.getElementById('doctor_summary');\n"
" const doctorDetails = document.getElementById('doctor_details');\n"
" const doctorRaw = document.getElementById('doctor_raw');\n"
" const rootsFetch = document.getElementById('roots_fetch');\n"
" const rootsStatus = document.getElementById('roots_status');\n"
" const rootsSummary = document.getElementById('roots_summary');\n"
" const rootsList = document.getElementById('roots_list');\n"
" const rootsDetails = document.getElementById('roots_details');\n"
" const rootsRaw = document.getElementById('roots_raw');\n"
" const syncStatusFetch = document.getElementById('sync_status_fetch');\n"
" const syncStatus = document.getElementById('sync_status');\n"
" const syncStatusSummary = document.getElementById('sync_status_summary');\n"
" const syncStatusTable = document.getElementById('sync_status_table');\n"
" const syncStatusDetails = document.getElementById('sync_status_details');\n"
" const syncStatusRaw = document.getElementById('sync_status_raw');\n"
" const mountsResolveFetch = document.getElementById('mounts_resolve_fetch');\n"
" const mountsResolveStatus = document.getElementById('mounts_resolve_status');\n"
" const mountsResolveSummary = document.getElementById('mounts_resolve_summary');\n"
" const mountsResolveDetails = document.getElementById('mounts_resolve_details');\n"
" const mountsResolveRaw = document.getElementById('mounts_resolve_raw');\n"
" const manifestFetch = document.getElementById('manifest_fetch');\n"
" const manifestStatus = document.getElementById('manifest_status');\n"
" const manifestSummary = document.getElementById('manifest_summary');\n"
" const manifestDetails = document.getElementById('manifest_details');\n"
" const manifestRaw = document.getElementById('manifest_raw');\n"
"\n" "\n"
" function getSpaceHeader() {\n" " function getSpaceHeader() {\n"
" const raw = spaceInput.value.trim();\n" " const raw = spaceInput.value.trim();\n"
@ -282,13 +381,25 @@ static const char k_amduatd_workspace_html[] =
" }\n" " }\n"
"\n" "\n"
" function renderCapabilities(caps) {\n" " function renderCapabilities(caps) {\n"
" if (!caps || !caps.store_ops) {\n" " if (!caps || !caps.supported_ops) {\n"
" setMonospaceText(capabilitiesEl, '-');\n" " setMonospaceText(capabilitiesEl, '-');\n"
" return;\n" " return;\n"
" }\n" " }\n"
" const keys = ['put','get','put_indexed','get_indexed','tombstone','tombstone_lift','log_scan','current_state','validate_config'];\n" " const keys = ['put','get','put_indexed','get_indexed','tombstone','tombstone_lift','log_scan','current_state','validate_config'];\n"
" const lines = keys.map((key) => `${key}: ${caps.store_ops[key] === true ? 'true' : 'false'}`);\n" " const lines = keys.map((key) => `${key}: ${caps.supported_ops[key] === true ? 'true' : 'false'}`);\n"
" capabilitiesEl.textContent = lines.join('\\n');\n" " capabilitiesEl.textContent = lines.join('\\n');\n"
" if (caps.implemented_ops) {\n"
" const impl = keys.map((key) => `${key}: ${caps.implemented_ops[key] === true ? 'true' : 'false'}`);\n"
" const details = document.createElement('details');\n"
" const summary = document.createElement('summary');\n"
" const pre = document.createElement('pre');\n"
" summary.textContent = 'Implemented ops (debug)';\n"
" pre.textContent = impl.join('\\n');\n"
" details.appendChild(summary);\n"
" details.appendChild(pre);\n"
" capabilitiesEl.appendChild(document.createElement('div'));\n"
" capabilitiesEl.appendChild(details);\n"
" }\n"
" }\n" " }\n"
"\n" "\n"
" function renderMounts(mounts) {\n" " function renderMounts(mounts) {\n"
@ -319,6 +430,75 @@ static const char k_amduatd_workspace_html[] =
" });\n" " });\n"
" }\n" " }\n"
"\n" "\n"
" function truncateBody(text, limit) {\n"
" if (!text) {\n"
" return '';\n"
" }\n"
" if (text.length <= limit) {\n"
" return text;\n"
" }\n"
" return text.slice(0, limit) + '...';\n"
" }\n"
"\n"
" async function fetchPanel(endpoint, statusEl, summaryEl, rawEl, detailsEl, onData) {\n"
" clearMessage();\n"
" statusEl.textContent = 'loading';\n"
" summaryEl.textContent = '';\n"
" if (detailsEl) {\n"
" detailsEl.hidden = true;\n"
" }\n"
" if (rawEl) {\n"
" rawEl.textContent = '';\n"
" }\n"
" const space = getSpaceHeader();\n"
" if (spaceError.hidden === false) {\n"
" statusEl.textContent = 'error';\n"
" return;\n"
" }\n"
" const headers = {};\n"
" if (space) {\n"
" headers['X-Amduat-Space'] = space;\n"
" }\n"
" let res;\n"
" try {\n"
" res = await fetch(endpoint, { headers });\n"
" } catch (err) {\n"
" statusEl.textContent = 'error';\n"
" summaryEl.textContent = 'network error';\n"
" return;\n"
" }\n"
" const body = await res.text();\n"
" if (rawEl) {\n"
" rawEl.textContent = body;\n"
" }\n"
" if (detailsEl) {\n"
" detailsEl.hidden = false;\n"
" }\n"
" if (!res.ok) {\n"
" statusEl.textContent = 'error';\n"
" summaryEl.textContent = `http ${res.status}: ${truncateBody(body, 240)}`;\n"
" if (onData) {\n"
" onData(null, res.status);\n"
" }\n"
" return;\n"
" }\n"
" let data;\n"
" try {\n"
" data = JSON.parse(body);\n"
" } catch (err) {\n"
" statusEl.textContent = 'error';\n"
" summaryEl.textContent = 'invalid json';\n"
" if (onData) {\n"
" onData(null, res.status);\n"
" }\n"
" return;\n"
" }\n"
" statusEl.textContent = 'ok';\n"
" if (onData) {\n"
" onData(data, res.status);\n"
" }\n"
" }\n"
"\n"
" async function loadWorkspace() {\n" " async function loadWorkspace() {\n"
" clearMessage();\n" " clearMessage();\n"
" const space = getSpaceHeader();\n" " const space = getSpaceHeader();\n"
@ -425,12 +605,140 @@ static const char k_amduatd_workspace_html[] =
" syncing.hidden = true;\n" " syncing.hidden = true;\n"
" }\n" " }\n"
"\n" "\n"
" refreshBtn.addEventListener('click', loadWorkspace);\n" " function renderDoctor(data, status) {\n"
" if (!data) {\n"
" return;\n"
" }\n"
" if (data.summary && (data.summary.ok_count !== undefined || data.summary.warn_count !== undefined)) {\n"
" const okCount = data.summary.ok_count !== undefined ? data.summary.ok_count : 0;\n"
" const warnCount = data.summary.warn_count !== undefined ? data.summary.warn_count : 0;\n"
" const failCount = data.summary.fail_count !== undefined ? data.summary.fail_count : 0;\n"
" const skippedCount = data.summary.skipped_count !== undefined ? data.summary.skipped_count : 0;\n"
" doctorSummary.textContent = `ok=${okCount} warn=${warnCount} fail=${failCount} skipped=${skippedCount}`;\n"
" return;\n"
" }\n"
" let warnCount = 0;\n"
" let okCount = 0;\n"
" let skipCount = 0;\n"
" if (Array.isArray(data.checks)) {\n"
" data.checks.forEach((item) => {\n"
" if (!item) {\n"
" return;\n"
" }\n"
" if (item.status === 'ok') {\n"
" okCount += 1;\n"
" } else if (item.status === 'skipped') {\n"
" skipCount += 1;\n"
" } else {\n"
" warnCount += 1;\n"
" }\n"
" });\n"
" }\n"
" if (okCount + warnCount + skipCount > 0) {\n"
" doctorSummary.textContent = `ok=${okCount} warn=${warnCount} skipped=${skipCount}`;\n"
" return;\n"
" }\n"
" doctorSummary.textContent = data.status !== undefined ? `status=${data.status}` : `http ${status}`;\n"
" }\n"
"\n"
" function renderRoots(data) {\n"
" rootsList.textContent = '';\n"
" if (!data || !Array.isArray(data.roots)) {\n"
" rootsSummary.textContent = 'roots=0';\n"
" return;\n"
" }\n"
" rootsSummary.textContent = `roots=${data.roots.length}`;\n"
" const shown = data.roots.slice(0, 10);\n"
" rootsList.textContent = shown.join('\\n');\n"
" }\n"
"\n"
" function renderSyncStatus(data) {\n"
" syncStatusTable.innerHTML = '';\n"
" if (!data || !Array.isArray(data.peers)) {\n"
" syncStatusSummary.textContent = 'peers=0 remotes=0';\n"
" return;\n"
" }\n"
" let remotes = 0;\n"
" data.peers.forEach((peer) => {\n"
" if (peer && Array.isArray(peer.remotes)) {\n"
" remotes += peer.remotes.length;\n"
" }\n"
" });\n"
" syncStatusSummary.textContent = `peers=${data.peers.length} remotes=${remotes}`;\n"
" let html = '<table><thead><tr><th>Peer</th><th>Remote</th><th>Pull</th><th>Push</th></tr></thead><tbody>';\n"
" data.peers.forEach((peer) => {\n"
" if (!peer || !Array.isArray(peer.remotes)) {\n"
" return;\n"
" }\n"
" peer.remotes.forEach((remote) => {\n"
" const pull = remote && remote.pull_cursor ? remote.pull_cursor : null;\n"
" const push = remote && remote.push_cursor ? remote.push_cursor : null;\n"
" const pullText = pull ? `present=${pull.present}` +\n"
" (pull.last_logseq !== undefined ? ` last_logseq=${pull.last_logseq}` : '') : '';\n"
" const pushText = push ? `present=${push.present}` +\n"
" (push.last_logseq !== undefined ? ` last_logseq=${push.last_logseq}` : '') : '';\n"
" html += `<tr><td class=\"mono\">${peer.peer_key || ''}</td>` +\n"
" `<td class=\"mono\">${remote.remote_space_id || 'null'}</td>` +\n"
" `<td class=\"mono\">${pullText}</td>` +\n"
" `<td class=\"mono\">${pushText}</td></tr>`;\n"
" });\n"
" });\n"
" html += '</tbody></table>';\n"
" syncStatusTable.innerHTML = html;\n"
" }\n"
"\n"
" function renderMountsResolve(data) {\n"
" if (!data || !Array.isArray(data.mounts)) {\n"
" mountsResolveSummary.textContent = 'mounts=0 pinned=0 track=0';\n"
" return;\n"
" }\n"
" let pinned = 0;\n"
" let track = 0;\n"
" data.mounts.forEach((mount) => {\n"
" if (mount && mount.mode === 'pinned') {\n"
" pinned += 1;\n"
" } else if (mount && mount.mode === 'track') {\n"
" track += 1;\n"
" }\n"
" });\n"
" mountsResolveSummary.textContent = `mounts=${data.mounts.length} pinned=${pinned} track=${track}`;\n"
" }\n"
"\n"
" function renderManifest(data, status) {\n"
" if (!data) {\n"
" if (status === 404) {\n"
" manifestSummary.textContent = 'no manifest configured';\n"
" }\n"
" return;\n"
" }\n"
" const count = data.manifest && Array.isArray(data.manifest.mounts)\n"
" ? data.manifest.mounts.length : 0;\n"
" manifestSummary.textContent = `ref=${data.manifest_ref || '-'} mounts=${count}`;\n"
" }\n"
"\n"
" refreshBtn.addEventListener('click', () => {\n"
" loadWorkspace();\n"
" });\n"
" syncBtn.addEventListener('click', syncMounts);\n" " syncBtn.addEventListener('click', syncMounts);\n"
" doctorFetch.addEventListener('click', () => {\n"
" fetchPanel('/v1/space/doctor', doctorStatus, doctorSummary, doctorRaw, doctorDetails, renderDoctor);\n"
" });\n"
" rootsFetch.addEventListener('click', () => {\n"
" fetchPanel('/v1/space/roots', rootsStatus, rootsSummary, rootsRaw, rootsDetails, renderRoots);\n"
" });\n"
" syncStatusFetch.addEventListener('click', () => {\n"
" fetchPanel('/v1/space/sync/status', syncStatus, syncStatusSummary, syncStatusRaw, syncStatusDetails, renderSyncStatus);\n"
" });\n"
" mountsResolveFetch.addEventListener('click', () => {\n"
" fetchPanel('/v1/space/mounts/resolve', mountsResolveStatus, mountsResolveSummary, mountsResolveRaw, mountsResolveDetails, renderMountsResolve);\n"
" });\n"
" manifestFetch.addEventListener('click', () => {\n"
" fetchPanel('/v1/space/manifest', manifestStatus, manifestSummary, manifestRaw, manifestDetails, renderManifest);\n"
" });\n"
" window.addEventListener('load', loadWorkspace);\n" " window.addEventListener('load', loadWorkspace);\n"
" </script>\n" " </script>\n"
"</body>\n" "</body>\n"
"</html>\n"; "</html>\n";
static const char k_amduatd_contract_v1_json[] = static const char k_amduatd_contract_v1_json[] =
"{" "{"
"\"contract\":\"AMDUATD/API/1\"," "\"contract\":\"AMDUATD/API/1\","
@ -785,6 +1093,59 @@ static const char k_amduatd_contract_v1_json[] =
"}," "},"
"\"peers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/schemas/space_sync_status_peer\"}}" "\"peers\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/schemas/space_sync_status_peer\"}}"
"}" "}"
"},"
"\"space_workspace_response\":{"
"\"type\":\"object\","
"\"required\":[\"effective_space\",\"store_backend\",\"federation\",\"capabilities\",\"manifest_ref\",\"manifest\",\"mounts\"],"
"\"properties\":{"
"\"effective_space\":{\"type\":\"object\"},"
"\"store_backend\":{\"type\":\"string\"},"
"\"federation\":{"
"\"type\":\"object\","
"\"required\":[\"enabled\",\"transport\"],"
"\"properties\":{"
"\"enabled\":{\"type\":\"boolean\"},"
"\"transport\":{\"type\":\"string\"}"
"}"
"},"
"\"capabilities\":{"
"\"type\":\"object\","
"\"required\":[\"supported_ops\"],"
"\"properties\":{"
"\"supported_ops\":{"
"\"type\":\"object\","
"\"properties\":{"
"\"put\":{\"type\":\"boolean\"},"
"\"get\":{\"type\":\"boolean\"},"
"\"put_indexed\":{\"type\":\"boolean\"},"
"\"get_indexed\":{\"type\":\"boolean\"},"
"\"tombstone\":{\"type\":\"boolean\"},"
"\"tombstone_lift\":{\"type\":\"boolean\"},"
"\"log_scan\":{\"type\":\"boolean\"},"
"\"current_state\":{\"type\":\"boolean\"},"
"\"validate_config\":{\"type\":\"boolean\"}"
"}"
"},"
"\"implemented_ops\":{"
"\"type\":\"object\","
"\"properties\":{"
"\"put\":{\"type\":\"boolean\"},"
"\"get\":{\"type\":\"boolean\"},"
"\"put_indexed\":{\"type\":\"boolean\"},"
"\"get_indexed\":{\"type\":\"boolean\"},"
"\"tombstone\":{\"type\":\"boolean\"},"
"\"tombstone_lift\":{\"type\":\"boolean\"},"
"\"log_scan\":{\"type\":\"boolean\"},"
"\"current_state\":{\"type\":\"boolean\"},"
"\"validate_config\":{\"type\":\"boolean\"}"
"}"
"}"
"}"
"},"
"\"manifest_ref\":{\"type\":\"string\"},"
"\"manifest\":{\"type\":\"object\"},"
"\"mounts\":{\"type\":\"array\"}"
"}"
"}" "}"
"}" "}"
"}\n"; "}\n";

View file

@ -92,12 +92,81 @@ static bool amduatd_workspace_buf_append_char(amduatd_workspace_buf_t *b,
static bool amduatd_workspace_append_capabilities( static bool amduatd_workspace_append_capabilities(
amduatd_workspace_buf_t *b, amduatd_workspace_buf_t *b,
const amduat_asl_store_t *store) { const amduat_asl_store_t *store,
amduatd_store_backend_t store_backend) {
const amduat_asl_store_ops_t *ops = store != NULL ? &store->ops : NULL; 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\":{")) { if (!amduatd_workspace_buf_append_cstr(b, ",\"capabilities\":{")) {
return false; return false;
} }
if (!amduatd_workspace_buf_append_cstr(b, "\"store_ops\":{")) { if (!amduatd_workspace_buf_append_cstr(b, "\"supported_ops\":{")) {
return false;
}
if (!amduatd_workspace_buf_append_cstr(
b,
"\"put\":") ||
!amduatd_workspace_buf_append_cstr(b, caps.put ? "true" : "false")) {
return false;
}
if (!amduatd_workspace_buf_append_cstr(
b,
",\"get\":") ||
!amduatd_workspace_buf_append_cstr(b, caps.get ? "true" : "false")) {
return false;
}
if (!amduatd_workspace_buf_append_cstr(
b,
",\"put_indexed\":") ||
!amduatd_workspace_buf_append_cstr(
b, caps.put_indexed ? "true" : "false")) {
return false;
}
if (!amduatd_workspace_buf_append_cstr(
b,
",\"get_indexed\":") ||
!amduatd_workspace_buf_append_cstr(
b, caps.get_indexed ? "true" : "false")) {
return false;
}
if (!amduatd_workspace_buf_append_cstr(
b,
",\"tombstone\":") ||
!amduatd_workspace_buf_append_cstr(
b, caps.tombstone ? "true" : "false")) {
return false;
}
if (!amduatd_workspace_buf_append_cstr(
b,
",\"tombstone_lift\":") ||
!amduatd_workspace_buf_append_cstr(
b, caps.tombstone_lift ? "true" : "false")) {
return false;
}
if (!amduatd_workspace_buf_append_cstr(
b,
",\"log_scan\":") ||
!amduatd_workspace_buf_append_cstr(
b, caps.log_scan ? "true" : "false")) {
return false;
}
if (!amduatd_workspace_buf_append_cstr(
b,
",\"current_state\":") ||
!amduatd_workspace_buf_append_cstr(
b, caps.current_state ? "true" : "false")) {
return false;
}
if (!amduatd_workspace_buf_append_cstr(
b,
",\"validate_config\":") ||
!amduatd_workspace_buf_append_cstr(
b, caps.validate_config ? "true" : "false")) {
return false;
}
if (!amduatd_workspace_buf_append_cstr(b, "},\"implemented_ops\":{")) {
return false; return false;
} }
if (!amduatd_workspace_buf_append_cstr( if (!amduatd_workspace_buf_append_cstr(
@ -428,7 +497,7 @@ amduatd_space_workspace_status_t amduatd_space_workspace_get(
if (!amduatd_workspace_buf_append_cstr(&b, "}")) { if (!amduatd_workspace_buf_append_cstr(&b, "}")) {
goto workspace_cleanup; goto workspace_cleanup;
} }
if (!amduatd_workspace_append_capabilities(&b, store)) { if (!amduatd_workspace_append_capabilities(&b, store, store_backend)) {
goto workspace_cleanup; goto workspace_cleanup;
} }
if (!amduatd_workspace_buf_append_cstr(&b, ",\"manifest_ref\":\"") || if (!amduatd_workspace_buf_append_cstr(&b, ",\"manifest_ref\":\"") ||

View file

@ -31,6 +31,33 @@ const char *amduatd_store_backend_name(amduatd_store_backend_t backend) {
} }
} }
bool amduatd_store_caps_supported(amduatd_store_backend_t backend,
amduatd_store_caps_t *out_caps) {
if (out_caps == NULL) {
return false;
}
memset(out_caps, 0, sizeof(*out_caps));
if (backend == AMDUATD_STORE_BACKEND_FS) {
out_caps->get = true;
out_caps->put = true;
out_caps->validate_config = true;
return true;
}
if (backend == AMDUATD_STORE_BACKEND_INDEX) {
out_caps->get = true;
out_caps->put = true;
out_caps->get_indexed = true;
out_caps->put_indexed = true;
out_caps->log_scan = true;
out_caps->current_state = true;
out_caps->tombstone = true;
out_caps->tombstone_lift = true;
out_caps->validate_config = true;
return true;
}
return true;
}
bool amduatd_store_init(amduat_asl_store_t *store, bool amduatd_store_init(amduat_asl_store_t *store,
amduat_asl_store_fs_config_t *cfg, amduat_asl_store_fs_config_t *cfg,
amduatd_store_ctx_t *ctx, amduatd_store_ctx_t *ctx,

View file

@ -22,11 +22,26 @@ typedef struct {
amduat_asl_store_index_fs_t index_fs; amduat_asl_store_index_fs_t index_fs;
} amduatd_store_ctx_t; } 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, bool amduatd_store_backend_parse(const char *value,
amduatd_store_backend_t *out_backend); amduatd_store_backend_t *out_backend);
const char *amduatd_store_backend_name(amduatd_store_backend_t 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, bool amduatd_store_init(amduat_asl_store_t *store,
amduat_asl_store_fs_config_t *cfg, amduat_asl_store_fs_config_t *cfg,
amduatd_store_ctx_t *ctx, amduatd_store_ctx_t *ctx,

View file

@ -375,6 +375,13 @@ static int amduatd_test_workspace_snapshot(void) {
"store backend present"); "store backend present");
expect(strstr(workspace_json, "\"transport\":\"stub\"") != NULL, expect(strstr(workspace_json, "\"transport\":\"stub\"") != NULL,
"federation transport present"); "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);