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
13 changes: 11 additions & 2 deletions packages/core/src/__tests__/runtime-event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ test('shares one decoder across all TurnOrigin variants', () => {
});

describe('continuation-start protocol', () => {
test('accepts only the replay projection version defined by v2', () => {
test('reads legacy and current replay projections but rejects unknown versions', () => {
const continuationStart = {
protocol: 'continuation_start_v2',
provenance: 'runtime_admission',
Expand Down Expand Up @@ -162,12 +162,21 @@ describe('continuation-start protocol', () => {
).actions?.continuationStart,
continuationStart,
);
assert.equal(
decodeRuntimeEvent({
...baseEvent({ role: 'system', author: 'system', content: undefined }),
actions: {
continuationStart: { ...continuationStart, providerProjectionVersion: 2 },
},
}).actions?.continuationStart?.providerProjectionVersion,
2,
);
assert.throws(
() =>
decodeRuntimeEvent({
...baseEvent({ role: 'system', author: 'system', content: undefined }),
actions: {
continuationStart: { ...continuationStart, providerProjectionVersion: 2 },
continuationStart: { ...continuationStart, providerProjectionVersion: 3 },
},
}),
/RuntimeEvent schema/,
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/agent-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ export interface AgentRunHeader {
backendKind: PersistedBackendKind;
/** Immutable Connection entity identity. Optional only on legacy run headers. */
llmConnectionId?: string;
/**
* Opaque identity of the provider endpoint and credential ownership frozen
* before this run's first provider dispatch. Optional only on legacy or
* non-provider run headers.
*/
providerStateIdentity?: `sha256:${string}`;
llmConnectionSlug: string;
modelId: string;
cwd: string;
Expand Down Expand Up @@ -568,6 +574,7 @@ const AGENT_RUN_HEADER_SHAPE = defineObjectShape<AgentRunHeader>()(
[
'invocationId',
'llmConnectionId',
'providerStateIdentity',
'completedAt',
'parentRunId',
'resumedFromRunId',
Expand Down Expand Up @@ -646,6 +653,9 @@ export function decodeAgentRunHeader(value: unknown): AgentRunHeader {
isPersistedBackendKind(value.backendKind) &&
(value.llmConnectionId === undefined ||
(typeof value.llmConnectionId === 'string' && value.llmConnectionId.length > 0)) &&
(value.providerStateIdentity === undefined ||
(typeof value.providerStateIdentity === 'string' &&
/^sha256:[0-9a-f]{64}$/.test(value.providerStateIdentity))) &&
typeof value.llmConnectionSlug === 'string' &&
typeof value.modelId === 'string' &&
typeof value.cwd === 'string' &&
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/backend-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import type { InteractionClosureReason } from './interaction.js';
import type { RuntimeEvent } from './runtime-event.js';
import type { SandboxBoundaryResponse, SandboxBoundarySettlement } from './sandbox-boundary.js';
import type { StoredMessage, PersistedBackendKind } from './session.js';
import type { AgentRunHeader } from './agent-run.js';
import type { UserQuestionResponse } from './user-question.js';
import type { ContextBudgetDiagnostic } from './usage-stats/types.js';
import type { EffectiveOrchestration } from './orchestration.js';
Expand Down Expand Up @@ -88,6 +89,12 @@ export interface BackendSendInput {
* compatibility projection.
*/
runtimeContext?: RuntimeEvent[];
/**
* Existing durable run headers for `runtimeContext`, used only to verify
* provider-owned replay against the current model route. RuntimeEvents stay
* the transcript authority; route provenance remains owned by AgentRun.
*/
runtimeContextRunHeaders?: readonly AgentRunHeader[];
/** Continue from an already committed RuntimeEvent boundary without adding another user turn. */
continuation?: RuntimeContinuationMetadata;
/**
Expand Down Expand Up @@ -167,6 +174,8 @@ export interface BackendCompactHistoryInput {
*/
runId: string;
runtimeContext: readonly RuntimeEvent[];
/** Source-run route authority for provider-owned history projected into the compaction call. */
runtimeContextRunHeaders?: readonly AgentRunHeader[];
}

export interface BackendCompactHistoryResult {
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/runtime-boundary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export interface ContinuationClaimV1 {
claimId: string;
boundaryDigest: RuntimeBoundaryDigest;
boundary: RuntimeBoundaryCursorV1;
providerProjectionVersion: 1;
providerProjectionVersion: 1 | 2;
providerReplayDigest: RuntimeBoundaryDigest;
target: {
sessionId: string;
Expand Down Expand Up @@ -242,7 +242,7 @@ export function decodeContinuationClaim(value: unknown): ContinuationClaimV1 {
!isNonEmptyString(value.target.invocationId) ||
!isNonEmptyString(value.target.runId) ||
!isNonEmptyString(value.target.turnId) ||
value.providerProjectionVersion !== 1 ||
(value.providerProjectionVersion !== 1 && value.providerProjectionVersion !== 2) ||
!Number.isSafeInteger(value.claimedAt) ||
(value.claimedAt as number) < 0
) {
Expand Down Expand Up @@ -302,7 +302,7 @@ export function decodeContinuationClaim(value: unknown): ContinuationClaimV1 {
claimId: value.claimId,
boundaryDigest,
boundary,
providerProjectionVersion: 1,
providerProjectionVersion: value.providerProjectionVersion,
providerReplayDigest,
target: {
sessionId: value.target.sessionId,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/runtime-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ export interface RuntimeEventContinuationStartV2 {
prefixDigest: `sha256:${string}`;
};
replayManifestDigest: `sha256:${string}`;
providerProjectionVersion: 1;
providerProjectionVersion: 1 | 2;
providerReplayDigest: `sha256:${string}`;
}

Expand Down Expand Up @@ -1030,7 +1030,7 @@ function isRuntimeContinuationStart(value: unknown): value is RuntimeEventContin
(value.immediateSource.highWater as number) > 0 &&
isSha256Digest(value.immediateSource.prefixDigest) &&
isSha256Digest(value.replayManifestDigest) &&
value.providerProjectionVersion === 1 &&
(value.providerProjectionVersion === 1 || value.providerProjectionVersion === 2) &&
isSha256Digest(value.providerReplayDigest)
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { HostConnectionEffectCoordinator } from '../server/connection-effect-coo
import { HostOAuthExecutionAuthority } from '../server/oauth-execution-authority.js';
import type { ConnectionContext } from '../server/operation-dispatcher.js';
import { RuntimePolicyActivationGate } from '../server/runtime-policy-activation-gate.js';
import { resolveExecutionTarget } from '../server/execution-model-authority.js';
import type { ConnectionOnboardingSaveResult, OperationOutcome } from '../protocol/index.js';

const context: ConnectionContext = {
Expand Down Expand Up @@ -490,6 +491,71 @@ test('a save whose connection changed between discovery and commit is superseded
});
});

test('provider state identity follows endpoint, credential, and request-header ownership', async () => {
await withFixture(async ({ stores }) => {
const connection = await createConnection(stores, 0, {
...connectionDraft('identity-relay', 'openai-compatible'),
baseUrl: 'https://relay-a.example.test/v1',
});
await setConnectionCredential(stores, connection, 'key-a');
const header = {
llmConnectionId: connection.connectionId,
llmConnectionSlug: connection.slug,
model: 'gpt-5',
};
const resolveIdentity = async () =>
(
await resolveExecutionTarget(
header,
stores,
new HostOAuthExecutionAuthority(stores),
() => {
throw new Error('API-key provider must not create an OAuth refresh transport');
},
)
).providerStateIdentity;
const initial = await resolveIdentity();

const moved = await stores.connectionCatalog.update({
expected: { connectionId: connection.connectionId, revision: connection.revision },
changes: {
name: connection.name,
baseUrl: 'https://relay-b.example.test/v1',
enabled: true,
enabledModelIds: connection.enabledModelIds,
},
});
assert.equal(moved.kind, 'committed');
const afterEndpoint = await resolveIdentity();
assert.notEqual(afterEndpoint, initial);

const status = await connectionCredentialStatus(stores, connection);
assert.equal(status.configured, true);
if (!status.configured) return;
const rotated = await stores.credentialVault.set({
locator: connectionCredential(connection),
expected: { credentialId: status.credentialId, revision: status.revision },
secret: 'key-b',
});
assert.equal(rotated.kind, 'committed');
const afterCredential = await resolveIdentity();
assert.notEqual(afterCredential, afterEndpoint);

const headers = await stores.credentialVault.set({
locator: {
scope: 'connection',
connectionId: connection.connectionId,
kind: 'request_headers',
},
expected: null,
secret: JSON.stringify({ 'X-Relay-Account': 'account-b' }),
});
assert.equal(headers.kind, 'committed');
const afterHeaders = await resolveIdentity();
assert.notEqual(afterHeaders, afterCredential);
});
});

test('onboarding probes with the custom request headers the models path sends, and a header rotation supersedes', async () => {
await withFixture(async ({ stores }) => {
// A connection that authenticates through a custom header (plus a body
Expand Down
Loading