Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

821 lines
48 KiB
Markdown

# Connector Architecture
The connector market is a shared desktop-daemon domain owned by the matched
modules under `packages/connector`. Tutti is the source repository and first
host; other hosts consume exact released Go and npm package versions from the
same package cohort.
## Authority Boundaries
Connector market uses two independent APIs:
- the remote Connector Market API publishes connector releases, manifests,
and immutable artifact metadata
- the local daemon API exposes the accepted catalog, installation,
authorization, compatibility, and durable operation state
owned by one desktop host
The remote market service owns its versioned API schema and generated
protobuf/HTTP artifacts. Tutti pins those artifacts by provider commit and
SHA-256 under `packages/clients/market-go`; it does not import the
`tsh-server` application module or redefine the remote schema in the local
daemon OpenAPI. Updates require a source checkout at the exact pinned commit and
digest-match every copied file. The client exposes the reusable
`/v1/market/categories`, `/v1/market/items`, and
`/v1/market/items/{item_type}/{item_key}` read boundary for both connectors and
Skills. Connector catalog requests always use `itemType=connector`; Skill
consumers use `itemType=skill`. The shared connector package may provide a
default `CatalogSource` adapter over that generated client, but must not copy or
redefine the remote schema. Remote transport DTOs and local daemon DTOs remain
separate.
The generated client adapter applies host authorization only to the initial
Market request. It preserves the host redirect policy and rejects any redirect
that leaves the configured scheme and host, including HTTPS downgrades, before
another credential-bearing request can be sent.
Published connectors use the remote market manifest v2 envelope: one
market-neutral `payload.implementation` and no `supportedMarkets` field. The
daemon rejects the legacy connector v1 envelope instead of adapting
`payload.implementations[market]`. At the boundary it projects the accepted v2
publication into the local daemon's stable connector-manifest v1 DTO; these
schema versions belong to different APIs and do not imply compatibility.
The renderer never calls the remote market. The local daemon is authoritative
for every state rendered by the desktop application.
An authoritative catalog refresh treats a missing Connector as delisted. A
not-installed Connector with no active lifecycle operation is deleted
immediately. Installed Connectors and Connectors with active operations retain
their private durable installation and cleanup evidence, but the daemon marks
them `removed_from_catalog` and omits the Connector and its operations from
public snapshots, single-Connector reads, and catalog-page projections. Public
validation runs only after that filter, so an obsolete manifest retained solely
for cleanup cannot make the visible catalog unavailable.
Category identifiers are opaque routing values. The Connector adapter sends
the exact server `categoryId` back as `sectionId` and never rewrites legacy or
new IDs. The daemon projects both `displayNameZh` and `displayNameEn` through
the local OpenAPI; the renderer selects the Chinese name for `zh*` locales and
the English name otherwise without another network request. During the bounded
compatibility window, only `featured`, `productivity`, `development`, and
`other` may use their released local i18n labels when an older daemon omits
both names. Unknown dynamic categories without a server name fail closed
instead of displaying their slug as product copy. Installation state stays on
the connector card; the renderer does not split the catalog into installed and
available categories, and catalog pages are loaded without the not-installed
filter so installed connectors remain in their server-owned sections.
Installation is a device fact. Authorization is an account projection. A
Connector may therefore be installed while inactive for the current account;
authorization completion or expiry schedules a normal durable runtime
reconcile without changing installed truth. Provider authorization completion
returns after that runtime Desired is durable; it does not wait for the runtime
Observed receipt. The background convergence worker owns retries, so a runtime
revision race cannot turn an already-committed authorization into a failed
authorization response. Remote MCP HTTP 428 and JSON-RPC
`-33001`/`-33002` during an enabled reconcile are authorization-required
observations, not retryable install failures. Core persists an expired account
projection, replans `RuntimeDesired` as disabled, and lets
`awaitRuntimeDesired` converge that inactive generation so the install
operation can complete. Every durable lifecycle command
freezes its `accountId` in `OperationScope`, while short-lived artifact and
credential grants are passed only through execution ports and are never
serialized into SQLite.
Connector Manifest v3 keeps one signed release and adds an execution-target
matrix keyed by canonical Go platform tuples such as `darwin-arm64` and
`linux-arm64`. The catalog adapter selects the daemon's exact target and
projects it into the existing single-implementation host contract before
validation or installation. Target selection never falls back across OS or
architecture boundaries because runtime ABIs and native executable checksums
are target-specific release facts. Manifest v1 and market-neutral v2 remain
read-compatible at this boundary.
## Ownership
The shared Connector modules own:
- connector, catalog, installation, authorization, compatibility, durable
operation, revision, and error contracts
- `connector/host`: Go state transitions, manifest validation, host ports,
application orchestration, and recovery rules
- `connector/daemon`: bootstrap fencing, scheduling, workers, and outbox
delivery
- `connector/store-sqlite`: the canonical repository, transactions, leases,
migrations, and durable outbox implementation
- `connector/runtime`: latest-only artifact download caching, no-network archive
import, secure artifact preparation, managed runtime identity, ABI
verification, and typed Node package installation
- a default remote-catalog domain adapter built over the authoritative market
client
- reusable artifact mechanics: bounded download, `current + candidate` cache
replacement, size and digest verification, no-network import, safe
extraction, release-to-package verification, atomic promotion, and cleanup
- `connector/market`: the reusable local daemon OpenAPI fragment under
`openapi/connector-market.v1.yaml`
- the renderer `ConnectorMarketBackend` contract, module Root/Runtime,
lifecycle and StartupJobs, Valtio-backed domain services, reusable renderer,
and connector-market i18n bundle
Each host daemon owns:
- database path selection and opening/closing the shared SQLite store
- remote market base URL, authentication, HTTP transport, proxy, TLS, logging,
and tracing configuration
- the state root supplied to the shared runtime
- process transport, artifact and executable enforcement, OS integration, product command
publication, invocation admission, and credential binding injected into the
shared runtime boundary
- secure credential storage and authorization callbacks
- a durable outbox and integration with the host event stream
- local transport DTO mapping, product compatibility inputs, and diagnostics
The modules keep ports for host-specific implementations even when they offer
a default adapter. A host can replace a default without forking shared
application or renderer semantics.
## Artifact And Runtime Boundary
Artifact preparation and runtime activation are different responsibilities.
They may also live on different machines:
```text
durable install operation
-> ReleaseInstallationManager
-> control-plane DownloadCache prepares one verified candidate
-> same-machine import, or host data-plane sync to the runtime machine
-> runtime Importer revalidates size/SHA and safely extracts
-> optional typed CLI package installation
-> release installation receipt (local paths or opaque runtime reference)
-> repository commits device-installed truth
-> cache candidate becomes current
authorization observation / runtime reconcile
-> RuntimeBindingResolver derives active or inactive account intent
-> connector/runtime implementation adapter
-> generation-fenced MCP/CLI routes and observed process state
```
For a CLI declared with a typed `node_package` installation, the prepared
artifact contains connector metadata and skills, not the CLI npm package. CLI
installation remains part of the physical release install receipt, while route
reconciliation is a separate operation:
A CLI may instead declare `artifact_native` when its platform executable was
acquired and verified by the Connector publication pipeline and is already
inside the signed install artifact. The host does not receive an upstream URL
or add another installer. It verifies the prepared artifact inventory, copies
the declared entrypoint into the read-only execution snapshot with executable
permission only for that entry, then verifies its exact size and SHA-256 at
every launch. Windows executes the declared `.exe` directly; `.cmd` remains a
user-facing PATH projection rather than the native launch boundary.
One portable install artifact may contain binaries for multiple exact targets.
The signed v3 manifest selects one target implementation without OS or
architecture fallback; unused target files remain inert data in that release.
Per-target artifacts are a distribution-size optimization and do not change
the runtime trust or launch contract.
The local daemon connector-manifest v1 contract includes required icons, typed
package installation, explicit Node ranges, and mapping-free generic CLI. It is
an internal host projection of the remote market v2 publication rather than a
copy of the server envelope.
```text
typed node_package intent
-> resolve connector-node-static
-> verify the managed Node executable, ABI, and version range
-> run fixed pnpm through that Node (never a user shell/npm)
-> shared content-addressed store + release-scoped link tree
-> verify package name, exact version, sha512 integrity, lock, and bin entry
-> atomically publish the installation receipt
```
A device has one active connector Node runtime. Connector manifests may state
an explicit Node version range, but cannot download Node or select a second
runtime. An incompatible range rejects installation; it never falls back to
the user's system Node, nvm, npm global prefix, or `PATH`.
Node package storage separates physical reuse from execution isolation:
```text
<state-dir>/connectors/node-packages/
shared/
pnpm-store/ # shared content-addressed package data
corepack/ # shared fixed-package-manager cache
npm-cache/ # shared registry/download cache
pnpm-home/
packages/<connector-key>/<release-digest>/
package.json
pnpm-lock.yaml
node_modules/ # hardlinks and release-local links into shared content
.tutti-cli-installation.json
```
The package manager is daemon-selected and version-pinned. Manifests declare a
typed package name, exact version, integrity, and optional allowlisted Node
lifecycle entrypoints; they do not provide arbitrary shell commands. Package
manager lifecycle scripts are disabled. An allowed lifecycle entrypoint is
launched directly by the same verified Node inside the connector installer
verified process transport. A `node_script` launch continues through that Node; a `native` launch
must declare the expected platform-binary SHA-256, and activation executes only
the file matching that digest. All connections and workspaces reuse one
installed connector release. A release-scoped rollback removes only its
incomplete release. An explicit Connector uninstall is connector-scoped: it
fences every matching runtime route across connection IDs, cancels matching
credential-broker sessions, closes processes and execution snapshots, removes
stable CLI shims, and removes every prepared artifact and private Node package
tree for that Connector. It preserves account authorization, user/workspace
state, and the shared Node package store and package-manager caches.
Catalog display metadata includes a required public HTTPS icon URL. Publishing
stores the source PNG, WebP, or SVG as an immutable versioned object and places
its CDN URL in the Market manifest. The daemon rejects missing URLs, inline
`data:` images, non-HTTPS schemes, credentials, surrounding whitespace, and
URLs longer than 2048 bytes before the release reaches a renderer or runtime.
Manifest permissions use a lowercase stable permission name with an optional
scope (`permission`, `permission:scope`, or `permission:*`). The daemon keeps
fail-closed validation for the permission name, scope grammar, duplicates, and
all other manifest fields; a scoped permission is not treated as a plain
identifier. The host may currently collapse a scoped permission to a broader
runtime capability, so accepting the syntax does not imply scope-level runtime
enforcement.
CLI manifests do not require action mappings. The host publishes one stable
`tutti-connector-<key>` executable per active CLI connector into the daemon
state `bin/` directory. The executable launches only the verified installed
entrypoint and forwards argv without invoking a shell. Installed Skills supply
the connector-specific arguments and workflow, while the Agent uses the normal
shell execution path. The selected connector remains a separate routing and
policy boundary.
Physical installation is inspected only through verified artifact and CLI
receipts. Connector-owned commands never decide whether a release is present.
The installation manager reports `present`, `absent`, `invalid`, or
`indeterminate`; only the first three may change an installation projection.
An installed CLI may separately declare a bounded `readinessProbe`. It runs
through the already resolved CLI entrypoint after installation and contributes
only interface readiness to the runtime receipt.
Connector releases may declare optional `agentRouting.aliases` containing only
stable product or brand names. Connector id and display name are included by
the host automatically; authors use aliases for additional language and legacy
brand forms, not generic capability words. After activation, the implementation
host projects this bounded, validated routing data into new Agent runtimes. An
alias match makes `connector available` the first discovery step. Its connector
summaries include recursively discovered Skill names, titles and descriptions,
plus the active native interfaces. Skills are discovered from every `SKILL.md` below
the verified release's `skills/` directory; manifests do not duplicate a
central Skill list. Tutti Agent receives that content-addressed directory as a
native extra Skill root before thread start/resume, so relative references,
scripts, and assets resolve from the connector package and the Skill survives
connector process restarts. There is no command-layer Skill read fallback;
providers consume Skills through their normal native Skill mechanism. Market
listings that are not installed and routes that are not active are never added
to Agent instructions.
The initial Lark profile is representative: it pins `@larksuite/cli@1.0.83`
and its npm sha512 integrity, requires the shared Node 22 profile, runs only the
package's declared `scripts/install.js` lifecycle with `curl` and `tar`
explicitly admitted, and then launches the resulting verified `bin/lark-cli`
native binary. The connector artifact contains metadata, icon, and Skills, not
the npm package or a second Node runtime.
Archive handling must reject absolute paths, parent traversal, and symlink or
hardlink escapes. It must enforce limits for file count, individual file size,
expanded bytes, and compression ratio. Artifact code is not executed before
verification and preparation complete.
The staging and active directories must be on the same filesystem when atomic
rename is used. Activation failure preserves the previous active version. The
implementation host removes orphan staging and ready execution snapshots at
startup before restoring routes after an unclean shutdown. The daemon resolves
the artifact key against its configured artifact base URL. The
production base URL is the public-assets CloudFront prefix
`https://d27a59zdy4534h.cloudfront.net/tutti/connector-market/`; CloudFront
serves immutable versioned objects from the private `tsh-public-assets` S3
origin. The daemon never addresses S3 directly. The Tutti desktop host uses an
ordinary direct GET without workspace identity. A split control-plane/runtime
host may instead issue a short-lived presigned artifact grant under the frozen
account scope and let its runtime machine download directly. The grant is not
persisted or logged. Both modes verify the immutable object version, release
digest, artifact digest, and size before installation. Operations persist the
artifact key, release identity, object version, digest, and size. Staging and
local integration may override the CDN prefix
with `TUTTI_CONNECTOR_ARTIFACT_BASE_URL`; production should leave the public
CloudFront default in place.
`connector/host` owns the implementation-host port and durable reconcile
semantics; `connector/runtime` owns portable artifact and managed-runtime
installation primitives, while each daemon supplies the concrete
implementation host, process, and product-command adapters. In Tutti, `managed_stdio`
connectors resolve an exact Node/Python runtime profile. MCP servers are
long-lived daemon children governed by the route generation fence and process
registry. CLI routes instead atomically publish stable shims that directly exec
the verified release entrypoint through the Agent's normal shell; the physical
installer owns receipt-based installation inspection, while the process adapter
handles CLI readiness and credential-broker operations. Both interfaces use the same verified artifact snapshot,
executable identity, generation lifecycle, and connection-scoped state path.
An installed runtime is daemon-global and is available to every Agent and the
local Tutti CLI. TSH runs the same runtime module inside its managed VM and
supplies a guest process adapter.
## Durable Operations And Recovery
Remote refresh, install, update, uninstall, disconnect, and runtime reconcile
use at-least-once execution. Exactly-once execution is not assumed across
SQLite, the filesystem, runtime activation, and process restarts. Start
authorization is not in that set: the user secret exists only in the
`BeginAuthorization` command, so an accepted or running `start_authorization`
row must not be executed from persisted state.
An installation request carries an immutable operation identity and release
identity plus an explicit account execution scope. Each stage is idempotent for at least:
```text
operationId + connectorKey + version + releaseDigest
```
The durable install flow is intentionally coarse-grained. A failed attempt is
restarted from the idempotent release installer; the business repository does
not persist internal download/sync/import sub-stages:
```text
install/update:
accepted -> installing(receipt) -> runtime_pending(candidate + Desired)
-> Observed(exact generation) -> current promoted -> completed
uninstall:
accepted -> deactivating(Desired=disabled) -> Observed(disabled)
-> removing -> absent -> completed
```
An authorization-required observation still completes install after the
disabled generation is Observed. Install completion does not require an
enabled Agent route.
These are short database transactions separated by idempotent external
effects, not one long transaction. Every external effect is preceded by a
durable phase/receipt. A retryable error leaves the Operation non-terminal and
the continuous recovery scanner resumes it; a deterministic install failure
clears Candidate and its convergence row before terminalizing. During update,
Current and its route remain usable until Candidate has been observed ready.
The repository owns operation leases and attempt metadata. Recovery observes
staging markers, active-version markers, and host runtime state before deciding
which stage to resume. Install and uninstall return verifiable results to the
application; artifact helpers do not write the business repository or publish
events directly.
Every operation row also stores canonical `owner_account_id` and visibility.
User commands are `account` visible and public reads always use
`GetOperationForScope`; an ownership mismatch is indistinguishable from a
missing row. Runtime reconcile and operations whose legacy owner cannot be
proven are `system_private`: workers may still recover them, but snapshots and
operation endpoints never publish them. Legacy rows derive ownership from
`operation_json.scope.accountId`. The idempotency key is
`(owner_account_id, client_request_id)`, while the active physical lifecycle
constraint remains device-global per Connector.
`accepted` and `running` rows have no age-based expiry. The SQLite repository
protects them with one-active-operation constraints plus renewable,
token-fenced leases. Daemon startup reschedules every recoverable row whose
effect is durable. Start authorization is command-inline and is not scheduled
on accept; bootstrap fails leftover accepted or running `start_authorization`
rows so they cannot block a later user retry, and the continuous recovery
scanner must not execute them. The cleanup path is therefore intentionally
unable to select an active operation.
Terminal operation results are retained for 24 hours after `updatedAt`, with
the cutoff inclusive. This is the supported `GET operation` result window and
the `clientRequestId` idempotency window: repeating the same command identity
inside the window returns the original operation; after the row expires the
identity is treated as a new command and normal revision validation applies.
The renderer polls non-terminal operations with a 250 ms to 2 second backoff. A
permanent missing result, or a reconnect after the result window, converges
from the authoritative connector/snapshot projection instead of relying on
operation history.
Installed release evidence is durable recovery input, not operation history.
The SQLite store records immutable Current and prepared Candidate releases by
`(connector_key, release_digest)` in
`connector_market_release_installations`; the legacy one-row table remains a
compatibility projection. Uninstall completion removes all versions in the
same transaction as the business transition. Receipt-detected drift retains
Current evidence while the installation projection is failed so repair and
uninstall still target the accepted release.
Runtime recovery therefore remains valid after the corresponding completed
install operation has expired, including when the accepted catalog has advanced
to a newer release.
Runtime publication is durable convergence state, not a public Operation. The
runtime-pending transaction atomically commits Candidate evidence, a non-secret
`RuntimeDesired`, the running install Operation, and the public outbox event.
Only an exact current-boot `RuntimeObserved` allows the final transaction to
promote Candidate to Current and complete the Operation.
`RuntimeDesired.generation` is a Connector-and-account-scope clock
independent from catalog and event revisions. A continuous daemon scanner claims
`Desired != Observed` work with a renewable token-fenced lease, resolves any
one-shot credential grant only immediately before the host call, and commits
`RuntimeObserved` with a compare-and-swap on the exact desired generation. An
Observed receipt from another daemon boot is stale even when its generation
matches. Scheduling is only a latency hint; a lost wake-up cannot lose work.
Before bootstrap republishes installed routes, it asks the physical installation
manager to inspect the accepted artifact and optional CLI receipts. `absent` or
`invalid` changes the installation projection to `failed` while retaining the
installed release evidence needed for safe repair or uninstall. A later
`present` observation for that release restores `installed`; `indeterminate`
preserves the projection. Runtime reconcile then performs interface readiness
checks before any route is published.
Authorization creation is serialized per account and Connector. A repeated
`clientRequestId` resumes the same external session. Every provider presentation
has a monotonic, non-secret authorization-step revision. A client can return the
last revision as a cursor; the Host then waits for a later broker event for a
bounded interval and may replay the current step. This keeps multi-step flows
ordered without persisting authorization URLs or device codes. Accepted or running
`start_authorization` is not recovered by replaying the operation: that would
complete a native-secret control-plane session with an empty secret and fail
the live command. Completed authorization receipts recover through
`ReconcileAuthorizations`, not through the durable operation scanner. A
different request retains the compatibility conflict unless it explicitly
sends `replacementPolicy=replace_active`. Replace
is Host-owned: it interrupts an in-progress initial Begin, moves an existing
receipt through durable `canceling`, asks the provider to terminate the exact
attempt, waits until that attempt can no longer publish credentials or events,
then resolves it as `superseded` before accepting the new operation. A provider
without confirmed attempt cancellation rejects replacement rather than running
two sessions against shared credential state. Managed runtimes are inspected
during convergence; restart finishes `canceling` receipts and no longer
fabricates a permanently pending observation. Disconnect is completed only
after the disconnected projection and exact disabled Runtime Observed state are
durable.
Managed credential brokers read process output through the context-aware
transport when available. Cancellation closes the owned process connection as
the termination fallback and waits for the broker consumer to finish before the
session and route are released; an explicit cancel does not publish a transient
authorization failure while shutdown is in progress.
For account-scoped runtimes, `AccountRuntimeBindingResolver` maps `none`
authorization to an always-active device connection. OAuth/API-key connectors
remain inactive until the current account projection is `connected`; only then
does the resolver request a one-shot credential-broker grant. `expired`,
`disconnected`, and missing projections reconcile inactive. If an enabled
reconcile observes authorization-required from the remote MCP before the
projection has expired, Core writes `expired` and replans inactive instead of
leaving the install or convergence row retrying 428. Daemon or guest
restart uses `BootstrapForScope` to rebuild the same projection explicitly.
An enabled Runtime Reconcile returns one identity-fenced receipt containing
the bounded, non-secret `ConnectorSummary` for the exact route generation it
committed. The receipt is available while capability publication is disabled,
so a cross-machine host can project readiness without consulting an
Agent-facing registry. A later key-only Route lookup is forbidden because an
upgrade or concurrent connection reconcile could otherwise attach metadata
from a different release or generation.
Remote Connector authorization uses that account projection for Start,
observation, presentation, and route publication; the device Connector's
authorization field is not remote authorization truth. A completed Start
operation may retain a private authorization-session receipt while provider
work is pending. The Start response preserves the current session's `pending`
state when the durable account projection is still missing, disconnected,
expired, or failed; this ephemeral response state lets the caller continue the
same idempotent session and does not replace the projection as durable truth.
An already connected projection wins a race with a stale pending session. Each
receipt has a terminal resolution, and only unresolved
receipts for the daemon's current account are polled. Applying an authoritative
connected Snapshot atomically writes its monotonic Projection and surfaces all
matching account-and-Connector receipts. The daemon holds the account lifecycle
fence and is the single runtime scheduler for receipt recovery. Host projection
does not enqueue a second public operation. The daemon updates or joins the
scope's durable Runtime Desired, awaits the exact generation in Observed, and
only then resolves the receipts as
`account_state_converged`. A same-revision Snapshot still surfaces a receipt created
after an earlier Snapshot does not cause permanent polling. WebSocket hints and
the five-minute calibration both fetch Snapshot; runtime reconcile is
level-triggered and can safely repeat after restart or an interrupted pass.
New external Connector mutations use `expectedConnectorRevision`, so unrelated
Connectors accepted from one Snapshot can proceed independently. The required
global `expectedRevision` remains in the wire contract for old clients and is
used when the Connector fence is absent. Catalog refresh retains its global
revision fence. Internal level-triggered repair reads current durable state
inside its transaction.
Active-operation exclusion follows the same ownership boundary through exact
scheduling lanes. The empty lane is reserved for catalog refresh; each
Connector key is its own device lifecycle lane. A catalog refresh and a
Connector operation may therefore coexist, while a second refresh or a second
operation for the same Connector is rejected atomically by the durable store.
An in-flight reconcile may have resolved its binding before a newer Projection
was persisted. Its Observed compare-and-swap is rejected after Desired advances;
the scanner then applies the newer generation before receipt resolution.
Authorization execution is selected from the exact release frozen into the
durable operation. `managed_stdio` delegates to the local implementation host;
`remote_streamable_http` delegates to the account control plane through
`packages/connector/daemon/adapters/controlplane`. The latter receives account
authentication only through a host-supplied request authorizer. Neither an API
key submitted by the user nor the product account session is copied into the
runtime VM. Remote MCP execution follows the product host's authenticated
relay, while the VM receives only the non-credential runtime route identity.
Route activation may reuse a same-process `tools/list` for an unchanged
authorization identity; Agent-visible lists stay live and are never persisted
or TTL-cached.
Remote authorization replacement propagates the explicit replacement policy to
Start and uses the session-scoped control-plane Cancel endpoint when a receipt
already exists. This covers both a returned session and an interrupted initial
Begin without relying on a device-local process handle.
## Event Consistency
Business state and its invalidation event are written to a durable outbox in
the same SQLite transaction. The host publisher delivers outbox entries through
its existing event stream and records delivery progress. A full Snapshot carries
the maximum outbox `eventCursor` read in the same SQLite snapshot. Each delivered
event carries its durable outbox cursor; duplicates are ignored and a gap causes
one authoritative reload. Snapshot revision, event cursor, and per-Connector
revision are separate watermarks, so a late partial fetch cannot suppress an
unrelated Connector update or overwrite a newer entity.
Account authorization overlays are read in the same SQLite Snapshot as market
state. A public projection change atomically advances the Connector revision and
appends its invalidation event; account state can no longer change invisibly
between market Snapshot reads. Composer capability projection and prompt
admission use the current account-scoped Snapshot as well, so their connected
state cannot diverge from Connector Market management surfaces.
Operation-bearing events carry an internal account audience and are delivered
only while that owner is the host's active account. Every state change also
produces a machine-level Connector invalidation without an operation id, so a
different account still observes device installation truth without learning
another account's command identity. Legacy or private events are fail-closed by
stripping operation and owner identifiers before publication.
Concurrent catalog requests use a process-local monotonically increasing fetch
fence: an older page or refresh response that returns after a newer response is
dropped before its write transaction. This is a daemon compatibility mechanism
and requires no remote catalog protocol change.
Pending outbox entries never expire. Published entries are delivery receipts,
not the replay or diagnostic authority, and are retained for one hour before
becoming eligible for deletion. Publication failures and cleanup summaries are
written as structured daemon logs; the business database is not an unbounded
diagnostic archive.
Events carry a monotonic revision or sequence and remain invalidation hints.
The current host does not use published outbox rows as a replay store, so the
renderer refreshes on daemon reconnect, window resume, and command completion.
A future durable replay transport may resume from a known sequence and must
signal a retention gap so the renderer can reload a full daemon snapshot. The
daemon snapshot, not the event, is always authoritative, and revision fencing
remains required.
Lifecycle cleanup runs once when the daemon host starts and then hourly. Each
SQLite transaction deletes at most 500 eligible terminal operations and 500
eligible published events, ordered by their indexed terminal/publication time
and stable identity. A run repeats bounded transactions until neither category
fills its batch, so an existing expired backlog drains without enlarging a
transaction. The delete predicates are repeated outside the bounded subqueries
so a row must still be terminal or published when the write occurs.
The active-operation partial unique index, terminal-time cleanup index,
pending-outbox index, and published-outbox cleanup index keep admission,
delivery, and maintenance scans separate. Multiple daemon/store connections
may race cleanup safely through SQLite write serialization and idempotent
predicates.
## Renderer Boundary
Each renderer host owns the generated local daemon client and the adapter that
maps wire DTOs into `@tutti-os/connector-renderer/application` domain types.
The public Renderer package never constructs a daemon client and never reads
preload or window globals. Connector-specific React belongs only to
`@tutti-os/connector-renderer/ui`; AgentGUI supplies neutral projection models
and retains Agent draft/prompt semantics.
The renderer domain follows the same Root + Runtime + StartupJob boundary as
TSH Room Chat:
- `ConnectorMarketModule.activate()` creates a child DI container and is called
by the workspace module startup flow before the settings UI can render
- `ConnectorMarketRuntime` owns the lifecycle sequence `created -> starting ->
synchronizing -> materializing -> ready`; failure is terminal and disposes
the child container
- Market, UiState, and View each have one StartupJob; the Market job places an
initial-load barrier in `synchronizing`, and View materialization starts only
after that barrier resolves
- `ConnectorMarketRoot` exposes the three stable services to renderer context;
React never resolves or constructs individual services
- every service exposes a `readonly dataStore = proxy(...)` as its only writable
state source, and only its owning service mutates it
- asynchronous responses are fenced by request sequence, service generation,
and daemon revision; `dispose()` is idempotent and terminal
- installation intent is projected into the reactive Market store before the
host request starts, so cards and dialogs become busy immediately even when a
host keeps the mutation request open until runtime work completes; the local
projection is cleared on both success and failure and never replaces daemon
installation truth
- the first user authorization action stays in flight until it completes or
the user Cancels; a second Authorize click joins that Promise and must not
create another `clientRequestId` or send `replace_active`. After Cancel, the
next Authorize is a new action: it creates a new `clientRequestId` and sends
`replacementPolicy=replace_active`. Continuation polling within one action
reuses the same identity. A canceled renderer Promise cannot retain the
Connector mutation token
- command admission and presentation share one renderer-local per-Connector
mutation phase (`installing`, `updating`, `uninstalling`, `authorizing`,
`disconnecting`, or `updating_runtime`) in the reactive Market store. Cards
remain busy and an open authorization dialog remains on its waiting view
until that phase is released, even when a newer daemon projection has already
reached `connected`. A disconnect submitted through stale UI or another
caller while authorization is settling waits for that authorization action,
rereads the projected state, and runs at most once
- event refreshes are coalesced, daemon reconnect performs a full reload, and
accepted commands are followed through the operation endpoint or events
- the settings catalog toolbar Refresh control indicates an explicit
`refreshCatalog()` command only. The daemon also runs a scheduled catalog
refresh after bootstrap and about once a minute; that background sync may set
`catalogState=refreshing` while keeping the last-known-good catalog visible,
and must not spin or disable the toolbar control
- catalog cards show a status field only after installation. Idle uninstalled
cards keep the Install action and omit Not installed, so Authorization
required remains the distinctive installed-but-disconnected mark. First-time
install and update still show their in-progress status because that is live
work, not an idle catalog tier
- hosts gate connector-market transport through `canRequest`; Tutti binds it to
account authentication, activates the module without network access while
signed out, reloads after login, and keeps reconnect/resume paths silent
after logout
- install intent that arrives while Tutti is signed out invokes the host-owned
account login flow through `requestInstallAdmission`; the service rechecks
admission and returns `not_admitted` without calling the backend or showing
installation success when login is still pending
- Tutti enables renderer-owned automatic updates for installed, compatible
Connectors. After an authoritative snapshot, catalog page, or connector event
publishes a different active release digest, Market starts the existing
install/update command in the background. It never opens login from a
background update and attempts one release digest only once per renderer
lifetime, leaving explicit update available after failure
- the shared renderer subscribes at leaf components through a stable context,
uses `@tutti-os/ui-system`, and owns no transport, startup, disposal, or
business-state reconciliation
Compact composer surfaces reuse `ConnectorComposerMenu` from the shared UI
entrypoint. The menu consumes only a host-neutral projection of connector key,
name, icon, setup state, and the scoped runtime state projected by Connector
Market. An installed Connector is shown as started only when the current boot
has observed its latest desired generation as enabled and ready; `starting`,
`stopped`, and `failed` remain off. Older hosts that omit this optional runtime
projection retain the legacy authorization-based presentation. AgentGUI maps
its provider-neutral capability options into that projection and retains only
placement plus its Tutti Mode fallback. Selecting one item emits a semantic
connector-open intent. Composer “install” waits on the durable install
operation; authorization-required remote MCP must complete that operation
with an inactive route so the trigger can move from install to authorize
instead of spinning. The host
executes `openConnectorMarketDialog(root, connectorKey)`, which waits for the
authoritative market view, rejects invalid or unknown keys, and then advances
the package-owned dialog state machine. Before applying the bounded quick-list
limit, the shared menu ranks already installed and authorized connectors
(`connected` and installed-but-stopped `disabled`) ahead of connectors that
still require authorization or setup. The secondary key is the installation
event timestamp (`installedAtUnixMs`, newest first). Connectors without a
timestamp keep their relative host catalog order after timestamped entries in
the same group. Its compact trigger previews the authorized and running
(`connected`) group without requiring those connectors to be selected in the
current draft; draft selection continues to control only structured prompt
content. Selecting “more” remains host navigation because settings/workbench
location is product-owned.
Connector access-policy editors reuse `ConnectorAccessSelectionPanel` from the
same shared UI entrypoint. The controlled panel accepts only loading/error/ready
state, neutral Connector items, selected keys, caller-localized labels, and
semantic callbacks. It owns the Connector-specific loading, empty, error,
selection, disabled, and busy presentation. The host retains authorization and
sharing policy, selected-key normalization, persistence, navigation, and
catalog projection; the panel imports no AgentGUI draft/store, Desktop global,
or daemon client.
Every renderer window mounts exactly one `ConnectorMarketDialogHost` alongside
its other window-level panel hosts. Composer entries and catalog cards never
mount their own dialog host. This keeps dialog identity and mutual exclusion in
one shared Root while allowing several AgentGUI surfaces in the same window to
open it.
Connector details are represented by one modal state machine, never by a fixed
right-hand pane. An uninstalled connector opens an installation confirmation.
An unconnected installed connector opens the authorization dialog. The token
form keeps typed secrets after submit so a failed or in-flight attempt does
not force the user to re-enter them. Completing authorization in that dialog
closes the modal and reports success without exposing the settling management
projection. Reopening an already authorized connector opens the management
dialog directly, where disconnect and try remain available. Blocked releases
open the blocked-state dialog. Only one dialog host is mounted at a time, so
the catalog keeps the full settings content width and never leaves an empty
right column.
Closing an authorization dialog while a request is in flight does not start a
new session and does not dismiss the modal; the user must finish the provider
flow or use Cancel. Browser OAuth keeps the in-flight footer on authorizing
and does not surface Continue for a synthesized `external_link` view; the
Start command already opened that URL. Cancel calls the Host cancellation
command, then the next Authorize is a new replace-active attempt. A leftover
pending session without an in-flight renderer request also shows Authorize,
not a second Start disguised as Continue.
## Local OpenAPI Reuse
Host aggregate documents compose the same local daemon fragment instead of
copying paths or schemas. Tutti may reference the repository path. External
hosts resolve the fragment exported by an exact installed
`@tutti-os/connector-contracts/openapi/connector-market.v1.yaml` resource. The
OpenAPI generator rejects malformed references and merge conflicts.
The aggregate document continues to own server URLs, security, and
product-only routes. The fragment owns common connector-market paths, DTOs,
enums, revisions, operations, and error codes.
## State Boundaries
Installation, authorization, and compatibility are independent state machines.
A connector can therefore be installed but disconnected, or visible but blocked
by an unsupported implementation, without overloading one ambiguous status field.
Credentials and sensitive implementation configuration are never part of the
renderer projection. Unknown or incomplete implementation kinds remain visible
but cannot be installed.
## Current Checkpoint
The shared package now contains immutable operation targets and execution
receipts, recoverable install/uninstall/authorization flows, latest-only
artifact download caching, a no-network archive importer, host ports, the local daemon OpenAPI
fragment, and the complete reusable renderer module: Root, Runtime, lifecycle,
per-service StartupJobs, UiState, render-ready View, i18n, catalog, and modal
state branches.
Tutti now composes that fragment, persists catalog/operations/leases and a
transactional outbox in SQLite, reads the typed remote catalog, exposes
generated local handlers and clients, publishes invalidation events, and
registers the shared renderer module through injected daemon-client and event
adapters, activates it as part of workspace startup, and renders its shared
panel from Settings > Agent > Connectors. Event-stream reconnect causes an
authoritative snapshot reload.
The registered Tutti Host reads the ordinary TSH market item API, downloads an
artifact directly from the configured artifact base URL, verifies its declared
SHA-256 and size, retains only the current archive plus one replaceable
candidate, prepares an installed snapshot, selects the local
Node/Python runtime, installs typed Node CLI packages into a private shared-store
layout when requested, and exposes one daemon-owned MCP/CLI runtime per installed
connector connection. Installation commits before runtime publication;
authorization state and bootstrap drive separate generation-fenced reconcile.
Crash recovery adopts every host-touching operation into the current boot
epoch. Catalog refresh failure does not invalidate installed release evidence.
The only public connector discovery command is `connector available`. It
returns active connector metadata, discoverable Skill summaries, and the
native interface needed to use each connector; it does not expose package
paths, ports, bearer tokens, or upstream credentials.
All active connector-owned MCP implementations are registered into the local
loopback Streamable HTTP MCP server named `connector`. Agent-facing bindings
are issued by `packages/connector/runtime/agentgateway`, whose listener and
bearer authority can outlive replacement of the bundle-owned backend.
`packages/connector/runtime/mcpserver` remains the replaceable MCP protocol
backend; product daemons own both lifecycles.
Before creating a Connector binding, the Agent runtime resolves the exact
provider adapter. Standard ACP adapters enable Connector only when the
`initialize` response explicitly declares
`agentCapabilities.mcpCapabilities.http == true`; a missing or false field,
capability-probe failure, or Connector binding/preparation failure falls back
to the ordinary Connector-free session. Unknown future adapters are also
Connector-free until they explicitly implement this capability contract.
The fallback omits the session binding, MCP configuration, routing hints,
Connector policy, Skill roots, and Connector CLI path together.
Tool names are
namespaced as `<connector-key>_<upstream-tool-name>`. Each Agent session receives
a short-lived bearer binding through its provider-native MCP configuration;
custom user MCP servers remain in their existing configuration and are not
merged into this registry. Install, reconnect, disconnect, upgrade, and
uninstall reconcile the registry dynamically and emit MCP
`notifications/tools/list_changed` to subscribed clients.
CLI connectors are used through their stable `tutti-connector-<key>` executable
and the Agent's normal shell execution path. Connector Skills are injected as
native Skill roots. The command broker does not expose `connector capabilities`,
`connector skills`, `connector skill read`, or `connector invoke`, and the first
phase intentionally does not expose connector enable/disable commands.
The first production compatibility boundary is deliberately narrow:
`managed_stdio` connectors are installable when their runtime contract is
supported. Authorized connectors must declare a connector-owned
credential broker and exact HTTPS authorization hosts. Durable
event replay is still follow-up hardening; renderer reconnect, resume, command
completion, and revision-fenced invalidations therefore trigger authoritative
snapshot reloads.