Files
openclaw/src/agents/agent-command.ts
T
Pavan Kumar Gondhi 8668aeb969 fix(discord): bind transcript capture to source account [AI] (#118579)
* fix(discord): bind transcript capture to source account

* style(agents): keep transcript tool wiring compact

* fix(transcripts): declare account binding channels

* fix(transcripts): report effective capture account

* fix(transcripts): enforce account lifecycle ownership

* fix(transcripts): preserve cross-surface control

* fix(copilot): preserve transcript channel context

* fix(transcripts): fail closed for legacy channel owners

* fix(transcripts): add trusted legacy recovery

* fix(transcripts): preserve auto-start cleanup ownership

* fix(transcripts): reject untrusted account starts

* fix(transcripts): keep persisted ownership authoritative

* fix(transcripts): harden legacy recovery

* fix(transcripts): preserve agent ownership boundary

* fix(transcripts): scope account binding to source channel

* fix(transcripts): preserve unattributed owner isolation

* fix(transcripts): own configured captures by account

* docs(plugins): clarify transcript auto-start ownership

* test(transcripts): cover account-less recovery

* docs(transcripts): scope legacy recovery by provider

* fix(discord): reuse eligible account ordering for transcripts

* test(discord): use neutral transcript account fixtures

* fix(transcripts): keep accountless recovery local

* fix(discord): resolve transcript accounts by voice capability

* fix(transcripts): bound account resolution failures

* fix(transcripts): bound account tool output

* fix(transcripts): honor unresolved provider accounts

* fix(transcripts): preserve binding when providers are missing

* fix(transcripts): fail closed on unknown binding provenance

* fix(transcripts): qualify account lifecycle capability

* fix(transcripts): normalize provable legacy owners

* fix(transcripts): bind scheduled capture to caller authority

* fix(transcripts): preserve scheduled caller identity tuple

* fix(transcripts): preserve channel-less scheduled authority

* fix(plugin-sdk): publish transcript provider types

* fix(transcripts): use exact lifecycle ownership tokens

* fix(transcripts): preserve local ownerless lifecycle access

* fix(transcripts): allow local configured capture control

* fix(transcripts): preserve scheduled caller channel

* fix(transcripts): retain named-agent legacy recovery

* fix(transcripts): deny unrelated remote channels

* fix(doctor): validate transcript owner inference

* fix(transcripts): restrict legacy remote recovery

* fix(ci): align transcript Doctor checks

* fix(transcripts): require Doctor-owned legacy metadata

* fix(transcripts): reject unowned remote capture starts

* fix(transcripts): reject unbound Discord lifecycle calls

* fix(transcripts): distinguish legacy owner rows

* test(discord): keep unavailable account fixture typed

* fix(transcripts): mark current imports for Doctor

* fix(transcripts): complete account ownership validation

* fix(discord): restore transcript package boundary

* fix(discord): preserve bundled transcript entry boundary

* docs(transcripts): clarify Discord auto-start account

* fix(transcripts): bind account-owned imports

* fix: preserve transcript and cron policy state

* fix(cron): preserve scheduled transcript authority

* fix(discord): keep legacy transcript rows local

* fix(transcripts): narrow account ownership boundary

* fix(transcripts): preserve trusted caller ownership

* fix(discord): enforce transcript source authorization

* fix(ci): bound Control UI gzip build variance

* test(qa): align transcript scenario contracts

* fix(agents): repair rebased caller context

* fix(discord): restore rebased account ownership

* test(discord): restore voice account fixtures

---------

Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
2026-08-15 12:10:43 -07:00

695 lines
26 KiB
TypeScript

/** Main agent command orchestration for sessions, model selection, delivery, and attempts. */
import { coerceErrorMessage } from "@openclaw/normalization-core/error-coercion";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import type { VerboseLevel } from "../auto-reply/thinking.js";
import type { CliDeps } from "../cli/deps.types.js";
import { resolveSessionWorkStartError } from "../config/sessions/lifecycle.js";
import { buildRestartRecoveryClaimCleanupPatch } from "../config/sessions/restart-recovery-state.js";
import type { RestartRecoveryTerminalDeliveryEvidenceResult } from "../config/sessions/restart-recovery-types.js";
import type { SessionEntry } from "../config/sessions/types.js";
import {
assertAgentRunLifecycleGenerationCurrent,
captureAgentRunLifecycleGeneration,
withAgentRunLifecycleGeneration,
} from "../infra/agent-events.js";
import { clearAgentRunContext } from "../infra/agent-run-registry.js";
import { formatErrorMessage } from "../infra/errors.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { isSubagentSessionKey } from "../routing/session-key.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { isAgentMediatedCompletionSourceTool } from "../sessions/input-provenance.js";
import { resolveSendPolicy } from "../sessions/send-policy.js";
import { ensureSessionDiffBaseline } from "../sessions/session-diff-baseline.js";
import { beginSessionWorkAdmission } from "../sessions/session-lifecycle-admission.js";
import { classifySessionStateActor } from "../sessions/session-state-events.js";
import { sessionDeliveryChannel, type DeliveryContext } from "../utils/delivery-context.shared.js";
import {
executionIdentity,
prepareAgentCommandExecutionIdentity,
sanitizePublicAgentCommandIngressOpts,
type AgentCommandAdmissionIngress,
} from "./agent-command-execution-identity.js";
import { runLocalAgentCommand } from "./agent-command-local.js";
import { runWithAgentCommandRecoveryOwner } from "./agent-command-recovery-owner.js";
import {
buildCurrentRunRestartRecoveryClaim,
shouldPersistRestartRecoveryCleanup,
shouldPersistRestartRecoveryContextClaim,
} from "./agent-command-restart-recovery.js";
import { runAcpAgentCommand } from "./command/acp-execution.js";
import { repairPendingAssistantTranscriptTurns } from "./command/assistant-transcript-repair.js";
import { persistAgentSession } from "./command/attempt-execution.shared.js";
import { emitIngressModelUsageDiagnostic } from "./command/ingress-diagnostics.js";
import { resolveEmbeddedModelSelection } from "./command/model-selection.js";
import { finalizeEmbeddedAgentCommand } from "./command/post-run.js";
import { prepareAgentCommandExecution } from "./command/prepare.js";
import { runEmbeddedAgentAttempt } from "./command/run-embedded-attempt.js";
import { loadSessionStoreRuntime, resolveAgentCommandDeps } from "./command/runtime-loaders.js";
import { prepareCurrentRunDelivery } from "./command/session-helpers.js";
import { prepareEmbeddedSessionState } from "./command/session-preparation.js";
import { clearRotatedSessionMetadata } from "./command/session.js";
import type {
AgentCommandGatewayIngressOpts,
AgentCommandIngressOpts,
AgentCommandOpts,
} from "./command/types.js";
import {
removeInternalSessionEffectsSession,
resolveInternalSessionEffectsTarget,
} from "./internal-session-effects.js";
import { AGENT_LANE_SUBAGENT } from "./lanes.js";
import type { MainSessionRecoveryPendingTarget } from "./main-session-recovery/main-session-recovery-store.js";
import type { AgentRunSessionTarget } from "./run-session-target.js";
import { createAgentRunRestartAbortError } from "./run-termination.js";
import { withAgentPluginRegistry } from "./runtime-plugins.js";
import { measureAgentStartup } from "./startup-timing.js";
const log = createSubsystemLogger("agents/agent-command");
async function agentCommandInternal(
prepared: Awaited<ReturnType<typeof prepareAgentCommandExecution>>,
initialOpts: AgentCommandOpts,
admissionIngress: AgentCommandAdmissionIngress,
runtime: RuntimeEnv = defaultRuntime,
deps?: CliDeps,
) {
const resolvedDeps = await resolveAgentCommandDeps(deps);
const isRawModelRun = initialOpts.modelRun === true || initialOpts.promptMode === "none";
const suppressVisibleSessionEffects = initialOpts.sessionEffects === "internal";
const preserveUserFacingSessionModelState =
initialOpts.preserveUserFacingSessionModelState === true;
const lifecycleAbortController = new AbortController();
const storedDeliveryMediaUrls =
prepared.sessionEntry?.restartRecoveryDeliveryRunId === prepared.runId &&
Array.isArray(prepared.sessionEntry.restartRecoveryDeliveryMediaUrls)
? prepared.sessionEntry.restartRecoveryDeliveryMediaUrls
: undefined;
const preparedOpts =
storedDeliveryMediaUrls !== undefined
? {
...prepared.opts,
internalDeliveryMediaUrls: [...storedDeliveryMediaUrls],
internalDeliverySuppressText: prepared.sessionEntry?.restartRecoverySuppressTextDelivery,
sourceReplyDeliveryMode: prepared.sessionEntry?.restartRecoverySourceReplyDeliveryMode,
disableMessageTool: prepared.sessionEntry?.restartRecoveryDisableMessageTool,
forceRestartSafeTools: prepared.sessionEntry?.restartRecoveryForceSafeTools,
}
: prepared.opts;
if (
(preparedOpts.internalDeliverySuppressText === true &&
preparedOpts.internalDeliveryMediaUrls === undefined) ||
((preparedOpts.internalDeliveryMediaUrls !== undefined ||
preparedOpts.internalDeliverySuppressText === true) &&
(preparedOpts.forceRestartSafeTools !== true ||
preparedOpts.disableMessageTool !== true ||
preparedOpts.sourceReplyDeliveryMode !== "automatic"))
) {
throw new Error(
"internal delivery media constraints require automatic delivery with restart-safe tools and no message tool",
);
}
let opts: AgentCommandOpts = {
...preparedOpts,
abortSignal: preparedOpts.abortSignal
? AbortSignal.any([preparedOpts.abortSignal, lifecycleAbortController.signal])
: lifecycleAbortController.signal,
};
const {
body,
transcriptBody,
cfg,
configuredThinkingCatalog,
agentCfg,
thinkOverride,
thinkOnce,
verboseOverride,
sessionId,
sessionKey,
sessionStore,
storePath,
isNewSession,
persistedThinking,
persistedVerbose,
sessionAgentId,
outboundSession,
workspaceDir,
cwd,
runId,
isSubagentLane,
acpManager,
acpResolution,
pluginsEnabled,
manifestMetadataSnapshot,
modelManifestContext,
} = prepared;
let lifecycleGeneration = opts.lifecycleGeneration ?? captureAgentRunLifecycleGeneration(runId);
let sessionEntry = prepared.sessionEntry,
runOwnedSessionId = sessionId;
const sessionStateActor = classifySessionStateActor({
inputProvenance: opts.inputProvenance,
internalEvents: opts.internalEvents,
sessionEffects: opts.sessionEffects,
});
// Subagent-lane turns are the parent's own task dispatch into the child (they
// carry no inter_session provenance today); classifying them as human would tell
// the parent a human interjected on every spawn, for embedded and ACP children alike.
const isSubagentLaneTurn = normalizeOptionalString(opts.lane) === AGENT_LANE_SUBAGENT;
let sessionReboundDuringRun = false;
let trackedRestartRecoveryDeliveryClaim = false;
let currentRunDeliveryContext: DeliveryContext | undefined;
let restartRecoveryTerminalDeliveryEvidence:
| RestartRecoveryTerminalDeliveryEvidenceResult
| undefined;
const preparedSessionId = sessionEntry?.sessionId;
const internalModelRunTargets =
initialOpts.modelRun === true && suppressVisibleSessionEffects
? new Map<string, AgentRunSessionTarget>()
: undefined;
const trackInternalModelRunTarget = (target: AgentRunSessionTarget | undefined) => {
if (!internalModelRunTargets || !target?.sessionKey || !target.storePath) {
return;
}
internalModelRunTargets.set(`${target.storePath}\n${target.sessionKey}`, target);
};
if (internalModelRunTargets && storePath) {
trackInternalModelRunTarget(
resolveInternalSessionEffectsTarget({ agentId: sessionAgentId, runId, storePath }),
);
}
let sessionWorkAdmission: Awaited<ReturnType<typeof beginSessionWorkAdmission>> | undefined;
let preparedRunAdmission: ReturnType<typeof executionIdentity.prepare> | undefined;
try {
assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration);
const sessionStoreRuntime =
storePath && sessionKey ? await loadSessionStoreRuntime() : undefined;
// Reset marks its mutation before interrupting work. An aborted run must not
// queue behind that mutation or reset would wait on the run holding the queue.
sessionWorkAdmission = await beginSessionWorkAdmission({
scope: storePath ?? `agent:${sessionAgentId}`,
identities: [sessionKey, sessionId],
signal: opts.abortSignal,
onInterrupt: () => lifecycleAbortController.abort(createAgentRunRestartAbortError()),
assertAllowed: () => {
const currentEntry =
sessionStoreRuntime && storePath && sessionKey
? sessionStoreRuntime.loadSessionEntry({
storePath,
sessionKey,
readConsistency: "latest",
})
: sessionEntry;
if (!currentEntry && preparedSessionId) {
throw new Error(
`Session "${sessionKey ?? sessionId}" changed while starting work. Retry.`,
);
}
const matchesIntentionalRollover =
isNewSession && currentEntry?.sessionId === preparedSessionId;
if (currentEntry && currentEntry.sessionId !== sessionId && !matchesIntentionalRollover) {
throw new Error(
`Session "${sessionKey ?? sessionId}" changed while starting work. Retry.`,
);
}
const archivedSessionError = resolveSessionWorkStartError(
sessionKey ?? sessionId,
currentEntry,
);
if (archivedSessionError) {
throw new Error(archivedSessionError);
}
sessionEntry = currentEntry;
if (sessionStore && sessionKey) {
if (currentEntry) {
sessionStore[sessionKey] = currentEntry;
} else {
delete sessionStore[sessionKey];
}
}
},
});
return await sessionWorkAdmission.run(async () => {
preparedRunAdmission = prepareAgentCommandExecutionIdentity({
opts,
prepared,
ingress: admissionIngress,
lifecycleGeneration,
});
if (sessionStore && sessionKey && !suppressVisibleSessionEffects) {
try {
await repairPendingAssistantTranscriptTurns({
context: {
sessionKey,
sessionEntry,
sessionStore,
storePath,
sessionAgentId,
config: cfg,
},
});
sessionEntry = sessionStore[sessionKey] ?? sessionEntry;
} catch (error) {
if (!isNewSession) {
throw error;
}
// A reset starts a fresh transcript. Do not let predecessor repair
// state leak into it when the old transcript remains unavailable.
log.warn(
`Could not repair predecessor transcript before session reset for ${sessionKey}: ${formatErrorMessage(error)}`,
);
}
}
if (opts.deliver === true) {
const sendPolicy = resolveSendPolicy({
cfg,
entry: sessionEntry,
sessionKey,
channel: sessionDeliveryChannel(sessionEntry),
chatType: sessionEntry?.chatType,
});
if (sendPolicy === "deny") {
throw new Error("send blocked by session policy");
}
}
if (!isRawModelRun && acpResolution?.kind === "stale") {
throw acpResolution.error;
}
let currentRunDeliveryPrepared = false;
const prepareDeliveryForRun = async (candidateSessionEntry?: SessionEntry) => {
if (currentRunDeliveryPrepared || opts.deliver !== true) {
return;
}
currentRunDeliveryPrepared = true;
let preparedDelivery: Awaited<ReturnType<typeof prepareCurrentRunDelivery>>;
try {
preparedDelivery = await prepareCurrentRunDelivery({
cfg,
opts,
agentId: sessionAgentId,
currentSessionKey: sessionKey,
sessionEntry: candidateSessionEntry,
});
} catch (error) {
if (opts.bestEffortDeliver !== true) {
throw error;
}
log.warn(
`delivery preflight failed; continuing model run with requested delivery intent because bestEffortDeliver is enabled: ${coerceErrorMessage(error)}`,
);
}
assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration);
if (preparedDelivery) {
currentRunDeliveryContext = preparedDelivery.context;
opts = {
...opts,
replyChannel: preparedDelivery.context.channel,
replyTo: preparedDelivery.context.to,
replyAccountId: preparedDelivery.context.accountId,
threadId: preparedDelivery.context.threadId,
deliveryTargetMode: preparedDelivery.targetMode,
};
}
};
if (
sessionStore &&
sessionKey &&
!suppressVisibleSessionEffects &&
!isSubagentSessionKey(sessionKey)
) {
const now = Date.now();
const currentStoreEntry = sessionStore[sessionKey];
const allowCreateRestartRecoveryEntry =
currentStoreEntry === undefined && sessionEntry === undefined;
const initialEntry = currentStoreEntry ??
sessionEntry ?? { sessionId, updatedAt: now, sessionStartedAt: now };
const isSessionRollover = isNewSession && initialEntry.sessionId !== sessionId;
const entry = isSessionRollover ? clearRotatedSessionMetadata(initialEntry) : initialEntry;
await prepareDeliveryForRun(entry);
const generatedMediaSourceRunId =
opts.internalDeliveryMediaUrls !== undefined &&
opts.inputProvenance?.kind === "inter_session" &&
isAgentMediatedCompletionSourceTool(opts.inputProvenance.sourceTool)
? runId
: undefined;
assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration);
const next: SessionEntry = {
...entry,
sessionId,
updatedAt: now,
sessionStartedAt: isSessionRollover ? now : entry.sessionStartedAt,
lastInteractionAt: isSessionRollover ? now : entry.lastInteractionAt,
...buildCurrentRunRestartRecoveryClaim({
deliveryContext: currentRunDeliveryContext,
deliveryMediaUrls: opts.internalDeliveryMediaUrls,
disableMessageTool: opts.disableMessageTool,
entry,
forceRestartSafeTools: opts.forceRestartSafeTools,
runId,
sourceIngress: generatedMediaSourceRunId ? "internal" : undefined,
sourceRunId: generatedMediaSourceRunId,
sourceReplyDeliveryMode: opts.sourceReplyDeliveryMode,
suppressTextDelivery: opts.internalDeliverySuppressText,
}),
};
const persisted = await persistAgentSession({
sessionStore,
sessionKey,
storePath,
initialEntry,
entry: next,
shouldPersist: (current) =>
isSessionRollover
? current?.sessionId === initialEntry.sessionId
: shouldPersistRestartRecoveryContextClaim(
current,
sessionId,
runId,
allowCreateRestartRecoveryEntry,
),
});
sessionEntry = persisted;
trackedRestartRecoveryDeliveryClaim = persisted?.restartRecoveryDeliveryRunId === runId;
}
if (sessionEntry && sessionKey && !suppressVisibleSessionEffects) {
try {
sessionEntry = await ensureSessionDiffBaseline({
cwd: cwd ?? workspaceDir,
entry: sessionEntry,
isNewSession,
sessionKey,
storePath,
});
if (sessionStore) {
sessionStore[sessionKey] = sessionEntry;
}
} catch (error) {
log.warn(
`session diff baseline capture failed; continuing without attribution filtering: ${coerceErrorMessage(error)}`,
);
}
}
await prepareDeliveryForRun(sessionEntry);
if (!isRawModelRun && acpResolution?.kind === "ready" && sessionKey) {
assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration);
return await runAcpAgentCommand({
cfg,
deps: resolvedDeps,
runtime,
opts,
outboundSession,
sessionEntry,
sessionStore,
body,
transcriptBody,
suppressVisibleSessionEffects,
provenance: isSubagentLaneTurn ? "agent" : sessionStateActor.actorType,
sessionAgentId,
sessionId,
sessionKey,
storePath,
workspaceDir,
runId,
lifecycleGeneration,
acpManager,
acpResolution,
trackInternalModelRunTarget,
preparedRunAdmission,
});
}
const embeddedSessionState = await measureAgentStartup(
"session-state",
() =>
prepareEmbeddedSessionState({
cfg,
opts,
sessionEntry,
sessionStore,
sessionKey,
sessionId,
storePath,
sessionAgentId,
lifecycleGeneration,
runId,
workspaceDir,
isNewSession,
isSubagentLaneTurn,
suppressVisibleSessionEffects,
thinkOnce,
thinkOverride,
persistedThinking,
verboseOverride,
persistedVerbose,
verboseDefault: agentCfg?.verboseDefault as VerboseLevel | undefined,
sessionStateActor,
}),
{ config: cfg },
);
sessionEntry = embeddedSessionState.sessionEntry;
const { requestedThinkLevel, runContext } = embeddedSessionState;
const modelSelection = await measureAgentStartup(
"model-selection",
() =>
resolveEmbeddedModelSelection({
cfg,
opts,
sessionEntry,
sessionStore,
sessionKey,
sessionId,
storePath,
sessionAgentId,
workspaceDir,
pluginsEnabled,
manifestMetadataSnapshot,
modelManifestContext,
configuredThinkingCatalog,
requestedThinkLevel,
thinkOverride,
thinkOnce,
isSubagentLane,
suppressVisibleSessionEffects,
runContext,
}),
{ config: cfg },
);
sessionEntry = modelSelection.sessionEntry;
const embeddedAttempt = await runEmbeddedAgentAttempt({
prepared,
opts,
sessionEntry,
lifecycleGeneration,
onLifecycleGenerationChanged: (nextLifecycleGeneration) => {
lifecycleGeneration = nextLifecycleGeneration;
},
suppressVisibleSessionEffects,
preserveUserFacingSessionModelState,
modelSelection,
embeddedSessionState,
trackInternalModelRunTarget,
preparedRunAdmission,
});
if (embeddedAttempt.fallbackExhausted) {
opts.onModelFallbackExhausted?.();
}
sessionEntry = embeddedAttempt.sessionEntry;
lifecycleGeneration = embeddedAttempt.lifecycleGeneration;
const finalized = await finalizeEmbeddedAgentCommand({
prepared,
opts,
deps: resolvedDeps,
runtime,
sessionEntry,
attempt: embeddedAttempt,
embeddedSessionState,
suppressVisibleSessionEffects,
preserveUserFacingSessionModelState,
currentRunDeliveryContext,
sessionOwnership: { runOwnedSessionId, sessionReboundDuringRun },
trackInternalModelRunTarget,
onSessionOwnershipChanged: (ownership) => {
runOwnedSessionId = ownership.runOwnedSessionId;
sessionReboundDuringRun = ownership.sessionReboundDuringRun;
},
onTerminalDeliveryEvidenceChanged: (evidence) => {
restartRecoveryTerminalDeliveryEvidence = evidence;
},
});
sessionEntry = finalized.sessionEntry;
runOwnedSessionId = finalized.runOwnedSessionId;
sessionReboundDuringRun = finalized.sessionReboundDuringRun;
return finalized.deliveryResult;
});
} finally {
preparedRunAdmission?.close();
sessionWorkAdmission?.release();
if (internalModelRunTargets) {
// Compaction may rotate a private session identity. Remove every owned
// SQLite row only after delivery; transcript and trajectory rows cascade.
for (const target of internalModelRunTargets.values()) {
try {
await removeInternalSessionEffectsSession(target);
} catch (error) {
// Cleanup remains best-effort so a terminal SQLite write failure does
// not replace the completed model-run result; the DB layer warns too.
log.warn(`failed to remove model-run SQLite session: ${coerceErrorMessage(error)}`);
}
}
}
if (
!sessionReboundDuringRun &&
trackedRestartRecoveryDeliveryClaim &&
sessionStore &&
sessionKey
) {
try {
const entry = sessionStore[sessionKey] ?? sessionEntry;
if (entry?.restartRecoveryDeliveryRunId === runId) {
const next: SessionEntry = {
...entry,
...buildRestartRecoveryClaimCleanupPatch({
entry,
recordTerminalSource: true,
terminalRunId: runId,
terminalDeliveryEvidence: restartRecoveryTerminalDeliveryEvidence,
}),
updatedAt: Date.now(),
};
const persisted = await persistAgentSession({
sessionStore,
sessionKey,
storePath,
initialEntry: entry,
entry: next,
shouldPersist: (current) =>
shouldPersistRestartRecoveryCleanup(current, runOwnedSessionId, runId),
});
sessionEntry = persisted;
}
} catch (error) {
log.warn(
`failed to clear restart recovery delivery context for ${sessionKey}: ${coerceErrorMessage(error)}`,
);
}
}
clearAgentRunContext(runId, lifecycleGeneration);
}
}
async function agentCommandWithAdmissionIngress(
opts: AgentCommandOpts,
admissionIngress: AgentCommandAdmissionIngress,
runtime: RuntimeEnv = defaultRuntime,
deps?: CliDeps,
) {
return await runLocalAgentCommand({
opts,
runtime,
deps,
operatorAuthority: admissionIngress.kind === "local-cli",
run: async (prepared, resolvedDeps) =>
await agentCommandInternal(prepared, prepared.opts, admissionIngress, runtime, resolvedDeps),
});
}
export async function agentCommand(
opts: AgentCommandOpts,
runtime: RuntimeEnv = defaultRuntime,
deps?: CliDeps,
) {
const { localIngress } = executionIdentity;
return await agentCommandWithAdmissionIngress(opts, localIngress, runtime, deps);
}
export async function agentCommandFromSystem(
opts: AgentCommandOpts,
admission: { boundary: string },
runtime: RuntimeEnv = defaultRuntime,
deps?: CliDeps,
) {
const ingress = executionIdentity.systemIngress(admission.boundary);
return await agentCommandWithAdmissionIngress(opts, ingress, runtime, deps);
}
async function agentCommandFromIngressInternal(
opts: AgentCommandGatewayIngressOpts,
runtime: RuntimeEnv = defaultRuntime,
deps?: CliDeps,
recovery?: {
restoreAdmittedRecovery?: () => Promise<MainSessionRecoveryPendingTarget | undefined>;
},
) {
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 () => {
let preparedAgentDir: string | undefined;
const result = await runWithAgentCommandRecoveryOwner({
lifecycleGeneration,
mode: "claim",
opts: {
...opts,
lifecycleGeneration,
senderIsOwner: opts.senderIsOwner === true,
},
prepare: async (preparedOpts) => await prepareAgentCommandExecution(preparedOpts, runtime),
restoreAdmittedRecovery: recovery?.restoreAdmittedRecovery,
run: async (prepared) => {
preparedAgentDir = prepared.agentDir;
return await withAgentPluginRegistry({
config: prepared.cfg,
workspaceDir: prepared.workspaceDir,
run: async () =>
await agentCommandInternal(
prepared,
prepared.opts,
{ kind: "api", boundary: "agent-command.from-ingress", state: "unknown" },
runtime,
deps,
),
});
},
});
if (result && preparedAgentDir) {
emitIngressModelUsageDiagnostic(result, opts, preparedAgentDir);
}
return result;
});
}
/** Runs an agent turn from an inbound channel/gateway ingress context. */
export async function agentCommandFromIngress(
opts: AgentCommandIngressOpts,
runtime: RuntimeEnv = defaultRuntime,
deps?: CliDeps,
) {
// Plugin SDK callers may be plain JavaScript. Enforce the private recovery
// boundary at runtime so extra or inherited properties cannot author audit identity.
return await agentCommandFromIngressInternal(
sanitizePublicAgentCommandIngressOpts(opts),
runtime,
deps,
);
}
/** Internal Gateway entrypoint that restores a rejected restart-recovery admission. */
export async function agentCommandFromGatewayIngress(
opts: AgentCommandGatewayIngressOpts,
runtime: RuntimeEnv,
deps: CliDeps | undefined,
recovery: {
restoreAdmittedRecovery?: () => Promise<MainSessionRecoveryPendingTarget | undefined>;
},
) {
return await agentCommandFromIngressInternal(opts, runtime, deps, recovery);
}