Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 33 additions & 32 deletions apps/elf-api/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
mod admin_notes;
mod admin_ops;
mod consolidation;
mod context_pack;
mod contract;
mod core_memory;
mod docs;
Expand Down Expand Up @@ -73,28 +74,28 @@ use elf_service::{
ConsolidationProposalReviewRequest, ConsolidationProposalsListRequest,
ConsolidationProposalsListResponse, ConsolidationRunCreateRequest,
ConsolidationRunCreateResponse, ConsolidationRunGetRequest, ConsolidationRunResponse,
ConsolidationRunsListRequest, ConsolidationRunsListResponse, CoreBlockAttachRequest,
CoreBlockAttachResponse, CoreBlockDetachRequest, CoreBlockDetachResponse,
CoreBlockUpsertRequest, CoreBlockUpsertResponse, CoreBlocksGetRequest, CoreBlocksResponse,
DeleteRequest, DeleteResponse, DocType, DocsDeleteRequest, DocsDeleteResponse,
DocsExcerptResponse, DocsExcerptsGetRequest, DocsGetRequest, DocsGetResponse, DocsPutRequest,
DocsPutResponse, DocsSearchL0Request, DocsSearchL0Response, DreamingReviewQueueRequest,
DreamingReviewQueueResponse, EntityMemoryViewRequest, EntityMemoryViewResponse, Error,
EventMessage, GranteeKind, GraphQueryEntityRef, GraphQueryPredicateRef, GraphQueryRequest,
GraphQueryResponse, GraphReportRequest, GraphReportResponse, IngestionProfileSelector,
KnowledgePageChangedSource, KnowledgePageGetRequest, KnowledgePageLintRequest,
KnowledgePageLintResponse, KnowledgePageRebuildRequest, KnowledgePageRebuildResponse,
KnowledgePageResponse, KnowledgePageSearchRequest, KnowledgePageSearchResponse,
KnowledgePageWatchRebuildRequest, KnowledgePageWatchRebuildResponse, KnowledgePagesListRequest,
KnowledgePagesListResponse, ListRequest, ListResponse, MemoryCorrectionAction,
MemoryCorrectionRequest, MemoryCorrectionResponse, MemoryHistoryGetRequest,
MemoryHistoryResponse, NoteFetchRequest, NoteFetchResponse, NoteProvenanceBundleResponse,
NoteProvenanceGetRequest, PayloadLevel, PublishNoteRequest, QueryPlan, RankingRequestOverride,
RebuildReport, RecallDebugPanelRequest, RecallDebugPanelResponse, SearchDetailsRequest,
SearchDetailsResult, SearchExplainRequest, SearchExplainResponse, SearchIndexItem,
SearchRequest, SearchResponse, SearchSessionGetRequest, SearchTimelineGroup,
SearchTimelineRequest, SearchTrajectoryResponse, SearchTrajectorySummary, ShareScope,
SpaceGrantRevokeRequest, SpaceGrantRevokeResponse, SpaceGrantUpsertRequest,
ConsolidationRunsListRequest, ConsolidationRunsListResponse, ContextPackRequest,
ContextPackResponse, CoreBlockAttachRequest, CoreBlockAttachResponse, CoreBlockDetachRequest,
CoreBlockDetachResponse, CoreBlockUpsertRequest, CoreBlockUpsertResponse, CoreBlocksGetRequest,
CoreBlocksResponse, DeleteRequest, DeleteResponse, DocType, DocsDeleteRequest,
DocsDeleteResponse, DocsExcerptResponse, DocsExcerptsGetRequest, DocsGetRequest,
DocsGetResponse, DocsPutRequest, DocsPutResponse, DocsSearchL0Request, DocsSearchL0Response,
DreamingReviewQueueRequest, DreamingReviewQueueResponse, EntityMemoryViewRequest,
EntityMemoryViewResponse, Error, EventMessage, GranteeKind, GraphQueryEntityRef,
GraphQueryPredicateRef, GraphQueryRequest, GraphQueryResponse, GraphReportRequest,
GraphReportResponse, IngestionProfileSelector, KnowledgePageChangedSource,
KnowledgePageGetRequest, KnowledgePageLintRequest, KnowledgePageLintResponse,
KnowledgePageRebuildRequest, KnowledgePageRebuildResponse, KnowledgePageResponse,
KnowledgePageSearchRequest, KnowledgePageSearchResponse, KnowledgePageWatchRebuildRequest,
KnowledgePageWatchRebuildResponse, KnowledgePagesListRequest, KnowledgePagesListResponse,
ListRequest, ListResponse, MemoryCorrectionAction, MemoryCorrectionRequest,
MemoryCorrectionResponse, MemoryHistoryGetRequest, MemoryHistoryResponse, NoteFetchRequest,
NoteFetchResponse, NoteProvenanceBundleResponse, NoteProvenanceGetRequest, PayloadLevel,
PublishNoteRequest, QueryPlan, RankingRequestOverride, RebuildReport, RecallDebugPanelRequest,
RecallDebugPanelResponse, SearchDetailsRequest, SearchDetailsResult, SearchExplainRequest,
SearchExplainResponse, SearchIndexItem, SearchRequest, SearchResponse, SearchSessionGetRequest,
SearchTimelineGroup, SearchTimelineRequest, SearchTrajectoryResponse, SearchTrajectorySummary,
ShareScope, SpaceGrantRevokeRequest, SpaceGrantRevokeResponse, SpaceGrantUpsertRequest,
SpaceGrantsListRequest, TextPositionSelector, TextQuoteSelector, TraceBundleGetRequest,
TraceBundleResponse, TraceGetRequest, TraceGetResponse, TraceRecentListRequest,
TraceRecentListResponse, TraceTrajectoryGetRequest, UnpublishNoteRequest, UpdateRequest,
Expand All @@ -117,16 +118,16 @@ use types::{
AdminIngestionProfileCreateBody, AdminIngestionProfileDefaultResponseV2,
AdminIngestionProfileDefaultSetBody, AdminIngestionProfileGetQuery, AdminNoteCorrectionBody,
ConsolidationProposalReviewBody, ConsolidationProposalsListQuery, ConsolidationRunCreateBody,
ConsolidationRunsListQuery, CoreBlockAttachBody, CoreBlockUpsertBody, DocsExcerptsGetBody,
DocsPutBody, DocsSearchL0Body, DreamingReviewQueueQuery, ErrorBody, EventsIngestRequest,
GraphQueryBody, GraphReportBody, KnowledgePageRebuildBody, KnowledgePageWatchRebuildBody,
KnowledgePagesListQuery, KnowledgePagesSearchBody, NotePatchRequest, NotesIngestRequest,
NotesListQuery, PublishResponseV2, RecallDebugPanelBody, SearchCreateRequest,
SearchCreateResponseV2, SearchDetailsBody, SearchDetailsResponseV2, SearchIndexResponseV2,
SearchSessionGetQuery, SearchTimelineQuery, SearchTimelineResponseV2, ShareScopeBody,
SpaceGrantItemV2, SpaceGrantUpsertBody, SpaceGrantUpsertResponseV2, SpaceGrantsListResponseV2,
TraceBundleGetQuery, TraceRecentListQuery, WorkJournalEntryCreateBody,
WorkJournalSessionReadbackBody,
ConsolidationRunsListQuery, ContextPackBody, CoreBlockAttachBody, CoreBlockUpsertBody,
DocsExcerptsGetBody, DocsPutBody, DocsSearchL0Body, DreamingReviewQueueQuery, ErrorBody,
EventsIngestRequest, GraphQueryBody, GraphReportBody, KnowledgePageRebuildBody,
KnowledgePageWatchRebuildBody, KnowledgePagesListQuery, KnowledgePagesSearchBody,
NotePatchRequest, NotesIngestRequest, NotesListQuery, PublishResponseV2, RecallDebugPanelBody,
SearchCreateRequest, SearchCreateResponseV2, SearchDetailsBody, SearchDetailsResponseV2,
SearchIndexResponseV2, SearchSessionGetQuery, SearchTimelineQuery, SearchTimelineResponseV2,
ShareScopeBody, SpaceGrantItemV2, SpaceGrantUpsertBody, SpaceGrantUpsertResponseV2,
SpaceGrantsListResponseV2, TraceBundleGetQuery, TraceRecentListQuery,
WorkJournalEntryCreateBody, WorkJournalSessionReadbackBody,
};
#[cfg(test)] use viewer::VIEWER_HTML;

Expand Down
59 changes: 59 additions & 0 deletions apps/elf-api/src/routes/context_pack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use crate::routes::{
self, ApiError, AppState, ContextPackBody, ContextPackRequest, ContextPackResponse, ErrorBody,
HeaderMap, Json, JsonRejection, RequestContext, State, StatusCode,
};

#[utoipa::path(
post,
path = "/v2/context-packs",
tag = "context-pack",
request_body = Value,
responses(
(status = 200, description = "Read-time Context Pack v1.", body = Value),
(status = 400, description = "Invalid request.", body = ErrorBody),
(status = 401, description = "Authentication required.", body = ErrorBody),
(status = 403, description = "Scope denied.", body = ErrorBody),
(status = 500, description = "Internal error.", body = ErrorBody),
)
)]
pub(super) async fn context_pack_build(
State(state): State<AppState>,
headers: HeaderMap,
payload: Result<Json<ContextPackBody>, JsonRejection>,
) -> Result<Json<ContextPackResponse>, ApiError> {
let ctx = RequestContext::from_headers(&headers)?;
let read_profile = routes::required_read_profile(&headers)?;
let Json(payload) = payload.map_err(|err| {
tracing::warn!(error = %err, "Invalid request payload.");

routes::json_error(
StatusCode::BAD_REQUEST,
"INVALID_REQUEST",
"Invalid request payload.",
None,
)
})?;
let response = state
.service
.context_pack_build(ContextPackRequest {
tenant_id: ctx.tenant_id,
project_id: ctx.project_id,
agent_id: ctx.agent_id,
read_profile,
task: payload.task,
title: payload.title,
description: payload.description,
trace_id: payload.trace_id,
query: payload.query,
docs_query: payload.docs_query,
knowledge_query: payload.knowledge_query,
graph_subject: payload.graph_subject,
graph_predicate: payload.graph_predicate,
include_dreaming: payload.include_dreaming,
limit: payload.limit,
debug_overrides: None,
})
.await?;

Ok(Json(response))
}
2 changes: 2 additions & 0 deletions apps/elf-api/src/routes/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::routes::{
__path_consolidation_proposals_list, __path_consolidation_run_create,
__path_consolidation_run_get, __path_consolidation_runs_list,
},
context_pack::__path_context_pack_build,
core_memory::{
__path_admin_core_block_attach, __path_admin_core_block_detach,
__path_admin_core_block_upsert, __path_core_blocks_get, __path_entity_memory_get,
Expand Down Expand Up @@ -92,6 +93,7 @@ pub const SCALAR_DOCS_PATH: &str = "/docs";
docs_excerpts_get,
core_blocks_get,
entity_memory_get,
context_pack_build,
admin_core_block_upsert,
admin_core_block_attach,
admin_core_block_detach,
Expand Down
1 change: 1 addition & 0 deletions apps/elf-api/src/routes/route_builder/public.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub(super) fn public_api_router() -> Router<AppState> {
.route("/v2/events/ingest", routing::post(routes::events::events_ingest))
.route("/v2/core-blocks", routing::get(routes::core_memory::core_blocks_get))
.route("/v2/entity-memory", routing::get(routes::core_memory::entity_memory_get))
.route("/v2/context-packs", routing::post(routes::context_pack::context_pack_build))
.route("/v2/recall-debug/panel", routing::post(routes::recall::recall_debug_panel))
.route("/v2/searches", routing::post(routes::search::searches_create))
.route("/v2/searches/{search_id}", routing::get(routes::search::searches_get))
Expand Down
2 changes: 2 additions & 0 deletions apps/elf-api/src/routes/types.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod consolidation;
mod context_pack;
mod core_memory;
mod docs;
mod errors;
Expand All @@ -18,6 +19,7 @@ pub(in crate::routes) use self::{
ConsolidationProposalReviewBody, ConsolidationProposalsListQuery,
ConsolidationRunCreateBody, ConsolidationRunsListQuery, DreamingReviewQueueQuery,
},
context_pack::ContextPackBody,
core_memory::{CoreBlockAttachBody, CoreBlockUpsertBody},
docs::{DocsExcerptsGetBody, DocsPutBody, DocsSearchL0Body},
errors::ErrorBody,
Expand Down
16 changes: 16 additions & 0 deletions apps/elf-api/src/routes/types/context_pack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use crate::routes::types::{Deserialize, GraphQueryEntityRef, GraphQueryPredicateRef, Uuid};

#[derive(Clone, Debug, Deserialize)]
pub(in crate::routes) struct ContextPackBody {
pub(in crate::routes) task: String,
pub(in crate::routes) title: Option<String>,
pub(in crate::routes) description: Option<String>,
pub(in crate::routes) trace_id: Option<Uuid>,
pub(in crate::routes) query: Option<String>,
pub(in crate::routes) docs_query: Option<String>,
pub(in crate::routes) knowledge_query: Option<String>,
pub(in crate::routes) graph_subject: Option<GraphQueryEntityRef>,
pub(in crate::routes) graph_predicate: Option<GraphQueryPredicateRef>,
pub(in crate::routes) include_dreaming: Option<bool>,
pub(in crate::routes) limit: Option<u32>,
}
6 changes: 3 additions & 3 deletions apps/elf-mcp/src/app/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ use rmcp::handler::server::router::tool::ToolRouter;

#[cfg(test)]
use schemas::{
docs_excerpts_get_schema, docs_put_schema, docs_search_l0_schema, notes_ingest_schema,
recall_debug_panel_schema, searches_create_schema, searches_get_schema, searches_notes_schema,
searches_timeline_schema, work_journal_entry_create_schema,
context_pack_build_schema, docs_excerpts_get_schema, docs_put_schema, docs_search_l0_schema,
notes_ingest_schema, recall_debug_panel_schema, searches_create_schema, searches_get_schema,
searches_notes_schema, searches_timeline_schema, work_journal_entry_create_schema,
work_journal_session_readback_schema,
};
use state::{ElfContextHeaders, ElfMcp, HttpMethod};
Expand Down
4 changes: 2 additions & 2 deletions apps/elf-mcp/src/app/server/schemas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ pub(in crate::app::server) use self::{
events::events_ingest_schema,
graph::{graph_query_schema, graph_report_schema},
memory::{
core_blocks_get_schema, dreaming_review_queue_schema, entity_memory_get_schema,
recall_debug_panel_schema,
context_pack_build_schema, core_blocks_get_schema, dreaming_review_queue_schema,
entity_memory_get_schema, recall_debug_panel_schema,
},
notes::{
notes_get_schema, notes_ingest_schema, notes_list_schema, notes_patch_schema,
Expand Down
71 changes: 71 additions & 0 deletions apps/elf-mcp/src/app/server/schemas/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,74 @@ pub(in crate::app::server) fn recall_debug_panel_schema() -> Arc<JsonObject> {
}
}))
}

pub(in crate::app::server) fn context_pack_build_schema() -> Arc<JsonObject> {
Arc::new(rmcp::object!({
"type": "object",
"additionalProperties": false,
"required": ["task"],
"properties": {
"task": { "type": "string" },
"title": { "type": ["string", "null"] },
"description": { "type": ["string", "null"] },
"trace_id": { "type": ["string", "null"], "format": "uuid" },
"query": { "type": ["string", "null"] },
"docs_query": { "type": ["string", "null"] },
"knowledge_query": { "type": ["string", "null"] },
"graph_subject": {
"oneOf": [
{
"type": "object",
"additionalProperties": false,
"required": ["entity_id"],
"properties": {
"entity_id": {
"type": "string",
"format": "uuid"
}
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["surface"],
"properties": {
"surface": { "type": "string" }
}
},
{ "type": "null" }
]
},
"graph_predicate": {
"oneOf": [
{
"type": "object",
"additionalProperties": false,
"required": ["predicate_id"],
"properties": {
"predicate_id": {
"type": "string",
"format": "uuid"
}
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["surface"],
"properties": {
"surface": { "type": "string" }
}
},
{ "type": "null" }
]
},
"include_dreaming": { "type": ["boolean", "null"] },
"limit": {
"type": ["integer", "null"],
"minimum": 1,
"maximum": 50
}
}
}))
}
1 change: 1 addition & 0 deletions apps/elf-mcp/src/app/server/tests/schemas.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod context_pack;
mod docs;
mod notes;
mod recall_debug;
Expand Down
47 changes: 47 additions & 0 deletions apps/elf-mcp/src/app/server/tests/schemas/context_pack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use serde_json::Value;

use crate::app::server;

#[test]
fn context_pack_schema_rejects_context_override_fields() {
let schema = server::context_pack_build_schema();
let properties = schema
.get("properties")
.and_then(Value::as_object)
.expect("context pack schema is missing properties.");
let required = schema
.get("required")
.and_then(Value::as_array)
.expect("context pack schema is missing required fields.");

assert_eq!(schema.get("additionalProperties"), Some(&Value::Bool(false)));
assert!(required.iter().any(|value| value.as_str() == Some("task")));

for key in ["tenant_id", "project_id", "agent_id", "read_profile"] {
assert!(!properties.contains_key(key), "{key} must not be a tool param.");
}

assert!(
!properties.contains_key("debug_overrides"),
"debug overrides must not be public MCP params."
);

for key in ["graph_subject", "graph_predicate"] {
let one_of = properties
.get(key)
.and_then(Value::as_object)
.and_then(|schema| schema.get("oneOf"))
.and_then(Value::as_array)
.expect("selector schema is missing oneOf.");

for branch in one_of.iter().filter_map(Value::as_object) {
if branch.get("type").and_then(Value::as_str) == Some("object") {
assert_eq!(
branch.get("additionalProperties"),
Some(&Value::Bool(false)),
"{key} selector object branches must be closed."
);
}
}
}
}
1 change: 1 addition & 0 deletions apps/elf-mcp/src/app/server/tests/tool_definitions.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub(super) mod catalog;

mod context_pack;
mod recall_debug;
mod registration;
mod search_payload;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::collections::HashMap;

use crate::app::server::HttpMethod;

const ALL_TOOL_DEFINITIONS: [ToolDefinition; 37] = [
const ALL_TOOL_DEFINITIONS: [ToolDefinition; 38] = [
ToolDefinition::new(
"elf_notes_ingest",
HttpMethod::Post,
Expand Down Expand Up @@ -51,6 +51,12 @@ const ALL_TOOL_DEFINITIONS: [ToolDefinition; 37] = [
"/v2/admin/dreaming/review-queue",
"List source-backed Dreaming review queue proposals with variants, affected refs, lint flags, policy gates, and review audit.",
),
ToolDefinition::new(
"elf_context_pack_build",
HttpMethod::Post,
"/v2/context-packs",
"Build an ephemeral Context Pack v1 as a read-time scoped view with automatic layer routing, source-backed item refs, and activation trace.",
),
ToolDefinition::new(
"elf_recall_debug_panel",
HttpMethod::Post,
Expand Down
10 changes: 10 additions & 0 deletions apps/elf-mcp/src/app/server/tests/tool_definitions/context_pack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use crate::app::server::tests::tool_definitions::catalog;

#[test]
fn context_pack_tool_uses_public_agent_route() {
let tools = catalog::build_tools();
let tool = tools.get("elf_context_pack_build").expect("Missing context pack tool.");

assert_eq!(tool.path, "/v2/context-packs");
assert!(tool.description.contains("read-time scoped view"));
}
Loading