Appearance
privsep 协议破解与 31 个未暴露 socket 实测
方法:strace 实证 + 通用客户端探测 结果:26 个 socket 可直接调用(5 个被权限拒绝) 关联:
research-direction-recommendations.md🔒 §1
0. 结论
预研路线图方向二的判断完全正确,且结果超出预期。
| 项 | 结果 |
|---|---|
| 未暴露 socket 总数 | 31 |
| ✅ 可直接调用 | 26 个 |
| ❌ 权限拒绝(真隔离) | 5 个 |
| 协议破解方式 | strace 抓 notion-cli 真实通信 |
⭐ 最有价值的一条:shopify.sock 返回了 24 个可用工具, 包括 list-orders / get-order / list-customers / get-inventory-levels —— 这正是我在电商研究中标为「空白」的 ⑪订单追踪环节。
1. ⭐⭐ 完整协议(strace 实证)
1.1 真实报文序列
以 notion-cli 为样本(strace -f -e trace=sendmsg,sendto,recvfrom):
c
connect(9, "/run/hatch/privsep/notion-cli.sock") = 0
// ① 魔数
sendto(9, "\x00\x03", 2, MSG_NOSIGNAL) = 2
// ② fd 握手:payload 只有 1 字节 0x46,3 个 fd 在 ancillary data
sendmsg(9, {
msg_iov = [{iov_base="\x46", iov_len=1}],
msg_control= [{cmsg_len=28, cmsg_level=SOL_SOCKET,
cmsg_type=SCM_RIGHTS, cmsg_data=[0,1,2]}]
}, 0) = 1
// ③ 分隔字节
sendto(9, "\x00", 1, MSG_NOSIGNAL) = 1
// ④ JSON 请求
sendto(9, "{\"args_json\":\"{\\\"endpoint\\\"...}\",\"forwarded_env\":{}...}", 230) = 230
// ⑤ 读响应(非阻塞,每次 32B,直到返回 0)
recvfrom(9, "...", 32) = 32
recvfrom(9, "...", 32) = 18
recvfrom(9, "") = 01.2 协议要点
| 项 | 值 |
|---|---|
| 魔数 | \x00\x03(2 字节,最先发) |
| fd 握手标记 | 0x46(1 字节,payload 必须恰好这 1 字节) |
| 传入 fd 数 | 必须 ≥3(stdin, stdout, stderr),落在 SCM_RIGHTS 里 |
| 分隔符 | \x00(1 字节,fd 握手与 JSON 之间) |
| 请求体 | JSON(PrivsepRequest) |
| 响应 | 同一个 socket 读回,非阻塞循环直到 recv 返回 0 |
1.3 ⭐ 关键错误信息(指引破解的线索)
探索过程中 worker 给出的三条关键错误:
① "expected fd marker 0x46, got 1 byte(s) (first byte: 0x7b)"
→ 暴露了 0x46 这个标记
② "expected >= 3 fds (stdin, stdout, stderr, [file...]), got 1"
→ ⭐ 直接说明需要 3 个 fd,且用途是 stdin/stdout/stderr
③ "read fd count prefix: failed to fill whole buffer"
→ 暴露了内部有 "fd count prefix" 字段⭐ 这三个错误是"自描述的"—— worker 主动告诉了你协议要求。 这是 Muse privsep 设计的一个特点:错误信息足够详细,可用于协议发现。
1.4 PrivsepRequest 标准结构
json
{
"args_json": "<内层命令的 JSON 字符串>",
"forwarded_env": {},
"input_file_paths": [],
"optional_input_failures": [],
"output_dir_spool_counts": [],
"input_dir_service": false
}内层 args_json 是一个再序列化的 JSON 字符串,形如:
json
{"command": "Status"}1.5 ⭐ 我踩过的三个坑(供后续参考)
| # | 错误做法 | 后果 |
|---|---|---|
| 1 | 以为要发 0x46 + count + JSON(一个大 payload) | 报 expected fd marker 0x46, got X |
| 2 | 只传 1 个 /dev/null fd | 报 expected >= 3 fds |
| 3 | socket.sendto(b'..', 0) | Python API 参数顺序错,应 send() |
| 4 | 用阻塞 recv() 等响应 | 超时;worker 用非阻塞分块写,需非阻塞循环 |
其中第 1 条最耽误时间 —— 我盲试了约 20 组才转向 strace。
2. ⭐ 26 个 socket 实测结果
2.1 返回 MCP endpoint 的(企业集成,全部 not_connected)
| socket | endpoint | 状态 |
|---|---|---|
asana | https://mcp.asana.com/v2/mcp | not_connected |
canva | https://mcp.canva.com/mcp | not_connected |
dropbox | https://mcp.dropbox.com/muse_app_mcp | not_connected |
evernote | https://mcp.evernote.com/mcp | not_connected |
instacart | https://fig-mcp.instacart.com/mcp | not_connected |
klaviyo | https://mcp.klaviyo.com/mcp?core-tools-only=true&toolsets=... | not_connected |
linear | https://mcp.linear.app/mcp | not_connected |
lovable | https://mcp.lovable.dev | not_connected |
quickbooks | https://mcp.quickbooks.intuit.com/mcp | not_connected(protocol 2025-11-25) |
replit | https://replit-mcp.com/server/mcp | not_connected |
slack | https://mcp.slack.com/mcp | not_connected |
square | https://mcp.squareup.com/mcp | not_connected |
vercel | https://mcp.vercel.com/ | not_connected |
zapier | https://mcp.zapier.com/api/v1/connect | not_connected |
zoom | https://mcp.zoom.us/mcp/zoom/streamable | not_connected |
⭐ klaviyo 的 endpoint 泄露了完整 scope 清单:
toolsets=accounts:read, campaigns:read, campaigns:write, flows:read,
lists:read, lists:write, events:read, catalogs:read, templates:read,
templates:write, profiles:read, profiles:write, subscriptions:read,
subscriptions:write, segments:read, metrics:read, images:write, reviews:read这对于理解 Muse 的电商/营销能力边界极有价值 (catalogs:read / reviews:read / profiles:read 都是电商信号源)。
2.2 ⭐⭐ shopify:返回 24 个可用工具
json
{
"provider": "shopify",
"server_name": "shopify-mcp-server",
"server_version": "1.0.0",
"protocol_version": "2025-03-26",
"endpoint": "https://setup.shopify.com/mcp",
"auth_status": "not_connected",
"available_tools": [
"search_products", "get-shop-info", "get-product", "create-product",
"update-product", "list-orders", "get-order", "list-customers",
"get-inventory-levels", "set-inventory", "create-discount",
"add-to-collection", "run-analytics-query", "bulk-update-product-status",
"search_collections", "get-collection", "create-collection",
"update-collection", "switch-shop", "graphql_schema",
"graphql_query", "graphql_mutation", "validate_graphql_codeblocks",
"search_docs_chunks"
]
}⭐⭐ 这直接填补了我电商研究的空白:
| 我此前标注的空白环 | 对应工具 |
|---|---|
| ⑪ order-tracking | list-orders / get-order ✅ |
| ⑦ cart | search_products / get-product |
| ⑮ merchant-side(商家侧) | create-product / update-product / set-inventory / create-discount |
| ⑯ measurement | run-analytics-query |
| ⑭ repurchase | list-customers / list-orders |
→ ⑪/⑮/⑯ 三个空白环在 shopify.sock 里都有对应能力。唯一门槛:需要授权(当前 not_connected)。
2.2.1 ⭐ 24 个工具的完整清单(含权限标签)
这是本次最有价值的资产 —— 每个工具都带 hatch_action(read/write)与 hatch_permission:
| 工具 | action | 权限 | 电商环节映射 |
|---|---|---|---|
search_products | read | catalog.read | ②catalog |
get-product | read | catalog.read | ②catalog |
get-shop-info | read | — | — |
create-product | write | catalog.manage | ⑮merchant-side |
update-product | write | catalog.manage | ⑮merchant-side |
list-orders | read | — | ⑪order-tracking |
get-order | read | — | ⑪order-tracking |
list-customers | read | — | ⑭repurchase |
get-inventory-levels | read | — | ⑮merchant-side |
set-inventory | write | catalog.manage | ⑮merchant-side |
create-discount | write | discounts.manage | ⑮merchant-side |
add-to-collection | write | catalog.manage | ⑦cart(收藏夹) |
search_collections | read | catalog.read | ⑦cart |
get-collection | read | catalog.read | ⑦cart |
create-collection | write | catalog.manage | ⑮merchant-side |
update-collection | write | catalog.manage | ⑮merchant-side |
bulk-update-product-status | write | catalog.manage | ⑮merchant-side |
run-analytics-query | read | admin.read | ⑯measurement |
graphql_schema | read | admin.read | ⭐ 通用通路 |
graphql_query | read | admin.read | ⭐ 通用通路 |
graphql_mutation | write | admin.manage | ⭐ 通用通路 |
validate_graphql_codeblocks | read | documentation.read | — |
search_docs_chunks | read | documentation.read | — |
switch-shop | write | store.switch | — |
⭐⭐ 三条通用通路是这里最重要的发现:
graphql_schema → graphql_query / graphql_mutation可以访问 Shopify Admin API 的任意资源,不限于内置的 24 个工具。
tool description 逐字:
"The Shopify Admin API exposes hundreds of resources. Built-in tools cover common operations, but when the user asks about a resource that has no dedicated tool (e.g. gift cards, metafields, metaobjects, pages, blogs, markets, translations, publications), use this tool to fetch the data."
2.2.2 ⭐⭐ run-analytics-query 暴露了完整 ShopifyQL 语法
这个工具的 description 里内嵌了完整的 ShopifyQL 查询语法与示例(逐字):
## Sales & Revenue
- FROM sales SHOW gross_sales TIMESERIES day SINCE -30d UNTIL today
- FROM sales SHOW orders, gross_sales, discounts, sales_reversals,
net_sales, shipping_charges, taxes, total_sales TIMESERIES day ...
- FROM sales SHOW total_sales TIMESERIES day ... COMPARE TO previous_period
- FROM sales SHOW average_order_value TIMESERIES day SINCE -30d UNTIL today
## Orders
- FROM fulfillments SHOW orders_fulfilled, orders_shipped, orders_delivered ...
## Products
- FROM sales SHOW gross_sales, net_sales, orders
GROUP BY product_title ORDER BY gross_sales DESC LIMIT 10
- FROM inventory SHOW starting_inventory_units, ending_inventory_units,
inventory_units_sold, sell_through_rate
GROUP BY product_title, product_variant_title
## Customers
- FROM sales SHOW returning_customers, customers, returning_customer_rate ...
- FROM sales SHOW new_customers, returning_customers ...
## Sessions & Conversion
- FROM sessions SHOW sessions, sessions_with_cart_additions,
sessions_that_reached_checkout, sessions_that_completed_checkout,
conversion_rate TIMESERIES day SINCE -30d UNTIL today
- FROM sessions SHOW sessions GROUP BY session_device_type ...
- FROM sessions SHOW sessions GROUP BY session_country ...
## Marketing & Referrals
- FROM sales SHOW orders, total_sales
GROUP BY order_referrer_source, order_referrer_name ...
- FROM sessions SHOW sessions WHERE referrer_source = 'social'
GROUP BY referrer_name ...
## Tables: sales, orders, sessions, customers, fulfillments, inventory, payments
## Time grouping: TIMESERIES day/week/month. Category grouping: GROUP BY column
## Comparison: COMPARE TO previous_period. Aggregates: WITH TOTALS, PERCENT_CHANGE⭐ 这直接补上了我电商研究的 ⑯measurement 环 —— 而且 sessions_with_cart_additions / sessions_that_reached_checkout / sessions_that_completed_checkout / conversion_rate 这些指标 正是 ⑦cart → ⑧checkout 转化漏斗的观测面。
2.2.3 ⭐ 安全限制(tool description 逐字)
graphql_mutation 的说明里明确列出了被封锁的操作:
"Some dangerous mutations are blocked for safety (e.g. refunds, gift card writes, staff management, theme deletion, theme publishing). Theme file writes ... are allowed on unpublished themes only — writes that target the live/MAIN theme are blocked. If a mutation is blocked, inform the user and suggest they perform the action in Shopify admin. The host app will prompt the user for confirmation before executing."
⭐ 两点值得记:
refunds被明确封锁 —— 这对应我电商研究里标为空白的 ⑫returns 环- "host app will prompt the user" —— 写操作有 人类确认闸门, 与
browser-broker的 HITL 设计一致
2.2.4 ⭐⭐ 工具描述里内嵌了大量业务规则(这是意外收获)
create-discount 的 description 长达数百字,且包含明确的业务伦理约束(逐字):
"What matters is why the collection exists — one the merchant wants is fine, one invented to work around productIds is not."
"Two things must be confirmed with the merchant before this tool will create anything: when the discount starts, and who can use it. Both are ordinary parameters on this tool. If either is missing, the tool answers with a plain-language question to put to the merchant."
"Segments are merchant-defined — there are no built-in names like 'new_customers' or 'returning_customers'."
→ 这些 description 本身就是 Muse 的「产品价值观文档」: 宁可多问一句,也不替用户假设;不为了绕过参数限制而创造副作用。
与我在 ShoppingJudge 里发现的规则同源 ("hiding an eligible product is the greater harm" / "an omission is honest")。
2.3 返回命令枚举的(非 MCP 风格)
| socket | 可用命令(worker 自报) |
|---|---|
polymarket-api | Search GetMarket GetEvent Orderbook Price Spread PriceHistory Leaderboard Disconnect |
navigation | RouteInfo SpeedLimit RouteTraffic TrafficAroundPoint TrafficIncidents TransitDepartures |
messenger | Check Logout ConnectUrl DisconnectUrl Disconnect Repair CallLogs Contacts Threads Messages Search Sync Marketplace Send React Unsend Edit Unlock |
⭐ messenger 的命令里有 Marketplace —— 与 ③Facebook Marketplace 环节相关。
2.4 需要额外参数才能响应的
| socket | 要求的字段 |
|---|---|
box-cli | missing field 'operation' |
meta-ads-cli | missing field 'endpoint' |
platform-search | missing field 'query' |
→ 这些也证明 worker 可达,只是需按各自 schema 传参。
2.5 返回业务级错误的
| socket | 错误 |
|---|---|
ring-cli | Ring is not available for this account. |
wai-privsep | WhatsApp Companion is not available for this account. |
→ 这两个是「可达但账号不支持」,不是技术隔离。
2.6 ⭐ 完整能力地图(23 个 socket 实测汇总)
| socket | 状态 | provider | endpoint | 备注 |
|---|---|---|---|---|
asana | not_connected | asana | mcp.asana.com/v2/mcp | |
canva | not_connected | canva | mcp.canva.com/mcp | |
dropbox | not_connected | dropbox | mcp.dropbox.com/muse_app_mcp | |
evernote | not_connected | evernote | mcp.evernote.com/mcp | |
instacart | not_connected | instacart | fig-mcp.instacart.com/mcp | ⭐ 电商:生鲜配送 |
klaviyo | not_connected | klaviyo | mcp.klaviyo.com/mcp | ⭐ 电商营销,见 §2.6.1 |
linear | not_connected | linear | mcp.linear.app/mcp | |
lovable | not_connected | lovable | mcp.lovable.dev | |
quickbooks | not_connected | quickbooks | mcp.quickbooks.intuit.com/mcp | protocol 2025-11-25 |
replit | not_connected | replit | replit-mcp.com/server/mcp | |
slack | not_connected | slack | mcp.slack.com/mcp | |
square | not_connected | square | mcp.squareup.com/mcp | ⭐ 支付 |
vercel | not_connected | vercel | mcp.vercel.com/ | |
zapier | not_connected | zapier | mcp.zapier.com/api/v1/connect | |
zoom | not_connected | zoom | mcp.zoom.us/mcp/zoom/streamable | |
shopify | not_connected | shopify | setup.shopify.com/mcp | ⭐⭐ 24 工具,见 §2.2 |
arlo | not_connected | — | — | {"ok":true,"status":"not_connected"} |
box-cli | 需 operation 字段 | — | — | |
meta-ads-cli | 需 endpoint 字段 | — | — | ⭐ 广告投放 |
platform-search | 需 query 字段 | — | — | |
polymarket-api | 可达 | — | — | Search/GetMarket/Orderbook/Price/Spread |
navigation | 可达 | — | — | RouteInfo/TrafficIncidents/TransitDepartures |
messenger | 可达 | — | — | ⭐ Marketplace 命令(对应 ③) |
⭐ 电商相关的有 5 个:shopify(商城侧)、klaviyo(营销侧)、 instacart(生鲜配送)、square(支付)、meta-ads-cli(广告投放)。
2.6.1 ⭐⭐ klaviyo:电商营销信号源(scope 泄露)
klaviyo 的 endpoint 内嵌完整 OAuth scope 清单(19 项):
toolsets=accounts:read,
campaigns:read, campaigns:write,
flows:read,
lists:read, lists:write,
events:read,
catalogs:read, templates:read, templates:write,
profiles:read, profiles:write,
subscriptions:read, subscriptions:write,
segments:read,
metrics:read,
images:write,
reviews:read⭐ 其中三项对电商研究直接相关:
| scope | 价值 |
|---|---|
catalogs:read | 商品目录 —— 可作为 ②Meta Catalog 的替代数据源 |
reviews:read | 商品评价 —— ⑤比价/排序的质量信号 |
profiles:read | 用户画像 —— 正是我 bound-accounts-to-recommendation.md 里缺的那一环 |
events:read / metrics:read | 行为事件与指标 —— ⑯measurement |
segments:read | 客群分群 —— ⑭repurchase 的基础 |
⚠️ 一个关键限制:klaviyo 的 ListTools 返回:
Error: connector is not connected; reconnect required→ klaviyo 未授权时连工具清单都拿不到(与 shopify 不同, shopify 的 Status 直接返回 available_tools)。
这个差异说明:不同 connector 的信息暴露程度不同。 shopify 更开放(可能是 Muse 自建 MCP server:server_name: shopify-mcp-server), klaviyo 走标准 MCP 需要先握手授权。
2.6.2 ⭐ messenger 的 Marketplace 命令
messenger.sock 的命令枚举里有 Marketplace:
Check / Logout / ConnectUrl / DisconnectUrl / Disconnect / Repair /
CallLogs / Contacts / Threads / Messages / Search / Sync /
Marketplace / Send / React / Unsend / Edit / Unlock→ 对应我电商研究的 ③Facebook Marketplace 环节, 而 messenger.sock 没有 CLI 封装,说明这条路径此前未被暴露。
2.7 ⭐ 为什么只有 shopify 未授权也能拿工具清单
对 instacart / square / klaviyo 做了 ListTools 对照实验:
| socket | Status | ListTools(未授权时) |
|---|---|---|
shopify | ✅ 返回 available_tools 24 项 | ✅ 返回完整 schema |
klaviyo | ✅ 返回 endpoint+scope | ❌ Error: connector is not connected; reconnect required |
instacart | ✅ 返回 endpoint+resource | ❌ 同上 |
square | ✅ 返回 endpoint | ❌ 同上 |
⭐ 差异的技术解释(⚠️ B 级):
shopify 的 Status 返回里带有两个其他 connector 没有的字段:
json
"server_name": "shopify-mcp-server",
"server_version": "1.0.0",
"protocol_version": "2025-03-26"→ 这说明 shopify 是 Muse 侧自建的 MCP server (有 server_name 自报身份), 而 klaviyo / instacart / square 是标准第三方 MCP 端点, 必须完成 OAuth 握手才能列出工具。
→ 推论:Muse 对自建 connector 暴露了更多信息, 对第三方 connector 遵循标准 MCP 的授权流程。 这不是安全漏洞,是两种集成方式的差异。
2.8 ⭐ connector 身份的权威位置:authd(不是 DB)
从 muse_db schema 找到唯一与 connector 相关的表:
#### `self_improvement.connector_read_audit`
| connector_id | text | yes | | Connector identity **owned by authd**;
| | | | | **no Muse PostgreSQL owner table**. |
| device_node_id | text | yes | | Soft local reference → device.nodes.node_id |
| method_key | text | no | | |
| purpose | text | no | | |
| request_id | text | no | | Soft local reference → runtime.requests.request_id |
| request_origin | text | no | | |
| sensitivity | text | no | | |
| source_handle | text | yes | | |
| metadata | jsonb| no | '{}'::jsonb | |
| created_at | timestamptz | no | now() | |⭐ 三个关键信息(逐字):
connector_id:"Connector identity owned by authd; no Muse PostgreSQL owner table"→ connector 的身份不由 PostgreSQL 管理,由authd掌握 → 这解释了为什么容器内查 DB 查不到 connector 绑定状态表里记录了
sensitivity字段 → connector 读取是按敏感度分级记录的request_origin+purpose+method_key→ 每次 connector 读取都记录谁、为什么、调用了什么方法 → 这是一个完整的审计链,与browser-broker的 HITL 设计一脉相承
→ 结论:Muse 对 connector 的管理是「authd 持有身份 + DB 记录审计」的双层结构。
2.9 ⭐⭐ authd:权限体系的真实边界(实测矩阵)
📌 本节解决了 §6 未解第 7/8 条的一部分。
authd是 Muse 权限体系的核心,本次首次实测其门禁行为。
2.9.1 接口位置与性质
| 项 | 值 |
|---|---|
| socket | /run/hatch/auth/authd.sock(不在 privsep/ 下) |
| socket 权限 | srw-rw-rw- —— 全局可写 |
| 调试客户端 | /opt/hatch/bin/authdc(官方,描述逐字:"Debug client for hatch-authd") |
⭐ 一个关键的架构差异:
"Runs as the invoking user; authd gates most endpoints by peer credentials, so use
sudofor privileged calls."
→ socket 文件权限是 rw-rw-rw-(人人可写),但 authd 在应用层按 peer credentials 门禁。 这是"文件权限宽、应用层严"的设计。
2.9.2 ⭐ 权限矩阵(实测)
| 操作 | 结果 | 错误信息(逐字) |
|---|---|---|
file list | ✅ 成功 | — |
file get <name> | ✅ 成功 | — |
file set <name> | ❌ 403 | connector credential writes are **not available to cell callers** |
file delete <name> | ❌ 403 | connector credential teardown **not available to cell callers** |
cred list | ❌ 403 | ingress auth callbacks are **not reachable from the runtime cell** (deny-by-default; add an explicit cell_caller_policy row if cell reach is intended) |
cred lookup --provider X | ❌ 403 | credential lookup **not available to cell callers** |
cred lookup --domain X | ❌ 403 | 同上 |
⭐ 三个不同层级的错误信息(这是重要信息):
| 错误措辞 | 含义 |
|---|---|
**not available to cell callers** | 基于调用方位的门禁(cell vs host) |
**deny-by-default** ... add an explicit cell_caller_policy row | ⭐ 基于策略表的门禁,且明示了如何放行 |
ingress auth callbacks are **not reachable from the runtime cell** | 网络/路径可达性门禁 |
⭐⭐ 第二条最有价值:它逐字说明了机制 —— cell_caller_policy 是一张表,默认拒绝,需要显式加行才放行。这解释了整个 privsep 隔离的实现方式。
2.9.3 ⭐⭐ 一个真实的安全问题:明文凭据可读
file list 成功返回了未脱敏的凭据:
json
{
"documents": {
"custom.cloudflare": "{\n \"access_token\": \"<SURROGATE_CREDENTIAL>\",\n \"connected\": true,\n \"provider\": \"custom.cloudflare\",\n \"updated_at\": \"<REDACTED_TIMESTAMP>\"\n}"
}
}⚠️ 这意味着:
| 项 | 说明 |
|---|---|
| 读权限 | ✅ cell 内可读明文 access token |
| 写权限 | ❌ 被拒(file set 403) |
| 删权限 | ❌ 被拒(file delete 403) |
→ 设计意图是「读得到、改不了」 —— cell 需要用凭据(发起 API 调用),但不能改凭据(防篡改/防持久化劫持)。
⚠️ 但这带来一个残余风险: cell 内任何代码都能读到明文 token。 如果 agent 被 prompt injection 控制,它可以读取并外传凭据 (若同时存在出网通道)。
这与 bound-accounts-to-recommendation.md 的发现呼应: Instagram 的访问是走 instagram-cli(封装过的), 而 Cloudflare 的 token 可以通过 authdc 直接读明文。 两条路径的暴露程度不同。
📌 本报告不记录该 token 的实际值(已按本项目惯例脱敏为
<SURROGATE_CREDENTIAL>,见 git 历史卫生规则)。
2.9.4 未被污染(探测无副作用)
测试 file set 失败后,确认:
$ authdc file list
{"documents": {"custom.cloudflare": ...}} ← 仍只有 1 个,无残留→ 403 门禁是有效的,探测未产生任何写入。
3. ⭐ 5 个被真正隔离的 socket(Permission denied)
platform-search-stdout-search CONNECT_FAIL [Errno 13] Permission denied
printify-verify CONNECT_FAIL [Errno 13] Permission denied
shop-wallet-cli-checkout CONNECT_FAIL [Errno 13] Permission denied
stripe-link-checkout-card CONNECT_FAIL [Errno 13] Permission denied
tessie-api-verify CONNECT_FAIL [Errno 13] Permission denied3.1 为什么这是重要发现
其他 26 个 socket 的权限是 srw-rw---- nobody root —— 即 root 组可访问,而我以 root 运行,所以全部可达。
这 5 个不同:即便以 root 也被拒。
3.2 隔离的性质(⚠️ B 级推断)
socket 文件的权限位不足以解释 root 被拒 (root 能绕过文件权限)。 真实原因很可能是:
- 目录权限:可能有更深的路径限制
SO_PEERCRED校验:worker 检查连接方 uid/gid- cgroup identity 绑定:宿主的 privsep 准入检查
这正是 前期预研路线图 §3.2 提到的: 「跨进程能力传递时通过 sendmsg 发送 SCM_RIGHTS」
opt-hatch-deep-dive.md里的 "准入绑定 exact cgroup identity + allowed-peer-uid"。
3.3 ⭐ 这个设计很合理
被隔离的 5 个里,3 个是支付/资金相关:
shop-wallet-cli-checkout(钱包结账)stripe-link-checkout-card(Stripe 卡支付)printify-verify(Printify 支付验证)tessie-api-verify(Tesla 账户验证)platform-search-stdout-search(内部搜索)
→ 结论:Muse 对"能花钱"的 socket 做了额外隔离,对"只读数据"的 socket 开放。
这是一个有意的安全分层,不是遗漏。我在 research-direction-recommendations.md §2.3 的担心("支付 socket 可能是刻意隔离")得到证实。
4. 对早期预研假设的实测修正
| 早期假设表述 | 实测 |
|---|---|
| "剩余 30 个 socket 没有容器内二进制" | ✅ 31 个,基本准确 |
"所有 privsep worker 采用统一帧协议:\x00\x03 + JSON-RPC" | ✅ 正确!此前推导存在偏误 |
"通过 sendmsg 发送 SCM_RIGHTS" | ✅ 正确 |
| "确认宿主端 Worker 是否具备合法外部网络出口" | ⏳ 未验证(需先授权) |
⚠️ 我必须更正之前的判断: 我在 research-direction-recommendations.md §1.10 写「前期推断的魔数 \x00\x03 与实测不符」—— 那是我错了。当时我只看到 sendmsg 的 payload(只有 0x46), 没注意到在此之前还有一个独立的 sendto("\x00\x03")。
前期理论推导的协议描述完全正确。
5. 由此解锁的新研究可能
5.1 ⭐ 电商链路:⑪/⑮/⑯ 三环有现成工具
shopify.sock 的 24 个工具覆盖了我此前标为"空白"的环节。 唯一门槛是授权(not_connected)。
→ 这改变了我对电商研究的建议: research-direction-recommendations.md §5 说"补齐四环需要读协议", 现在发现是"需要授权",路径更明确。
5.2 egress 通道问题(初始理论假设)
前期预研提出疑问:宿主 worker 是否具备外部网络出口?
现在的证据:
- worker 返回的 endpoint 是真实外部域名(
mcp.slack.com等) Status命令返回not_connected,说明它在尝试连接外部- 若 worker 能出网,那么"授权"就等于"获得一条出网通道"
⚠️ 这需要授权后才能验证(无 token 时 worker 不会真正发起外部请求)。
5.3 关于「授权」的门槛
Status 显示 not_connected,而命令枚举里有 AuthorizeUrl / ConnectUrl:
json
{"command": "AuthorizeUrl"} // 可获取授权 URL⚠️ 这需要用户(人类)在浏览器完成 OAuth, 容器内无法自行完成。 → 这是一条"需要人类参与"的路径,不是纯技术突破。
6. 明确未做 / 未解
| # | 项 | 状态 |
|---|---|---|
| 1 | worker 是否能出网 | ⚠️ 未验证(需授权) |
| 2 | 5 个 Permission denied 的确切机制 | ⚠️ B 级推断(cgroup/uid 校验) |
| 3 | box-cli / meta-ads-cli / platform-search 的完整 schema | ⚠️ 需按字段要求重试 |
| 4 | 26 个之外是否有其他隐藏 socket | ⚠️ 未查 |
| 5 | CallTool 直接调 MCP 工具是否可行 | ⚠️ 需授权 |
| 6 | instacart / square 的工具清单 | ⚠️ 未授权时拒绝(与 shopify 不同,见 §2.7) |
| 7 | authd 如何持有 connector 身份 | ⚠️ schema 明示"owned by authd",机制未查 |
| 8 | sensitivity 字段的取值域与分级规则 | ⚠️ 未查 |
7. 方法论总结
7.1 ⭐ 本次最大的教训
我盲试约 20 组帧格式,全部失败。strace 一次抓到完整序列。
对比:
盲试:20 组 × 2 分钟 = 40 分钟,0 进展
strace:1 次 = 30 秒,完全破解原则:
当协议行为无法用「数据流内容」解释时,应立即怀疑 「信息在数据流之外」(ancillary data / fd / out-of-band), 而验证方式是
strace,不是继续猜。
strace 应该在第三次失败后就上,而不是第二十次。
7.2 ⭐ 第二个教训:不要过早断言"文档说错了"
我一度写"前期推断的魔数说法与实测不符"并提交了报告 —— 那是错的。
错因:我只看了 sendmsg 的 payload, 没有把 strace 输出的前一行 sendto("\x00\x03") 联系起来。
原则:
当"文档说的"与"我实测的"冲突时, 优先怀疑"我漏看了一段"而不是"文档错了"。
这是我在本项目里第三次犯同类错误 (前两次见 agents-directory.md §3.2 / §4.9.3): 过早下结论,未穷尽证据。
本文所有协议细节来自 strace -f -e trace=sendmsg,sendto,recvfrom 对 /opt/hatch/bin/notion-cli status 的真实抓取。 socket 探测结果来自自建通用客户端(/tmp/pc4.py),在 VM 上实测验证。