PBIMCP写入适配器

Kemetra2026-10-26查看 1667复制 699

通过 Microsoft 官方的 Power BI Modeling MCP,在 Seshat 记录的具名人工审批之后,APPLY 一个已获批准的 Power BI 语义模型变更——闭合受治理的最后一公里,使该变更不会在 Desktop 中被手工应用而毫无记录。当有人要求应用、推送或发布一个已批准的模型编辑,要求运行 `seshat pbi-mcp apply` / `plan-write`,或询问某次写入为何被拒绝时使用。此适配器 EXECUTES 一个 Core Authority 已做出的决定:它读取 COMMITTED appr...

Download (.md)

📘如何使用这个技能

  1. 1
    复制技能内容·点击上方「Copy SKILL.md」复制全部内容,或 Download 下载到本地
  2. 2
    打开你的 AI 客户端·支持 Claude Code、Trae、Cursor、ChatGPT、DeepSeek、Kimi 等任意 AI 助手
  3. 3
    粘贴为系统提示/知识文件·Claude Code:放入 .claude/skills/ 目录;普通对话:作为首条消息发送并说「请严格按这份技能文档执行」
  4. 4
    直接提出你的需求·AI 会按技能定义的工作流、格式和约束来处理,无需再解释背景

💡 提示:技能生效后,每次对话只需一句需求,AI 会按 SKILL.md 的流程执行。

SKILL.md10803 chars
---
name: pbi-mcp-write-adapter
description: >-
  APPLY an already-approved Power BI semantic-model change through Microsoft's official
  Power BI Modeling MCP, behind Seshat's recorded named-human approval -- closing the
  governed last mile so the change is not applied by hand in Desktop with nothing
  recorded. Use when someone asks to apply, push, or publish an approved model edit, to
  run `seshat pbi-mcp apply` / `plan-write`, or asks why a write was refused. This adapter
  EXECUTES a decision Core Authority already made: it reads COMMITTED approvals as the GO
  signal, refuses on any unmet precondition, validates the touched artifact afterwards, and
  writes derived run-evidence. It never defines a metric or a model, never moves a readiness
  stage to pass, never writes `approvals[]`, and never grants itself the authority it checks.
---

# pbi-mcp-write-adapter

- **Authority category (F024):** Execution Adapter / `publish-capable`, execution-only --
  the binding declaration is `templates/pbi-mcp-adapter-contract.md`.
- **Roadmap feature:** F016 slice 5  **On-disk spec:** `specs/149-pbi-mcp-write-adapter`
- **Authorizing decision:** `docs/decisions/0018-unpark-f016-power-bi-mcp-execution-adapter.md`
  (ratified by the owner 2026-08-18). All eight ADR decisions bind together; none is severable.

## What problem this closes

A user who has taken a semantic model all the way to an approved, signed-off state still had
to leave the governed workflow and apply the change by hand in Power BI Desktop. Nothing
recorded what was applied, to which target, on whose authority, or whether the artifact still
validated afterwards. Microsoft's official Power BI MCP *can* apply such changes, but its own
documentation warns that autonomous or misconfigured clients may perform destructive actions
and that its safety flags are non-standard and client-dependent.

So the gap was never "we lack a tool". It was "the available tool's safety model is weaker
than the approval spine we already run." This adapter puts Seshat's recorded, named-human
approval **above** the vendor tool rather than trusting the vendor's prompts.

## The rule you must not talk yourself out of

**You never supply the evidence that permits your own write.** Every precondition is
*derived* from committed state, never accepted as something you assert. If you find yourself
reasoning "the user said it was approved" or "I already checked that", stop: ask *checked
against what?* If the answer is "what I was told", the gate has not been satisfied.

This is not a style preference. Five separate versions of that exact defect were caught in
this feature's own review -- a caller-supplied allowlist, a caller-asserted operation binding,
a caller-asserted backup, a permissive git-state default, and a gate that read the working
tree (where an agent can write) instead of HEAD.

## Preconditions for a write (all required, none severable)

A write proceeds only when **every** one of these holds. Each has its own typed blocker, so a
refusal names the specific missing authority:

| # | Precondition | Blocker |
|---|---|---|
| 1 | `semantic_model_ready` is `pass` for the target, read from **HEAD** | `PBIMCP-GATE-01` |
| 2 | readiness state is readable (absent / malformed / unreadable all refuse) | `PBIMCP-GATE-02` |
| 3 | readiness state is **committed** -- tracked and identical to HEAD | `PBIMCP-GATE-03` |
| 4 | a shape-valid named-human `publish_ready` approval exists | `PBIMCP-GATE-04` |
| 5 | that approval's note names **this** target as a whole token | `PBIMCP-GATE-05` |
| 6 | the operation resolves to an approved operation for this target | `PBIMCP-GATE-06` |
| 7 | the target is in the **committed** allowlist | `PBIMCP-GATE-07` |
| 8 | the allowlisted artifact exists on disk | `PBIMCP-GATE-08` |
| 9 | the tree is clean, or a **resolvable** backup ref was named | `PBIMCP-GATE-09` |

Plus: an uncommitted allowlist (`-10`), an unprobed git state (`-11`), and an unresolvable
backup ref (`-12`) each refuse on their own. A model-folder target also refuses (`-16`) when
another table in that folder has its own readiness record without a committed
`semantic_model_ready: pass` -- the flush rewrites every table file in the folder.

Two of these deserve emphasis because they are the ones most likely to be argued away:

- **#3, committed state.** A passing `readiness-status.yaml` that exists only in the working
  tree is refused. An agent can write files, so a worktree read would let the agent author
  its own approval. `dagster_adapter/gate.py` guards only `unresolved-questions.md`, so
  "mirror the shipped reader" reproduces this hole -- don't.
- **#5, whole-token naming.** An approval naming `sales_model` does **not** authorize
  `sales_model_v2`. A substring check would let a loosely worded note widen its own scope,
  which is self-granted authority.

## The standing invariant

`--skipconfirmation` is refused in **every** mode -- including read-only, including in test
fixtures -- and the check runs before any invocation. It is not a branch inside write mode.
`seshat.pbi_mcp.detect.refuse_if_bypass_flag` **raises**; there is no verdict to ignore.

Read-only is the resting state. Write mode is never reached by omission, and `--readwrite` is
never a default.

## How to drive it

```bash
# 1. ALWAYS dry-run first. Evaluates every precondition, mutates nothing.
seshat pbi-mcp plan-write --target <target_id> --operation <operation_id> --json

# 2. Only once plan-write reports no blockers:
seshat pbi-mcp apply --target <target_id> --operation <operation_id> --json

# When the tree is legitimately dirty, name a backup ref that RESOLVES:
seshat pbi-mcp apply --target <t> --operation <o> --backup-ref refs/tags/pre-write
```

Both legs accept identical arguments, so `plan-write` is a truthful preflight for `apply`.

**Exit codes** -- 2 and 3 are deliberately distinct:

| Code | Meaning |
|---|---|
| 0 | applied and confirmed by validation (`materialized`), or a clean dry run (`deferred`) |
| 1 | refused; nothing was mutated |
| 2 | applied, then post-write validation FAILED -- rollback guidance is printed |
| 3 | **indeterminate** -- the runtime stalled or died; the artifact may be half-written |

Never treat 3 as 1. A clean failure and a possibly-corrupted artifact are different problems.

## What a write does NOT do

- **It does not move any readiness stage.** A successful write leaves `publish_ready` exactly
  as it found it (FR-018). A green write is not an approval and never becomes one.
- **It does not write `approvals[]`.** Approval and readiness records are read-only inputs.
- **It does not define anything.** No metrics, no mappings, no semantic logic, no dashboard
  design. It executes a decision made upstream; if the definition is absent, it refuses
  rather than inventing one.
- **It does not vendor the runtime.** The official MCP is invoked through `npx` -- external,
  unforked, independently upgradeable (ADR 0018 rejected vendoring).

## Evidence

Every run -- success, failure, and refusal -- writes exactly one record to
`.seshat/pbi-mcp-write-evidence.json` (latest run; every earlier run is retained in the append-only `.seshat/pbi-mcp-write-evidence.jsonl`): what ran, in which mode, against which target, when,
and how it ended. The record carries a fixed authority label (`derived-evidence-only`), typed
blockers, and **no numeric, maturity, or confidence score** of any kind.

`mutation_attempted` tells a refusal apart from an indeterminate run -- both can read
`blocked`, and only that field distinguishes "nothing was touched" from "state unknown".

An **intent** record lands *before* the mutation, so a process killed mid-write still leaves a
trace naming what was attempted.

## Post-write validation

A zero exit from the vendor runtime is not confirmation. Validation runs
`seshat semantic-check --require-inputs` against the touched artifact:

- the **semantic-model** family, not the report-layer R-family (which iterates
  `*.Report/definition.pbir` and contains no TMDL, so it would examine zero bytes of what
  changed);
- `--require-inputs` because `semantic-check` otherwise exits 0 on an empty corpus while
  printing "nothing was verified ... This is NOT a clean result";
- and "validated" is structurally unrepresentable when zero artifacts were examined.

A validation failure is **blocking with rollback guidance**, never a warning.

## Deliberate non-features (do not "fix" these)

- **No approval expiry.** The gate takes the first shape-valid `publish_ready`
  approval and checks only that its `at:` date parses. An approval from a year ago
  still authorizes a write. This is recorded as an open owner decision rather than
  silently chosen: a staleness bound is a governance policy (how old is too old?),
  not an implementation detail, and inventing a number would be fabricating the
  owner's judgment. `rules/readiness_status.py` has a `_check_audit_freshness`
  notion this gate deliberately does not consume. Raise it with the owner if a
  bound is wanted; do not add one unasked.
- **No concurrency lock.** Two simultaneous writes to the same target both
  re-evaluate the gate and can both proceed; the evidence path is fixed, so the
  second run overwrites the first's record. The spec's "must not interleave" claim
  is stronger than what the design delivers. Per-invocation re-evaluation is not
  mutual exclusion.
- **The vendor argv is unvalidated against the real binary.** Every runner test
  injects a fake, so `build_argv`'s flag names are unverified against a live
  `npx @microsoft/powerbi-modeling-mcp`. Both servers are unreleased previews;
  capability drift is what guards against the flag names having moved.

## Known blocked scope

- **FR-011b (approval-time content hash) is EXTERNALLY BLOCKED**, by owner decision
  2026-08-18. Verifying a definition against a hash recorded at sign-off needs a producer
  written by a named human at approval time, and this feature is forbidden to write
  approvals. Operation *resolution* and target-match still apply. Do **not** "fix" this by
  writing the hash yourself -- that is the adapter authorizing its own mutation.
- **Slice 6 (the remote query-only server) is out of scope** (ADR decision 7). Nothing here
  may make remote query results an input to any readiness stage.

## When to hard-stop and ask

- The approval note does not name the target → the owner records a fresh approval. **Never
  reword an existing note.** A missing approval is closed by a human deciding, not by an
  agent editing prose.
- The target is not in the committed allowlist → adding it is a reviewed, committed change.
- Any Principle-V judgment call (what the change should be, whether it is correct, whether a
  stage may advance) → not yours. Stop.
来源:skillsdirectory

评论0

登录后参与评论

还没有评论,来抢沙发吧