mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-29 04:32:57 +00:00
fix(cli): select permission mode before imported agents run
Carry an explicit import mode into both fresh sessions and archived restores. Update the last observed mode on restore so it cannot override the requested mode. Co-authored-by: Keith Kowal <keithpk+paseo-dev@kowal.me>
This commit is contained in:
@@ -46,6 +46,11 @@ describe("canonical CLI surface", () => {
|
||||
expect(help).toContain("--forge <forge>");
|
||||
});
|
||||
|
||||
it("offers provider mode selection when importing an agent", () => {
|
||||
const importCommand = createCli().commands.find((command) => command.name() === "import");
|
||||
expect(importCommand?.helpInformation()).toContain("--mode <mode>");
|
||||
});
|
||||
|
||||
it("uses --no-wait for execution, --internal for visibility, and reserves detach for ownership", () => {
|
||||
const run = createCli().commands.find((command) => command.name() === "run");
|
||||
const help = run?.helpInformation();
|
||||
|
||||
@@ -11,6 +11,7 @@ export function addImportOptions(cmd: Command): Command {
|
||||
.argument("<id>", "Provider session/thread ID to import")
|
||||
.requiredOption("--provider <provider>", "Agent provider id")
|
||||
.option("--cwd <path>", "Working directory for providers that require it")
|
||||
.option("--mode <mode>", "Provider-specific mode (e.g., plan, default, bypassPermissions)")
|
||||
.option(
|
||||
"--label <key=value>",
|
||||
"Add label(s) to the agent (can be used multiple times)",
|
||||
@@ -22,6 +23,7 @@ export function addImportOptions(cmd: Command): Command {
|
||||
export interface AgentImportOptions extends CommandOptions {
|
||||
provider?: string;
|
||||
cwd?: string;
|
||||
mode?: string;
|
||||
label?: string[];
|
||||
host?: string;
|
||||
}
|
||||
@@ -119,6 +121,7 @@ export async function runImportCommand(
|
||||
provider,
|
||||
sessionId,
|
||||
cwd,
|
||||
...(options.mode ? { modeId: options.mode } : {}),
|
||||
...(Object.keys(labels).length > 0 ? { labels } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -5402,6 +5402,7 @@ test("imports an agent by provider handle id", async () => {
|
||||
providerId: "custom-codex",
|
||||
providerHandleId: "thread-1",
|
||||
cwd: "/tmp/repo",
|
||||
modeId: "full-access",
|
||||
});
|
||||
|
||||
expect(mock.sent).toHaveLength(1);
|
||||
@@ -5414,6 +5415,7 @@ test("imports an agent by provider handle id", async () => {
|
||||
providerHandleId?: string;
|
||||
sessionId?: string;
|
||||
cwd?: string;
|
||||
modeId?: string;
|
||||
};
|
||||
};
|
||||
expect(request.message).toMatchObject({
|
||||
@@ -5421,6 +5423,7 @@ test("imports an agent by provider handle id", async () => {
|
||||
providerId: "custom-codex",
|
||||
providerHandleId: "thread-1",
|
||||
cwd: "/tmp/repo",
|
||||
modeId: "full-access",
|
||||
});
|
||||
expect(request.message).not.toHaveProperty("sessionId");
|
||||
|
||||
|
||||
@@ -202,6 +202,7 @@ interface ImportAgentInputBase {
|
||||
cwd?: string;
|
||||
workspaceId?: string;
|
||||
labels?: Record<string, string>;
|
||||
modeId?: string;
|
||||
}
|
||||
|
||||
export type ImportAgentInput =
|
||||
@@ -3220,6 +3221,7 @@ export class DaemonClient {
|
||||
...(input.cwd ? { cwd: input.cwd } : {}),
|
||||
...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
|
||||
...(input.labels && Object.keys(input.labels).length > 0 ? { labels: input.labels } : {}),
|
||||
...(input.modeId ? { modeId: input.modeId } : {}),
|
||||
});
|
||||
|
||||
const status = await this.sendRequest({
|
||||
|
||||
@@ -1831,6 +1831,7 @@ export const ImportAgentRequestMessageSchema = z.object({
|
||||
cwd: z.string().optional(),
|
||||
workspaceId: z.string().optional(),
|
||||
labels: z.record(z.string(), z.string()).optional(),
|
||||
modeId: z.string().optional(),
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
|
||||
@@ -411,6 +411,7 @@ describe("workspace message schemas", () => {
|
||||
providerId: "custom-codex",
|
||||
providerHandleId: "thread-1",
|
||||
cwd: "/tmp/repo",
|
||||
modeId: "full-access",
|
||||
});
|
||||
const legacyRequest = SessionInboundMessageSchema.parse({
|
||||
type: "import_agent_request",
|
||||
@@ -426,6 +427,7 @@ describe("workspace message schemas", () => {
|
||||
providerId: "custom-codex",
|
||||
providerHandleId: "thread-1",
|
||||
cwd: "/tmp/repo",
|
||||
modeId: "full-access",
|
||||
});
|
||||
expect(legacyRequest).toEqual({
|
||||
type: "import_agent_request",
|
||||
|
||||
@@ -4219,6 +4219,7 @@ test("importProviderSession imports the selected session without listing and pub
|
||||
class ImportClient extends TestAgentClient {
|
||||
listCalls = 0;
|
||||
importInput: unknown = null;
|
||||
importConfig: AgentSessionConfig | null = null;
|
||||
importLaunchContext: AgentLaunchContext | undefined;
|
||||
|
||||
async listImportableSessions() {
|
||||
@@ -4228,10 +4229,11 @@ test("importProviderSession imports the selected session without listing and pub
|
||||
|
||||
async importSession(input: ImportProviderSessionInput, context: ImportProviderSessionContext) {
|
||||
this.importInput = input;
|
||||
this.importConfig = context.storedConfig;
|
||||
this.importLaunchContext = context.launchContext;
|
||||
return {
|
||||
session,
|
||||
config: { provider: "codex" as const, cwd: workdir },
|
||||
config: context.storedConfig,
|
||||
persistence: {
|
||||
provider: "codex" as const,
|
||||
sessionId: input.providerHandleId,
|
||||
@@ -4304,10 +4306,12 @@ test("importProviderSession imports the selected session without listing and pub
|
||||
providerHandleId: "thread-selected",
|
||||
cwd: workdir,
|
||||
workspaceId: "ws-imported",
|
||||
modeId: "full-access",
|
||||
});
|
||||
|
||||
expect(client.listCalls).toBe(0);
|
||||
expect(client.importInput).toEqual({ providerHandleId: "thread-selected", cwd: workdir });
|
||||
expect(client.importConfig?.modeId).toBe("full-access");
|
||||
expect(client.importLaunchContext).toEqual({
|
||||
agentId: imported.id,
|
||||
env: {
|
||||
@@ -4316,6 +4320,7 @@ test("importProviderSession imports the selected session without listing and pub
|
||||
},
|
||||
});
|
||||
expect(imported.lifecycle).toBe("idle");
|
||||
expect(imported.config.modeId).toBe("full-access");
|
||||
expect(imported.historyPrimed).toBe(true);
|
||||
expect(manager.getTimeline(imported.id)).toEqual([
|
||||
{ type: "user_message", text: "Trace provider imports" },
|
||||
@@ -8640,6 +8645,7 @@ test("unarchiveSnapshot unarchives native provider storage before clearing archi
|
||||
const unarchived = await manager.unarchiveSnapshot(agent.id, {
|
||||
workspaceId: "ws-restored",
|
||||
labels: { [PARENT_AGENT_ID_LABEL]: null, source: "reimport" },
|
||||
modeId: "full-access",
|
||||
});
|
||||
const stored = await storage.get(agent.id);
|
||||
|
||||
@@ -8650,8 +8656,52 @@ test("unarchiveSnapshot unarchives native provider storage before clearing archi
|
||||
expect(stored?.archivedAt).toBeNull();
|
||||
expect(stored?.workspaceId).toBe("ws-restored");
|
||||
expect(stored?.labels).toEqual({ retained: "yes", source: "reimport" });
|
||||
expect(stored?.config?.modeId).toBe("full-access");
|
||||
});
|
||||
|
||||
test.each(["full-access", undefined])(
|
||||
"unarchiveSnapshot resumes with the requested mode %s or the last observed mode",
|
||||
async (modeId) => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-unarchive-mode-"));
|
||||
const storage = new AgentStorage(join(workdir, "agents"), logger);
|
||||
class ModeClient extends TestAgentClient {
|
||||
override async resumeSession(
|
||||
_handle: AgentPersistenceHandle,
|
||||
config?: Partial<AgentSessionConfig>,
|
||||
): Promise<AgentSession> {
|
||||
return new (class extends TestAgentSession {
|
||||
override async getCurrentMode() {
|
||||
return config?.modeId ?? null;
|
||||
}
|
||||
})({ provider: "codex", cwd: workdir, ...config });
|
||||
}
|
||||
}
|
||||
const manager = new AgentManager({
|
||||
clients: { codex: new ModeClient() },
|
||||
registry: storage,
|
||||
logger,
|
||||
});
|
||||
try {
|
||||
const agent = await manager.createAgent(
|
||||
{ provider: "codex", cwd: workdir, modeId: "default" },
|
||||
undefined,
|
||||
{ workspaceId: undefined },
|
||||
);
|
||||
await manager.archiveAgent(agent.id);
|
||||
await manager.unarchiveSnapshot(agent.id, { modeId });
|
||||
const restored = await ensureAgentLoaded(agent.id, {
|
||||
agentManager: manager,
|
||||
agentStorage: storage,
|
||||
logger,
|
||||
});
|
||||
expect(restored.currentModeId).toBe(modeId ?? "default");
|
||||
} finally {
|
||||
for (const agent of manager.listAgents()) await manager.closeAgent(agent.id);
|
||||
rmSync(workdir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test("unarchiveSnapshotByHandle unarchives native provider storage for the matched snapshot", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-native-unarchive-handle-"));
|
||||
const storagePath = join(workdir, "agents");
|
||||
|
||||
@@ -182,6 +182,21 @@ interface TimeoutOptions {
|
||||
onLateError?: (error: unknown) => void;
|
||||
}
|
||||
|
||||
interface ImportProviderSessionInput {
|
||||
provider: AgentProvider;
|
||||
providerHandleId: string;
|
||||
cwd: string;
|
||||
workspaceId: string;
|
||||
labels?: Record<string, string>;
|
||||
modeId?: string;
|
||||
}
|
||||
|
||||
interface UnarchiveSnapshotUpdates {
|
||||
workspaceId?: string;
|
||||
labels?: AgentLabelPatch;
|
||||
modeId?: string;
|
||||
}
|
||||
|
||||
function formatProviderList(providers: readonly string[]): string {
|
||||
return providers.length > 0 ? providers.join(", ") : "none";
|
||||
}
|
||||
@@ -1445,23 +1460,13 @@ export class AgentManager {
|
||||
});
|
||||
}
|
||||
|
||||
importProviderSession(input: {
|
||||
provider: AgentProvider;
|
||||
providerHandleId: string;
|
||||
cwd: string;
|
||||
workspaceId: string;
|
||||
labels?: Record<string, string>;
|
||||
}): Promise<ManagedAgent> {
|
||||
importProviderSession(input: ImportProviderSessionInput): Promise<ManagedAgent> {
|
||||
return this.trackAgentRegistrationOperation(this.importProviderSessionInternal(input));
|
||||
}
|
||||
|
||||
private async importProviderSessionInternal(input: {
|
||||
provider: AgentProvider;
|
||||
providerHandleId: string;
|
||||
cwd: string;
|
||||
workspaceId: string;
|
||||
labels?: Record<string, string>;
|
||||
}): Promise<ManagedAgent> {
|
||||
private async importProviderSessionInternal(
|
||||
input: ImportProviderSessionInput,
|
||||
): Promise<ManagedAgent> {
|
||||
this.assertAcceptingAgentRegistrations();
|
||||
const resolvedAgentId = validateAgentId(this.idFactory(), "importProviderSession");
|
||||
this.requireEnabledProvider(input.provider);
|
||||
@@ -1475,6 +1480,7 @@ export class AgentManager {
|
||||
{
|
||||
provider: input.provider,
|
||||
cwd: input.cwd,
|
||||
modeId: input.modeId,
|
||||
},
|
||||
resolvedAgentId,
|
||||
);
|
||||
@@ -2307,10 +2313,7 @@ export class AgentManager {
|
||||
return nextRecord;
|
||||
}
|
||||
|
||||
async unarchiveSnapshot(
|
||||
agentId: string,
|
||||
updates?: { workspaceId?: string; labels?: AgentLabelPatch },
|
||||
): Promise<boolean> {
|
||||
async unarchiveSnapshot(agentId: string, updates?: UnarchiveSnapshotUpdates): Promise<boolean> {
|
||||
return this.runLifecycleMutation(agentId, () =>
|
||||
this.unarchiveSnapshotUnlocked(agentId, updates),
|
||||
);
|
||||
@@ -2318,7 +2321,7 @@ export class AgentManager {
|
||||
|
||||
private async unarchiveSnapshotUnlocked(
|
||||
agentId: string,
|
||||
updates?: { workspaceId?: string; labels?: AgentLabelPatch },
|
||||
updates?: UnarchiveSnapshotUpdates,
|
||||
): Promise<boolean> {
|
||||
const registry = this.requireRegistry();
|
||||
const record = await registry.get(agentId);
|
||||
@@ -2335,6 +2338,9 @@ export class AgentManager {
|
||||
...record,
|
||||
...(updates?.workspaceId ? { workspaceId: updates.workspaceId } : {}),
|
||||
...(updates?.labels ? { labels: applyLabelPatch(record.labels, updates.labels) } : {}),
|
||||
...(updates?.modeId
|
||||
? { config: { ...record.config, modeId: updates.modeId }, lastModeId: updates.modeId }
|
||||
: {}),
|
||||
archivedAt: null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
@@ -204,7 +204,7 @@ export async function unarchiveAgentState(
|
||||
_agentStorage: AgentStorage,
|
||||
agentManager: AgentUnarchiveController,
|
||||
agentId: string,
|
||||
updates?: { workspaceId?: string; labels?: Record<string, string | null> },
|
||||
updates?: { workspaceId?: string; labels?: Record<string, string | null>; modeId?: string },
|
||||
): Promise<boolean> {
|
||||
const unarchived = await agentManager.unarchiveSnapshot(agentId, updates);
|
||||
if (!unarchived) return false;
|
||||
|
||||
@@ -558,11 +558,13 @@ test("normalizeImportAgentRequest accepts new and legacy import handle shapes",
|
||||
requestId: "new-shape",
|
||||
providerId: "custom-codex",
|
||||
providerHandleId: "thread-1",
|
||||
modeId: "full-access",
|
||||
}),
|
||||
).toEqual({
|
||||
requestId: "new-shape",
|
||||
provider: "custom-codex",
|
||||
providerHandleId: "thread-1",
|
||||
modeId: "full-access",
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -633,7 +635,11 @@ class ProviderImportHarness {
|
||||
},
|
||||
unarchiveSnapshot: async (
|
||||
agentId: string,
|
||||
updates?: { workspaceId?: string; labels?: Record<string, string | null> },
|
||||
updates?: {
|
||||
workspaceId?: string;
|
||||
labels?: Record<string, string | null>;
|
||||
modeId?: string;
|
||||
},
|
||||
) => {
|
||||
if (this.unarchiveWait) {
|
||||
await this.unarchiveWait;
|
||||
@@ -654,6 +660,7 @@ class ProviderImportHarness {
|
||||
...record,
|
||||
workspaceId: updates?.workspaceId ?? record.workspaceId,
|
||||
labels,
|
||||
config: updates?.modeId ? { ...record.config, modeId: updates.modeId } : record.config,
|
||||
archivedAt: null,
|
||||
});
|
||||
return true;
|
||||
@@ -735,7 +742,12 @@ class ProviderImportHarness {
|
||||
};
|
||||
}
|
||||
|
||||
import(input: { providerHandleId: string; cwd?: string; labels?: Record<string, string> }) {
|
||||
import(input: {
|
||||
providerHandleId: string;
|
||||
cwd?: string;
|
||||
labels?: Record<string, string>;
|
||||
modeId?: string;
|
||||
}) {
|
||||
return importProviderSession({
|
||||
request: {
|
||||
requestId: "import-thread",
|
||||
@@ -743,6 +755,7 @@ class ProviderImportHarness {
|
||||
providerHandleId: input.providerHandleId,
|
||||
cwd: input.cwd,
|
||||
labels: input.labels,
|
||||
modeId: input.modeId,
|
||||
},
|
||||
workspaceProvisioning: createImportWorkspace("ws-restored"),
|
||||
agentManager: this.manager,
|
||||
@@ -752,7 +765,7 @@ class ProviderImportHarness {
|
||||
}
|
||||
}
|
||||
|
||||
test("importProviderSession uses the provider import path with the requested labels", async () => {
|
||||
test("importProviderSession uses the provider import path with the requested labels and mode", async () => {
|
||||
const harness = await ProviderImportHarness.create();
|
||||
harness.timeline = [
|
||||
{ type: "user_message", text: "Trace recent provider sessions" },
|
||||
@@ -763,6 +776,7 @@ test("importProviderSession uses the provider import path with the requested lab
|
||||
providerHandleId: "thread-imported",
|
||||
cwd: "/tmp/imported-agent",
|
||||
labels: { source: "import" },
|
||||
modeId: "full-access",
|
||||
});
|
||||
|
||||
expect(harness.freshImports).toEqual([
|
||||
@@ -772,6 +786,7 @@ test("importProviderSession uses the provider import path with the requested lab
|
||||
cwd: "/tmp/imported-agent",
|
||||
workspaceId: "ws-restored",
|
||||
labels: { source: "import" },
|
||||
modeId: "full-access",
|
||||
},
|
||||
]);
|
||||
expect(result).toEqual({
|
||||
@@ -813,6 +828,7 @@ test("importProviderSession restores an archived session as the same standalone
|
||||
providerHandleId: "thread-archived",
|
||||
cwd: harness.snapshot.cwd,
|
||||
labels: { source: "reimport" },
|
||||
modeId: "full-access",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -824,6 +840,7 @@ test("importProviderSession restores an archived session as the same standalone
|
||||
id: harness.snapshot.id,
|
||||
workspaceId: "ws-restored",
|
||||
labels: { existing: "label", source: "reimport" },
|
||||
config: { modeId: "full-access" },
|
||||
archivedAt: null,
|
||||
});
|
||||
expect((await harness.storage.get(harness.snapshot.id))?.labels).not.toHaveProperty(
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface NormalizedImportAgentRequest {
|
||||
cwd?: string;
|
||||
workspaceId?: string;
|
||||
labels?: Record<string, string>;
|
||||
modeId?: string;
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
@@ -112,6 +113,7 @@ export function normalizeImportAgentRequest(
|
||||
cwd: msg.cwd,
|
||||
workspaceId: msg.workspaceId,
|
||||
labels: msg.labels,
|
||||
...(msg.modeId ? { modeId: msg.modeId } : {}),
|
||||
requestId: msg.requestId,
|
||||
};
|
||||
}
|
||||
@@ -230,6 +232,7 @@ async function importProviderSessionNow(
|
||||
await unarchiveAgentState(input.agentStorage, input.agentManager, archivedRecord.id, {
|
||||
workspaceId,
|
||||
labels: Object.keys(labelPatch).length > 0 ? labelPatch : undefined,
|
||||
modeId: input.request.modeId,
|
||||
});
|
||||
try {
|
||||
const snapshot = await ensureAgentLoaded(archivedRecord.id, {
|
||||
@@ -253,6 +256,7 @@ async function importProviderSessionNow(
|
||||
cwd,
|
||||
workspaceId,
|
||||
labels,
|
||||
...(input.request.modeId ? { modeId: input.request.modeId } : {}),
|
||||
});
|
||||
await unarchiveAgentState(input.agentStorage, input.agentManager, snapshot.id);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user