feat(worker): project objectives as native WIP objects

This commit is contained in:
2026-10-04 01:28:45 +09:00
parent e2181e9052
commit e5f255b6fd
7 changed files with 1756 additions and 161 deletions
+37 -9
View File
@@ -1551,13 +1551,16 @@ where
}
let host_worker_observation_provider = worker.worker_observation_provider();
let wip_mode = worker.manifest().worker.mode == manifest::WorkerMode::Wip;
let wip_permissions = worker.manifest().permissions.clone();
let mut wip_mount_registry = crate::wip::WipMountRegistry::new();
{
let workspace_client = worker.workspace_client_handle();
let engine = worker.engine_mut();
// Objective tools expose read-only project Objective context through the
// Backend Workspace API. Workers must not guess local `.yoi/objectives`
// paths or read Objective files directly.
// Objective tools expose scoped project Objective reads and mutations through
// the Backend Workspace API. Workers must not guess local `.yoi/objectives`
// paths or read/edit Objective files directly.
if feature_config.objective.enabled {
if workspace_client.is_available() && workspace_client.workspace_id().is_some() {
for definition in crate::feature::builtin::objective::workspace_http_objective_tools(
@@ -1565,6 +1568,28 @@ where
) {
engine.register_tool(definition);
}
if wip_mode {
let feature_route = wip_mount_registry
.allocate_feature_route("objective")
.map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("allocate Objective WIP route: {error}"),
)
})?;
crate::feature::builtin::objective::mount_workspace_http_objective_wip(
&mut wip_mount_registry,
workspace_client.clone(),
wip_permissions.clone(),
&feature_route,
)
.map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("mount Objective WIP projection: {error}"),
)
})?;
}
} else {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
@@ -1631,22 +1656,25 @@ where
if let Some((resident_summary, system_prompt_override)) = feature_prompt_contribution {
worker.install_system_prompt_contribution(resident_summary, system_prompt_override);
}
if worker.manifest().worker.mode == manifest::WorkerMode::Wip {
let permissions = worker.manifest().permissions.clone();
if wip_mode {
let security_context = format!(
"workspace={};worker={};session={}",
worker.workspace_id().map_or("standalone", |id| id.as_str()),
worker.manifest().worker.name,
worker.session_id()
);
crate::wip::install_wip_mode(worker.engine_mut(), permissions, security_context).map_err(
|error| {
crate::wip::install_wip_mode_with_mounts(
worker.engine_mut(),
wip_permissions,
security_context,
wip_mount_registry,
)
.map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("install WIP Worker surface: {error}"),
)
},
)?;
})?;
}
worker.attach_tracker(tracker);
Ok(workdir_for_view)
File diff suppressed because it is too large Load Diff
@@ -78,7 +78,9 @@ pub(super) struct ModelObjectiveDetail {
objective: String,
title: String,
body: String,
body_truncated: bool,
state: String,
revision: String,
created_at: Option<String>,
updated_at: Option<String>,
linked_tickets: Vec<ModelTicketSummary>,
@@ -86,12 +88,6 @@ pub(super) struct ModelObjectiveDetail {
event_page: ModelObjectiveEventPage,
}
impl ModelObjectiveDetail {
pub(super) fn objective_ref(&self) -> &str {
&self.objective
}
}
#[derive(Debug, Serialize)]
struct ModelWorkerSummary {
worker: String,
@@ -363,7 +359,9 @@ pub(super) fn project_objective_detail(value: Value) -> Result<ModelObjectiveDet
objective: resource_ref(root, "resource_key", "O-")?,
title: string_field(root, "title")?,
body: string_field(root, "body")?,
body_truncated: bool_field(root, "body_truncated")?,
state: string_field(root, "state")?,
revision: string_field(root, "revision")?,
created_at: optional_string(root, "created_at")?,
updated_at: optional_string(root, "updated_at")?,
linked_tickets: array_field(root, "linked_ticket_summaries")?
@@ -671,7 +669,9 @@ mod tests {
"resource_key": "O-543",
"title": "Objective",
"body": "Body",
"body_truncated": false,
"state": "active",
"revision": "rev-3",
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-02T00:00:00Z",
"linked_tickets": ["00001M0E82D1V"],
@@ -695,6 +695,8 @@ mod tests {
let text = json.to_string();
assert!(text.contains("O-543"));
assert!(text.contains("T-496"));
assert_eq!(json["revision"], "rev-3");
assert_eq!(json["body_truncated"], false);
assert!(!text.contains("00001M10HW6BV"));
assert!(!text.contains("00001M0E82D1V"));
assert!(!text.contains("event_ref"));
+422 -13
View File
@@ -127,6 +127,7 @@ pub trait WipOperationHandler: Send + Sync {
/// One route contribution. Native projections replace compatibility projections
/// only when both declare the same semantic capability; unrelated collisions fail.
#[derive(Clone)]
pub struct WipProjection {
pub route: String,
pub capability: String,
@@ -138,6 +139,55 @@ pub struct WipProjection {
pub handler: Arc<dyn WipOperationHandler>,
}
/// One dynamically resolved direct-child object beneath a Feature-owned
/// collection route. The Host retains route allocation and descriptor authority;
/// the Feature only binds a validated child segment to an object and handler.
pub struct WipDynamicItem {
pub object: Object,
pub handler: Arc<dyn WipOperationHandler>,
}
pub trait WipDynamicItemResolver: Send + Sync {
fn resolve(&self, item_reference: &str) -> Option<WipDynamicItem>;
}
pub struct WipDynamicMount {
pub collection_route: String,
pub capability: String,
pub interface: String,
pub descriptor: InterfaceDescriptor,
pub interface_validator: Option<Vec<u8>>,
pub resolver: Arc<dyn WipDynamicItemResolver>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WipFeatureRoute {
root: String,
}
impl WipFeatureRoute {
pub fn root(&self) -> &str {
&self.root
}
pub fn child(&self, segment: &str) -> Result<String, WipMountError> {
if segment.is_empty()
|| segment == "."
|| segment == ".."
|| segment.contains('/')
|| !segment
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
{
return Err(WipMountError::InvalidRoute {
route: format!("{}/{}", self.root, segment),
message: "Feature child must be one canonical path segment".into(),
});
}
Ok(format!("{}/{}", self.root, segment))
}
}
struct MountedProjection {
projection: WipProjection,
}
@@ -145,6 +195,9 @@ struct MountedProjection {
#[derive(Default)]
pub struct WipMountRegistry {
mounts: BTreeMap<String, MountedProjection>,
dynamic_mounts: Vec<WipDynamicMount>,
feature_routes: BTreeSet<String>,
replaced_compatibility_capabilities: BTreeMap<String, String>,
}
impl WipMountRegistry {
@@ -152,11 +205,47 @@ impl WipMountRegistry {
Self::default()
}
/// Allocate the Host namespace owned by one enabled Feature. Features join
/// checked relative segments to this route instead of selecting global paths.
pub fn allocate_feature_route(
&mut self,
feature: &str,
) -> Result<WipFeatureRoute, WipMountError> {
if feature.is_empty()
|| !feature
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
{
return Err(WipMountError::InvalidRoute {
route: format!("/features/{feature}"),
message: "Feature name must use lowercase ASCII letters, digits, or '-'".into(),
});
}
let root = format!("/features/{feature}");
if !self.feature_routes.insert(root.clone()) {
return Err(WipMountError::RouteCollision {
route: root,
existing: format!("feature:{feature}"),
});
}
Ok(WipFeatureRoute { root })
}
pub fn mount(
&mut self,
projection: WipProjection,
) -> Result<WipMountDisposition, WipMountError> {
validate_projection(&projection)?;
if self.dynamic_mounts.iter().any(|dynamic| {
projection
.route
.starts_with(&format!("{}/", dynamic.collection_route))
}) {
return Err(WipMountError::RouteCollision {
route: projection.route,
existing: "dynamic Feature collection".into(),
});
}
if let Some(existing) = self.mounts.get(&projection.route) {
if existing.projection.capability != projection.capability {
return Err(WipMountError::RouteCollision {
@@ -188,16 +277,150 @@ impl WipMountRegistry {
Ok(WipMountDisposition::Mounted)
}
/// Mount a one-segment item family below an already mounted collection.
/// Dynamic resolution never creates authority: the collection route must be
/// native and every resolved child is validated before it is exposed.
pub fn mount_dynamic(&mut self, mount: WipDynamicMount) -> Result<(), WipMountError> {
wip_protocol::validate_path(&mount.collection_route).map_err(|error| {
WipMountError::InvalidRoute {
route: mount.collection_route.clone(),
message: error.to_string(),
}
})?;
let Some(collection) = self.mounts.get(&mount.collection_route) else {
return Err(WipMountError::InvalidProjection {
route: mount.collection_route,
message: "dynamic item mount requires an existing collection projection".into(),
});
};
if collection.projection.kind != WipProjectionKind::Native {
return Err(WipMountError::InvalidProjection {
route: mount.collection_route,
message: "dynamic item mount requires a native collection projection".into(),
});
}
mount
.descriptor
.validate()
.map_err(|error| WipMountError::InvalidProjection {
route: mount.collection_route.clone(),
message: error.to_string(),
})?;
if let Some(existing) = self.dynamic_mounts.iter().find(|existing| {
existing.collection_route == mount.collection_route
|| mount
.collection_route
.starts_with(&format!("{}/", existing.collection_route))
|| existing
.collection_route
.starts_with(&format!("{}/", mount.collection_route))
}) {
return Err(WipMountError::RouteCollision {
route: mount.collection_route,
existing: existing.capability.clone(),
});
}
if let Some((_, existing)) = self
.mounts
.iter()
.find(|(route, _)| route.starts_with(&format!("{}/", mount.collection_route)))
{
return Err(WipMountError::RouteCollision {
route: mount.collection_route,
existing: existing.projection.capability.clone(),
});
}
if self.interface_conflicts(
&mount.interface,
&mount.descriptor,
mount.interface_validator.as_deref(),
None,
) {
return Err(WipMountError::InterfaceCollision {
interface: mount.interface,
});
}
self.dynamic_mounts.push(mount);
Ok(())
}
/// Declare ordinary tool capabilities fully represented by native mounts.
/// Claimed compatibility projections are removed from WIP mode while the
/// normal Tool-mode registration remains unchanged.
pub fn replace_compatibility_tools<'a>(
&mut self,
owner_route: &str,
tool_names: impl IntoIterator<Item = &'a str>,
) -> Result<(), WipMountError> {
if !self.mounts.contains_key(owner_route)
&& !self
.dynamic_mounts
.iter()
.any(|mount| mount.collection_route == owner_route)
{
return Err(WipMountError::InvalidProjection {
route: owner_route.to_string(),
message: "compatibility replacement owner is not mounted".into(),
});
}
let capabilities = tool_names
.into_iter()
.map(|name| format!("tool:{name}"))
.collect::<Vec<_>>();
for capability in &capabilities {
if let Some(existing) = self
.replaced_compatibility_capabilities
.get(capability)
.filter(|existing| existing.as_str() != owner_route)
{
return Err(WipMountError::RouteCollision {
route: owner_route.to_string(),
existing: existing.clone(),
});
}
}
for capability in capabilities {
self.replaced_compatibility_capabilities
.insert(capability, owner_route.to_string());
}
Ok(())
}
fn replaces_compatibility(&self, capability: &str) -> bool {
self.replaced_compatibility_capabilities
.contains_key(capability)
}
fn interface_conflicts(
&self,
interface: &str,
descriptor: &InterfaceDescriptor,
validator: Option<&[u8]>,
replacing_route: Option<&str>,
) -> bool {
self.mounts.iter().any(|(route, mounted)| {
Some(route.as_str()) != replacing_route
&& mounted.projection.interface == interface
&& (mounted.projection.descriptor != *descriptor
|| mounted.projection.interface_validator.as_deref() != validator)
}) || self.dynamic_mounts.iter().any(|mounted| {
mounted.interface == interface
&& (mounted.descriptor != *descriptor
|| mounted.interface_validator.as_deref() != validator)
})
}
fn ensure_interface_available(
&self,
candidate: &WipProjection,
replacing_route: Option<&str>,
) -> Result<(), WipMountError> {
if self.mounts.iter().any(|(route, mounted)| {
Some(route.as_str()) != replacing_route
&& mounted.projection.interface == candidate.interface
&& mounted.projection.descriptor != candidate.descriptor
}) {
if self.interface_conflicts(
&candidate.interface,
&candidate.descriptor,
candidate.interface_validator.as_deref(),
replacing_route,
) {
return Err(WipMountError::InterfaceCollision {
interface: candidate.interface.clone(),
});
@@ -274,13 +497,38 @@ impl WipHost {
mounted.projection.interface_validator.as_deref(),
)
})
.or_else(|| {
self.registry
.dynamic_mounts
.iter()
.find(|mounted| mounted.interface == reference)
.map(|mounted| (&mounted.descriptor, mounted.interface_validator.as_deref()))
})
}
fn projection(&self, path: &str) -> Option<&WipProjection> {
self.registry
.mounts
.get(path)
.map(|mounted| &mounted.projection)
fn projection(&self, path: &str) -> Option<WipProjection> {
if let Some(mounted) = self.registry.mounts.get(path) {
return Some(mounted.projection.clone());
}
self.registry.dynamic_mounts.iter().find_map(|mounted| {
let item_reference = path.strip_prefix(&format!("{}/", mounted.collection_route))?;
if item_reference.is_empty() || item_reference.contains('/') {
return None;
}
let item = mounted.resolver.resolve(item_reference)?;
let projection = WipProjection {
route: path.to_string(),
capability: mounted.capability.clone(),
kind: WipProjectionKind::Native,
object: item.object,
interface: mounted.interface.clone(),
descriptor: mounted.descriptor.clone(),
interface_validator: mounted.interface_validator.clone(),
handler: item.handler,
};
validate_projection(&projection).ok()?;
Some(projection)
})
}
fn object_at(&self, path: &str) -> Option<Object> {
@@ -1358,6 +1606,7 @@ fn wip_tool_definitions(runtime: Arc<WipRuntime>) -> Vec<ToolDefinition> {
/// Replace all currently registered ordinary tools with one WIP client surface.
/// Call this once after every enabled Feature has contributed its tools.
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn install_wip_mode<C: LlmClient, A: Send + Sync>(
engine: &mut Engine<C, Mutable, A>,
permissions: Option<ToolPermissionConfig>,
@@ -1403,12 +1652,16 @@ pub fn install_wip_mode_with_mounts<C: LlmClient, A: Send + Sync>(
let (meta, tool) = handle
.get_tool(&definition.name)
.ok_or_else(|| format!("registered tool `{}` disappeared", definition.name))?;
let capability = format!("tool:{}", meta.name);
let name = meta.name.clone();
if !registry.replaces_compatibility(&capability) {
let projection = compatibility_projection(meta, tool, permissions.clone())
.map_err(|error| error.to_string())?;
registry
.mount(projection)
.map_err(|error| error.to_string())?;
names.push(definition.name);
}
names.push(name);
}
let runtime = Arc::new(WipRuntime::new(
WipHost::new(registry),
@@ -1632,7 +1885,7 @@ fn decode_native_arguments(
.map_err(|error| format!("native WIP arguments do not match the descriptor: {error}"))
}
fn json_to_wip(value: &Json) -> Result<Value, String> {
pub(crate) fn json_to_wip(value: &Json) -> Result<Value, String> {
match value {
Json::Null => Ok(Value::Unit),
Json::Bool(value) => Ok(Value::Boolean(*value)),
@@ -1670,7 +1923,7 @@ fn json_to_wip(value: &Json) -> Result<Value, String> {
}
}
fn wip_to_json(value: &Value) -> Result<Json, String> {
pub(crate) fn wip_to_json(value: &Value) -> Result<Json, String> {
match value {
Value::Unit => Ok(Json::Null),
Value::Boolean(value) => Ok(Json::Bool(*value)),
@@ -2227,6 +2480,162 @@ mod tests {
assert_eq!(state.session.security_context().as_str(), "worker-a");
}
struct DynamicResolver {
revision: Arc<AtomicUsize>,
calls: Arc<AtomicUsize>,
}
struct DynamicHandler {
calls: Arc<AtomicUsize>,
}
#[async_trait]
impl WipOperationHandler for DynamicHandler {
async fn call(
&self,
_operation: &str,
_arguments: &BTreeMap<String, Value>,
_context: WipCallContext,
) -> Result<WipOperationOutput, WipOperationError> {
self.calls.fetch_add(1, Ordering::SeqCst);
Ok(WipOperationOutput::native(Value::Unit))
}
}
impl WipDynamicItemResolver for DynamicResolver {
fn resolve(&self, item_reference: &str) -> Option<WipDynamicItem> {
let sequence = item_reference.strip_prefix("O-")?;
if sequence.is_empty() || !sequence.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
Some(WipDynamicItem {
object: Object {
name: item_reference.into(),
description: None,
interfaces: vec!["test.dynamic/v1".into()],
r#ref: Some(format!("objective:{item_reference}")),
validator: Some(vec![self.revision.load(Ordering::SeqCst) as u8]),
},
handler: Arc::new(DynamicHandler {
calls: Arc::clone(&self.calls),
}),
})
}
}
fn dynamic_registry(revision: Arc<AtomicUsize>, calls: Arc<AtomicUsize>) -> WipMountRegistry {
let mut registry = WipMountRegistry::new();
let feature = registry.allocate_feature_route("objective").unwrap();
let collection_route = feature.child("objectives").unwrap();
let mut collection = compatibility_projection(
meta("Dynamic"),
Arc::new(EchoTool {
calls: Arc::clone(&calls),
}),
None,
)
.unwrap();
collection.route = collection_route.clone();
collection.capability = "objective:collection".into();
collection.kind = WipProjectionKind::Native;
collection.object.name = "objectives".into();
collection.object.interfaces = vec!["test.dynamic/v1".into()];
collection.interface = "test.dynamic/v1".into();
collection.handler = Arc::new(DynamicHandler {
calls: Arc::clone(&calls),
});
let descriptor = collection.descriptor.clone();
let interface_validator = collection.interface_validator.clone();
registry.mount(collection).unwrap();
registry
.mount_dynamic(WipDynamicMount {
collection_route,
capability: "objective:item".into(),
interface: "test.dynamic/v1".into(),
descriptor,
interface_validator,
resolver: Arc::new(DynamicResolver { revision, calls }),
})
.unwrap();
registry
}
#[tokio::test]
async fn dynamic_routes_resolve_only_direct_children_and_reject_stale_calls() {
let revision = Arc::new(AtomicUsize::new(1));
let calls = Arc::new(AtomicUsize::new(0));
let host = WipHost::new(dynamic_registry(Arc::clone(&revision), Arc::clone(&calls)));
let initial = host
.projection("/features/objective/objectives/O-3")
.unwrap();
assert!(
host.projection("/features/objective/objectives/O-3/other")
.is_none()
);
assert!(
host.projection("/features/objective/objectives/T-3")
.is_none()
);
assert!(host.projection("/hidden/objectives/O-3").is_none());
revision.store(2, Ordering::SeqCst);
let result = host
.call(
CallOperationRequest {
target: wip_protocol::Target {
path: "/features/objective/objectives/O-3".into(),
validator: initial.object.validator,
},
interface: wip_protocol::InterfaceTarget {
reference: initial.interface,
validator: initial.interface_validator,
},
operation: "call".into(),
arguments: BTreeMap::from([(
"input".into(),
json_to_wip(&json!({"message": "ok"})).unwrap(),
)]),
},
WipCallContext {
execution: ToolExecutionContext::direct(),
security_context: "worker-a".into(),
},
)
.await;
assert!(matches!(
result,
Err(WipOperationError::Protocol(ProtocolError {
code: ProtocolErrorCode::ValidatorMismatch,
..
}))
));
assert_eq!(calls.load(Ordering::SeqCst), 0);
}
#[test]
fn native_capability_claim_hides_only_claimed_compatibility_route() {
let calls = Arc::new(AtomicUsize::new(0));
let mut engine = Engine::<_, Mutable, ()>::new_annotated(DummyClient);
engine.register_tool(echo_definition("Echo".into(), Arc::clone(&calls)));
engine.register_tool(echo_definition("Other".into(), Arc::clone(&calls)));
let revision = Arc::new(AtomicUsize::new(1));
let mut registry = dynamic_registry(revision, calls);
registry
.replace_compatibility_tools("/features/objective/objectives", ["Echo"])
.unwrap();
let runtime =
install_wip_mode_with_mounts(&mut engine, None, "worker-a".into(), registry).unwrap();
assert!(runtime.host.projection("/tools/Echo").is_none());
assert!(runtime.host.projection("/tools/Other").is_some());
assert!(
runtime
.host
.projection("/features/objective/objectives")
.is_some()
);
}
struct FailingTool {
cancelled: bool,
}
+1
View File
@@ -34,6 +34,7 @@ It is not a dumping ground for external research, old plans, API inventories, or
26. [`design/subjektiv-surface.md`](design/subjektiv-surface.md) — confirmed Memoryから生成するresident surface、availability、append-only restore injection。
27. [`development/subjektiv-product-cutover.md`](development/subjektiv-product-cutover.md) — legacy Workspace Memoryからsubjektivへのoperator cutover、explicit reset、backup/rollback runbook。
28. [`design/worker-wip-mode.md`](design/worker-wip-mode.md) — explicit-opt-in Worker WIP mode, Worldspace projection, authority, cache, and retry contracts.
29. [`design/objective-wip-projection.md`](design/objective-wip-projection.md) — Host-assigned Objective collection/item routes, operation mapping, Backend authority, bounds, and compatibility suppression.
## What belongs here
+55
View File
@@ -0,0 +1,55 @@
# Objective native WIP projection
## Purpose and authority
When a Worker explicitly runs in `wip` mode and the Objective Feature is enabled, the Worker Host projects Objectives as native Worldspace objects. The projection is an adapter over the existing scoped Workspace API. It does not introduce another Objective store, state machine, reference resolver, or audit path.
The Host allocates `/features/objective` to the Feature through `WipMountRegistry`. The Objective Feature then mounts:
```text
/features/objective/objectives
/features/objective/objectives/<objective-reference>
```
`<objective-reference>` is either a canonical `O-<digits>` key or an existing canonical internal alphanumeric ID. Only one direct child segment is accepted. Nested suffixes, Ticket keys, and unrelated or hidden prefixes do not resolve as Objective objects.
The Backend remains authoritative for existence, `O-*`/internal-ID resolution, Workspace scope, validation, audit events, revision generation, pagination, body/event bounds, and linked-Ticket validation. A discovered route is not independent authority; every operation still uses the Worker's current `WorkspaceClient`.
## Current operation inventory
The collection interface is `yoi.objective/collection/v1`:
| Native operation | Existing Tool permission identity | Parameters |
| --- | --- | --- |
| `query` | `QueryObjective` | `query`, `states`, `linked_ticket_id`, `updated_after`, `updated_before`, `sort`, `limit`, `cursor` |
| `create` | `ObjectiveCreate` | `title`, `body_md`, `state`, `linked_tickets` |
The item interface is `yoi.objective/item/v1`:
| Native operation | Existing Tool permission identity | Parameters |
| --- | --- | --- |
| `read` | `ShowObjective` | `event_limit`, `event_cursor` |
| `edit` | `ObjectiveEdit` | `title`, `old_string`, `new_string`, `replace_all` |
| `set_state` | `ObjectiveSetState` | `state` |
| `link_ticket` | `ObjectiveLinkTicket` | `ticket_id` |
| `unlink_ticket` | `ObjectiveUnlinkTicket` | `ticket_id` |
The item operations intentionally do not accept an Objective `id`. The handler is created from the resolved item route and reconstructs the equivalent ordinary Tool permission arguments with that route-bound identity. Descriptor validation rejects a caller attempting to supply a second Objective target.
`deny` and `ask` permission results fail closed before Backend dispatch. Backend 401/403 map to permission denial, 404 to missing target, and deterministic client validation failures to invalid arguments. Transport, server, or response-projection failures after dispatch retain the common unknown-outcome contract; mutating operations must not be retried automatically when the outcome is unknown.
## Bounded reads and references
`query` retains the ordinary Worker limit of 1–100 and returns stable snippets, canonical `O-*` references, linked `T-*` references, `next_cursor`, and `has_more`. Each result also includes its canonical item path. It does not expand Objective bodies, complete events, or Ticket records.
`read` retains the ordinary Worker event limit of 1–50 and projects the Backend-bounded body, `body_truncated`, authoritative `revision`, linked-Ticket summaries, bounded events, and event pagination. Internal Objective, Ticket, and event-storage IDs are removed from model-visible results.
`create` returns the authoritative Objective detail plus its canonical item path. Mutations return the authoritative post-operation detail. The revision observed in a successful read or mutation updates the item's object validator. The common Host validator contract rejects stale object/interface calls before dispatch, and the call response carries the refreshed route validator.
Linked Ticket references remain strings passed to the existing Objective Backend operation. Their presence does not publish a Ticket object, grant Ticket read or mutation authority, or require a Ticket-native WIP projection.
## Compatibility and Tool mode
The native collection/item pair claims all seven Objective compatibility capabilities in WIP mode. Therefore `/tools/QueryObjective`, `/tools/ShowObjective`, and `/tools/Objective*` are not published alongside the native routes. Unrelated enabled tools keep their compatibility projections.
Normal `tools` mode is unchanged: it still exposes the same seven Objective tools with their existing names and input schemas. Both surfaces share the same `WorkspaceHttpObjectiveBackend` and scoped Backend endpoints.
+5 -4
View File
@@ -52,14 +52,15 @@ The interface descriptor uses WIP `Json` for compatibility input and includes th
## Native projection extension and collisions
`worker::wip::WipMountRegistry` is the Host-owned mount allocator. A Feature can independently construct a `WipProjection` with an async `WipOperationHandler`, then pass its aggregate registry through `install_wip_mode_with_mounts` before compatibility finalization.
`worker::wip::WipMountRegistry` is the Host-owned mount allocator. A Feature can independently construct a `WipProjection` with an async `WipOperationHandler`, then pass its aggregate registry through `install_wip_mode_with_mounts` before compatibility finalization. A Feature first requests a Host-owned `/features/<feature>` namespace with `allocate_feature_route`; checked direct-child joins prevent a Feature from claiming an ambient global route.
- Routes must be canonical absolute WIP paths.
- Object names must match the final route segment.
- A projected object exposes exactly its declared interface.
- Unrelated duplicate routes and conflicting interface descriptors fail startup.
- A native projection replaces a compatibility projection only when both declare the same semantic capability (`tool:<registration-name>` for compatibility tools).
- When native is already selected, the compatibility tool is hidden rather than exposed through a second entry.
- Unrelated duplicate routes and conflicting interface descriptors or validators fail startup.
- A bounded dynamic mount may resolve exactly one item segment below an already mounted native collection; deeper paths never fall through to the resolver.
- A native projection replaces a compatibility projection either by the original same-route semantic capability or by an explicit registry capability claim owned by a mounted native route.
- Conflicting native claims fail startup. When native is selected, the claimed compatibility tool is hidden rather than exposed through a second entry. Normal Tool mode is unaffected because it does not install the WIP registry.
`WipInspect` renders the complete descriptor, including descriptor-local named declarations and recursive record, list, enum, and union shapes with required flags and declaration documentation. The unit samples `mount_collision_and_native_replacement_are_explicit` and `native_projection_preserves_descriptor_and_decodes_typed_arguments_end_to_end` demonstrate independent native registration, deterministic replacement, collision rejection, complete descriptor inspection, and a descriptor-typed native invocation.