Files
tutti/docs/architecture/agent-runtime-preparation.md
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

13 KiB

Agent Runtime Preparation

Agent session setup is split into two reusable modules:

  • packages/agent/daemon owns session control and provider protocols.
  • packages/agent/runtimeprep owns canonical system prompts, skills, capability resolution, provider-local files, launch environment overlays, manifests, and cleanup.

Both local and VM-backed hosts execute runtime preparation on the machine where the provider runs. A VM-backed host may use RPC to reach that machine, but the RPC service is only a transport/path/security adapter and must call the same runtimeprep.DefaultPreparer; it must not maintain separate Claude or Codex preparers.

For any Agent Session launched with RTK saver mode enabled, provider-neutral runtime preparation keeps the selected model unchanged and resolves the pinned Tutti-owned rtk executable. Packaged Desktop supplies it from app resources; other hosts resolve the SHA-256-verified rtk-saver managed-runtime component.

RTK saver mode is independent from the existing Codex saver mode. The latter remains Codex-only and installs the session-scoped Luna worker role and Codex routing policy; enabling either mode does not implicitly enable or disable the other. Preparation copies that executable and the canonical RTK.md into the exact Session runtime and prepends only the private binary directory to that Session's PATH. Session-private Codex, Tutti Agent, and OpenCode AGENTS.md files start with an absolute @<runtime>/rtk/RTK.md reference, matching RTK's native Codex integration. The common Tutti Runtime policy also carries the same RTK instructions inline through every provider's native instruction channel as a compatibility fallback (for example, Claude's system-prompt file and Cursor's plugin context). Claude and Cursor install native pre-tool hooks, OpenCode installs a command-rewrite plugin, and Hermes installs a session-home plugin. Kimi receives a session-home plugin system prompt because its pre-tool hooks can block but cannot mutate tool input. RTK's database, tee output, and telemetry policy are also isolated under the Session runtime.

Runtime preparation deliberately does not inspect the user PATH or install RTK through Homebrew, Cargo, an upstream shell script, or any other global toolchain. A missing or invalid bundled/managed artifact fails closed. Disabled Sessions receive no RTK files or environment overlay, and cleanup removes only the enabled Session's recorded runtime paths. The Tutti integrated terminal also prepends the Tutti-owned RTK directory to its child environment, but Tutti never mutates the operating-system or user-global PATH.

Deployment differences are expressed with DeploymentProfile and CapabilityPack. A pack resolves policy, skills, and environment together. Dynamic host skills use SkillSource; per-session skills use ExtraSkills. The canonical template and shared skill bodies remain in runtimeprep so hosts do not fork the actual prompt content. PrepareInput.SharedInvocation and EnabledConnectors render the session-sticky enable-set protocol in connector-discovery: a non-empty set is the current user-enabled connectors, and an empty set is discovery mode over the listed connectors. Runtimeprep renders each available Connector key, display name, and alias into one routing index. A request matching any generated entry, or asking to operate a service represented by that index, must begin with connector available --json before the provider asks clarifying questions, reads a Connector-owned Skill, or calls a Connector interface. The current Turn's discovery result is authoritative; the policy does not hard-code service-specific mappings. Shared invocations add Caller-versus-Owner routing rules. Hosts pass the full enable set on each turn as connector prompt blocks; packages/agent/daemon injects only enable/disable deltas into the provider-visible turn.

Tutti Agent keeps auth, configuration, transcripts, and other mutable state in its session-scoped TUTTI_AGENT_HOME, while Tutti-managed Skills use a daemon-owned content-addressed store. Runtimeprep computes the stable bundle, then carries its absolute root as internal launch metadata through the existing environment overlay. CodexAppServerAdapter consumes and strips that metadata, then applies the full root list with skills/extraRoots/set after app-server initialization and before starting or resuming a thread. This is an adapter-only contract: materialized bundle roots do not enter the Host public lifecycle types or user-visible runtime context. A failed RPC fails startup so the thread cannot silently run without its managed Skills.

Tutti Agent installs its provider-owned embedded Skills beneath the run-scoped home during app-server initialization. Before the same extra-roots refresh, the Adapter content-addresses that installed tree under agent/system-skill-bundles/v1/<digest>/.system and atomically replaces the run-scoped .system directory with a symlink to the immutable tree. The RPC then clears the provider Skill cache before thread/start or thread/resume, so both embedded and Tutti-managed Skill paths are canonical and stable in the rendered developer prefix. A provider content or marker change produces a new digest automatically. Invalid bundles, unsafe files, replacement failures, or RPC failures fail closed before a thread starts.

Cursor preparation materializes one session-scoped plugin outside the workspace and supplies it to cursor-agent acp through --plugin-dir. The plugin carries the resolved Skill bundle. Runtimeprep also renders the canonical Tutti policy and a catalog of the bundle's actual materialized paths from the same resolved capability profile; the Cursor adapter must not maintain a separate hard-coded Skill catalog. Re-preparing the same Session replaces current managed Skills and removes only stale Tutti-managed entries, so resume cannot accumulate suffixed duplicates or retain a capability that is no longer resolved. Because Cursor ACP does not project plugin Skills or Rules into the model context, the standard ACP adapter appends that prepared context to the first provider prompt only. It is provider-only content and is never projected as a user message; a newly connected or resumed provider Session receives it again. This makes Tutti capabilities available at session start without writing provider instructions or Skills into the workspace. Current Cursor Agent runtimes discover a plugin-scoped hooks/hooks.json. When RTK saver mode is enabled, runtimeprep adds a preToolUse Shell hook that runs rtk hook cursor; it remains inside the Session plugin and never writes user or project Cursor configuration. A focused background-Task guard remains dormant and independent of this Shell rewrite hook.

OpenCode preparation follows the same session-isolation rule as Codex without changing OpenCode's standard ACP transport. It creates a session-scoped OPENCODE_CONFIG_DIR, writes the canonical Tutti runtime policy to that directory's AGENTS.md, installs an RTK tool.execute.before rewrite plugin, and materializes the resolved Tutti Skill bundle under its native skills/ root. Re-preparing the Session reconciles that managed root to the current resolved bundle while preserving unmanaged entries. This happens for every Session, including Sessions without a model access plan, so mention-driven handoff, context, issue, and workspace-app behavior never depends on user-installed Skills. A bound model access plan continues to use the OPENCODE_CONFIG file in the same isolated directory; provider settings and permissions continue through their existing OpenCode config layers.

Agent Extensions may declare a constrained runtimePrep overlay in the signed composer profile when a provider needs a per-session home or config merge. The service validates that declaration before launch and passes it through PrepareInput.ExtensionRuntimePrep; runtimeprep must not branch on extension provider IDs. The overlay can write the standard instructions file, create a session-scoped home, copy declared opaque files from a user home source, expose the home through one validated environment variable, materialize Tutti-managed skills into session-scoped roots derived from the extension's declared workspace skill roots, and merge those session roots into a supported YAML string-list key such as skills.external_dirs. A signed profile may additionally list safe relative sharedDirs beneath that same source home. Runtimeprep projects those mutable provider-owned directories into each isolated session home instead of copying them, so verified helper binaries and equivalent caches survive session cleanup. Configuration, authentication, provider state, and undeclared paths remain session-local; Tutti core never selects a shared directory by provider ID.

Source-home resolution is also descriptor-driven. A declared source environment variable has highest priority. On Windows, a sourceDefaultRel whose top-level name starts with a dot first resolves under the native user cache root with the leading dot removed (for example .vendor becomes %LOCALAPPDATA%\vendor). If that native directory does not exist, runtimeprep falls back to the literal user-home-relative directory so migrated Unix-style locations keep working. Other platforms keep the literal user-home-relative default. The shared resolver remains provider-neutral and only copies files explicitly declared by the signed profile.

For Hermes, the extension profile declares the HERMES_HOME overlay instead of Tutti core knowing acp:hermes. The resulting session keeps per-session state, copies only the declared auth/env/config files needed for provider login, and references both Tutti's session-scoped extension skill roots and the user's native skill directory through the declared config merge. It declares bin as a shared mutable directory because Hermes installs its checksum-verified Tirith helper there on supported platforms. When the stable directory is still empty, preparation searches manifest-owned, same-provider legacy session homes from newest to oldest and seeds it from the first usable cache; subsequent sessions reuse that helper instead of placing a GitHub Release download on every first terminal command. Windows keeps the same declarative projection contract even though the current Hermes runtime does not ship Tirith for Windows. Merge precedence is explicit: the user's original config content is parsed as YAML, existing user list entries remain first, Tutti session roots are appended next, and the user's native skill root is appended last. Exact duplicate paths are ignored after their first occurrence. Invalid YAML or an incompatible target key shape stops runtime preparation with a clear error instead of silently omitting skills. The merge uses the repository YAML parser (yaml.v3) and only the constrained string-list merge declared by the signed profile; runtimeprep must not maintain a Hermes-specific line parser or silently return an unmodified config after a failed merge. The Tutti-managed skill files are mirrored into the session runtime root before being referenced from skills.external_dirs, so two Hermes sessions from the same workspace do not share rendered SKILL.md files that contain session or target context. There is no provider-ID migration branch to remove; if Hermes later exposes a native ACP/runtime option for additive skill roots and home isolation, remove the signed runtimePrep declaration from the Hermes package and let the generic instruction/skill preparer handle it like other ACP extensions.

Product-owned responsibilities remain outside the module:

  • process or VM transport;
  • physical/logical workspace path mapping;
  • environment trust filtering;
  • account login and token exchange;
  • deployment capability availability and profile selection.

Dedicated Command Projections

An internal dedicated Session may persist a CommandCapabilityProjection in its immutable runtime snapshot. Runtime preparation uses that projection to render one consistent command guide across initial launch and resume. For Codex, a projected Session also receives exact command-path approval rules instead of the normal whole-CLI prefix rule. The daemon independently resolves the persisted projection for session-aware capability discovery and command invocation, and fails closed when that projection cannot be resolved. When AllowedIDs is non-empty it is an exact set across public and promoted integration commands; an unavailable required command fails runtime preparation rather than silently broadening or degrading the dedicated Session.

This mechanism narrows normal agent behavior but is not an operating-system security boundary. Tutti's local daemon credential is available to trusted same-user CLI clients through the global listener-info file. A hostile same-user process that escapes the supplied Session context can therefore make an unscoped request. Features that require protection from a malicious provider process must add process/filesystem credential isolation (or remove unscoped credentials) rather than treating runtime environment variables, generated command guides, provider approval rules, or daemon projection checks as sufficient isolation.

See the runtimeprep package README for the public integration contract.