mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
Revert "refactor(agents): propagate exact attribution across runtimes"
This reverts commit df31882c38.
This commit is contained in:
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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"),
|
||||
});
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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() ||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")>(
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user