Revert "refactor(agents): propagate exact attribution across runtimes"

This reverts commit df31882c38.
This commit is contained in:
joshavant
2026-08-07 13:53:38 -05:00
committed by Josh Avant
parent 54ea39d553
commit 07680362ff
50 changed files with 91 additions and 1477 deletions
@@ -122,7 +122,7 @@ aa8a411ad37c1d1143b67376bf2d20255b9eedff61d80815f42e4f8ed7bd8e58 module/secret-
57dcb1462d4c4f9a98d934c4ca975b163d704758af9821a64001ff3ac05637c3 module/secret-input-runtime
dc0ee07d392a85c218939000b28c0138f139215da00f5592b34a68ba8e29a25d module/secret-ref-runtime
f97549081955e412d8eb64070c64bb5921bb1324744d824db05767d3adcce403 module/security-runtime
4bfa843c91a04a4cff0496ed89d9b9481c07b8dbdce6de784e2743f4f66afc1d module/session-catalog
5b1e30fe228e0d8c74c11d6ad2d9aed3df80cd3ea4ba2dd63c30cc5f084e93c9 module/session-catalog
50f5e344f98c27570b7a30e32a906b612e2383d21f102e88cd93e1d5425a6de9 module/session-discussion
f112bdabc51ba8659b37d0a6f6a32a2b1d471e5b49b56e108bf750ec55a7ea71 module/session-store-runtime
36affbe151431a6141664b6838e20f2d121ff210d57a3c1b4b41a8818b5c81d8 module/setup
@@ -1,161 +0,0 @@
import { afterEach, describe, expect, it } from "vitest";
import {
configureExecutionIdentityAdmissionSink,
type ExecutionIdentityAdmissionWork,
} from "../audit/execution-identity-admission.js";
import { executionIdentity } from "./agent-command-execution-identity.js";
import { createAgentExecutionAttribution } from "./agent-execution-attribution.js";
describe("agent command execution identity", () => {
let restoreSink: (() => void) | undefined;
afterEach(() => {
restoreSink?.();
restoreSink = undefined;
});
it("records the runtime correlation without requiring an audit admission token", () => {
const work: ExecutionIdentityAdmissionWork[] = [];
restoreSink = configureExecutionIdentityAdmissionSink((item) => {
work.push(item);
return true;
});
const attribution = createAgentExecutionAttribution({
runId: "run-1",
lifecycleGeneration: "generation-1",
});
executionIdentity.record({
attribution,
agentId: "main",
cfg: { logging: { audit: { enabled: true, executionIdentity: true } } },
ingress: executionIdentity.localIngress,
runId: attribution.runId,
runtimeKind: "embedded",
});
expect(work).toHaveLength(1);
expect(work[0]).toMatchObject({
kind: "capture",
envelope: {
contextId: attribution.contextId,
executionId: attribution.executionId,
createdAt: attribution.createdAt,
},
});
expect(attribution).not.toHaveProperty("executionIdentityAdmission");
});
it("uses exact attribution as the lifecycle authority", () => {
const attribution = createAgentExecutionAttribution({
runId: "run-1",
lifecycleGeneration: "generation-attribution",
});
expect(
executionIdentity.resolveAttribution(
{
executionAttribution: attribution,
lifecycleGeneration: "generation-flat",
} as never,
{ runId: attribution.runId },
),
).toEqual({
attribution,
lifecycleGeneration: "generation-attribution",
});
});
it("rejects attribution captured for a different run", () => {
const attribution = createAgentExecutionAttribution({
runId: "run-attribution",
lifecycleGeneration: "generation-attribution",
});
expect(() =>
executionIdentity.resolveAttribution(
{
executionAttribution: attribution,
} as never,
{ runId: "run-command" },
),
).toThrow("Agent command execution attribution runId does not match the command runId.");
});
it("allocates private attribution for direct command admission", () => {
const resolved = executionIdentity.resolveAttribution(
{ lifecycleGeneration: "generation-local" } as never,
{
runId: "run-local",
sessionKey: "agent:main:local",
sessionId: "session-local",
sessionAgentId: "main",
},
);
expect(resolved.attribution).toMatchObject({
runId: "run-local",
lifecycleGeneration: "generation-local",
sessionKey: "agent:main:local",
sessionId: "session-local",
agentId: "main",
});
expect(resolved.attribution).not.toHaveProperty("executionIdentityAdmission");
});
it("replaces attribution only after lifecycle rebound", () => {
const attribution = createAgentExecutionAttribution({
runId: "run-1",
lifecycleGeneration: "generation-1",
});
const opts = { executionAttribution: attribution } as never;
expect(executionIdentity.replaceAttribution(opts, attribution)).toBe(opts);
expect(
executionIdentity.replaceAttribution(
opts,
createAgentExecutionAttribution({
...attribution,
lifecycleGeneration: "generation-2",
}),
),
).not.toBe(opts);
});
it("strips untrusted ingress attribution and preserves trusted gateway attribution", () => {
const attribution = createAgentExecutionAttribution({
runId: "run-1",
lifecycleGeneration: "generation-1",
});
const opts = {
allowModelOverride: false,
executionAttribution: attribution,
lifecycleGeneration: "generation-flat",
runId: attribution.runId,
} as never;
expect(executionIdentity.prepareIngress(opts, false)).toEqual({
lifecycleGeneration: "generation-flat",
opts: {
allowModelOverride: false,
executionAttribution: undefined,
lifecycleGeneration: "generation-flat",
runId: attribution.runId,
},
});
expect(executionIdentity.prepareIngress(opts, true)).toEqual({
lifecycleGeneration: "generation-flat",
opts,
});
const inheritedOpts = Object.assign(Object.create({ executionAttribution: attribution }), {
allowModelOverride: false,
lifecycleGeneration: "generation-flat",
runId: attribution.runId,
}) as never;
expect(executionIdentity.prepareIngress(inheritedOpts, false).opts).toHaveProperty(
"executionAttribution",
undefined,
);
});
});
+1 -69
View File
@@ -4,9 +4,7 @@ import {
type ExecutionIdentityAdmissionFacts,
} from "../audit/execution-identity-admission.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { captureAgentRunLifecycleGeneration } from "../infra/agent-events.js";
import { createAgentExecutionAttribution } from "./agent-execution-attribution.js";
import type { AgentCommandGatewayIngressOpts, AgentCommandOpts } from "./command/types.js";
import type { AgentCommandOpts } from "./command/types.js";
type AgentCommandAdmissionIngress = ExecutionIdentityAdmissionFacts["ingress"];
@@ -55,74 +53,8 @@ function recordAgentCommandExecutionIdentity(params: {
);
}
function resolveAgentCommandExecutionAttribution(
opts: AgentCommandOpts,
params: {
runId: string;
sessionKey?: string;
sessionId?: string;
sessionAgentId?: string;
},
): {
attribution: NonNullable<AgentCommandOpts["executionAttribution"]>;
lifecycleGeneration: string;
} {
if (opts.executionAttribution && opts.executionAttribution.runId !== params.runId) {
throw new Error("Agent command execution attribution runId does not match the command runId.");
}
const lifecycleGeneration =
opts.executionAttribution?.lifecycleGeneration ??
opts.lifecycleGeneration ??
captureAgentRunLifecycleGeneration(params.runId);
return {
attribution:
opts.executionAttribution ??
createAgentExecutionAttribution({
runId: params.runId,
lifecycleGeneration,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
agentId: params.sessionAgentId,
}),
lifecycleGeneration,
};
}
function replaceAgentCommandExecutionAttribution(
opts: AgentCommandOpts,
attribution: AgentCommandOpts["executionAttribution"],
): AgentCommandOpts {
return attribution === opts.executionAttribution
? opts
: { ...opts, executionAttribution: attribution };
}
function prepareAgentCommandIngress(
opts: AgentCommandGatewayIngressOpts,
trustedAttribution: boolean,
): {
lifecycleGeneration: string;
opts: AgentCommandGatewayIngressOpts;
} {
const internalOpts: AgentCommandGatewayIngressOpts = trustedAttribution
? opts
: { ...opts, executionAttribution: undefined };
if (typeof internalOpts.allowModelOverride !== "boolean") {
throw new Error("allowModelOverride must be explicitly set for ingress agent runs.");
}
return {
lifecycleGeneration:
internalOpts.lifecycleGeneration ??
captureAgentRunLifecycleGeneration(internalOpts.runId ?? ""),
opts: internalOpts,
};
}
export const executionIdentity = {
localIngress: LOCAL_CLI_ADMISSION_INGRESS,
prepareIngress: prepareAgentCommandIngress,
record: recordAgentCommandExecutionIdentity,
replaceAttribution: replaceAgentCommandExecutionAttribution,
resolveAttribution: resolveAgentCommandExecutionAttribution,
systemIngress,
};
@@ -26,7 +26,6 @@ import {
resolveTestModelAliasFromPair,
resolveTestModelRefFromString,
} from "./agent-command.live-model-switch.test-helpers.js";
import { createAgentExecutionAttribution } from "./agent-execution-attribution.js";
import {
INTERNAL_RUNTIME_CONTEXT_BEGIN,
INTERNAL_RUNTIME_CONTEXT_END,
@@ -119,7 +118,6 @@ const state = vi.hoisted(() => ({
resolvedSessionKeyMock: undefined as string | undefined,
trajectoryRecorderParamsMock: vi.fn(),
enqueueExecutionIdentityContextAtAdmissionMock: vi.fn(),
executionIdentityCounter: 0,
}));
vi.mock("./model-fallback-runner.js", () => ({
@@ -127,19 +125,8 @@ vi.mock("./model-fallback-runner.js", () => ({
}));
vi.mock("../audit/execution-identity-admission.js", () => ({
createExecutionIdentityAdmissionToken: (runId: string) => {
const sequence = ++state.executionIdentityCounter;
return {
tokenVersion: 1,
contextId: `context-${sequence}`,
executionId: `execution-${sequence}`,
runId,
createdAt: sequence,
};
},
enqueueExecutionIdentityContextAtAdmission: (...args: unknown[]) =>
state.enqueueExecutionIdentityContextAtAdmissionMock(...args),
parseExecutionIdentityAdmissionToken: (token: unknown) => token,
}));
vi.mock("./command/attempt-execution.runtime.js", () => ({
@@ -677,14 +664,12 @@ vi.mock("../acp/control-plane/manager.js", () => ({
let agentCommand: typeof import("./agent-command.js").agentCommand;
let agentCommandFromSystem: typeof import("./agent-command.js").agentCommandFromSystem;
let agentCommandFromIngress: typeof import("./agent-command.js").agentCommandFromIngress;
let agentCommandTesting: typeof import("./agent-command.js").testing;
beforeAll(async () => {
const mod = await import("./agent-command.js");
agentCommand ??= mod.agentCommand;
agentCommandFromSystem ??= mod.agentCommandFromSystem;
agentCommandFromIngress ??= mod.agentCommandFromIngress;
agentCommandTesting ??= mod.testing;
});
@@ -1105,12 +1090,7 @@ describe("agentCommand LiveSessionModelSwitchError retry", () => {
expect.objectContaining({
ingress: { kind: "local-cli", boundary: "agent-command.local", state: "present" },
}),
expect.objectContaining({
enabled: false,
contextId: expect.any(String),
executionId: expect.any(String),
now: expect.any(Number),
}),
{ enabled: false },
);
expect(state.runAgentAttemptMock).toHaveBeenCalledTimes(1);
});
@@ -1130,24 +1110,14 @@ describe("agentCommand LiveSessionModelSwitchError retry", () => {
expect.objectContaining({
ingress: { kind: "local-cli", boundary: "agent-command.local", state: "present" },
}),
expect.objectContaining({
enabled: true,
contextId: expect.any(String),
executionId: expect.any(String),
now: expect.any(Number),
}),
{ enabled: true },
);
expect(state.enqueueExecutionIdentityContextAtAdmissionMock).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
ingress: { kind: "system", boundary: "gateway.boot", state: "present" },
}),
expect.objectContaining({
enabled: true,
contextId: expect.any(String),
executionId: expect.any(String),
now: expect.any(Number),
}),
{ enabled: true },
);
});
@@ -4559,25 +4529,16 @@ describe("agentCommand LiveSessionModelSwitchError retry", () => {
it("keeps session provenance for internal ACP turns", async () => {
setupAcpSession();
const attribution = createAgentExecutionAttribution({
runId: "session-1",
lifecycleGeneration: "test-generation",
sessionKey: "agent:main:main",
sessionId: "session-1",
agentId: "main",
});
await agentCommand({
message: "internal ACP turn",
sessionKey: "agent:main:main",
sessionEffects: "internal",
executionAttribution: attribution,
});
expect(state.registerAgentRunContextMock).toHaveBeenCalledWith(
"session-1",
expect.objectContaining({
attribution,
sessionKey: "agent:main:main",
sessionId: "session-1",
isControlUiVisible: false,
@@ -4585,32 +4546,6 @@ describe("agentCommand LiveSessionModelSwitchError retry", () => {
);
});
it("drops caller-supplied attribution at the public ingress boundary", async () => {
setupAcpSession();
const forgedAttribution = createAgentExecutionAttribution({
runId: "forged-run",
lifecycleGeneration: "forged-generation",
sessionKey: "agent:main:forged",
sessionId: "forged-session",
agentId: "forged-agent",
});
await agentCommandFromIngress({
message: "public ingress ACP turn",
sessionKey: "agent:main:main",
sessionEffects: "internal",
allowModelOverride: false,
executionAttribution: forgedAttribution,
} as Parameters<typeof agentCommandFromIngress>[0] & {
executionAttribution: typeof forgedAttribution;
});
expect(state.registerAgentRunContextMock).toHaveBeenCalledWith(
"session-1",
expect.not.objectContaining({ attribution: forgedAttribution }),
);
});
it("allows manual ACP spawn turns when ACP dispatch is disabled", async () => {
setupAcpSession();
state.resolveAcpDispatchPolicyErrorMock.mockReturnValue(
+18 -17
View File
@@ -144,8 +144,7 @@ async function agentCommandInternal(
manifestMetadataSnapshot,
modelManifestContext,
} = prepared;
let { attribution: executionAttribution, lifecycleGeneration } =
executionIdentity.resolveAttribution(opts, prepared);
let lifecycleGeneration = opts.lifecycleGeneration ?? captureAgentRunLifecycleGeneration(runId);
let sessionEntry = prepared.sessionEntry,
runOwnedSessionId = sessionId;
const sessionStateActor = classifySessionStateActor({
@@ -232,7 +231,7 @@ async function agentCommandInternal(
});
return await sessionWorkAdmission.run(async () => {
executionIdentity.record({
attribution: executionAttribution,
attribution: opts.executionAttribution,
agentId: sessionAgentId,
cfg,
ingress: admissionIngress,
@@ -423,7 +422,6 @@ async function agentCommandInternal(
workspaceDir,
runId,
lifecycleGeneration,
attribution: executionAttribution,
acpManager,
acpResolution,
trackInternalModelRunTarget,
@@ -490,12 +488,11 @@ async function agentCommandInternal(
sessionEntry = modelSelection.sessionEntry;
const embeddedAttempt = await runEmbeddedAgentAttempt({
prepared,
opts: executionIdentity.replaceAttribution(opts, executionAttribution),
opts,
sessionEntry,
lifecycleGeneration,
onLifecycleGenerationChanged: (nextLifecycleGeneration, nextAttribution) => {
onLifecycleGenerationChanged: (nextLifecycleGeneration) => {
lifecycleGeneration = nextLifecycleGeneration;
executionAttribution = nextAttribution ?? executionAttribution;
},
suppressVisibleSessionEffects,
preserveUserFacingSessionModelState,
@@ -636,20 +633,20 @@ async function agentCommandFromIngressInternal(
recovery?: {
restoreAdmittedRecovery?: () => Promise<MainSessionRecoveryPendingTarget | undefined>;
},
trustedAttribution = false,
) {
const { lifecycleGeneration, opts: internalOpts } = executionIdentity.prepareIngress(
opts,
trustedAttribution,
);
if (typeof opts.allowModelOverride !== "boolean") {
throw new Error("allowModelOverride must be explicitly set for ingress agent runs.");
}
const lifecycleGeneration =
opts.lifecycleGeneration ?? captureAgentRunLifecycleGeneration(opts.runId ?? "");
return await withAgentRunLifecycleGeneration(lifecycleGeneration, async () => {
const result = await runWithAgentCommandRecoveryOwner({
lifecycleGeneration,
mode: "claim",
opts: {
...internalOpts,
...opts,
lifecycleGeneration,
senderIsOwner: internalOpts.senderIsOwner === true,
senderIsOwner: opts.senderIsOwner === true,
},
prepare: async (preparedOpts) => await prepareAgentCommandExecution(preparedOpts, runtime),
restoreAdmittedRecovery: recovery?.restoreAdmittedRecovery,
@@ -664,7 +661,7 @@ async function agentCommandFromIngressInternal(
});
if (result) {
emitIngressModelUsageDiagnostic(result, internalOpts);
emitIngressModelUsageDiagnostic(result, opts);
}
return result;
@@ -679,7 +676,11 @@ export async function agentCommandFromIngress(
) {
// Plugin SDK callers may be plain JavaScript. Enforce the private execution
// boundary at runtime so extra or inherited properties cannot author audit identity.
return await agentCommandFromIngressInternal(opts, runtime, deps);
return await agentCommandFromIngressInternal(
{ ...opts, executionAttribution: undefined },
runtime,
deps,
);
}
/** Internal Gateway entrypoint that restores a rejected restart-recovery admission. */
@@ -691,7 +692,7 @@ export async function agentCommandFromGatewayIngress(
restoreAdmittedRecovery?: () => Promise<MainSessionRecoveryPendingTarget | undefined>;
},
) {
return await agentCommandFromIngressInternal(opts, runtime, deps, recovery, true);
return await agentCommandFromIngressInternal(opts, runtime, deps, recovery);
}
export const testing = {
@@ -66,26 +66,6 @@ describe("createAgentExecutionAttribution", () => {
expect(attribution).not.toHaveProperty("executionIdentityAdmission");
});
it("ignores inherited audit admission tokens", () => {
const inheritedToken = createExecutionIdentityAdmissionToken("run-1", {
contextId: "inherited-context",
executionId: "inherited-execution",
now: 123,
});
const params = Object.assign(
Object.create({
executionIdentityAdmission: { token: inheritedToken, retryOnly: true },
}) as object,
{ runId: "run-1", lifecycleGeneration: "generation-1" },
);
const attribution = createAgentExecutionAttribution(params);
expect(attribution.contextId).not.toBe("inherited-context");
expect(attribution.executionId).not.toBe("inherited-execution");
expect(attribution).not.toHaveProperty("executionIdentityAdmission");
});
it.each([false, true])(
"changes only lifecycle ownership when rebound (token=%s)",
(withToken) => {
+5 -14
View File
@@ -30,12 +30,6 @@ function requireAttributionField(value: string, field: "runId" | "lifecycleGener
return value;
}
function freezeAgentExecutionAttribution(
value: AgentExecutionAttribution,
): AgentExecutionAttribution {
return Object.freeze(Object.assign(Object.create(null) as AgentExecutionAttribution, value));
}
export function createAgentExecutionAttribution(params: {
runId: string;
lifecycleGeneration: string;
@@ -45,11 +39,8 @@ export function createAgentExecutionAttribution(params: {
executionIdentityAdmission?: AgentExecutionIdentityAdmission;
}): AgentExecutionAttribution {
const runId = requireAttributionField(params.runId, "runId");
const executionIdentityAdmission = Object.hasOwn(params, "executionIdentityAdmission")
? params.executionIdentityAdmission
: undefined;
const token = executionIdentityAdmission
? parseExecutionIdentityAdmissionToken(executionIdentityAdmission.token)
const token = params.executionIdentityAdmission
? parseExecutionIdentityAdmissionToken(params.executionIdentityAdmission.token)
: undefined;
if (token && token.runId !== runId) {
throw new TypeError("Agent execution attribution token disagrees with runId");
@@ -57,7 +48,7 @@ export function createAgentExecutionAttribution(params: {
const sessionKey = normalizeOptionalString(params.sessionKey);
const sessionId = normalizeOptionalString(params.sessionId);
const agentId = normalizeOptionalString(params.agentId);
return freezeAgentExecutionAttribution({
return Object.freeze({
runId,
contextId: token?.contextId ?? randomUUID(),
executionId: token?.executionId ?? randomUUID(),
@@ -67,7 +58,7 @@ export function createAgentExecutionAttribution(params: {
? {
executionIdentityAdmission: Object.freeze({
token,
retryOnly: executionIdentityAdmission?.retryOnly === true,
retryOnly: params.executionIdentityAdmission?.retryOnly === true,
}),
}
: {}),
@@ -81,7 +72,7 @@ export function rebindAgentExecutionAttribution(
attribution: AgentExecutionAttribution,
lifecycleGeneration: string,
): AgentExecutionAttribution {
return freezeAgentExecutionAttribution({
return Object.freeze({
...attribution,
lifecycleGeneration: requireAttributionField(lifecycleGeneration, "lifecycleGeneration"),
});
-20
View File
@@ -9,7 +9,6 @@ import {
mintSecretSentinel,
resolveSecretSentinel,
} from "../secrets/sentinel.js";
import { createAgentExecutionAttribution } from "./agent-execution-attribution.js";
import type { AgentHarness } from "./harness/types.js";
import type { AgentRuntimeAuthPlan } from "./runtime-plan/types.js";
@@ -795,13 +794,6 @@ describe("runBtwSideQuestion", () => {
const codexSideQuestionMock = registerCodexSideQuestionHarness({
supports,
});
const attribution = createAgentExecutionAttribution({
runId: "run-btw-codex",
lifecycleGeneration: "generation-1",
sessionKey: DEFAULT_SESSION_KEY,
sessionId: "session-1",
agentId: "main",
});
resolveModelWithRegistryMock.mockReturnValue({
provider: "openai",
id: "gpt-5.5",
@@ -837,7 +829,6 @@ describe("runBtwSideQuestion", () => {
});
const result = await runSideQuestion({
attribution,
provider: "openai",
model: "gpt-5.5",
sessionKey: DEFAULT_SESSION_KEY,
@@ -855,7 +846,6 @@ describe("runBtwSideQuestion", () => {
expect(result).toEqual({ text: "Codex side answer." });
expect(codexSideQuestionMock).toHaveBeenCalledTimes(1);
expect(mockArg(codexSideQuestionMock, 0, 0)).not.toHaveProperty("attribution");
expect(codexSideQuestionMock).toHaveBeenCalledWith(
expect.objectContaining({
provider: "openai",
@@ -1476,16 +1466,8 @@ describe("runBtwSideQuestion", () => {
it("runs CLI-runtime alias BTW as an ephemeral CLI side question", async () => {
const { cleanup, prepared } = mockCliOutput({ text: "CLI side answer." });
const attribution = createAgentExecutionAttribution({
runId: "run-btw-cli",
lifecycleGeneration: "generation-1",
sessionKey: DEFAULT_SESSION_KEY,
sessionId: "session-1",
agentId: "main",
});
const result = await runSideQuestion({
attribution,
cfg: {
agents: {
defaults: {
@@ -1509,9 +1491,7 @@ describe("runBtwSideQuestion", () => {
cliSessionId?: string;
extraSystemPrompt?: string;
prompt?: string;
attribution?: unknown;
};
expect(prepareParams.attribution).toBe(attribution);
expect(prepareParams.executionMode).toBe("side-question");
expect(prepareParams.provider).toBe("claude-cli");
expect(prepareParams.model).toBe("claude-opus-4-7");
+1 -8
View File
@@ -21,7 +21,6 @@ import type {
} from "../llm/types.js";
import { prepareProviderRuntimeAuth } from "../plugins/provider-runtime.js";
import { isModelSelectionLocked } from "../sessions/model-overrides.js";
import type { AgentExecutionAttribution } from "./agent-execution-attribution.js";
import {
resolveAgentWorkspaceDir,
resolveDefaultAgentDir,
@@ -581,8 +580,6 @@ async function resolveRuntimeModel(params: {
}
type RunBtwSideQuestionParams = {
/** Host-owned execution identity; never projected onto the public harness params object. */
attribution?: AgentExecutionAttribution;
cfg: OpenClawConfig;
agentDir: string;
provider: string;
@@ -627,7 +624,6 @@ type RunBtwSideQuestionParams = {
};
async function runCliBtwSideQuestion(params: {
attribution?: AgentExecutionAttribution;
cfg: OpenClawConfig;
model: string;
question: string;
@@ -652,7 +648,6 @@ async function runCliBtwSideQuestion(params: {
overrideSeconds: params.opts?.timeoutOverrideSeconds,
});
const prepared = await prepareCliRunContext({
...(params.attribution ? { attribution: params.attribution } : {}),
sessionId: params.sessionId,
sessionKey: params.sessionKey,
sessionEntry: params.sessionEntry,
@@ -969,9 +964,8 @@ export async function runBtwSideQuestion(
runtimeAuthPlan.modelRoute?.authRequirement === "api-key" && "auth" in resolvedAttempt
? resolvedAttempt.auth.apiKey?.trim()
: undefined;
const { attribution: _attribution, ...publicParams } = params;
const result = await selectedHarness.runSideQuestion({
...publicParams,
...params,
provider: runtimeModel.provider,
model: runtimeModel.id,
runtimeModel,
@@ -1092,7 +1086,6 @@ export async function runBtwSideQuestion(
: undefined);
if (cliProvider) {
return runCliBtwSideQuestion({
...(params.attribution ? { attribution: params.attribution } : {}),
cfg: params.cfg,
model: params.model,
question: params.question,
+2 -5
View File
@@ -47,7 +47,6 @@ import {
attachCliMessagingDeliveryEvidence,
getCliMessagingDeliveryEvidence,
} from "./cli-runner/delivery-evidence.js";
import { bindCliRunExecutionAttribution } from "./cli-runner/execution-attribution.js";
import { cliBackendLog, formatCliBackendOutputDigest } from "./cli-runner/log.js";
import { hashCliReseedPrompt } from "./cli-runner/reseed-envelope.js";
import {
@@ -590,12 +589,10 @@ async function finalizeCliContextEngineTurn(params: {
/** Prepares and runs one CLI-backed agent turn. */
export function runCliAgent(paramsInput: RunCliAgentParams): Promise<EmbeddedAgentRunResult> {
const attributedParams = bindCliRunExecutionAttribution(paramsInput);
const lifecycleGeneration =
attributedParams.lifecycleGeneration ??
captureAgentRunLifecycleGeneration(attributedParams.runId);
paramsInput.lifecycleGeneration ?? captureAgentRunLifecycleGeneration(paramsInput.runId);
const params = {
...attributedParams,
...paramsInput,
lifecycleGeneration,
};
// Observability services register before turns and keep subscriptions process-stable.
@@ -1,78 +0,0 @@
import { describe, expect, it } from "vitest";
import { createAgentExecutionAttribution } from "../agent-execution-attribution.js";
import { bindCliRunExecutionAttribution } from "./execution-attribution.js";
import type { RunCliAgentParams } from "./types.js";
function createRunParams(overrides: Partial<RunCliAgentParams> = {}): RunCliAgentParams {
return {
sessionId: "legacy-session",
sessionKey: "agent:legacy:main",
agentId: "legacy-agent",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp/workspace",
prompt: "test",
provider: "test-cli",
timeoutMs: 1_000,
runId: "legacy-run",
lifecycleGeneration: "legacy-generation",
...overrides,
};
}
describe("bindCliRunExecutionAttribution", () => {
it("projects admitted identity over legacy flat fields", () => {
const attribution = createAgentExecutionAttribution({
runId: "admitted-run",
lifecycleGeneration: "admitted-generation",
sessionKey: "agent:main:main",
sessionId: "admitted-session",
agentId: "main",
});
expect(bindCliRunExecutionAttribution(createRunParams({ attribution }))).toMatchObject({
attribution,
runId: "admitted-run",
lifecycleGeneration: "admitted-generation",
sessionKey: "agent:main:main",
sessionId: "admitted-session",
agentId: "main",
});
});
it("preserves operational CLI routing absent from admitted attribution", () => {
const attribution = createAgentExecutionAttribution({
runId: "admitted-run",
lifecycleGeneration: "admitted-generation",
sessionId: "admitted-session",
});
const bound = bindCliRunExecutionAttribution(createRunParams({ attribution }));
expect(bound).toMatchObject({
attribution,
runId: "admitted-run",
lifecycleGeneration: "admitted-generation",
sessionKey: "agent:legacy:main",
sessionId: "admitted-session",
agentId: "legacy-agent",
});
});
it("preserves required CLI routing for sparse admitted attribution", () => {
const attribution = createAgentExecutionAttribution({
runId: "admitted-run",
lifecycleGeneration: "admitted-generation",
});
const bound = bindCliRunExecutionAttribution(createRunParams({ attribution }));
expect(bound).toMatchObject({
attribution,
runId: "admitted-run",
lifecycleGeneration: "admitted-generation",
sessionId: "legacy-session",
sessionKey: "agent:legacy:main",
agentId: "legacy-agent",
});
});
});
@@ -1,31 +0,0 @@
import type { RunCliAgentParams } from "./types.js";
/** Projects admitted execution identity over legacy flat CLI-run fields. */
export function bindCliRunExecutionAttribution(params: RunCliAgentParams): RunCliAgentParams {
const attribution = params.attribution;
if (!attribution) {
return params;
}
const {
runId: _legacyRunId,
lifecycleGeneration: _legacyLifecycleGeneration,
sessionKey: _legacySessionKey,
sessionId: _legacySessionId,
agentId: _legacyAgentId,
...run
} = params;
return {
...run,
runId: attribution.runId,
lifecycleGeneration: attribution.lifecycleGeneration,
sessionId: attribution.sessionId ?? _legacySessionId,
// Optional attribution fields are audit facts. When they are absent, keep
// the CLI candidate's operational routing instead of selecting a default.
...((attribution.sessionKey ?? _legacySessionKey)
? { sessionKey: attribution.sessionKey ?? _legacySessionKey }
: {}),
...((attribution.agentId ?? _legacyAgentId)
? { agentId: attribution.agentId ?? _legacyAgentId }
: {}),
};
}
+4 -14
View File
@@ -27,7 +27,6 @@ import {
createTestRegistry,
} from "../../test-utils/channel-plugins.js";
import { captureEnv, setTestEnvValue } from "../../test-utils/env.js";
import { createAgentExecutionAttribution } from "../agent-execution-attribution.js";
import { readExternalCliBootstrapCredential as readExternalCliBootstrapCredentialImpl } from "../auth-profiles/external-cli-sync.js";
import { resolveApiKeyForProfile as resolveApiKeyForProfileImpl } from "../auth-profiles/oauth.js";
import {
@@ -2973,7 +2972,7 @@ describe("prepareCliRunContext", () => {
expect(resolveMcpLoopbackScopedTools).not.toHaveBeenCalled();
});
it("binds admitted current turn context into the bundle MCP client grant", async () => {
it("binds current turn context into the bundle MCP client grant", async () => {
const getActiveMcpLoopbackRuntime = vi.fn(() => ({
port: 31783,
ownerToken: "loopback-owner-token",
@@ -3020,21 +3019,12 @@ describe("prepareCliRunContext", () => {
},
});
const context = await fixture.prepare({
attribution: createAgentExecutionAttribution({
runId: "run-test-room-event-tools",
lifecycleGeneration: "generation-admitted",
sessionKey: "agent:main:telegram:group:chat123",
sessionId: "session-test",
agentId: "worker",
}),
sessionKey: "agent:forged:main",
sessionId: "forged-session",
sessionKey: "agent:main:telegram:group:chat123",
runtimePolicySessionKey: "agent:worker:discord:default:direct:canonical-sender",
agentId: "forged-agent",
agentId: "worker",
provider: "native-cli",
modelProvider: "anthropic",
runId: "forged-run",
lifecycleGeneration: "generation-forged",
runId: "run-test-room-event-tools",
sessionEntry: {
execHost: "node",
execSecurity: "allowlist",
+1 -5
View File
@@ -126,7 +126,6 @@ import {
resolveBundledCliBackendAuthPolicy,
type BundledCliBackendAuthPolicy,
} from "./cli-backend-auth-policy.js";
import { bindCliRunExecutionAttribution } from "./execution-attribution.js";
import { buildCliAgentSystemPrompt, isClaudeCliProvider, normalizeCliModel } from "./helpers.js";
import { cliBackendLog } from "./log.js";
import { buildCliMcpGrantContext, normalizeOptionalMcpContextValue } from "./mcp-grant-context.js";
@@ -401,10 +400,7 @@ function buildCliAuthProfileResolutionError(params: {
export async function prepareCliRunContext(
inputParams: RunCliAgentParams,
): Promise<PreparedCliRunContext> {
const attributedParams = bindCliRunExecutionAttribution(inputParams);
let params = attributedParams.config
? attributedParams
: { ...attributedParams, config: getRuntimeConfig() };
let params = inputParams.config ? inputParams : { ...inputParams, config: getRuntimeConfig() };
const runConfig = params.config!;
const selectedOwner = normalizeAgentId(
params.agentId?.trim() ||
-3
View File
@@ -34,7 +34,6 @@ import type { InputProvenance } from "../../sessions/input-provenance.js";
import type { UserTurnTranscriptRecorder } from "../../sessions/user-turn-transcript.js";
import type { SkillSnapshot } from "../../skills/types.js";
import type { AuthProfileStore } from "../auth-profiles/types.js";
import type { AgentExecutionAttribution } from "../agent-execution-attribution.js";
import type { ExecElevatedDefaults } from "../bash-tools.exec-types.js";
import type { BootstrapContextMode } from "../bootstrap-files.js";
import type { BootstrapContextRunKind } from "../bootstrap-mode.js";
@@ -64,8 +63,6 @@ type CliSessionRetryParams = {
/** Input contract for one CLI-backed agent run. */
export type RunCliAgentParams = {
/** Admission-owned execution correlation carried unchanged across CLI retries. */
attribution?: AgentExecutionAttribution;
/** Caller-owned in-memory transcript for ephemeral helper runs. */
sessionManager?: SessionManager;
sessionId: string;
-3
View File
@@ -8,7 +8,6 @@ import { formatErrorMessage } from "../../infra/errors.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { normalizeAgentId, resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
import type { RuntimeEnv } from "../../runtime.js";
import type { AgentExecutionAttribution } from "../agent-execution-attribution.js";
import { prepareInternalSessionEffectsSession } from "../internal-session-effects.js";
import type { AgentRunSessionTarget } from "../run-session-target.js";
import { isAgentRunRestartAbortReason } from "../run-termination.js";
@@ -50,7 +49,6 @@ export async function runAcpAgentCommand(params: {
workspaceDir: string;
runId: string;
lifecycleGeneration: string;
attribution?: AgentExecutionAttribution;
acpManager: PreparedAgentCommandExecution["acpManager"];
acpResolution: AcpReadyResolution;
trackInternalModelRunTarget: (target: AgentRunSessionTarget | undefined) => void;
@@ -59,7 +57,6 @@ export async function runAcpAgentCommand(params: {
const acpToolTracker = attemptExecutionRuntime.createAcpToolLifecycleTracker();
const startedAt = Date.now();
registerAgentRunContext(params.runId, {
...(params.attribution ? { attribution: params.attribution } : {}),
sessionKey: params.sessionKey,
sessionId: params.sessionId,
agentId: params.sessionAgentId,
@@ -406,9 +406,6 @@ vi.mock("../model-runtime-aliases.js", async () => {
vi.mock("../embedded-agent.js", () => ({
runEmbeddedAgent: runEmbeddedAgentMock,
}));
vi.mock("../embedded-agent-runner/run-orchestrator.js", () => ({
runEmbeddedAgentInternal: runEmbeddedAgentMock,
}));
vi.mock("../session-write-lock.js", async () => {
const actual = await vi.importActual<typeof import("../session-write-lock.js")>(
+6 -21
View File
@@ -51,7 +51,6 @@ import {
} from "../../tasks/task-status-access.js";
import { resolveUserPath } from "../../utils.js";
import { resolveMessageChannel } from "../../utils/message-channel.js";
import type { AgentExecutionAttribution } from "../agent-execution-attribution.js";
import type { AgentRunTerminalReplySnapshot } from "../agent-run-terminal-reply.js";
import { resolveAuthProfileOrder } from "../auth-profiles/order.js";
import { ensureAuthProfileStore } from "../auth-profiles/store.js";
@@ -75,12 +74,7 @@ import {
} from "../cli-session.js";
import { resolveConversationCapabilityProfile } from "../conversation-capability-profile.js";
import { resolveConversationToolPolicies } from "../conversation-tool-policy-pipeline.js";
import { runEmbeddedAgentInternal } from "../embedded-agent-runner/run-orchestrator.js";
import type {
AgentExecutionAttributionInfo,
RunEmbeddedAgentInternalParams,
} from "../embedded-agent-runner/run/internal-params.js";
import type { EmbeddedAgentRunResult } from "../embedded-agent.js";
import { runEmbeddedAgent, type EmbeddedAgentRunResult } from "../embedded-agent.js";
import type { ContextEngineLogicalTurnLease } from "../harness/context-engine-logical-turn.js";
import type { ContextEngineTurnAttemptFacts } from "../harness/context-engine-turn-attempt.js";
import { runAgentHarnessBeforeMessageWriteHook } from "../harness/hook-helpers.js";
@@ -579,10 +573,7 @@ export function runAgentAttempt(params: {
contextEngineLogicalTurnLease?: ContextEngineLogicalTurnLease;
onUserMessagePersisted?: (message: Extract<AgentMessage, { role: "user" }>) => void;
onContextEngineTurnCandidate?: (facts: ContextEngineTurnAttemptFacts) => void;
onLifecycleGenerationChanged?: (
lifecycleGeneration: string,
attribution?: AgentExecutionAttribution,
) => void;
onLifecycleGenerationChanged?: (lifecycleGeneration: string) => void;
}) {
const sessionAuthProfileId = params.sessionEntry?.authProfileOverride?.trim();
const sessionAuthProfileSource = resolveSessionAuthProfileOverrideSource(params.sessionEntry);
@@ -972,9 +963,6 @@ export function runAgentAttempt(params: {
runId: params.runId,
lifecycleGeneration: params.lifecycleGeneration,
onExecutionStarted: params.opts.onExecutionStarted,
...(params.opts.executionAttribution
? { attribution: params.opts.executionAttribution }
: {}),
lane: params.opts.lane,
extraSystemPrompt: params.opts.extraSystemPrompt,
inputProvenance: params.opts.inputProvenance,
@@ -1155,7 +1143,7 @@ export function runAgentAttempt(params: {
});
}
const embeddedRunParams: RunEmbeddedAgentInternalParams = {
const embeddedRunParams: Parameters<typeof runEmbeddedAgent>[0] = {
sessionId: params.sessionId,
sessionKey: params.sessionKey,
chatType: params.sessionEntry?.chatType,
@@ -1216,7 +1204,6 @@ export function runAgentAttempt(params: {
runTimeoutOverrideMs: params.runTimeoutOverrideMs,
runId: params.runId,
lifecycleGeneration: params.lifecycleGeneration,
...(params.opts.executionAttribution ? { attribution: params.opts.executionAttribution } : {}),
lane: params.opts.lane,
// Hidden internal runs lack an event consumer; visible lanes still feed UI and parent relays.
suppressLiveStreamOutput: shouldSuppressEmbeddedLiveStreamOutput(params),
@@ -1256,12 +1243,10 @@ export function runAgentAttempt(params: {
contextEngineLogicalTurnLease: params.contextEngineLogicalTurnLease,
onContextEngineTurnCandidate: params.onContextEngineTurnCandidate,
onUserMessagePersisted: params.onUserMessagePersisted,
onExecutionStarted: () => {
onExecutionStarted: (info) => {
params.opts.onExecutionStarted?.();
},
onExecutionAttributionChanged: (info: AgentExecutionAttributionInfo) => {
if (info?.lifecycleGeneration) {
params.onLifecycleGenerationChanged?.(info.lifecycleGeneration, info.attribution);
params.onLifecycleGenerationChanged?.(info.lifecycleGeneration);
}
},
onSessionIdChanged: params.opts.onSessionIdChanged,
@@ -1273,7 +1258,7 @@ export function runAgentAttempt(params: {
embeddedRunParams,
readChannelSourceTurnSameThreadRequired(params.runContext),
);
return runEmbeddedAgentInternal(embeddedRunParams);
return runEmbeddedAgent(embeddedRunParams);
}
export function buildAcpResult(params: {
+4 -13
View File
@@ -15,7 +15,6 @@ import {
} from "../../tasks/task-status-access.js";
import { createTrajectoryRuntimeRecorder } from "../../trajectory/runtime.js";
import { resolveMessageChannel } from "../../utils/message-channel.js";
import type { AgentExecutionAttribution } from "../agent-execution-attribution.js";
import {
clearAutoFallbackPrimaryProbeSelection,
entryMatchesAutoFallbackPrimaryProbe,
@@ -68,10 +67,7 @@ export async function runEmbeddedAgentAttempt(params: {
opts: AgentCommandOpts;
sessionEntry?: SessionEntry;
lifecycleGeneration: string;
onLifecycleGenerationChanged: (
lifecycleGeneration: string,
attribution?: AgentExecutionAttribution,
) => void;
onLifecycleGenerationChanged: (lifecycleGeneration: string) => void;
suppressVisibleSessionEffects: boolean;
preserveUserFacingSessionModelState: boolean;
modelSelection: EmbeddedModelSelection;
@@ -99,7 +95,6 @@ export async function runEmbeddedAgentAttempt(params: {
timeoutMs,
runTimeoutOverrideMs,
} = params.prepared;
let executionAttribution = params.opts.executionAttribution;
const { runContext, skillsSnapshot, resolvedVerboseLevel } = params.embeddedSessionState;
const {
defaultProvider,
@@ -479,10 +474,7 @@ export async function runEmbeddedAgentAttempt(params: {
runTimeoutOverrideMs,
runId,
lifecycleGeneration,
opts:
executionAttribution === params.opts.executionAttribution
? params.opts
: { ...params.opts, executionAttribution },
opts: params.opts,
runContext,
spawnedBy,
messageChannel,
@@ -508,11 +500,10 @@ export async function runEmbeddedAgentAttempt(params: {
contextEngineLogicalTurnLease: runOptions.contextEngineLogicalTurnLease,
onContextEngineTurnCandidate: runOptions.onContextEngineTurnCandidate,
onUserMessagePersisted: attemptLifecycleCallbacks.onUserMessagePersisted,
onLifecycleGenerationChanged: (nextLifecycleGeneration, nextAttribution) => {
onLifecycleGenerationChanged: (nextLifecycleGeneration) => {
lifecycleGeneration = nextLifecycleGeneration;
executionAttribution = nextAttribution ?? executionAttribution;
// Outer cleanup owns the run context, so publish before the attempt can reject.
params.onLifecycleGenerationChanged(nextLifecycleGeneration, nextAttribution);
params.onLifecycleGenerationChanged(nextLifecycleGeneration);
},
onAgentEvent: attemptLifecycleCallbacks.onAgentEvent,
deferTerminalLifecycle: true,
@@ -1,9 +1,8 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { emitAgentEvent } from "../../infra/agent-events.js";
import { createAgentExecutionAttribution } from "../agent-execution-attribution.js";
import { resolveEmbeddedCliBackendDispatchEligibility } from "./cli-backend-dispatch-eligibility.js";
import { runEmbeddedAgentViaCliBackendIfEligible } from "./cli-backend-dispatch.js";
import type { RunEmbeddedAgentInternalParams } from "./run/internal-params.js";
import type { RunEmbeddedAgentParams } from "./run/params.js";
import type { EmbeddedAgentRunResult } from "./types.js";
const ensureAuthProfileStore = vi.hoisted(() => vi.fn());
@@ -44,9 +43,7 @@ vi.mock("./cli-backend-dispatch-transcript.js", () => ({
createCliDispatchTranscriptRecorder,
}));
function baseRunParams(
overrides: Partial<RunEmbeddedAgentInternalParams> = {},
): RunEmbeddedAgentInternalParams {
function baseRunParams(overrides: Partial<RunEmbeddedAgentParams> = {}): RunEmbeddedAgentParams {
return {
sessionId: "recall-session",
sessionKey: "agent:main:recall",
@@ -213,7 +210,7 @@ describe("resolveEmbeddedCliBackendDispatchEligibility", () => {
});
describe("runEmbeddedAgentViaCliBackendIfEligible gate", () => {
const runGate = (overrides: Partial<RunEmbeddedAgentInternalParams> = {}) =>
const runGate = (overrides: Partial<RunEmbeddedAgentParams> = {}) =>
runEmbeddedAgentViaCliBackendIfEligible(baseRunParams(overrides));
it("returns undefined without the opt-in", async () => {
@@ -395,7 +392,7 @@ describe("runEmbeddedAgentViaCliBackendIfEligible execution", () => {
] as const)("refuses dispatch for %s", async (_label, overrides) => {
expect(
await runEmbeddedAgentViaCliBackendIfEligible(
baseRunParams(overrides as Partial<RunEmbeddedAgentInternalParams>),
baseRunParams(overrides as Partial<RunEmbeddedAgentParams>),
),
).toBeUndefined();
expect(runCliAgent).not.toHaveBeenCalled();
@@ -411,34 +408,6 @@ describe("runEmbeddedAgentViaCliBackendIfEligible execution", () => {
expect(onExecutionStarted).toHaveBeenCalledWith({ lifecycleGeneration: "gen-1" });
});
it("preserves admission attribution through the embedded-to-CLI bridge", async () => {
const attribution = createAgentExecutionAttribution({
runId: "run-cli-dispatch-test",
lifecycleGeneration: "gen-1",
sessionKey: "agent:main:recall",
sessionId: "recall-session",
agentId: "main",
});
const onExecutionStarted = vi.fn();
const onExecutionAttributionChanged = vi.fn();
await runEmbeddedAgentViaCliBackendIfEligible(
baseRunParams({
attribution,
lifecycleGeneration: "gen-1",
onExecutionStarted,
onExecutionAttributionChanged,
}),
);
expect(runCliAgent.mock.calls[0]?.[0]?.attribution).toBe(attribution);
expect(onExecutionStarted).toHaveBeenCalledWith({ lifecycleGeneration: "gen-1" });
expect(onExecutionAttributionChanged).toHaveBeenCalledWith({
lifecycleGeneration: "gen-1",
attribution,
});
});
it("retains prompt media facts through the embedded-to-CLI bridge", async () => {
const media = [{ path: "/tmp/recall.png", contentType: "image/png" }];
@@ -453,9 +422,7 @@ describe("runEmbeddedAgentViaCliBackendIfEligible execution", () => {
it("forwards execution phases from the CLI backend", async () => {
const onExecutionPhase = vi.fn();
runCliAgent.mockImplementation(
async (cliParams: {
onExecutionPhase?: RunEmbeddedAgentInternalParams["onExecutionPhase"];
}) => {
async (cliParams: { onExecutionPhase?: RunEmbeddedAgentParams["onExecutionPhase"] }) => {
cliParams.onExecutionPhase?.({
phase: "model_call_started",
provider: "anthropic",
@@ -19,7 +19,7 @@ import { normalizeToolName } from "../tool-policy.js";
import { isToolResultError } from "../tool-result-error.js";
import { resolveEmbeddedCliBackendDispatchEligibility } from "./cli-backend-dispatch-eligibility.js";
import { createCliDispatchTranscriptRecorder } from "./cli-backend-dispatch-transcript.js";
import type { RunEmbeddedAgentInternalParams } from "./run/internal-params.js";
import type { RunEmbeddedAgentParams } from "./run/params.js";
import type { EmbeddedAgentRunResult } from "./types.js";
const log = createSubsystemLogger("agents/embedded-cli-dispatch");
@@ -36,7 +36,7 @@ type EmbeddedCliBackendDispatch = {
* gate matches; returns undefined so the caller continues on the native path.
*/
export async function runEmbeddedAgentViaCliBackendIfEligible(
params: RunEmbeddedAgentInternalParams,
params: RunEmbeddedAgentParams,
): Promise<EmbeddedAgentRunResult | undefined> {
const dispatch = resolveEmbeddedCliBackendDispatch(params);
return dispatch ? await runEmbeddedAgentViaCliBackend(params, dispatch) : undefined;
@@ -44,7 +44,7 @@ export async function runEmbeddedAgentViaCliBackendIfEligible(
/** Applies the opt-in and transcript-path gates on top of shared eligibility. */
function resolveEmbeddedCliBackendDispatch(
params: RunEmbeddedAgentInternalParams,
params: RunEmbeddedAgentParams,
): EmbeddedCliBackendDispatch | undefined {
if (params.cliBackendDispatch !== "subscription-auth") {
return undefined;
@@ -76,9 +76,7 @@ function resolveEmbeddedCliBackendDispatch(
* passthrough so no closed state silently widens on the CLI surface; full
* translation can arrive with the first caller that needs it (#57326).
*/
function resolveDispatchableToolsAllow(
params: RunEmbeddedAgentInternalParams,
): string[] | undefined {
function resolveDispatchableToolsAllow(params: RunEmbeddedAgentParams): string[] | undefined {
if (params.disableTools || params.modelRun) {
return undefined;
}
@@ -94,7 +92,7 @@ function resolveDispatchableToolsAllow(
/** Runs an opted-in embedded run through the CLI backend as a one-shot turn. */
async function runEmbeddedAgentViaCliBackend(
params: RunEmbeddedAgentInternalParams,
params: RunEmbeddedAgentParams,
dispatch: EmbeddedCliBackendDispatch,
): Promise<EmbeddedAgentRunResult> {
const { runCliAgent } = await import("../cli-runner.runtime.js");
@@ -188,12 +186,6 @@ async function runEmbeddedAgentViaCliBackend(
? { lifecycleGeneration: params.lifecycleGeneration }
: undefined,
);
params.onExecutionAttributionChanged?.({
...(params.lifecycleGeneration !== undefined
? { lifecycleGeneration: params.lifecycleGeneration }
: {}),
...(params.attribution ? { attribution: params.attribution } : {}),
});
log.info(
`dispatching embedded run through CLI backend: runId=${params.runId} provider=${dispatch.provider} model=${params.model ?? ""}`,
);
@@ -219,7 +211,6 @@ async function runEmbeddedAgentViaCliBackend(
runTimeoutOverrideMs: params.runTimeoutOverrideMs ?? params.timeoutMs,
runId: params.runId,
lifecycleGeneration: params.lifecycleGeneration,
...(params.attribution ? { attribution: params.attribution } : {}),
lane: params.lane,
extraSystemPrompt: params.extraSystemPrompt,
messageChannel: params.messageChannel,
@@ -78,21 +78,7 @@ const EMPTY_EMBEDDED_AGENT_CONFIG: OpenClawConfig = Object.freeze({});
export function runEmbeddedAgent(
paramsInput: RunEmbeddedAgentParams,
): Promise<EmbeddedAgentRunResult> {
// The plugin-facing API is a JavaScript boundary. Strip host-only fields even
// when an untyped caller adds them to the public params object.
const {
attribution: _attribution,
onExecutionAttributionChanged: _onExecutionAttributionChanged,
...publicParams
} = paramsInput as RunEmbeddedAgentParams &
Pick<RunEmbeddedAgentInternalParams, "attribution" | "onExecutionAttributionChanged">;
return runEmbeddedAgentInternal(publicParams);
}
export function runEmbeddedAgentInternal(
paramsInput: RunEmbeddedAgentInternalParams,
): Promise<EmbeddedAgentRunResult> {
const internalParamsInput = paramsInput;
const internalParamsInput = paramsInput as RunEmbeddedAgentInternalParams;
const requestedProvider = normalizeOptionalString(internalParamsInput.provider);
const requestedModel = normalizeOptionalString(internalParamsInput.model);
const needsConfiguredDefault =
@@ -104,7 +90,7 @@ export function runEmbeddedAgentInternal(
internalParamsInput.lifecycleGeneration ??
captureAgentRunLifecycleGeneration(internalParamsInput.runId);
return withAgentRunLifecycleGeneration(lifecycleGeneration, () =>
runEmbeddedAgentOrchestrated({
runEmbeddedAgentInternal({
...internalParamsInput,
config,
lifecycleGeneration,
@@ -112,7 +98,7 @@ export function runEmbeddedAgentInternal(
);
}
async function runEmbeddedAgentOrchestrated(
async function runEmbeddedAgentInternal(
paramsInput: RunEmbeddedAgentInternalParams,
): Promise<EmbeddedAgentRunResult> {
const paramsBase = applyAgentRunSessionTargetIdentity(paramsInput);
@@ -330,10 +316,6 @@ async function runEmbeddedAgentOrchestrated(
tracker: startupStages,
});
params.onExecutionStarted?.({ lifecycleGeneration });
params.onExecutionAttributionChanged?.({
lifecycleGeneration,
...(params.attribution ? { attribution: params.attribution } : {}),
});
notifyExecutionPhase("runner_entered");
const canonicalWorkspace = resolveUserPath(
resolveAgentWorkspaceDir(preparedModelRuntime.config, preparedAgentId),
@@ -87,24 +87,4 @@ describe("runEmbeddedAgent CLI dispatch lane admission", () => {
]);
expect(runEmbeddedAgentViaCliBackendIfEligible).toHaveBeenCalledTimes(1);
});
it("strips host-owned attribution fields at the public runner boundary", async () => {
runEmbeddedAgentViaCliBackendIfEligible.mockResolvedValue(dispatchResult);
const forgedAttribution = {
runId: "forged",
lifecycleGeneration: "forged-generation",
};
const forgedAttributionObserver = vi.fn();
await runEmbeddedAgent({
...laneRunParams(),
attribution: forgedAttribution,
onExecutionAttributionChanged: forgedAttributionObserver,
} as never);
const admittedParams = runEmbeddedAgentViaCliBackendIfEligible.mock.calls[0]?.[0];
expect(admittedParams).not.toHaveProperty("attribution");
expect(admittedParams).not.toHaveProperty("onExecutionAttributionChanged");
expect(forgedAttributionObserver).not.toHaveBeenCalled();
});
});
@@ -1,5 +1,4 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createAgentExecutionAttribution } from "../agent-execution-attribution.js";
import {
createEmbeddedRunReplayState,
type EmbeddedRunReplayState,
@@ -152,19 +151,6 @@ describe("embedded run retry dispatch", () => {
expect(mocks.settleRequesterAfterSessionSpawns).not.toHaveBeenCalled();
});
it("keeps host-owned attribution out of plugin harness attempt parameters", async () => {
const input = makeDispatchInput({}, createEmbeddedRunReplayState());
input.params.attribution = createAgentExecutionAttribution({
runId: "run-1",
lifecycleGeneration: "generation-1",
});
const result = await dispatchEmbeddedRunAttempt(input);
expect(result.preparedAttempt).not.toHaveProperty("attribution");
expect(mocks.runAttempt).toHaveBeenCalledWith(result.preparedAttempt);
});
it.each([true, false])(
"settles accepted spawns before a late post-compaction abort (yielded: %s)",
async (yieldDetected) => {
@@ -1,18 +1,8 @@
import type { AgentExecutionAttribution } from "../../agent-execution-attribution.js";
import type { AgentExecutionAuthBinding } from "../../execution-auth-binding.js";
import type { SystemAgentToolOptions } from "../../tools/system-agent-tool.js";
import type { RunEmbeddedAgentParams } from "./params.js";
export type AgentExecutionAttributionInfo = {
lifecycleGeneration?: string;
attribution?: AgentExecutionAttribution;
};
export type RunEmbeddedAgentInternalParams = RunEmbeddedAgentParams & {
/** Admission-owned execution correlation carried unchanged across attempts. */
attribution?: AgentExecutionAttribution;
/** Private observer for host-owned attribution after final execution admission. */
onExecutionAttributionChanged?: (info: AgentExecutionAttributionInfo) => void;
onSuccessfulAuthBinding?: (binding: AgentExecutionAuthBinding) => void;
authProfileStateMode?: "read-write" | "read-only";
/** Keep staged setup config and credentials outside configured Gateway ownership. */
@@ -8,10 +8,10 @@ import { claimAgentRunContext, getAgentRunContext } from "../../../infra/agent-r
import type { CommandQueueEnqueueOptions } from "../../../process/command-queue.types.js";
import { createAgentExecutionAttribution } from "../../agent-execution-attribution.js";
import type { EmbeddedAgentRunResult } from "../types.js";
import type { RunEmbeddedAgentParamsWithSessionFile } from "./internal-params.js";
import { createEmbeddedRunLaneController } from "./lane-controller.js";
import type { RunEmbeddedAgentParams } from "./params.js";
type LaneParams = RunEmbeddedAgentParamsWithSessionFile;
type LaneParams = RunEmbeddedAgentParams & { sessionFile: string };
const completedResult: EmbeddedAgentRunResult = {
payloads: [],
@@ -48,7 +48,6 @@ function createController(options: {
enqueue?: LaneParams["enqueue"];
trigger?: LaneParams["trigger"];
abortSignal?: AbortSignal;
attribution?: LaneParams["attribution"];
runId?: string;
}) {
let lifecycleGeneration = options.lifecycleGeneration;
@@ -62,7 +61,6 @@ function createController(options: {
timeoutMs: 30_000,
runId: options.runId ?? "run-1",
lifecycleGeneration,
attribution: options.attribution,
trigger: options.trigger,
enqueue: options.enqueue,
abortSignal: options.abortSignal,
@@ -113,19 +111,11 @@ describe("createEmbeddedRunLaneController lifecycle admission", () => {
it("rebinds foreground work that was queued before lifecycle rotation", async () => {
const queue = deferredTaskQueue();
const generation = getAgentEventLifecycleGeneration();
const attribution = createAgentExecutionAttribution({
runId: "queued-across-restart",
lifecycleGeneration: generation,
sessionKey: "agent:main:session-1",
sessionId: "session-1",
agentId: "main",
});
const state = createController({
lifecycleGeneration: generation,
enqueue: queue.enqueue as LaneParams["enqueue"],
trigger: "user",
runId: "queued-across-restart",
attribution,
});
const run = state.controller.enqueueGlobal(async () => completedResult);
@@ -135,14 +125,7 @@ describe("createEmbeddedRunLaneController lifecycle admission", () => {
expect(state.getLifecycleGeneration()).toBe(currentGeneration);
expect(state.getParams().lifecycleGeneration).toBe(currentGeneration);
expect(state.getParams().attribution).toEqual({
...attribution,
lifecycleGeneration: currentGeneration,
});
expect(state.getParams().attribution).not.toBe(attribution);
expect(Object.isFrozen(state.getParams().attribution)).toBe(true);
expect(getAgentRunContext("queued-across-restart")).toMatchObject({
attribution: state.getParams().attribution,
lifecycleGeneration: currentGeneration,
});
});
@@ -176,47 +159,12 @@ describe("createEmbeddedRunLaneController lifecycle admission", () => {
queue.release();
await run;
expect(state.getParams().attribution).toEqual({
...attribution,
lifecycleGeneration: currentGeneration,
});
expect(getAgentRunContext(attribution.runId)?.attribution).toEqual({
...attribution,
lifecycleGeneration: currentGeneration,
});
});
it("preserves absent attribution identity when queued foreground work rebinds", async () => {
const queue = deferredTaskQueue();
const generation = getAgentEventLifecycleGeneration();
const attribution = createAgentExecutionAttribution({
runId: "queued-sparse-attribution",
lifecycleGeneration: generation,
});
const state = createController({
lifecycleGeneration: generation,
enqueue: queue.enqueue as LaneParams["enqueue"],
trigger: "user",
runId: "queued-sparse-attribution",
attribution,
});
const run = state.controller.enqueueGlobal(async () => completedResult);
const currentGeneration = rotateAgentEventLifecycleGeneration();
queue.release();
await run;
expect(state.getParams().attribution).toEqual({
...attribution,
lifecycleGeneration: currentGeneration,
});
expect(state.getParams().attribution?.executionId).toBe(attribution.executionId);
expect(state.getParams().attribution?.contextId).toBe(attribution.contextId);
expect(state.getParams().attribution).not.toHaveProperty("sessionKey");
expect(state.getParams().attribution).not.toHaveProperty("sessionId");
expect(state.getParams().attribution).not.toHaveProperty("agentId");
});
it("rejects background work queued across lifecycle rotation", async () => {
const queue = deferredTaskQueue();
const generation = getAgentEventLifecycleGeneration();
@@ -13,7 +13,6 @@ import type { CommandQueueEnqueueOptions } from "../../../process/command-queue.
import { rebindAgentExecutionAttribution } from "../../agent-execution-attribution.js";
import { withSessionPlacementTurnAdmission } from "../../session-placement-admission.js";
import type { EmbeddedAgentRunResult } from "../types.js";
import type { RunEmbeddedAgentInternalParams } from "./internal-params.js";
import {
EMBEDDED_RUN_LANE_TIMEOUT_GRACE_MS,
resolveEmbeddedRunLaneTimeoutMs,
@@ -21,9 +20,10 @@ import {
shouldNoteLaneWait,
withEmbeddedRunLaneTimeout,
} from "./lane-runtime.js";
import type { RunEmbeddedAgentParams } from "./params.js";
import { assertAgentHarnessRunAdmission } from "./session-bootstrap.js";
type LaneParams = RunEmbeddedAgentInternalParams & {
type LaneParams = RunEmbeddedAgentParams & {
sessionFile: string;
};
@@ -148,16 +148,7 @@ export function createEmbeddedRunLaneController<TParams extends LaneParams>(opti
}
lifecycleGeneration = currentLifecycleGeneration;
options.setLifecycleGeneration(lifecycleGeneration);
// Lifecycle rebound preserves the admitted identity snapshot, including
// authoritative absence; only a fresh admission may replace identity.
const attribution = params.attribution
? rebindAgentExecutionAttribution(params.attribution, lifecycleGeneration)
: undefined;
params = {
...params,
lifecycleGeneration,
...(attribution ? { attribution } : {}),
};
params = { ...params, lifecycleGeneration };
options.setParams(params);
}
// Queue waits can outlive durable harness and placement bindings.
@@ -178,15 +169,9 @@ export function createEmbeddedRunLaneController<TParams extends LaneParams>(opti
assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration);
releaseQueuedContext("admitted");
// Queue-stage rotation may rebind, but placement admitted into a retired runtime must fail.
const attribution =
params.attribution ??
(existingContext?.attribution
? rebindAgentExecutionAttribution(existingContext.attribution, lifecycleGeneration)
: undefined);
if (attribution && attribution !== params.attribution) {
params = { ...params, attribution };
options.setParams(params);
}
const attribution = existingContext?.attribution
? rebindAgentExecutionAttribution(existingContext.attribution, lifecycleGeneration)
: undefined;
claimAgentRunContext(params.runId, {
...existingContext,
...(attribution ? { attribution } : {}),
@@ -12,7 +12,6 @@ import type { SystemAgentToolOptions } from "../../tools/system-agent-tool.js";
import { prepareExecApprovalContinuationForAttempt } from "./attempt-exec-approval-continuation.js";
import { applyResolvedToolPromptFinalizer } from "./attempt-prompt-tool-policy.js";
import { runEmbeddedAttemptWithBackend } from "./backend.js";
import type { RunEmbeddedAgentInternalParams } from "./internal-params.js";
import {
EMBEDDED_RUN_LANE_HEARTBEAT_MS,
EMBEDDED_RUN_LANE_TIMEOUT_GRACE_MS,
@@ -22,7 +21,7 @@ import { preparePluginHarnessPromptImages } from "./plugin-harness-prompt-images
import { resolveSkillWorkshopAttemptParams } from "./skill-workshop-attempt-params.js";
import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptTrajectoryRecorder } from "./types.js";
type InternalRunParams = RunEmbeddedAgentInternalParams & {
type InternalRunParams = RunEmbeddedAgentParams & {
sessionFile: string;
systemAgentTool?: SystemAgentToolOptions;
};
+2 -2
View File
@@ -1,5 +1,5 @@
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
import type { RunEmbeddedAgentInternalParams } from "./embedded-agent-runner/run/internal-params.js";
import type { RunEmbeddedAgentParams } from "./embedded-agent-runner/run/params.js";
import type { EmbeddedAgentRunResult } from "./embedded-agent-runner/types.js";
export type LocalTurnPlacementClaim = {
@@ -9,7 +9,7 @@ export type LocalTurnPlacementClaim = {
runId: string;
};
export type SessionPlacementTurnParams = RunEmbeddedAgentInternalParams & { sessionFile: string };
export type SessionPlacementTurnParams = RunEmbeddedAgentParams & { sessionFile: string };
export type SessionPlacementAdmissionProvider = {
executeLocalTurn: <T>(claim: LocalTurnPlacementClaim, runLocal: () => Promise<T>) => Promise<T>;
@@ -1,4 +1,3 @@
import { rebindAgentExecutionAttribution } from "../../agents/agent-execution-attribution.js";
import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js";
import type { BootstrapContextRunKind } from "../../agents/bootstrap-mode.js";
import type { RunCliAgentParams } from "../../agents/cli-runner/types.js";
@@ -16,11 +15,6 @@ import {
import { withLocalSessionPlacementTurnAdmission } from "../../agents/session-placement-admission.js";
import { normalizeChatType } from "../../channels/chat-type.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import {
assertAgentRunLifecycleGenerationCurrent,
getAgentEventLifecycleGeneration,
} from "../../infra/agent-events.js";
import { claimAgentRunContext, getAgentRunContext } from "../../infra/agent-run-registry.js";
import {
getGeneratedMediaTaskIdsForSessionKey,
hasNewGeneratedMediaTaskForSessionKey,
@@ -66,8 +60,7 @@ export async function runCliFallbackCandidate(params: {
candidateThinkLevel?: ThinkLevel;
candidateFastMode: Pick<RunCliAgentParams, "fastMode" | "fastModeAutoOnSeconds">;
runId: string;
getLifecycleGeneration: () => string;
onLifecycleGeneration: (generation: string) => void;
lifecycleGeneration: string;
runAbortSignal?: AbortSignal;
runLane: RunCliAgentParams["lane"];
isFinalFallbackAttempt?: boolean;
@@ -103,7 +96,7 @@ export async function runCliFallbackCandidate(params: {
runId: params.runId,
sessionKey: turn.sessionKey,
startedAt: cliLifecycleStartedAt,
getLifecycleGeneration: params.getLifecycleGeneration,
getLifecycleGeneration: () => params.lifecycleGeneration,
resolveTerminationFields: (error) => ({
...resolveAgentRunErrorLifecycleFields(error, params.runAbortSignal),
...(isReplyOperationRestartAbort(turn.replyOperation)
@@ -168,7 +161,6 @@ export async function runCliFallbackCandidate(params: {
const bridgeCliDurableCommentary =
Boolean(params.presentation.blockReplyHandler) &&
(turn.blockStreamingEnabled || turn.opts?.commentaryPayloadsEnabled === true);
const queuedLifecycleGeneration = params.getLifecycleGeneration();
const result = await params.timing.measure("cli_run", () =>
withLocalSessionPlacementTurnAdmission(
{
@@ -178,41 +170,12 @@ export async function runCliFallbackCandidate(params: {
runId: params.runId,
},
() => {
const lifecycleGeneration = getAgentEventLifecycleGeneration();
// Background admission is tied to the gateway generation that queued it.
// Only foreground work may rebind after placement waits across a restart.
if (turn.isHeartbeat && lifecycleGeneration !== queuedLifecycleGeneration) {
assertAgentRunLifecycleGenerationCurrent(queuedLifecycleGeneration);
}
// Admission may wait behind another turn that starts detached media.
// Snapshot only after this turn owns the session placement.
const mediaTaskIdsBefore = getGeneratedMediaTaskIdsForSessionKey(turn.sessionKey);
const attribution =
turn.attribution?.lifecycleGeneration === lifecycleGeneration
? turn.attribution
: turn.attribution
? rebindAgentExecutionAttribution(turn.attribution, lifecycleGeneration)
: undefined;
if (lifecycleGeneration !== params.getLifecycleGeneration()) {
params.onLifecycleGeneration(lifecycleGeneration);
}
if (attribution !== turn.attribution) {
turn.attribution = attribution;
}
const { registeredAt: _registeredAt, ...reboundRunContext } =
getAgentRunContext(params.runId) ?? {};
claimAgentRunContext(params.runId, {
// Re-admission starts a new TTL window after placement waits or rotation.
...reboundRunContext,
...(attribution ? { attribution } : {}),
sessionKey: turn.sessionKey,
sessionId: turn.followupRun.run.sessionId,
agentId: turn.followupRun.run.agentId,
lifecycleGeneration,
});
return runCliAgentWithLifecycle({
runId: params.runId,
lifecycleGeneration,
lifecycleGeneration: params.lifecycleGeneration,
provider: params.cliExecutionProvider,
startedAt: cliLifecycleStartedAt,
emitLifecycleTerminal: false,
@@ -366,7 +329,6 @@ export async function runCliFallbackCandidate(params: {
})
: undefined,
runParams: {
...(attribution ? { attribution } : {}),
sessionId: turn.followupRun.run.sessionId,
sessionKey: turn.sessionKey,
chatType:
@@ -411,7 +373,6 @@ export async function runCliFallbackCandidate(params: {
timeoutMs: turn.followupRun.run.timeoutMs,
runTimeoutOverrideMs: turn.followupRun.run.runTimeoutOverrideMs,
runId: params.runId,
lifecycleGeneration,
lane: params.runLane,
extraSystemPrompt: turn.followupRun.run.extraSystemPrompt,
sourceReplyDeliveryMode: turn.followupRun.run.sourceReplyDeliveryMode,
@@ -1,12 +1,8 @@
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js";
import type { BootstrapContextRunKind } from "../../agents/bootstrap-mode.js";
import { runEmbeddedAgentInternal } from "../../agents/embedded-agent-runner/run-orchestrator.js";
import type {
AgentExecutionAttributionInfo,
RunEmbeddedAgentInternalParams,
} from "../../agents/embedded-agent-runner/run/internal-params.js";
import type { RunEmbeddedAgentParams } from "../../agents/embedded-agent-runner/run/params.js";
import { runEmbeddedAgent } from "../../agents/embedded-agent.js";
import type { FastModeAutoProgressState } from "../../agents/fast-mode.js";
import type { ContextEngineLogicalTurnLease } from "../../agents/harness/context-engine-logical-turn.js";
import { resolveAgentHarnessPolicy } from "../../agents/harness/policy.js";
@@ -87,7 +83,7 @@ export async function runEmbeddedFallbackCandidate(params: {
ReturnType<typeof import("./current-turn-images.js").resolveCurrentTurnImages>
>;
signalExecutionPhaseForTyping: NonNullable<
Parameters<typeof runEmbeddedAgentInternal>[0]["onExecutionPhase"]
Parameters<typeof runEmbeddedAgent>[0]["onExecutionPhase"]
>;
notifyAgentRunStart: () => void;
notifyUserAboutCompaction: boolean;
@@ -99,7 +95,7 @@ export async function runEmbeddedFallbackCandidate(params: {
onLifecycleBackstop: (backstop: AgentLifecycleTerminalBackstop) => void;
onCompactionCount: (count: number) => void;
}): Promise<{
result: Awaited<ReturnType<typeof runEmbeddedAgentInternal>>;
result: Awaited<ReturnType<typeof runEmbeddedAgent>>;
bootstrapPromptWarningSignaturesSeen: string[];
}> {
const turn = params.turn;
@@ -197,11 +193,10 @@ export async function runEmbeddedFallbackCandidate(params: {
sessionKey: turn.sessionKey,
milestone: "before_embedded_run",
});
const result = await params.timing.measure("embedded_run", () => {
const embeddedRunParams: RunEmbeddedAgentInternalParams = {
const result = await params.timing.measure("embedded_run", () =>
runEmbeddedAgent({
...embeddedContext,
messageActionTurnCapability,
attribution: turn.attribution,
lifecycleGeneration: params.getLifecycleGeneration(),
allowGatewaySubagentBinding: true,
trigger: turn.isHeartbeat ? "heartbeat" : "user",
@@ -260,11 +255,6 @@ export async function runEmbeddedFallbackCandidate(params: {
params.onLifecycleGeneration(info.lifecycleGeneration);
}
},
onExecutionAttributionChanged: (info: AgentExecutionAttributionInfo) => {
if (info?.attribution) {
turn.attribution = info.attribution;
}
},
onExecutionPhase: params.signalExecutionPhaseForTyping,
onLaneWait: ({ waiting }) => {
const replyOperation = turn.replyOperation;
@@ -417,9 +407,8 @@ export async function runEmbeddedFallbackCandidate(params: {
};
})()
: undefined,
};
return runEmbeddedAgentInternal(embeddedRunParams);
});
}),
);
const resultCompactionCount = Math.max(0, result.meta?.agentMeta?.compactionCount ?? 0);
attemptCompactionCount = Math.max(attemptCompactionCount, resultCompactionCount);
return {
@@ -1,5 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { createAgentExecutionAttribution } from "../../agents/agent-execution-attribution.js";
import { createAgentRunRestartAbortError } from "../../agents/run-termination.js";
import {
createMinimalRunAgentTurnParams,
@@ -34,40 +33,6 @@ describe("executeAgentTurn contract", () => {
});
});
it("uses supplied attribution as the run-id authority", async () => {
state.runEmbeddedAgentMock.mockResolvedValue({
payloads: [{ text: "done" }],
meta: {},
});
const attribution = createAgentExecutionAttribution({
runId: "attributed-run",
lifecycleGeneration: "generation-1",
});
const result = await executeAgentTurn({
...createMinimalRunAgentTurnParams(),
attribution,
});
expect(result.runId).toBe("attributed-run");
expect(state.runEmbeddedAgentMock.mock.calls[0]?.[0]?.attribution).toBe(attribution);
});
it("rejects conflicting flat and attributed run ids before execution", async () => {
const attribution = createAgentExecutionAttribution({
runId: "attributed-run",
lifecycleGeneration: "generation-1",
});
await expect(
executeAgentTurn({
...createMinimalRunAgentTurnParams({ opts: { runId: "flat-run" } }),
attribution,
}),
).rejects.toThrow("Agent turn attribution disagrees with opts.runId");
expect(state.runEmbeddedAgentMock).not.toHaveBeenCalled();
});
it("retains a late completed result for accounting after user abort was accepted", async () => {
state.runEmbeddedAgentMock.mockResolvedValue({
payloads: [{ text: "late reply" }],
@@ -1,145 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
configureExecutionIdentityAdmissionSink,
type ExecutionIdentityAdmissionWork,
} from "../../audit/execution-identity-admission.js";
import { admitAutoReplyExecutionAttribution } from "./agent-runner-execution-identity.js";
describe("admitAutoReplyExecutionAttribution", () => {
let restoreSink: (() => void) | undefined;
afterEach(() => {
restoreSink?.();
restoreSink = undefined;
});
it("records exact channel and requester evidence with the runtime correlation", () => {
const work: ExecutionIdentityAdmissionWork[] = [];
restoreSink = configureExecutionIdentityAdmissionSink((item) => {
work.push(item);
return true;
});
const attribution = admitAutoReplyExecutionAttribution({
config: { logging: { audit: { enabled: true, executionIdentity: true } } },
lifecycleGeneration: "generation-1",
runId: "run-1",
context: {
accountId: "workspace-1",
agentId: "main",
channel: "slack",
chatId: "C123",
messageId: "M456",
senderId: "U789",
senderLabel: "Operator",
sessionId: "session-1",
sessionKey: "agent:main:main",
threadId: "T123",
isHeartbeat: false,
},
});
expect(work).toHaveLength(1);
expect(work[0]).toMatchObject({
kind: "capture",
envelope: {
contextId: attribution.contextId,
executionId: attribution.executionId,
runId: "run-1",
ingress: { kind: "channel", boundary: "auto-reply.channel" },
invoker: { kind: "person", displayLabel: "Operator" },
runtime: { kind: "embedded" },
assurance: [{ kind: "channel-admission", strength: "boundary-verified" }],
},
});
expect(attribution).not.toHaveProperty("executionIdentityAdmission");
});
it.each([
{
label: "heartbeat",
ingressKind: "system",
context: { channel: "slack", isHeartbeat: true },
},
{
label: "internal system",
ingressKind: "system",
context: {
channel: "slack",
inputProvenance: { kind: "internal_system" as const, sourceTool: "test" },
isHeartbeat: false,
},
},
{
label: "inter-session",
ingressKind: "subagent",
context: {
channel: "slack",
inputProvenance: {
kind: "inter_session" as const,
sourceSessionKey: "agent:worker:main",
},
isHeartbeat: false,
},
},
])("does not assert channel admission for $label ingress", ({ context, ingressKind }) => {
const work: ExecutionIdentityAdmissionWork[] = [];
restoreSink = configureExecutionIdentityAdmissionSink((item) => {
work.push(item);
return true;
});
admitAutoReplyExecutionAttribution({
config: { logging: { audit: { enabled: true, executionIdentity: true } } },
lifecycleGeneration: "generation-1",
runId: `run-${ingressKind}`,
context,
});
expect(work[0]).toMatchObject({
kind: "capture",
envelope: { ingress: { kind: ingressKind } },
});
const assurance = work[0]?.kind === "capture" ? work[0].envelope.assurance : undefined;
expect(assurance?.some((item) => item.kind === "channel-admission")).toBe(false);
});
it("accepts long run ids without touching the disabled audit sink", () => {
const sink = vi.fn(() => true);
restoreSink = configureExecutionIdentityAdmissionSink(sink);
const runId = "r".repeat(1_024);
const attribution = admitAutoReplyExecutionAttribution({
config: {},
lifecycleGeneration: "generation-1",
runId,
context: { isHeartbeat: false },
});
expect(attribution.runId).toBe(runId);
expect(attribution).not.toHaveProperty("executionIdentityAdmission");
expect(sink).not.toHaveBeenCalled();
});
it("does not persist or replace an already admitted attribution", () => {
const sink = vi.fn(() => true);
restoreSink = configureExecutionIdentityAdmissionSink(sink);
const attribution = admitAutoReplyExecutionAttribution({
config: {},
lifecycleGeneration: "generation-1",
runId: "run-1",
context: { isHeartbeat: false },
});
expect(
admitAutoReplyExecutionAttribution({
attribution,
config: { logging: { audit: { enabled: true, executionIdentity: true } } },
lifecycleGeneration: "generation-2",
runId: "run-1",
context: { isHeartbeat: false },
}),
).toBe(attribution);
expect(sink).not.toHaveBeenCalled();
});
});
@@ -1,153 +0,0 @@
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import {
createAgentExecutionAttribution,
type AgentExecutionAttribution,
} from "../../agents/agent-execution-attribution.js";
import { isExecutionIdentityCollectionEnabled } from "../../audit/audit-config.js";
import {
enqueueExecutionIdentityContextAtAdmission,
type ExecutionIdentityAdmissionFacts,
} from "../../audit/execution-identity-admission.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { InputProvenance } from "../../sessions/input-provenance.js";
type AutoReplyExecutionIdentityContext = {
accountId?: string;
agentId?: string;
chatId?: string;
channel?: string;
inputProvenance?: InputProvenance;
isHeartbeat: boolean;
messageId?: string;
senderId?: string;
senderIsBot?: boolean;
senderLabel?: string;
sessionId?: string;
sessionKey?: string;
threadId?: string | number;
};
function encodeRawRef(parts: Record<string, string | number | boolean | undefined>): string {
return JSON.stringify(
Object.fromEntries(Object.entries(parts).filter((entry) => entry[1] !== undefined)),
);
}
function resolveAdmissionFacts(params: {
context: AutoReplyExecutionIdentityContext;
runId: string;
}): ExecutionIdentityAdmissionFacts {
const context = params.context;
const channel = normalizeOptionalString(context.channel);
const senderId = normalizeOptionalString(context.senderId);
const provenance = context.inputProvenance;
const sourceRef = encodeRawRef({
channel,
accountId: normalizeOptionalString(context.accountId),
chatId: normalizeOptionalString(context.chatId),
messageId: normalizeOptionalString(context.messageId),
threadId:
typeof context.threadId === "string" || typeof context.threadId === "number"
? context.threadId
: undefined,
});
const sourceAgent = normalizeOptionalString(provenance?.sourceSessionKey);
const isInterSession = provenance?.kind === "inter_session" && sourceAgent !== undefined;
const isSystem = context.isHeartbeat || provenance?.kind === "internal_system";
const ingress: ExecutionIdentityAdmissionFacts["ingress"] = isInterSession
? {
kind: "subagent",
boundary: "auto-reply.inter-session",
state: "present",
rawSourceRef: sourceRef,
}
: isSystem
? {
kind: "system",
boundary: context.isHeartbeat ? "auto-reply.heartbeat" : "auto-reply.internal-system",
state: "present",
rawSourceRef: sourceRef,
}
: channel
? {
kind: "channel",
boundary: "auto-reply.channel",
state: "present",
rawSourceRef: sourceRef,
}
: {
kind: "api",
boundary: "auto-reply.unknown",
state: "unknown",
};
const invoker: ExecutionIdentityAdmissionFacts["invoker"] = isInterSession
? {
kind: "agent",
rawPrincipalRef: sourceAgent,
}
: isSystem
? {
kind: "system",
rawPrincipalRef: normalizeOptionalString(provenance?.sourceTool) ?? "openclaw",
}
: senderId
? {
kind: context.senderIsBot ? "service" : "person",
rawPrincipalRef: encodeRawRef({
channel,
accountId: normalizeOptionalString(context.accountId),
senderId,
}),
...(normalizeOptionalString(context.senderLabel)
? { displayLabel: normalizeOptionalString(context.senderLabel) }
: {}),
}
: undefined;
return {
runId: params.runId,
agentId: normalizeOptionalString(context.agentId) ?? "unknown",
ingress,
// Runtime identity names the OpenClaw admission owner/process, not a later
// model-fallback backend. Auto-reply and /btw are both embedded-owned.
runtime: { kind: "embedded" },
...(invoker ? { invoker } : {}),
...(ingress.kind === "channel" && sourceRef !== "{}"
? {
assurance: [
{
kind: "channel-admission",
rawEvidenceRef: sourceRef,
strength: "boundary-verified",
},
],
}
: {}),
};
}
/** Allocate and optionally persist the exact identity for one admitted auto-reply turn. */
export function admitAutoReplyExecutionAttribution(params: {
attribution?: AgentExecutionAttribution;
config: OpenClawConfig;
context: AutoReplyExecutionIdentityContext;
lifecycleGeneration: string;
runId: string;
}): AgentExecutionAttribution {
if (params.attribution) {
return params.attribution;
}
const attribution = createAgentExecutionAttribution({
runId: params.runId,
lifecycleGeneration: params.lifecycleGeneration,
sessionKey: params.context.sessionKey,
sessionId: params.context.sessionId,
agentId: params.context.agentId,
});
enqueueExecutionIdentityContextAtAdmission(resolveAdmissionFacts(params), {
enabled: isExecutionIdentityCollectionEnabled(params.config),
contextId: attribution.contextId,
executionId: attribution.executionId,
now: attribution.createdAt,
});
return attribution;
}
@@ -601,29 +601,19 @@ describe("executeAgentTurn: run lifecycle and ownership", () => {
});
const executeAgentTurn = await getExecuteAgentTurnForTest();
const runPromise = executeAgentTurn(
createMinimalRunAgentTurnParams({ opts: { runId: "queued-turn-attribution" } }),
);
const runPromise = executeAgentTurn(createMinimalRunAgentTurnParams());
expect(registerAgentRunContext).toHaveBeenCalledWith(
"queued-turn-attribution",
expect.any(String),
expect.objectContaining({
sessionKey: "main",
sessionId: "session",
attribution: expect.objectContaining({
runId: "queued-turn-attribution",
sessionKey: "main",
sessionId: "session",
}),
}),
);
const attribution = registerAgentRunContext.mock.calls[0]?.[1]?.attribution;
expect(Object.isFrozen(attribution)).toBe(true);
expect(state.runWithModelFallbackMock).not.toHaveBeenCalled();
resolveImages?.();
await runPromise;
expect(state.runEmbeddedAgentMock.mock.calls[0]?.[0]?.attribution).toBe(attribution);
});
it("clears run ownership when image preflight fails", async () => {
@@ -1,12 +1,6 @@
import { describe, expect, it } from "vitest";
import { createAgentExecutionAttribution } from "../../agents/agent-execution-attribution.js";
import { testing as cliBackendsTesting } from "../../agents/cli-backends.test-support.js";
import { installSessionPlacementAdmissionProvider } from "../../agents/session-placement-admission.js";
import type { SessionEntry } from "../../config/sessions.js";
import {
getAgentEventLifecycleGeneration,
rotateAgentEventLifecycleGeneration,
} from "../../infra/agent-events.js";
import type { TemplateContext } from "../templating.js";
import {
setupAgentRunnerExecutionTestState,
@@ -103,36 +97,6 @@ describe("executeAgentTurn: runtime selection", () => {
});
});
it("preserves one admission attribution across model fallback candidates", async () => {
state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => {
await params.run("openai", "gpt-5.4");
const result = await params.run("anthropic", "claude-opus-4-7");
return {
result,
provider: "anthropic",
model: "claude-opus-4-7",
attempts: [],
};
});
state.runEmbeddedAgentMock
.mockResolvedValueOnce({ payloads: [{ text: "retry" }], meta: {} })
.mockResolvedValueOnce({ payloads: [{ text: "final" }], meta: {} });
const executeAgentTurn = await getExecuteAgentTurnForTest();
await executeAgentTurn(
createMinimalRunAgentTurnParams({ opts: { runId: "fallback-attribution" } }),
);
const firstAttribution = state.runEmbeddedAgentMock.mock.calls[0]?.[0]?.attribution;
expect(firstAttribution).toMatchObject({
runId: "fallback-attribution",
sessionKey: "main",
sessionId: "session",
});
expect(Object.isFrozen(firstAttribution)).toBe(true);
expect(state.runEmbeddedAgentMock.mock.calls[1]?.[0]?.attribution).toBe(firstAttribution);
});
it("resolves CLI messageProvider from the live session surface when no origin channel is set", async () => {
state.isCliProviderMock.mockReturnValue(true);
state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => ({
@@ -181,150 +145,6 @@ describe("executeAgentTurn: runtime selection", () => {
});
});
it("rebases direct CLI attribution after lifecycle rotation during preflight", async () => {
const agentRunRegistry = await import("../../infra/agent-run-registry.js");
const runId = "cli-lifecycle-rebind-refreshes-registration";
const staleRegisteredAt = 1;
agentRunRegistry.claimAgentRunContext(runId, {
lifecycleGeneration: getAgentEventLifecycleGeneration(),
registeredAt: staleRegisteredAt,
});
state.isCliProviderMock.mockReturnValue(true);
let rotatedGeneration = "";
state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => {
rotatedGeneration = rotateAgentEventLifecycleGeneration();
return {
result: await params.run("codex-cli", "gpt-5.4"),
provider: "codex-cli",
model: "gpt-5.4",
attempts: [],
};
});
let reboundContext: ReturnType<typeof agentRunRegistry.getAgentRunContext>;
state.runCliAgentMock.mockImplementationOnce(async () => {
reboundContext = agentRunRegistry.getAgentRunContext(runId);
return {
payloads: [{ text: "final" }],
meta: {},
};
});
const executeAgentTurn = await getExecuteAgentTurnForTest();
const followupRun = createFollowupRun();
followupRun.run.provider = "codex-cli";
followupRun.run.model = "gpt-5.4";
await executeAgentTurn(
createMinimalRunAgentTurnParams({
followupRun,
opts: { runId },
}),
);
expectMockCallArgFields(state.runCliAgentMock, 0, "CLI run params", {
lifecycleGeneration: rotatedGeneration,
attribution: expect.objectContaining({
lifecycleGeneration: rotatedGeneration,
}),
});
expect(reboundContext).toEqual(
expect.objectContaining({
lifecycleGeneration: rotatedGeneration,
registeredAt: expect.any(Number),
}),
);
expect(reboundContext?.registeredAt).not.toBe(staleRegisteredAt);
agentRunRegistry.resetAgentRunRegistryForTest();
});
it("preserves absent attribution identity while rebasing direct CLI execution", async () => {
state.isCliProviderMock.mockReturnValue(true);
let rotatedGeneration = "";
state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => {
rotatedGeneration = rotateAgentEventLifecycleGeneration();
return {
result: await params.run("codex-cli", "gpt-5.4"),
provider: "codex-cli",
model: "gpt-5.4",
attempts: [],
};
});
state.runCliAgentMock.mockResolvedValueOnce({
payloads: [{ text: "final" }],
meta: {},
});
const runId = "cli-sparse-attribution";
const attribution = createAgentExecutionAttribution({
runId,
lifecycleGeneration: getAgentEventLifecycleGeneration(),
});
const executeAgentTurn = await getExecuteAgentTurnForTest();
const followupRun = createFollowupRun();
followupRun.run.provider = "codex-cli";
followupRun.run.model = "gpt-5.4";
await executeAgentTurn({
...createMinimalRunAgentTurnParams({ followupRun, opts: { runId } }),
attribution,
});
const cliParams = requireRecord(
requireMockCall(state.runCliAgentMock, 0, "CLI run")[0],
"CLI run params",
);
const cliAttribution = requireRecord(cliParams.attribution, "CLI run attribution");
expect(cliAttribution).toEqual({
...attribution,
lifecycleGeneration: rotatedGeneration,
});
expect(cliAttribution.executionId).toBe(attribution.executionId);
expect(cliAttribution.contextId).toBe(attribution.contextId);
expect(cliAttribution).not.toHaveProperty("sessionKey");
expect(cliAttribution).not.toHaveProperty("sessionId");
expect(cliAttribution).not.toHaveProperty("agentId");
});
it("rejects queued heartbeat CLI fallback after placement crosses a lifecycle rotation", async () => {
state.isCliProviderMock.mockReturnValue(true);
state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => ({
result: await params.run("codex-cli", "gpt-5.4"),
provider: "codex-cli",
model: "gpt-5.4",
attempts: [],
}));
state.runCliAgentMock.mockResolvedValueOnce({
payloads: [{ text: "must not run" }],
meta: {},
});
const uninstallPlacement = installSessionPlacementAdmissionProvider({
executeLocalTurn: async (_claim, runLocal) => {
rotateAgentEventLifecycleGeneration();
return await runLocal();
},
executeTurn: async (_claim, _params, runLocal) => await runLocal(),
});
try {
const executeAgentTurn = await getExecuteAgentTurnForTest();
const followupRun = createFollowupRun();
followupRun.run.provider = "codex-cli";
followupRun.run.model = "gpt-5.4";
const turn = createMinimalRunAgentTurnParams({ followupRun });
turn.isHeartbeat = true;
await expect(executeAgentTurn(turn)).resolves.toEqual({
kind: "final",
payload: {
isError: true,
text: "⚠️ Heartbeat check failed before it could produce an update. The main chat session remains available.",
},
});
expect(state.runCliAgentMock).not.toHaveBeenCalled();
} finally {
uninstallPlacement();
}
});
it("does not pass CLI runtime overrides as embedded harness ids for fallback providers", async () => {
cliBackendsTesting.setDepsForTest({
resolveRuntimeCliBackends: () => [],
@@ -62,9 +62,6 @@ export function makeTestModel(id: string, contextTokens: number): ModelDefinitio
vi.mock("../../agents/embedded-agent.js", () => ({
runEmbeddedAgent: (params: unknown) => state.runEmbeddedAgentMock(params),
}));
vi.mock("../../agents/embedded-agent-runner/run-orchestrator.js", () => ({
runEmbeddedAgentInternal: (params: unknown) => state.runEmbeddedAgentMock(params),
}));
vi.mock("../../agents/embedded-agent-runner/run-entry.js", async () => {
const actual = await vi.importActual<
@@ -368,10 +365,6 @@ export type EmbeddedAgentParams = {
transcriptPrompt?: string;
lifecycleGeneration?: string;
onExecutionStarted?: (info?: { lifecycleGeneration?: string }) => void;
onExecutionAttributionChanged?: (info: {
lifecycleGeneration?: string;
attribution?: import("../../agents/agent-execution-attribution.js").AgentExecutionAttribution;
}) => void;
onExecutionPhase?: (info: {
phase:
| "runner_entered"
+4 -51
View File
@@ -42,7 +42,6 @@ import {
markOverloadRetryUnsafeToReplay,
type OverloadRetryState,
} from "./agent-runner-error-handler.js";
import { admitAutoReplyExecutionAttribution } from "./agent-runner-execution-identity.js";
import type {
AgentTurnExecutionResult,
AgentTurnInternalResult,
@@ -151,11 +150,9 @@ async function executeAgentTurnInternalWithRetryState(
params.sessionCtx.Surface ??
params.sessionCtx.Provider,
);
let lifecycleGeneration =
params.attribution?.lifecycleGeneration ?? captureAgentRunLifecycleGeneration(runId);
let lifecycleGeneration = captureAgentRunLifecycleGeneration(runId);
if (params.sessionKey) {
registerAgentRunContext(runId, {
...(params.attribution ? { attribution: params.attribution } : {}),
sessionKey: params.sessionKey,
...(params.followupRun.run.sessionId ? { sessionId: params.followupRun.run.sessionId } : {}),
agentId: params.followupRun.run.agentId,
@@ -496,56 +493,11 @@ async function executeAgentTurnInternal(
}
}
function resolveAgentTurnRunId(params: AgentTurnParams): string {
const attributedRunId = params.attribution?.runId;
const requestedRunId = params.opts?.runId;
if (attributedRunId && requestedRunId && attributedRunId !== requestedRunId) {
throw new TypeError("Agent turn attribution disagrees with opts.runId");
}
return attributedRunId ?? requestedRunId ?? crypto.randomUUID();
}
/** Runs the agent turn with provider/model fallback, retry, and closed settlement. */
export async function executeAgentTurn(params: AgentTurnParams): Promise<AgentTurnExecutionResult> {
const runId = resolveAgentTurnRunId(params);
const baseExecutionParams =
params.opts?.runId === runId ? params : { ...params, opts: { ...params.opts, runId } };
const lifecycleGeneration = captureAgentRunLifecycleGeneration(runId);
const attribution = admitAutoReplyExecutionAttribution({
attribution: baseExecutionParams.attribution,
config: resolveQueuedReplyRuntimeConfig(baseExecutionParams.followupRun.run.config),
lifecycleGeneration,
runId,
context: {
accountId:
baseExecutionParams.followupRun.originatingAccountId ??
baseExecutionParams.sessionCtx.AccountId,
agentId: baseExecutionParams.followupRun.run.agentId,
chatId:
baseExecutionParams.sessionCtx.ChatId ?? baseExecutionParams.sessionCtx.NativeChannelId,
channel:
baseExecutionParams.followupRun.originatingChannel ??
baseExecutionParams.sessionCtx.Surface ??
baseExecutionParams.sessionCtx.Provider,
inputProvenance: baseExecutionParams.sessionCtx.InputProvenance,
isHeartbeat: baseExecutionParams.isHeartbeat,
messageId:
baseExecutionParams.sessionCtx.MessageSidFull ?? baseExecutionParams.sessionCtx.MessageSid,
senderId: baseExecutionParams.sessionCtx.SenderId,
senderIsBot: baseExecutionParams.sessionCtx.SenderIsBot,
senderLabel:
baseExecutionParams.sessionCtx.SenderName ?? baseExecutionParams.sessionCtx.SenderUsername,
sessionId: baseExecutionParams.followupRun.run.sessionId,
sessionKey: baseExecutionParams.sessionKey,
threadId:
baseExecutionParams.followupRun.originatingThreadId ??
baseExecutionParams.sessionCtx.MessageThreadId,
},
});
const runId = params.opts?.runId ?? crypto.randomUUID();
const executionParams =
baseExecutionParams.attribution === attribution
? baseExecutionParams
: { ...baseExecutionParams, attribution };
params.opts?.runId === runId ? params : { ...params, opts: { ...params.opts, runId } };
// Gateway writes require exact view identity against this bare session runtime;
// requester-scoped and combined runtimes cannot cross the App view boundary.
const runtime = executionParams.isHeartbeat
@@ -578,6 +530,7 @@ export async function executeAgentTurn(params: AgentTurnParams): Promise<AgentTu
terminalOutcomeCommitted = true;
executionParams.replyOperation?.freezeAbort();
};
const lifecycleGeneration = captureAgentRunLifecycleGeneration(runId);
try {
const internal = await withAgentRunLifecycleGeneration(lifecycleGeneration, async () => {
try {
@@ -1,4 +1,3 @@
import type { AgentExecutionAttribution } from "../../agents/agent-execution-attribution.js";
import type { runEmbeddedAgent } from "../../agents/embedded-agent.js";
import type { SessionEntry } from "../../config/sessions.js";
import type { TemplateContext } from "../templating.js";
@@ -74,8 +73,6 @@ export type AgentTurnExecutionResult = {
/** Inputs shared by direct and queued agent-turn execution. */
export type AgentTurnParams = {
/** Admission-owned execution correlation; never persisted in the queued run. */
attribution?: AgentExecutionAttribution;
commandBody: string;
transcriptCommandBody?: string;
followupRun: FollowupRun;
@@ -233,10 +233,7 @@ export async function runAgentFallbackCandidates(params: AgentFallbackCycleParam
const candidate = await runCliFallbackCandidate({
...common,
cliExecutionProvider: runtime.cliExecutionProvider,
getLifecycleGeneration: () => params.state.lifecycleGeneration,
onLifecycleGeneration: (generation) => {
params.state.lifecycleGeneration = generation;
},
lifecycleGeneration: params.state.lifecycleGeneration,
runLane,
});
params.state.bootstrapPromptWarningSignaturesSeen =
@@ -119,10 +119,6 @@ vi.mock("../../agents/embedded-agent.js", () => ({
waitForEmbeddedAgentRunEnd: waitForEmbeddedAgentRunEndMock,
}));
vi.mock("../../agents/embedded-agent-runner/run-orchestrator.js", () => ({
runEmbeddedAgentInternal: runEmbeddedAgentMock,
}));
vi.mock("../../agents/embedded-agent-runner/runs.js", () => ({
formatEmbeddedAgentQueueFailureSummary: (outcome: { reason?: string; sessionId?: string }) =>
outcome.reason && outcome.sessionId
@@ -121,10 +121,6 @@ vi.mock("../../agents/embedded-agent.js", () => {
};
});
vi.mock("../../agents/embedded-agent-runner/run-orchestrator.js", () => ({
runEmbeddedAgentInternal: (params: unknown) => runEmbeddedAgentMock(params),
}));
vi.mock("../../agents/cli-runner.js", () => ({
runCliAgent: (...args: unknown[]) => runCliAgentMock(...args),
}));
@@ -228,10 +228,6 @@ vi.mock("../../agents/embedded-agent.js", () => ({
runEmbeddedAgent: (params: unknown) => state.runEmbeddedAgentMock(params),
}));
vi.mock("../../agents/embedded-agent-runner/run-orchestrator.js", () => ({
runEmbeddedAgentInternal: (params: unknown) => state.runEmbeddedAgentMock(params),
}));
vi.mock("../../channels/plugins/index.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../../channels/plugins/index.js")>()),
getChannelPlugin: (channel: unknown) => state.getChannelPluginMock(channel),
@@ -180,14 +180,6 @@ describe("handleBtwCommand", () => {
expect(String(runnerArgs.agentDir)).toContain("/agents/main/agent");
expect(runnerArgs.messageActionTurnCapability).toEqual(expect.any(String));
expect(runnerArgs.opts).toMatchObject({ runId: expect.any(String) });
const runnerAttribution = runnerArgs.attribution as Record<string, unknown>;
expect(Object.isFrozen(runnerAttribution)).toBe(true);
expect(runnerAttribution).toMatchObject({
runId: (runnerArgs.opts as { runId?: string }).runId,
sessionKey: "agent:main:main",
sessionId: "session-1",
agentId: "main",
});
expect(resolvedTurnContext).toMatchObject({
requesterAccountId: "account-1",
requesterSenderId: "sender-1",
-23
View File
@@ -10,8 +10,6 @@ import {
mintMessageActionTurnCapability,
revokeMessageActionTurnCapability,
} from "../../gateway/message-action-turn-capability.js";
import { captureAgentRunLifecycleGeneration } from "../../infra/agent-events.js";
import { admitAutoReplyExecutionAttribution } from "./agent-runner-execution-identity.js";
import { extractBtwQuestion } from "./btw-command.js";
import { commandReply, defineAuthorizedTextCommand } from "./command-gates.js";
import type { CommandHandler } from "./commands-types.js";
@@ -54,26 +52,6 @@ export const handleBtwCommand: CommandHandler = defineAuthorizedTextCommand(
const chatType = normalizeChatType(params.ctx.ChatType);
const groupId = resolveGroupSessionKey(params.ctx)?.id ?? targetSessionEntry.groupId;
const runId = params.opts?.runId ?? `btw-${randomUUID()}`;
const attribution = admitAutoReplyExecutionAttribution({
config: params.cfg,
lifecycleGeneration: captureAgentRunLifecycleGeneration(runId),
runId,
context: {
accountId: params.ctx.AccountId,
agentId: sessionAgentId,
chatId: nativeChannelId,
channel: params.command.channel || params.ctx.Surface || params.ctx.Provider,
inputProvenance: params.ctx.InputProvenance,
isHeartbeat: false,
messageId: params.ctx.MessageSidFull ?? params.ctx.MessageSid,
senderId: params.ctx.SenderId ?? params.command.senderId,
senderIsBot: params.ctx.SenderIsBot,
senderLabel: params.ctx.SenderName ?? params.ctx.SenderUsername,
sessionId: targetSessionEntry.sessionId,
sessionKey: params.sessionKey,
threadId: params.ctx.MessageThreadId ?? params.ctx.TransportThreadId,
},
});
const currentChannelProvider = normalizeAnyChannelId(params.ctx.Provider);
const capabilitySessionKey = params.ctx.RuntimePolicySessionKey ?? params.sessionKey;
const messageActionTurnCapability =
@@ -101,7 +79,6 @@ export const handleBtwCommand: CommandHandler = defineAuthorizedTextCommand(
let reply: Awaited<ReturnType<typeof runBtwSideQuestion>>;
try {
reply = await runBtwSideQuestion({
attribution,
cfg: params.cfg,
agentDir,
provider: params.provider,
+4 -6
View File
@@ -451,7 +451,7 @@ describe("agentCommand", () => {
).rejects.toThrow("allowModelOverride must be explicitly set for ingress agent runs.");
});
it("replaces private execution attribution on runtime-shaped public ingress", async () => {
it("strips private execution attribution from runtime-shaped public ingress", async () => {
await withTempHome(async (home) => {
const store = path.join(home, "sessions.json");
mockConfig(home, store);
@@ -491,11 +491,9 @@ describe("agentCommand", () => {
runtime,
);
const recordedAttribution = record.mock.calls[0]?.[0].attribution;
expect(recordedAttribution).toMatchObject({ runId: "public-ingress-run" });
expect(recordedAttribution?.contextId).not.toBe("inherited-context");
expect(recordedAttribution?.contextId).not.toBe("forged-context");
expect(recordedAttribution).not.toHaveProperty("executionIdentityAdmission");
expect(record).toHaveBeenCalledWith(
expect.objectContaining({ attribution: undefined, runId: "public-ingress-run" }),
);
} finally {
record.mockRestore();
if (priorDescriptor) {
@@ -294,7 +294,6 @@ describe("gateway agent handler", () => {
);
const callArgs = await waitForAgentCommandCall<{
executionAttribution?: Record<string, unknown>;
sessionEffects?: string;
suppressPromptPersistence?: boolean;
}>();
@@ -323,7 +322,6 @@ describe("gateway agent handler", () => {
lifecycleGeneration: "test-generation",
});
expect(Object.isFrozen(runContext.attribution)).toBe(true);
expect(callArgs.executionAttribution).toBe(runContext.attribution);
});
it("preserves the admitted idempotency key exactly in execution attribution", async () => {
@@ -3,7 +3,6 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createAgentExecutionAttribution } from "../../agents/agent-execution-attribution.js";
import { createEmbeddedRunLaneController } from "../../agents/embedded-agent-runner/run/lane-controller.js";
import { installSessionPlacementAdmissionProvider } from "../../agents/session-placement-admission.js";
import { SessionManager } from "../../agents/sessions/session-manager.js";
@@ -887,15 +886,6 @@ describe("worker turn launcher", () => {
destroy: vi.fn(async () => attachedEnvironment()),
};
const provider = createWorkerSessionTurnPlacementProvider({ environments, placements });
const attribution = createAgentExecutionAttribution({
runId: "run-persisted-user",
lifecycleGeneration: "worker-generation",
sessionKey: SESSION_KEY,
sessionId: SESSION_ID,
agentId: "main",
});
const onExecutionStarted = vi.fn();
const onExecutionAttributionChanged = vi.fn();
await provider.executeTurn(
{
@@ -906,22 +896,11 @@ describe("worker turn launcher", () => {
},
{
...turn("run-persisted-user"),
attribution,
lifecycleGeneration: "worker-generation",
onExecutionStarted,
onExecutionAttributionChanged,
suppressNextUserMessagePersistence: true,
},
async () => ({ meta: { durationMs: 1 } }),
);
expect(onExecutionStarted).toHaveBeenCalledWith({
lifecycleGeneration: "worker-generation",
});
expect(onExecutionAttributionChanged).toHaveBeenCalledWith({
lifecycleGeneration: "worker-generation",
attribution,
});
expect(descriptor?.assignment.prompt).toBe("Inspect this workspace");
expect(descriptor?.assignment.initialMessages).toMatchObject([
{ role: "user" },
@@ -236,10 +236,6 @@ async function executeWorkerTurn(params: {
const startedAt = Date.now();
turn.onExecutionStarted?.({ lifecycleGeneration: turn.lifecycleGeneration });
turn.onExecutionAttributionChanged?.({
lifecycleGeneration: turn.lifecycleGeneration,
attribution: turn.attribution,
});
turn.onExecutionPhase?.({ phase: "runner_entered", backend: "cloud-worker" });
const transcriptTarget = resolveWorkerTurnTranscriptTarget(turn);
const manager = SessionManager.open(transcriptTarget);
-1
View File
@@ -85,7 +85,6 @@ export const forcedUnitFastTestFiles = [
"src/acp/translator.session-setup.test.ts",
"src/acp/translator.session-snapshot.test.ts",
"src/acp/translator.tool-streaming.test.ts",
"src/auto-reply/reply/agent-runner-execution-runtime.test.ts",
"src/browser-lifecycle-cleanup.test.ts",
"extensions/canvas/src/host/server.test.ts",
"src/system-agent/audit.test.ts",