feat: migrate workspace configuration domains to ServerApi

This commit is contained in:
2026-09-23 04:27:52 +09:00
parent c8766e9fb9
commit 6cbc3a6988
34 changed files with 10679 additions and 454 deletions
Generated
+1
View File
@@ -4149,6 +4149,7 @@ name = "server-api"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"api-macros", "api-macros",
"memory",
"protocol", "protocol",
"runtime-api", "runtime-api",
"schemars", "schemars",
+39
View File
@@ -128,6 +128,45 @@ fn server_api_client(
.map_err(|error| BackendWorkspaceClientError::InvalidTarget(error.to_string())) .map_err(|error| BackendWorkspaceClientError::InvalidTarget(error.to_string()))
} }
pub(crate) fn memory_document_blocking(
backend: &BackendApiClient,
workspace_id: &str,
) -> Result<server_api::MemoryDocumentResponse, BackendWorkspaceClientError> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|error| BackendWorkspaceClientError::InvalidTarget(error.to_string()))?;
runtime.block_on(async {
server_api_client(backend)?
.memory_document(workspace_id.to_owned())
.await
.map_err(|error| server_client_error(backend, error))
})
}
pub(crate) fn memory_staging_list_blocking(
backend: &BackendApiClient,
workspace_id: &str,
limit: usize,
) -> Result<server_api::MemoryStagingListResponse, BackendWorkspaceClientError> {
let limit = u32::try_from(limit).map_err(|_| {
BackendWorkspaceClientError::InvalidTarget("Memory staging limit exceeds u32".to_string())
})?;
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|error| BackendWorkspaceClientError::InvalidTarget(error.to_string()))?;
runtime.block_on(async {
server_api_client(backend)?
.memory_staging_list(
workspace_id.to_owned(),
server_api::MemoryStagingQuery { limit: Some(limit) },
)
.await
.map_err(|error| server_client_error(backend, error))
})
}
pub fn list_backend_workspaces_blocking( pub fn list_backend_workspaces_blocking(
target: &BackendWorkspaceCatalogTarget, target: &BackendWorkspaceCatalogTarget,
) -> Result<Vec<BackendWorkspace>, BackendWorkspaceClientError> { ) -> Result<Vec<BackendWorkspace>, BackendWorkspaceClientError> {
+12 -3
View File
@@ -310,14 +310,14 @@ impl BackendWorkspaceProductClient {
} }
pub fn memory_document(&self) -> Result<MemoryDocumentResponse, BackendWorkspaceClientError> { pub fn memory_document(&self) -> Result<MemoryDocumentResponse, BackendWorkspaceClientError> {
self.get_json("/memory") crate::backend_workspace::memory_document_blocking(&self.api, &self.workspace_id)
} }
pub fn list_memory_staging( pub fn list_memory_staging(
&self, &self,
limit: usize, limit: usize,
) -> Result<MemoryStagingListResponse, BackendWorkspaceClientError> { ) -> Result<MemoryStagingListResponse, BackendWorkspaceClientError> {
self.get_json(&format!("/memory/staging?limit={limit}")) crate::backend_workspace::memory_staging_list_blocking(&self.api, &self.workspace_id, limit)
} }
pub fn launch_ticket_intake( pub fn launch_ticket_intake(
@@ -813,7 +813,16 @@ mod tests {
let error = client.list_memory_staging(10).unwrap_err(); let error = client.list_memory_staging(10).unwrap_err();
assert!(matches!(error, BackendWorkspaceClientError::Http(_))); assert!(matches!(
error,
BackendWorkspaceClientError::ServerApi(
server_api::client_support::ClientError::Failure(
server_api::client_support::ClientFailure::Decode {
kind: server_api::client_support::DecodeKind::Success,
},
),
)
));
assert!( assert!(
request request
.recv() .recv()
+38 -15
View File
@@ -4,6 +4,7 @@
//! backend operation boundary. Repository-local audit logs are not an authority. //! backend operation boundary. Repository-local audit logs are not an authority.
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
@@ -11,7 +12,7 @@ fn is_zero_usize(value: &usize) -> bool {
*value == 0 *value == 0
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum AuditWorker { pub enum AuditWorker {
MemoryExtract, MemoryExtract,
@@ -27,7 +28,7 @@ impl AuditWorker {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum WorkerLifecycleStatus { pub enum WorkerLifecycleStatus {
Started, Started,
@@ -49,7 +50,7 @@ impl WorkerLifecycleStatus {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum AuditTrigger { pub enum AuditTrigger {
SessionEnd, SessionEnd,
@@ -77,7 +78,7 @@ impl AuditTrigger {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum AuditStatus { pub enum AuditStatus {
Success, Success,
@@ -85,7 +86,7 @@ pub enum AuditStatus {
Skipped, Skipped,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ModelAudit { pub struct ModelAudit {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub ref_: Option<String>, pub ref_: Option<String>,
@@ -95,31 +96,39 @@ pub struct ModelAudit {
pub model_id: Option<String>, pub model_id: Option<String>,
} }
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct UsageAudit { pub struct UsageAudit {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub input_tokens: Option<u64>, pub input_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub output_tokens: Option<u64>, pub output_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub total_tokens: Option<u64>, pub total_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub cache_read_input_tokens: Option<u64>, pub cache_read_input_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub cache_creation_input_tokens: Option<u64>, pub cache_creation_input_tokens: Option<u64>,
} }
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ExtractAudit { pub struct ExtractAudit {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>, pub session_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub segment_id: Option<String>, pub segment_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<crate::schema::JsonSafeU64Pair>")]
pub entry_range: Option<[u64; 2]>, pub entry_range: Option<[u64; 2]>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<crate::schema::JsonSafeU64Pair>")]
pub history_range: Option<[u64; 2]>, pub history_range: Option<[u64; 2]>,
#[serde(default)] #[serde(default)]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub staging_count: usize, pub staging_count: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub staging_ids: Vec<String>, pub staging_ids: Vec<String>,
@@ -127,13 +136,16 @@ pub struct ExtractAudit {
pub staging_paths: Vec<String>, pub staging_paths: Vec<String>,
} }
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ConsolidationAudit { pub struct ConsolidationAudit {
#[serde(default)] #[serde(default)]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub staging_count: usize, pub staging_count: usize,
#[serde(default, skip_serializing_if = "is_zero_usize")] #[serde(default, skip_serializing_if = "is_zero_usize")]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub invalid_staging_count: usize, pub invalid_staging_count: usize,
#[serde(default)] #[serde(default)]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub staging_bytes: u64, pub staging_bytes: u64,
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub consumed_staging_ids: Vec<String>, pub consumed_staging_ids: Vec<String>,
@@ -141,19 +153,25 @@ pub struct ConsolidationAudit {
pub operations: OperationCounts, pub operations: OperationCounts,
} }
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct OperationCounts { pub struct OperationCounts {
#[serde(default)] #[serde(default)]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub write: usize, pub write: usize,
#[serde(default)] #[serde(default)]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub edit: usize, pub edit: usize,
#[serde(default)] #[serde(default)]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub delete: usize, pub delete: usize,
#[serde(default)] #[serde(default)]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub drop: usize, pub drop: usize,
#[serde(default)] #[serde(default)]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub merge: usize, pub merge: usize,
#[serde(default)] #[serde(default)]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub trim: usize, pub trim: usize,
} }
@@ -163,15 +181,17 @@ impl OperationCounts {
} }
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct MemorySettingsAudit { pub struct MemorySettingsAudit {
pub workspace_id: String, pub workspace_id: String,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub settings_revision: u64, pub settings_revision: u64,
pub language: String, pub language: String,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct WorkerLifecycleAudit { pub struct WorkerLifecycleAudit {
#[schemars(with = "String")]
pub run_id: Uuid, pub run_id: Uuid,
pub worker: AuditWorker, pub worker: AuditWorker,
pub status: WorkerLifecycleStatus, pub status: WorkerLifecycleStatus,
@@ -189,7 +209,7 @@ pub struct WorkerLifecycleAudit {
pub consolidation: Option<ConsolidationAudit>, pub consolidation: Option<ConsolidationAudit>,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct RecordOperationAudit { pub struct RecordOperationAudit {
pub op: String, pub op: String,
pub status: AuditStatus, pub status: AuditStatus,
@@ -204,7 +224,7 @@ pub struct RecordOperationAudit {
pub reason: Option<String>, pub reason: Option<String>,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct RecordUsageAudit { pub struct RecordUsageAudit {
pub op: String, pub op: String,
pub status: AuditStatus, pub status: AuditStatus,
@@ -216,12 +236,13 @@ pub struct RecordUsageAudit {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub query: Option<String>, pub query: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub result_count: Option<usize>, pub result_count: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>, pub reason: Option<String>,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "event", rename_all = "snake_case")] #[serde(tag = "event", rename_all = "snake_case")]
pub enum AuditPayload { pub enum AuditPayload {
WorkerLifecycle(WorkerLifecycleAudit), WorkerLifecycle(WorkerLifecycleAudit),
@@ -229,9 +250,11 @@ pub enum AuditPayload {
RecordUsage(RecordUsageAudit), RecordUsage(RecordUsageAudit),
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct AuditEvent { pub struct AuditEvent {
#[schemars(with = "String")]
pub id: Uuid, pub id: Uuid,
#[schemars(with = "String")]
pub occurred_at: DateTime<Utc>, pub occurred_at: DateTime<Utc>,
#[serde(flatten)] #[serde(flatten)]
pub payload: AuditPayload, pub payload: AuditPayload,
+19 -13
View File
@@ -11,7 +11,7 @@ use crate::audit::AuditEvent;
use crate::extract::{ExtractedCandidate, ExtractedPayload, StagingEvidence}; use crate::extract::{ExtractedCandidate, ExtractedPayload, StagingEvidence};
use crate::schema::{SourceEvidenceRef, SourceRef}; use crate::schema::{SourceEvidenceRef, SourceRef};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "operation", rename_all = "snake_case")] #[serde(tag = "operation", rename_all = "snake_case")]
pub enum MemoryBackendOperation { pub enum MemoryBackendOperation {
Query(MemoryQueryOperation), Query(MemoryQueryOperation),
@@ -26,7 +26,7 @@ pub enum MemoryBackendOperation {
StagingClose(MemoryStagingCloseOperation), StagingClose(MemoryStagingCloseOperation),
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "status", rename_all = "snake_case")] #[serde(tag = "status", rename_all = "snake_case")]
pub enum MemoryBackendHttpResponse { pub enum MemoryBackendHttpResponse {
Ok { Ok {
@@ -37,7 +37,7 @@ pub enum MemoryBackendHttpResponse {
}, },
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")] #[serde(tag = "kind", rename_all = "snake_case")]
pub enum MemoryBackendOperationResult { pub enum MemoryBackendOperationResult {
ToolOutput(MemoryToolOutput), ToolOutput(MemoryToolOutput),
@@ -45,14 +45,14 @@ pub enum MemoryBackendOperationResult {
StagingWritten(MemoryStagingWriteOutput), StagingWritten(MemoryStagingWriteOutput),
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MemoryToolOutput { pub struct MemoryToolOutput {
pub summary: String, pub summary: String,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<String>, pub content: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MemoryQueryOperation { pub struct MemoryQueryOperation {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub query: Option<String>, pub query: Option<String>,
@@ -61,8 +61,10 @@ pub struct MemoryQueryOperation {
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MemoryDocumentReadOperation { pub struct MemoryDocumentReadOperation {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub offset: Option<usize>, pub offset: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub limit: Option<usize>, pub limit: Option<usize>,
} }
@@ -74,15 +76,15 @@ pub struct MemoryDocumentUpdateOperation {
pub replace_all: bool, pub replace_all: bool,
} }
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct MemoryResidentSummaryOperation {} pub struct MemoryResidentSummaryOperation {}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MemoryAppendAuditOperation { pub struct MemoryAppendAuditOperation {
pub event: AuditEvent, pub event: AuditEvent,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MemoryStageCandidateOperation { pub struct MemoryStageCandidateOperation {
pub source: SourceRef, pub source: SourceRef,
pub extract_run_id: String, pub extract_run_id: String,
@@ -93,7 +95,7 @@ pub struct MemoryStageCandidateOperation {
pub source_refs: Vec<SourceEvidenceRef>, pub source_refs: Vec<SourceEvidenceRef>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MemoryStageExtractedOperation { pub struct MemoryStageExtractedOperation {
pub source: SourceRef, pub source: SourceRef,
pub payload: ExtractedPayload, pub payload: ExtractedPayload,
@@ -102,6 +104,7 @@ pub struct MemoryStageExtractedOperation {
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MemoryStagingListOperation { pub struct MemoryStagingListOperation {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub limit: Option<usize>, pub limit: Option<usize>,
} }
@@ -140,28 +143,31 @@ pub enum MemoryStagingAffectedMemoryOperation {
Edit, Edit,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct MemoryConsolidateStagingOperation { pub struct MemoryConsolidateStagingOperation {
#[serde(default)] #[serde(default)]
pub force: bool, pub force: bool,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MemoryConsolidationOutput { pub struct MemoryConsolidationOutput {
pub status: String, pub status: String,
pub summary: String, pub summary: String,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub candidate_count: usize, pub candidate_count: usize,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub total_bytes: u64, pub total_bytes: u64,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MemoryBackendAckOutput { pub struct MemoryBackendAckOutput {
pub summary: String, pub summary: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MemoryStagingWriteOutput { pub struct MemoryStagingWriteOutput {
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub staging_count: usize, pub staging_count: usize,
pub staging_ids: Vec<String>, pub staging_ids: Vec<String>,
} }
+2 -1
View File
@@ -73,12 +73,13 @@ impl ExtractedPayload {
} }
/// Bounded evidence snippet copied into a flat staging record. /// Bounded evidence snippet copied into a flat staging record.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct StagingEvidence { pub struct StagingEvidence {
pub id: String, pub id: String,
pub kind: EvidenceKind, pub kind: EvidenceKind,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<crate::schema::JsonSafeU64Pair>")]
pub entry_range: Option<[u64; 2]>, pub entry_range: Option<[u64; 2]>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub origin: Option<EvidenceOrigin>, pub origin: Option<EvidenceOrigin>,
+12 -1
View File
@@ -4,14 +4,23 @@ use lint_common::RecordLintError;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Schema-only representation for JSON integers that must remain exactly
/// representable by JavaScript consumers.
#[allow(dead_code)]
#[derive(JsonSchema)]
pub struct MemoryJsonSafeU64(#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))] u64);
pub type JsonSafeU64Pair = [MemoryJsonSafeU64; 2];
pub use lint_common::Frontmatter; pub use lint_common::Frontmatter;
/// Reference to a session-store entry range. Stored in `sources` / /// Reference to a session-store entry range. Stored in `sources` /
/// `last_sources` arrays for traceability back to raw session logs. /// `last_sources` arrays for traceability back to raw session logs.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
pub struct SourceRef { pub struct SourceRef {
pub segment_id: String, pub segment_id: String,
/// `[start_entry, end_entry]` inclusive range of session-store entry indices. /// `[start_entry, end_entry]` inclusive range of session-store entry indices.
#[schemars(with = "crate::schema::JsonSafeU64Pair")]
pub range: [u64; 2], pub range: [u64; 2],
} }
@@ -99,6 +108,7 @@ pub struct EvidenceOrigin {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub flow_definition_id: Option<String>, pub flow_definition_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub flow_definition_revision: Option<u64>, pub flow_definition_revision: Option<u64>,
} }
@@ -118,6 +128,7 @@ pub struct SourceEvidenceRef {
pub segment_id: Option<String>, pub segment_id: Option<String>,
/// `[start_entry, end_entry]` inclusive range of session-store entry indices. /// `[start_entry, end_entry]` inclusive range of session-store entry indices.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<crate::schema::JsonSafeU64Pair>")]
pub entry_range: Option<[u64; 2]>, pub entry_range: Option<[u64; 2]>,
/// Host-assigned evidence id within the referenced evidence set. /// Host-assigned evidence id within the referenced evidence set.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
+1
View File
@@ -8,6 +8,7 @@ mod decision;
mod request; mod request;
mod summary; mod summary;
pub(crate) use common::JsonSafeU64Pair;
pub use common::{ pub use common::{
EvidenceKind, EvidenceOrigin, EvidenceOriginKind, Frontmatter, SourceEvidenceRef, SourceRef, EvidenceKind, EvidenceOrigin, EvidenceOriginKind, Frontmatter, SourceEvidenceRef, SourceRef,
split_frontmatter, split_frontmatter,
+1
View File
@@ -11,6 +11,7 @@ typescript = ["dep:ts-rs", "protocol/typescript"]
[dependencies] [dependencies]
api-macros = { workspace = true, features = ["reqwest", "axum", "openapi"] } api-macros = { workspace = true, features = ["reqwest", "axum", "openapi"] }
memory.workspace = true
protocol.workspace = true protocol.workspace = true
runtime-api.workspace = true runtime-api.workspace = true
schemars.workspace = true schemars.workspace = true
File diff suppressed because it is too large Load Diff
+14 -3
View File
@@ -532,18 +532,29 @@ impl WorkspaceClient for RuntimeOwnedWorkspaceClient {
response.status, response.body response.status, response.body
))); )));
} }
let projection: WorkspacePromptProjection = let wire_projection: server_api::WorkspacePromptProjection =
serde_json::from_str(&response.body).map_err(|error| { serde_json::from_str(&response.body).map_err(|error| {
WorkspaceClientError::Request(format!( WorkspaceClientError::Request(format!(
"invalid active Workspace Prompt projection response: {error}" "invalid active Workspace Prompt projection response: {error}"
)) ))
})?; })?;
if projection.workspace_id != self.workspace_id { if wire_projection.workspace_id != self.workspace_id {
return Err(WorkspaceClientError::Request(format!( return Err(WorkspaceClientError::Request(format!(
"active Workspace Prompt projection scope mismatch: expected {}, got {}", "active Workspace Prompt projection scope mismatch: expected {}, got {}",
self.workspace_id, projection.workspace_id self.workspace_id, wire_projection.workspace_id
))); )));
} }
let projection: WorkspacePromptProjection =
serde_json::from_value(serde_json::to_value(wire_projection).map_err(|error| {
WorkspaceClientError::Request(format!(
"project active Workspace Prompt projection response: {error}"
))
})?)
.map_err(|error| {
WorkspaceClientError::Request(format!(
"convert active Workspace Prompt projection response: {error}"
))
})?;
let resolution = cache let resolution = cache
.observe(projection) .observe(projection)
.map_err(WorkspaceClientError::Request)?; .map_err(WorkspaceClientError::Request)?;
+12 -4
View File
@@ -104,7 +104,7 @@ async fn execute_memory_backend(
let response = client.execute(WorkspaceRequest::json( let response = client.execute(WorkspaceRequest::json(
WorkspaceRequestMethod::Post, WorkspaceRequestMethod::Post,
format!("/api/w/{workspace_id}/memory/backend"), format!("/api/w/{workspace_id}/memory/backend"),
serde_json::to_string(&operation)?, serde_json::to_string(&server_api::MemoryBackendRequest(operation))?,
))?; ))?;
let status = reqwest::StatusCode::from_u16(response.status) let status = reqwest::StatusCode::from_u16(response.status)
.unwrap_or(reqwest::StatusCode::INTERNAL_SERVER_ERROR); .unwrap_or(reqwest::StatusCode::INTERNAL_SERVER_ERROR);
@@ -114,7 +114,7 @@ async fn execute_memory_backend(
body: response.body, body: response.body,
}); });
} }
match serde_json::from_str::<MemoryBackendHttpResponse>(&response.body)? { match serde_json::from_str::<server_api::MemoryBackendResponse>(&response.body)?.0 {
MemoryBackendHttpResponse::Ok { result } => Ok(result), MemoryBackendHttpResponse::Ok { result } => Ok(result),
MemoryBackendHttpResponse::Error { message } => { MemoryBackendHttpResponse::Error { message } => {
Err(WorkspaceMemoryBackendError::Backend(message)) Err(WorkspaceMemoryBackendError::Backend(message))
@@ -138,7 +138,9 @@ async fn execute_memory_consolidation(
let response = client.execute(WorkspaceRequest::json( let response = client.execute(WorkspaceRequest::json(
WorkspaceRequestMethod::Post, WorkspaceRequestMethod::Post,
format!("/api/w/{workspace_id}/memory/consolidation"), format!("/api/w/{workspace_id}/memory/consolidation"),
serde_json::to_string(&operation)?, serde_json::to_string(&server_api::MemoryConsolidateStagingRequest {
force: operation.force,
})?,
))?; ))?;
let status = reqwest::StatusCode::from_u16(response.status) let status = reqwest::StatusCode::from_u16(response.status)
.unwrap_or(reqwest::StatusCode::INTERNAL_SERVER_ERROR); .unwrap_or(reqwest::StatusCode::INTERNAL_SERVER_ERROR);
@@ -148,7 +150,13 @@ async fn execute_memory_consolidation(
body: response.body, body: response.body,
}); });
} }
serde_json::from_str::<MemoryConsolidationOutput>(&response.body).map_err(Into::into) let response = serde_json::from_str::<server_api::MemoryConsolidationResponse>(&response.body)?;
Ok(MemoryConsolidationOutput {
status: response.status,
summary: response.summary,
candidate_count: response.candidate_count,
total_bytes: response.total_bytes,
})
} }
pub fn workspace_http_memory_tools(client: Arc<dyn WorkspaceClient>) -> Vec<ToolDefinition> { pub fn workspace_http_memory_tools(client: Arc<dyn WorkspaceClient>) -> Vec<ToolDefinition> {
+4 -14
View File
@@ -1,23 +1,13 @@
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use crate::worker::WorkspaceClient; use crate::worker::WorkspaceClient;
pub use server_api::{ pub use server_api::{
SkillActivationStatus, SkillCatalogEntry, SkillCatalogResponse, SkillDetailResponse, SkillActivationResponse, SkillActivationStatus, SkillCatalogEntry, SkillCatalogResponse,
SkillDiagnostic, SkillDiagnosticSeverity, SkillProjectionIdentity, SkillProjectionStatus, SkillDetailResponse, SkillDiagnostic, SkillDiagnosticSeverity, SkillProjectionIdentity,
SkillProvenance, SkillResourceRef, SkillSourceKind, SkillProjectionStatus, SkillProvenance, SkillResourceRef, SkillSourceKind,
}; };
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SkillActivationResponse {
pub name: String,
pub provenance: SkillProvenance,
#[serde(default)]
pub diagnostics: Vec<SkillDiagnostic>,
/// Imported Markdown content to append to Worker history on explicit activation.
pub body: String,
}
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum SkillClientError { pub enum SkillClientError {
#[error("workspace client is unavailable: {0}")] #[error("workspace client is unavailable: {0}")]
+10 -4
View File
@@ -3889,8 +3889,8 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
.ok_or_else(|| { .ok_or_else(|| {
WorkerError::FlowInput("Workspace client has no Workspace scope".to_string()) WorkerError::FlowInput("Workspace client has no Workspace scope".to_string())
})?; })?;
let request = flow::FlowSourceResolveRequest { let request = server_api::FlowSourceResolveRequest {
selector: selector.clone(), selector: selector.to_string(),
}; };
let response = workspace let response = workspace
.execute(WorkspaceRequest { .execute(WorkspaceRequest {
@@ -3915,13 +3915,19 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
response.status response.status
))); )));
} }
let source: flow::ResolvedFlowSource = serde_json::from_str(&response.body) let wire_source: server_api::ResolvedFlowSource = serde_json::from_str(&response.body)
.map_err(|error| WorkerError::FlowInput(format!("decode Flow source: {error}")))?; .map_err(|error| WorkerError::FlowInput(format!("decode Flow source: {error}")))?;
if source.workspace_id != workspace_id || source.selector != selector { if wire_source.workspace_id != workspace_id || wire_source.selector != selector.to_string()
{
return Err(WorkerError::FlowInput( return Err(WorkerError::FlowInput(
"Workspace returned a Flow source outside the requested scope".to_string(), "Workspace returned a Flow source outside the requested scope".to_string(),
)); ));
} }
let source: flow::ResolvedFlowSource =
serde_json::from_value(serde_json::to_value(wire_source).map_err(|error| {
WorkerError::FlowInput(format!("project Flow source transport: {error}"))
})?)
.map_err(|error| WorkerError::FlowInput(format!("convert Flow source: {error}")))?;
let (state, initial_instructions) = let (state, initial_instructions) =
flow::FlowRuntimeState::start(&source, uuid::Uuid::now_v7().to_string()) flow::FlowRuntimeState::start(&source, uuid::Uuid::now_v7().to_string())
.map_err(|error| WorkerError::FlowInput(error.to_string()))?; .map_err(|error| WorkerError::FlowInput(error.to_string()))?;
File diff suppressed because it is too large Load Diff
+7243 -1
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -6,7 +6,7 @@
"dev": "deno run -A npm:vite@7.2.7 dev", "dev": "deno run -A npm:vite@7.2.7 dev",
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787", "dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
"check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json", "check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json",
"test": "deno test --allow-read=src,test,tests,../../docs --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts", "test": "deno test --allow-read=src,test,tests,../../docs --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/config-source/api.test.ts tests/memory-api.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts",
"test:component": "deno run -A npm:vitest@3.2.4 run --config vitest.config.ts", "test:component": "deno run -A npm:vitest@3.2.4 run --config vitest.config.ts",
"build": "deno run -A npm:vite@7.2.7 build", "build": "deno run -A npm:vite@7.2.7 build",
"preview": "deno run -A npm:vite@7.2.7 preview", "preview": "deno run -A npm:vite@7.2.7 preview",
@@ -1,140 +1,565 @@
// This file is generated by `cargo run -p server-api --features typescript --example generate_legacy_typescript | deno fmt -`. // This file is generated by `cargo run -p server-api --features typescript --example generate_legacy_typescript | deno fmt -`.
// Do not edit this file directly. // Do not edit this file directly.
import type { RepositoryObservedStatus, RepositorySource } from "./repository-api"; import type {
RepositoryObservedStatus,
RepositorySource,
} from "./repository-api";
export type WorkspaceSummary = { workspace_id: string, owner_account_id: string, display_name: string, state: string, created_at: string, updated_at: string, }; export type WorkspaceSummary = {
workspace_id: string;
owner_account_id: string;
display_name: string;
state: string;
created_at: string;
updated_at: string;
};
export type WorkspaceCatalogListResponse = Array<WorkspaceSummary>; export type WorkspaceCatalogListResponse = Array<WorkspaceSummary>;
export type WorkspaceRepositoryRecord = { workspace_id: string, repository_key: string, kind: string, provider: string | null, source: RepositorySource, default_ref: string | null, source_revision: number, source_fingerprint: string, observed_status: RepositoryObservedStatus, observed_at: string | null, created_at: string, updated_at: string, }; export type WorkspaceRepositoryRecord = {
workspace_id: string;
repository_key: string;
kind: string;
provider: string | null;
source: RepositorySource;
default_ref: string | null;
source_revision: number;
source_fingerprint: string;
observed_status: RepositoryObservedStatus;
observed_at: string | null;
created_at: string;
updated_at: string;
};
export type WorkspaceCreateResponse = { workspace: WorkspaceSummary, repository: WorkspaceRepositoryRecord, config_revision: number, request_fingerprint: string, replayed: boolean, }; export type WorkspaceCreateResponse = {
workspace: WorkspaceSummary;
repository: WorkspaceRepositoryRecord;
config_revision: number;
request_fingerprint: string;
replayed: boolean;
};
export type WorkspaceAuthConfig = { "Passkey": { rp_id: string, origin: string, public_base_url: string, cookie_name: string, } }; export type WorkspaceAuthConfig = {
"Passkey": {
rp_id: string;
origin: string;
public_base_url: string;
cookie_name: string;
};
};
export type WorkspacePermissionSummary = { manage_repositories: boolean, manage_secrets: boolean, manage_runtimes: boolean, delete_workspace: boolean, }; export type WorkspacePermissionSummary = {
manage_repositories: boolean;
manage_secrets: boolean;
manage_runtimes: boolean;
delete_workspace: boolean;
};
export type WorkspaceDeletionState = "queued" | "running" | "blocked" | "failed" | "succeeded"; export type WorkspaceDeletionState =
| "queued"
| "running"
| "blocked"
| "failed"
| "succeeded";
export type WorkspaceDeletionBlockerKind = "last_accessible_workspace" | "revision_conflict" | "dirty_workdir" | "worker_removal_blocked" | "workdir_removal_blocked" | "retention_hold" | "cleanup_unavailable"; export type WorkspaceDeletionBlockerKind =
| "last_accessible_workspace"
| "revision_conflict"
| "dirty_workdir"
| "worker_removal_blocked"
| "workdir_removal_blocked"
| "retention_hold"
| "cleanup_unavailable";
export type WorkspaceDeletionBlocker = { kind: WorkspaceDeletionBlockerKind, resource_kind: string | null, resource_key: string | null, message: string, }; export type WorkspaceDeletionBlocker = {
kind: WorkspaceDeletionBlockerKind;
resource_kind: string | null;
resource_key: string | null;
message: string;
};
export type WorkspaceDeletionResourceCounts = { workers: number, workdirs: number, repositories: number, runtime_bindings: number, secrets: number, artifacts: number, }; export type WorkspaceDeletionResourceCounts = {
workers: number;
workdirs: number;
repositories: number;
runtime_bindings: number;
secrets: number;
artifacts: number;
};
export type WorkspaceDeletionPreflightResponse = { workspace_id: string, display_name: string, export type WorkspaceDeletionPreflightResponse = {
/** workspace_id: string;
* Opaque persisted Workspace metadata revision used as a CAS fence. display_name: string;
*/ /**
expected_revision: string, can_delete: boolean, resources: WorkspaceDeletionResourceCounts, blockers: Array<WorkspaceDeletionBlocker>, }; * Opaque persisted Workspace metadata revision used as a CAS fence.
*/
expected_revision: string;
can_delete: boolean;
resources: WorkspaceDeletionResourceCounts;
blockers: Array<WorkspaceDeletionBlocker>;
};
export type WorkspaceDeletionRequest = { operation_id: string, expected_revision: string, confirmation: string, }; export type WorkspaceDeletionRequest = {
operation_id: string;
expected_revision: string;
confirmation: string;
};
export type WorkspaceDeletionOperationResponse = { operation_id: string, workspace_id: string, display_name: string, state: WorkspaceDeletionState, resources: WorkspaceDeletionResourceCounts, child_operation_ids: Array<string>, blockers: Array<WorkspaceDeletionBlocker>, failure_category: string | null, created_at: string, updated_at: string, completed_at: string | null, }; export type WorkspaceDeletionOperationResponse = {
operation_id: string;
workspace_id: string;
display_name: string;
state: WorkspaceDeletionState;
resources: WorkspaceDeletionResourceCounts;
child_operation_ids: Array<string>;
blockers: Array<WorkspaceDeletionBlocker>;
failure_category: string | null;
created_at: string;
updated_at: string;
completed_at: string | null;
};
export type DiagnosticSeverity = "info" | "warning" | "error"; export type DiagnosticSeverity = "info" | "warning" | "error";
export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, }; export type Diagnostic = {
code: string;
severity: DiagnosticSeverity;
message: string;
};
export type WorkspaceExtensionPointState = { status: string, note: string, diagnostics: Array<Diagnostic>, }; export type WorkspaceExtensionPointState = {
status: string;
note: string;
diagnostics: Array<Diagnostic>;
};
export type WorkspaceExtensionPoints = { store: string, event_stream: WorkspaceExtensionPointState, host_worker_bridge: WorkspaceExtensionPointState, companion_console: WorkspaceExtensionPointState, }; export type WorkspaceExtensionPoints = {
store: string;
event_stream: WorkspaceExtensionPointState;
host_worker_bridge: WorkspaceExtensionPointState;
companion_console: WorkspaceExtensionPointState;
};
export type WorkspaceResponse = { workspace_id: string, display_name: string, record_authority: string, schema_version: number, auth: WorkspaceAuthConfig, permissions: WorkspacePermissionSummary, extension_points: WorkspaceExtensionPoints, }; export type WorkspaceResponse = {
workspace_id: string;
display_name: string;
record_authority: string;
schema_version: number;
auth: WorkspaceAuthConfig;
permissions: WorkspacePermissionSummary;
extension_points: WorkspaceExtensionPoints;
};
export type WorkspaceMetadataSettingsResponse = { workspace_id: string, display_name: string, created_at: string, revision: string, source: string, diagnostics: Array<Diagnostic>, }; export type WorkspaceMetadataSettingsResponse = {
workspace_id: string;
display_name: string;
created_at: string;
revision: string;
source: string;
diagnostics: Array<Diagnostic>;
};
export type UpdateWorkspaceMetadataRequest = { display_name: string, revision: string, }; export type UpdateWorkspaceMetadataRequest = {
display_name: string;
revision: string;
};
export type WorkspaceMetadataMutationResponse = { workspace: WorkspaceMetadataSettingsResponse, diagnostics: Array<Diagnostic>, }; export type WorkspaceMetadataMutationResponse = {
workspace: WorkspaceMetadataSettingsResponse;
diagnostics: Array<Diagnostic>;
};
export type WorkspaceSigningIdentityState = "pending_provisioning" | "active"; export type WorkspaceSigningIdentityState = "pending_provisioning" | "active";
export type WorkspaceSigningIdentityPublic = { workspace_id: string, key_id: string, algorithm: string, public_key?: string, public_key_fingerprint?: string, revision: number, state: WorkspaceSigningIdentityState, created_at: string, provisioned_at?: string, }; export type WorkspaceSigningIdentityPublic = {
workspace_id: string;
key_id: string;
algorithm: string;
public_key?: string;
public_key_fingerprint?: string;
revision: number;
state: WorkspaceSigningIdentityState;
created_at: string;
provisioned_at?: string;
};
export type WorkspacePublicIdentityBundle = { workspace_id: string, backend_url: string, key_id: string, algorithm: string, public_key: string, public_key_fingerprint: string, revision: number, }; export type WorkspacePublicIdentityBundle = {
workspace_id: string;
backend_url: string;
key_id: string;
algorithm: string;
public_key: string;
public_key_fingerprint: string;
revision: number;
};
export type WorkspaceSigningIdentityResponse = { identity: WorkspaceSigningIdentityPublic, public_bundle?: WorkspacePublicIdentityBundle, }; export type WorkspaceSigningIdentityResponse = {
identity: WorkspaceSigningIdentityPublic;
public_bundle?: WorkspacePublicIdentityBundle;
};
export type ProfileSettingsResponse = { workspace_id: string, registry_revision: string, config_revision?: number | null, tree_digest?: string | null, projection_digest?: string | null, default_profile?: string | null, profiles: Array<WorkspaceProfileSummary>, sources: Array<WorkspaceProfileSourceSummary>, diagnostics: Array<Diagnostic>, }; export type ConfigContentType = "decodal" | "text";
export type WorkspaceProfileSummary = { profile_id: string, selector: string, label: string, source_kind: string, profile_source_id?: string | null, description?: string | null, editable: boolean, is_default: boolean, diagnostics: Array<Diagnostic>, }; export type ConfigEntry = {
path: string;
content_type: ConfigContentType;
content: string;
content_digest: string;
};
export type WorkspaceProfileSourceSummary = { profile_source_id: string, display_path: string, kind: string, content_type: string, content_digest: string, provenance: WorkspaceProfileSourceProvenance, editable: boolean, revision: string, size_bytes: number, diagnostics: Array<Diagnostic>, }; export type ConfigTreeSnapshot = {
revision: number;
digest: string;
entries: { [key in string]: ConfigEntry };
};
export type ConfigTreeChange =
| {
"kind": "create";
path: string;
content_type: ConfigContentType;
content: string;
}
| { "kind": "update"; path: string; expected_digest: string; content: string }
| { "kind": "rename"; from: string; to: string; expected_digest: string }
| { "kind": "delete"; path: string; expected_digest: string };
export type ConfigProjectionValidator = {
"kind": "static_template_catalog";
namespace: string;
key_aliases?: { [key in string]: string };
};
export type ConfigSchemaContribution = {
provider_id: string;
namespace: string;
version: string;
source: string;
projection_validator?: ConfigProjectionValidator;
source_digest: string;
};
export type WorkspaceConfigSchemaBundle = {
contributions: Array<ConfigSchemaContribution>;
source: string;
fingerprint: string;
};
export type ToolchainContract = {
contract_version: number;
decodal_version: string;
schema_version: number;
entrypoints: Array<string>;
import_policy_version: number;
schema_bundle: WorkspaceConfigSchemaBundle;
fingerprint: string;
};
export type WorkspaceConfigTreeResponse = {
snapshot: ConfigTreeSnapshot;
contract: ToolchainContract;
projection_digest: string;
};
export type ConfigCommitRequest = {
base_revision: number;
base_digest: string;
changes: Array<ConfigTreeChange>;
entrypoints: Array<string>;
};
export type ProfileSettingsResponse = {
workspace_id: string;
registry_revision: string;
config_revision?: number | null;
tree_digest?: string | null;
projection_digest?: string | null;
default_profile?: string | null;
profiles: Array<WorkspaceProfileSummary>;
sources: Array<WorkspaceProfileSourceSummary>;
diagnostics: Array<Diagnostic>;
};
export type WorkspaceProfileSummary = {
profile_id: string;
selector: string;
label: string;
source_kind: string;
profile_source_id?: string | null;
description?: string | null;
editable: boolean;
is_default: boolean;
diagnostics: Array<Diagnostic>;
};
export type WorkspaceProfileSourceSummary = {
profile_source_id: string;
display_path: string;
kind: string;
content_type: string;
content_digest: string;
provenance: WorkspaceProfileSourceProvenance;
editable: boolean;
revision: string;
size_bytes: number;
diagnostics: Array<Diagnostic>;
};
export type WorkspaceProfileSourceProvenance = "project_profile_source_tree"; export type WorkspaceProfileSourceProvenance = "project_profile_source_tree";
export type GitCommitSummary = { hash: string, short_hash: string, summary: string, author_name: string, author_email: string, author_date: string, parents: Array<string>, refs: Array<string>, }; export type GitCommitSummary = {
hash: string;
short_hash: string;
summary: string;
author_name: string;
author_email: string;
author_date: string;
parents: Array<string>;
refs: Array<string>;
};
export type RepositorySshConnectionProbeRequest = { runtime_id: string, }; export type RepositorySshConnectionProbeRequest = { runtime_id: string };
export type RepositorySshHostKeyCandidate = { algorithm: string, host_key: string, fingerprint: string, }; export type RepositorySshHostKeyCandidate = {
algorithm: string;
host_key: string;
fingerprint: string;
};
export type RepositorySshConnectionTrustState = "untrusted" | "verified" | "changed"; export type RepositorySshConnectionTrustState =
| "untrusted"
| "verified"
| "changed";
export type RepositorySshConnectionProbeResponse = { workspace_id: string, repository_key: string, runtime_id: string, hostname: string, port: number, trust_state: RepositorySshConnectionTrustState, host_trust_id: string, expected_host_trust_revision: number | null, candidates: Array<RepositorySshHostKeyCandidate>, }; export type RepositorySshConnectionProbeResponse = {
workspace_id: string;
repository_key: string;
runtime_id: string;
hostname: string;
port: number;
trust_state: RepositorySshConnectionTrustState;
host_trust_id: string;
expected_host_trust_revision: number | null;
candidates: Array<RepositorySshHostKeyCandidate>;
};
export type ConfirmRepositorySshHostTrustRequest = { operation_id: string, runtime_id: string, host_key: string, expected_host_trust_revision: number | null, }; export type ConfirmRepositorySshHostTrustRequest = {
operation_id: string;
runtime_id: string;
host_key: string;
expected_host_trust_revision: number | null;
};
export type RepositoryLogResponse = { workspace_id: string, repository_key: string, default_selector?: string | null, limit: number, items: Array<GitCommitSummary>, diagnostics: Array<Diagnostic>, }; export type RepositoryLogResponse = {
workspace_id: string;
repository_key: string;
default_selector?: string | null;
limit: number;
items: Array<GitCommitSummary>;
diagnostics: Array<Diagnostic>;
};
export type RuntimeSourceKind = "embedded_worker_runtime" | "remote_http"; export type RuntimeSourceKind = "embedded_worker_runtime" | "remote_http";
export type RuntimeSourceStatus = "active" | "reserved"; export type RuntimeSourceStatus = "active" | "reserved";
export type RuntimeIdentityAuthority = "runtime_registry_projection" | "server_runtime_configuration"; export type RuntimeIdentityAuthority =
| "runtime_registry_projection"
| "server_runtime_configuration";
export type RuntimeSourceSummary = { kind: RuntimeSourceKind, status: RuntimeSourceStatus, identity_authority: RuntimeIdentityAuthority, note: string, }; export type RuntimeSourceSummary = {
kind: RuntimeSourceKind;
status: RuntimeSourceStatus;
identity_authority: RuntimeIdentityAuthority;
note: string;
};
export type RuntimeSummary = { runtime_id: string, label: string, kind: string, status: string, source: RuntimeSourceSummary, host_ids: Array<string>, worker_creation_available: boolean, os: string, arch: string, diagnostics: Array<Diagnostic>, }; export type RuntimeSummary = {
runtime_id: string;
label: string;
kind: string;
status: string;
source: RuntimeSourceSummary;
host_ids: Array<string>;
worker_creation_available: boolean;
os: string;
arch: string;
diagnostics: Array<Diagnostic>;
};
export type WorkspaceRuntimeBindingState = "configured" | "verified" | "revoked"; export type WorkspaceRuntimeBindingState =
| "configured"
| "verified"
| "revoked";
export type RuntimeConnectionDisplayState = "configured" | "verified" | "unavailable" | "revoked"; export type RuntimeConnectionDisplayState =
| "configured"
| "verified"
| "unavailable"
| "revoked";
export type RuntimeVerificationOutcome = "verified" | "challenge_issued" | "verification_failed" | "connectivity_failed"; export type RuntimeVerificationOutcome =
| "verified"
| "challenge_issued"
| "verification_failed"
| "connectivity_failed";
export type RuntimeVerificationEvidenceSummary = { verified_at: string | null, last_checked_at: string, last_outcome: RuntimeVerificationOutcome, binding_revision: number, workspace_key_id: string, workspace_identity_revision: number, workspace_trust_generation: number, runtime_public_key_fingerprint: string, runtime_identity_revision: number, }; export type RuntimeVerificationEvidenceSummary = {
verified_at: string | null;
last_checked_at: string;
last_outcome: RuntimeVerificationOutcome;
binding_revision: number;
workspace_key_id: string;
workspace_identity_revision: number;
workspace_trust_generation: number;
runtime_public_key_fingerprint: string;
runtime_identity_revision: number;
};
export type WorkspaceRuntimeBindingSummary = { state: WorkspaceRuntimeBindingState, connection_state: RuntimeConnectionDisplayState, revision: number, workspace_key_id?: string | null, workspace_key_generation?: number | null, verification?: RuntimeVerificationEvidenceSummary | null, }; export type WorkspaceRuntimeBindingSummary = {
state: WorkspaceRuntimeBindingState;
connection_state: RuntimeConnectionDisplayState;
revision: number;
workspace_key_id?: string | null;
workspace_key_generation?: number | null;
verification?: RuntimeVerificationEvidenceSummary | null;
};
export type RuntimeManagementSummary = { built_in: boolean, config_managed: boolean, removable: boolean, endpoint_configured: boolean, token_ref_configured: boolean, binding?: WorkspaceRuntimeBindingSummary | null, }; export type RuntimeManagementSummary = {
built_in: boolean;
config_managed: boolean;
removable: boolean;
endpoint_configured: boolean;
token_ref_configured: boolean;
binding?: WorkspaceRuntimeBindingSummary | null;
};
export type WorkspaceRuntimeResource = { management: RuntimeManagementSummary, runtime_id: string, label: string, kind: string, status: string, source: RuntimeSourceSummary, host_ids: Array<string>, worker_creation_available: boolean, os: string, arch: string, diagnostics: Array<Diagnostic>, }; export type WorkspaceRuntimeResource = {
management: RuntimeManagementSummary;
runtime_id: string;
label: string;
kind: string;
status: string;
source: RuntimeSourceSummary;
host_ids: Array<string>;
worker_creation_available: boolean;
os: string;
arch: string;
diagnostics: Array<Diagnostic>;
};
export type RuntimeTrustKeyStatus = "unconfigured" | "active" | "revoked"; export type RuntimeTrustKeyStatus = "unconfigured" | "active" | "revoked";
export type RuntimeTrustKeyState = { status: RuntimeTrustKeyStatus, fingerprint?: string | null, revision?: number | null, created_at?: string | null, updated_at?: string | null, revoked_at?: string | null, }; export type RuntimeTrustKeyState = {
status: RuntimeTrustKeyStatus;
fingerprint?: string | null;
revision?: number | null;
created_at?: string | null;
updated_at?: string | null;
revoked_at?: string | null;
};
export type RuntimeTrustAuditAction = "created" | "replaced" | "reactivated" | "revoked"; export type RuntimeTrustAuditAction =
| "created"
| "replaced"
| "reactivated"
| "revoked";
export type RuntimeTrustAuditEntry = { action: RuntimeTrustAuditAction, actor_account_id: string, old_fingerprint?: string | null, new_fingerprint?: string | null, revision: number, at: string, }; export type RuntimeTrustAuditEntry = {
action: RuntimeTrustAuditAction;
actor_account_id: string;
old_fingerprint?: string | null;
new_fingerprint?: string | null;
revision: number;
at: string;
};
export type WorkspaceRuntimeDetail = { workspace_id: string, runtime: WorkspaceRuntimeResource, endpoint?: string | null, trust_key: RuntimeTrustKeyState, recent_audit: Array<RuntimeTrustAuditEntry>, }; export type WorkspaceRuntimeDetail = {
workspace_id: string;
runtime: WorkspaceRuntimeResource;
endpoint?: string | null;
trust_key: RuntimeTrustKeyState;
recent_audit: Array<RuntimeTrustAuditEntry>;
};
export type RuntimeTrustKeyRevealResponse = { public_key: string, }; export type RuntimeTrustKeyRevealResponse = { public_key: string };
export type RevokeRuntimeTrustKeyRequest = { expected_revision: number, }; export type RevokeRuntimeTrustKeyRequest = { expected_revision: number };
export type RemoveRuntimeRequest = { operation_id: string, expected_binding_revision: number, }; export type RemoveRuntimeRequest = {
operation_id: string;
expected_binding_revision: number;
};
export type RuntimeRemovalOperationState = "pending" | "cleanup_pending" | "succeeded" | "failed"; export type RuntimeRemovalOperationState =
| "pending"
| "cleanup_pending"
| "succeeded"
| "failed";
export type RuntimeRemovalOperationResponse = { operation_id: string, workspace_id: string, runtime_id: string, state: RuntimeRemovalOperationState, binding_removed: boolean, runtime_registration_removed: boolean | null, failure_category?: string | null, created_at: string, updated_at: string, completed_at?: string | null, }; export type RuntimeRemovalOperationResponse = {
operation_id: string;
workspace_id: string;
runtime_id: string;
state: RuntimeRemovalOperationState;
binding_removed: boolean;
runtime_registration_removed: boolean | null;
failure_category?: string | null;
created_at: string;
updated_at: string;
completed_at?: string | null;
};
export type RuntimeTrustConflictKind = "stale_revision" | "fingerprint_in_use"; export type RuntimeTrustConflictKind = "stale_revision" | "fingerprint_in_use";
export type RuntimeTrustConflictResponse = { error: RuntimeTrustConflictKind, message: string, current_revision?: number, current_fingerprint?: string | null, }; export type RuntimeTrustConflictResponse = {
error: RuntimeTrustConflictKind;
message: string;
current_revision?: number;
current_fingerprint?: string | null;
};
export type RuntimePublicIdentityBundle = { identity_id: string, public_key: string, }; export type RuntimePublicIdentityBundle = {
identity_id: string;
public_key: string;
};
export type CreateRemoteRuntimeRequest = { public_bundle: RuntimePublicIdentityBundle, display_name?: string | null, endpoint: string, expected_revision?: number | null, }; export type CreateRemoteRuntimeRequest = {
public_bundle: RuntimePublicIdentityBundle;
display_name?: string | null;
endpoint: string;
expected_revision?: number | null;
};
export type UpdateRemoteRuntimeRequest = { display_name?: string | null, endpoint: string, }; export type UpdateRemoteRuntimeRequest = {
display_name?: string | null;
endpoint: string;
};
export type RuntimeConnectionTestStatus = "compatible" | "failed"; export type RuntimeConnectionTestStatus = "compatible" | "failed";
export type RuntimeConnectionTestFailureKind = "authentication" | "authorization" | "network_unreachable" | "timeout" | "tls_or_transport" | "malformed_response" | "protocol_version_mismatch" | "runtime_identity_mismatch" | "configuration"; export type RuntimeConnectionTestFailureKind =
| "authentication"
| "authorization"
| "network_unreachable"
| "timeout"
| "tls_or_transport"
| "malformed_response"
| "protocol_version_mismatch"
| "runtime_identity_mismatch"
| "configuration";
export type RuntimeConnectionTestResponse = { workspace_id: string, runtime_id: string, binding_revision: number, connection_state: RuntimeConnectionDisplayState, verification: RuntimeVerificationEvidenceSummary | null, checked_at: string, status: RuntimeConnectionTestStatus, failure_kind: RuntimeConnectionTestFailureKind | null, expected_protocol_version: number, actual_protocol_version: number | null, diagnostics: Array<Diagnostic>, }; export type RuntimeConnectionTestResponse = {
workspace_id: string;
runtime_id: string;
binding_revision: number;
connection_state: RuntimeConnectionDisplayState;
verification: RuntimeVerificationEvidenceSummary | null;
checked_at: string;
status: RuntimeConnectionTestStatus;
failure_kind: RuntimeConnectionTestFailureKind | null;
expected_protocol_version: number;
actual_protocol_version: number | null;
diagnostics: Array<Diagnostic>;
};
+93 -11
View File
@@ -1,26 +1,108 @@
// Generated from server-api. Do not edit by hand. // Generated from server-api. Do not edit by hand.
// Regenerate: cargo run -q -p server-api --features typescript --example generate_memory_api_types > web/workspace/src/lib/generated/memory-api.ts // Regenerate: cargo run -q -p server-api --features typescript --example generate_memory_api_types > web/workspace/src/lib/generated/memory-api.ts
export const MEMORY_API_LIMITS = {
maxResponseBytes: 8388608,
maxDocumentBytes: 4194304,
maxCollectionItems: 500,
maxStringBytes: 1048576,
maxIdentifierBytes: 512,
} as const;
export type DiagnosticSeverity = "info" | "warning" | "error"; export type DiagnosticSeverity = "info" | "warning" | "error";
export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, }; export type Diagnostic = {
code: string;
severity: DiagnosticSeverity;
message: string;
};
export type MemoryDocumentResponse = { body_md: string, created_at: string, updated_at: string, bytes: number, record_source: string, }; export type MemoryDocumentResponse = {
body_md: string;
created_at: string;
updated_at: string;
bytes: number;
record_source: string;
};
export type MemoryCandidateKind = "preference" | "working_assumption" | "constraint" | "decision" | "open_question" | "lesson"; export type MemoryCandidateKind =
| "preference"
| "working_assumption"
| "constraint"
| "decision"
| "open_question"
| "lesson";
export type MemoryEvidenceOriginKind = "human_input" | "worker_input" | "flow_instruction" | "backend_instruction" | "model_output" | "tool_output" | "derived_summary" | "legacy_unknown"; export type MemoryEvidenceOriginKind =
| "human_input"
| "worker_input"
| "flow_instruction"
| "backend_instruction"
| "model_output"
| "tool_output"
| "derived_summary"
| "legacy_unknown";
export type MemoryEvidenceOrigin = { kind: MemoryEvidenceOriginKind, account_id?: string | null, workspace_id?: string | null, runtime_id?: string | null, worker_id?: string | null, flow_selector?: string | null, flow_definition_id?: string | null, flow_definition_revision?: number | null, }; export type MemoryEvidenceOrigin = {
kind: MemoryEvidenceOriginKind;
account_id?: string | null;
workspace_id?: string | null;
runtime_id?: string | null;
worker_id?: string | null;
flow_selector?: string | null;
flow_definition_id?: string | null;
flow_definition_revision?: number | null;
};
export type MemorySourceRef = { segment_id: string, range: [number, number], }; export type MemorySourceRef = { segment_id: string; range: [number, number] };
export type MemoryStagingEvidence = { id: string, kind: string, entry_range: [number, number] | null, origin?: MemoryEvidenceOrigin | null, excerpt: string | null, summary: string | null, }; export type MemoryStagingEvidence = {
id: string;
kind: string;
entry_range: [number, number] | null;
origin?: MemoryEvidenceOrigin | null;
excerpt: string | null;
summary: string | null;
};
export type MemorySourceEvidenceRef = { session_id: string | null, segment_id: string | null, entry_range: [number, number] | null, evidence_id: string | null, origin?: MemoryEvidenceOrigin | null, evidence_kind: string | null, label: string | null, summary: string | null, }; export type MemorySourceEvidenceRef = {
session_id: string | null;
segment_id: string | null;
entry_range: [number, number] | null;
evidence_id: string | null;
origin?: MemoryEvidenceOrigin | null;
evidence_kind: string | null;
label: string | null;
summary: string | null;
};
export type MemoryStagingRecord = { schema_version: number, id: string, extract_run_id: string, source: MemorySourceRef, kind: MemoryCandidateKind, claim: string, why_useful: string, staleness: string | null, evidence: Array<MemoryStagingEvidence>, source_refs: Array<MemorySourceEvidenceRef>, }; export type MemoryStagingRecord = {
schema_version: number;
id: string;
extract_run_id: string;
source: MemorySourceRef;
kind: MemoryCandidateKind;
claim: string;
why_useful: string;
staleness: string | null;
evidence: Array<MemoryStagingEvidence>;
source_refs: Array<MemorySourceEvidenceRef>;
};
export type MemoryStagingEntry = { id: string, byte_len: number, record: MemoryStagingRecord, }; export type MemoryStagingEntry = {
id: string;
byte_len: number;
record: MemoryStagingRecord;
};
export type MemoryStagingListResponse = { limit: number, returned_count: number, total_valid_count: number, invalid_count: number, truncated: boolean, order: string, record_authority: string, items: Array<MemoryStagingEntry>, diagnostics: Array<Diagnostic>, }; export type MemoryStagingListResponse = {
limit: number;
returned_count: number;
total_valid_count: number;
invalid_count: number;
truncated: boolean;
order: string;
record_authority: string;
items: Array<MemoryStagingEntry>;
diagnostics: Array<Diagnostic>;
};
@@ -1,26 +1,93 @@
// Generated from server-api. Do not edit by hand. // Generated from server-api. Do not edit by hand.
// Regenerate: cargo run -q -p server-api --features typescript --example generate_repository_access_types > web/workspace/src/lib/generated/repository-access-api.ts // Regenerate: cargo run -q -p server-api --features typescript --example generate_repository_access_types > web/workspace/src/lib/generated/repository-access-api.ts
export type RepositorySshCredential = { credential_id: string, workspace_id: string, name: string, public_key_algorithm: string, public_key_fingerprint: string, current_revision: number, status: string, created_at: string, rotated_at: string | null, referenced_repositories: Array<string>, }; export type RepositorySshCredential = {
credential_id: string;
workspace_id: string;
name: string;
public_key_algorithm: string;
public_key_fingerprint: string;
current_revision: number;
status: string;
created_at: string;
rotated_at: string | null;
referenced_repositories: Array<string>;
};
export type CreateRepositorySshCredentialRequest = { operation_id: string, credential_id: string, name: string, private_key: string, passphrase: string | null, }; export type CreateRepositorySshCredentialRequest = {
operation_id: string;
credential_id: string;
name: string;
private_key: string;
passphrase: string | null;
};
export type GenerateRepositorySshCredentialRequest = { operation_id: string, credential_id: string, name: string, }; export type GenerateRepositorySshCredentialRequest = {
operation_id: string;
credential_id: string;
name: string;
};
export type RepositorySshPublicKey = { credential_id: string, current_revision: number, public_key_algorithm: string, public_key_fingerprint: string, public_key: string, }; export type RepositorySshPublicKey = {
credential_id: string;
current_revision: number;
public_key_algorithm: string;
public_key_fingerprint: string;
public_key: string;
};
export type RotateRepositorySshCredentialRequest = { operation_id: string, expected_revision: number, private_key: string, passphrase: string | null, }; export type RotateRepositorySshCredentialRequest = {
operation_id: string;
expected_revision: number;
private_key: string;
passphrase: string | null;
};
export type DeleteRepositorySshCredentialRequest = { operation_id: string, expected_revision: number, }; export type DeleteRepositorySshCredentialRequest = {
operation_id: string;
expected_revision: number;
};
export type RepositorySshHostTrust = { host_trust_id: string, workspace_id: string, hostname: string, port: number, key_algorithm: string, host_key: string, fingerprint: string, current_revision: number, created_at: string, updated_at: string, referenced_repositories: Array<string>, }; export type RepositorySshHostTrust = {
host_trust_id: string;
workspace_id: string;
hostname: string;
port: number;
key_algorithm: string;
host_key: string;
fingerprint: string;
current_revision: number;
created_at: string;
updated_at: string;
referenced_repositories: Array<string>;
};
export type PutRepositorySshHostTrustRequest = { operation_id: string, host_trust_id: string, hostname: string, port: number, host_key: string, expected_revision: number | null, }; export type PutRepositorySshHostTrustRequest = {
operation_id: string;
host_trust_id: string;
hostname: string;
port: number;
host_key: string;
expected_revision: number | null;
};
export type DeleteRepositorySshHostTrustRequest = { operation_id: string, expected_revision: number, }; export type DeleteRepositorySshHostTrustRequest = {
operation_id: string;
expected_revision: number;
};
export type RepositoryAccessMode = "read_only" | "read_write"; export type RepositoryAccessMode = "read_only" | "read_write";
export type RepositorySshAccessBinding = { repository_key: string, credential_id: string, host_trust_id: string, access: RepositoryAccessMode, }; export type RepositorySshAccessBinding = {
repository_key: string;
credential_id: string;
host_trust_id: string;
access: RepositoryAccessMode;
};
export type RepositoryAccessProjection = { workspace_id: string, config_revision: number, projection_digest: string, bindings: Array<RepositorySshAccessBinding>, }; export type RepositoryAccessProjection = {
workspace_id: string;
config_revision: number;
projection_digest: string;
bindings: Array<RepositorySshAccessBinding>;
};
@@ -1,6 +1,6 @@
// @generated by yoi-repository-openapi-typescript@1. Do not edit. // @generated by yoi-repository-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json // Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:b478a5e12b43d9ac1261ba31358a5b80bb31e398bd57eccbd797f5e92c6f48b7) // Input contract: 0.1.0 (sha256:386c56c830b49766e9f523f08c5bd4e24de490ba18c99c3d04063021d6b33352)
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output // Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
// Output: web/workspace/src/lib/generated/repository-api.ts // Output: web/workspace/src/lib/generated/repository-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check // Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
+61 -7
View File
@@ -20,22 +20,76 @@ export const SKILL_API_LIMITS = {
export type SkillDiagnosticSeverity = "error" | "warning"; export type SkillDiagnosticSeverity = "error" | "warning";
export type SkillDiagnostic = { severity: SkillDiagnosticSeverity, code: string, message: string, source?: string, }; export type SkillDiagnostic = {
severity: SkillDiagnosticSeverity;
code: string;
message: string;
source?: string;
};
export type SkillSourceKind = "builtin" | "workspace"; export type SkillSourceKind = "builtin" | "workspace";
export type SkillProvenance = { kind: SkillSourceKind, id: string, virtual_path?: string, revision?: number, source_digest?: string, tree_digest?: string, }; export type SkillProvenance = {
kind: SkillSourceKind;
id: string;
virtual_path?: string;
revision?: number;
source_digest?: string;
tree_digest?: string;
};
export type SkillActivationStatus = "active" | "inactive"; export type SkillActivationStatus = "active" | "inactive";
export type SkillProjectionStatus = "valid" | "invalid"; export type SkillProjectionStatus = "valid" | "invalid";
export type SkillProjectionIdentity = { config_revision: number, tree_digest: string, }; export type SkillProjectionIdentity = {
config_revision: number;
tree_digest: string;
};
export type SkillResourceRef = { kind: string, name: string, supported: boolean, diagnostic?: string, }; export type SkillResourceRef = {
kind: string;
name: string;
supported: boolean;
diagnostic?: string;
};
export type SkillCatalogEntry = { name: string, description: string, activation_status: SkillActivationStatus, projection_status: SkillProjectionStatus, provenance: SkillProvenance, overrides: Array<SkillProvenance>, diagnostics: Array<SkillDiagnostic>, }; export type SkillCatalogEntry = {
name: string;
description: string;
activation_status: SkillActivationStatus;
projection_status: SkillProjectionStatus;
provenance: SkillProvenance;
overrides: Array<SkillProvenance>;
diagnostics: Array<SkillDiagnostic>;
};
export type SkillCatalogResponse = { authority: string, projection: SkillProjectionIdentity, entries: Array<SkillCatalogEntry>, diagnostics: Array<SkillDiagnostic>, }; export type SkillCatalogResponse = {
authority: string;
projection: SkillProjectionIdentity;
entries: Array<SkillCatalogEntry>;
diagnostics: Array<SkillDiagnostic>;
};
export type SkillDetailResponse = { authority: string, projection: SkillProjectionIdentity, name: string, description: string, provenance: SkillProvenance, overrides: Array<SkillProvenance>, diagnostics: Array<SkillDiagnostic>, activation_status: SkillActivationStatus, projection_status: SkillProjectionStatus, body: string, allowed_tools: Array<string>, allowed_tools_status: string, resources: Array<SkillResourceRef>, }; export type SkillDetailResponse = {
authority: string;
projection: SkillProjectionIdentity;
name: string;
description: string;
provenance: SkillProvenance;
overrides: Array<SkillProvenance>;
diagnostics: Array<SkillDiagnostic>;
activation_status: SkillActivationStatus;
projection_status: SkillProjectionStatus;
body: string;
allowed_tools: Array<string>;
allowed_tools_status: string;
resources: Array<SkillResourceRef>;
};
export type SkillActivationResponse = {
name: string;
provenance: SkillProvenance;
diagnostics: Array<SkillDiagnostic>;
body: string;
};
@@ -1,5 +1,9 @@
import { error } from "@sveltejs/kit"; import { error } from "@sveltejs/kit";
import { RepositoryAccessSchemaError } from "./repository-access.ts"; import { readBoundedJson } from "./http.ts";
import {
REPOSITORY_ACCESS_MAX_RESPONSE_BYTES,
RepositoryAccessSchemaError,
} from "./repository-access.ts";
export async function loadRepositoryAccessJson<T>( export async function loadRepositoryAccessJson<T>(
fetcher: typeof fetch, fetcher: typeof fetch,
@@ -27,7 +31,10 @@ export async function loadRepositoryAccessJson<T>(
let payload: unknown; let payload: unknown;
try { try {
payload = await response.json(); payload = await readBoundedJson(
response,
REPOSITORY_ACCESS_MAX_RESPONSE_BYTES,
);
} catch { } catch {
error(502, { error(502, {
message: "Repository Access returned an invalid JSON response.", message: "Repository Access returned an invalid JSON response.",
@@ -5,6 +5,10 @@ import type {
RepositorySshPublicKey, RepositorySshPublicKey,
} from "../../generated/repository-access-api.ts"; } from "../../generated/repository-access-api.ts";
export const REPOSITORY_ACCESS_MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
const MAX_COLLECTION_ITEMS = 1000;
const MAX_STRING_BYTES = 1024 * 1024;
export class RepositoryAccessSchemaError extends Error { export class RepositoryAccessSchemaError extends Error {
constructor(path: string, expected: string) { constructor(path: string, expected: string) {
super( super(
@@ -165,7 +169,7 @@ function readRecord(
); );
if (unknownKey !== undefined) { if (unknownKey !== undefined) {
throw new RepositoryAccessSchemaError( throw new RepositoryAccessSchemaError(
`${path}.${unknownKey}`, `${path}.${unknownKey.slice(0, 128)}`,
"no unknown field", "no unknown field",
); );
} }
@@ -173,7 +177,7 @@ function readRecord(
} }
function readArray(value: unknown, path: string): unknown[] { function readArray(value: unknown, path: string): unknown[] {
if (!Array.isArray(value)) { if (!Array.isArray(value) || value.length > MAX_COLLECTION_ITEMS) {
throw new RepositoryAccessSchemaError(path, "an array"); throw new RepositoryAccessSchemaError(path, "an array");
} }
return value; return value;
@@ -185,8 +189,11 @@ function readString(
path: string, path: string,
): string { ): string {
const value = record[key]; const value = record[key];
if (typeof value !== "string") { if (
throw new RepositoryAccessSchemaError(`${path}.${key}`, "a string"); typeof value !== "string" ||
new TextEncoder().encode(value).byteLength > MAX_STRING_BYTES
) {
throw new RepositoryAccessSchemaError(`${path}.${key}`, "a bounded string");
} }
return value; return value;
} }
@@ -197,8 +204,15 @@ function readNullableString(
path: string, path: string,
): string | null { ): string | null {
const value = record[key]; const value = record[key];
if (value !== null && typeof value !== "string") { if (
throw new RepositoryAccessSchemaError(`${path}.${key}`, "a string or null"); value !== null &&
(typeof value !== "string" ||
new TextEncoder().encode(value).byteLength > MAX_STRING_BYTES)
) {
throw new RepositoryAccessSchemaError(
`${path}.${key}`,
"a bounded string or null",
);
} }
return value; return value;
} }
@@ -210,10 +224,13 @@ function readStringArray(
): string[] { ): string[] {
const values = readArray(record[key], `${path}.${key}`); const values = readArray(record[key], `${path}.${key}`);
values.forEach((value, index) => { values.forEach((value, index) => {
if (typeof value !== "string") { if (
typeof value !== "string" ||
new TextEncoder().encode(value).byteLength > MAX_STRING_BYTES
) {
throw new RepositoryAccessSchemaError( throw new RepositoryAccessSchemaError(
`${path}.${key}[${index}]`, `${path}.${key}[${index}]`,
"a string", "a bounded string",
); );
} }
}); });
@@ -395,6 +395,27 @@ function workspaceRepositoryRecord(
}; };
} }
function workspaceString(value: unknown, path: string): string {
const parsed = string(value, path);
if (parsed.length > REPOSITORY_API_LIMITS.maxStringCodeUnits) {
throw new Error(`${path} exceeds the Workspace API string limit`);
}
return parsed;
}
function workspaceDiagnostics(value: unknown, path: string): Diagnostic[] {
const entries = array(value, path);
if (entries.length > 100) {
throw new Error(`${path} exceeds the Workspace API collection limit`);
}
return entries.map((entry, index) => {
const parsed = diagnostic(entry, `${path}[${index}]`);
workspaceString(parsed.code, `${path}[${index}].code`);
workspaceString(parsed.message, `${path}[${index}].message`);
return parsed;
});
}
function extensionPoint( function extensionPoint(
value: unknown, value: unknown,
path: string, path: string,
@@ -402,12 +423,9 @@ function extensionPoint(
const item = object(value, path); const item = object(value, path);
exactKeys(item, ["status", "note", "diagnostics"], path); exactKeys(item, ["status", "note", "diagnostics"], path);
return { return {
status: string(item.status, `${path}.status`), status: workspaceString(item.status, `${path}.status`),
note: string(item.note, `${path}.note`), note: workspaceString(item.note, `${path}.note`),
diagnostics: array(item.diagnostics, `${path}.diagnostics`).map(( diagnostics: workspaceDiagnostics(item.diagnostics, `${path}.diagnostics`),
entry,
index,
) => diagnostic(entry, `${path}.diagnostics[${index}]`)),
}; };
} }
@@ -423,7 +441,7 @@ function extensionPoints(
"companion_console", "companion_console",
], path); ], path);
return { return {
store: string(item.store, `${path}.store`), store: workspaceString(item.store, `${path}.store`),
event_stream: extensionPoint(item.event_stream, `${path}.event_stream`), event_stream: extensionPoint(item.event_stream, `${path}.event_stream`),
host_worker_bridge: extensionPoint( host_worker_bridge: extensionPoint(
item.host_worker_bridge, item.host_worker_bridge,
@@ -447,13 +465,16 @@ function authConfig(value: unknown, path: string): WorkspaceAuthConfig {
); );
return { return {
Passkey: { Passkey: {
rp_id: string(passkey.rp_id, `${path}.Passkey.rp_id`), rp_id: workspaceString(passkey.rp_id, `${path}.Passkey.rp_id`),
origin: string(passkey.origin, `${path}.Passkey.origin`), origin: workspaceString(passkey.origin, `${path}.Passkey.origin`),
public_base_url: string( public_base_url: workspaceString(
passkey.public_base_url, passkey.public_base_url,
`${path}.Passkey.public_base_url`, `${path}.Passkey.public_base_url`,
), ),
cookie_name: string(passkey.cookie_name, `${path}.Passkey.cookie_name`), cookie_name: workspaceString(
passkey.cookie_name,
`${path}.Passkey.cookie_name`,
),
}, },
}; };
} }
@@ -575,23 +596,27 @@ export function parseWorkspaceResponse(value: unknown): WorkspaceResponse {
], ],
"workspace response", "workspace response",
); );
const schemaVersion = integer(
response.schema_version,
"workspace response.schema_version",
);
if (schemaVersion < 0) {
throw new Error("workspace response.schema_version must be non-negative");
}
return { return {
workspace_id: string( workspace_id: workspaceString(
response.workspace_id, response.workspace_id,
"workspace response.workspace_id", "workspace response.workspace_id",
), ),
display_name: string( display_name: workspaceString(
response.display_name, response.display_name,
"workspace response.display_name", "workspace response.display_name",
), ),
record_authority: string( record_authority: workspaceString(
response.record_authority, response.record_authority,
"workspace response.record_authority", "workspace response.record_authority",
), ),
schema_version: integer( schema_version: schemaVersion,
response.schema_version,
"workspace response.schema_version",
),
auth: authConfig(response.auth, "workspace response.auth"), auth: authConfig(response.auth, "workspace response.auth"),
permissions: permissions( permissions: permissions(
response.permissions, response.permissions,
@@ -1,20 +1,267 @@
import type { import type {
ConfigCommitRequest, ConfigCommitRequest,
ConfigContentType,
ConfigEntry, ConfigEntry,
ConfigProjectionValidator,
ConfigSchemaContribution,
ConfigTreeSnapshot, ConfigTreeSnapshot,
ToolchainContract,
WorkspaceConfigSchemaBundle,
WorkspaceConfigTreeResponse, WorkspaceConfigTreeResponse,
} from "./types.ts"; } from "./types.ts";
const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
const MAX_ERROR_BYTES = 4 * 1024;
const MAX_ENTRY_COUNT = 256;
const MAX_ENTRY_BYTES = 256 * 1024;
const MAX_PATH_BYTES = 512;
const MAX_STRING_BYTES = 4 * 1024;
const MAX_DIGEST_BYTES = 256;
const MAX_COLLECTION_ITEMS = 256;
export class ConfigSourceApiError extends Error {
constructor(
message: string,
readonly status: number,
) {
super(message);
this.name = "ConfigSourceApiError";
}
}
type JsonRecord = Record<string, unknown>;
type Parser<T> = (value: unknown) => T;
function sourceTreeUrl(workspaceId: string): string { function sourceTreeUrl(workspaceId: string): string {
return `/api/w/${encodeURIComponent(workspaceId)}/config/source-tree`; return `/api/w/${encodeURIComponent(workspaceId)}/config/source-tree`;
} }
async function readJson<T>(response: Response): Promise<T> { async function readJson<T>(
if (!response.ok) { response: Response,
const body = await response.text(); parser: Parser<T>,
throw new Error(body || `${response.status} ${response.statusText}`); ): Promise<T> {
const limit = response.ok ? MAX_RESPONSE_BYTES : MAX_ERROR_BYTES;
const declared = response.headers.get("content-length");
if (declared !== null) {
const bytes = Number(declared);
if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > limit) {
throw new ConfigSourceApiError(
response.ok
? "Workspace configuration response is too large."
: "Workspace configuration request failed.",
response.ok ? 502 : response.status,
);
}
} }
return await response.json() as T; const bytes = new Uint8Array(await response.arrayBuffer());
if (bytes.byteLength > limit) {
throw new ConfigSourceApiError(
response.ok
? "Workspace configuration response is too large."
: "Workspace configuration request failed.",
response.ok ? 502 : response.status,
);
}
const text = new TextDecoder().decode(bytes);
if (!response.ok) {
throw new ConfigSourceApiError(
text || `${response.status} ${response.statusText}`,
response.status,
);
}
let value: unknown;
try {
value = JSON.parse(text) as unknown;
} catch {
throw schemaError();
}
return parser(value);
}
function schemaError(): ConfigSourceApiError {
return new ConfigSourceApiError(
"Workspace configuration returned an invalid response.",
502,
);
}
function objectValue(value: unknown): JsonRecord {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw schemaError();
}
return value as JsonRecord;
}
function record(
value: unknown,
required: readonly string[],
optional: readonly string[] = [],
): JsonRecord {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw schemaError();
}
const result = value as JsonRecord;
const allowed = new Set([...required, ...optional]);
if (
required.some((key) => !(key in result)) ||
Object.keys(result).some((key) => !allowed.has(key))
) {
throw schemaError();
}
return result;
}
function boundedString(value: unknown, maxBytes = MAX_STRING_BYTES): string {
if (
typeof value !== "string" ||
new TextEncoder().encode(value).byteLength > maxBytes
) {
throw schemaError();
}
return value;
}
function safeInteger(value: unknown): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw schemaError();
}
return value as number;
}
function boundedArray(value: unknown): unknown[] {
if (!Array.isArray(value) || value.length > MAX_COLLECTION_ITEMS) {
throw schemaError();
}
return value;
}
function parseContentType(value: unknown): ConfigContentType {
if (value !== "decodal" && value !== "text") throw schemaError();
return value;
}
export function parseConfigEntry(value: unknown): ConfigEntry {
const item = record(value, [
"path",
"content_type",
"content",
"content_digest",
]);
return {
path: boundedString(item.path, MAX_PATH_BYTES),
content_type: parseContentType(item.content_type),
content: boundedString(item.content, MAX_ENTRY_BYTES),
content_digest: boundedString(item.content_digest, MAX_DIGEST_BYTES),
};
}
function parseStringMap(value: unknown): Record<string, string> {
const item = objectValue(value);
const entries = Object.entries(item);
if (entries.length > MAX_COLLECTION_ITEMS) throw schemaError();
const result: Record<string, string> = {};
for (const [key, entry] of entries) {
const boundedKey = boundedString(key, MAX_PATH_BYTES);
result[boundedKey] = boundedString(entry, MAX_PATH_BYTES);
}
return result;
}
function parseProjectionValidator(value: unknown): ConfigProjectionValidator {
const item = record(value, ["kind", "namespace"], ["key_aliases"]);
if (item.kind !== "static_template_catalog") throw schemaError();
return {
kind: "static_template_catalog",
namespace: boundedString(item.namespace, MAX_PATH_BYTES),
key_aliases: item.key_aliases === undefined
? {}
: parseStringMap(item.key_aliases),
};
}
function parseContribution(value: unknown): ConfigSchemaContribution {
const item = record(
value,
["provider_id", "namespace", "version", "source", "source_digest"],
["projection_validator"],
);
const projection = item.projection_validator === undefined
? undefined
: parseProjectionValidator(item.projection_validator);
return {
provider_id: boundedString(item.provider_id),
namespace: boundedString(item.namespace),
version: boundedString(item.version),
source: boundedString(item.source),
...(projection === undefined ? {} : { projection_validator: projection }),
source_digest: boundedString(item.source_digest, MAX_DIGEST_BYTES),
};
}
function parseSchemaBundle(value: unknown): WorkspaceConfigSchemaBundle {
const item = record(value, ["contributions", "source", "fingerprint"]);
return {
contributions: boundedArray(item.contributions).map(parseContribution),
source: boundedString(item.source),
fingerprint: boundedString(item.fingerprint, MAX_DIGEST_BYTES),
};
}
function parseToolchainContract(value: unknown): ToolchainContract {
const item = record(value, [
"contract_version",
"decodal_version",
"schema_version",
"entrypoints",
"import_policy_version",
"schema_bundle",
"fingerprint",
]);
return {
contract_version: safeInteger(item.contract_version),
decodal_version: boundedString(item.decodal_version),
schema_version: safeInteger(item.schema_version),
entrypoints: boundedArray(item.entrypoints).map((entry) =>
boundedString(entry, MAX_PATH_BYTES)
),
import_policy_version: safeInteger(item.import_policy_version),
schema_bundle: parseSchemaBundle(item.schema_bundle),
fingerprint: boundedString(item.fingerprint, MAX_DIGEST_BYTES),
};
}
export function parseConfigTreeSnapshot(value: unknown): ConfigTreeSnapshot {
const item = record(value, ["revision", "digest", "entries"]);
const entriesRecord = objectValue(item.entries);
const entries = Object.entries(entriesRecord);
if (entries.length > MAX_ENTRY_COUNT) throw schemaError();
const parsedEntries: Record<string, ConfigEntry> = {};
for (const [path, entry] of entries) {
const boundedPath = boundedString(path, MAX_PATH_BYTES);
const parsed = parseConfigEntry(entry);
if (parsed.path !== boundedPath) throw schemaError();
parsedEntries[boundedPath] = parsed;
}
return {
revision: safeInteger(item.revision),
digest: boundedString(item.digest, MAX_DIGEST_BYTES),
entries: parsedEntries,
};
}
export function parseWorkspaceConfigTreeResponse(
value: unknown,
): WorkspaceConfigTreeResponse {
const item = record(value, ["snapshot", "contract", "projection_digest"]);
return {
snapshot: parseConfigTreeSnapshot(item.snapshot),
contract: parseToolchainContract(item.contract),
projection_digest: boundedString(
item.projection_digest,
MAX_DIGEST_BYTES,
),
};
} }
export async function fetchConfigTree( export async function fetchConfigTree(
@@ -25,6 +272,7 @@ export async function fetchConfigTree(
await fetcher(sourceTreeUrl(workspaceId), { await fetcher(sourceTreeUrl(workspaceId), {
headers: { accept: "application/json" }, headers: { accept: "application/json" },
}), }),
parseWorkspaceConfigTreeResponse,
); );
} }
@@ -38,6 +286,7 @@ export async function fetchConfigEntry(
`${sourceTreeUrl(workspaceId)}/entries/${encodeURIComponent(path)}`, `${sourceTreeUrl(workspaceId)}/entries/${encodeURIComponent(path)}`,
{ headers: { accept: "application/json" } }, { headers: { accept: "application/json" } },
), ),
parseConfigEntry,
); );
} }
@@ -50,6 +299,7 @@ export async function fetchConfigRevision(
await fetcher(`${sourceTreeUrl(workspaceId)}/revisions/${revision}`, { await fetcher(`${sourceTreeUrl(workspaceId)}/revisions/${revision}`, {
headers: { accept: "application/json" }, headers: { accept: "application/json" },
}), }),
parseConfigTreeSnapshot,
); );
} }
@@ -64,5 +314,6 @@ export async function commitConfigTree(
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
body: JSON.stringify(request), body: JSON.stringify(request),
}), }),
parseWorkspaceConfigTreeResponse,
); );
} }
@@ -1,16 +1,21 @@
// Core and HTTP DTOs are generated from Rust with ts-rs. // Workspace REST DTOs are generated from the server-api Rust authority.
export type { ConfigContentType } from "./generated/types/ConfigContentType.ts"; export type {
ConfigCommitRequest,
ConfigContentType,
ConfigEntry,
ConfigProjectionValidator,
ConfigSchemaContribution,
ConfigTreeChange,
ConfigTreeSnapshot,
ToolchainContract,
WorkspaceConfigSchemaBundle,
WorkspaceConfigTreeResponse,
} from "$lib/generated/legacy-server-api.ts";
// Browser-local Decodal/WASM projections remain owned by config-source.
export type { ConfigDiagnostic } from "./generated/types/ConfigDiagnostic.ts"; export type { ConfigDiagnostic } from "./generated/types/ConfigDiagnostic.ts";
export type { ConfigDiagnosticLabel } from "./generated/types/ConfigDiagnosticLabel.ts"; export type { ConfigDiagnosticLabel } from "./generated/types/ConfigDiagnosticLabel.ts";
export type { ConfigEntry } from "./generated/types/ConfigEntry.ts";
export type { ConfigSpan } from "./generated/types/ConfigSpan.ts"; export type { ConfigSpan } from "./generated/types/ConfigSpan.ts";
export type { ConfigTreeChange } from "./generated/types/ConfigTreeChange.ts";
export type { ConfigSchemaContribution } from "./generated/types/ConfigSchemaContribution.ts";
export type { WorkspaceConfigSchemaBundle } from "./generated/types/WorkspaceConfigSchemaBundle.ts";
export type { ConfigTreeSnapshot } from "./generated/types/ConfigTreeSnapshot.ts";
export type { EvaluatedProjection } from "./generated/types/EvaluatedProjection.ts"; export type { EvaluatedProjection } from "./generated/types/EvaluatedProjection.ts";
export type { EvaluationResult } from "./generated/types/EvaluationResult.ts"; export type { EvaluationResult } from "./generated/types/EvaluationResult.ts";
export type { ToolchainContract } from "./generated/types/ToolchainContract.ts";
export type { VirtualPath } from "./generated/types/VirtualPath.ts"; export type { VirtualPath } from "./generated/types/VirtualPath.ts";
export type { ConfigCommitRequest } from "./generated/types/ConfigCommitRequest.ts";
export type { WorkspaceConfigState as WorkspaceConfigTreeResponse } from "./generated/types/WorkspaceConfigState.ts";
+33 -12
View File
@@ -1,3 +1,4 @@
import { MEMORY_API_LIMITS } from "$lib/generated/memory-api";
import type { import type {
Diagnostic, Diagnostic,
DiagnosticSeverity, DiagnosticSeverity,
@@ -13,10 +14,10 @@ import type {
MemoryStagingRecord, MemoryStagingRecord,
} from "$lib/generated/memory-api"; } from "$lib/generated/memory-api";
const MAX_STAGING_ITEMS = 500; const MAX_STAGING_ITEMS = MEMORY_API_LIMITS.maxCollectionItems;
const MAX_EVIDENCE_PER_RECORD = 500; const MAX_EVIDENCE_PER_RECORD = MEMORY_API_LIMITS.maxCollectionItems;
const MAX_SOURCE_REFS_PER_RECORD = 500; const MAX_SOURCE_REFS_PER_RECORD = MEMORY_API_LIMITS.maxCollectionItems;
const MAX_ORIGIN_VALUE_LENGTH = 512; const MAX_ORIGIN_VALUE_LENGTH = MEMORY_API_LIMITS.maxIdentifierBytes;
const candidateKinds = new Set<MemoryCandidateKind>([ const candidateKinds = new Set<MemoryCandidateKind>([
"preference", "preference",
@@ -51,7 +52,11 @@ export function parseMemoryDocumentResponse(
"Memory document response", "Memory document response",
); );
return { return {
body_md: requiredString(record, "body_md"), body_md: requiredString(
record,
"body_md",
MEMORY_API_LIMITS.maxDocumentBytes,
),
created_at: requiredString(record, "created_at"), created_at: requiredString(record, "created_at"),
updated_at: requiredString(record, "updated_at"), updated_at: requiredString(record, "updated_at"),
bytes: requiredNonNegativeInteger(record, "bytes"), bytes: requiredNonNegativeInteger(record, "bytes"),
@@ -258,7 +263,10 @@ function parseEvidenceOrigin(value: unknown): MemoryEvidenceOrigin {
) { ) {
if (key in record) { if (key in record) {
const text = nullableString(record, key); const text = nullableString(record, key);
if (text !== null && text.length > MAX_ORIGIN_VALUE_LENGTH) { if (
text !== null &&
new TextEncoder().encode(text).byteLength > MAX_ORIGIN_VALUE_LENGTH
) {
invalid(`${key} exceeds the Memory origin limit`); invalid(`${key} exceeds the Memory origin limit`);
} }
result[key] = text; result[key] = text;
@@ -328,11 +336,19 @@ function boundedArray(
return value; return value;
} }
function requiredString(record: Record<string, unknown>, key: string): string { function requiredString(
if (typeof record[key] !== "string") { record: Record<string, unknown>,
invalid(`${key} must be a string`); key: string,
maxBytes: number = MEMORY_API_LIMITS.maxStringBytes,
): string {
const value = record[key];
if (
typeof value !== "string" ||
new TextEncoder().encode(value).byteLength > maxBytes
) {
invalid(`${key} must be a bounded string`);
} }
return record[key]; return value;
} }
function nullableString( function nullableString(
@@ -340,8 +356,13 @@ function nullableString(
key: string, key: string,
): string | null { ): string | null {
const value = record[key]; const value = record[key];
if (value !== null && typeof value !== "string") { if (
invalid(`${key} must be a string or null`); value !== null &&
(typeof value !== "string" ||
new TextEncoder().encode(value).byteLength >
MEMORY_API_LIMITS.maxStringBytes)
) {
invalid(`${key} must be a bounded string or null`);
} }
return value; return value;
} }
@@ -13,6 +13,12 @@ import type {
WorkspaceSigningIdentityResponse, WorkspaceSigningIdentityResponse,
WorkspaceSigningIdentityState, WorkspaceSigningIdentityState,
} from "$lib/generated/legacy-server-api"; } from "$lib/generated/legacy-server-api";
import { readBoundedJson } from "$lib/workspace/api/http.ts";
const MAX_PROFILE_RESPONSE_BYTES = 4 * 1024 * 1024;
const MAX_PROFILE_ERROR_BYTES = 4 * 1024;
const MAX_PROFILE_STRING_BYTES = 1024 * 1024;
const MAX_PROFILE_COLLECTION_ITEMS = 1000;
export class ProfileApiError extends Error { export class ProfileApiError extends Error {
constructor( constructor(
@@ -49,10 +55,7 @@ function exactKeys(
} }
function stringValue(value: unknown, context: string): string { function stringValue(value: unknown, context: string): string {
if (typeof value !== "string") { return boundedStringValue(value, context, MAX_PROFILE_STRING_BYTES);
throw new ProfileApiError(`${context} returned an invalid response.`, 502);
}
return value;
} }
function boundedStringValue( function boundedStringValue(
@@ -60,7 +63,10 @@ function boundedStringValue(
context: string, context: string,
maxBytes: number, maxBytes: number,
): string { ): string {
const text = stringValue(value, context); if (typeof value !== "string") {
throw new ProfileApiError(`${context} returned an invalid response.`, 502);
}
const text = value;
if (new TextEncoder().encode(text).byteLength > maxBytes) { if (new TextEncoder().encode(text).byteLength > maxBytes) {
throw new ProfileApiError(`${context} returned an invalid response.`, 502); throw new ProfileApiError(`${context} returned an invalid response.`, 502);
} }
@@ -107,7 +113,7 @@ function arrayValue<T>(
parser: (item: unknown) => T, parser: (item: unknown) => T,
context: string, context: string,
): T[] { ): T[] {
if (!Array.isArray(value)) { if (!Array.isArray(value) || value.length > MAX_PROFILE_COLLECTION_ITEMS) {
throw new ProfileApiError(`${context} returned an invalid response.`, 502); throw new ProfileApiError(`${context} returned an invalid response.`, 502);
} }
return value.map(parser); return value.map(parser);
@@ -502,17 +508,23 @@ export function parseWorkspaceSigningIdentityResponse(
}; };
} }
async function boundedErrorText(response: Response): Promise<string> {
const bytes = new Uint8Array(await response.arrayBuffer());
if (bytes.byteLength > MAX_PROFILE_ERROR_BYTES) return "";
return new TextDecoder().decode(bytes);
}
async function parseResponse<T>( async function parseResponse<T>(
response: Response, response: Response,
parser: (value: unknown) => T, parser: (value: unknown) => T,
): Promise<T> { ): Promise<T> {
if (!response.ok) { if (!response.ok) {
throw new ProfileApiError( throw new ProfileApiError(
(await response.text()) || response.statusText, (await boundedErrorText(response)) || response.statusText,
response.status, response.status,
); );
} }
return parser(await response.json() as unknown); return parser(await readBoundedJson(response, MAX_PROFILE_RESPONSE_BYTES));
} }
export async function fetchWorkspaceMetadata( export async function fetchWorkspaceMetadata(
@@ -7,21 +7,18 @@ import type { LayoutLoad } from "./$types";
export const load: LayoutLoad = async ({ fetch, params }) => { export const load: LayoutLoad = async ({ fetch, params }) => {
const workspaceId = params.workspaceId; const workspaceId = params.workspaceId;
const [workspaceResult, repositoryResult] = await Promise.all([ const [workspaceResult, repositoryResult] = await Promise.all([
loadJson<unknown>(fetch, workspaceApiPath(workspaceId, "/workspace")), loadJson(
fetch,
workspaceApiPath(workspaceId, "/workspace"),
undefined,
parseWorkspaceResponse,
{ diagnosticLabel: "Workspace API", maxResponseBytes: 1024 * 1024 },
),
loadWorkspaceRepositoryList(fetch, workspaceId), loadWorkspaceRepositoryList(fetch, workspaceId),
]); ]);
let workspace = null; const workspace = workspaceResult.data;
let workspaceError = workspaceResult.error; const workspaceError = workspaceResult.error;
if (workspaceResult.data !== null) {
try {
workspace = parseWorkspaceResponse(workspaceResult.data);
} catch (cause) {
workspaceError = cause instanceof Error
? cause.message
: "invalid workspace response";
}
}
if (!workspace) { if (!workspace) {
error(404, { error(404, {
message: workspaceError ?? `Workspace ${workspaceId} is unavailable`, message: workspaceError ?? `Workspace ${workspaceId} is unavailable`,
@@ -1,3 +1,4 @@
import { MEMORY_API_LIMITS } from "$lib/generated/memory-api";
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import { parseMemoryDocumentResponse } from "$lib/workspace/memory/api"; import { parseMemoryDocumentResponse } from "$lib/workspace/memory/api";
import type { PageLoad } from "./$types"; import type { PageLoad } from "./$types";
@@ -10,6 +11,10 @@ export const load: PageLoad = async ({ fetch, params }) => {
workspaceApiPath(params.workspaceId, "/memory"), workspaceApiPath(params.workspaceId, "/memory"),
undefined, undefined,
parseMemoryDocumentResponse, parseMemoryDocumentResponse,
{
diagnosticLabel: "Memory API",
maxResponseBytes: MEMORY_API_LIMITS.maxResponseBytes,
},
), ),
}; };
}; };
@@ -1,3 +1,4 @@
import { MEMORY_API_LIMITS } from "$lib/generated/memory-api";
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import { parseMemoryStagingListResponse } from "$lib/workspace/memory/api"; import { parseMemoryStagingListResponse } from "$lib/workspace/memory/api";
import type { PageLoad } from "./$types"; import type { PageLoad } from "./$types";
@@ -10,6 +11,10 @@ export const load: PageLoad = async ({ fetch, params }) => {
`${workspaceApiPath(params.workspaceId, "/memory/staging")}?limit=200`, `${workspaceApiPath(params.workspaceId, "/memory/staging")}?limit=200`,
undefined, undefined,
parseMemoryStagingListResponse, parseMemoryStagingListResponse,
{
diagnosticLabel: "Memory API",
maxResponseBytes: MEMORY_API_LIMITS.maxResponseBytes,
},
), ),
}; };
}; };
@@ -12,10 +12,12 @@
RotateRepositorySshCredentialRequest, RotateRepositorySshCredentialRequest,
} from '$lib/generated/repository-access-api'; } from '$lib/generated/repository-access-api';
import { import {
REPOSITORY_ACCESS_MAX_RESPONSE_BYTES,
parseRepositorySshCredential, parseRepositorySshCredential,
parseRepositorySshHostTrust, parseRepositorySshHostTrust,
parseRepositorySshPublicKey, parseRepositorySshPublicKey,
} from '$lib/workspace/api/repository-access'; } from '$lib/workspace/api/repository-access';
import { readBoundedJson } from '$lib/workspace/api/http';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
let { data }: PageProps = $props(); let { data }: PageProps = $props();
@@ -71,13 +73,21 @@
body: JSON.stringify(body) body: JSON.stringify(body)
}); });
if (!response.ok) { if (!response.ok) {
await response.body?.cancel();
throw new Error(`Repository Access request failed with status ${response.status}.`); throw new Error(`Repository Access request failed with status ${response.status}.`);
} }
if (response.status === 204) return undefined;
const payload: unknown = await response.json();
if (parse === null) { if (parse === null) {
throw new Error('Repository Access returned an unexpected response body.'); if (response.status !== 204) {
await response.body?.cancel();
throw new Error('Repository Access returned an unexpected response body.');
}
await response.body?.cancel();
return undefined;
} }
if (response.status === 204) {
throw new Error('Repository Access returned an unexpected empty response.');
}
const payload = await readBoundedJson(response, REPOSITORY_ACCESS_MAX_RESPONSE_BYTES);
return parse(payload); return parse(payload);
} }
@@ -86,10 +96,11 @@
`${base}/credentials/${encodeURIComponent(credentialId)}/public-key`, `${base}/credentials/${encodeURIComponent(credentialId)}/public-key`,
{ headers: { accept: 'application/json' } } { headers: { accept: 'application/json' } }
); );
const body = await response.json();
if (!response.ok) { if (!response.ok) {
await response.body?.cancel();
throw new Error(`Repository Access request failed with status ${response.status}.`); throw new Error(`Repository Access request failed with status ${response.status}.`);
} }
const body = await readBoundedJson(response, REPOSITORY_ACCESS_MAX_RESPONSE_BYTES);
return parseRepositorySshPublicKey(body); return parseRepositorySshPublicKey(body);
} }
+99 -4
View File
@@ -1,11 +1,18 @@
/// <reference lib="deno.ns" /> /// <reference lib="deno.ns" />
import { assert, assertEquals } from "jsr:@std/assert"; import {
assert,
assertEquals,
assertRejects,
assertThrows,
} from "jsr:@std/assert";
import { import {
commitConfigTree, commitConfigTree,
ConfigSourceApiError,
fetchConfigEntry, fetchConfigEntry,
fetchConfigRevision, fetchConfigRevision,
fetchConfigTree, fetchConfigTree,
parseWorkspaceConfigTreeResponse,
} from "../../src/lib/workspace/config-source/api.ts"; } from "../../src/lib/workspace/config-source/api.ts";
function response(body: unknown, status = 200): Response { function response(body: unknown, status = 200): Response {
@@ -15,11 +22,43 @@ function response(body: unknown, status = 200): Response {
}); });
} }
const entry = {
path: "profiles/main.dcdl",
content_type: "decodal",
content: "profile = {}",
content_digest: "sha256:entry",
};
const snapshot = {
revision: 7,
digest: "sha256:tree",
entries: { "profiles/main.dcdl": entry },
};
const tree = {
snapshot,
contract: {
contract_version: 1,
decodal_version: "0.4.0",
schema_version: 1,
entrypoints: ["main.dcdl"],
import_policy_version: 1,
schema_bundle: {
contributions: [],
source: "builtin",
fingerprint: "sha256:schema",
},
fingerprint: "sha256:toolchain",
},
projection_digest: "sha256:projection",
};
Deno.test("config source API commits directly through the workspace scope", async () => { Deno.test("config source API commits directly through the workspace scope", async () => {
const calls: Array<{ url: string; init?: RequestInit }> = []; const calls: Array<{ url: string; init?: RequestInit }> = [];
const fetcher = ((input: string | URL | Request, init?: RequestInit) => { const fetcher = ((input: string | URL | Request, init?: RequestInit) => {
calls.push({ url: String(input), init }); const url = String(input);
return Promise.resolve(response({ ok: true })); calls.push({ url, init });
if (url.includes("/entries/")) return Promise.resolve(response(entry));
if (url.includes("/revisions/")) return Promise.resolve(response(snapshot));
return Promise.resolve(response(tree));
}) as typeof fetch; }) as typeof fetch;
await fetchConfigTree("w/one", fetcher); await fetchConfigTree("w/one", fetcher);
@@ -44,7 +83,63 @@ Deno.test("config source API commits directly through the workspace scope", asyn
); );
}); });
Deno.test("config source API surfaces failed evaluation instead of treating it as a successful commit", async () => { Deno.test("config source API rejects unknown response fields", async () => {
const fetcher =
(() =>
Promise.resolve(response({ ...tree, unexpected: true }))) as typeof fetch;
await assertRejects(
() => fetchConfigTree("w", fetcher),
ConfigSourceApiError,
"invalid response",
);
});
Deno.test("config source API rejects mismatched entry map paths", async () => {
const invalid = {
...tree,
snapshot: {
...snapshot,
entries: { "profiles/other.dcdl": entry },
},
};
const fetcher = (() => Promise.resolve(response(invalid))) as typeof fetch;
await assertRejects(
() => fetchConfigTree("w", fetcher),
ConfigSourceApiError,
"invalid response",
);
});
Deno.test("config source API rejects unsafe revisions and oversized entry content", () => {
assertThrows(
() =>
parseWorkspaceConfigTreeResponse({
...tree,
snapshot: { ...snapshot, revision: Number.MAX_SAFE_INTEGER + 1 },
}),
ConfigSourceApiError,
"invalid response",
);
assertThrows(
() =>
parseWorkspaceConfigTreeResponse({
...tree,
snapshot: {
...snapshot,
entries: {
"profiles/main.dcdl": {
...entry,
content: "x".repeat(256 * 1024 + 1),
},
},
},
}),
ConfigSourceApiError,
"invalid response",
);
});
Deno.test("config source API surfaces bounded failed evaluation details", async () => {
const fetcher = (() => const fetcher = (() =>
Promise.resolve( Promise.resolve(
new Response("structured diagnostics", { status: 422 }), new Response("structured diagnostics", { status: 422 }),