Skip to content

Commit 5147ae6

Browse files
author
Mack Ding
committed
feat: improve clone UX and default sdk full access
1 parent d989f87 commit 5147ae6

11 files changed

Lines changed: 337 additions & 69 deletions

File tree

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ WORKSPACE_ROOT=.
1414
CODEX_WORKDIR=.
1515
CODEX_SDK_CONFIG={}
1616
CODEX_SDK_SKIP_GIT_REPO_CHECK=true
17-
CODEX_SDK_SANDBOX_MODE=workspace-write
18-
CODEX_SDK_APPROVAL_POLICY=
17+
CODEX_SDK_SANDBOX_MODE=danger-full-access
18+
CODEX_SDK_APPROVAL_POLICY=never
1919
CODEX_SDK_REASONING_EFFORT=
2020
CODEX_SDK_NETWORK_ACCESS_ENABLED=
2121
CODEX_SDK_WEB_SEARCH_MODE=

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ Key design goals:
7878

7979
## Screenshot
8080

81-
8281
### Install
8382

8483
```bash
@@ -364,15 +363,15 @@ SDK-related options:
364363
CODEX_BACKEND=sdk
365364
CODEX_SDK_CONFIG={}
366365
CODEX_SDK_SKIP_GIT_REPO_CHECK=true
367-
CODEX_SDK_SANDBOX_MODE=workspace-write
366+
CODEX_SDK_SANDBOX_MODE=danger-full-access
368367
CODEX_SDK_APPROVAL_POLICY=never
369368
CODEX_SDK_REASONING_EFFORT=high
370369
CODEX_SDK_NETWORK_ACCESS_ENABLED=true
371370
CODEX_SDK_WEB_SEARCH_MODE=live
372371
CODEX_SDK_ADDITIONAL_DIRECTORIES=["/abs/path/extra-worktree"]
373372
```
374373

375-
If `CODEX_SDK_SANDBOX_MODE` is unset, the bot now defaults SDK threads to `workspace-write` so normal coding tasks can modify files inside the active repo. Set it explicitly to `read-only` only if you want analysis-only behavior.
374+
If `CODEX_SDK_SANDBOX_MODE` is unset, the bot now defaults SDK threads to Full Access: `danger-full-access` with `approvalPolicy=never`. Set it explicitly to `workspace-write` or `read-only` only if you want a more restricted mode.
376375

377376
CLI-related options:
378377

scripts/telegramSmoke.ts

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import "dotenv/config";
22
import process from "node:process";
33
import {
4-
buildTelegramApiUrl,
5-
createTelegramFetchDispatcher,
6-
normalizeTelegramApiBase
4+
normalizeTelegramApiBase,
5+
requestTelegramJson
76
} from "../src/lib/telegramApi.js";
87

98
interface TelegramBotUser {
@@ -34,25 +33,24 @@ const expectedUsername = String(process.env.TELEGRAM_EXPECTED_USERNAME || "")
3433
.replace(/^@/, "");
3534
const smokeChatId = String(process.env.TELEGRAM_SMOKE_CHAT_ID || "").trim();
3635
const apiBase = normalizeTelegramApiBase(process.env.TELEGRAM_API_BASE);
37-
const dispatcher = createTelegramFetchDispatcher(
38-
process.env.TELEGRAM_PROXY_URL
39-
);
36+
const proxyUrl = process.env.TELEGRAM_PROXY_URL;
4037

4138
if (!token) {
4239
console.error("Missing BOT_TOKEN.");
4340
process.exit(1);
4441
}
4542

46-
const getMeResponse = await fetch(
47-
buildTelegramApiUrl(apiBase, token, "getMe"),
48-
dispatcher ? { dispatcher } : undefined
49-
);
50-
const getMePayload =
51-
(await getMeResponse.json()) as TelegramApiResponse<TelegramBotUser>;
43+
const { statusCode: getMeStatusCode, payload: getMePayload } =
44+
await requestTelegramJson<TelegramApiResponse<TelegramBotUser>>({
45+
apiBase,
46+
token,
47+
method: "getMe",
48+
proxyUrl
49+
});
5250

53-
if (!getMeResponse.ok || !getMePayload?.ok) {
51+
if (getMeStatusCode < 200 || getMeStatusCode >= 300 || !getMePayload?.ok) {
5452
console.error(
55-
`Telegram getMe failed: ${getMePayload?.description || getMeResponse.status}`
53+
`Telegram getMe failed: ${getMePayload?.description || getMeStatusCode}`
5654
);
5755
process.exit(1);
5856
}
@@ -68,26 +66,21 @@ if (expectedUsername && botUser.username !== expectedUsername) {
6866

6967
if (smokeChatId) {
7068
const message = `codex-telegram-claws smoke check ${new Date().toISOString()}`;
71-
const sendResponse = await fetch(
72-
buildTelegramApiUrl(apiBase, token, "sendMessage"),
73-
{
74-
method: "POST",
75-
headers: {
76-
"content-type": "application/json"
77-
},
78-
body: JSON.stringify({
69+
const { statusCode: sendStatusCode, payload: sendPayload } =
70+
await requestTelegramJson<TelegramApiResponse<TelegramSendMessageResult>>({
71+
apiBase,
72+
token,
73+
method: "sendMessage",
74+
proxyUrl,
75+
body: {
7976
chat_id: smokeChatId,
8077
text: message
81-
}),
82-
...(dispatcher ? { dispatcher } : {})
83-
}
84-
);
85-
const sendPayload =
86-
(await sendResponse.json()) as TelegramApiResponse<TelegramSendMessageResult>;
78+
}
79+
});
8780

88-
if (!sendResponse.ok || !sendPayload?.ok) {
81+
if (sendStatusCode < 200 || sendStatusCode >= 300 || !sendPayload?.ok) {
8982
console.error(
90-
`Telegram sendMessage failed: ${sendPayload?.description || sendResponse.status}`
83+
`Telegram sendMessage failed: ${sendPayload?.description || sendStatusCode}`
9184
);
9285
process.exit(1);
9386
}

src/bot/handlers.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import path from "node:path";
12
import { Markup } from "telegraf";
23
import {
34
buildPlanPrompt,
@@ -15,7 +16,10 @@ import type { Scheduler } from "../cron/scheduler.js";
1516
import { toErrorMessage } from "../lib/errors.js";
1617
import type { Router } from "../orchestrator/router.js";
1718
import type { PtyManager } from "../runner/ptyManager.js";
18-
import type { ShellManager } from "../runner/shellManager.js";
19+
import type {
20+
ShellExecutionResult,
21+
ShellManager
22+
} from "../runner/shellManager.js";
1923
import type { DevServerManager } from "../runner/devServerManager.js";
2024
import type { SkillRegistry } from "../orchestrator/skillRegistry.js";
2125

@@ -145,6 +149,65 @@ function suggestProjectName(
145149
return suggestClosestWord(input, candidates, threshold);
146150
}
147151

152+
function isInsideWorkspaceRoot(root: string, candidate: string): boolean {
153+
const target = path.resolve(candidate);
154+
const relative = path.relative(root, target);
155+
156+
return (
157+
relative === "" ||
158+
(!relative.startsWith("..") && !path.isAbsolute(relative))
159+
);
160+
}
161+
162+
function extractCloneTarget(
163+
result: Pick<
164+
ShellExecutionResult,
165+
"status" | "command" | "output" | "workdir"
166+
>
167+
): string | null {
168+
if (
169+
result.status !== "passed" ||
170+
!result.command ||
171+
!result.output ||
172+
!result.workdir ||
173+
!/^git\s+clone(?:\s|$)/i.test(result.command)
174+
) {
175+
return null;
176+
}
177+
178+
const match = result.output.match(/Cloning into '([^']+)'\.\.\./i);
179+
if (!match?.[1]) {
180+
return null;
181+
}
182+
183+
return path.resolve(result.workdir, match[1]);
184+
}
185+
186+
function buildShellSuccessFollowUp(
187+
locale: Locale,
188+
result: Pick<
189+
ShellExecutionResult,
190+
"status" | "command" | "output" | "workdir"
191+
>,
192+
workspaceRoot: string
193+
): string | null {
194+
const cloneTarget = extractCloneTarget(result);
195+
if (!cloneTarget) {
196+
return null;
197+
}
198+
199+
const relativePath = path.relative(workspaceRoot, cloneTarget) || ".";
200+
const repoCommand = isInsideWorkspaceRoot(workspaceRoot, cloneTarget)
201+
? `/repo ${relativePath}`
202+
: "";
203+
204+
return t(locale, "shellCloneSucceeded", {
205+
relativePath,
206+
workdir: cloneTarget,
207+
repoCommand
208+
});
209+
}
210+
148211
export function registerHandlers({
149212
bot,
150213
router,
@@ -509,6 +572,15 @@ export function registerHandlers({
509572
}
510573

511574
await sendChunkedMarkdown(ctx, t(locale, "shellResult", { result }));
575+
576+
const followUp = buildShellSuccessFollowUp(
577+
locale,
578+
result,
579+
status.workspaceRoot
580+
);
581+
if (followUp) {
582+
await sendChunkedMarkdown(ctx, followUp);
583+
}
512584
});
513585

514586
bot.command("dev", async (ctx: any) => {

src/bot/i18n.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,15 @@ const MESSAGES: Record<Locale, TranslationCatalog> = {
282282
"output:",
283283
result.output
284284
]),
285+
shellCloneSucceeded: ({ relativePath, workdir, repoCommand }) =>
286+
joinLines([
287+
"Clone completed.",
288+
`project: ${relativePath}`,
289+
`workdir: ${workdir}`,
290+
repoCommand
291+
? `next: switch to it with ${repoCommand}`
292+
: "next: switch to the cloned repo with /repo <name>"
293+
]),
285294
modelCurrent: ({ model }) =>
286295
`Current model: ${model || "inherit codex default"}`,
287296
modelReset: ({ closed }) =>
@@ -752,6 +761,15 @@ const MESSAGES: Record<Locale, TranslationCatalog> = {
752761
"output:",
753762
result.output
754763
]),
764+
shellCloneSucceeded: ({ relativePath, workdir, repoCommand }) =>
765+
joinLines([
766+
"仓库拉取完成。",
767+
`project: ${relativePath}`,
768+
`workdir: ${workdir}`,
769+
repoCommand
770+
? `下一步: 使用 ${repoCommand} 切换到这个仓库`
771+
: "下一步: 使用 /repo <name> 切换到新仓库"
772+
]),
755773
modelCurrent: ({ model }) =>
756774
`当前模型: ${model || "inherit codex default"}`,
757775
modelReset: ({ closed }) =>

src/config.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -339,11 +339,14 @@ export function loadConfig(): AppConfig {
339339
"read-only",
340340
"workspace-write",
341341
"danger-full-access"
342-
]) || (runnerBackend === "sdk" ? "workspace-write" : undefined),
343-
approvalPolicy: parseEnum<CodexApprovalPolicy>(
344-
process.env.CODEX_SDK_APPROVAL_POLICY,
345-
["never", "on-request", "on-failure", "untrusted"]
346-
),
342+
]) || (runnerBackend === "sdk" ? "danger-full-access" : undefined),
343+
approvalPolicy:
344+
parseEnum<CodexApprovalPolicy>(process.env.CODEX_SDK_APPROVAL_POLICY, [
345+
"never",
346+
"on-request",
347+
"on-failure",
348+
"untrusted"
349+
]) || (runnerBackend === "sdk" ? "never" : undefined),
347350
modelReasoningEffort: parseEnum<CodexReasoningEffort>(
348351
process.env.CODEX_SDK_REASONING_EFFORT,
349352
["minimal", "low", "medium", "high", "xhigh"]

src/lib/telegramApi.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Agent as HttpAgent } from "node:http";
22
import type { Dispatcher } from "undici";
3-
import { ProxyAgent } from "undici";
3+
import { ProxyAgent, request } from "undici";
44
import { HttpsProxyAgent } from "https-proxy-agent";
55

66
const DEFAULT_TELEGRAM_API_BASE = "https://api.telegram.org";
@@ -22,8 +22,7 @@ export function buildTelegramApiUrl(
2222
method: string
2323
): string {
2424
const normalized = normalizeTelegramApiBase(apiBase);
25-
const baseUrl = new URL(`${normalized}/`);
26-
return new URL(`bot${token}/${method}`, baseUrl).toString();
25+
return `${normalized}/bot${token}/${method}`;
2726
}
2827

2928
export function createTelegramApiAgent(
@@ -41,3 +40,48 @@ export function createTelegramFetchDispatcher(
4140
if (!normalized) return undefined;
4241
return new ProxyAgent(normalized);
4342
}
43+
44+
interface TelegramRequestResponse<T> {
45+
statusCode: number;
46+
payload: T;
47+
}
48+
49+
type TelegramRequestImpl = typeof request;
50+
51+
export async function requestTelegramJson<T>(
52+
{
53+
apiBase,
54+
token,
55+
method,
56+
proxyUrl,
57+
body
58+
}: {
59+
apiBase: string;
60+
token: string;
61+
method: string;
62+
proxyUrl?: string;
63+
body?: unknown;
64+
},
65+
requestImpl: TelegramRequestImpl = request
66+
): Promise<TelegramRequestResponse<T>> {
67+
const dispatcher = createTelegramFetchDispatcher(proxyUrl);
68+
const response = await requestImpl(
69+
buildTelegramApiUrl(apiBase, token, method),
70+
{
71+
method: body ? "POST" : "GET",
72+
headers: body
73+
? {
74+
"content-type": "application/json"
75+
}
76+
: undefined,
77+
body: body ? JSON.stringify(body) : undefined,
78+
dispatcher
79+
}
80+
);
81+
const rawBody = await response.body.text();
82+
83+
return {
84+
statusCode: response.statusCode,
85+
payload: JSON.parse(rawBody) as T
86+
};
87+
}

src/ops/healthcheck.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ import type { AppConfig } from "../config.js";
66
import { repairNodePtySpawnHelperPermissions } from "../runner/ptyPreflight.js";
77
import { extractCodexExecResponse } from "../bot/formatter.js";
88
import { toErrorMessage } from "../lib/errors.js";
9-
import {
10-
buildTelegramApiUrl,
11-
createTelegramFetchDispatcher
12-
} from "../lib/telegramApi.js";
9+
import { requestTelegramJson } from "../lib/telegramApi.js";
1310

1411
export type HealthcheckStatus = "pass" | "warn" | "fail";
1512

@@ -295,20 +292,19 @@ export async function runHealthcheck(
295292
const liveTelegramCheck = Boolean(options.telegramLiveCheck);
296293
if (liveTelegramCheck) {
297294
try {
298-
const url = buildTelegramApiUrl(
299-
config.telegram.apiBase,
300-
config.telegram.botToken,
301-
"getMe"
302-
);
303-
const dispatcher = createTelegramFetchDispatcher(
304-
config.telegram.proxyUrl
305-
);
306-
const response = await fetch(
307-
url,
308-
dispatcher ? { dispatcher } : undefined
309-
);
310-
const payload = (await response.json()) as TelegramGetMeResponse;
311-
if (response.ok && payload?.ok && payload?.result?.username) {
295+
const { statusCode, payload } =
296+
await requestTelegramJson<TelegramGetMeResponse>({
297+
apiBase: config.telegram.apiBase,
298+
token: config.telegram.botToken,
299+
method: "getMe",
300+
proxyUrl: config.telegram.proxyUrl
301+
});
302+
if (
303+
statusCode >= 200 &&
304+
statusCode < 300 &&
305+
payload?.ok &&
306+
payload?.result?.username
307+
) {
312308
checks.push(
313309
makeCheck(
314310
"telegram api",
@@ -321,7 +317,7 @@ export async function runHealthcheck(
321317
makeCheck(
322318
"telegram api",
323319
"fail",
324-
payload?.description || `HTTP ${response.status}`
320+
payload?.description || `HTTP ${statusCode}`
325321
)
326322
);
327323
}

0 commit comments

Comments
 (0)