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
1 change: 1 addition & 0 deletions .changes/extension-install-boundary.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Extension 安装改为只确认 published Release;Python 兼容性和已加载版本保护在启用前执行。
1 change: 1 addition & 0 deletions .changes/peer-runtime-identity.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Publish each running Peer's application version with its runtime identity.
19 changes: 10 additions & 9 deletions app/business/extension/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,21 +251,15 @@ def _resolve(
raise ExtensionCompatibilityError(
f"{name}@{version} is not available for this operation"
)
association = require_python_association(release)
return release, association
return release

async def install(self, name: str, version: str) -> InstalledExtension:
validate_coordinate(name, version)
existing = await self.store.get(name)
if existing is not None and existing.version == version:
return existing
loaded_version = self._loaded_versions.get(name)
if loaded_version is not None and loaded_version != version:
raise ExtensionRestartRequiredError(
f"{name} {loaded_version} was already imported; restart before installing {version}"
)
release_client, _ = await self._operation_consumers()
release, _ = await asyncio.to_thread(
release = await asyncio.to_thread(
self._resolve,
name,
version,
Expand Down Expand Up @@ -323,13 +317,20 @@ async def _operation_consumers(

async def _acquire(self, state: InstalledExtension):
release_client, distribution_consumer = await self._operation_consumers()
release, association = await asyncio.to_thread(
release = await asyncio.to_thread(
self._resolve,
state.name,
state.version,
allow_yanked=True,
release_client=release_client,
)
association = require_python_association(release)
loaded_version = self._loaded_versions.get(state.name)
if loaded_version is not None and loaded_version != state.version:
raise ExtensionRestartRequiredError(
f"{state.name} {loaded_version} was already imported; "
f"restart before enabling {state.version}"
)
acquired = await asyncio.to_thread(distribution_consumer.acquire, release, association)
return association, acquired

Expand Down
6 changes: 3 additions & 3 deletions app/business/peer/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
- `route_to_peer` is a caller-local constraint and never enters payload/advertisement。
- A normal response or outcome-unknown dispatch stops generic failover。
- HTTP absolute URLs come only from owner config + fixed inbound paths;do not infer them from bind host or requests。
- Browser runtimes register with an ordinary `peers` upsert whose payload contains only
runtime-owned `id`、`name`、`config_schema` and `capabilities`; omitted owner-authored
`config` and `labels` must remain unchanged.
- Peer runtimes register with an ordinary `peers` upsert. A default `name` is inserted only for a
new identity; registration refreshes runtime-owned `application_version` and `config_schema`
without replacing Human-owned `name`、`config` or `labels`.
6 changes: 5 additions & 1 deletion app/business/peer/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
normalize_capability_snapshot,
)
from app.settings import settings
from app.version import APPLICATION_VERSION

from .contracts import (
CapabilityDelegationUnavailable,
Expand Down Expand Up @@ -58,7 +59,10 @@ async def register_self(cls) -> PeerModel:
"""Upsert runtime-owned identity/schema without changing owner configuration."""
async with peer_uow() as peers:
peer = await peers.register(
settings.peer_id, settings.peer_name, cls._config_contract.json_schema()
settings.peer_id,
settings.peer_name,
APPLICATION_VERSION,
cls._config_contract.json_schema(),
)
logger.info("Peer registered", extra={"peer": str(peer.id), "name": peer.name})
return peer
Expand Down
3 changes: 2 additions & 1 deletion app/database_contract/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,8 @@ def development_baseline_fingerprint(
(
"development_peer",
sql.SQL(
"SELECT id::text, name, labels, config, config_schema, capabilities, "
"SELECT id::text, name, application_version, labels, config, config_schema, "
"capabilities, "
"EXTRACT(EPOCH FROM lease_expires_at)::bigint, "
"EXTRACT(EPOCH FROM created_at)::bigint "
"FROM {}.peers WHERE id = %s"
Expand Down
2 changes: 1 addition & 1 deletion app/database_contract/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


CONTRACT_FORMAT = 1
CONTRACT_REVISION = "peer-extension-setup-v1"
CONTRACT_REVISION = "peer-runtime-identity-v1"

PROTOCOL_SCHEMA = "inkcre"
INTERNAL_SCHEMA = "inkcre_internal"
Expand Down
3 changes: 2 additions & 1 deletion app/database_contract/readiness.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ def _seed_component(cursor, profile: str) -> dict[str, Any]:
return {"status": "not_required"}
cursor.execute(
sql.SQL(
"SELECT name, labels, config, config_schema, capabilities, "
"SELECT name, application_version, labels, config, config_schema, capabilities, "
"lease_expires_at, created_at, updated_at "
"FROM {}.peers WHERE id = %s"
).format(sql.Identifier(PROTOCOL_SCHEMA)),
Expand All @@ -519,6 +519,7 @@ def _seed_component(cursor, profile: str) -> dict[str, Any]:
row = cursor.fetchone()
expected = (
DEVELOPMENT_PEER_NAME,
None,
["development", "canonical-seed"],
{},
{},
Expand Down
18 changes: 15 additions & 3 deletions app/persistence/peer/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,26 @@ class PeerRepository:
def __init__(self, session: AsyncSession) -> None:
self._session = session

async def register(self, peer_id: PeerRef, name: str, schema: dict) -> PeerModel:
async def register(
self,
peer_id: PeerRef,
name: str,
application_version: str,
schema: dict,
) -> PeerModel:
statement = insert(PeerModel).values(
id=peer_id, name=name, labels=[], config={}, config_schema=schema, capabilities=[]
id=peer_id,
name=name,
application_version=application_version,
labels=[],
config={},
config_schema=schema,
capabilities=[],
)
statement = statement.on_conflict_do_update(
index_elements=["id"],
set_={
"name": statement.excluded.name,
"application_version": statement.excluded.application_version,
"config_schema": statement.excluded.config_schema,
},
).returning(PeerModel)
Expand Down
4 changes: 4 additions & 0 deletions app/schemas/peer/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ class PeerModel(sqlmodel.SQLModel, table=True):
),
)
name: str = sqlmodel.Field(sa_column=sqlalchemy.Column(sqlalchemy.Text, nullable=False))
application_version: str | None = sqlmodel.Field(
default=None,
sa_column=sqlalchemy.Column(sqlalchemy.Text, nullable=True),
)
labels: list[str] = sqlmodel.Field(
default_factory=list,
sa_column=sqlalchemy.Column(
Expand Down
8 changes: 7 additions & 1 deletion app/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
"""Core Extension Host SDK version, independent of the service package version."""
"""Core application and Extension Host SDK versions."""

from pathlib import Path
import tomllib

CORE_VERSION = "0.3.0"
APPLICATION_VERSION = tomllib.loads(
(Path(__file__).parents[1] / "pyproject.toml").read_text(encoding="utf-8")
)["project"]["version"]
10 changes: 6 additions & 4 deletions docs/40-deployment/database-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,12 @@ Deleting a referenced `storages` catalog row is `RESTRICT`。Changing/deleting a

## Peer Discovery Protocol

`peers` 持有 deployment Peer identity、owner config/schema、完整 capability/inbound snapshot 与
`lease_expires_at`。Capability snapshot 是 runtime-owned derived projection;`labels` 不参与 routing,readiness
不进入 advertisement。HTTP inbound 的 absolute URL 只由当前 Peer 的 `config.http_public_base_url` 与固定业务 path
组合,Peer row 不再持有通用 `rest_api_url`。
`peers` 持有 deployment Peer identity、应用版本、owner config/schema、完整 capability/inbound snapshot 与
`lease_expires_at`。Peer runtime 在注册时写入自己的 `application_version`,供管理界面识别实际运行的应用;它不替代
Extension Host SDK 的兼容性版本。旧行可以暂时为 `NULL`,Peer 再次注册后自然补齐。注册只在首次创建 identity 时写入
默认名称,后续刷新应用版本和 schema 时保留 Human 修改的名称、配置与标签。Capability snapshot 是 runtime-owned
derived projection;`labels` 不参与 routing,readiness 不进入 advertisement。HTTP inbound 的 absolute URL 只由当前
Peer 的 `config.http_public_base_url` 与固定业务 path 组合,Peer row 不再持有通用 `rest_api_url`。

`renew_peer_lease(peer uuid, ttl_seconds integer) -> timestamptz` 是 `SECURITY INVOKER`、database-time helper。
调用者提供正 TTL;不存在的 Peer 或非正 TTL 明确失败。普通 Peer row update 不续租,graceful shutdown 清空 expiry,
Expand Down
2 changes: 1 addition & 1 deletion docs/_shared
Submodule _shared updated 55 files
+43 −3 .github/workflows/pages-preview.yml
+2 −2 10-prd/_drivers/business-and-service-objectives.md
+2 −2 10-prd/_drivers/market-and-user-pressures.md
+1 −1 10-prd/_drivers/operational-realities.md
+2 −2 10-prd/glossary.md
+16 −2 20-product-tdd/extension-documentation-hosting.md
+13 −0 20-product-tdd/peer-database-runtime-contract.md
+3 −3 20-product-tdd/semantic-retrieval-and-peer-capabilities.md
+5 −3 20-product-tdd/system-state-and-authority.md
+97 −0 tasks/user-getting-started/implementation-plan.md
+46 −28 website/.vitepress/config.mts
+190 −12 website/.vitepress/theme/custom.css
+78 −13 website/README.md
+16 −22 website/content/en/about/index.md
+77 −65 website/content/en/developer/architecture.md
+15 −14 website/content/en/developer/ecosystem/index.md
+14 −14 website/content/en/developer/ecosystem/source-extension.md
+6 −6 website/content/en/developer/index.md
+23 −40 website/content/en/getting-started.md
+7 −6 website/content/en/guide/collect.md
+7 −7 website/content/en/guide/connect-cli.md
+13 −9 website/content/en/guide/connect.md
+10 −20 website/content/en/guide/daily-use.md
+7 −2 website/content/en/guide/extensions.md
+14 −13 website/content/en/guide/first-source.md
+70 −0 website/content/en/guide/organization.md
+19 −42 website/content/en/guide/schedules.md
+46 −20 website/content/en/guide/search.md
+11 −13 website/content/en/guide/sinks/chatgpt.md
+4 −6 website/content/en/guide/sources.md
+10 −13 website/content/en/guide/sources/github.md
+29 −27 website/content/en/guide/sources/mail.md
+22 −21 website/content/en/guide/sources/memos.md
+16 −19 website/content/en/guide/sources/rss.md
+15 −17 website/content/en/guide/sources/telegram.md
+18 −15 website/content/en/guide/sources/twitter.md
+2 −2 website/content/en/guide/troubleshooting.md
+54 −27 website/content/en/index.md
+0 −80 website/content/en/self-hosted/advanced.md
+122 −0 website/content/en/self-hosted/custom.md
+0 −51 website/content/en/self-hosted/getting-started.md
+7 −7 website/content/en/self-hosted/heroku-neon.md
+5 −12 website/content/en/self-hosted/index.md
+7 −7 website/content/en/self-hosted/render-neon.md
+ − website/content/public/images/client-web/collection-job.png
+ − website/content/public/images/client-web/extension-management.png
+ − website/content/public/images/client-web/info-base-graph.png
+ − website/content/public/images/client-web/lexical-search.png
+ − website/content/public/images/client-web/mail-content.png
+ − website/content/public/images/client-web/memos-setup.png
+ − website/content/public/images/client-web/settings-overview.png
+ − website/content/public/images/client-web/source-schedule.png
+ − website/content/public/images/client-web/sources-overview.png
+ − website/content/public/images/client-web/twitter-setup.png
+9 −0 website/content/public/images/inkcre-mark.svg
1 change: 1 addition & 0 deletions migrations/revision-integrity.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"3f7a9c2d5e1b_merge_extension_registry_feature_retrieval.py": "a12ebb2f37a00dcc13b6dea6b7f9be0f6870d57530e8d234eb934c080d027c07",
"50b2c08dd267_add_relation_endpoint_indexes.py": "56a0dbfd5e032fa57b125e478940ff89254c167b38fe0a7a98046498577d13e5",
"77cd53ad8080_add_global_jobs_crons_and_source_runtime.py": "8eda54520a7099c957ee6a1b5e6e48d5ee2b3dbe18b0dea4b6a5246dbef04bd2",
"a0465e3b028f_add_peer_application_version.py": "6ba72a21e6361cd92c5977856602105498be0f4ea6e6454330dfdb89e3b50253",
"a1b2c3d4e5f6_add_clients_modify_extensions_enabled.py": "15292a9fcf834474c8453e3d1d9db962962fb5d729bc0b80053615cf71315b96",
"a8b9c0d1e2f3_add_ai_registry_and_profiles.py": "4c9346dac54e797ba8cb6fa7dfa8604482a114905bea88d3ff71b32fb9da5246",
"b8c1d2e3f4a5_native_extension_distribution_cutover.py": "c0527edff210a9646c69de2da01c2877bce4acbc05af969c5bfb8d1a3fd0d3c8",
Expand Down
52 changes: 52 additions & 0 deletions migrations/versions/a0465e3b028f_add_peer_application_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""add peer application version

Revision ID: a0465e3b028f
Revises: d41cc84db0c5
Create Date: 2026-09-22 20:24:20.666591

"""

from collections.abc import Sequence

from alembic import op
import sqlalchemy as sa

from app.database_contract import INTERNAL_SCHEMA, PROTOCOL_SCHEMA


revision: str = "a0465e3b028f"
down_revision: str | Sequence[str] | None = "d41cc84db0c5"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None

_CONTRACT_REVISION = "peer-runtime-identity-v1"
_PREVIOUS_CONTRACT_REVISION = "peer-extension-setup-v1"


def _set_contract_revision(value: str) -> None:
op.execute(
sa.text(
f"""
UPDATE "{INTERNAL_SCHEMA}".contract_state
SET contract_revision = :revision,
updated_at = statement_timestamp()
WHERE singleton
"""
).bindparams(revision=value)
)


def upgrade() -> None:
op.add_column(
"peers",
sa.Column("application_version", sa.Text(), nullable=True),
schema=PROTOCOL_SCHEMA,
)
_set_contract_revision(_CONTRACT_REVISION)
op.execute("NOTIFY pgrst, 'reload schema'")


def downgrade() -> None:
_set_contract_revision(_PREVIOUS_CONTRACT_REVISION)
op.drop_column("peers", "application_version", schema=PROTOCOL_SCHEMA)
op.execute("NOTIFY pgrst, 'reload schema'")
8 changes: 8 additions & 0 deletions tests/peer/integration/test_peer_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from app.business.peer import PeerHTTPInbound, PeerManager
from tests.database import TestSession
from app.schemas.peer import PEER_HTTP_PROTOCOL, PeerModel
from app.version import APPLICATION_VERSION


pytestmark = pytest.mark.skipif(
Expand Down Expand Up @@ -41,6 +42,9 @@ def test_real_snapshot_database_lease_and_candidate_filtering(monkeypatch, async
with TestSession() as db:
local_row = db.get(PeerModel, local)
assert local_row is not None
assert local_row.application_version == APPLICATION_VERSION
local_row.name = "Human name"
local_row.application_version = "0.0.0"
local_row.config = {"http_public_base_url": "https://local.example/root/"}
db.add(local_row)
now = (
Expand Down Expand Up @@ -84,6 +88,10 @@ def test_real_snapshot_database_lease_and_candidate_filtering(monkeypatch, async
)
db.commit()

registered_again = async_runner.run(PeerManager.register_self())
assert registered_again.name == "Human name"
assert registered_again.application_version == APPLICATION_VERSION

published = async_runner.run(PeerManager.publish_self())
assert published.capabilities == [
{
Expand Down
Loading