From dfaad8e07a643a789e53a6d563f571c74578fbf4 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Sun, 4 Oct 2026 17:03:55 +0800 Subject: [PATCH 1/5] feat(material): support explicitly authorized project source owners Signed-off-by: huangruiteng --- .../material_lifecycle/__init__.py | 6 +- .../material_lifecycle/_validation.py | 4 +- .../material_lifecycle/architecture.py | 7 ++ .../capabilities/material_lifecycle/intake.py | 60 +++++++++++-- .../material_lifecycle/inventory.py | 9 +- .../material_lifecycle/ownership.py | 88 +++++++++++++++++++ .../material_lifecycle/ranking.py | 16 ++-- .../material_lifecycle/readable_projection.py | 9 +- .../material_lifecycle/settlement.py | 9 +- 9 files changed, 181 insertions(+), 27 deletions(-) create mode 100644 loopx/capabilities/material_lifecycle/ownership.py diff --git a/loopx/capabilities/material_lifecycle/__init__.py b/loopx/capabilities/material_lifecycle/__init__.py index e4f52b0bae..a3a9c57fa1 100644 --- a/loopx/capabilities/material_lifecycle/__init__.py +++ b/loopx/capabilities/material_lifecycle/__init__.py @@ -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, @@ -93,6 +95,8 @@ ) __all__ = [ + "MaterialProjectScope", + "MaterialProjectScopeVerifier", "MATERIAL_CANDIDATE_INTAKE_APPLY_RECEIPT_SCHEMA_VERSION", "MATERIAL_CANDIDATE_INTAKE_PROPOSAL_SCHEMA_VERSION", "MATERIAL_CANDIDATE_INTAKE_ROLLBACK_RECEIPT_SCHEMA_VERSION", diff --git a/loopx/capabilities/material_lifecycle/_validation.py b/loopx/capabilities/material_lifecycle/_validation.py index 3c84295479..bcf6da1d13 100644 --- a/loopx/capabilities/material_lifecycle/_validation.py +++ b/loopx/capabilities/material_lifecycle/_validation.py @@ -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, diff --git a/loopx/capabilities/material_lifecycle/architecture.py b/loopx/capabilities/material_lifecycle/architecture.py index 6ef45824fd..22e1b596e1 100644 --- a/loopx/capabilities/material_lifecycle/architecture.py +++ b/loopx/capabilities/material_lifecycle/architecture.py @@ -57,6 +57,13 @@ def build_material_lifecycle_architecture_packet() -> dict[str, object]: MATERIAL_READABLE_PROJECTION_RECEIPT_SCHEMA_VERSION, MATERIAL_EXPLORE_INTENT_SCHEMA_VERSION, ], + "project_source_scope": { + "requires_explicit_source_profile": True, + "requires_existing_workspace_write_grant": True, + "requires_source_authorization_verifier": True, + "creates_goal": False, + "creates_source_authority": False, + }, "sibling_capabilities": { "decision_context": ( "supplies revisioned evidence for bounded rerank proposals" diff --git a/loopx/capabilities/material_lifecycle/intake.py b/loopx/capabilities/material_lifecycle/intake.py index 451ee4e651..c655f55071 100644 --- a/loopx/capabilities/material_lifecycle/intake.py +++ b/loopx/capabilities/material_lifecycle/intake.py @@ -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, @@ -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, @@ -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( @@ -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, @@ -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", @@ -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", @@ -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, @@ -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", @@ -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, @@ -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, @@ -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, @@ -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 @@ -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, @@ -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, @@ -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"), @@ -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( diff --git a/loopx/capabilities/material_lifecycle/inventory.py b/loopx/capabilities/material_lifecycle/inventory.py index 5b99fc567a..8b587e2303 100644 --- a/loopx/capabilities/material_lifecycle/inventory.py +++ b/loopx/capabilities/material_lifecycle/inventory.py @@ -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, @@ -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, @@ -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"), @@ -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( diff --git a/loopx/capabilities/material_lifecycle/ownership.py b/loopx/capabilities/material_lifecycle/ownership.py new file mode 100644 index 0000000000..e6b5fe04e7 --- /dev/null +++ b/loopx/capabilities/material_lifecycle/ownership.py @@ -0,0 +1,88 @@ +"""Material ownership metadata; references never grant write authority.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any, Protocol, cast + +from ._validation import check_record_keys, compact_token + + +@dataclass(frozen=True) +class MaterialProjectScope: + """An explicitly activated source profile within one authorized workspace.""" + + project_ref: str + source_profile_ref: str + workspace_grant_ref: str + + +def material_owner_fields( + *, + goal_id: str | None = None, + project_scope: MaterialProjectScope | Mapping[str, Any] | None = None, +) -> dict[str, Any]: + if (goal_id is None) == (project_scope is None): + raise ValueError("exactly one goal_id or project_scope is required") + if goal_id is not None: + return {"goal_id": compact_token(goal_id, field="goal_id")} + scope: Mapping[str, Any] + if isinstance(project_scope, MaterialProjectScope): + scope = { + "project_ref": project_scope.project_ref, + "source_profile_ref": project_scope.source_profile_ref, + "workspace_grant_ref": project_scope.workspace_grant_ref, + } + elif isinstance(project_scope, Mapping): + scope = project_scope + else: + raise TypeError("project_scope must be MaterialProjectScope or an object") + fields = {"project_ref", "source_profile_ref", "workspace_grant_ref"} + check_record_keys(scope, field="project_scope", allowed=fields, required=fields) + return { + "project_scope": { + key: compact_token(scope[key], field=f"project_scope.{key}") + for key in sorted(fields) + } + } + + +class MaterialProjectScopeVerifier(Protocol): + """Source adapter resolves authorization through its existing Core owner.""" + + def verify_project_scope( + self, *, project_scope: Mapping[str, str], store_id: str, + owner_gate_ref: str, observed_at: str, + ) -> bool: ... + + +def verify_project_material_write( + provider: object, + *, + owner: Mapping[str, Any], + store_id: str, + owner_gate_ref: str, + observed_at: str, +) -> None: + """Require the source owner to resolve current authorization before writes. + + Project refs, profile refs and grant refs are selectors, not permission. + The adapter must resolve the exact store/profile/workspace and current + caller, audience, gate expiry, write scope and revocation. No Goal is + created or used to borrow manager authority. + """ + scope = owner.get("project_scope") + if scope is None: + return + verifier = getattr(provider, "verify_project_scope", None) + if not callable(verifier): + raise ValueError("project material writes require a source authorization verifier") + verified = cast(MaterialProjectScopeVerifier, provider).verify_project_scope( + project_scope=dict(scope), + store_id=store_id, + owner_gate_ref=owner_gate_ref, + observed_at=observed_at, + ) + if verified is not True: + raise ValueError("project material source authorization was not verified") diff --git a/loopx/capabilities/material_lifecycle/ranking.py b/loopx/capabilities/material_lifecycle/ranking.py index 7e770c3ed0..f483ba0fc3 100644 --- a/loopx/capabilities/material_lifecycle/ranking.py +++ b/loopx/capabilities/material_lifecycle/ranking.py @@ -6,6 +6,8 @@ from collections.abc import Mapping, Sequence from typing import Any +from .ownership import MaterialProjectScope, material_owner_fields + from ._validation import ( capability_contract, check_record_keys, @@ -115,7 +117,8 @@ def _normalize_moves( def build_material_rerank_proposal( *, - goal_id: str, + goal_id: str | None = None, + project_scope: MaterialProjectScope | None = None, proposal_id: str, inventory_ref: str, decision_evidence_ref: str, @@ -153,7 +156,7 @@ def build_material_rerank_proposal( proposal: dict[str, Any] = { "schema_version": MATERIAL_RERANK_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"), "inventory_ref": compact_token(inventory_ref, field="inventory_ref"), "decision_evidence_ref": compact_token( @@ -162,7 +165,7 @@ def build_material_rerank_proposal( ), "observed_at": iso_timestamp(observed_at, field="observed_at"), "visibility": "public_safe", - "capability": capability_contract(packet_role="rerank_proposal"), + "capability": capability_contract(packet_role="rerank_proposal", project_scoped=project_scope is not None), "constraints": { "target_window_size": window, "max_moved_items": max_moved, @@ -185,7 +188,8 @@ def build_material_rerank_proposal( def build_material_rerank_apply_receipt( *, - goal_id: str, + goal_id: str | None = None, + project_scope: MaterialProjectScope | None = None, receipt_id: str, proposal_ref: str, observed_at: str, @@ -238,7 +242,7 @@ def build_material_rerank_apply_receipt( receipt: dict[str, Any] = { "schema_version": MATERIAL_RERANK_APPLY_RECEIPT_SCHEMA_VERSION, - "goal_id": compact_token(goal_id, field="goal_id"), + **material_owner_fields(goal_id=goal_id, project_scope=project_scope), "receipt_id": compact_token(receipt_id, field="receipt_id"), "proposal_ref": compact_token(proposal_ref, field="proposal_ref"), "observed_at": iso_timestamp(observed_at, field="observed_at"), @@ -249,7 +253,7 @@ def build_material_rerank_apply_receipt( "validation_ref": compact_token(validation_ref, field="validation_ref"), "applied_material_refs": applied_refs, "visibility": "public_safe", - "capability": capability_contract(packet_role="rerank_apply_receipt"), + "capability": capability_contract(packet_role="rerank_apply_receipt", project_scoped=project_scope is not None), "raw_content_captured": False, "private_locations_captured": False, } diff --git a/loopx/capabilities/material_lifecycle/readable_projection.py b/loopx/capabilities/material_lifecycle/readable_projection.py index 01806dfb61..cfd50dab77 100644 --- a/loopx/capabilities/material_lifecycle/readable_projection.py +++ b/loopx/capabilities/material_lifecycle/readable_projection.py @@ -7,6 +7,8 @@ from typing import Any from urllib.parse import urlsplit +from .ownership import MaterialProjectScope, material_owner_fields + from ._validation import ( capability_contract, check_record_keys, @@ -337,7 +339,8 @@ def _markdown_lines( def build_material_readable_projection( *, - goal_id: str, + goal_id: str | None = None, + project_scope: MaterialProjectScope | None = None, projection_id: str, authority_revision: str, observed_at: str, @@ -463,7 +466,7 @@ def build_material_readable_projection( projection_ref = f"material-readable-projection-{projection_digest[:20]}" receipt: dict[str, Any] = { "schema_version": MATERIAL_READABLE_PROJECTION_RECEIPT_SCHEMA_VERSION, - "goal_id": compact_token(goal_id, field="goal_id"), + **material_owner_fields(goal_id=goal_id, project_scope=project_scope), "projection_id": compact_token(projection_id, field="projection_id"), "authority_revision": compact_token( authority_revision, @@ -492,7 +495,7 @@ def build_material_readable_projection( }, "visibility": "public_safe", "projection_visibility": "local_private_allowed", - "capability": capability_contract(packet_role="readable_projection_receipt"), + "capability": capability_contract(packet_role="readable_projection_receipt", project_scoped=project_scope is not None), "raw_content_captured": False, "private_locations_captured": False, } diff --git a/loopx/capabilities/material_lifecycle/settlement.py b/loopx/capabilities/material_lifecycle/settlement.py index 1e5a745169..703bc6488b 100644 --- a/loopx/capabilities/material_lifecycle/settlement.py +++ b/loopx/capabilities/material_lifecycle/settlement.py @@ -5,6 +5,8 @@ from collections.abc import Sequence from typing import Any +from .ownership import MaterialProjectScope, material_owner_fields + from ._validation import ( capability_contract, compact_text, @@ -31,7 +33,8 @@ def _required_bool(value: Any, *, field: str) -> bool: def build_material_intake_ranking_settlement( *, - goal_id: str, + goal_id: str | None = None, + project_scope: MaterialProjectScope | None = None, settlement_id: str, material_ref: str, observed_at: str, @@ -176,7 +179,7 @@ def build_material_intake_ranking_settlement( settlement: dict[str, Any] = { "schema_version": MATERIAL_INTAKE_RANKING_SETTLEMENT_SCHEMA_VERSION, - "goal_id": compact_token(goal_id, field="goal_id"), + **material_owner_fields(goal_id=goal_id, project_scope=project_scope), "settlement_id": compact_token(settlement_id, field="settlement_id"), "material_ref": normalized_material_ref, "observed_at": iso_timestamp(observed_at, field="observed_at"), @@ -212,7 +215,7 @@ def build_material_intake_ranking_settlement( "ranked_membership_verified": membership_verified, }, "visibility": "public_safe", - "capability": capability_contract(packet_role="intake_ranking_settlement"), + "capability": capability_contract(packet_role="intake_ranking_settlement", project_scoped=project_scope is not None), "raw_content_captured": False, "private_locations_captured": False, } From 14223525a95b00974902f1de81dbc550f567ddc0 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Sun, 4 Oct 2026 17:04:07 +0800 Subject: [PATCH 2/5] docs(material): define project source scope and adoption boundary Signed-off-by: huangruiteng --- .../rfcs/loopx-overall-roadmap-v0.md | 2 +- .../material-lifecycle-architecture-v0.md | 18 +++++++++ ...aterial-lifecycle-architecture-v0.zh-CN.md | 6 +++ .../capabilities/material_lifecycle/README.md | 37 +++++++++++++++++++ .../material_lifecycle/README.zh-CN.md | 8 ++++ skills/loopx-material/SKILL.md | 30 ++++++++++----- 6 files changed, 90 insertions(+), 11 deletions(-) diff --git a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md index 732c26ad7a..549ec33973 100644 --- a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md +++ b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md @@ -75,7 +75,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. [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 | diff --git a/docs/reference/protocols/material-lifecycle-architecture-v0.md b/docs/reference/protocols/material-lifecycle-architecture-v0.md index 148d5e5012..e1d12bffa6 100644 --- a/docs/reference/protocols/material-lifecycle-architecture-v0.md +++ b/docs/reference/protocols/material-lifecycle-architecture-v0.md @@ -208,3 +208,21 @@ focused tests, and a public smoke. It does not ship: Those require a private read-only adapter, exact dual-read reconciliation, and an explicit owner gate. + +## Project source scope for ordinary conversations + +The inventory, candidate intake/rollback, rerank, readable projection and +intake-ranking settlement builders also accept an explicit `project_scope`. +The ownership fields are exclusive: a packet has either its existing `goal_id` +or `project_scope = {project_ref, source_profile_ref, workspace_grant_ref}`. +Project packets declare capability scope `project`, remain default-off and +contain no synthetic Goal. Existing Goal packets retain their representation. + +These refs do not authorize actions. Project candidate adapters must resolve +current source and Core workspace authority through +`MaterialProjectScopeVerifier.verify_project_scope`, binding the exact store, +profile, current caller/audience and owner gate with expiry/revocation checks. +Intake/rollback recheck before provider access and before publication; the +adapter also enforces its authorization fence within the write transaction. +Private profile initialization and storage stay source-owned. No migration, +rebuild, Explore, global authority or scheduler behavior is changed. diff --git a/docs/reference/protocols/material-lifecycle-architecture-v0.zh-CN.md b/docs/reference/protocols/material-lifecycle-architecture-v0.zh-CN.md index 47750baf64..0fd386b4f3 100644 --- a/docs/reference/protocols/material-lifecycle-architecture-v0.zh-CN.md +++ b/docs/reference/protocols/material-lifecycle-architecture-v0.zh-CN.md @@ -172,3 +172,9 @@ apply/rollback 编排、受限决策规划、catalog、架构 CLI、聚焦测试 - 自动重排、provider 调用、自动归档或自动推进 cursor。 这些能力必须经过私有只读 adapter、精确双读对账和显式 owner gate。 + +## 普通对话的项目来源 scope + +inventory、candidate intake/rollback、rerank、readable projection 与 intake-ranking settlement 支持明确的 `project_scope`。packet 必须二选一:保留原 `goal_id`,或使用 `{project_ref, source_profile_ref, workspace_grant_ref}`;项目形式不包含 dummy Goal。capability scope 为 `project`,默认关闭及 source owner 边界不变。 + +引用仅用于选择既有授权,不发放权限。project candidate adapter 必须通过 `MaterialProjectScopeVerifier.verify_project_scope` 核对当前 Core 调用者、受众、精确 store/profile、工作区写入边界及 owner gate 有效期/撤销状态。访问 provider 与发布 authority 前分别重新核验;真正的 adapter 还必须在写入事务内保持授权 fence。私有 profile 初始化与存储仍由来源 owner 提供,未扩大 migration/rebuild/Explore,也不改变全局 authority 或 scheduler。 diff --git a/loopx/capabilities/material_lifecycle/README.md b/loopx/capabilities/material_lifecycle/README.md index 60aa0354d1..520af23bbf 100644 --- a/loopx/capabilities/material_lifecycle/README.md +++ b/loopx/capabilities/material_lifecycle/README.md @@ -178,6 +178,43 @@ rebuild, bounded rerank, readable projection, Explore intent, apply, and rollback. Concrete legacy parsers, private storage adapters, source profiles, and provider credentials remain project owned. +## Explicit Project Source Ownership + +Ordinary project conversations can use an explicitly activated material source +without creating a Goal. Inventory, candidate intake/rollback, rerank packets, +readable projections and intake-ranking settlement accept `project_scope` in +place of `goal_id`: + +```python +from loopx.capabilities.material_lifecycle import MaterialProjectScope + +scope = MaterialProjectScope( + project_ref="project:example", + source_profile_ref="profile:materials", + workspace_grant_ref="grant:workspace-write", +) +``` + +Exactly one owner is required. The project form contains no `goal_id`; the +existing Goal form and default-off behavior are unchanged. These opaque refs +select an existing Core project grant and source profile. Constructing a scope +or packet does not activate a profile, grant writes, register a Goal or expose +manager context. + +A project candidate provider must implement `MaterialProjectScopeVerifier`. +Its `verify_project_scope` resolves the current Core caller, audience, workspace +write boundary, exact store/profile and owner gate, including expiry and +revocation. Intake and rollback require this verifier before provider access +and again before authority publication. Missing, mismatched or revoked +verification fails closed. The source adapter must also enforce authorization +inside its staging/publication transaction; the preflight Boolean is not a +transaction fence or an authority issuer. + +Fresh source initialization, private storage formats and transport integration +remain source-owner responsibilities. Goal-only migration, rebuild and Explore +APIs are not widened by this project intake path. A project skill or these +packet fields alone cannot initialize or mutate a source store. + ## Relationship To Other Capabilities | Capability | Primary question | Relationship | diff --git a/loopx/capabilities/material_lifecycle/README.zh-CN.md b/loopx/capabilities/material_lifecycle/README.zh-CN.md index b88e085cec..e61a040626 100644 --- a/loopx/capabilities/material_lifecycle/README.zh-CN.md +++ b/loopx/capabilities/material_lifecycle/README.zh-CN.md @@ -159,6 +159,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 的有效期与撤销状态。Core 在访问 provider 前及发布 authority 前重新核验;adapter 还须在自身 staging/publication 事务内执行授权检查,不能把预检查的 Boolean 当作事务 fence。 + +默认关闭不变;本路径不注册 Goal、不创建 source authority,不扩大 Goal-only migration/rebuild/Explore。新来源初始化、私有存储与 transport 接入仍由 source owner 提供,不能仅凭这些字段或安装 skill 开始写入。 + ## 与其他能力的关系 | 能力 | 核心问题 | 与 Material Lifecycle 的关系 | diff --git a/skills/loopx-material/SKILL.md b/skills/loopx-material/SKILL.md index 4a39e8dde7..4a37be38d1 100644 --- a/skills/loopx-material/SKILL.md +++ b/skills/loopx-material/SKILL.md @@ -25,24 +25,34 @@ Use `--surface claude-code` or `--surface opencode` for those hosts; repeat the flag to install multiple host-native copies in one transaction. Managed copies live under `.agents/skills/`, `.claude/skills/`, or `.opencode/skills/` and are upgraded or removed through the same CLI. Project-local discovery does not -itself activate material-store writes; the selected goal still needs explicit -Material Lifecycle authority. +itself activate material-store writes; the selected execution owner still +needs explicit Material Lifecycle source authority. ## Activation Gate Before changing a material store: -1. Resolve the current project, `goal_id`, registered agent, and active todo - through `loopx start-goal --guided`, `loopx status`, or `loopx diagnose`. +1. Resolve the current project and execution owner. For a Goal route, use the + existing registered agent/active Todo. For an ordinary project route, + resolve the current Core project context and accepted request; do not run + `start-goal` or create a dummy Goal merely to obtain material metadata. 2. Run `loopx project-skill status --project . --skill loopx-material` and confirm the required host surfaces are current. -3. Confirm the selected todo explicitly targets `material_lifecycle`, or that - the goal authority declares an active Material Lifecycle profile and its - source store. A catalog entry or project-local skill is not activation. -4. Confirm the goal boundary covers the exact private adapter and authority - paths. Public LoopX contracts never grant access to private source content. +3. Require explicit activation: either the selected Goal Todo targets + `material_lifecycle`/the Goal declares its source profile, or the project + source owner declares an active material profile and an existing Core + workspace write grant. A catalog entry, scope reference or skill copy is + not activation. +4. Confirm the Goal boundary or Core project write grant covers the exact + private adapter/authority paths. The project source verifier must bind the + current caller, audience, store/profile and owner gate, including expiry + and revocation. Ref names never grant access; enforce the authorization + fence within staging/publication as well as preflight checks. 5. Run `loopx material-lifecycle architecture --format json` and preserve its - default-off, owner-gated, provider-neutral boundaries. + default-off, owner-gated, provider-neutral boundaries. The project source + route supports inventory, candidate intake/rollback, rerank packets, + readable projection and settlement; it does not widen Goal-only migration, + rebuild or Explore APIs. If the project-local skill is missing, preview an explicit project install; do not fall back to a global copy. If activation or authority is missing, stop From 67ff849210f5239e1c91fd9cca2e04cb3f5ea59e Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Sun, 4 Oct 2026 17:04:07 +0800 Subject: [PATCH 3/5] test(material): qualify project intake ownership and rollback fences Signed-off-by: huangruiteng --- .../test_material_candidate_intake.py | 176 ++++++++++++++++++ .../test_material_project_scope.py | 65 +++++++ 2 files changed, 241 insertions(+) create mode 100644 tests/capabilities/test_material_project_scope.py diff --git a/tests/capabilities/test_material_candidate_intake.py b/tests/capabilities/test_material_candidate_intake.py index e04737e11b..4be7edaadf 100644 --- a/tests/capabilities/test_material_candidate_intake.py +++ b/tests/capabilities/test_material_candidate_intake.py @@ -384,3 +384,179 @@ def stage_candidate(self, **kwargs: object) -> MaterialStagedCandidate: with pytest.raises(ValueError, match="local path"): apply(UnsafeReadbackProvider()) + + +class AuthorizedProjectProvider(FakeCandidateProvider): + """Source-owner fixture resolves refs; packet metadata is not permission.""" + + def __init__(self): + super().__init__() + self.write_allowed = True + self.revoke_after_stage = False + + def verify_project_scope(self, *, project_scope, store_id, owner_gate_ref, observed_at): + self.calls.append('verify-project') + return ( + self.write_allowed + and project_scope == { + 'project_ref': 'project:alpha', + 'source_profile_ref': 'profile:materials', + 'workspace_grant_ref': 'grant:workspace-write', + } + and store_id == 'store:materials' + and owner_gate_ref == 'gate:material-input' + ) + + def stage_candidate(self, **kwargs): + result = super().stage_candidate(**kwargs) + if self.revoke_after_stage: + self.write_allowed = False + return result + + +def project_proposal(**scope_changes): + from loopx.capabilities.material_lifecycle import MaterialProjectScope + scope = { + 'project_ref': 'project:alpha', + 'source_profile_ref': 'profile:materials', + 'workspace_grant_ref': 'grant:workspace-write', + **scope_changes, + } + return build_material_candidate_intake_proposal( + project_scope=MaterialProjectScope(**scope), + proposal_id='proposal:project-input', store_id='store:materials', + source_authority_revision='revision:42', material_ref='material:A43', + source_ref='source:paper-43', source_revision='source-revision:7', + exact_read_ref='exact-read:paper-43', content_digest=CONTENT_DIGEST, + content_size_bytes=len(CONTENT), observed_at=OBSERVED_AT, + ) + + +def apply_project(provider, intake_proposal=None, gate='gate:material-input'): + return apply_material_candidate_intake( + provider=provider, provider_id=provider.provider_id, + intake_proposal=intake_proposal or project_proposal(), content=CONTENT, + owner_gate_ref=gate, receipt_id='receipt:project-intake', observed_at=OBSERVED_AT, + ) + + +def test_project_intake_and_rollback_keep_scope_without_a_goal(): + provider = AuthorizedProjectProvider() + candidate = project_proposal() + receipt = apply_project(provider, candidate) + assert 'goal_id' not in candidate and 'goal_id' not in receipt + assert receipt['project_scope'] == candidate['project_scope'] + assert receipt['capability']['scope'] == 'project' + assert provider.calls.count('verify-project') == 2 + restored = rollback_material_candidate_intake( + provider=provider, provider_id=provider.provider_id, apply_receipt=receipt, + owner_gate_ref='gate:material-input', receipt_id='receipt:project-rollback', + observed_at=OBSERVED_AT, + ) + assert restored['project_scope'] == candidate['project_scope'] + assert 'goal_id' not in restored + assert restored['capability']['scope'] == 'project' + assert provider.authority_revision == 'revision:42' + + +def test_project_intake_requires_a_source_verifier_before_provider_access(): + provider = FakeCandidateProvider() + with pytest.raises(ValueError, match='source authorization verifier'): + apply_project(provider) + assert provider.calls == [] + + +@pytest.mark.parametrize('field,value', [ + ('project_ref', 'project:other'), + ('source_profile_ref', 'profile:other'), + ('workspace_grant_ref', 'grant:read-only'), +]) +def test_project_scope_selectors_cannot_borrow_other_authority(field, value): + provider = AuthorizedProjectProvider() + with pytest.raises(ValueError, match='authorization was not verified'): + apply_project(provider, project_proposal(**{field: value})) + assert provider.calls == ['verify-project'] + assert provider.authority_revision == 'revision:42' + + +def test_project_gate_must_match_the_exact_source_operation(): + provider = AuthorizedProjectProvider() + with pytest.raises(ValueError, match='authorization was not verified'): + apply_project(provider, gate='gate:unrelated') + assert provider.calls == ['verify-project'] + + +def test_project_revocation_after_staging_blocks_authority_cutover(): + provider = AuthorizedProjectProvider() + provider.revoke_after_stage = True + with pytest.raises(ValueError, match='authorization was not verified'): + apply_project(provider) + assert provider.authority_revision == 'revision:42' + assert provider.calls.count('verify-project') == 2 + assert 'switch' not in provider.calls + + +def test_revoked_project_scope_blocks_rollback_before_provider_access(): + provider = AuthorizedProjectProvider() + receipt = apply_project(provider) + provider.write_allowed = False + provider.calls.clear() + with pytest.raises(ValueError, match='authorization was not verified'): + rollback_material_candidate_intake( + provider=provider, provider_id=provider.provider_id, apply_receipt=receipt, + owner_gate_ref='gate:material-input', receipt_id='receipt:project-rollback', + observed_at=OBSERVED_AT, + ) + assert provider.calls == ['verify-project'] + assert provider.authority_revision == 'revision:43' + + +@pytest.mark.parametrize('goal_id,scope', [ + (None, None), + ('goal:manager', {'project_ref': 'project:alpha', 'source_profile_ref': 'profile:materials', + 'workspace_grant_ref': 'grant:workspace-write'}), + (None, {'project_ref': 'project:alpha'}), + (None, {'project_ref': 'project:alpha', 'source_profile_ref': 'https://example.test/private', + 'workspace_grant_ref': 'grant:workspace-write'}), +]) +def test_material_owner_requires_one_complete_public_safe_scope(goal_id, scope): + from loopx.capabilities.material_lifecycle.ownership import material_owner_fields + with pytest.raises((ValueError, TypeError)): + material_owner_fields(goal_id=goal_id, project_scope=scope) + + +def test_project_metadata_cannot_reuse_a_goal_capability_declaration(): + provider = AuthorizedProjectProvider() + candidate = project_proposal() + candidate['capability']['scope'] = 'goal' + with pytest.raises(ValueError, match='project capability scope'): + apply_project(provider, candidate) + assert provider.calls == [] + + + +def test_project_rollback_rejects_a_mismatched_capability_before_provider_access(): + provider = AuthorizedProjectProvider() + receipt = apply_project(provider) + receipt['capability']['scope'] = 'goal' + provider.calls.clear() + with pytest.raises(ValueError, match='project capability scope'): + rollback_material_candidate_intake( + provider=provider, provider_id=provider.provider_id, apply_receipt=receipt, + owner_gate_ref='gate:material-input', receipt_id='receipt:project-rollback', + observed_at=OBSERVED_AT, + ) + assert provider.calls == [] + assert provider.authority_revision == 'revision:43' + + +def test_project_verifier_must_return_true_rather_than_a_truthy_value(): + class TruthyProvider(AuthorizedProjectProvider): + def verify_project_scope(self, **kwargs): + super().verify_project_scope(**kwargs) + return 1 + + provider = TruthyProvider() + with pytest.raises(ValueError, match='authorization was not verified'): + apply_project(provider) + assert provider.calls == ['verify-project'] diff --git a/tests/capabilities/test_material_project_scope.py b/tests/capabilities/test_material_project_scope.py new file mode 100644 index 0000000000..9c5cd784a8 --- /dev/null +++ b/tests/capabilities/test_material_project_scope.py @@ -0,0 +1,65 @@ +"""Project material packets keep source ownership without a synthetic Goal.""" +from loopx.capabilities.material_lifecycle import ( + MaterialProjectScope, + build_material_intake_ranking_settlement, + build_material_readable_projection, + build_material_rerank_apply_receipt, + build_material_rerank_proposal, + build_material_store_inventory, +) + +SCOPE = MaterialProjectScope('project:alpha', 'profile:materials', 'grant:workspace-write') +NOW = '2026-10-04T08:30:00+00:00' + + +def test_project_inventory_ranking_projection_and_settlement_share_owner(): + inventory = build_material_store_inventory( + project_scope=SCOPE, store_id='store:materials', store_revision='revision:42', + observed_at=NOW, source_snapshot_ref='snapshot:42', backup_ref='backup:42', + source_digest='digest:42', lifecycle_counts={}, stable_ids_verified=True, + backup_verified=True, + ) + assert inventory['item_count'] == 0 + proposal = build_material_rerank_proposal( + project_scope=SCOPE, proposal_id='proposal:rank', inventory_ref=inventory['inventory_ref'], + decision_evidence_ref='decision:project-priorities', observed_at=NOW, + target_window_size=30, max_moved_items=3, max_rank_displacement=3, + no_change_reason='initial project policy is unchanged', + ) + ranking = build_material_rerank_apply_receipt( + project_scope=SCOPE, receipt_id='receipt:ranking', proposal_ref=proposal['proposal_ref'], + observed_at=NOW, status='applied', before_revision='revision:43', after_revision='revision:44', + owner_gate_ref='gate:material-input', validation_ref='validation:ranking', + applied_material_refs=['material:one'], rollback_ref='rollback:ranking', + ) + rendered, projection = build_material_readable_projection( + project_scope=SCOPE, projection_id='projection:current', authority_revision='revision:44', + observed_at=NOW, entries=[{ + 'entry_ref': 'entry:one', 'rank': 1, 'title': 'One source', 'stage': 'candidate', + 'judgment': 'Current project relevance', 'action': 'Read the source', + 'materials': [{'material_ref': 'material:one', 'title': 'One source', + 'summary': 'Synthetic source text', 'judgment': 'Project relevance', + 'action': 'Read the source', 'audit_ref': 'audit:one', + 'source_status': 'Exact read completed'}], + }], expected_material_refs=['material:one'], canonical_item_count=1, top_window_size=1, + ) + settlement = build_material_intake_ranking_settlement( + project_scope=SCOPE, settlement_id='settlement:one', material_ref='material:one', + observed_at=NOW, decision_evidence_ref='decision:project-priorities', + value_classification='high_value', ranking_disposition='top_window', + intake_before_revision='revision:42', intake_after_revision='revision:43', + ranking_before_revision='revision:43', ranking_after_revision='revision:44', + intake_receipt_ref='receipt:intake', ranking_receipt_ref=ranking['receipt_ref'], + owner_gate_ref='gate:material-input', validation_ref='validation:settlement', + top_window_size=30, target_rank=1, authority_readback_verified=True, + ranking_source_contains_material_verified=True, ranked_membership_verified=True, + projection_receipt_ref=projection['receipt_ref'], rollback_ref='rollback:ranking', + ) + assert b'Synthetic source text' in rendered + for packet in [inventory, proposal, ranking, projection, settlement]: + assert 'goal_id' not in packet + assert packet['project_scope'] == inventory['project_scope'] + assert packet['capability']['scope'] == 'project' + assert packet['capability']['default_enabled'] is False + assert packet['capability']['creates_authority'] is False + assert packet['capability']['mutates_core_state'] is False From 52854809477a79921794581072deb69ad19f0f78 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Sun, 4 Oct 2026 18:04:15 +0800 Subject: [PATCH 4/5] refactor(material): keep project intake off the architecture advertisement Signed-off-by: huangruiteng --- loopx/capabilities/material_lifecycle/architecture.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/loopx/capabilities/material_lifecycle/architecture.py b/loopx/capabilities/material_lifecycle/architecture.py index 22e1b596e1..6ef45824fd 100644 --- a/loopx/capabilities/material_lifecycle/architecture.py +++ b/loopx/capabilities/material_lifecycle/architecture.py @@ -57,13 +57,6 @@ def build_material_lifecycle_architecture_packet() -> dict[str, object]: MATERIAL_READABLE_PROJECTION_RECEIPT_SCHEMA_VERSION, MATERIAL_EXPLORE_INTENT_SCHEMA_VERSION, ], - "project_source_scope": { - "requires_explicit_source_profile": True, - "requires_existing_workspace_write_grant": True, - "requires_source_authorization_verifier": True, - "creates_goal": False, - "creates_source_authority": False, - }, "sibling_capabilities": { "decision_context": ( "supplies revisioned evidence for bounded rerank proposals" From 98ebdc15c01f5223a2a8a3bee177e300e8e4b3b3 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Sun, 4 Oct 2026 18:04:15 +0800 Subject: [PATCH 5/5] docs(material): limit project scope guidance to the exercised intake path Signed-off-by: huangruiteng --- .../material-lifecycle-architecture-v0.md | 18 ------- ...aterial-lifecycle-architecture-v0.zh-CN.md | 6 --- .../capabilities/material_lifecycle/README.md | 50 ++++++------------- 3 files changed, 14 insertions(+), 60 deletions(-) diff --git a/docs/reference/protocols/material-lifecycle-architecture-v0.md b/docs/reference/protocols/material-lifecycle-architecture-v0.md index e1d12bffa6..148d5e5012 100644 --- a/docs/reference/protocols/material-lifecycle-architecture-v0.md +++ b/docs/reference/protocols/material-lifecycle-architecture-v0.md @@ -208,21 +208,3 @@ focused tests, and a public smoke. It does not ship: Those require a private read-only adapter, exact dual-read reconciliation, and an explicit owner gate. - -## Project source scope for ordinary conversations - -The inventory, candidate intake/rollback, rerank, readable projection and -intake-ranking settlement builders also accept an explicit `project_scope`. -The ownership fields are exclusive: a packet has either its existing `goal_id` -or `project_scope = {project_ref, source_profile_ref, workspace_grant_ref}`. -Project packets declare capability scope `project`, remain default-off and -contain no synthetic Goal. Existing Goal packets retain their representation. - -These refs do not authorize actions. Project candidate adapters must resolve -current source and Core workspace authority through -`MaterialProjectScopeVerifier.verify_project_scope`, binding the exact store, -profile, current caller/audience and owner gate with expiry/revocation checks. -Intake/rollback recheck before provider access and before publication; the -adapter also enforces its authorization fence within the write transaction. -Private profile initialization and storage stay source-owned. No migration, -rebuild, Explore, global authority or scheduler behavior is changed. diff --git a/docs/reference/protocols/material-lifecycle-architecture-v0.zh-CN.md b/docs/reference/protocols/material-lifecycle-architecture-v0.zh-CN.md index 0fd386b4f3..47750baf64 100644 --- a/docs/reference/protocols/material-lifecycle-architecture-v0.zh-CN.md +++ b/docs/reference/protocols/material-lifecycle-architecture-v0.zh-CN.md @@ -172,9 +172,3 @@ apply/rollback 编排、受限决策规划、catalog、架构 CLI、聚焦测试 - 自动重排、provider 调用、自动归档或自动推进 cursor。 这些能力必须经过私有只读 adapter、精确双读对账和显式 owner gate。 - -## 普通对话的项目来源 scope - -inventory、candidate intake/rollback、rerank、readable projection 与 intake-ranking settlement 支持明确的 `project_scope`。packet 必须二选一:保留原 `goal_id`,或使用 `{project_ref, source_profile_ref, workspace_grant_ref}`;项目形式不包含 dummy Goal。capability scope 为 `project`,默认关闭及 source owner 边界不变。 - -引用仅用于选择既有授权,不发放权限。project candidate adapter 必须通过 `MaterialProjectScopeVerifier.verify_project_scope` 核对当前 Core 调用者、受众、精确 store/profile、工作区写入边界及 owner gate 有效期/撤销状态。访问 provider 与发布 authority 前分别重新核验;真正的 adapter 还必须在写入事务内保持授权 fence。私有 profile 初始化与存储仍由来源 owner 提供,未扩大 migration/rebuild/Explore,也不改变全局 authority 或 scheduler。 diff --git a/loopx/capabilities/material_lifecycle/README.md b/loopx/capabilities/material_lifecycle/README.md index 520af23bbf..baf8a60f91 100644 --- a/loopx/capabilities/material_lifecycle/README.md +++ b/loopx/capabilities/material_lifecycle/README.md @@ -178,42 +178,20 @@ rebuild, bounded rerank, readable projection, Explore intent, apply, and rollback. Concrete legacy parsers, private storage adapters, source profiles, and provider credentials remain project owned. -## Explicit Project Source Ownership - -Ordinary project conversations can use an explicitly activated material source -without creating a Goal. Inventory, candidate intake/rollback, rerank packets, -readable projections and intake-ranking settlement accept `project_scope` in -place of `goal_id`: - -```python -from loopx.capabilities.material_lifecycle import MaterialProjectScope - -scope = MaterialProjectScope( - project_ref="project:example", - source_profile_ref="profile:materials", - workspace_grant_ref="grant:workspace-write", -) -``` - -Exactly one owner is required. The project form contains no `goal_id`; the -existing Goal form and default-off behavior are unchanged. These opaque refs -select an existing Core project grant and source profile. Constructing a scope -or packet does not activate a profile, grant writes, register a Goal or expose -manager context. - -A project candidate provider must implement `MaterialProjectScopeVerifier`. -Its `verify_project_scope` resolves the current Core caller, audience, workspace -write boundary, exact store/profile and owner gate, including expiry and -revocation. Intake and rollback require this verifier before provider access -and again before authority publication. Missing, mismatched or revoked -verification fails closed. The source adapter must also enforce authorization -inside its staging/publication transaction; the preflight Boolean is not a -transaction fence or an authority issuer. - -Fresh source initialization, private storage formats and transport integration -remain source-owner responsibilities. Goal-only migration, rebuild and Explore -APIs are not widened by this project intake path. A project skill or these -packet fields alone cannot initialize or mutate a source store. +## 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