Skip to content
2 changes: 1 addition & 1 deletion docs/architecture/rfcs/loopx-overall-roadmap-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ P0 blocks correctness or continuity in the current user journey. P1 enables repe
| **S3 Goal planning and multi-Agent collaboration · P0/P1** | Vision/replan, peer frontiers, claim/lease, directory, manager_context and explicit continuation exist; general handoff/shared amendment remain incomplete | R2 proves peer dependency; R3 closes parallel joins, pipelines, help/review, continuation and automatic return; R4 delivers one intent-preserving amendment class. Cover cycles, invalidated inputs, rejection/deferral, lease transfer, competing bases and aggregate acceptance |
| **S4 Runtime/host/daemon · P0/P1** | Attached/managed, Turn, broker, runtime connectors and Desktop repairs exist; registration does not establish executable capacity | Qualify multi-Turn supervision for one real supported combination; restart/cancel/drain/stop retain work and fence old executors. Then expand host parity, unique service-profile ownership, clean installation and upgrades; show unsupported adapter capabilities |
| **S5 Frontend, Lark and human interaction · P0/P1** | Local chat, settings, proposals and partial Goal Channel verticals exist; shared audience/session/work readback needs qualification | One journey spans settings, work graph, handoff, blockers, cost, corrections, artifacts and return. Shared typed projections; reconnect/repeated-click/stale/original-route cases. Realtime IM reuses Chat/Turn: isolate independent conversations on one listener, then qualify ordinary DM onboarding, explicit role selection, busy-session admission, provisional progress, media and exact permission callbacks through [the shared operational contract](capable-manager-semantic-handoff-v0.md#10-operational-contract). [Live team workspace](live-team-workspace-v0.md) makes exchange, revision and original-coordinator continuation visible. Its [Work-scale map track](live-team-workspace-v0.md#11-delivery-order-and-relationship-to-aggressive-r2-progress) draws each Goal's typed Todo relations first (W1), then live state and outputs on the same nodes. Then intelligent review, keyboard accessibility, bilingual terminology, actionable errors and offline degradation; interrupt only for actual decisions |
| **S6 Materials, evidence, memory and learning · P1** | Authority registry, material lifecycle/frontier, decision context, reward memory and turn recall exist; direction baseline and parts of attribution remain proposed | Connect material revision→same-Agent read→decision reference→artifact/outcome. Expose expiry/revocation/source loss and forgetting policy. Handoff preserves decision-relevant summaries and authorized artifacts; qualify OpenViking/Obelisk as optional providers. Prove causal utility with controls, not relevance alone |
| **S6 Materials, evidence, memory and learning · P1** | Authority registry, material lifecycle/frontier, decision context, reward memory and turn recall exist; explicit project material packets and source-verifier gates are implemented, while private source initialization/Core binding and ordinary-conversation adoption remain unqualified; direction baseline and parts of attribution remain proposed | Connect material revision→same-Agent read→decision reference→artifact/outcome. Expose expiry/revocation/source loss and forgetting policy. Handoff preserves decision-relevant summaries and authorized artifacts; qualify OpenViking/Obelisk as optional providers. Prove causal utility with controls, not relevance alone |
| **S7 Budget, scheduling and fleet scale · P0 observation/P1–P2 expansion** | Quota/scheduler and partial usage aggregates exist; full provider cost, distributed reservations and hundred-Agent concurrency need evidence | Separate configured budget, admission, consumption and estimates; unknown is not zero and replay cannot double-charge. R7 pagination/bounded summaries and [complete-history transport](typescript-control-plane-migration-v0.md), including refresh/replay/single-debit evidence beyond the RPC limit; provider/host limits, fairness, backpressure, event wake and isolation; report registration/activity/throughput and cost per accepted outcome separately |
| **S8 Capabilities, extensions and domain integration · P1/P2** | Capability catalog, extension lifecycle, hooks, engineering/research/content/office capabilities and computer-use contracts exist | First exercise the shared control plane with existing issue-fix/PR-review and material/research callers. Every provider has readiness/version/permissions/default-off/uninstall/rollback/isolation and real-entry evidence. New domain effects start with one simulated operation, not a marketplace or workflow DSL |
| **S9 Identity, authority, privacy and trust · continuous P0/P1–P2 remote** | Public/private scope, capability gates, fencing and confirmation contracts belong to existing owners | R1/R3 cover sender/audience/artifact scope and stale authority; R6 authenticates tenant/Goal/actor/host, rotation/revocation and least privilege. Qualify credential custody, untrusted tool/document inputs, dependency supply chain, audit retention/deletion and vulnerability response through real paths; roles/messages/memory mint no write authority |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ migration, candidate/archive transitions, and bounded rerank proposals. It does
not own raw documents, private source locations, provider credentials, or Core
goal authority.

Ordinary project conversations can use an explicitly activated source profile
and an existing Core workspace write grant for inventory, candidate
intake/rollback, rerank packets, readable projection and ranking settlement.
These SDK packets select exactly one Goal or project owner; references never
grant access. The source verifier resolves current Core authority and retains
its staging/publication fence. Migration, rebuild and Explore remain Goal-only;
installing the project skill or importing the SDK does not activate a source.

```mermaid
flowchart LR
RAW["Private raw material store<br/>files, messages, web captures"]
Expand Down
15 changes: 15 additions & 0 deletions loopx/capabilities/material_lifecycle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,21 @@ rebuild, bounded rerank, readable projection, Explore intent, apply, and
rollback. Concrete legacy parsers, private storage adapters, source profiles,
and provider credentials remain project owned.

## Project Conversation Intake

An explicitly activated project source can pass `MaterialProjectScope` instead
of `goal_id` to the existing inventory, intake/rollback, ranking, projection and
settlement builders. Exactly one owner is required; the project path creates
no Goal. Its project/profile/grant references select existing Core context and
source ownership, and never grant access by themselves.

Project intake/rollback require the source provider's `verify_project_scope`
to resolve the current Core caller, audience, exact profile/store, workspace
write grant and expiring owner gate. Verification runs before source access
and publication; the source must retain its transaction authorization fence.
Source initialization stays project owned. Migration, rebuild and Explore keep
their existing Goal route. No new CLI or transport configuration path is added.

## Relationship To Other Capabilities

| Capability | Primary question | Relationship |
Expand Down
8 changes: 8 additions & 0 deletions loopx/capabilities/material_lifecycle/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,14 @@ migration preparation、lifecycle receipt、ranked-entry rebuild、bounded reran
readable projection、Explore intent、apply 与 rollback。具体 legacy parser、私有
storage adapter、source profile 和 provider credential 仍由项目拥有。

## 明确的项目来源 scope

普通项目对话可为已显式启用的素材来源使用 `MaterialProjectScope`,无需创建 Goal。inventory、candidate intake/rollback、rerank packet、readable projection 和 intake-ranking settlement 支持以 `project_scope` 替代 `goal_id`;两者必须且只能选一个。项目 packet 不含 Goal 标识,也不带入 manager 上下文。

scope 的 `project_ref`、`source_profile_ref`、`workspace_grant_ref` 是既有 Core 授权与来源 profile 的选择器,不授予权限。project candidate provider 必须实现 `MaterialProjectScopeVerifier`,核对当前调用者/受众、精确 store/profile、工作区写入边界、owner gate 的有效期与撤销状态。SDK 在访问 provider 前及发布 authority 前调用该 verifier,授权事实仍由既有 Core owner 提供;adapter 还须在自身 staging/publication 事务内执行授权检查,不能把预检查的 Boolean 当作事务 fence。

默认关闭不变;本路径不注册 Goal、不创建 source authority,不扩大 Goal-only migration/rebuild/Explore。新来源初始化、私有存储与 transport 接入仍由 source owner 提供,不能仅凭这些字段或安装 skill 开始写入。

## 与其他能力的关系

| 能力 | 核心问题 | 与 Material Lifecycle 的关系 |
Expand Down
6 changes: 5 additions & 1 deletion loopx/capabilities/material_lifecycle/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Goal-scoped Material Lifecycle capability contracts."""
"""Material Lifecycle contracts for explicit Goal or project source owners."""

from .ownership import MaterialProjectScope, MaterialProjectScopeVerifier

from .apply import (
MATERIAL_MIGRATION_APPLY_RECEIPT_SCHEMA_VERSION,
Expand Down Expand Up @@ -95,6 +97,8 @@
)

__all__ = [
"MaterialProjectScope",
"MaterialProjectScopeVerifier",
"MATERIAL_CANDIDATE_INTAKE_APPLY_RECEIPT_SCHEMA_VERSION",
"MATERIAL_CANDIDATE_INTAKE_PROPOSAL_SCHEMA_VERSION",
"MATERIAL_CANDIDATE_INTAKE_ROLLBACK_RECEIPT_SCHEMA_VERSION",
Expand Down
4 changes: 2 additions & 2 deletions loopx/capabilities/material_lifecycle/_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,10 @@ def packet_ref(prefix: str, packet: Mapping[str, Any]) -> str:
return f"{prefix}-{digest}"


def capability_contract(*, packet_role: str) -> dict[str, Any]:
def capability_contract(*, packet_role: str, project_scoped: bool = False) -> dict[str, Any]:
return {
"capability_id": "material_lifecycle",
"scope": "goal",
"scope": "project" if project_scoped else "goal",
"default_enabled": False,
"packet_role": packet_role,
"creates_authority": False,
Expand Down
60 changes: 51 additions & 9 deletions loopx/capabilities/material_lifecycle/intake.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
from dataclasses import dataclass
from typing import Any, Protocol

from .ownership import (
MaterialProjectScope, material_owner_fields, verify_project_material_write,
)

from ._validation import (
capability_contract,
check_record_keys,
Expand Down Expand Up @@ -252,7 +256,8 @@ def _readback(

def build_material_candidate_intake_proposal(
*,
goal_id: str,
goal_id: str | None = None,
project_scope: MaterialProjectScope | None = None,
proposal_id: str,
store_id: str,
source_authority_revision: str,
Expand All @@ -268,7 +273,7 @@ def build_material_candidate_intake_proposal(

proposal: dict[str, Any] = {
"schema_version": MATERIAL_CANDIDATE_INTAKE_PROPOSAL_SCHEMA_VERSION,
"goal_id": compact_token(goal_id, field="goal_id"),
**material_owner_fields(goal_id=goal_id, project_scope=project_scope),
"proposal_id": compact_token(proposal_id, field="proposal_id"),
"store_id": compact_token(store_id, field="store_id"),
"source_authority_revision": compact_token(
Expand Down Expand Up @@ -299,12 +304,23 @@ def build_material_candidate_intake_proposal(
"raw_content_captured": False,
"private_locations_captured": False,
"visibility": "public_safe",
"capability": capability_contract(packet_role="candidate_intake_proposal"),
"capability": capability_contract(packet_role="candidate_intake_proposal", project_scoped=project_scope is not None),
}
proposal["proposal_ref"] = packet_ref("material-candidate-intake", proposal)
return proposal


def _packet_owner(value: Mapping[str, Any]) -> dict[str, Any]:
owner = material_owner_fields(
goal_id=value.get("goal_id"), project_scope=value.get("project_scope"),
)
if "project_scope" in owner:
contract = value.get("capability")
if not isinstance(contract, Mapping) or contract.get("scope") != "project":
raise ValueError("project material packet must declare project capability scope")
return owner


def _proposal(value: Mapping[str, Any]) -> dict[str, Any]:
check_record_keys(
value,
Expand All @@ -317,6 +333,7 @@ def _proposal(value: Mapping[str, Any]) -> dict[str, Any]:
"exact_read_ref",
"exact_read_verified",
"goal_id",
"project_scope",
"lifecycle_state",
"material_ref",
"observed_at",
Expand All @@ -337,7 +354,6 @@ def _proposal(value: Mapping[str, Any]) -> dict[str, Any]:
"content_digest",
"content_size_bytes",
"exact_read_ref",
"goal_id",
"material_ref",
"proposal_ref",
"schema_version",
Expand All @@ -349,6 +365,8 @@ def _proposal(value: Mapping[str, Any]) -> dict[str, Any]:
)
if value.get("schema_version") != MATERIAL_CANDIDATE_INTAKE_PROPOSAL_SCHEMA_VERSION:
raise ValueError("candidate intake proposal has an unsupported schema_version")
owner = _packet_owner(value)

required_truth = {
"owner_gate_required": True,
"exact_read_verified": True,
Expand All @@ -362,7 +380,7 @@ def _proposal(value: Mapping[str, Any]) -> dict[str, Any]:
if value.get(field) != expected:
raise ValueError(f"candidate intake proposal has invalid {field}")
return {
"goal_id": compact_token(value["goal_id"], field="proposal.goal_id"),
**owner,
"proposal_ref": compact_token(
value["proposal_ref"],
field="proposal.proposal_ref",
Expand Down Expand Up @@ -471,6 +489,11 @@ def apply_material_candidate_intake(
timestamp = iso_timestamp(observed_at, field="observed_at")
owner_gate = compact_token(owner_gate_ref, field="owner_gate_ref")

verify_project_material_write(
provider, owner=proposal, store_id=store_id, owner_gate_ref=owner_gate,
observed_at=timestamp,
)

before = _snapshot(
provider,
provider_id=expected_provider,
Expand Down Expand Up @@ -624,6 +647,11 @@ def apply_material_candidate_intake(
field="append_reconciliation.validation_ref",
)

verify_project_material_write(
provider, owner=proposal, store_id=store_id, owner_gate_ref=owner_gate,
observed_at=timestamp,
)

transition = provider.switch_authority(
store_id=store_id,
expected_revision=source_revision,
Expand Down Expand Up @@ -680,7 +708,9 @@ def apply_material_candidate_intake(

receipt: dict[str, Any] = {
"schema_version": MATERIAL_CANDIDATE_INTAKE_APPLY_RECEIPT_SCHEMA_VERSION,
"goal_id": proposal["goal_id"],
**material_owner_fields(
goal_id=proposal.get("goal_id"), project_scope=proposal.get("project_scope"),
),
"receipt_id": compact_token(receipt_id, field="receipt_id"),
"proposal_ref": proposal["proposal_ref"],
"provider_id": expected_provider,
Expand Down Expand Up @@ -718,7 +748,7 @@ def apply_material_candidate_intake(
"private_locations_captured": False,
"visibility": "public_safe",
"observed_at": timestamp,
"capability": capability_contract(packet_role="candidate_intake_apply_receipt"),
"capability": capability_contract(packet_role="candidate_intake_apply_receipt", project_scoped="project_scope" in proposal),
}
receipt["receipt_ref"] = packet_ref("material-candidate-intake-apply", receipt)
return receipt
Expand Down Expand Up @@ -766,6 +796,12 @@ def rollback_material_candidate_intake(
timestamp = iso_timestamp(observed_at, field="observed_at")
owner_gate = compact_token(owner_gate_ref, field="owner_gate_ref")

owner = _packet_owner(apply_receipt)
verify_project_material_write(
provider, owner=owner, store_id=store_id, owner_gate_ref=owner_gate,
observed_at=timestamp,
)

current = _snapshot(
provider,
provider_id=expected_provider,
Expand All @@ -775,6 +811,11 @@ def rollback_material_candidate_intake(
if current.authority_revision != current_revision:
raise ValueError("candidate intake rollback authority revision CAS failed")

verify_project_material_write(
provider, owner=owner, store_id=store_id, owner_gate_ref=owner_gate,
observed_at=timestamp,
)

transition = provider.switch_authority(
store_id=store_id,
expected_revision=current_revision,
Expand Down Expand Up @@ -828,7 +869,7 @@ def rollback_material_candidate_intake(

receipt: dict[str, Any] = {
"schema_version": MATERIAL_CANDIDATE_INTAKE_ROLLBACK_RECEIPT_SCHEMA_VERSION,
"goal_id": compact_token(apply_receipt.get("goal_id"), field="receipt.goal_id"),
**owner,
"receipt_id": compact_token(receipt_id, field="receipt_id"),
"apply_receipt_ref": compact_token(
apply_receipt.get("receipt_ref"),
Expand Down Expand Up @@ -859,7 +900,8 @@ def rollback_material_candidate_intake(
"visibility": "public_safe",
"observed_at": timestamp,
"capability": capability_contract(
packet_role="candidate_intake_rollback_receipt"
packet_role="candidate_intake_rollback_receipt",
project_scoped="project_scope" in owner,
),
}
receipt["receipt_ref"] = packet_ref(
Expand Down
9 changes: 6 additions & 3 deletions loopx/capabilities/material_lifecycle/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from collections.abc import Mapping, Sequence
from typing import Any

from .ownership import MaterialProjectScope, material_owner_fields

from ._validation import (
capability_contract,
compact_token,
Expand All @@ -28,7 +30,8 @@

def build_material_store_inventory(
*,
goal_id: str,
goal_id: str | None = None,
project_scope: MaterialProjectScope | None = None,
store_id: str,
store_revision: str,
observed_at: str,
Expand Down Expand Up @@ -61,7 +64,7 @@ def build_material_store_inventory(

inventory: dict[str, Any] = {
"schema_version": MATERIAL_STORE_INVENTORY_SCHEMA_VERSION,
"goal_id": compact_token(goal_id, field="goal_id"),
**material_owner_fields(goal_id=goal_id, project_scope=project_scope),
"store_id": compact_token(store_id, field="store_id"),
"store_revision": compact_token(store_revision, field="store_revision"),
"observed_at": iso_timestamp(observed_at, field="observed_at"),
Expand All @@ -72,7 +75,7 @@ def build_material_store_inventory(
"backup_ref": compact_token(backup_ref, field="backup_ref"),
"source_digest": compact_token(source_digest, field="source_digest"),
"visibility": "public_safe",
"capability": capability_contract(packet_role="inventory"),
"capability": capability_contract(packet_role="inventory", project_scoped=project_scope is not None),
"lifecycle_counts": counts,
"item_count": sum(counts.values()),
"parse_error_refs": token_list(
Expand Down
Loading
Loading