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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type FieldCopy = Record<string, Readonly<{ description?: string; label: string }

const capabilityCopy: Record<WorkspaceLocale, Record<string, LocalizedCopy>> = {
en: {
goal_storage: { displayName: "New Goal storage target", description: "Fixed at creation and used after reviewed promotion. Existing Goals require a separate backed-up migration." },
manager_runtime: {
displayName: "Runtime",
description: "Selects the persistent host-tool profile used by owner manager conversations.",
Expand Down Expand Up @@ -77,6 +78,7 @@ const capabilityCopy: Record<WorkspaceLocale, Record<string, LocalizedCopy>> = {
},
},
"zh-CN": {
goal_storage: { displayName: "新 Goal 的目标存储", description: "创建时固定,审核晋升后生效。已有 Goal 需要单独备份、迁移;更改这里不会迁移数据。" },
manager_runtime: {
displayName: "运行环境",
description: "选择管家会话持续生效的宿主工具模式。",
Expand Down Expand Up @@ -145,6 +147,7 @@ const capabilityCopy: Record<WorkspaceLocale, Record<string, LocalizedCopy>> = {

const fieldCopy: Record<WorkspaceLocale, FieldCopy> = {
en: {
new_goal_provider: { label: "New Goal storage target (after promotion)", description: "File or SQLite; this setting does not perform promotion or migration." },
runtime_profile: { label: "Runtime profile", description: "Restricted keeps scoped LoopX reads only. Trusted owner enables normal host tools while protected operations retain separate checks." },
selection_policy: { label: "Selection policy", description: "Preferred allows an explicit user choice; pinned rejects another executor; flexible permits fallback only inside the eligible pool." },
executor_endpoint: { label: "Primary steward executor", description: "The preferred or pinned executor for this machine. In a flexible pool it is tried first when available." },
Expand All @@ -171,6 +174,7 @@ const fieldCopy: Record<WorkspaceLocale, FieldCopy> = {
enabled_agents: { label: "Enabled Goal Agents", description: "Enter one registered Goal-local Agent id per line. A private binding currently accepts exactly one Agent." },
},
"zh-CN": {
new_goal_provider: { label: "新 Goal 的目标存储(晋升后生效)", description: "选择 File 或 SQLite;保存设置不会自动晋升,也不会迁移已有 Goal。" },
runtime_profile: { label: "运行模式", description: "restricted 仅使用受限 LoopX 读取;trusted_owner 开放常规宿主工具,但受保护操作仍单独校验。" },
selection_policy: { label: "选择策略", description: "preferred 允许用户显式改选;pinned 拒绝其他执行器;flexible 只在已授权资源池内回退。" },
executor_endpoint: { label: "首选管家执行器", description: "本机首选或锁定的执行器;灵活池模式下优先尝试它。" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,15 @@ export function MachineConfigurationSettings({ section }: { section: "steward" |
<CapabilityEditorStatus available={editorAvailable} t={t} description={!selected.available_scopes.includes("machine") ? t("machine.goalOnly")
: t("machine.editorUnavailableDescription")} />

{selected.capability_id === "goal_storage" ? (
<section className="personal-capability-behavior-note">
<ShieldCheck aria-hidden size={18} />
<div><strong>{locale === "zh-CN" ? "仅影响此后创建的 Goal" : "Future Goals only"}</strong><p>{locale === "zh-CN"
? "创建时固定选择,审核晋升后生效。已有 Goal 不变;迁移需单独备份、停止写入并结算租约。"
: "Fixed at creation and used after reviewed promotion. Existing Goals are unchanged; migration requires a separate backup, stopped writers and settled leases."}</p></div>
</section>
) : null}

{selected.capability_id === "periodic_report" ? (
<section className="personal-capability-behavior-note">
<ShieldCheck aria-hidden size={18} />
Expand Down
51 changes: 51 additions & 0 deletions docs/reference/local-authority-provider-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,54 @@ and PostgreSQL continue to share the provider-neutral transaction conformance
contract; PostgreSQL's real-server qualification remains a separate gate.

See [reviewed promotion and recovery](reviewed-coordination-promotion.md) for the explicit saved-plan CLI journey.

## New Goal storage target (machine setting)

The **New Goal storage target** setting fixes a File or SQLite target at
creation. It is not live inheritance, automatic promotion, or an existing-Goal
migration. Until separately reviewed promotion, the existing legacy source is
still authoritative. After promotion the selected provider serves canonical
Todo/lease state; Run artifacts and other independently owned stores are not
moved by this preference.

Use **Settings → Capability Center → Device defaults → New Goal storage target**
or the revision-checked CLI:

```sh
# goal-storage.json:
# {"schema_version":"loopx_goal_storage_defaults_v0","new_goal_provider":"sqlite"}
loopx machine-config preview --namespace goal_storage --config-json goal-storage.json
loopx machine-config apply --namespace goal_storage --config-json goal-storage.json \
--expected-plan-revision PLAN_REVISION --execute
loopx machine-config inspect
loopx bootstrap --project ./new-project --goal-id new-project --dry-run
```

The preview reports `storage_target`; creation reports `storage_selection` with
`promotion_performed=false`. CLI and App creation share the same bootstrap
owner. Creation stores its intent before provider initialization, so retry after
interruption uses the same target even if the machine preference changed.
Reconnecting an existing Goal, including an implicit File Goal, does not adopt
a newer machine default. Importing existing Markdown does not count as a new
empty Goal. Explicit provider selection never falls back on failure.

Without this namespace, existing behavior remains unchanged. To stop applying
the preference to future Goals, preview `loopx machine-config remove
--namespace goal_storage`, then use its returned plan revision with `--execute`.
Configuration rollback also affects future creation only. Neither operation
switches existing storage or removes data. A File target keeps implicit File
routing until a committed authority exists; it does not create a dangling
identity-bound selector for an empty File document.

For already-promoted Goals use the [reviewed File/SQLite cutover](file-authority-state-log.md#reviewed-filesqlite-cutover):
stop writers, settle leases, review the saved plan, retain verified backups,
then migrate. Reverse migration must preserve newer writes. New-Goal defaults
and current-provider selection are separate facts. This opt-in setting does
not change the release default or complete D2/D3 qualification.

### 新 Goal 的目标存储

这是创建时固定的目标,审核晋升后才接管 canonical Todo/lease;不是“所有数据
已经存入 SQLite”。更改默认值只影响此后创建的空 Goal,既有 Goal、重新连接或
导入已有 Markdown 均不自动切换。创建中断后重试沿用已记录的选择。关闭或回滚
设置不迁回数据;已有 Goal 需停止写入、结算租约,走独立的备份和审核迁移流程。
21 changes: 21 additions & 0 deletions loopx/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import re
from pathlib import Path

from .capabilities.machine_configuration.goal_storage import new_goal_storage_target, initialize_goal_storage_target
from .registry import find_registry_goal
from .control_plane.coordination.legacy_writer_fence import legacy_todo_write_transaction, require_legacy_state_replacement_allowed
from .control_plane.coordination.runtime_shadow_writer_adapter import require_runtime_shadow_capture_prepared, begin_todo_runtime_shadow_capture, settle_todo_runtime_shadow_capture
Expand Down Expand Up @@ -371,6 +372,12 @@ def bootstrap_project(
execution_profile=execution_profile,
display_name=display_name,
)
previous_goal = find_registry_goal(registry, goal_id)
storage_target = ((previous_goal or {}).get("coordination") or {}).get("storage_target")
if previous_goal is None and not state_file.exists():
storage_target = new_goal_storage_target(runtime_root)
if storage_target is not None:
goal_entry.setdefault("coordination", {})["storage_target"] = storage_target
registry, registry_goal_action = merge_goal(registry, goal_entry, force=force)

state_exists = state_file.exists()
Expand Down Expand Up @@ -496,6 +503,7 @@ def bootstrap_project(
"private_boundary_note": "Add .loopx/ and .codex/goals/ to the project .gitignore if the goal state contains private evidence.",
"error": str(global_writability.get("error") or "global registry is not writable"),
}
storage_selection = None
shadow_capture = None
shadow_evidence: dict[str, Any] = {}
if not dry_run:
Expand All @@ -509,6 +517,13 @@ def bootstrap_project(
):
current_registry = registry_transaction.payload_copy()
current_goal = find_registry_goal(current_registry, goal_id)
# A concurrent creator or reconnect owns its frozen target, including absence.
if current_goal is not None or state_file.exists():
frozen = ((current_goal or {}).get("coordination") or {}).get("storage_target")
goal_entry.setdefault("coordination", {}).pop("storage_target", None)
if frozen is not None:
goal_entry["coordination"]["storage_target"] = frozen

previous_root = resolve_runtime_root(current_registry, None, registry_path=registry_path)
if previous_root != runtime_root:
for previous_goal in current_registry.get("goals", []):
Expand Down Expand Up @@ -550,6 +565,10 @@ def bootstrap_project(
current_registry["common_runtime_root"] = str(runtime_root)
registry, registry_goal_action = merge_goal(current_registry, goal_entry, force=force)
registry_transaction.commit(registry)
# Registry intent survives an interrupted initialization. Reconnect retries
# it outside the legacy/registry locks; changing machine defaults cannot
# retarget that Goal. The TS owner refuses replacing an existing provider.
storage_selection = initialize_goal_storage_target(runtime_root, find_registry_goal(registry, goal_id) or {})
if shadow_capture is not None:
shadow_evidence = settle_todo_runtime_shadow_capture({}, registry_path=registry_path,
runtime_root=runtime_root, goal_id=goal_id, capture=shadow_capture, emit_disabled=False)
Expand All @@ -564,6 +583,8 @@ def bootstrap_project(

return {
**shadow_evidence,
"storage_selection": storage_selection,
"storage_target": (find_registry_goal(registry, goal_id) or {}).get("coordination", {}).get("storage_target"),
"ok": True,
"dry_run": dry_run,
"project": str(project),
Expand Down
6 changes: 6 additions & 0 deletions loopx/capabilities/configuration_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ def capability_configuration_editor(
# effort the owning namespace would reject.
steward_endpoints, steward_efforts = _steward_executor_editor_options()
definitions: dict[str, dict[str, Any]] = {
"goal_storage": {
"supported_scopes": ["machine"], "writable_scopes": ["machine"],
"fields": [_field("new_goal_provider", "New Goal storage target (after promotion)", "select",
options=["file", "sqlite"], required=True,
description="Fixed at creation. Existing Goals need a separate backed-up migration; this setting does not promote them.")],
},
"todo_replan_cadence": {
"supported_scopes": ["machine", "goal"],
"writable_scopes": ["machine", "goal"],
Expand Down
3 changes: 3 additions & 0 deletions loopx/capabilities/machine_configuration/builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ def build_builtin_machine_configuration_registry() -> MachineConfigurationRegist
todo_replan_cadence_machine_configuration_namespace,
)

from .goal_storage import goal_storage_machine_configuration_namespace

return (
MachineConfigurationRegistry()
.register(goal_storage_machine_configuration_namespace())
.register(manager_runtime_machine_configuration_namespace())
.register(periodic_report_machine_configuration_namespace())
.register(todo_replan_cadence_machine_configuration_namespace())
Expand Down
53 changes: 53 additions & 0 deletions loopx/capabilities/machine_configuration/goal_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Machine-configuration and bootstrap transport for the TS storage owner."""
from __future__ import annotations

from collections.abc import Mapping
from pathlib import Path
from typing import Any

from .contract import MachineConfigurationNamespace
from ...control_plane.effect_runtime import effect_runtime_result

GOAL_STORAGE_DEFAULTS_SCHEMA = "loopx_goal_storage_defaults_v0"
NEW_GOAL_STORAGE_METHOD = "coordination.local_authority.new_goal_storage"


def normalize_goal_storage_defaults(raw: Mapping[str, Any]) -> dict[str, Any]:
# Configuration-envelope validation only; target resolution/admission is TS-owned.
if set(raw) != {"schema_version", "new_goal_provider"} or raw.get("schema_version") != GOAL_STORAGE_DEFAULTS_SCHEMA:
raise ValueError("goal_storage requires schema_version and new_goal_provider")
if raw.get("new_goal_provider") not in ("file", "sqlite"):
raise ValueError("new_goal_provider must be file or sqlite")
return dict(raw)


def goal_storage_machine_configuration_namespace() -> MachineConfigurationNamespace:
return MachineConfigurationNamespace(
namespace="goal_storage", schema_versions=frozenset({GOAL_STORAGE_DEFAULTS_SCHEMA}),
normalize=normalize_goal_storage_defaults, project_public=dict,
apply_public_update=lambda _current, update: dict(update),
title="New Goal storage target",
description=("Fixed when a new Goal is created; used after reviewed promotion. "
"Does not promote Goals or migrate existing data. Existing Goals keep their selection."),
default_configuration={"schema_version": GOAL_STORAGE_DEFAULTS_SCHEMA, "new_goal_provider": "file"},
documentation={"path": "docs/reference/local-authority-provider-selection.md",
"url": "https://github.com/loopx-project/loopx/blob/main/docs/reference/local-authority-provider-selection.md"},
)


def new_goal_storage_target(runtime_root: Path) -> dict[str, Any] | None:
from .builtins import build_builtin_machine_configuration_registry
from .store import read_machine_configuration
configuration = read_machine_configuration(runtime_root, registry=build_builtin_machine_configuration_registry())
raw = (configuration or {}).get("namespaces", {}).get("goal_storage")
if raw is None:
return None
return effect_runtime_result(NEW_GOAL_STORAGE_METHOD, {"action": "resolve", "configuration": raw})


def initialize_goal_storage_target(runtime_root: Path, goal: Mapping[str, Any]) -> dict[str, Any] | None:
target = (goal.get("coordination") or {}).get("storage_target")
if target is None:
return None
return effect_runtime_result(NEW_GOAL_STORAGE_METHOD, {"action": "initialize", "runtime_root": str(runtime_root),
"goal_id": goal["id"], "target": target})
29 changes: 29 additions & 0 deletions loopx/control_plane/coordination/local_authority_defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/** New-Goal target selection; never live inheritance or authority promotion. */
import type {JsonObject} from "../effect_program.ts";
import {requireJsonObject} from "../runtime_decode.ts";
import {requireAuthorityStoreId} from "./authority_store_codec.ts";
import {requireLocalAuthorityRuntimeRoot, selectLocalAuthorityTarget} from "./local_authority_provider.ts";

const GOAL_STORAGE_DEFAULTS_SCHEMA = "loopx_goal_storage_defaults_v0";
const NEW_GOAL_STORAGE_TARGET_SCHEMA = "loopx_new_goal_storage_target_v0";
function provider(value: unknown): "file" | "sqlite" {
if (value !== "file" && value !== "sqlite") throw new Error("New Goal storage must be file or sqlite");
return value;
}
export async function manageNewGoalStorage(request: JsonObject): Promise<JsonObject> {
if (request.action === "resolve") {
const config = requireJsonObject(request.configuration, "Goal storage defaults");
if (config.schema_version !== GOAL_STORAGE_DEFAULTS_SCHEMA || Object.keys(config).some(key => !["schema_version", "new_goal_provider"].includes(key))) {
throw new Error("Invalid Goal storage defaults");
}
return {schema_version: NEW_GOAL_STORAGE_TARGET_SCHEMA, provider: provider(config.new_goal_provider)};
}
if (request.action !== "initialize") throw new Error("Unknown new Goal storage action");
const target = requireJsonObject(request.target, "New Goal storage target");
if (target.schema_version !== NEW_GOAL_STORAGE_TARGET_SCHEMA || Object.keys(target).some(key => !["schema_version", "provider"].includes(key))) {
throw new Error("Invalid new Goal storage target");
}
const selected = await selectLocalAuthorityTarget(requireLocalAuthorityRuntimeRoot(request.runtime_root),
requireAuthorityStoreId(request.goal_id, "goal id"), provider(target.provider), true, "creation_retry");
return {...selected, status: selected.selection_preserved ? "existing_authority_preserved" : "selected", applies_to: "canonical_authority", promotion_performed: false};
}
Loading
Loading