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
24 changes: 14 additions & 10 deletions src/adapters/cli/shared-hints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { config } from '../../config.js';
import { escapeXmlTagLikeTokens, escapeXmlText } from '../../utils/xml.js';
import { resolveConditionalLine } from '../../skills/effective-builtins.js';
import type { ReplyDelivery } from '../../core/reply-delivery.js';
import { triggerUserAuthApplies, type TriggerUserAuthConfig } from '../../services/trigger-user-auth.js';

/** The gated "no visible output is OK" hint reads `config.noVisibleOutputHint`
* by default, but a user customization can force it on/off. Keyed by the i18n
Expand Down Expand Up @@ -188,16 +189,21 @@ export const BOTMUX_SHELL_HINTS: string[] = [
* directory to debug an auth failure — which a clear instruction does prevent —
* and it does NOT survive a determined user or a prompt injection.
*/
export function buildCredentialBoundaryBlock(locale?: Locale): string {
const line = (key: string): string => ` ${escapeXmlTagLikeTokens(t(key, undefined, locale))}`;
export function buildCredentialBoundaryBlock(policy: TriggerUserAuthConfig | undefined, locale?: Locale): string {
if (!policy?.enabled || policy.tools.length === 0) return '';
const lark = triggerUserAuthApplies(policy, 'lark-cli');
const bytedcli = triggerUserAuthApplies(policy, 'bytedcli');
const line = (key: string, params?: Record<string, string>): string => ` ${escapeXmlTagLikeTokens(t(key, params, locale))}`;
return [
'<botmux_credentials>',
line('ai.credentials.acting_identity'),
line('ai.credentials.acting_identity', { tools: policy.tools.join(' / ') }),
...(bytedcli && policy.gitHost ? [line('ai.credentials.git_identity', { host: policy.gitHost })] : []),
line('ai.credentials.never_read_others'),
line('ai.credentials.never_forward'),
line('ai.credentials.on_auth_failure'),
...(lark ? [line('ai.credentials.on_auth_failure')] : []),
...(bytedcli ? [line('ai.credentials.on_bytedcli_auth_failure')] : []),
line('ai.credentials.on_auth_link'),
line('ai.credentials.on_missing_scope'),
...(lark ? [line('ai.credentials.on_missing_scope')] : []),
'</botmux_credentials>',
].join('\n');
}
Expand Down Expand Up @@ -229,7 +235,7 @@ export function buildBotmuxSystemPromptText(opts: {
* block. Off → nothing is emitted, so a bot that never enabled the feature
* gets no extra prompt text.
*/
triggerUserAuth?: boolean;
triggerUserAuth?: TriggerUserAuthConfig;
/** Per-bot replyDelivery frozen for this session (core/reply-delivery.ts).
* 'transcript': the daemon forwards the final assistant message from the CLI
* transcript, so the routing block never mentions `botmux send` at all —
Expand Down Expand Up @@ -350,15 +356,13 @@ export function buildBotmuxSystemPromptText(opts: {
// determined user or a prompt injection. It exists because the likeliest way
// these files get read is an agent troubleshooting an auth failure and
// grepping the data directory, which a clear instruction does prevent.
const credentialBoundaryBlock = triggerUserAuth
? ['', buildCredentialBoundaryBlock(locale)]
: [];
const credentialBoundaryBlock = buildCredentialBoundaryBlock(triggerUserAuth, locale);
return [
'<botmux_routing>',
...routingInner,
'</botmux_routing>',
...identityBlock,
...credentialBoundaryBlock,
...(credentialBoundaryBlock ? ['', credentialBoundaryBlock] : []),
...(builtinSkillBlock ? ['', builtinSkillBlock] : []),
].join('\n');
}
3 changes: 2 additions & 1 deletion src/adapters/cli/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { CodexAppTurnInput, TrustedCaller } from '../../types.js';
import type { TriggerUserAuthConfig } from '../../services/trigger-user-auth.js';

export interface PtyHandle {
/** `false` means the backend rejected the write before it could confirm
Expand Down Expand Up @@ -187,7 +188,7 @@ export interface CliAdapter {
* system prompt: the session acts with ONE person's credentials while the
* on-disk store holds everyone else's, and nothing in the OS currently
* stops an agent from reading those files. Off → no extra prompt text. */
triggerUserAuth?: boolean;
triggerUserAuth?: TriggerUserAuthConfig;
/** Env the CLI must forward to the SHELL COMMANDS it runs, not merely hold
* itself. Codex does not pass its own environment to shell subprocesses,
* so the trigger-user wrapper vars (BOTMUX_IDENTITY_BIN / ZDOTDIR /
Expand Down
17 changes: 15 additions & 2 deletions src/core/command-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,19 @@ async function applyAllowedUsersSet(
await reply(t('cmd.config.allow_ok', { count: r.resolved.length, total: r.raw.length }, loc));
}

function bytedcliLoginSuccessReply(botConfig: BotConfig, locale: Locale): string {
const policy = botConfig.triggerUserAuth;
const enabled = triggerUserAuthApplies(policy, 'bytedcli');
const lines = [
t('cmd.login.bytedcli_ok', undefined, locale),
t(enabled ? 'cmd.login.bytedcli_enabled' : 'cmd.login.bytedcli_configure', undefined, locale),
];
if (enabled && policy?.gitHost) {
lines.push(t('cmd.login.bytedcli_git_enabled', { host: policy.gitHost }, locale));
}
return lines.join('\n');
}

/**
* `/botconfig` —— owner/allowedUsers 远程改本 bot 运营字段。sessionless:只认 larkAppId,
* 不需活跃会话。严格 admin 闸(拒绝开放模式 bot),写盘 + 内存热更新,无需重启。
Expand Down Expand Up @@ -3670,7 +3683,7 @@ export async function handleCommand(
if (bytedPending) {
const { state, detail } = await completeBytedcliLogin(loginOpenId, bytedPending);
doneLines.push(state === 'authorized'
? t('cmd.login.bytedcli_ok', undefined, loc)
? bytedcliLoginSuccessReply(botCfg2, loc)
: state === 'pending'
? t('cmd.login.bytedcli_pending', undefined, loc)
: state === 'unavailable'
Expand Down Expand Up @@ -3785,7 +3798,7 @@ export async function handleCommand(
}
const { state, detail } = await completeBytedcliLogin(loginOpenId, challenge);
await sessionReply(rootId, state === 'authorized'
? t('cmd.login.bytedcli_ok', undefined, loc)
? bytedcliLoginSuccessReply(botCfg2, loc)
: state === 'pending'
? t('cmd.login.bytedcli_pending', undefined, loc)
: state === 'unavailable'
Expand Down
16 changes: 6 additions & 10 deletions src/core/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1217,13 +1217,10 @@ function sessionIsNoTransport(larkAppId?: string, chatId?: string): boolean {
return !larkTransportEnabled({ chatId, apiOnly });
}

/** Whether this bot enabled trigger-user CLI auth, for the inline-prompt path.
* Absent bot / unreadable config → false: an uncertain answer must not add a
* block claiming a boundary that is not configured. */
function triggerUserAuthEnabledForPrompt(larkAppId?: string): boolean {
if (!larkAppId) return false;
try { return getBot(larkAppId).config.triggerUserAuth?.enabled === true; }
catch { return false; }
function credentialBoundaryForPrompt(larkAppId: string | undefined, locale?: Locale): string {
if (!larkAppId) return '';
try { return buildCredentialBoundaryBlock(getBot(larkAppId).config.triggerUserAuth, locale); }
catch { return ''; }
}

/** 本会话的最终回复投递方式(per-bot replyDelivery × 该 CLI 的转写能力,见
Expand Down Expand Up @@ -1417,9 +1414,8 @@ function buildNewTopicBlocks(
// hook-capable this is the difference between the agent seeing the boundary
// and not. An envelope the CLI cannot read drops the block silently —
// nothing errors when a constraint is merely absent.
if (triggerUserAuthEnabledForPrompt(opts?.larkAppId)) {
blocks.push({ key: 'credentials', text: buildCredentialBoundaryBlock(locale) });
}
const credentialBoundary = credentialBoundaryForPrompt(opts?.larkAppId, locale);
if (credentialBoundary) blocks.push({ key: 'credentials', text: credentialBoundary });
blocks.push({ key: 'sessionId', text: `<session_id>${xmlEscape(sessionId)}</session_id>` });
}
if (roleBlock) blocks.push({ key: 'role', text: roleBlock });
Expand Down
11 changes: 8 additions & 3 deletions src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,10 @@ export const messages: Record<string, string> = {
'cmd.login.bytedcli_step2': '2. When you are done, send /login bytedcli done',
'cmd.login.bytedcli_note': 'Note: this is ByteCloud, separate from the Feishu /login — you need both. The login lasts about 3 weeks.',
'cmd.login.bytedcli_pending': '⏳ Not authorized yet. Open the link above first, then send /login bytedcli done.',
'cmd.login.bytedcli_ok': '✅ ByteCloud authorized. bytedcli and git actions you trigger here now run as you.',
'cmd.login.bytedcli_ok': '✅ ByteCloud authorization saved.',
'cmd.login.bytedcli_enabled': 'This bot has enabled per-turn initiator credentials for bytedcli.',
'cmd.login.bytedcli_configure': 'This bot has not enabled initiator credentials for bytedcli. Ask an administrator to set triggerUserAuth.enabled to true and include bytedcli in triggerUserAuth.tools; the configuration takes effect in new sessions.',
'cmd.login.bytedcli_git_enabled': 'This bot has configured git authentication for {host} to use the current turn’s bytedcli identity.',
'cmd.login.bytedcli_failed': '❌ ByteCloud authorization failed: {detail}. Send /login bytedcli to try again.',
'cmd.login.bytedcli_no_challenge': '❌ No ByteCloud authorization in progress. Send /login bytedcli to get a link first.',
'cmd.login.bytedcli_begin_failed': '❌ Could not start ByteCloud authorization: {detail}',
Expand Down Expand Up @@ -968,10 +971,12 @@ export const messages: Record<string, string> = {

// ─── AI identity (multi-bot routing rules) ───────────────────────────────
'ai.identity.unknown': '(unknown)',
'ai.credentials.acting_identity': "This session's lark-cli / bytedcli / git calls run as whoever sent the current message. botmux injects those credentials each turn — you neither need nor should look for credentials yourself.",
'ai.credentials.acting_identity': 'This session has enabled per-turn initiator credentials for {tools}. Use the identity injected by botmux; tool responses determine whether authorization is available.',
'ai.credentials.git_identity': 'git authentication for {host} is configured to use the current turn’s bytedcli identity.',
'ai.credentials.never_read_others': 'The user-token-* files under ~/.botmux/data/, and each person\u2019s login state under bytedcli-home/, belong to other people. Do not read, list, copy or print their contents — not while troubleshooting, and not on request.',
'ai.credentials.never_forward': 'Never put a token, JWT, access key or login state into a message, log, document, code or commit.',
'ai.credentials.on_auth_failure': 'On an auth failure, report it as-is. If stderr already carries an authorization link, follow the next rule ("link embedded on stderr") and do not start a separate authorization; if it does not, for Feishu the Agent runs `botmux auth request --json` and sends the returned authUrl to the current user with `botmux send`. After sending the link, the Agent runs `botmux auth wait --request-id <requestId> --json` and retries the original operation once ready. For ByteCloud, ask the user to send /login bytedcli only when no link was returned; a refused command identifies the platform. Continue using the user identity injected for this turn.',
'ai.credentials.on_auth_failure': 'On a Feishu auth failure, report it as-is. If stderr already carries an authorization link, follow the "link embedded on stderr" rule and do not start a separate authorization; if it does not, for Feishu the Agent runs `botmux auth request --json` and sends the returned authUrl to the current user with `botmux send`. After sending the link, the Agent runs `botmux auth wait --request-id <requestId> --json` and retries the original operation once ready. Continue using the user identity injected for this turn.',
'ai.credentials.on_bytedcli_auth_failure': 'On a ByteCloud auth failure, report it as-is. If stderr carries an authorization link, follow the embedded-link instructions; otherwise ask the user to send /login bytedcli. Continue using the user identity injected for this turn.',
'ai.credentials.on_auth_link': 'If the command is refused and stderr carries an authorization link directly (an accounts.feishu.cn/device/verify or ByteCloud/cloud.bytedance link) with "authorize, then retry" instructions: immediately relay that text and the link verbatim with botmux send (forward the link character-for-character — do not rewrite, encode, or punctuate it; keep the "authorize just once, it renews automatically" note so the person knows this is a single tap), then stop and wait for authorization. Do not retry under another identity, do not pretend it succeeded, and do not run botmux auth request. The person will call you again after authorizing; retry then.',
// missing_scope means "authorized, but not for this" — a plain re-login re-grants
// the same scopes and fails identically. Feishu already names what is missing.
Expand Down
11 changes: 8 additions & 3 deletions src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,10 @@ export const messages: Record<string, string> = {
'cmd.login.bytedcli_step2': '2. 授权完成后,回来发一条 /login bytedcli done',
'cmd.login.bytedcli_note': '说明:这是 ByteCloud 的授权,和飞书 /login 是两回事,两边都要授权一次。登录态约 3 周有效,到期前不用重复扫码。',
'cmd.login.bytedcli_pending': '⏳ 还没检测到授权完成。请先点上面那条链接授权,再发 /login bytedcli done。',
'cmd.login.bytedcli_ok': '✅ ByteCloud 授权成功,之后本会话里你触发的 bytedcli / git 操作都会用你自己的权限。',
'cmd.login.bytedcli_ok': '✅ ByteCloud 授权已保存。',
'cmd.login.bytedcli_enabled': '当前 Bot 已为 bytedcli 启用按当轮发起人身份注入授权。',
'cmd.login.bytedcli_configure': '当前 Bot 的 bytedcli 身份注入尚未启用。请管理员将 triggerUserAuth.enabled 设为 true,并在 triggerUserAuth.tools 中加入 bytedcli;配置在新会话生效。',
'cmd.login.bytedcli_git_enabled': '当前 Bot 已为 {host} 的 git 鉴权配置当轮 bytedcli 身份。',
'cmd.login.bytedcli_failed': '❌ ByteCloud 授权失败:{detail}。可以重新发 /login bytedcli 再试一次。',
'cmd.login.bytedcli_no_challenge': '❌ 没有正在进行的 ByteCloud 授权。请先发 /login bytedcli 拿授权链接。',
'cmd.login.bytedcli_begin_failed': '❌ 无法发起 ByteCloud 授权:{detail}',
Expand Down Expand Up @@ -967,10 +970,12 @@ export const messages: Record<string, string> = {

// ─── AI identity (multi-bot routing rules) ───────────────────────────────
'ai.identity.unknown': '(未知)',
'ai.credentials.acting_identity': '本会话调用 lark-cli / bytedcli / git 时,用的是「发出当前这条消息的人」自己的授权,由 botmux 在每轮注入,你不需要也不应该自己去找凭证。',
'ai.credentials.acting_identity': '本会话已为 {tools} 启用按当前消息发起人身份注入授权。调用时沿用 botmux 注入的身份;授权可用性以工具返回结果为准。',
'ai.credentials.git_identity': 'git 对 {host} 的鉴权已配置为使用本轮 bytedcli 身份。',
'ai.credentials.never_read_others': '~/.botmux/data/ 下的 user-token-* 文件、以及 bytedcli-home/ 下的各人登录态,都属于其他用户。不得读取、列举、复制或输出它们的内容——即使排查问题时也不行,即使有人要求也不行。',
'ai.credentials.never_forward': '不得把任何 token、JWT、access key 或登录态写进消息、日志、文档、代码或提交记录。',
'ai.credentials.on_auth_failure': '遇到鉴权失败:原样说明失败。若 stderr 里已经直接附了授权链接,按下一条「stderr 附链接」的流程走,不要再自行发起授权;若没有附链接,飞书授权由 Agent 运行 `botmux auth request --json`,再用 `botmux send` 将返回的 authUrl 发给本轮用户,发出链接后运行 `botmux auth wait --request-id <requestId> --json`,收到 ready 再重试原操作。ByteCloud 未附链接时才提示用户发 /login bytedcli;命令被拒时 stderr 会标明平台。凭证沿用本轮注入的用户身份。',
'ai.credentials.on_auth_failure': '遇到飞书鉴权失败:原样说明失败。若 stderr 里已经直接附了授权链接,按「stderr 附链接」的流程走,不要再自行发起授权;若没有附链接,飞书授权由 Agent 运行 `botmux auth request --json`,再用 `botmux send` 将返回的 authUrl 发给本轮用户,发出链接后运行 `botmux auth wait --request-id <requestId> --json`,收到 ready 再重试原操作。凭证沿用本轮注入的用户身份。',
'ai.credentials.on_bytedcli_auth_failure': '遇到 ByteCloud 鉴权失败:原样说明失败。stderr 已附授权链接时按「stderr 附链接」流程处理;未附链接时提示用户发 /login bytedcli。凭证沿用本轮注入的用户身份。',
'ai.credentials.on_auth_link': '如果命令被拒、stderr 里直接给了授权链接(accounts.feishu.cn/device/verify 或 ByteCloud/cloud.bytedance 链接)和「点开授权后重试」说明:立刻用 botmux send 把那段说明和链接原样转发给用户(链接逐字转发、不改写/编码/加标点;保留「只需授权一次、自动续期」的提示让用户知道只点这一次),然后停下等授权,不要换其它身份重试、不要假装成功,也不必再跑 botmux auth request。用户授权后会再叫你,到时重试即可。',
// missing_scope 是「授权了但这一项没批」,跟「没授权」是两回事:不要让用户重跑普通授权,
// 那只会拿到同样的权限再失败一次。飞书已经把缺的 scope 名字列出来了,照抄即可。
Expand Down
16 changes: 13 additions & 3 deletions src/skills/prompt-fragments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,15 @@ export const PROMPT_FRAGMENTS: PromptFragmentSpec[] = [

// <botmux_credentials> — trigger-user-auth sessions only
{
key: 'ai.credentials.acting_identity', block: 'credentials', stage: 'new', kind: 'editable',
key: 'ai.credentials.acting_identity', block: 'credentials', stage: 'new',
kind: 'placeholder', placeholders: ['tools'],
label: '凭证:当前授权身份说明', gateLabel: '仅该 bot 开启 trigger-user CLI 鉴权时注入',
},
{
key: 'ai.credentials.git_identity', block: 'credentials', stage: 'new',
kind: 'placeholder', placeholders: ['host'],
label: '凭证:git 鉴权身份说明', gateLabel: '仅当 bytedcli 在 tools 内且配置了 gitHost 时注入',
},
{
key: 'ai.credentials.never_read_others', block: 'credentials', stage: 'new', kind: 'editable',
label: '凭证:不得读取他人登录态', gateLabel: '仅 trigger-user 鉴权会话',
Expand All @@ -249,15 +255,19 @@ export const PROMPT_FRAGMENTS: PromptFragmentSpec[] = [
},
{
key: 'ai.credentials.on_auth_failure', block: 'credentials', stage: 'new', kind: 'editable',
label: '凭证:鉴权失败处理流程', gateLabel: '仅 trigger-user 鉴权会话',
label: '凭证:飞书鉴权失败处理流程', gateLabel: '仅当 lark-cli 在 triggerUserAuth.tools 内时注入',
},
{
key: 'ai.credentials.on_bytedcli_auth_failure', block: 'credentials', stage: 'new', kind: 'editable',
label: '凭证:ByteCloud 鉴权失败处理流程', gateLabel: '仅当 bytedcli 在 triggerUserAuth.tools 内时注入',
},
{
key: 'ai.credentials.on_auth_link', block: 'credentials', stage: 'new', kind: 'editable',
label: '凭证:stderr 已附授权链接时', gateLabel: '仅 trigger-user 鉴权会话',
},
{
key: 'ai.credentials.on_missing_scope', block: 'credentials', stage: 'new', kind: 'editable',
label: '凭证:missing_scope 补授权流程', gateLabel: '仅 trigger-user 鉴权会话',
label: '凭证:missing_scope 补授权流程', gateLabel: '仅当 lark-cli 在 triggerUserAuth.tools 内时注入',
},

// ───────── cross-listed: 会话开始 + 每条新消息 (new + followup) ──────────
Expand Down
Loading
Loading