Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions apps/ade-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,11 @@ ade actions run git.stageFile --arg laneId=lane-id --arg path=src/index.ts
ade actions run pty.resumeSession --arg sessionId=session-id
ade actions run external-sessions.list --input-json '{"scope":"project","limit":20}' --text # claude/codex/cursor/droid/opencode sessions on this machine; discovery that cannot run — `opencode` is not installed, say — fails the call when that provider is the only one asked for, rather than reporting an empty list; in a multi-provider scan it is skipped and logged
ade actions run external-sessions.import --input-json '{"provider":"codex","sessionId":"thread-id","laneId":"lane-1","target":"cli","mode":"resume"}' --text
ade actions run ai.piLoginProviders --text # Pi providers that can be signed into, with the auth methods each accepts and whether it is already configured
ade --role cto actions run ai.piLoginStart --input-json '{"providerId":"anthropic"}' --json # blocks until the human finishes Pi's own OAuth/device-code flow
ade actions call stream_events --arg category=runtime --json # drain piAuthStatus prompts/notices raised by an in-flight sign-in
ade --role cto actions run ai.piLoginSubmit --input-json "$(jq -n --arg v "$PI_API_KEY" '{providerId:"anthropic",requestId:"req-1",value:$v}')" # answer a prompt; keep the value out of argv and shell history
ade --role cto actions run ai.piLoginCancel --input-json '{"providerId":"anthropic"}'
ade cursor cloud agents list --text
ade cursor cloud agents create --repo https://github.com/owner/repo --prompt "fix flaky test" --auto-pr
ade --role cto github app-auth login # device-flow authorize the machine ADE GitHub App (headless/brain)
Expand All @@ -635,6 +640,26 @@ stored PAT order. Writes skip the read-only GitHub App. `github.getStatus`
reports the active read/write sources, per-credential failure/cooldown state,
fallback details, and any background-refresh pause without exposing tokens.

Pi sign-in has no typed command, the same way OpenCode's `ai.opencodeOAuth*`
actions do not. `ai.piLoginStart` blocks until a human finishes Pi's own browser
or device-code flow, and any prompt it raises is answered by a *second* call
(`ai.piLoginSubmit`) carrying a `requestId` that only appears on the
`piAuthStatus` runtime event — so the workflow is two processes plus an event
drain, not one command. From a terminal the shorter path is `ade code`'s
`/login`, which drops into `pi` and uses Pi's native interactive sign-in.

Three details when driving it through `ade actions run` anyway. The `piLogin`
start/submit/cancel actions are CTO-only, so they need `--role cto`;
`ai.piLoginProviders` is not. The two-process shape only works against a running
brain or desktop socket — a headless invocation builds its own runtime, so a
second `ade` process cannot see the first's in-flight sign-in. And
`ai.piLoginStart` carries its own transport floor (11 minutes, the same budget
the desktop client uses) so the CLI does not report a timeout while the daemon
is still waiting on the user; `--timeout-ms` still applies when it asks for
Comment on lines +656 to +658

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the Pi login timeout constants and their values.
rg -nP -C3 'PI_LOGIN_[A-Z_]*TIMEOUT_MS|piLoginStart' --type=ts -g '!**/*.test.ts'

Repository: arul28/ADE

Length of output: 148


🏁 Script executed:

#!/bin/bash
set -u

echo "## repo files matching names/types"
git ls-files | rg -n '(^|/)(README\.md|.*pi.*|.*login.*|.*(test|spec)\.ts|.*(js|ts)$)' | sed -n '1,200p'

echo
echo "## search all tracked files for PI_LOGIN/piLogin/start timeout constants"
rg -nP -C3 'PI_LOGIN_|piLoginStart|transport floor|minutes|timeout' -S -g '!node_modules' -g '!dist' -g '!build' . | sed -n '1,240p'

Repository: arul28/ADE

Length of output: 33173


🏁 Script executed:

#!/bin/bash
set -u

echo "## apps/ade-cli/README.md around lines 640-670"
sed -n '640,670p' apps/ade-cli/README.md

echo
echo "## exact search across tracked files for timeout constant patterns (case-insensitive)"
rg -nP -C3 'login.*timeout|timeout.*login|PI_LOGIN|LOGIN_IPC|START.*TIMEOUT|transport floor|10 minutes|11 minutes|660000|600000|659000' --glob '!apps/ade-cli/README.md' --glob '!node_modules' --glob '!dist' --glob '!build' . | sed -n '1,220p'

echo
echo "## focused constants/timeout declarations in CLI app source files"
rg -nP -C3 'TIMEOUT|timeout_ms|timeoutMs|setTimeout|Timeout|wait.*Login|login.*wait|transport' apps/ade-cli/src --glob '!*.test.ts' | sed -n '1,260p'

Repository: arul28/ADE

Length of output: 47672


🏁 Script executed:

#!/bin/bash
set -u

echo "## tracked Python/JS/TS files containing piLoginStart/PiLogin/PI_LOGIN constants"
rg -n -C6 'const P[I_1A-Z0-9_]+[I_1A-Z0-9_]*\s*=|export const P[I_1A-Z0-9_]+[I_1A-Z0-9_]*\s*=|PI_LOGIN|piLoginStart|PI_LOGIN_IPC_TIMEOUT_MS|TimeoutError|IPC_TIMEOUT|login timeout|login-timeline' \
  --glob '!apps/ade-cli/README.md' \
  --glob '!node_modules' \
  --glob '!dist' \
  --glob '!build' \
  --glob '!*.mjs' \
  --glob '!*.cjs' \
  . | sed -n '1,260p'

echo
echo "## files in apps/ade-cli/src containing any timeout-related declarations"
rg -n -C4 '^(const|let|var|export const|export let|export var) .*[Tt]imeout|TIMEOUT|timeout_ms|timeoutMs|Seconds\(\d+\)|Timeout' \
  apps/ade-cli/src \
  --glob '!*.test.ts' \
  | sed -n '1,320p'

Repository: arul28/ADE

Length of output: 49525


🏁 Script executed:

#!/bin/bash
set -u

echo "## desktop Pi login service header and timeout usage"
sed -n '1,80p' apps/desktop/src/main/services/ai/piAuthService.ts
echo
sed -n '250,278p' apps/desktop/src/main/services/ai/piAuthService.ts

echo
echo "## all tracked definitions of piAuthService or PiLogin timeout symbols"
rg -n -C4 '(PI_LOGIN_TIMEOUT_MS|10 \* 60 \* 1000|6[0-9]{5})|startPiLogin|listPiLoginProviders|piAuthStatus' apps/desktop/src/main/services apps/desktop/src/main/services apps/ade-cli/src docs docs/*.md apps --glob '!node_modules' --glob '!dist' --glob '!build' | sed -n '1,320p'

Repository: arul28/ADE

Length of output: 47062


Fix the documented Pi login timeout value.

apps/desktop/src/main/services/ai/piAuthService.ts defines PI_LOGIN_TIMEOUT_MS = 10 * 60 * 1000, but this section and docs/ARCHITECTURE.md describe an 11-minute transport budget. Update this to the actual 10-minute value unless the constant is being raised.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ade-cli/README.md` around lines 656 - 658, Update the Pi login timeout
documentation in this README section and the corresponding ARCHITECTURE.md
description to match PI_LOGIN_TIMEOUT_MS, which is 10 minutes; only document 11
minutes if the constant is intentionally raised instead.

more. `ai.piLoginSubmit`'s `value` can be a raw API key: pass it through
`--input-json` built from an environment variable rather than typing it inline,
and never echo the result.

`ade tools` is deliberately not backed by a service action. The pinned-tool cache
is a property of the machine's filesystem, not of a project runtime, so the
command calls `src/services/tools/` in-process and works on a headless box with
Expand Down
39 changes: 39 additions & 0 deletions apps/ade-cli/src/adeRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,45 @@ describe("adeRpcServer", () => {
expect(runtime.ptyService.dispose).not.toHaveBeenCalled();
});

it("exposes the Pi sign-in actions through ade actions, with the write half gated on cto", async () => {
// The CLI has no typed `pi login` command, so `ade actions run ai.piLogin*`
// is the only path to it. That makes these four names part of the CLI's
// contract: if the registry stops resolving them the escape hatch is gone.
const { runtime } = createRuntime();

const agentHandler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
await initialize(agentHandler, { role: "agent" });
const agentActions = await callTool(agentHandler, "list_ade_actions", { domain: "ai" });
const agentNames = agentActions.structuredContent.actions.map(
(entry: { name: string }) => entry.name,
);
expect(agentNames).toContain("ai.piLoginProviders");
expect(agentNames).not.toContain("ai.piLoginStart");

// Rejected on the role gate, before the service is reached — an agent can
// never spawn a Pi worker waiting on a human that is not there.
const denied = await callTool(agentHandler, "run_ade_action", {
domain: "ai",
action: "piLoginStart",
args: { providerId: "anthropic" },
});
expect(denied.isError).toBe(true);
expect(JSON.stringify(denied.error)).toMatch(/elevated role/i);

const ctoHandler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
await initialize(ctoHandler, { callerId: "cto-1", role: "cto" });
const ctoNames = (await callTool(ctoHandler, "list_ade_actions", { domain: "ai" }))
.structuredContent.actions.map((entry: { name: string }) => entry.name);
expect(ctoNames).toEqual(
expect.arrayContaining([
"ai.piLoginProviders",
"ai.piLoginStart",
"ai.piLoginSubmit",
"ai.piLoginCancel",
]),
);
});

it("routes app/navigate through the runtime navigation service", async () => {
const { runtime } = createRuntime();
const navigate = vi.fn(async () => ({ ok: true, mode: "desktop", windowId: 7 }));
Expand Down
23 changes: 23 additions & 0 deletions apps/ade-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
DEVELOPMENT_ADE_CLERK_OAUTH_CLIENT_ID,
} from "../../desktop/src/shared/accountDirectory";
import { isAdeRuntimeNamedPipePath } from "../../desktop/src/shared/adeRuntimeIpc";
import { PI_LOGIN_IPC_TIMEOUT_MS } from "../../desktop/src/main/services/localRuntime/localRuntimeTimeoutPolicy";
import { resolveMachineAdeLayout } from "./services/projects/machineLayout";
import { generateRpcAuthToken } from "./rpcAuth";
import { JsonRpcClient } from "./tuiClient/jsonRpcClient";
Expand Down Expand Up @@ -2572,6 +2573,28 @@ describe("ADE CLI", () => {
).toThrow(/account actions accept object input/);
});

it("gives a Pi sign-in the daemon's own transport budget instead of the CLI default", () => {
// ai.piLoginStart blocks on a human finishing Pi's OAuth/device-code flow.
// The default 10-minute request budget expires inside that window, so the
// plan has to carry the shared long-running floor or the CLI reports a
// timeout for a sign-in the daemon is still legitimately running.
const login = expectExecutePlan(
buildCliPlan(["actions", "run", "ai.piLoginStart", "--arg", "providerId=anthropic"]),
);
expect(login.minTimeoutMs).toBe(PI_LOGIN_IPC_TIMEOUT_MS);
expect(login.minTimeoutMs!).toBeGreaterThan(parseCliArgs([]).options.timeoutMs);

// Everything else keeps the default budget; the floor is table-driven, not
// a blanket raise.
expect(
expectExecutePlan(buildCliPlan(["actions", "run", "ai.piLoginProviders"])).minTimeoutMs,
).toBeUndefined();
expect(
expectExecutePlan(buildCliPlan(["actions", "run", "git.push", "--arg", "laneId=lane-1"]))
.minTimeoutMs,
).toBeUndefined();
});

it("builds chat create with both model and modelId plus explicit reasoning and fast-mode args", () => {
// This strict-equality assertion must not absorb the ambient parent
// default when the test itself runs inside an ADE-tracked agent shell.
Expand Down
30 changes: 29 additions & 1 deletion apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
readAutomationsEnvOverride,
} from "../../desktop/src/shared/automationAvailability";
import { parseLinearGraphQLInput } from "../../desktop/src/main/services/cto/linearGraphQLInput";
import { longRunningLocalRuntimeActionTimeoutMs } from "../../desktop/src/main/services/localRuntime/localRuntimeTimeoutPolicy";
import { browseProjectDirectories } from "../../desktop/src/main/services/projects/projectBrowserService";
import { createProjectScaffoldService } from "../../desktop/src/main/services/projects/projectScaffoldService";
import { resolveRepoRoot } from "../../desktop/src/main/services/projects/projectService";
Expand Down Expand Up @@ -324,6 +325,18 @@ type CliPlan =
* account action is CTO-only, so it must connect as the machine operator.
*/
connectRole?: GlobalOptions["role"];
/**
* Floor for this plan's request timeout, in milliseconds. `--timeout-ms`
* (and its 10-minute default) still applies when it is the larger of the
* two, so a caller can always ask for more patience but never less than
* the daemon's own budget for the action being run.
*
* Sourced from the shared long-running action table the desktop client
* uses, so the CLI cannot report a transport timeout while the daemon is
* still legitimately working — `ai.piLoginStart` blocks on a human
* finishing Pi's sign-in and is budgeted well past the CLI default.
*/
minTimeoutMs?: number;
historyOperationId?: string;
historyStatusFilter?: string;
historyListFilters?: {
Expand Down Expand Up @@ -11076,12 +11089,21 @@ function buildActionsPlan(args: string[]): CliPlan {
}
if (sub === "run") {
const target = parseActionRunTarget(args);
// Actions the daemon is allowed to spend longer on than the client's
// default budget carry their own transport floor, straight from the table
// the desktop client reads. Without it `ade actions run ai.piLoginStart`
// reports a timeout at 10 minutes while the sign-in it started is still
// waiting on the human.
const minTimeoutMs = longRunningLocalRuntimeActionTimeoutMs(
`${target.domain}.${target.action}`,
);
return {
kind: "execute",
label: "action run",
...(target.domain === "chat" && target.action === "createScheduledWork"
? { formatter: "scheduled-work-create" as const }
: {}),
...(minTimeoutMs != null ? { minTimeoutMs } : {}),
steps: [buildActionRunStep(args, target)],
};
}
Expand Down Expand Up @@ -21232,10 +21254,16 @@ async function executePlan(
// A plan may force a specific runtime role for its connection (e.g. `ade
// logout`, whose signOut account action is CTO-only). Honor it so the caller
// asserts the operator role and the machine account gate resolves to cto.
const connectionOptions =
const roledConnectionOptions =
plan.connectRole
? { ...baseConnectionOptions, role: plan.connectRole }
: baseConnectionOptions;
// An explicit --timeout-ms above the floor still wins; the floor only stops
// the client from giving up while the daemon is still inside its own budget.
const connectionOptions =
plan.minTimeoutMs != null && plan.minTimeoutMs > roledConnectionOptions.timeoutMs
? { ...roledConnectionOptions, timeoutMs: plan.minTimeoutMs }
: roledConnectionOptions;
try {
connection = await createConnection(connectionOptions, {
autoRegisterProject: shouldAutoRegisterProjectForPlan(plan),
Expand Down
Loading
Loading