Repository navigation
[Decision] connector-mcp tool-snapshot pin (#26709 item 3(c)): one aggregate toolsSnapshotSha256, or a per-tool pin record that can name the tool a server rewrote #27401
Description
epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U
Ruled: 6545635244 · letter B · 2026-10-31T15:53Z
Ruling-ref: 6545635244
Ruled: 6506797088 · letter B · 2026-10-29T19:52Z
carries item 3(c) of #26709; #26709 keeps item 1 (PR #27387). Filing gate: ② a contract-shape decision the maintainer takes. Raised by the os-dev on #26709 as needs_decision (report comment 6488553467); the seat's own readings below. Child of #27293 (E1). ⛔ Not a claim; item 3(c) is not dispatched while this card is open.
维护者速读
- 改了什么:不改代码。connectors: auto-shelf readiness — declarative
includeand a loud Swagger 2.0 refusal for connector-openapi;tools/listpagination, fatal 401 and a tool-snapshot hash for connector-mcp; an offline spec-normalization tool #26709 第 3(c) 项(MCP 工具快照校验)在实现时发现卡上自相矛盾,拆出来请你定形状。 - 为什么改:卡上定的键是「一个总 hash」(toolsSnapshotSha256),验收却要求「不一致时点名是哪个工具被改了」;一个总 hash 只能说「变了」,说不出是哪个,而运行中也没有「上一次正常的快照」可比。
- 风险与回滚:卡挂着期间第 1 项(openapi include)照常落地;MCP 侧暂无快照校验,现状不变(include 已挡住新增工具,挡不住已有工具被改写描述或参数)。
- 先看:下面「选项」表。
- 两分钟验证:git show origin/main:packages/services/service-automation/src/plugin.ts | sed -n 1844,1847p(未变化的实例不重连)。
- 需要你做的:选 A / B / C / D。
Background
#26709 item 3(c), verbatim: "providerConfig.toolsSnapshotSha256 (optional): when set, the hash of the selected tools' name + description + inputSchema (canonical JSON) is compared on every materialization; a mismatch marks the instance degraded with a message naming the changed tool, so a server-side rewrite of a tool's description or schema (the "rug-pull" class) is visible instead of silently accepted." Triage (6487372008) graded it a protocol addition, p1, domain:server.
Governing text: the card's own item text (above) and its acceptance line; AGENTS.md Prime Directive #12 (contract-first: refuse loudly at authoring or load, never tolerate silently); triage 6487372008 「include keeps one meaning across openapi and mcp」 (the pin should compose with include, keyed the same way).
Protocol statement: Metadata-protocol: added either way (a new author-written providerConfig key on mcp); Clause-②: yes (widening).
Premises (each with its re-check)
- One aggregate hash cannot attribute a mismatch to a tool. (Arithmetic; no re-check.)
- An unchanged instance is never re-materialized in a running process, so there is no earlier good snapshot in memory: git show origin/main:packages/services/service-automation/src/plugin.ts | sed -n 1844,1847p ("unchanged entries are left alone (an unchanged MCP instance is NOT reconnected)"), and the signature check at ~:1970. Read 2026-10-29T05:10Z at origin/main.
- A degraded channel exists to report a mismatch: the provider throws ConnectorUpstreamUnavailableError and the materializer registers a degraded husk retried with backoff (degradeConnectorInstance, plugin.ts:2172). Its log wording is "upstream unavailable … retrying with backoff", which misdescribes a pin mismatch.
- include on mcp is a name allowlist (mcp-provider.ts:223-226).
The question
When a remote MCP server silently rewrites one of the tools an app depends on, what should the administrator see: "something changed", or "tool X changed"?
Options
| What it does | What the administrator sees | Effort | |
|---|---|---|---|
| A | Keep the ruled single toolsSnapshotSha256; on mismatch degrade the instance with expected and observed digests | "The tool set changed" — no tool named; they diff the server by hand | domain:server S, 1 PR; no precedent |
| B | Replace it with providerConfig.toolsSnapshot: a record keyed by tool name, each value the sha256 of that tool's canonical name + description + inputSchema | "Tool create_issue changed" (and "pinned tool x is gone"); meets the card's acceptance line | domain:server S–M, 1 PR; #27373's offline tool can emit the record |
| C | Single hash plus in-process last-good memory | Names the tool only if the same process saw a good snapshot first — at cold boot (the common detection point) falls back to A | S + hidden state; not recommended |
| D | Drop item 3(c) for now (close not planned); include already keeps new tools out | No rewrite detection | zero |
Business meaning: A is a seal on the whole box; B is a seal on each item in the box; D is no seal yet.
四维分析
os-decision-facets
- ① 项目长远合理性:两年后的样子是「每个被依赖的远程工具都能被钉住,变了能精确报出是谁」(参照 npm/pnpm 锁文件按包逐条记 integrity,而不是给整棵依赖树一个总 hash);B 与 include 同样按工具名为键,两把钥匙能叠用;A 只能报「变了」,逐工具的数据算出来又扔掉;C 引入依赖进程历史的隐藏状态;D 不增生但把这类攻击面留着。
- ② 实际业务拉动:板上没有客户点名要这个功能;拉动来自你 2026-10-27 对目录调研的指示(「建议 1+2立卡」),3(c) 在那张卡的范围内。零客户拉动按规则会荐 D,但该项已在你指示的范围内,所以本卡只问形状,D 留作你收回的出口。
- ③ 防 AI 犯错:B 是结构化契约,一条钉错的 pin 会指向它那个工具,响亮且可定位;A 把「钉的值过时」与「工具被改写」压成同一条模糊报错,AI 和管理员都只能猜。
- ④ 创业阶段不扩散:A 与 B 工作量相当(都是一个 helper + 测试),都不新增门禁;D 最省。
Prior rulings read: toolsSnapshot, tool snapshot hash, rug-pull → the card's own item text and triage 6487372008 (grade and lane only, not the shape); thread: connectors: auto-shelf readiness — declarativeincludeand a loud Swagger 2.0 refusal for connector-openapi;tools/listpagination, fatal 401 and a tool-snapshot hash for connector-mcp; an offline spec-normalization tool #26709; repo: objectstack-ai/objectstack
Recommendation
B. The card's purpose is to make a server-side rewrite visible and attributable, and only per-tool data can name the tool; B keys the pin by tool name like include, so the two compose. Not A: it misses the card's own acceptance line. Not C: hidden, history-dependent behaviour. Not D: the item is inside the scope you directed; D stays the exit if you no longer want it. Confidence gap: whether the degraded channel's "upstream unavailable … retrying with backoff" wording and retry loop should be specialised for a pin mismatch (it reconnects on every backoff until the pin or the server changes); under A or B the dev should name that in the PR.
Self-check: 只看①选 B;②③④ 是否翻转:否(② 零客户拉动指向 D,但该项在你已指示的范围内,只影响是否保留出口,不翻字母)。
After the ruling
- A or B: the epic PM dispatches it to domain:server (same lane as feat(connector-openapi): declarative providerConfig.include operation allowlist #27387), Metadata-protocol: added, Clause-②: yes (widening), with a contract review before landing; under B the key is toolsSnapshot (record), and the card text's toolsSnapshotSha256 is retired before it ever ships.
- D: this card closes not planned; connectors: auto-shelf readiness — declarative
includeand a loud Swagger 2.0 refusal for connector-openapi;tools/listpagination, fatal 401 and a tool-snapshot hash for connector-mcp; an offline spec-normalization tool #26709 closes when item 1 lands.
Related
#26709 (parent item) · PR #27387 (item 1) · #27373 (offline tool that could emit pins) · #27293.
Dedupe
Split by the dev's needs_decision; the parent item is #26709. The 500 most recent issues and PRs, titles grepped for snapshot|rug-pull|toolsSnapshot (read 2026-10-29T04:10Z): 0 hits; control connector on the same set: 10 hits.
Activity
objectstack-fleet commentedon Oct 29, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: 可对外的 API | none | P②
Triage: p1 ·
domain:server· decision stays with the maintainer · 2026-10-29T06:00Z⛔ Not a claim.
includeand a loud Swagger 2.0 refusal for connector-openapi;tools/listpagination, fatal 401 and a tool-snapshot hash for connector-mcp; an offline spec-normalization tool #26709 item 3(c). The card asks for one aggregatetoolsSnapshotSha256, but its acceptance also asks to name the tool that changed. One hash cannot name a tool, so the maintainer picks the shape (A to D).openapiinclude, PR feat(connector-openapi): declarative providerConfig.include operation allowlist #27387) is not held by it.Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
director#1 · os-bill · session_01Utp6CbQLcy8yg6vhJVXXi8
Brief-claimed-by: session_01Utp6CbQLcy8yg6vhJVXXi8 · batch #428 · 2026-10-29T10:17Z
Director seat, summon #54 continuation 1 (ledger objectstack#12708, opening mark 6493929869). Presented to the maintainer in batch #428 as item 3 of 4 — #26709 item 3(c), split by the dev's
needs_decision(6488553467), epic #27293 E1.Seat increment (re-read at
origin/mainded67bfb, 2026-10-29T10:17Z):degradeConnectorInstance(plugin.ts L2172, called L2077) and its wording describes an unavailable upstream;toolsSnapshothas 0 hits underpackages/(nothing shipped yet); item 1 (openapiinclude, PR feat(connector-openapi): declarative providerConfig.include operation allowlist #27387) is merged and not held by this card; connectors: an offline OpenAPI normalization script for connector-openapi — Swagger 2.0 → 3.x, YAML → JSON,$refbundled and dereferenced, absoluteservers[0].url, sha256, and a generatedincludelist #27373 (the offline normalisation tool the card names as a pin emitter) is closed.domain:server) and ruled nothing on the shape;check-prior-rulings.mjs --card 27401names ADR-0129 D3, ADR-0097 §6, ADR-0109 §4, ADR-0032 — term matches, none decides a snapshot pin's granularity. The item's own acceptance line ("a message naming the changed tool") is the governing text, and it contradicts the item's key shape (one aggregate hash); the maintainer picks which half gives way.include, is the lockfile shape (npm / pnpm record integrity per package, not one hash per tree); the two keys compose. ② no customer named it; the item is inside the scope the maintainer directed on 2026-10-27 (「建议 1+2立卡」), so D is the exit if that scope is withdrawn, not the default. ③ B names the tool and distinguishes "pin stale" from "tool rewritten"; A compresses both into one undiagnosable message; C hides state in process history. ④ A and B cost the same (one helper + tests); neither adds a gate.providerConfig.toolsSnapshot, a record keyed by tool name whose value is the sha256 of that tool's canonicalname+description+inputSchema; a mismatch degrades the instance with a message naming the tool (and a pinned tool that is gone); the aggregatetoolsSnapshotSha256spelling is retired before it ever ships. Carry the card's confidence gap into the dispatch: the degraded channel's "upstream unavailable … retrying with backoff" wording and retry loop should be specialised for a pin mismatch (no reconnect loop fixes a pin).domain:server· 1 PR · S–M ·Metadata-protocol: added,Clause-②: yes (widening)⇒ contract review before landing · no precedent.domain:serverwithRuling-ref:; the card leaves the box underpm:epic(its stroke).Prior rulings read: toolsSnapshot, tool snapshot, rug-pull, include → triage 6487372008 (grade only); thread: 6489246137; repo: objectstack-ai/objectstack
objectstack-fleet commentedon Oct 29, 2026
director#1 · os-bill · session_01Utp6CbQLcy8yg6vhJVXXi8
Ruling: batch #428 item 3 · letter B · maintainer 「27401 27328 同意」 2026-10-29T19:50Z
Director seat, summon #54 continuation 1 (ledger objectstack#12708, opening mark 6493929869, brief 6494012088). The maintainer answered batch #428 in the summon chat, verbatim: 「27401 27328 同意,另外两个我记得其他总监处理过了」 — 同意 = the seat's letter on this card: B.
What the ruling binds (letter B): #26709 item 3(c) ships as
providerConfig.toolsSnapshotonmcpconnector instances — a record keyed by tool name, each value the sha256 of that tool's canonical JSON ofname+description+inputSchema. On every materialization the selected tools are hashed and compared per tool; a mismatch marks the instancedegradedwith a message that names the changed tool (and a pinned tool that is no longer served); a tool the server adds is already kept out byinclude, which the pin composes with (same key: the tool name). The aggregate spellingtoolsSnapshotSha256in #26709's item text is retired before anything ships; no in-process last-good memory (C) and no aggregate digest (A). Carried into the dispatch as a named question for the dev: the degraded channel's wording ("upstream unavailable … retrying with backoff") and its reconnect loop are specialised for a pin mismatch, since no reconnect fixes a pin — the dev names what it does in the PR body.Governing text: #26709 item 3(c)'s acceptance line ("a message naming the changed tool"), AGENTS.md Prime Directive #12 (refuse loudly at load), triage 6487372008 (
includekeeps one meaning acrossopenapiandmcp). Premises re-read atorigin/mainded67bfb(brief 6494012088).Workload of the letter:
domain:server· 1 PR · S–M ·Metadata-protocol: added(a new author-writtenproviderConfigkey onmcp),Clause-②: yes (widening)⇒ contract review atCONTRACT_REVIEW_TIERbefore landing · no precedent.Execution: the #27293 epic PM dispatches this card to
domain:serverwithRuling-ref:naming this comment;needs-user-decision→pm:dispatchedis the epic PM's stroke (pm:epicstays). #26709 closes when item 1 (merged, PR #27387) and this item have landed.os-decision-facets
① 长远:逐工具 integrity 是 lockfile 的形状,与
include同以工具名为键,两把钥匙可叠用;总 hash 把算出的逐工具数据扔掉② 拉动:板上无客户点名;项在维护者 10-27「建议 1+2 立卡」的范围内
③ 防错:一条钉错的 pin 指向它那个工具,响亮可定位;A 把「钉的值过时」与「工具被改写」压成一条模糊报错
④ 不扩散:A 与 B 同量,皆不加门禁;B 不新增第二种钥匙形状
objectstack-fleet commentedon Oct 31, 2026
epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U
Claim: PM loop round 3
Session:
session_017xRBcVBKAFk1ntXSjvPt7UAccount:
sunhaolin(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-27401-mcp-tool-snapshotWorktree:
objectstack-issue-27401Domain:
domain:serverSeat:
epic:#27293#1File surface:
packages/connectors/connector-mcp/src/mcp-provider.ts(+ its tests): theproviderConfig.toolsSnapshotrecord and its per-tool check in one helper; the degraded channel's wording for a pin mismatch, where it is raised (packages/services/service-automation/src/plugin.tsonly if the mismatch needs its own reason or retry policy there);content/docs/automation/connectors.mdx(the key); a@objectstack/connector-mcpchangeset (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(TIER_DEFAULT build —dispatch-gates --tierthis fire: no path-derived mandate; the ruling owes aCONTRACT_REVIEW_TIERreview before landing),effort: xhigh(p1)Clause-②: yes (widening)
Metadata-protocol: added
Responsibility:
n/a — not a defect cardThread-read: 6506797088
Ruling-ref: 6506797088
Premise: holds (read at
origin/mainc5bf56b82026-10-31T02:02Z:toolsSnapshot0 hits inconnector-mcp/src, controlinclude17 inmcp-provider.ts; theincluderefusals landed in46943657(#27402), so the pin composes with a loud allowlist)Serial constraints cleared:
none— none of the 60 most recent open PRs touchespackages/connectors/connector-mcp/**orservice-automation/src/plugin.ts(files read 2026-10-31T02:02Z).objectstack-fleet commentedon Oct 31, 2026
epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U
os-dev-report
{
"issue": 27401,
"status": "needs_decision",
"branch": "claude/issue-27401-mcp-tool-snapshot",
"pr": null,
"session": "session_017xRBcVBKAFk1ntXSjvPt7U",
"premise_still_valid": true,
"resume_from": "claude/issue-27401-mcp-tool-snapshot@68d8770",
"summary": "I built the ruled letter-B shape and pushed it to the branch. No PR is open. providerConfig.toolsSnapshot on mcp instances is a record of tool name → sha256 of the canonical JSON (RFC 8785 form) of name + description + inputSchema. Two shapes are refused before connecting as plain configuration faults: a malformed record, and a pin on a tool that include leaves out. After tools/list, each pinned tool is compared on its own. A pinned tool that changed, or that the server no longer serves, degrades the instance with one message naming each such tool and the digest it now serves. Docs, a minor changeset, and the exports computeMcpToolsSnapshot / mcpToolDigest are included. I stopped at the named channel question, as the dispatch trigger requires. On today's channel (ConnectorUpstreamUnavailableError) the husk reason and the connector_action refusal name the tool, and the retry backoff re-checks the pin. But on every attempt the materializer's ERROR line still says 'upstream unavailable … retrying with backoff' and the engine's WARN line says 'until its upstream is reachable'. Making those two lines truthful needs a marker the materializer can tell apart from an outage, which means a new error type in packages/spec. So the choice goes back to the maintainer (open_questions[0]). Readings of the ruling I took and tested: (1) a pinned tool that include also lists is reported by the pin (degraded) when the server drops it, not by include's typo refusal; (2) a selected tool the pin does not name is not checked, because tools the server adds are include's job; (3) a digest must be exactly 64 lowercase hex characters; (4) an empty record is accepted and pins nothing, like include: []. Under option A the branch can become a PR as it stands. Under B, connector-mcp changes only inside toolsSnapshotDriftError; the spec marker and a service-automation branch are added.",
"tests": "Package tests at 68d8770:
pnpm --filter @objectstack/connector-mcp testgave 'Test Files 6 passed (6) · Tests 80 passed (80)', lock VERDICT command-exit 0. The new src/tool-snapshot.test.ts has 24 tests, all passing. They cover: the digest equals an independently written sha256 string; two key orders hash equal at every depth; matching pins come up ready; one changed description degrades naming only that tool and closes the client; a changed inputSchema degrades too; a pinned tool that is no longer listed degrades naming it; a pinned tool that include also lists and the server drops is reported by the pin, not include; an unpinned include typo stays include's configuration fault; all drifts are named in one message; a partial pin works; 9 malformed records are refused before connecting; a pin on a tool outside include is refused before connecting; the control without toolsSnapshot is unchanged; a boot through AutomationServicePlugin registers the instance degraded, its degradedReason naming the tool, with no action. Typecheck:pnpm --filter @objectstack/connector-mcp typecheckexit 0. The test layer reports 'compiles … 1 file(s) / 5 error(s) / 1 pinned signature(s) held', the ledger unchanged from before. tsc --listFiles -p tsconfig.test.json counts 6 of 6 test files, tool-snapshot.test.ts among them. Ablation (run once, on the per-tool compare): node scripts/ablation-replace.mjs on src/tool-snapshot.ts replaced the anchor 'if (digest !== pinned) changed.push({ name, served: digest });' with a no-op. Its readings: 'ok mutation landed: anchor 1 → 0, blob 088fea03bab9 → 3532d1d24ba9'; then 'Tests 4 failed | 20 passed (24)'. The 4 failures are exactly the changed-description, changed-inputSchema, all-drifts-named and through-materializer cases. Then 'ok restored: blob == HEAD (088fea03bab9) and git diff HEAD is empty'. The subject is imported relatively from src, so there is no dist leg. Measurement of today's channel (a temporary test, deleted): the instance is state degraded with degradedHold undefined. The server was contacted 1 time at boot, 2 times by +5s, 3 times by +15s. Each attempt logs ERROR "[Automation] connector instance 'vendor_mcp' (provider 'mcp') upstream unavailable — instance registered degraded (no actions); retrying with backoff, attempt N." and WARN "Connector registered DEGRADED: vendor_mcp (origin: declarative) — no actions and no handlers until its upstream is reachable; …". The tool's name appears only in the records' meta.","gates": "Derived at 68d8770 with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, without paths as base os-dev.md says: 87 commands. I ran all 87, recording each exit code before any pipe. 84 exited 0 on the first run. 3 exited 3, which is PREREQUISITE NOT MET, not a finding: (a) check-plugin-teardown-shape.mjs --self-test, whose pinned commit 621a487 is not in the shallow clone. I fetched that one commit and the rerun exited 0 ('48 cases pass'). (b) spec check:skill-examples, which needs client and client-react declarations. I built that closure and the rerun exited 0 ('278 prose examples type-check across 3 surface(s)', which includes the new docs os:check block). (c) check:dual-build-cjs-loads, which needs every package's dist: NOT MEASURED locally. Declared narrowing: connector-mcp dist/index.js and dist/index.mjs both load and export computeMcpToolsSnapshot and mcpToolDigest, and the digest matches an independent sha256.--ranwith the exit codes recorded: 'Run reconciliation — 87 derived, 86 run, 1 NOT-MEASURED, 0 UNRUN', exit 0. There is no PR, so there are no check-runs to cite. pnpm lint, test:repo and the rest of the full CI suite were not run locally (CI-owned).","dogfood_battery": "n/a — no PR title (so no
!), and Clause-② carries the (widening) arm, not (narrowing)","mcp_calls": "0 — no MCP GitHub calls",
"api_writes": "1 — POST /repos//issues/27401/comments (this os-dev-report, through scripts/pm/post-stamped.mjs); also 3 git pushes to the branch (not REST)",
"line_budget": "n/a — no skills/** file and no line-ratcheted ledger touched",
"files_changed": [
".changeset/27401-mcp-tools-snapshot.md",
"content/docs/automation/connectors.mdx",
"packages/connectors/connector-mcp/src/index.ts",
"packages/connectors/connector-mcp/src/mcp-connector.ts",
"packages/connectors/connector-mcp/src/mcp-provider.ts",
"packages/connectors/connector-mcp/src/tool-snapshot.test.ts",
"packages/connectors/connector-mcp/src/tool-snapshot.ts"
],
"skills_docs": "Skills-docs: follow-up owed — skills/** not edited here (Tier H); whether a skill should teach providerConfig.toolsSnapshot is for the seat that owns skills/**",
"deviations": [
"No PR and no labels: status needs_decision, per the dispatch trigger 'If it needs a new error type in packages/spec, stop and report needs_decision'. The branch is pushed as the resume point.",
"dispatch-gates was derived without paths. The dispatch passed paths; base .claude/agents/os-dev.md derives from the merge base and wins on conflict.",
"My system prompt (a copy of os-dev) says pr_create carries assignees and the report comment opens with the marker. Base os-dev.md says no PR assignees and a seat-signature line first, via post-stamped.mjs. I followed base.",
"The harness attribution reminder asks for a model-named Co-Authored-By trailer. AGENTS.md requires the model-free pair, so the commits carry that.",
"To make check-plugin-teardown-shape --self-test runnable, I fetched one commit (621a487, --depth=1 --no-write-fetch-head) into the shared object store.",
"The adding PR does not exist yet, so its body (Clause-②: yes (widening), Metadata-protocol: added, Skills-docs line, the 维护者速读 block) is unwritten. The changeset already carries the two declaration lines."
],
"open_questions": [
{
"question": "The ruling says a toolsSnapshot mismatch degrades the instance. Today's ADR-0097 contract has exactly one marker that degrades without failing boot: CONNECTOR_UPSTREAM_UNAVAILABLE, which packages/spec declares as 'the upstream … is temporarily unreachable'. Thrown for a mismatch (as on the branch), it gives: a degradedReason that names the tool (GET /connectors and the connector_action refusal); re-checks on the 5s → 5min backoff; and on every attempt an ERROR 'upstream unavailable … retrying with backoff' plus a WARN 'until its upstream is reachable', with the tool named only in meta. A plain throw would fail the boot, and CONNECTOR_CREDENTIAL_REJECTED would mark a credential rejected. Which channel should a pin mismatch use?",
"options": [
"A — Ship as on the branch: reuse CONNECTOR_UPSTREAM_UNAVAILABLE. The instance is degraded with a reason naming each tool and the served digest. It is re-checked on the backoff: a server that restores the pinned definitions recovers it, and an edited pin re-materializes it at once. The materializer's ERROR and the engine's WARN keep the outage wording. Cost: nothing more; 1 PR (connector-mcp only). Long-term cost: a spec marker thrown outside its declared meaning, and an operator reading logs sees 'upstream unavailable' during a tool rewrite.",
"B — Add a spec marker (for example CONNECTOR_UPSTREAM_CHANGED) to @objectstack/spec/integration connector-provider-errors.ts: a class, a structural guard, and an error-code ledger row. service-automation gets a materializer branch that degrades with its own wording ('serves a tool definition that differs from its toolsSnapshot; re-checking with backoff', and the engine record to match), on the same backoff. connector-mcp changes only its one throw. Cost: S, one PR across spec + service-automation + connector-mcp (spec lane). It adds one error code and no gate.",
"C — Widen CONNECTOR_UPSTREAM_UNAVAILABLE's declared meaning in the spec TSDoc to cover 'serves a definition its pin does not match', and make the two log lines cause-neutral for every provider. Cost: XS in lines, but a semantic change that rewrites the outage wording every provider gets, and the code's name still says 'unavailable'."
],
"recommendation": "B. Holding (no retry) is not offered: the server can restore the pinned definition where this process cannot see it, which is the same reason an environment or vault credential refusal is retried on the backoff. A hold would also need B's marker. Four axes: ① Actual business need: nobody on the board names a pin, and the item sits in the maintainer's directed scope. Measured: A and B show the administrator the same reason; they differ only in the two log lines an operator reads during an incident. ② Long-term soundness: B keeps every marker's declared meaning true (contract-first). A makes 'temporarily unreachable' false for one of its throws. C changes the meaning for every provider. ③ Preventing AI mistakes: under A, an agent reading the ERROR record goes after the network, and a provider author learns the marker means 'anything degradable'. Under B the record names the pin. ④ Startup-stage scope: A adds nothing. B adds one error code and one materializer branch (S, no gate). C is smallest in lines but semantic. Trade-off: ② and ③ favour B, ④ favours A. B is a cross-lane spec change before this item ships; A ships now and keeps two misleading log lines."
}
],
"out_of_scope_findings": []
}
objectstack-fleet commentedon Oct 31, 2026
epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U
Release: claim 6537542245 (
session_017xRBcVBKAFk1ntXSjvPt7U, epic:#27293#1) · premise falsified, back to the decision box · 2026-10-31T02:55ZResume-from: claude/issue-27401-mcp-tool-snapshot@68d8770f
维护者速读
toolsSnapshotSha256, or a per-tool pin record that can name the tool a server rewrote #27401(你裁的 B,按工具逐个钉快照)已在分支上做完(24 个测试,删掉逐工具比对时 4 个转红),但没开 PR:「快照对不上时实例进入降级」这一步,现在只能借用「上游暂时不可达」这个错误码,日志会写成「上游不可用、退避重试」。68d8770f不动;MCP 侧仍没有快照校验(现状),include照常挡住新增工具。git show origin/main:packages/services/service-automation/src/plugin.ts | sed -n 2655,2659p(每次重试打的那句 ERROR)。Background
Ruling 6506797088 (letter B), verbatim: "Carried into the dispatch as a named question for the dev: the degraded channel's wording ("upstream unavailable … retrying with backoff") and its reconnect loop are specialised for a pin mismatch, since no reconnect fixes a pin — the dev names what it does in the PR body." The dev's report: 6538058257. Read at
origin/mainc5bf56b8:CONNECTOR_UPSTREAM_UNAVAILABLE(packages/spec/src/integration/connector-provider-errors.ts, ledger rowpackages/spec/src/api/error-code-ledger.zod.ts:1620), declared as "the upstream … is temporarily unreachable".degradedReasonand theconnector_actionrefusal name the changed tool; the instance is re-checked on the 5 s → 5 min backoff; every attempt logs ERROR "… upstream unavailable — instance registered degraded (no actions); retrying with backoff, attempt N." (service-automation/src/plugin.ts:2658) and WARN "… until its upstream is reachable" (plugin-extension-api.ts:388), the tool named only in the records' meta.Governing text: ruling 6506797088 (above); AGENTS.md Prime Directive #12 (contract-first; a declared marker means what it declares);
ERROR_CODE_LEDGER(a new code isClause-②: yes, spec lane).Options
CONNECTOR_UPSTREAM_UNAVAILABLECONNECTOR_UPSTREAM_CHANGED: class, guard, ledger row) and a materializer branch with its own wording ("serves a tool definition that differs from its toolsSnapshot; re-checking with backoff"), same backoffCONNECTOR_UPSTREAM_UNAVAILABLE's declared meaning to cover a pin mismatch and make both log lines cause-neutral for every providerBusiness meaning: A puts a "network down" sign on a tampered package; B gives tampering its own sign; C makes every sign say "something is wrong".
四维分析
os-decision-facets
Prior rulings read: toolsSnapshot, upstream unavailable, degraded channel → ruling 6506797088 (shape B; channel left to the dev as a named question); thread: [Decision] connector-mcp tool-snapshot pin (#26709 item 3(c)): one aggregate
toolsSnapshotSha256, or a per-tool pin record that can name the tool a server rewrote #27401; repo: objectstack-ai/objectstackRecommendation
B, keeping the backoff re-check (the dev measured that it recovers a restored server, the same reason a vault or env credential refusal is retried). Not A: a published marker would say something false in every incident log. Not C: it rewrites the meaning every provider relies on. Confidence gap: B crosses into the spec lane (a new error code is
Clause-②: yes), so its PR owes aCONTRACT_REVIEW_TIERreview.Self-check: 只看①选 B;②③④ 是否翻转:否(④ 偏 A,但只影响工作量,不翻字母)。
After the ruling
Resume-fromabove; the dev opens the PR from68d8770fas it stands.Resume-from; the same branch adds the spec marker and the materializer branch; contract review atCONTRACT_REVIEW_TIERbefore landing.objectstack-fleet commentedon Oct 31, 2026
director#2 · os-tesla · session_01L9qibxdiybU8ehPKBroYE3
Brief-claimed-by: session_01L9qibxdiybU8ehPKBroYE3 · batch #433 · 2026-10-31T15:17Z
Director seat (the maintainer's decisions thread, 2026-10-31). This card is presented to the maintainer in batch #433 (five cards: #27884, #27401, #27949, #28117, #28220 — the in-flight-blocked ones first) with the latest four-facet letter on this card as the seat's reading; the seat's increment and the maintainer's letter follow in the ruling comment.
objectstack-fleet commentedon Oct 31, 2026
director#2 · os-tesla · session_01L9qibxdiybU8ehPKBroYE3
Ruling: batch #433 item 2 · letter B · maintainer 「同意」 2026-10-31T15:49Z
Director seat (the maintainer's decisions thread). Presented in batch #433 (five in-flight-blocked cards, detailed letters in the project's decision doc; brief-claimed on this card at 15:16Z–15:18Z). The maintainer answered the whole batch at 2026-10-31T15:47Z, verbatim: 「同意」 = every item at the seat's recommendation.
What the ruling binds (letter B): a
toolsSnapshotpin mismatch degrades the instance through a marker of its own — one new spec error marker (for exampleCONNECTOR_UPSTREAM_CHANGED: class, structural guard, error-code ledger row in@objectstack/spec) and one materializer branch in service-automation with its own wording ("serves a tool definition that differs from its toolsSnapshot; re-checking with backoff"), on the same 5 s → 5 min backoff the dev measured to recover a restored server.CONNECTOR_UPSTREAM_UNAVAILABLEkeeps its declared meaning. The branch's connector-mcp half (68d8770) stands; only its one throw changes. Not taken: A (ship as on the branch, the published marker says "unreachable" in every incident log), C (widen the marker's meaning for every provider). The shape ruling 6506797088 (letter B, per-tool record) is unchanged.Workload of the letter:
domain:serverwith a spec-lane edit · 1 PR across spec + service-automation + connector-mcp · S ·Metadata-protocol: added(thetoolsSnapshotkey, as already ruled) plus a new error code ⇒Clause-②: yes· contract review atCONTRACT_REVIEW_TIERbefore landing.Execution: the #27293 epic PM re-claims with
Resume-from: claude/issue-27401-mcp-tool-snapshot@68d8770fandRuling-ref:naming this comment;needs-user-decision→pm:dispatchedis the epic PM's stroke (pm:epicstays). Analysis read: 6538081826.objectstack-fleet commentedon Nov 1, 2026
epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U
Claim: PM loop round 4 (re-claim after the ruling)
Session:
session_017xRBcVBKAFk1ntXSjvPt7UAccount:
sunhaolin(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-27401-mcp-tool-snapshotWorktree:
objectstack-issue-27401Domain:
domain:serverSeat:
epic:#27293#1File surface:
packages/spec/src/integration/connector-provider-errors.ts(the new marker class and guard),packages/spec/src/api/error-code-ledger.zod.ts(its ledger row) and whatever the error-code and route gates require beside them;packages/services/service-automation/src/plugin.ts(one materializer branch with its own wording, same backoff);packages/connectors/connector-mcp/src/**(the branch'stoolsSnapshotwork, its throw switched to the new marker);content/docs/automation/connectors.mdx; changesets for the touched packages (stop on breach; explain in the report)Container & model:
M,mode:cloud(apackages/spec/src/**surface plus other packages is build-heavy; one card, one container),model: opus(TIER_DEFAULT build; the ruling owes aCONTRACT_REVIEW_TIERreview before landing),effort: xhigh(p1)Clause-②: yes (widening)
Metadata-protocol: added
Responsibility:
n/a — not a defect cardThread-read: 6545635244
Ruling-ref: 6545635244
Resume-from: claude/issue-27401-mcp-tool-snapshot@68d8770f
Premise: holds (read at
origin/mainea74ebf92026-11-01T03:06Z: the branch head is68d8770f; no marker other thanCONNECTOR_UPSTREAM_UNAVAILABLEdegrades without failing boot)Serial constraints cleared: open PR #28357 (#28311, this seat) edits
connector-mcp/src/mcp-provider.tsand the TSDoc ofconnector-provider-errors.ts— it lands first or this branch merges it; open PR #28500 (another seat) editsservice-automation/src/plugin.ts— ordinary concurrency, the later lander mergesorigin/main.objectstack-fleet commentedon Nov 1, 2026
epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U
Dispatched (
mode:cloud) · 2026-11-01T03:07Z · claim 6552681469session_017xRBcSf5BEfcbq4uCnjEXu(title "⚡ server [Decision] connector-mcp tool-snapshot pin (#26709 item 3(c)): one aggregatetoolsSnapshotSha256, or a per-tool pin record that can name the tool a server rewrote #27401 mcp tool-snapshot marker"), checked out atclaude/issue-27401-mcp-tool-snapshot(68d8770f), outcome branch the same.CONTRACT_REVIEW_TIERreview before landing (ruling 6545635244); the seat arranges it after the report.