IronClaw GitHub 扩展完全指南:49 个工具的 WASM 能力面、凭据注入与 Webhook 归一化
IronClaw GitHub 扩展完全指南:49 个工具的 WASM 能力面、凭据注入与 Webhook 归一化
本篇技术指南以 IronClaw 开源仓库中 github 扩展包 为核心,系统讲解这个以 WASM guest 形式交付、覆盖仓库/Issue/PR/评审/文件/Release/Actions 等 49 个工具的数据型扩展:包括 manifest 声明与工具注册模型、[auth.github] 凭据体系与宿主侧 egress 安全边界、WASM 源码内部的请求路由与参数校验实现,以及 Webhook 事件归一化到系统事件的工作机制。读完你将掌握该扩展从"安装清单"到"字节级实现"的完整脉络,并能在 IronClaw 中以源码为据排查 GitHub 工具的授权、校验与错误分类问题。
扩展一览:目录中工具面最大的数据型包
github 是 IronClaw 扩展目录(crates/extensions/packages/)中 工具面最大的包:扩展 id 为 github,共 49 个工具(从 github.get_repo 到 github.handle_webhook),并附带 [auth.github] 认证配置。它被归类为 data-only 包——没有独立 crate,行为完全以 WASM guest 形式随包交付。
| 维度 | 值 |
|---|---|
| 扩展 id | github |
| 工具面 | 49 个工具(github.get_repo … github.handle_webhook)+ [auth.github] |
| Vendor(凭据权威) | github |
| 运行时 | wasm —— 已提交产物在 wasm/,guest 源码在 wasm-src/(被排除在工作区构建图之外) |
| 包内容 | manifest.toml、prompts/、schemas/、wasm/、wasm-src/ |
| 嵌入方式 | 由 ironclaw_extension_support::packages::github 通过 include_str!/include_bytes! 内联 |
| 测试 / 检查 | manifest 投影:cargo test -p ironclaw_extension_registry;产物新鲜度:python3 scripts/ci/check-wasm-artifact-freshness.py |
数据型包之所以"无需 crate",遵循的是 crates/extensions/AGENTS.md 中明确的 package-to-crate 规则:只有实现 channel adapter 或 provider surface、或携带重量级原生依赖的包才需要自己的 crate;其余包是一个由 manifest + 资源数据构成的目录,需要原生(非 WASM)工具逻辑时,作为模块放进共享的 ironclaw_extension_support crate,并以包的 manifest 身份注册。github 恰好属于"纯 WASM 交付"一类,因此整个行为面都在 wasm/github_tool.wasm 中。
包目录解剖:五类资产的各自职责
crates/extensions/packages/github/
├── manifest.toml # 扩展声明:schema v3、运行时、49 个工具、[auth.github]
├── prompts/github/ # 每个工具的模型可见 prompt 文档(49 个 .md)
├── schemas/github/ # 每个工具的输入/输出 JSON Schema(.input.v1.json / .output.v1.json)
├── wasm/
│ └── github_tool.wasm # 已编译并提交的 WASM guest 产物
└── wasm-src/ # guest 源码(独立 Cargo 工程,不参与工作区构建)
├── Cargo.toml
└── src/
├── lib.rs # Guest trait 实现:execute / schema / description
├── dispatch.rs # capability_id → 具体操作的派发
├── request.rs # GitHub REST 请求构造与宿主 egress 调用
├── response.rs # 响应处理
├── schema.rs # 静态 JSON Schema 生成
├── types.rs # GitHubAction 等反序列化类型
├── validation.rs # 输入长度/路径/ref/搜索限定符校验
├── webhook.rs # Webhook 归一化为系统事件
└── api/ # 按领域拆分的 API 实现(actions/contents/issues/pulls/repos/review_threads/search)
artifact boundary(产物边界) 是这套结构的关键约束:已提交的 wasm/*.wasm 由带外构建产生(./scripts/build-wasm-extensions.sh --first-party),wasm-src/ 的 guest crate 被排除在工作区构建图之外;scripts/ci/check-wasm-artifact-freshness.py 把每个已提交产物与其 wasm-src/ 目录树的摘要(记录在 scripts/ci/wasm-src-digests.toml)绑定——修改 wasm-src/ 而不重建并重新记录摘要会导致 CI 失败。也就是说,扩展的行为以"源码 → 摘要 → 产物"的三方一致为交付前提。
manifest 声明:49 个工具的注册模型
manifest.toml 以 schema_version = "reborn.extension_manifest.v3" 声明(当前包版本 0.2.8,trust = "first_party_requested"),运行时块指向 WASM 模块:
schema_version = "reborn.extension_manifest.v3"
id = "github"
name = "GitHub"
version = "0.2.8"
description = "GitHub repository, issue, pull request, search, branch, file, release, workflow, fork, and webhook capabilities."
trust = "first_party_requested"
[runtime]
kind = "wasm"
module = "wasm/github_tool.wasm"
每个工具都通过 [[tools]] 数组条目声明,携带四组关键元数据,以 github.get_repo 为例:
[[tools]]
origin_gate_matrix = { loop_run = "gated_unless_granted", product = "forbidden", automation = "forbidden" }
id = "github.get_repo"
description = "Get a GitHub repository."
effects = ["network", "use_secret"]
default_permission = "allow"
visibility = "model"
input_schema_ref = "schemas/github/get_repo.input.v1.json"
prompt_doc_ref = "prompts/github/get_repo.md"
origin_gate_matrix:所有 GitHub 工具统一为loop_run = "gated_unless_granted"(循环运行中默认受门控、除非被授予)、product与automation均为forbidden——即该工具面只对 agent loop 开放,产品直连与自动化流程被禁止。effects:读操作为["network", "use_secret"];写操作(创建/更新/删除、external_write类副作用)追加"external_write"。default_permission:只读查询类工具为allow(如get_repo、list_issues、get_pull_request、get_workflow_runs、get_job_logs等);一切产生外部写入的工具为ask(如create_repo、create_issue、merge_pull_request、trigger_workflow、delete_file等)。值得注意的两个特例:github.search_code虽是读操作却默认为ask(代码搜索内容敏感,需要显式确认);github.handle_webhook无任何effects(纯本地归一化)但同样为ask。input_schema_ref/prompt_doc_ref:分别指向schemas/github/*.input.v1.json与prompts/github/*.md;仅github.get_job_logs额外声明output_schema_ref = "schemas/github/raw_output.v1.json"(纯文本日志输出)。
49 个工具全景
按能力域分类(均为 github. 前缀):
| 能力域 | 工具 |
|---|---|
| 仓库 | get_repo、create_repo、list_repos、fork_repo、get_authenticated_user |
| Issues | list_issues、get_issue、create_issue、update_issue、list_issue_comments、create_issue_comment、comment_issue(兼容别名)、add_issue_labels、remove_issue_label、add_issue_assignees、remove_issue_assignees |
| Pull Requests | list_pull_requests、get_pull_request、create_pull_request、update_pull_request、get_pull_request_files、merge_pull_request、get_combined_status |
| 评审与评论 | create_pr_review、list_pull_request_comments、reply_pull_request_comment、get_pull_request_reviews、list_pull_request_review_threads、resolve_review_thread、unresolve_review_thread |
| 搜索 | search_repositories、search_code、search_issues_pull_requests、search_issues(兼容别名) |
| 分支与文件 | list_branches、create_branch、get_file_content、create_or_update_file、delete_file |
| Releases | list_releases、create_release |
| GitHub Actions | trigger_workflow、get_workflow_runs、get_workflow_run_jobs、get_job_logs、get_workflow_run_artifacts、rerun_failed_workflow_run_jobs、rerun_workflow_job |
| Webhook | handle_webhook |
manifest 中对两个"兼容别名"有明确说明:github.comment_issue 是 github.create_issue_comment 的别名;github.search_issues 是 github.search_issues_pull_requests 的紧凑结果别名。这类别名在 WASM 派发层被归一(见下文 dispatch.rs),模型侧不会感知到两套实现。
认证与凭据安全:[auth.github] 与宿主 egress 边界
github 扩展的凭据体系在 manifest 尾部以 [auth.github] 声明,采用 API Key 方式,配以在线校验:
[auth.github]
method = "api_key"
display_name = "GitHub personal access token"
fields = [ { handle = "github_runtime_token", label = "Personal access token", secret = true } ]
validation = { method = "GET", url = "https://api.github.com/user", success_status = [200], inject = { handle = "github_runtime_token", type = "header", name = "authorization", prefix = "Bearer " } }
- 凭据句柄固定为
github_runtime_token,标记为secret,产品层展示名为 "GitHub personal access token"。 - 绑定校验:对
GET https://api.github.com/user发起请求,仅当返回 200 时认为凭据有效;该校验请求以Bearer <token>形式注入authorization头。
每次调用时的凭据注入
每个工具的 [[tools.credentials]] 声明了运行时凭据注入协议,全包统一:
[[tools.credentials]]
handle = "github_runtime_token"
vendor = "github"
audience = { scheme = "https", host = "api.github.com" }
injection = { type = "header", name = "authorization", prefix = "token " }
placeholder_env = "GH_TOKEN"
其要点是 凭据不进入 WASM guest,而是由宿主在 egress(出站)时注入。manifest 注释也引用了 GitHub CLI 的同类做法:Authorization: token <GH_TOKEN>。这意味着 guest 代码(wasm-src/src/lib.rs 文件头注释)"从不读取或构造 GitHub token"——token 只在宿主侧按 audience(仅 api.github.com)匹配后附加到请求头。placeholder_env = "GH_TOKEN" 则作为工具说明中的环境变量占位符供模型理解。
重定向白名单:get_job_logs 的 Azure blob 跳转
github.get_job_logs 是唯一声明了额外 network_targets 的工具,对应 GitHub Actions 日志接口 302 重定向到短期预签名 Azure blob URL 的行为:
# The logs endpoint 302-redirects to a short-lived, pre-signed Azure blob URL
# (e.g. productionresultssaN.blob.core.windows.net). The host egress follows the
# redirect only after re-authorizing the destination against this allowlist and
# strips the Authorization header on the hop, so the GitHub token is never sent
# to blob storage. No credential is injected for these hosts.
network_targets = [
{ scheme = "https", host_pattern = "*.blob.core.windows.net" },
]
这是凭据防泄漏的一个典型设计:宿主 egress 仅在目标地址通过 *.blob.core.windows.net 白名单复核后才跟随重定向,并在跳转那一跳剥离 Authorization 头——GitHub token 永远不会被送往 blob 存储;该目标也不注入任何凭据。其余 48 个工具的网络目标只有 api.github.com 这一个 audience。
WASM guest 内部:从 capability_id 到 HTTP egress 的完整调用链
扩展的行为在 wasm-src/ 中以独立 Rust crate(github-tool,edition 2021,crate-type = <a href="https://link.gitcode.com/i/8db0e61fa930fe73cce18559225bc5aa" target="_blank">"cdylib"],release 配置 opt-level = "z" + lto + panic = "abort" + strip)实现,通过 WIT 接口 sandboxed-tool(见 [crates/lanes/ironclaw_wasm/wit/tool.wit)与宿主交互。
入口:Guest trait 三件套
lib.rs 实现 exports::near::agent::tool::Guest:
execute(req):把请求参数与调用上下文交给dispatch::execute_inner,成功返回Response::Success,失败则取出最近一次 401 响应中捕获的 provider message 组装GuestFailure(含kind、code、message三个字段)。schema():返回静态生成的 JSON Schema(oneOf形式,每个动作一个分支),供宿主在工具发现时使用。description():一段面向模型的能力描述。
派发:操作由宿主上下文决定,而非参数形状
dispatch.rs 的关键设计是 动作来源与参数形状解耦:
pub(crate) fn action_from_context(context: Option<&str>) -> Result<String, String> {
let context = context.ok_or_else(|| "missing_invocation_context".to_string())?;
let context: ToolContext =
serde_json::from_str(context).map_err(|_| "invalid_invocation_context".to_string())?;
action_name_from_capability_id(&context.capability_id)
.ok_or_else(|| "unsupported_github_capability".to_string())
}
宿主通过调用上下文的 capability_id(如 github.get_issue)选择操作;随后 params_with_action 把动作名注入参数对象(并拒绝参数中已含 action 键),再反序列化为强类型的 GitHubAction 枚举,最后 match 分发到 api/ 各模块的具体函数。lib.rs 测试 operation_comes_from_host_context_not_param_shape 直接验证了这条约定:github.get_issue → get_issue、github.comment_issue → create_issue_comment(别名归一)、github.get_authenticated_user → get_authenticated_user;而缺失上下文返回 missing_invocation_context、未知 capability 返回 unsupported_github_capability。
同时 params_with_action 中 serde_json::from_str(params) 的反序列化是 默认拒绝未知字段 的(deny_unknown_fields),测试 serde_rejects_unknown_fields_before_egress 证明多余字段会在发起任何网络请求前被 invalid_parameters 拒绝。
请求层:固定的 API 常量与宿主 http_request
request.rs 定义了三个固定常量:
const GITHUB_API_ROOT: &str = "https://api.github.com";
const GITHUB_API_VERSION: &str = "2026-03-10";
const HTTP_TIMEOUT_MS: u32 = 10_000;
每次请求固定携带四个头:Accept: application/vnd.github+json、Content-Type: application/json、X-GitHub-Api-Version: 2026-03-10、User-Agent: IronClaw-GitHub-Reborn-WASM,然后调用宿主提供的 crate::near::agent::host::http_request(method, url, headers, body, timeout)(超时 10 秒)。响应处理有专门的分类逻辑:
- 2xx:返回 JSON 文本(空 body 时返回
{"status": <code>}); - 422 且响应体形如 GitHub 的
{"message": "Validation Failed", "errors": [...]}:归为github_api_error_status_422_validation(输入错误);单纯 "Validation failed, or the endpoint has been spammed." 这类消息不会被当作输入错误,避免误分类滥用风控响应; - 401:从响应体提取
message字段(Bad credentials等),截断到最多 512 字符存入线程局部槽位,供execute拼进guest-failure.message随AuthRequired一起传给宿主上的认证门控; - 其余状态码:
github_api_error_status_<code>。
宿主侧 HttpFailure 也会被映射为语义化错误码:AuthRequired、invalid_parameters、github_api_body_limit(输出过大)、github_api_egress_denied / github_api_redirect_denied(网络被拒)、github_api_request_failed 等。
错误分类到 ErrorKind
lib.rs 的 guest_error_kind 把上述字符串错误码收敛到 WIT 层的五类 ErrorKind:
| ErrorKind | 典型错误码 |
|---|---|
AuthRequired |
AuthRequired、github_api_error_status_401 |
Input |
invalid_parameters、invalid_repository、invalid_query_*、invalid_* 系列、github_api_error_status_422_validation、字符串校验错误(Invalid labels:...、Invalid assignees:...、invalid_comments:...) |
OutputTooLarge |
github_api_body_limit |
Executor |
github_api_timeout、github_api_executor_failed |
NetworkDenied |
github_api_egress_denied、github_api_redirect_denied |
Client |
github_api_error_status_403、github_api_error_status_429 |
OperationFailed |
其余一切(含通用 422) |
测试 guest_error_kind_does_not_classify_generic_422_as_input 特意锁死了"通用 422 不归为输入错误"这一边界。
输入校验:出站前的第一道防线
validation.rs 在 egress 之前完成全部输入校验,三个关键上限常量:
const MAX_TEXT_LENGTH: usize = 65536; // 单字段文本上限(body、path 等)
const MAX_SEARCH_QUERY_LENGTH: usize = 512; // 搜索查询上限
const MAX_REPOSITORY_SEGMENT_LENGTH: usize = 100; // 仓库名/owner 段上限
主要校验规则(均有测试覆盖):
- 路径安全:
validate_repo_path拒绝..、.相对段与空段(Invalid path: relative path segments not allowed/Invalid path: empty segment not allowed),路径段按/拆分后逐段百分号编码; - Git ref 校验:
validate_git_ref拒绝包含..、:、?、[、\、^、~、@{、//、以//.开头或结尾、以.lock结尾、含控制字符或空格的名字;normalize_ref_lookup只允许 branch/tag ref(refs/heads/*、refs/tags/*),明确拒绝裸 commit SHA(40 位十六进制); - 分页与数量:
page不能为 0,limit必须在 1..=100;order只能是asc/desc;add_issue_assignees的 assignees 上限 10 个(schemamaxItems: 10); - 搜索限定符:
build_issue_search_query支持repo:、author:、assignee:、involves:、state:、is:等限定符组合,且repository与owner/repo互斥(同时给出报invalid_repository);限定符值拒绝空白、控制字符、:、"、(、); - 搜索排序白名单:issue 搜索只允许 GitHub 官方支持的 sort 枚举(
comments、created、updated、reactions系列、interactions),PR 列表只允许created/updated/popularity/long-running。
这些规则的意义在于:绝大多数恶意或错误输入在发出任何网络请求之前就被拒绝(测试中反复断言 requests().is_empty()),既保护 GitHub API 调用额度,也缩小了 guest 构造出站请求的攻击面。
响应紧凑化:高基数接口的模型友好投影
manifest 顶部的注释解释了响应策略:"GitHub REST endpoints return heterogeneous provider-owned response shapes. Detail tools preserve those raw JSON contracts."——即:
- 详情类工具(
get_issue、get_pull_request、get_repo等)原样保留 provider 的原始 JSON 契约; - 高基数的列表/搜索工具(
list_pull_requests、search_issues_pull_requests等)保持顶层数组形状,但把每条记录投影为紧凑的模型友好摘要,完整详情只通过对应的详情工具获取。
lib.rs 测试 list_pull_requests_compacts_realistic_large_provider_response 用一份超过 10MB 的 provider fixture 验证:100 条紧凑 PR 摘要输出被压到 100KB 以内(output.len() < 100 * 1024),同时保留 number、title、user.login、labels[].name、assignees[].login、requested_reviewers[].login、milestone.title、head.ref/sha 等模型决策所需字段,而 body、_links、head.repo 等大字段被剥离。分页语义也得到保留:list_issues 等工具把逻辑分页映射到上游 per_page+page 扫描,并设有上游页扫描上限(测试证明 10 页全是 PR 时以 github_issue_page_scan_limit 中止,避免无限翻页)。
Webhook 归一化:把 GitHub 事件翻译成系统事件意图
github.handle_webhook 不产生任何外部副作用(effects = <a href="https://link.gitcode.com/i/54ce48280cbaa5797fa1fb36870ec96f" target="_blank">]),它在 guest 内部把 GitHub webhook payload 归一化为 SystemEventIntent(来源 github)。核心逻辑在 [webhook.rs:
- 事件识别:从请求头取
X-GitHub-Event(缺失或为空则报Missing X-GitHub-Event header),body 必须携带body_json; - 事件类型映射(
github_event_type):
| X-GitHub-Event | 归一化事件类型 |
|---|---|
issues |
issue |
pull_request |
pr |
issue_comment |
pr.comment(payload 含 /issue/pull_request)或 issue.comment |
pull_request_review |
pr.review |
pull_request_review_comment |
pr.review_comment |
pull_request_review_thread |
pr.review_thread |
check_suite |
ci.check_suite |
check_run |
ci.check_run |
status |
ci.status |
| 其他 | 原样保留 |
随后若 payload 含非空 action 字段,则拼接为 <base>.<action>(如 issue.opened、pr.closed、ci.check_run.completed);
- Payload 增强(
github_enriched_payload):以put_if_missing语义(仅当键不存在时补入)向原 payload 注入统一的扁平字段:event、event_type、delivery_id(取自X-GitHub-Delivery头)、action、repository_name(/repository/full_name)、repository_owner、sender_login、issue_number、pr_number(含 issue_comment 场景的 fallback:/pull_request/number缺失时若存在/issue/pull_request则取/issue/number)、comment_author、comment_body、review_state、pr_state、pr_merged、pr_draft、base_branch、head_branch、ci_status、ci_conclusion(check_run/check_suite/status 三种来源依次兜底)。
输出结构为 { "accepted": true, "emit_events": <a href="https://link.gitcode.com/i/dce052931603ef00a88ce527a582c3ed" target="_blank">{ "source": "github", "event_type": ..., "payload": ... }] },供宿主接入统一的事件/触发管线。这样下游消费者(如 [ironclaw_triggers 生态)只需处理一套扁平、带语义字段的事件形状,而无需理解各 GitHub 事件类型的原始差异。
嵌入与产物分发:extension_support 如何装载该包
数据型包通过 PACKAGES 清单在 crates/extensions/ironclaw_extension_support/src/packages/github.rs 注册:include_str!("../../../packages/github/manifest.toml") 内联 manifest、include_bytes!("../../../packages/github/wasm/github_tool.wasm") 内联 WASM 字节,schemas/ 与 prompts/ 下的每个资产也逐个 include_bytes! 挂入资源表,并附带 setup_url: Some("https://github.com/settings/personal-access-tokens/new") 作为凭据配置引导。这意味着 github 包在编译期就整体烘焙进二进制,运行时无需从磁盘加载任何包文件。
测试与质量门禁
该扩展的可验证性体现在三个层面:
- Guest 内部单元测试:
wasm-src/src/lib.rs内的#[cfg(test)]测试模块通过request::test_support的 mock 响应队列,验证了从 URL 构造、HTTP 方法、请求体、分页映射到错误分类的几乎全部行为(前述各节均已引用); - Manifest 投影测试:
cargo test -p ironclaw_extension_registry校验 manifest 能被 registry 正确解析、投影为 resolved 记录; - 产物新鲜度门禁:
python3 scripts/ci/check-wasm-artifact-freshness.py以scripts/ci/wasm-src-digests.toml中的摘要校验wasm/github_tool.wasm与wasm-src/一致;编辑 guest 源码后需先./scripts/build-wasm-extensions.sh --first-party重建、再--update更新摘要(该命令链在 github 包 README 与 crates/extensions/AGENTS.md 中均有说明)。
此外,家族级架构门禁(如 reborn_extension_specificity.rs 禁止通用代码出现 vendor 名、reborn_dependency_boundaries.rs 限制 per-crate 依赖边)同样约束 github 这类包的代码归属,确保"vendor 行为只存在于 packages/*"这一铁律不被破坏。
参考路径速查
| 目的 | 路径 |
|---|---|
| 包 README(本指南依据) | crates/extensions/packages/github/README.md |
| 包声明与 49 个工具注册 | crates/extensions/packages/github/manifest.toml |
| Guest 入口与错误分类 | crates/extensions/packages/github/wasm-src/src/lib.rs |
| capability 派发 | crates/extensions/packages/github/wasm-src/src/dispatch.rs |
| 请求构造与宿主 egress | crates/extensions/packages/github/wasm-src/src/request.rs |
| 输入校验规则 | crates/extensions/packages/github/wasm-src/src/validation.rs |
| Webhook 归一化 | crates/extensions/packages/github/wasm-src/src/webhook.rs |
| 包嵌入(extension_support) | crates/extensions/ironclaw_extension_support/src/packages/github.rs |
| 家族规则与 package-to-crate 规则 | crates/extensions/AGENTS.md |
| 产物新鲜度检查 | scripts/ci/check-wasm-artifact-freshness.py |