Summary
Add an explicit, opt-in resume-compatibility identity to RunAttachment, separate from the concrete service endpoint, SecretEnv, and ReuseKey.
A RunServiceProvider often allocates a fresh loopback port and bearer credential after a host restart while exposing the same logical capability. In v1.1.2, generic attachments are fingerprinted using the concrete RuntimeServiceRef and normalized MCP endpoint. A restarted provider therefore makes Thread(..., ResumeOnly()) fail with ErrThreadIncompatible before the Driver runs, even when the service is semantically unchanged and the new request could safely rebind the endpoint.
The SDK already solves this for its private hosted-Tool provider, but through a type-specific normalization path. Ecosystem RunServiceProvider implementations cannot express the same contract.
Current behavior and evidence
At v1.1.2 / main commit 919f140f64f89c80933802840c8878681a85a4d9:
acquireRun flattens every attachment's services into RuntimePayload.Ensured;
threadRuntimeCompatibility includes the concrete service URL, command, CWD, port, metadata, ReuseKey, effective MCP fingerprint, and secret environment variable names;
- secret values are correctly excluded;
normalizeHostedToolServiceCompatibility replaces the ephemeral URL and bearer-env name only when the provider is the SDK's private *hostedToolProvider with its known catalog fingerprint;
- the generic regression test deliberately expects an endpoint change to return
ErrThreadIncompatible (TestThreadResolvedRuntimeAttachmentParticipatesInCompatibility), while hosted Tools have a restart/resume exception.
Minimal generic failure mode:
first := attachment("http://127.0.0.1:4101", "TOKEN_A", "secret-a")
agent1 := adaptor.New(driver1,
adaptor.WithThreadStore(store),
adaptor.WithRunServices(first),
)
_, _ = agent1.Thread("t").Run(ctx, "first")
_ = agent1.Close(ctx)
second := attachment("http://127.0.0.1:4202", "TOKEN_B", "secret-b")
agent2 := adaptor.New(driver2,
adaptor.WithThreadStore(store),
adaptor.WithRunServices(second),
)
_, err := agent2.Thread("t", adaptor.ResumeOnly()).Run(ctx, "second")
// err is ErrThreadIncompatible before driver2 runs.
Using a fixed port and fixed environment-variable name is a host workaround, not a complete SDK contract. It introduces port ownership/collision concerns and still cannot describe a safe transport rebind independently of service reuse.
Why ReuseKey must not be overloaded
ReuseKey answers a different question: whether a concrete service instance/process can be reused or correlated by a runtime manager. Resume compatibility asks whether an existing provider conversation may continue while the current invocation receives a newly allocated endpoint and credential.
One string cannot safely answer both questions. In particular, ReuseKey does not fully describe:
- the MCP server key and transport;
- required/optional semantics;
- authentication mode and credential scope;
- the secret environment-variable binding shape;
- the exposed tool/capability catalog;
- whether an existing provider process must be replaced to observe the new endpoint.
Treating ReuseKey as a resume override would invite stale-process credential reuse or hide a real capability change.
Proposed root API
Add one provider-facing DTO and one optional field:
package adaptor
// RunServiceCompatibility is a provider assertion that concrete Services from
// one RunAttachment may be rebound without starting a new provider session.
// It is used only for Thread/session resume compatibility. It is not a service
// cache key, process-reuse key, integrity digest, or authorization decision.
type RunServiceCompatibility struct {
// ID is a stable, globally namespaced identity for the logical attachment
// role, for example "com.example.agent-ui". It must not contain secrets.
ID string
// Fingerprint is an opaque, stable, secret-free digest of every semantic
// property that can affect safe session continuation. The provider changes
// it whenever capability, transport semantics, auth scope, or protocol
// behavior changes. Core treats it as opaque and does not parse it.
Fingerprint string
}
type RunAttachment struct {
Services []ServiceRef
Events RunEventSource
// ResumeCompatibility opts this attachment into semantic resume
// compatibility. Nil preserves today's exact concrete fingerprinting.
// Core copies the value during AttachRun; later mutation has no effect.
ResumeCompatibility *RunServiceCompatibility
}
Example provider:
func (p *Provider) AttachRun(ctx context.Context, runID string) (adaptor.RunAttachment, error) {
endpoint, token := p.allocate(runID)
return adaptor.RunAttachment{
Services: []adaptor.ServiceRef{{
ID: "agent-ui",
Name: "agent-ui",
URL: endpoint,
Lifecycle: driver.RuntimeLifecycleEphemeral,
ReuseKey: "run/" + runID, // concrete service lifecycle only
MCP: mcp.StreamableHTTP("agent-ui", endpoint, "AGENT_UI_TOKEN"),
SecretEnv: []driver.EnvBinding{{Name: "AGENT_UI_TOKEN", Value: token}},
}},
ResumeCompatibility: &adaptor.RunServiceCompatibility{
ID: "com.example.agent-ui",
Fingerprint: p.semanticFingerprint(),
},
}, nil
}
semanticFingerprint() should cover, at minimum, the logical service roles, MCP keys, transport semantics, required flags, tool/capability schemas and revisions, authorization class/scope, protocol revision, and any behavior whose change would make an existing provider conversation unsafe. It must exclude the allocated address, port, run ID, bearer value, process ID, timestamps, and other allocation-only data.
Required semantics
Default remains fail-closed
ResumeCompatibility == nil preserves v1.1.2 behavior exactly: concrete endpoint or MCP changes remain incompatible.
- Both
ID and Fingerprint are required when the pointer is non-nil. Empty, whitespace-only, oversized, control-character, or duplicate IDs in one effective run fail before Driver launch.
- The SDK must not infer semantic compatibility from
ReuseKey, URL shape, loopback address, provider Go type, or metadata.
What the override covers
For the services contributed by that one attachment, the compatibility tuple replaces allocation/materialization details only in Thread/session compatibility projections:
- concrete URL, command, CWD and port;
- allocation-only metadata and
ReuseKey;
- concrete MCP URL/command and bearer-token environment-variable name;
- concrete
SecretEnv names and all values.
The tuple represents the full logical attachment, including its service set and MCP declarations. If any of those semantics change, the provider must change Fingerprint.
Core should domain-separate and hash the tuple; it must never persist or log endpoint credentials. Attachment ordering should remain deterministic under the existing option merge rules. Two effective attachments with the same ID should be rejected rather than silently collapsed.
Concrete delivery and process reuse remain exact
The override must not alter:
driver.Request.Runtime or driver.Request.MCP;
- concrete profile materialization (
ProfilePayload.Fingerprint);
- runtime reports;
- service-manager acquisition/release behavior;
- private persistent-process equality/signatures.
Every invocation receives the current URL and current SecretEnv. If a persistent provider process cannot ingest that new concrete configuration, it must be replaced even though the Thread/provider conversation resumes. Private process signatures may compare actual effective environment values in memory, but no secret value may enter a durable/public fingerprint, error, event, report, or log.
This separation is the point of the API:
Resume compatibility: stable semantic ID + semantic fingerprint
Concrete materialization: actual URL/MCP/env for this invocation
Service reuse: ReuseKey and lifecycle
Provider-process reuse: exact private effective configuration
MCP/profile compatibility
Core must apply the semantic identity consistently to both layers that currently reject a resume:
- the runtime-service portion of
threadInvocationFingerprint; and
ProfilePayload.SessionCompatibilityFingerprint, using a semantic MCP projection for attachment-owned MCP servers.
ProfilePayload.Fingerprint, RuntimePayload.Fingerprint, and the current concrete MCP fingerprint remain exact. A Driver therefore materializes the new endpoint, while its session guard sees the stable semantic profile identity.
The existing hosted-Tool special case should migrate to this generic contract so there is one implementation path rather than a privileged type assertion that ecosystem providers cannot use.
Implementation outline
Keep attachment boundaries until both concrete and session projections are built:
type resolvedRunAttachment struct {
services []driver.RuntimeServiceRef
compatibility *RunServiceCompatibility
}
Build two views:
- Concrete view: today's normalized
RuntimePayload, MCP payload, profile payload, and process signature.
- Session view: exact data for ordinary services/attachments;
{ID, Fingerprint} in place of concrete services and MCP materialization for opted-in attachments.
The session MCP projection must preserve every non-owned host/runtime MCP server exactly. It must not normalize a server merely because its name or URL resembles another provider's server.
No Driver SPI change is needed: Drivers continue to receive the concrete request. The existing requirement that resumed invocations apply the current driver.Request remains essential.
Threat model and trust boundary
RunServiceProvider is host-trusted executable code: it can already inject endpoints, commands, environment bindings, MCP declarations, and events. The SDK cannot prove that a claimed compatibility fingerprint is truthful, just as it cannot prove a Driver's SessionConfigFingerprint is truthful.
The API must nevertheless fail closed and document these hazards:
- Lying/stale fingerprint: may resume a conversation against changed tools or policy. Mitigation: opt-in only, explicit complete semantic contract, conformance tests, change fingerprint on semantic drift.
- Credential rotation vs authorization drift: rotating a token with the same scope is compatible; changing tenant, principal, allowed operations, or approval policy is not. Mitigation: include a non-secret authorization-class/scope digest in
Fingerprint.
- Endpoint substitution/SSRF: compatibility never authorizes or validates an endpoint. Existing endpoint validation and host policy still apply on every invocation.
- Stale persistent process: a resumed process may retain the old URL or token. Mitigation: concrete private process signatures remain exact and force recycle; conformance proves the new request is applied.
- Secret leakage: a provider may mistakenly place a secret in
ID or Fingerprint. Mitigation: docs require secret-free inputs, diagnostics never echo values, and tests use canary secrets across errors, stores, reports and logs. Core should hash rather than reproduce these strings in compatibility errors.
- Identity collision: unrelated providers could claim the same ID. Mitigation: globally namespaced IDs, duplicate rejection, and domain-separated hashing with attachment boundaries.
This API is a Type-1 contract: once ecosystem providers persist these identities, changing their meaning can silently corrupt resume behavior. The documentation must state that an ID's semantic meaning is immutable and a behavior change requires a new fingerprint (or a new ID for a different logical role).
Backward compatibility
This is source-compatible for existing providers:
- the field is additive;
- nil retains strict existing behavior;
- no existing
ServiceRef, ReuseKey, SecretEnv, Driver SPI, or execution verb changes;
- providers can opt in incrementally;
- old persisted Threads remain compatible only when their prior fingerprint can be compared safely. Do not guess an upgrade from an old concrete fingerprint to a new semantic fingerprint.
For the last point, the first opt-in release should fail closed on a pre-feature checkpoint unless an explicit, tested migration record stores both identities. Starting a new Thread once is preferable to silently accepting an unprovable legacy equivalence.
This proposal complements #20: immutable Agent topology does not remove genuinely run-scoped endpoints, and #20 explicitly keeps WithRunServices. It generalizes the private hosted-Tool behavior delivered by #14 rather than changing the public Tools API.
Required tests
Root and Thread contract
- unchanged semantic ID/fingerprint + changed loopback port resumes with
ResumeOnly across Agent close/reconstruction and a shared persistent ThreadStore;
- the resumed Driver receives the new URL, new MCP declaration, new secret env name, and rotated secret value;
- concrete runtime/MCP/profile fingerprints change while session compatibility fingerprints remain stable;
- changing
Fingerprint returns ErrThreadIncompatible before Driver invocation;
- nil compatibility preserves the current endpoint-change incompatibility;
- one semantic attachment plus one exact attachment: changing the exact attachment still rejects resume;
- different provider order, duplicate IDs, partial/invalid tuples, and concurrent runs fail or normalize deterministically as specified;
- semantic service-set, MCP key, transport, required flag, tool schema/revision, auth class/scope, and protocol changes are represented by a changed fingerprint;
- secret canaries never appear in ThreadStore records, compatibility errors, events, results, reports, or default logs.
Process and Driver conformance
- endpoint/token/env-name rotation resumes the same provider session but does not reuse a stale persistent process;
- the replacement process observes only the current endpoint and credential;
- cancellation and attachment cleanup remain correct if compatibility validation fails;
- Codex, Claude, Cursor, and CodeBuddy fixtures prove current MCP/profile configuration is applied on a resumed session;
- the existing hosted-Tool restart test passes through the generic compatibility mechanism with no type-specific normalization;
- race tests cover concurrent attachment, close/restart, process replacement, and Thread lease ownership.
Robustness matrix
| Case |
Expected result |
| Same capability, new port/token |
Resume; new concrete request applied |
| Same token shape, different tenant/scope |
Fingerprint changes; incompatible |
| Same endpoint, changed tool schema |
Fingerprint changes; incompatible |
| Provider omits compatibility |
Exact concrete behavior |
| Duplicate compatibility ID |
Typed pre-launch validation error |
| Persistent process keeps old env |
Process recycled; never dispatch with stale auth |
| Provider puts secret in a field |
No value echoed or persisted; conformance fails |
| Non-owned MCP server changes |
Incompatible; no accidental normalization |
Acceptance criteria
- A generic
RunServiceProvider can rotate endpoint allocation and credentials across host restart while safely resuming a compatible Thread.
- Resume identity, concrete invocation materialization,
ReuseKey, and persistent-process reuse remain four separate contracts.
- Nil is fail-closed and existing behavior is unchanged.
- Real semantic changes reject before Driver launch.
- Current endpoint/credential reaches every resumed invocation without durable secret leakage.
- Hosted Tools use the generic path; no provider-type special case remains.
- Thread, MCP/profile, process-reuse, cleanup, redaction, built-in Driver, and race tests pass.
- API reference, Thread/fingerprint docs, run-service docs, security docs, changelog, and API golden are updated together.
Non-goals
- Treating arbitrary remote endpoints as interchangeable.
- Making
ReuseKey a session-compatibility override.
- Hiding tool, protocol, authorization, or policy changes from Thread compatibility.
- Persisting or replaying bearer credentials.
- Adding a second execution path or changing Driver execution methods.
- Guaranteeing correctness for an untruthful host provider.
Summary
Add an explicit, opt-in resume-compatibility identity to
RunAttachment, separate from the concrete service endpoint,SecretEnv, andReuseKey.A
RunServiceProvideroften allocates a fresh loopback port and bearer credential after a host restart while exposing the same logical capability. In v1.1.2, generic attachments are fingerprinted using the concreteRuntimeServiceRefand normalized MCP endpoint. A restarted provider therefore makesThread(..., ResumeOnly())fail withErrThreadIncompatiblebefore the Driver runs, even when the service is semantically unchanged and the new request could safely rebind the endpoint.The SDK already solves this for its private hosted-Tool provider, but through a type-specific normalization path. Ecosystem
RunServiceProviderimplementations cannot express the same contract.Current behavior and evidence
At
v1.1.2/maincommit919f140f64f89c80933802840c8878681a85a4d9:acquireRunflattens every attachment's services intoRuntimePayload.Ensured;threadRuntimeCompatibilityincludes the concrete service URL, command, CWD, port, metadata,ReuseKey, effective MCP fingerprint, and secret environment variable names;normalizeHostedToolServiceCompatibilityreplaces the ephemeral URL and bearer-env name only when the provider is the SDK's private*hostedToolProviderwith its known catalog fingerprint;ErrThreadIncompatible(TestThreadResolvedRuntimeAttachmentParticipatesInCompatibility), while hosted Tools have a restart/resume exception.Minimal generic failure mode:
Using a fixed port and fixed environment-variable name is a host workaround, not a complete SDK contract. It introduces port ownership/collision concerns and still cannot describe a safe transport rebind independently of service reuse.
Why
ReuseKeymust not be overloadedReuseKeyanswers a different question: whether a concrete service instance/process can be reused or correlated by a runtime manager. Resume compatibility asks whether an existing provider conversation may continue while the current invocation receives a newly allocated endpoint and credential.One string cannot safely answer both questions. In particular,
ReuseKeydoes not fully describe:Treating
ReuseKeyas a resume override would invite stale-process credential reuse or hide a real capability change.Proposed root API
Add one provider-facing DTO and one optional field:
Example provider:
semanticFingerprint()should cover, at minimum, the logical service roles, MCP keys, transport semantics, required flags, tool/capability schemas and revisions, authorization class/scope, protocol revision, and any behavior whose change would make an existing provider conversation unsafe. It must exclude the allocated address, port, run ID, bearer value, process ID, timestamps, and other allocation-only data.Required semantics
Default remains fail-closed
ResumeCompatibility == nilpreserves v1.1.2 behavior exactly: concrete endpoint or MCP changes remain incompatible.IDandFingerprintare required when the pointer is non-nil. Empty, whitespace-only, oversized, control-character, or duplicate IDs in one effective run fail before Driver launch.ReuseKey, URL shape, loopback address, provider Go type, or metadata.What the override covers
For the services contributed by that one attachment, the compatibility tuple replaces allocation/materialization details only in Thread/session compatibility projections:
ReuseKey;SecretEnvnames and all values.The tuple represents the full logical attachment, including its service set and MCP declarations. If any of those semantics change, the provider must change
Fingerprint.Core should domain-separate and hash the tuple; it must never persist or log endpoint credentials. Attachment ordering should remain deterministic under the existing option merge rules. Two effective attachments with the same
IDshould be rejected rather than silently collapsed.Concrete delivery and process reuse remain exact
The override must not alter:
driver.Request.Runtimeordriver.Request.MCP;ProfilePayload.Fingerprint);Every invocation receives the current URL and current
SecretEnv. If a persistent provider process cannot ingest that new concrete configuration, it must be replaced even though the Thread/provider conversation resumes. Private process signatures may compare actual effective environment values in memory, but no secret value may enter a durable/public fingerprint, error, event, report, or log.This separation is the point of the API:
MCP/profile compatibility
Core must apply the semantic identity consistently to both layers that currently reject a resume:
threadInvocationFingerprint; andProfilePayload.SessionCompatibilityFingerprint, using a semantic MCP projection for attachment-owned MCP servers.ProfilePayload.Fingerprint,RuntimePayload.Fingerprint, and the current concrete MCP fingerprint remain exact. A Driver therefore materializes the new endpoint, while its session guard sees the stable semantic profile identity.The existing hosted-Tool special case should migrate to this generic contract so there is one implementation path rather than a privileged type assertion that ecosystem providers cannot use.
Implementation outline
Keep attachment boundaries until both concrete and session projections are built:
Build two views:
RuntimePayload, MCP payload, profile payload, and process signature.{ID, Fingerprint}in place of concrete services and MCP materialization for opted-in attachments.The session MCP projection must preserve every non-owned host/runtime MCP server exactly. It must not normalize a server merely because its name or URL resembles another provider's server.
No Driver SPI change is needed: Drivers continue to receive the concrete request. The existing requirement that resumed invocations apply the current
driver.Requestremains essential.Threat model and trust boundary
RunServiceProvideris host-trusted executable code: it can already inject endpoints, commands, environment bindings, MCP declarations, and events. The SDK cannot prove that a claimed compatibility fingerprint is truthful, just as it cannot prove a Driver'sSessionConfigFingerprintis truthful.The API must nevertheless fail closed and document these hazards:
Fingerprint.IDorFingerprint. Mitigation: docs require secret-free inputs, diagnostics never echo values, and tests use canary secrets across errors, stores, reports and logs. Core should hash rather than reproduce these strings in compatibility errors.This API is a Type-1 contract: once ecosystem providers persist these identities, changing their meaning can silently corrupt resume behavior. The documentation must state that an ID's semantic meaning is immutable and a behavior change requires a new fingerprint (or a new ID for a different logical role).
Backward compatibility
This is source-compatible for existing providers:
ServiceRef,ReuseKey,SecretEnv, Driver SPI, or execution verb changes;For the last point, the first opt-in release should fail closed on a pre-feature checkpoint unless an explicit, tested migration record stores both identities. Starting a new Thread once is preferable to silently accepting an unprovable legacy equivalence.
This proposal complements #20: immutable Agent topology does not remove genuinely run-scoped endpoints, and #20 explicitly keeps
WithRunServices. It generalizes the private hosted-Tool behavior delivered by #14 rather than changing the public Tools API.Required tests
Root and Thread contract
ResumeOnlyacross Agent close/reconstruction and a shared persistent ThreadStore;FingerprintreturnsErrThreadIncompatiblebefore Driver invocation;Process and Driver conformance
Robustness matrix
Acceptance criteria
RunServiceProvidercan rotate endpoint allocation and credentials across host restart while safely resuming a compatible Thread.ReuseKey, and persistent-process reuse remain four separate contracts.Non-goals
ReuseKeya session-compatibility override.