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
5 changes: 4 additions & 1 deletion docs/setup/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,12 @@ botmux skills install github:acme/agent-skills/skills/deploy-runbook
botmux skills install github:acme/agent-skills --path skills/deploy-runbook --ref main
```

私有仓库认证交给系统 Git 凭证、SSH agent 或 `gh auth`botmux 不保存 GitHub token;带 username/password/token 的 HTTPS Git URL 会被拒绝,避免凭证进入 registry 或 Dashboard。
私有仓库可复用部署机已有权限:GitHub HTTPS 来源依次读取进程或 `~/.botmux/.env` 中的 `GITHUB_TOKEN` / `GH_TOKEN`、当前 `gh auth` 账号;若注入的 token 鉴权失败,会先去掉临时请求头,让公开仓库匿名访问或系统 Git credential helper 接管,仍为鉴权失败时才自动改用 SSH URL 重试。显式 `git@github.com:owner/repo.git` 来源也直接使用 SSH agent/key。botmux 只把 HTTPS token 作为限定到 `github.com` 的临时 Git 请求头,不写入 URL、命令行或 registry;带 username/password/token 的 HTTPS Git URL 会被拒绝,避免凭证进入 Dashboard 和错误日志
Git/GitHub 的 `--path` 必须是仓库内相对路径;绝对路径、`..` segment 或解析到 checkout 外部的 symlink 会被拒绝。
Git 安装/更新会给底层 Git 命令设置超时,默认 60 秒;需要更长时间时可设置 `BOTMUX_SKILL_GIT_TIMEOUT_MS`。
Dashboard/CLI 的 Git `discover` 使用一次性 checkout,扫描结束即删除;只有实际安装/更新的来源保留在 `~/.botmux/skills/sources`,避免预览 URL 与最终安装 URL 不同时留下两份长期缓存。

Dashboard 安装/更新 job 完成后会通过现有 logger 写入 `[skills:audit]` 静态审计摘要,包括来源类型、commit、版本、文件/目录/symlink/字节数、相对可执行文件路径与 shebang runtime。审计不记录来源 URL 或绝对路径,也不会执行 Skill 的安装脚本、二进制或测试;失败 job 记录脱敏后的错误。

### 制品仓库(agentbuddy)

Expand Down
44 changes: 44 additions & 0 deletions src/core/github-auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { existsSync, readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { parse as dotenvParse } from 'dotenv';
Expand All @@ -10,6 +11,11 @@ export interface GithubAuthResolveOptions {
fileExists?: (path: string) => boolean;
}

export interface GithubGitAuthOptions extends GithubAuthResolveOptions {
/** Test seam; defaults to the active `gh auth` account on github.com. */
readGhToken?: () => string | null;
}

function firstNonBlank(values: Array<string | undefined>): string | null {
for (const value of values) {
const trimmed = typeof value === 'string' ? value.trim() : '';
Expand Down Expand Up @@ -53,6 +59,44 @@ function resolveGithubToken(options?: GithubAuthResolveOptions): string | null {
);
}

/**
* Inject GitHub HTTPS authentication without placing the token in the remote
* URL or process argv. Git treats GIT_CONFIG_KEY/VALUE pairs exactly like
* scoped `-c` configuration, while keeping command/error rendering secret-free.
*
* Existing injected config entries are preserved so callers that already use
* this mechanism do not lose their Git settings.
*/
function readGithubTokenFromGhCli(env: NodeJS.ProcessEnv): string | null {
try {
const token = execFileSync('gh', ['auth', 'token', '--hostname', 'github.com'], {
encoding: 'utf8',
env,
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 5_000,
}).trim();
return token || null;
} catch {
return null;
}
}

export function githubGitAuthEnv(options?: GithubGitAuthOptions): NodeJS.ProcessEnv {
const env = options?.env ?? process.env;
const token = resolveGithubToken(options)
?? (options?.readGhToken ?? (() => readGithubTokenFromGhCli(env)))();
if (!token) return {};

const parsedCount = Number.parseInt(env.GIT_CONFIG_COUNT ?? '', 10);
const count = Number.isSafeInteger(parsedCount) && parsedCount >= 0 ? parsedCount : 0;
const basic = Buffer.from(`x-access-token:${token}`, 'utf8').toString('base64');
return {
GIT_CONFIG_COUNT: String(count + 1),
[`GIT_CONFIG_KEY_${count}`]: 'http.https://github.com/.extraheader',
[`GIT_CONFIG_VALUE_${count}`]: `Authorization: Basic ${basic}`,
};
}

export function githubAuthHeaders(options?: GithubAuthResolveOptions): Record<string, string> {
const token = resolveGithubToken(options);
return token ? { Authorization: `Bearer ${token}` } : {};
Expand Down
22 changes: 22 additions & 0 deletions src/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import { createCliAdapterSync } from './adapters/cli/registry.js';
import type { ConnectorDefinition } from './services/connector-store.js';
import { hd2dAssetPath, hd2dStatus, startHd2dDownload } from './dashboard/hd2d-assets.js';
import {
buildSkillInstallAuditSummary,
installLocalSkillLinks,
readSkillRegistry,
removeInstalledSkill,
Expand Down Expand Up @@ -1915,9 +1916,30 @@ function startSkillJob(type: SkillJob['type'], run: () => Promise<SkillPackage |
job.skills = [result];
}
job.status = 'succeeded';
const audits = (job.skills ?? []).map(skill => {
try {
return buildSkillInstallAuditSummary(skill);
} catch {
return {
name: skill.name,
sourceType: skill.source.type,
auditError: 'static_scan_failed',
};
}
});
logger.info('[skills:audit] job succeeded', {
jobId: job.id,
operation: type,
skills: audits,
});
} catch (err: any) {
job.error = redactGitUrlCredentials(err?.message ?? String(err));
job.status = 'failed';
logger.warn('[skills:audit] job failed', {
jobId: job.id,
operation: type,
error: job.error,
});
} finally {
job.updatedAt = new Date().toISOString();
trimSkillJobs();
Expand Down
2 changes: 2 additions & 0 deletions src/dashboard/web/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1343,6 +1343,7 @@ const zh: DashboardMessages = {
'skills.sourceHelpAgentbuddy': '直接粘贴 agentbuddy 安装命令,例如 agentbuddy skill collection add <uid> / agentbuddy plugin collection add <uid>;开源 skills 用 skills add owner/repo 也认(走 GitHub 安装)。agentbuddy 命令需部署机已 agentbuddy login。',
'skills.agentbuddyNotFound': '部署机未找到 agentbuddy CLI。请在部署机全局安装,或设置 BOTMUX_AGENTBUDDY_CMD。',
'skills.agentbuddyNeedsLogin': '部署机 agentbuddy 未登录。请在部署机运行 agentbuddy login 后重试。',
'skills.gitNeedsAuth': 'GitHub 私有仓库鉴权失败。请在部署机执行 gh auth login,或在 ~/.botmux/.env 配置 GITHUB_TOKEN / GH_TOKEN;也可使用 SSH Git URL 复用主机 SSH key。',
'skills.agentbuddyCommandFailed': 'agentbuddy 安装失败,请查看部署机日志。',
'skills.agentbuddyTelemetryFailed': '内嵌 telemetry 清理失败,已中断安装(如需保留可设 BOTMUX_AGENTBUDDY_KEEP_TELEMETRY=1)。',
'skills.agentbuddyNoSkill': 'agentbuddy 未产出任何 Skill,请检查 identifier 是否正确。',
Expand Down Expand Up @@ -3147,6 +3148,7 @@ const en: DashboardMessages = {
'skills.sourceHelpAgentbuddy': 'Paste an agentbuddy install command, e.g. agentbuddy skill collection add <uid> / agentbuddy plugin collection add <uid>; the open-source skills CLI form skills add owner/repo also works (via GitHub install). agentbuddy commands need the deploy host logged in via agentbuddy login.',
'skills.agentbuddyNotFound': 'agentbuddy CLI not found on the deploy host. Install it globally there, or set BOTMUX_AGENTBUDDY_CMD.',
'skills.agentbuddyNeedsLogin': 'agentbuddy on the deploy host is not logged in. Run agentbuddy login there and retry.',
'skills.gitNeedsAuth': 'Private GitHub repository authentication failed. Run gh auth login on the deploy host, configure GITHUB_TOKEN / GH_TOKEN in ~/.botmux/.env, or use an SSH Git URL with the host SSH key.',
'skills.agentbuddyCommandFailed': 'agentbuddy install failed — check the deploy-host logs.',
'skills.agentbuddyTelemetryFailed': 'Embedded-telemetry removal failed; install aborted (set BOTMUX_AGENTBUDDY_KEEP_TELEMETRY=1 to keep it).',
'skills.agentbuddyNoSkill': 'agentbuddy produced no Skill — check the identifier.',
Expand Down
7 changes: 5 additions & 2 deletions src/dashboard/web/skills-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -941,10 +941,13 @@ function SkillsPage() {

// Translate the backend's terse install error codes into actionable messages.
// agentbuddy runs on the deploy host, so its failures (missing CLI, not logged
// in) need host-side guidance the operator can act on. Non-agentbuddy codes
// fall through unchanged.
// in) need host-side guidance the operator can act on. Git authentication
// failures get equivalent host-side guidance; other codes fall through.
function mapInstallError(raw: string): string {
const msg = raw || '';
if (msg.startsWith('skill_git_command_failed') && /authentication failed|could not read username|repository not found|unauthor|\b401\b|\b403\b/i.test(msg)) {
return tr('skills.gitNeedsAuth');
}
if (msg.startsWith('agentbuddy_not_found')) return tr('skills.agentbuddyNotFound');
if (msg.startsWith('agentbuddy_command_failed')) {
return /login|credential|unauthor|not logged|401|403/i.test(msg)
Expand Down
Loading