feat(runtime-api): add read-only snapshot listing
Harvest the safe GUI-facing snapshot list slice from PR #2808 without exposing restore, retry, patch-undo, or other runtime mutation endpoints. The endpoint is protected by the existing runtime API token middleware and mirrors the /restore list bound. Refs #2808, #2580. Co-authored-by: gaord <9567937+gaord@users.noreply.github.com>
This commit is contained in:
+9
-6
@@ -47,12 +47,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
remain deferred until the runtime semantics are safe (#2668).
|
||||
- Added an official VS Code extension Phase 0 scaffold with terminal launch,
|
||||
local runtime attach checks, status bar state, and a read-only Agent View
|
||||
preview backed by recent runtime thread summaries. This answers the VS Code
|
||||
GUI lane without exposing chat webviews, inline edits, or retry/undo runtime
|
||||
endpoints yet (#461, #462, #480, #1584, #2580). Thanks @AiurArtanis for the
|
||||
Agent View prompt, @lbcheng888 for the earlier scaffold, and @BigBenLabs,
|
||||
@lzx1545642258, @yangdaowan, @mangdehuang, @VerrPower, @hejia-v,
|
||||
@nasus9527, and @ygzhang-cn for the GUI/VS Code demand and validation trail.
|
||||
preview backed by recent runtime thread summaries, plus a read-only
|
||||
`GET /v1/snapshots` endpoint for GUI clients to inspect side-git restore
|
||||
points. This answers the VS Code GUI lane without exposing chat webviews,
|
||||
inline edits, or retry/undo/restore runtime mutation endpoints yet (#461,
|
||||
#462, #480, #1584, #2580, #2808). Thanks @AiurArtanis for the Agent View
|
||||
prompt, @lbcheng888 for the earlier scaffold, @gaord for the GUI runtime API
|
||||
direction, and @BigBenLabs, @lzx1545642258, @yangdaowan, @mangdehuang,
|
||||
@VerrPower, @hejia-v, @nasus9527, and @ygzhang-cn for the GUI/VS Code demand
|
||||
and validation trail.
|
||||
- Added a static prompt composer override for embedders that need to replace
|
||||
the byte-stable base/personality prompt segment while leaving mode metadata,
|
||||
approval policy, tool taxonomy, Context Management, and the Compaction Relay
|
||||
|
||||
@@ -47,12 +47,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
remain deferred until the runtime semantics are safe (#2668).
|
||||
- Added an official VS Code extension Phase 0 scaffold with terminal launch,
|
||||
local runtime attach checks, status bar state, and a read-only Agent View
|
||||
preview backed by recent runtime thread summaries. This answers the VS Code
|
||||
GUI lane without exposing chat webviews, inline edits, or retry/undo runtime
|
||||
endpoints yet (#461, #462, #480, #1584, #2580). Thanks @AiurArtanis for the
|
||||
Agent View prompt, @lbcheng888 for the earlier scaffold, and @BigBenLabs,
|
||||
@lzx1545642258, @yangdaowan, @mangdehuang, @VerrPower, @hejia-v,
|
||||
@nasus9527, and @ygzhang-cn for the GUI/VS Code demand and validation trail.
|
||||
preview backed by recent runtime thread summaries, plus a read-only
|
||||
`GET /v1/snapshots` endpoint for GUI clients to inspect side-git restore
|
||||
points. This answers the VS Code GUI lane without exposing chat webviews,
|
||||
inline edits, or retry/undo/restore runtime mutation endpoints yet (#461,
|
||||
#462, #480, #1584, #2580, #2808). Thanks @AiurArtanis for the Agent View
|
||||
prompt, @lbcheng888 for the earlier scaffold, @gaord for the GUI runtime API
|
||||
direction, and @BigBenLabs, @lzx1545642258, @yangdaowan, @mangdehuang,
|
||||
@VerrPower, @hejia-v, @nasus9527, and @ygzhang-cn for the GUI/VS Code demand
|
||||
and validation trail.
|
||||
- Added a static prompt composer override for embedders that need to replace
|
||||
the byte-stable base/personality prompt segment while leaving mode metadata,
|
||||
approval policy, tool taxonomy, Context Management, and the Compaction Relay
|
||||
|
||||
@@ -586,6 +586,7 @@ pub fn build_router(state: RuntimeApiState) -> Router {
|
||||
.route("/v1/automations/{id}/resume", post(resume_automation))
|
||||
.route("/v1/automations/{id}/runs", get(list_automation_runs))
|
||||
.route("/v1/usage", get(get_usage))
|
||||
.route("/v1/snapshots", get(list_snapshots))
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_runtime_token,
|
||||
@@ -2178,6 +2179,59 @@ async fn get_usage(
|
||||
Ok(Json(json!(aggregation)))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SnapshotsQuery {
|
||||
/// Maximum number of snapshots to return. Mirrors `/restore list [N]`.
|
||||
limit: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SnapshotEntry {
|
||||
id: String,
|
||||
label: String,
|
||||
timestamp: i64,
|
||||
}
|
||||
|
||||
async fn list_snapshots(
|
||||
State(state): State<RuntimeApiState>,
|
||||
Query(query): Query<SnapshotsQuery>,
|
||||
) -> Result<Json<Vec<SnapshotEntry>>, ApiError> {
|
||||
Ok(Json(snapshot_entries_for_workspace(
|
||||
&state.workspace,
|
||||
query,
|
||||
)?))
|
||||
}
|
||||
|
||||
fn snapshot_entries_for_workspace(
|
||||
workspace: &FsPath,
|
||||
query: SnapshotsQuery,
|
||||
) -> Result<Vec<SnapshotEntry>, ApiError> {
|
||||
const DEFAULT_LIMIT: usize = 20;
|
||||
const MAX_LIMIT: usize = 100;
|
||||
|
||||
let limit = match query.limit.unwrap_or(DEFAULT_LIMIT) {
|
||||
1..=MAX_LIMIT => query.limit.unwrap_or(DEFAULT_LIMIT),
|
||||
other => {
|
||||
return Err(ApiError::bad_request(format!(
|
||||
"limit must be between 1 and {MAX_LIMIT}; got {other}",
|
||||
)));
|
||||
}
|
||||
};
|
||||
let repo = crate::snapshot::SnapshotRepo::open_or_init(workspace)
|
||||
.map_err(|e| ApiError::internal(format!("Snapshot repo unavailable: {e}")))?;
|
||||
let snapshots = repo
|
||||
.list(limit)
|
||||
.map_err(|e| ApiError::internal(format!("Failed to list snapshots: {e}")))?;
|
||||
Ok(snapshots
|
||||
.into_iter()
|
||||
.map(|snapshot| SnapshotEntry {
|
||||
id: snapshot.id.as_str().to_string(),
|
||||
label: snapshot.label,
|
||||
timestamp: snapshot.timestamp,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
const MOBILE_HTML: &str = include_str!("runtime_mobile.html");
|
||||
|
||||
/// Built-in dev origins always allowed by the runtime API (whalescale#255).
|
||||
@@ -2307,6 +2361,7 @@ mod tests {
|
||||
use crate::core::ops::Op;
|
||||
use crate::models::Usage;
|
||||
use crate::runtime_threads::RuntimeEventRecord;
|
||||
use crate::test_support::{EnvVarGuard, lock_test_env};
|
||||
use anyhow::{Context, bail};
|
||||
use futures_util::StreamExt;
|
||||
use std::fs;
|
||||
@@ -4075,6 +4130,37 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshots_endpoint_lists_workspace_snapshots() -> Result<()> {
|
||||
let _lock = lock_test_env();
|
||||
let root = tempfile::tempdir()?;
|
||||
let home = root.path().join("home");
|
||||
fs::create_dir_all(&home)?;
|
||||
let _home = EnvVarGuard::set("HOME", &home);
|
||||
|
||||
let workspace = root.path().join("workspace");
|
||||
fs::create_dir_all(&workspace)?;
|
||||
let repo = crate::snapshot::SnapshotRepo::open_or_init(&workspace)?;
|
||||
fs::write(workspace.join("a.txt"), "v1")?;
|
||||
repo.snapshot("pre-turn:1")?;
|
||||
fs::write(workspace.join("a.txt"), "v2")?;
|
||||
repo.snapshot("post-turn:1")?;
|
||||
|
||||
let snapshots =
|
||||
snapshot_entries_for_workspace(&workspace, SnapshotsQuery { limit: Some(1) })
|
||||
.expect("snapshot listing should succeed");
|
||||
assert_eq!(snapshots.len(), 1);
|
||||
assert_eq!(snapshots[0].label, "post-turn:1");
|
||||
assert!(snapshots[0].id.len() >= 8);
|
||||
assert!(snapshots[0].timestamp > 0);
|
||||
|
||||
let bad_limit =
|
||||
snapshot_entries_for_workspace(&workspace, SnapshotsQuery { limit: Some(101) })
|
||||
.expect_err("limit above cap should fail");
|
||||
assert_eq!(bad_limit.status, StatusCode::BAD_REQUEST);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_delete_returns_404_for_missing_id() -> Result<()> {
|
||||
let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else {
|
||||
|
||||
Reference in New Issue
Block a user