feat: integrate host-owned WIP namespaces
This commit is contained in:
@@ -1593,12 +1593,12 @@ where
|
||||
let engine = worker.engine_mut();
|
||||
|
||||
if feature_config.merge_request.any() && wip_mode {
|
||||
let feature_route = wip_mount_registry
|
||||
.allocate_feature_route("merge-request")
|
||||
let namespace_route = wip_mount_registry
|
||||
.allocate_namespace("merge-request", "merge-requests")
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("allocate Merge Request WIP route: {error}"),
|
||||
format!("allocate Merge Request WIP namespace: {error}"),
|
||||
)
|
||||
})?;
|
||||
crate::feature::builtin::merge_request::mount_workspace_http_merge_request_wip(
|
||||
@@ -1606,7 +1606,7 @@ where
|
||||
workspace_client.clone(),
|
||||
feature_config.merge_request,
|
||||
wip_permissions.clone(),
|
||||
&feature_route,
|
||||
&namespace_route,
|
||||
)
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(
|
||||
@@ -1623,12 +1623,12 @@ where
|
||||
intake: feature_config.ticket.intake,
|
||||
workflow: feature_config.ticket.workflow,
|
||||
};
|
||||
let feature_route = wip_mount_registry
|
||||
.allocate_feature_route("ticket")
|
||||
let namespace_route = wip_mount_registry
|
||||
.allocate_namespace("ticket", "tickets")
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("allocate Ticket WIP route: {error}"),
|
||||
format!("allocate Ticket WIP namespace: {error}"),
|
||||
)
|
||||
})?;
|
||||
crate::feature::builtin::ticket::mount_workspace_http_ticket_wip(
|
||||
@@ -1636,7 +1636,7 @@ where
|
||||
workspace_client.clone(),
|
||||
ticket_access,
|
||||
wip_permissions.clone(),
|
||||
&feature_route,
|
||||
&namespace_route,
|
||||
)
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(
|
||||
@@ -1657,19 +1657,19 @@ where
|
||||
engine.register_tool(definition);
|
||||
}
|
||||
if wip_mode {
|
||||
let feature_route = wip_mount_registry
|
||||
.allocate_feature_route("objective")
|
||||
let namespace_route = wip_mount_registry
|
||||
.allocate_namespace("objective", "objectives")
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("allocate Objective WIP route: {error}"),
|
||||
format!("allocate Objective WIP namespace: {error}"),
|
||||
)
|
||||
})?;
|
||||
crate::feature::builtin::objective::mount_workspace_http_objective_wip(
|
||||
&mut wip_mount_registry,
|
||||
workspace_client.clone(),
|
||||
wip_permissions.clone(),
|
||||
&feature_route,
|
||||
&namespace_route,
|
||||
)
|
||||
.map_err(|error| {
|
||||
std::io::Error::new(
|
||||
|
||||
@@ -5,9 +5,9 @@ use crate::feature::{
|
||||
};
|
||||
use crate::permission::permission_action_for;
|
||||
use crate::wip::{
|
||||
WipCallContext, WipDynamicItem, WipDynamicItemResolver, WipDynamicMount, WipFeatureRoute,
|
||||
WipMountError, WipMountRegistry, WipOperationError, WipOperationHandler, WipOperationOutput,
|
||||
WipProjection, WipProjectionKind, json_to_wip, wip_to_json,
|
||||
WipCallContext, WipDynamicItem, WipDynamicItemResolver, WipDynamicMount, WipMountError,
|
||||
WipMountRegistry, WipNamespaceRoute, WipOperationError, WipOperationHandler,
|
||||
WipOperationOutput, WipProjection, WipProjectionKind, json_to_wip, wip_to_json,
|
||||
};
|
||||
use crate::worker::{WorkspaceClient, WorkspaceRequest, WorkspaceRequestMethod};
|
||||
use agen::tool::{Tool, ToolError, ToolExecutionContext, ToolMeta, ToolOutput};
|
||||
@@ -529,7 +529,7 @@ fn native_merge_request_tools(
|
||||
let (meta, tool) = definition(client.clone(), kind)();
|
||||
jsonschema::validator_for(&meta.input_schema).map_err(|error| {
|
||||
WipMountError::InvalidProjection {
|
||||
route: "/features/merge-request/merge-requests".into(),
|
||||
route: "/merge-requests".into(),
|
||||
message: format!("Merge Request tool schema cannot be retained: {error}"),
|
||||
}
|
||||
})?;
|
||||
@@ -562,9 +562,9 @@ pub fn mount_workspace_http_merge_request_wip(
|
||||
client: Arc<dyn WorkspaceClient>,
|
||||
config: MergeRequestFeatureConfig,
|
||||
permissions: Option<ToolPermissionConfig>,
|
||||
feature_route: &WipFeatureRoute,
|
||||
namespace_route: &WipNamespaceRoute,
|
||||
) -> Result<(), WipMountError> {
|
||||
let collection_route = feature_route.child("merge-requests")?;
|
||||
let collection_route = namespace_route.root().to_string();
|
||||
let tools = native_merge_request_tools(client, config)?;
|
||||
let claimed_tools = tools
|
||||
.iter()
|
||||
@@ -1049,7 +1049,7 @@ fn merge_request_operation_declaration(
|
||||
.get("properties")
|
||||
.and_then(Value::as_object)
|
||||
.ok_or_else(|| WipMountError::InvalidProjection {
|
||||
route: "/features/merge-request/merge-requests".into(),
|
||||
route: "/merge-requests".into(),
|
||||
message: format!(
|
||||
"Merge Request tool `{}` schema has no properties",
|
||||
tool.name
|
||||
@@ -1474,7 +1474,9 @@ mod tests {
|
||||
#[test]
|
||||
fn native_mount_uses_host_route_and_accepts_only_one_canonical_item_segment() {
|
||||
let mut registry = WipMountRegistry::new();
|
||||
let feature_route = registry.allocate_feature_route("merge-request").unwrap();
|
||||
let namespace_route = registry
|
||||
.allocate_namespace("merge-request", "merge-requests")
|
||||
.unwrap();
|
||||
mount_workspace_http_merge_request_wip(
|
||||
&mut registry,
|
||||
Arc::new(TestWorkspaceHttpClient::new("workspace", "http://unused")),
|
||||
@@ -1484,17 +1486,14 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
&feature_route,
|
||||
&namespace_route,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
registry.routes().collect::<Vec<_>>(),
|
||||
["/features/merge-request/merge-requests"]
|
||||
);
|
||||
assert_eq!(registry.routes().collect::<Vec<_>>(), ["/merge-requests"]);
|
||||
let resolver = MergeRequestItemResolver {
|
||||
tools: HashMap::new(),
|
||||
permissions: None,
|
||||
collection_route: "/features/merge-request/merge-requests".into(),
|
||||
collection_route: "/merge-requests".into(),
|
||||
revisions: Arc::new(Mutex::new(MergeRequestRevisionState::default())),
|
||||
};
|
||||
assert!(
|
||||
@@ -1645,7 +1644,7 @@ mod tests {
|
||||
let resolver = MergeRequestItemResolver {
|
||||
tools: HashMap::new(),
|
||||
permissions: None,
|
||||
collection_route: "/features/merge-request/merge-requests".into(),
|
||||
collection_route: "/merge-requests".into(),
|
||||
revisions: Arc::clone(&revisions),
|
||||
};
|
||||
let first = resolver.resolve("MR-1").unwrap().object.validator;
|
||||
|
||||
@@ -21,9 +21,9 @@ use wip_protocol::{
|
||||
|
||||
use crate::permission::permission_action_for;
|
||||
use crate::wip::{
|
||||
WipCallContext, WipDynamicItem, WipDynamicItemResolver, WipDynamicMount, WipFeatureRoute,
|
||||
WipMountError, WipMountRegistry, WipOperationError, WipOperationHandler, WipOperationOutput,
|
||||
WipProjection, WipProjectionKind, json_to_wip, wip_to_json,
|
||||
WipCallContext, WipDynamicItem, WipDynamicItemResolver, WipDynamicMount, WipMountError,
|
||||
WipMountRegistry, WipNamespaceRoute, WipOperationError, WipOperationHandler,
|
||||
WipOperationOutput, WipProjection, WipProjectionKind, json_to_wip, wip_to_json,
|
||||
};
|
||||
use crate::worker::{WorkspaceClient, WorkspaceRequest, WorkspaceRequestMethod};
|
||||
|
||||
@@ -551,9 +551,9 @@ pub fn mount_workspace_http_objective_wip(
|
||||
registry: &mut WipMountRegistry,
|
||||
client: Arc<dyn WorkspaceClient>,
|
||||
permissions: Option<ToolPermissionConfig>,
|
||||
feature_route: &WipFeatureRoute,
|
||||
namespace_route: &WipNamespaceRoute,
|
||||
) -> Result<(), WipMountError> {
|
||||
let collection_route = feature_route.child("objectives")?;
|
||||
let collection_route = namespace_route.root().to_string();
|
||||
let backend = WorkspaceHttpObjectiveBackend::new(client);
|
||||
let revisions = Arc::new(Mutex::new(ObjectiveRevisionState::default()));
|
||||
let collection_descriptor = objective_collection_descriptor();
|
||||
@@ -1775,7 +1775,9 @@ mod tests {
|
||||
}));
|
||||
|
||||
let mut registry = WipMountRegistry::new();
|
||||
let feature_route = registry.allocate_feature_route("objective").unwrap();
|
||||
let namespace_route = registry
|
||||
.allocate_namespace("objective", "objectives")
|
||||
.unwrap();
|
||||
mount_workspace_http_objective_wip(
|
||||
&mut registry,
|
||||
Arc::new(crate::worker::TestWorkspaceHttpClient::new(
|
||||
@@ -1783,18 +1785,16 @@ mod tests {
|
||||
"http://backend",
|
||||
)),
|
||||
None,
|
||||
&feature_route,
|
||||
&namespace_route,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
registry.routes().collect::<Vec<_>>(),
|
||||
["/features/objective/objectives"]
|
||||
assert_eq!(registry.routes().collect::<Vec<_>>(), ["/objectives"]);
|
||||
assert_eq!(namespace_route.root(), "/objectives");
|
||||
assert!(
|
||||
registry
|
||||
.allocate_namespace("objective", "../tickets")
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(
|
||||
feature_route.child("objectives").unwrap(),
|
||||
"/features/objective/objectives"
|
||||
);
|
||||
assert!(feature_route.child("../tickets").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1805,7 +1805,7 @@ mod tests {
|
||||
crate::worker::TestWorkspaceHttpClient::new("workspace", "http://backend"),
|
||||
)),
|
||||
permissions: None,
|
||||
collection_route: "/features/objective/objectives".into(),
|
||||
collection_route: "/objectives".into(),
|
||||
revisions: Arc::clone(&revisions),
|
||||
};
|
||||
let initial = resolver.resolve("O-3").unwrap();
|
||||
@@ -1971,7 +1971,7 @@ mod tests {
|
||||
let collection = ObjectiveCollectionWipHandler {
|
||||
backend,
|
||||
permissions: None,
|
||||
collection_route: "/features/objective/objectives".into(),
|
||||
collection_route: "/objectives".into(),
|
||||
revisions: Arc::new(Mutex::new(ObjectiveRevisionState::default())),
|
||||
};
|
||||
let result = collection
|
||||
@@ -2013,7 +2013,7 @@ mod tests {
|
||||
let collection = ObjectiveCollectionWipHandler {
|
||||
backend: backend.clone(),
|
||||
permissions: None,
|
||||
collection_route: "/features/objective/objectives".into(),
|
||||
collection_route: "/objectives".into(),
|
||||
revisions: Arc::clone(&revisions),
|
||||
};
|
||||
let context = || WipCallContext {
|
||||
@@ -2170,7 +2170,7 @@ mod tests {
|
||||
crate::worker::TestWorkspaceHttpClient::new("workspace", base_url),
|
||||
)),
|
||||
permissions: None,
|
||||
collection_route: "/features/objective/objectives".into(),
|
||||
collection_route: "/objectives".into(),
|
||||
revisions: Arc::new(Mutex::new(ObjectiveRevisionState::default())),
|
||||
};
|
||||
let output = match handler
|
||||
@@ -2191,10 +2191,7 @@ mod tests {
|
||||
let output = crate::wip::wip_to_json(&output.value).unwrap();
|
||||
assert_eq!(output["next_cursor"], "next-page");
|
||||
assert_eq!(output["has_more"], true);
|
||||
assert_eq!(
|
||||
output["objectives"][0]["path"],
|
||||
"/features/objective/objectives/O-3"
|
||||
);
|
||||
assert_eq!(output["objectives"][0]["path"], "/objectives/O-3");
|
||||
assert_eq!(output["objectives"][0]["linked_tickets"][0], "T-7");
|
||||
assert!(!output.to_string().contains("00001OBJECTIVE"));
|
||||
assert!(!output.to_string().contains("00001TICKET"));
|
||||
@@ -2243,7 +2240,7 @@ mod tests {
|
||||
let resolver = ObjectiveItemResolver {
|
||||
backend: backend.clone(),
|
||||
permissions: None,
|
||||
collection_route: "/features/objective/objectives".into(),
|
||||
collection_route: "/objectives".into(),
|
||||
revisions: Arc::clone(&revisions),
|
||||
};
|
||||
let before = resolver.resolve("O-3").unwrap().object.validator;
|
||||
|
||||
@@ -34,9 +34,9 @@ use crate::feature::{
|
||||
};
|
||||
use crate::permission::permission_action_for;
|
||||
use crate::wip::{
|
||||
WipCallContext, WipDynamicItem, WipDynamicItemResolver, WipDynamicMount, WipFeatureRoute,
|
||||
WipMountError, WipMountRegistry, WipOperationError, WipOperationHandler, WipOperationOutput,
|
||||
WipProjection, WipProjectionKind, json_to_wip, wip_to_json,
|
||||
WipCallContext, WipDynamicItem, WipDynamicItemResolver, WipDynamicMount, WipMountError,
|
||||
WipMountRegistry, WipNamespaceRoute, WipOperationError, WipOperationHandler,
|
||||
WipOperationOutput, WipProjection, WipProjectionKind, json_to_wip, wip_to_json,
|
||||
};
|
||||
use crate::worker::{WorkspaceClient, WorkspaceRequest, WorkspaceRequestMethod};
|
||||
use agen::tool::{Tool, ToolError, ToolExecutionContext, ToolMeta, ToolOutput};
|
||||
@@ -1377,7 +1377,7 @@ fn native_ticket_tools(
|
||||
let (meta, tool) = definition();
|
||||
let projection = native_ticket_operation(&meta.name).ok_or_else(|| {
|
||||
WipMountError::InvalidProjection {
|
||||
route: "/features/ticket/tickets".into(),
|
||||
route: "/tickets".into(),
|
||||
message: format!(
|
||||
"enabled Ticket tool `{}` has no native projection",
|
||||
meta.name
|
||||
@@ -1386,7 +1386,7 @@ fn native_ticket_tools(
|
||||
})?;
|
||||
jsonschema::validator_for(&meta.input_schema).map_err(|error| {
|
||||
WipMountError::InvalidProjection {
|
||||
route: "/features/ticket/tickets".into(),
|
||||
route: "/tickets".into(),
|
||||
message: format!("Ticket tool schema cannot be retained: {error}"),
|
||||
}
|
||||
})?;
|
||||
@@ -1418,9 +1418,9 @@ pub fn mount_workspace_http_ticket_wip(
|
||||
client: Arc<dyn WorkspaceClient>,
|
||||
access: TicketFeatureAccess,
|
||||
permissions: Option<ToolPermissionConfig>,
|
||||
feature_route: &WipFeatureRoute,
|
||||
namespace_route: &WipNamespaceRoute,
|
||||
) -> Result<(), WipMountError> {
|
||||
let collection_route = feature_route.child("tickets")?;
|
||||
let collection_route = namespace_route.root().to_string();
|
||||
let tools = native_ticket_tools(client, access)?;
|
||||
let claimed_tools = tools
|
||||
.iter()
|
||||
@@ -1920,7 +1920,7 @@ fn ticket_operation_declaration(
|
||||
.get("properties")
|
||||
.and_then(Value::as_object)
|
||||
.ok_or_else(|| WipMountError::InvalidProjection {
|
||||
route: "/features/ticket/tickets".into(),
|
||||
route: "/tickets".into(),
|
||||
message: format!("Ticket tool `{}` schema has no properties", tool.name),
|
||||
})?;
|
||||
let required = tool
|
||||
@@ -2103,7 +2103,7 @@ mod tests {
|
||||
#[test]
|
||||
fn native_ticket_mount_uses_host_route_and_rejects_non_ticket_children() {
|
||||
let mut registry = WipMountRegistry::new();
|
||||
let feature_route = registry.allocate_feature_route("ticket").unwrap();
|
||||
let namespace_route = registry.allocate_namespace("ticket", "tickets").unwrap();
|
||||
mount_workspace_http_ticket_wip(
|
||||
&mut registry,
|
||||
Arc::new(crate::worker::TestWorkspaceHttpClient::new(
|
||||
@@ -2112,20 +2112,21 @@ mod tests {
|
||||
)),
|
||||
TicketFeatureAccess::review(),
|
||||
None,
|
||||
&feature_route,
|
||||
&namespace_route,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
registry.routes().collect::<Vec<_>>(),
|
||||
["/features/ticket/tickets"]
|
||||
assert_eq!(registry.routes().collect::<Vec<_>>(), ["/tickets"]);
|
||||
assert!(
|
||||
registry
|
||||
.allocate_namespace("ticket", "../objectives")
|
||||
.is_err()
|
||||
);
|
||||
assert!(feature_route.child("../objectives").is_err());
|
||||
|
||||
let revisions = Arc::new(Mutex::new(TicketRevisionState::default()));
|
||||
let resolver = TicketItemResolver {
|
||||
tools: HashMap::new(),
|
||||
permissions: None,
|
||||
collection_route: "/features/ticket/tickets".into(),
|
||||
collection_route: "/tickets".into(),
|
||||
revisions,
|
||||
};
|
||||
assert!(resolver.resolve("T-42").is_some());
|
||||
@@ -2189,7 +2190,7 @@ mod tests {
|
||||
let resolver = TicketItemResolver {
|
||||
tools: HashMap::new(),
|
||||
permissions: None,
|
||||
collection_route: "/features/ticket/tickets".into(),
|
||||
collection_route: "/tickets".into(),
|
||||
revisions: Arc::clone(&revisions),
|
||||
};
|
||||
let unobserved_internal = resolver.resolve("00001TICKET").unwrap().object.validator;
|
||||
@@ -2243,7 +2244,7 @@ mod tests {
|
||||
let resolver = TicketItemResolver {
|
||||
tools: HashMap::new(),
|
||||
permissions: None,
|
||||
collection_route: "/features/ticket/tickets".into(),
|
||||
collection_route: "/tickets".into(),
|
||||
revisions: Arc::clone(&revisions),
|
||||
};
|
||||
let target_v1 = resolver.resolve("T-43").unwrap().object.validator;
|
||||
|
||||
+1423
-124
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,8 @@ It is not a dumping ground for external research, old plans, API inventories, or
|
||||
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.
|
||||
30. [`design/ticket-wip-projection.md`](design/ticket-wip-projection.md) — Host-assigned Ticket collection/item routes, role-specific operations, route identity, and typed Backend preservation.
|
||||
31. [`design/merge-request-wip-projection.md`](design/merge-request-wip-projection.md) — Host-assigned Merge Request routes and preserved review/integration authority gates.
|
||||
|
||||
## What belongs here
|
||||
|
||||
|
||||
@@ -4,13 +4,15 @@
|
||||
|
||||
When a Worker explicitly runs in `wip` mode and its Merge Request Feature has at least one enabled operation, the Worker Host projects that exact operation set as native Worldspace objects. The projection is an adapter over the existing scoped Workspace Merge Request tools and Backend routes. It does not create a second Merge Request store, provider integration path, review grant, approval rule, or completion authority.
|
||||
|
||||
The Host allocates `/features/merge-request` and the Feature mounts:
|
||||
The Host assigns the Merge Request provider the root `merge-requests` namespace through `WipMountRegistry` and mounts:
|
||||
|
||||
```text
|
||||
/features/merge-request/merge-requests
|
||||
/features/merge-request/merge-requests/<merge-request-id>
|
||||
/merge-requests
|
||||
/merge-requests/<merge-request-id>
|
||||
```
|
||||
|
||||
There is no Feature Object or `/features/merge-request` compatibility alias. Legacy paths remain historical display data only and fail as current input with rediscovery guidance.
|
||||
|
||||
An item identity is a single canonical path segment. Nested paths and percent-encoded target substitution do not resolve. Item operations remove `merge_request_id` from their descriptor and reconstruct it only from the selected object route. Discovery does not mint authority: every call still uses the Worker's scoped `WorkspaceClient`, the enabled Feature flags, ordinary Tool permission rules, and the Backend's Workspace, repository, assignment, provider-ref, review, and integration checks.
|
||||
|
||||
## Current Feature inventory
|
||||
|
||||
@@ -4,13 +4,15 @@
|
||||
|
||||
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:
|
||||
The Host assigns the Objective provider the root `objectives` namespace through `WipMountRegistry` and mounts:
|
||||
|
||||
```text
|
||||
/features/objective/objectives
|
||||
/features/objective/objectives/<objective-reference>
|
||||
/objectives
|
||||
/objectives/<objective-reference>
|
||||
```
|
||||
|
||||
There is no Feature Object or `/features/objective` compatibility alias. Legacy paths are retained only as historical text and fail as current inputs with rediscovery guidance.
|
||||
|
||||
`<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`.
|
||||
|
||||
@@ -4,13 +4,15 @@
|
||||
|
||||
When a Worker explicitly runs in `wip` mode and its Ticket Feature is enabled, the Worker Host projects the enabled Ticket surface as native Worldspace objects. The projection is an adapter over the existing scoped Workspace Ticket API and typed Ticket tools. It does not add a Ticket store, resolver, audit path, state machine, readiness rule, queue path, or relation implementation.
|
||||
|
||||
The Host allocates `/features/ticket` and the Feature mounts:
|
||||
The Host assigns the Ticket provider the root `tickets` namespace through `WipMountRegistry` and mounts:
|
||||
|
||||
```text
|
||||
/features/ticket/tickets
|
||||
/features/ticket/tickets/<ticket-reference>
|
||||
/tickets
|
||||
/tickets/<ticket-reference>
|
||||
```
|
||||
|
||||
There is no Feature Object or `/features/ticket` compatibility alias. A legacy path supplied as current input fails with rediscovery guidance; saved history containing that text is left unchanged.
|
||||
|
||||
`<ticket-reference>` is either a canonical `T-<digits>` key or an existing canonical internal alphanumeric ID. Only a direct child is accepted. Nested suffixes, Objective keys, unrelated prefixes, and hidden routes do not resolve.
|
||||
|
||||
Every call retains the Worker's scoped `WorkspaceClient`, current Ticket Feature access, and ordinary Tool permission policy. Discovery is not mutation authority. A direct call to an unpublished operation fails even when the caller knows its name. `deny` and interactive `ask` permission outcomes fail closed before Backend dispatch.
|
||||
|
||||
@@ -52,21 +52,47 @@ 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. 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.
|
||||
`worker::wip::WipMountRegistry` is the Host-owned namespace, Object, and operation registry. A provider requests a root namespace with `allocate_namespace(owner, namespace)`; the owner is retained as Host metadata and is not inserted into the public path. It then mounts the collection Object directly at `/<namespace>` and may install a bounded direct-child resolver. Features do not appear as intermediate Objects.
|
||||
|
||||
- Routes must be canonical absolute WIP paths.
|
||||
Object resolution and operations are separate contracts. One mounted `WipProjection` owns a static Object route and its initial operations. Another Feature may add operations with `contribute_operations` without replacing that Object. Dynamic families likewise keep one `WipDynamicItemResolver`, while `contribute_dynamic_operations` supplies disjoint handlers for every resolved item without a second resolver. The Host merges only contributions with the same interface format, documentation, and type declarations; duplicate operation names, inconsistent interfaces/validators, missing targets, duplicate namespace owners, and unrelated route claims fail deterministically at registration. The merged interface validator changes with the effective operation set, so stale Known Space cannot authorize a newly contributed operation.
|
||||
|
||||
- Routes must be canonical absolute WIP paths, and allocated namespaces are one lowercase root segment.
|
||||
- Object names must match the final route segment.
|
||||
- A projected object exposes exactly its declared interface.
|
||||
- Unrelated duplicate routes and conflicting interface descriptors or validators fail startup.
|
||||
- Object existence and resolution have one owner; operation contributors cannot remount or substitute the Object.
|
||||
- Multiple Features may contribute disjoint operations to the same Object interface.
|
||||
- Unrelated duplicate routes, duplicate operation names, and conflicting interface shapes 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.
|
||||
`WipInspect` renders the complete effective descriptor, including contributed operations, descriptor-local named declarations, and recursive record, list, enum, and union shapes with required flags and declaration documentation. The tests `host_namespace_object_resolution_and_operation_contributions_are_independent`, `operation_contributions_reject_interface_and_target_conflicts`, and `native_projection_preserves_descriptor_and_decodes_typed_arguments_end_to_end` demonstrate root allocation, unique resolution, contribution merging, collision rejection, current call-time authorization, complete descriptor inspection, and descriptor-typed invocation.
|
||||
|
||||
The built-in native route migration is:
|
||||
|
||||
| Historical route | Canonical route |
|
||||
| --- | --- |
|
||||
| `/features/ticket/tickets[/<reference>]` | `/tickets[/<reference>]` |
|
||||
| `/features/objective/objectives[/<reference>]` | `/objectives[/<reference>]` |
|
||||
| `/features/merge-request/merge-requests[/<reference>]` | `/merge-requests[/<reference>]` |
|
||||
|
||||
`/repositories` and `/workdirs` can be allocated by a later provider using the same registration contract; this layer does not implement or pre-allocate those domains. `/checkouts` content projection is likewise outside this contract.
|
||||
|
||||
### Registering a subsequent provider
|
||||
|
||||
1. Allocate `allocate_namespace("repository", "repositories")`. The returned `WipNamespaceRoute::root()` is `/repositories`; the owner is independent of any Feature registration name.
|
||||
2. Mount a native collection `WipProjection` at that root with capability `repository:collection`, matching Object name and one declared interface. Static Objects underneath the root use the same capability owner prefix. A mounted collection retains its own identity, interfaces, and validator even when it has static children.
|
||||
3. For dynamic items, register one `WipDynamicMount` on the collection with capability `repository:item`. A foreign capability owner is rejected. Its `WipDynamicItemResolver` owns reference acceptance, Object identity, and current Object validator. It returns only a bounded direct child; it must not reinterpret a reference as another namespace or grant permission from existence.
|
||||
4. The reference provider supplies its read operations and handlers independently of management Feature enablement. An Object-only provider may use an empty operation descriptor. Enabled management Features register `WipOperationContribution` for static Objects or `WipDynamicOperationContribution` for a dynamic family; they do not allocate the namespace again or replace its resolver.
|
||||
5. Contributions use the existing interface reference and identical format, documentation, and type declarations, supplying only their new operations. Interface references are global: extending a descriptor that is also mounted elsewhere must not create different descriptors/validators under the same reference (registration fails). Use separate references for collection and item contracts. Failed contributions leave the registry unchanged.
|
||||
6. Handlers are responsible for checking current target capability and current subject authorization before dispatch. Registration/discovery is metadata, not a grant. A resolver/handler must retain the original permission identity and domain audit/failure contracts. Revocation after discovery must fail before provider dispatch. Compatibility mounts are restricted to `/tools/<tool-name>` and cannot bypass namespace ownership or republish legacy routes.
|
||||
|
||||
Tests `management_enablement_is_independent_of_read_and_operation_permissions` and `contributed_interface_invalidates_observations_and_restore_requires_discovery` cover all eight enablement/read/manage combinations, direct-call rejection, effective descriptor changes, fresh runtime isolation under the same Worker identity, reset, and post-observation revocation.
|
||||
|
||||
## Authority and execution
|
||||
|
||||
Discovery is not authority. The Worldspace contains only the tools already registered from enabled Features and the Worker's current execution context. Calling a compatibility operation:
|
||||
Discovery is not authority. Object publication, operation implementation, target capability, and subject permission are independent checks. An Object may remain discoverable through an enabled read provider when a management Feature contributes no operations; conversely, publishing an operation never bypasses the target's current capability or the subject's current permission. Every handler re-evaluates permission at call time, so revocation after discovery is rejected before provider dispatch. Feature enablement selects implementations and does not itself grant read or mutation authority.
|
||||
|
||||
The Worldspace contains only the operations registered from enabled Features and the Worker's current execution context. Calling a compatibility operation:
|
||||
|
||||
1. resolves the exact mounted route;
|
||||
2. verifies object and interface membership and validators;
|
||||
@@ -85,6 +111,8 @@ The stateful `wip-client::Client` is owned by one Worker WIP runtime. Sessions a
|
||||
|
||||
Known Space and interface observations are therefore not shared across Workers, endpoints, or security contexts. A restored Worker creates a new runtime/client and must explore again. `WipDiscover(reset = true)` drops all observations before reconnect/authority-change exploration; `refresh = true` explicitly supersedes one cached observation. A normal compaction keeps the live runtime but the three gateway schemas are supplied again on every LLM request, including the instruction to begin discovery at `/`; the compacted transcript is not treated as cache authority.
|
||||
|
||||
Saved Session/history entries remain append-only evidence and are never rewritten or replayed as calls. In particular, historical `/features/<feature>/...` text stays displayable but is not a current route alias. Passing such a legacy path to discovery or call returns `NotFound` with guidance to rediscover from `/`; the Host does not translate it, resolve it under another namespace, or expose old and new routes in parallel.
|
||||
|
||||
## Errors, cancellation, and retries
|
||||
|
||||
The adapter keeps these outcomes distinct:
|
||||
@@ -96,6 +124,8 @@ The adapter keeps these outcomes distinct:
|
||||
- **disconnected/timeout after dispatch**: `wip-client` records an unknown outcome;
|
||||
- **operation outcome unknown**: an executing provider fails without a rollback/non-execution guarantee, a response cannot be completed after dispatch, or the call future is force-closed.
|
||||
|
||||
Cooperative cancellation is scoped to the active execution's selected operation and exact resolved handler instance. Static/dynamic dispatchers forward `cancel_operation` only to that operation's provider, once per cancellation request, never to unrelated contributors or every alias backed by a shared handler. The runtime pins the dispatcher used for execution rather than resolving the Object again on cancellation, since dynamic resolvers may return stateful instances. Providers may implement `cancel_operation` for operation-specific control or the shared `cancel` default. Terminal completion or a dropped execution future releases the active binding; unknown/finished execution IDs do not invoke any provider. A cancellation request is not itself terminal confirmation.
|
||||
|
||||
An unknown mutating outcome must never be retried automatically. The model must inspect external state or ask for operator guidance. Retrieval failures may be explicitly refreshed.
|
||||
|
||||
## Measurement
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Reviewer SubWorkerがreview開始前に停止する
|
||||
|
||||
## 状況
|
||||
|
||||
T-698の公開済みMerge Requestに対して、`builtin:reviewer`を指定し、対象Ticket/Merge Request、writable delegated scope、command grantを明示してReviewer SubWorkerを3回起動した。
|
||||
|
||||
いずれも`review_requested` eventは正しいsource refとTicket revisionを記録したが、SubWorkerは直後に`Stopped`となった。session observationには最初のuser taskだけが存在し、assistant output、tool call、typed review verdictは記録されなかった。Merge Request threadにもapprove/request_changesは追加されなかった。
|
||||
|
||||
## 障壁
|
||||
|
||||
CoderはReviewer approvalを代行できず、親Workerには`ReviewMergeRequest` authorityがないため、実装・公開・検証が完了していてもfresh approvalを取得できない。同じ入力を再送するだけでは診断情報が増えず、review requestだけが複数残る。
|
||||
|
||||
## 改善案
|
||||
|
||||
- Reviewer起動がmodel run前に停止した場合、停止理由を親へ構造化して通知する。
|
||||
- `review_requested`を記録した後にReviewer sessionの開始に失敗した場合、request eventと失敗eventを関連付け、未解決reviewとして明示する。
|
||||
- profile/provider初期化、review worktree準備、capability bindingのどの段階で失敗したかを、credentialやhost pathを露出しないbounded diagnosticとして提供する。
|
||||
- 同一sourceへの再試行時は、先行Reviewerが一度も実行されていないことを識別できるようにする。
|
||||
Reference in New Issue
Block a user