Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changes/twitter/0.2.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## 0.2.0 - 2026-08-17
### Added
* Added the guided OAuth and Bookmark Source setup wizard.

### Changed
* Limited the Core setup protocol to OAuth and account operations; Bookmark Source scheduling now uses ordinary deployment resources.
33 changes: 32 additions & 1 deletion app/business/cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from app.business.deployment_config import DeploymentConfigManager
from app.business.job import JobManager
from app.engine import SessionLocal
from app.schemas.cron import CronID, CronModel
from app.schemas.cron import CronForm, CronID, CronModel
from app.schemas.job import JobModel, JobStatus
from libs.obsrv.main import get_logger

Expand Down Expand Up @@ -132,3 +132,34 @@ def run_now(cls, cron_id: CronID) -> JobModel:
db_session.commit()
db_session.refresh(job)
return job

@classmethod
def create(cls, form: CronForm) -> CronModel:
"""Validate and create one Cron template."""
if not croniter.croniter.is_valid(form.schedule):
raise ValueError("Cron schedule must be a valid five-field UNIX expression")
with SessionLocal() as db_session:
cron = CronModel(**form.model_dump())
db_session.add(cron)
db_session.commit()
db_session.refresh(cron)
return cron

@classmethod
def update(cls, cron_id: CronID, form: CronForm) -> CronModel:
"""Validate and replace the editable fields of one Cron template."""
if not croniter.croniter.is_valid(form.schedule):
raise ValueError("Cron schedule must be a valid five-field UNIX expression")
with SessionLocal() as db_session:
cron = db_session.get(CronModel, cron_id)
if cron is None:
raise ValueError(f"Cron {cron_id} does not exist")
cron.schedule = form.schedule
cron.enabled = form.enabled
cron.job_type = form.job_type
cron.job_parameters = dict(form.job_parameters)
cron.job_timeout_seconds = form.job_timeout_seconds
db_session.add(cron)
db_session.commit()
db_session.refresh(cron)
return cron
9 changes: 8 additions & 1 deletion app/business/extension/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,17 @@
- enable 先启动 runtime,再调用 atomic enabled RPC;返回 version 不一致时移除 peer 并停止旧 runtime。
- disable 先停止 runtime,再调用 RPC;RPC 失败时重启 exact prior runtime,durable intent 不变。
- cold restore 失败不得删除 `enabled[]`;bootstrap/readiness 明确报告 durable intent 尚未运行。
- `ExtensionBase` 向 Extension 提供 fresh validated config 读写与 typed deployment-wide state
mutation;Extension 不接触 SQLModel,数据库行锁与并发语义仍由 Core store 实现。
- Extension-specific setup 通过 running Extension 发布的 typed Peer inbound 实现;Host 不提供
generic setup/wizard protocol。公开 OAuth callback 必须是 lifecycle-bound exact route claim。
- Registry origin 每次 operation 按 executing Peer override、deployment config、process fallback
解析一次,并由 exact Release 与 Distribution consumer 共用该 snapshot。

## 权限和持久化

`state.py` 是唯一 DB adapter。`enabled[]` 只能通过
`state.py` 是唯一 DB adapter。`extensions.state` 是 deployment-wide Extension-produced state;
`enabled[]` 只能通过
`inkcre.set_extension_peer_enabled(p_name text,p_peer_id uuid,p_enabled boolean)` 变更,禁止
read-modify-write。SQLModel 不应泄露成 Host 的稳定接口。

Expand Down
6 changes: 4 additions & 2 deletions app/business/extension/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
ExtensionBase,
ExtensionDelegationError,
ExtensionHost,
PublicHTTPRoute,
)
from .state import ExtensionState
from .state import InstalledExtension

__all__ = [
"EXTENSION_HOST",
"EXTENSION_MANAGEMENT_CAPABILITY",
"ExtensionBase",
"ExtensionDelegationError",
"ExtensionHost",
"ExtensionState",
"InstalledExtension",
"PublicHTTPRoute",
]
62 changes: 62 additions & 0 deletions app/business/extension/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Registry-origin authority for Core Extension Host operations."""

from urllib.parse import urlsplit, urlunsplit

import pydantic

from app.business.deployment_config import DeploymentConfigManager
from app.business.peer import PeerManager
from app.settings import settings


EXTENSION_REGISTRY_CONFIG_KEY = "extension.registry"
EXTENSION_REGISTRY_CONFIG_SCHEMA = "extension.registry.config.v1"


def normalize_registry_origin(value: str) -> str:
parts = urlsplit(value.strip())
if (
parts.scheme not in {"http", "https"}
or not parts.netloc
or parts.username is not None
or parts.password is not None
or parts.path not in {"", "/"}
or parts.query
or parts.fragment
):
raise ValueError("Extension Registry URL must be one HTTP(S) origin")
return urlunsplit((parts.scheme, parts.netloc, "", "", ""))


class ExtensionRegistryDeploymentConfig(pydantic.BaseModel):
"""Deployment default overridden only by an executing Host Peer."""

model_config = pydantic.ConfigDict(extra="forbid", frozen=True)

extension_registry_url: str | None = None

@pydantic.field_validator("extension_registry_url")
@classmethod
def validate_registry_origin(cls, value: str | None) -> str | None:
if value is None or not value.strip():
return None
return normalize_registry_origin(value)


DeploymentConfigManager.register_schema(
EXTENSION_REGISTRY_CONFIG_SCHEMA,
ExtensionRegistryDeploymentConfig,
)


def resolve_extension_registry_origin() -> str:
"""Resolve one immutable origin snapshot for a Host operation."""
peer_override = PeerManager.get_current_config().extension_registry_url
if peer_override is not None:
return peer_override
deployment = DeploymentConfigManager.get(EXTENSION_REGISTRY_CONFIG_KEY)
if deployment is not None:
configured = ExtensionRegistryDeploymentConfig.model_validate(deployment)
if configured.extension_registry_url is not None:
return configured.extension_registry_url
return normalize_registry_origin(settings.extension_registry_url)
Loading
Loading