macp-runtime 0.8.2

MACP reference runtime: a coordination kernel and gRPC server enforcing session boundaries, message validation, append-only history, modes, and governance policy.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
name: Spec drift watch

# The `conformance-oracle` job in ci.yml pins the spec repo to one commit
# (`SPEC_REV`) so that a required status check is reproducible: before the pin,
# CI read the spec's default branch at run time, which made our green depend on
# another repository's merge timing. On 2026-09-11 three separate upstream merges
# turned this repo's CI red with no change on our side, and the third blocked a
# release PR.
#
# Pinning alone would rot into a dead gate -- drift would accumulate silently
# until somebody happened to bump the pin. This job is the other half of the
# deal: it watches the pin, never blocks a PR, and files one tracked issue when
# the spec moves ahead of us in a way that actually touches the artifacts we
# mirror.
#
# Deliberately narrow: it reports only on `schemas/conformance/**`,
# `schemas/json/policy/**`, and `schemas/parity/**`, the three trees the
# conformance-oracle job consumes.
# RFC prose moving without a schema or fixture change is not drift we can act
# on mechanically, and reporting it would train everyone to ignore this issue.
#
# The comparison itself stays wide -- every byte difference in either tree is
# read and reported on the run -- but escalation (filing a GitHub issue) is
# classified. Two shapes are treated as non-actionable rather than dropped:
# a non-`.json` file under `schemas/conformance/` (outside `check_dir`'s glob
# in ci.yml, so it can never fail that gate) and a `schemas/json/policy/*.json`
# diff that is provably annotation-only -- structurally identical once
# `description`/`$comment`/`title` are stripped at every depth, which is all
# `enum_lists_match_the_canonical_schemas` (registry.rs) ever reads. Suppressing
# the *issue* for these is safe because neither oracle gate can see them; making
# the *diff* invisible would not be, because annotation text is exactly where
# the spec records new normative reasoning (see issue #169). So suppression
# only ever removes an entry from escalation, never from the step summary.

on:
  schedule:
    # Daily, 07:00 UTC. Spec merges cluster in working hours; one check a day is
    # enough for a gate nobody is blocked on.
    - cron: '0 7 * * *'
  workflow_dispatch:

permissions:
  contents: read
  issues: write

concurrency:
  group: spec-drift
  cancel-in-progress: false

jobs:
  drift:
    name: Spec drift against the pinned revision
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v7

      - name: Read the pinned revision from ci.yml
        id: pin
        run: |
          set -euo pipefail
          # Single source of truth: the SPEC_REV in ci.yml. Parsed rather than
          # duplicated here, so the pin can never disagree with what CI used.
          rev=$(grep -E '^[[:space:]]+SPEC_REV:' .github/workflows/ci.yml \
                | head -1 | sed -E 's/.*SPEC_REV:[[:space:]]*"?([0-9a-f]+)"?.*/\1/')
          if [ -z "$rev" ]; then
            echo "::error::could not parse SPEC_REV out of .github/workflows/ci.yml"
            exit 1
          fi
          echo "rev=$rev" >> "$GITHUB_OUTPUT"
          echo "pinned spec revision: $rev"

      - name: Checkout spec repo at the pinned revision
        uses: actions/checkout@v7
        with:
          repository: multiagentcoordinationprotocol/multiagentcoordinationprotocol
          ref: ${{ steps.pin.outputs.rev }}
          path: spec-pinned

      - name: Checkout spec repo at its default branch
        uses: actions/checkout@v7
        with:
          repository: multiagentcoordinationprotocol/multiagentcoordinationprotocol
          path: spec-head
          # Full history: "Commits since the pin" below walks
          # $PINNED..$head_sha on this checkout, which needs $PINNED to be a
          # reachable ancestor. The default fetch-depth: 1 makes that range
          # empty every single run (the pinned commit is never in a
          # one-commit-deep history) -- that was already happening before
          # this change and is fixed here because it's the same step.
          fetch-depth: 0

      - name: Compare the trees the conformance oracle consumes
        id: diff
        run: |
          # The printf formats below emit literal issue-body markdown, double-quoted
          # with escaped backticks rather than single-quoted. (Single-quoted backticks
          # elsewhere in this step -- the annotation-diff markdown fence markers below,
          # and the jq program in strip_is_annotation_only -- lint clean: SC2016 is
          # content-sensitive, not a blanket rule against single-quoting a backtick.)
          #
          # GitHub runs this step as `bash -e {0}` by default (no `shell:` override
          # here), layered on top of this script's own `set -uo pipefail` below -- so
          # -e IS live. Every command whose non-zero exit is expected and should not
          # abort the step needs `|| true`, `|| return 1`, or to sit inside an
          # `if`/while-condition, exactly like the `diff … || true` calls below.
          set -uo pipefail
          head_sha=$(git -C spec-head rev-parse HEAD)
          echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"

          no_drift_comment="The mirrored trees (\`schemas/conformance\`, \`schemas/json/policy\`, \`schemas/parity\`) no longer differ from the pinned revision. Closed automatically."

          if [ "$head_sha" = "$PINNED" ]; then
            echo "drift=none" >> "$GITHUB_OUTPUT"
            {
              echo 'close_comment<<REPORT_EOF'
              echo "$no_drift_comment"
              echo REPORT_EOF
            } >> "$GITHUB_OUTPUT"
            echo "pin is current; nothing to report"
            exit 0
          fi

          # Raw report, both directions, both trees. No annotation-stripping
          # and no relabelling here -- classification below needs the real
          # spec-pinned/spec-head paths to open the files, and relabelling
          # first would leave nothing for the classifier to match.
          report=$(mktemp)
          for tree in schemas/conformance schemas/json/policy schemas/parity; do
            # README.md is excluded deliberately: it is not vendored into this
            # repo, so a change to it implies no action from the issue's
            # checklist. Reporting it would be noise, and noise is how a
            # non-blocking issue gets trained into being ignored.
            #
            # LC_ALL=C pins diff's "Files ... differ" / "Only in ...: ..."
            # wording so the classifier below can match it as a contract,
            # not an incidental default.
            LC_ALL=C diff -rq -x 'README.md' "spec-pinned/$tree" "spec-head/$tree" >> "$report" 2>&1 || true
          done

          if [ ! -s "$report" ]; then
            echo "drift=prose-only" >> "$GITHUB_OUTPUT"
            {
              echo 'close_comment<<REPORT_EOF'
              echo "$no_drift_comment"
              echo REPORT_EOF
            } >> "$GITHUB_OUTPUT"
            echo "spec moved to $head_sha but none of the mirrored trees changed; not reporting"
            exit 0
          fi

          # ---- Classification ---------------------------------------------
          #
          # `check_dir` (ci.yml) only ever reads *.json directly under
          # schemas/conformance (and schemas/conformance/cmt-hash/*.json)
          # against the vendored tests/conformance tree -- so a non-.json
          # file there can never move that gate. And
          # enum_lists_match_the_canonical_schemas (registry.rs) only reads
          # schema *keywords* out of schemas/json/policy/*.json -- never
          # `description`, `$comment` or `title` -- so a policy-schema diff
          # that is structurally identical once those three are stripped at
          # every depth can never move that gate either. Everything else is
          # actionable, and the classifier fails closed: anything it cannot
          # positively place in the suppressible buckets stays actionable.

          strip_is_annotation_only() {
            # $1, $2: two JSON file paths. Exit 0 iff both parse and are
            # structurally identical once description/$comment/title are
            # removed at every depth. Any jq failure (malformed JSON, unreadable
            # file) is "not annotation-only" -- fail closed, never fail open.
            local a b
            a=$(jq -S 'walk(if type == "object" then del(.["$comment"], .description, .title) else . end)' "$1" 2>/dev/null) || return 1
            b=$(jq -S 'walk(if type == "object" then del(.["$comment"], .description, .title) else . end)' "$2" 2>/dev/null) || return 1
            [ "$a" = "$b" ]
          }

          actionable=$(mktemp)
          suppressed=$(mktemp)
          suppressed_detail=$(mktemp)

          while IFS= read -r line; do
            rel=""
            case "$line" in
              "Files spec-pinned/"*" and spec-head/"*" differ")
                # Field 2 of "Files spec-pinned/X and spec-head/Y differ" is
                # spec-pinned/X. X and Y are always equal here (both sides of
                # the checkout use the same relative tree layout), so field 2
                # alone is enough once the prefix is stripped.
                rel=$(printf '%s\n' "$line" | awk '{print $2}' | sed 's|^spec-pinned/||')
                ;;
            esac

            if [ -n "$rel" ]; then
              # Guard: if the extracted path isn't a real file on both sides,
              # the awk field-split was wrong (e.g. a path containing a
              # space truncates at the first space) -- don't trust a
              # possibly-truncated name, fall through to "actionable".
              if [ ! -f "spec-pinned/$rel" ] || [ ! -f "spec-head/$rel" ]; then
                rel=""
              fi
            fi

            if [ -z "$rel" ]; then
              # Not a matched "Files ... differ" line (an "Only in ..." line
              # in either direction, a stderr line, or a failed guard above)
              # -- actionable unexamined.
              echo "$line" >> "$actionable"
              continue
            fi

            case "$rel" in
              schemas/conformance/*.json)
                # Shell `case` globs cross `/`, so this also matches
                # cmt-hash/*.json and anything deeper -- the conservative
                # reading check_dir deserves. Fixture bytes must always
                # block: check_dir demands byte identity, and the fixtures'
                # own _comment/description keys are load-bearing bytes there
                # even though this repo's loader drops them at parse time.
                echo "$line" >> "$actionable"
                ;;
              schemas/conformance/*)
                echo "$line" >> "$suppressed"
                echo "- \`$rel\`: not a \`*.json\` fixture; outside \`check_dir\`'s glob (ci.yml)" >> "$suppressed_detail"
                ;;
              schemas/json/policy/*.json)
                if strip_is_annotation_only "spec-pinned/$rel" "spec-head/$rel"; then
                  echo "$line" >> "$suppressed"
                  {
                    echo "- \`$rel\`: annotation-only (\`description\`/\`\$comment\`/\`title\`), verified structurally. Diff:"
                    echo '  ```diff'
                    # diff -u exits 1 whenever the files differ, which is always --
                    # we only reach this line because the earlier diff -rq already
                    # said so. Under the live -e (see the step's opening comment)
                    # an unguarded pipeline here aborts the whole step, silently,
                    # on the exact happy path this phase exists to create.
                    LC_ALL=C diff -u "spec-pinned/$rel" "spec-head/$rel" | sed 's/^/  /' || true
                    echo '  ```'
                  } >> "$suppressed_detail"
                else
                  echo "$line" >> "$actionable"
                fi
                ;;
              *)
                echo "$line" >> "$actionable"
                ;;
            esac
          done < "$report"

          # Relabel both lists now that classification is done -- matching
          # first, relabelling second, is load-bearing: relabelling before
          # classifying would leave the case arms above matching nothing.
          sed -i 's|spec-pinned/|pinned:|g; s|spec-head/|upstream:|g' "$actionable" "$suppressed"

          if [ -s "$actionable" ]; then
            verdict="yes"
          elif [ -s "$suppressed" ]; then
            verdict="non-actionable"
          else
            # Unreachable in practice (every report line lands in one of the
            # two buckets above), kept as a safety net -- but it still needs
            # a close_comment, same as the other two non-"yes" exits, or the
            # close step's `gh issue close --comment ""` breaks under its
            # own `set -euo pipefail`.
            verdict="prose-only"
            {
              echo 'close_comment<<REPORT_EOF'
              echo "$no_drift_comment"
              echo REPORT_EOF
            } >> "$GITHUB_OUTPUT"
          fi
          echo "drift=$verdict" >> "$GITHUB_OUTPUT"

          # ---- Visibility ---------------------------------------------------
          #
          # Must run before any verdict branches below, and unconditionally
          # whenever something was suppressed -- including when the overall
          # verdict is "yes", since a run can carry both actionable and
          # suppressed entries at once. This is the half of the design that
          # keeps suppression from turning into silence (see the file header
          # and issue #169): nothing here should be trimmed for tidiness.
          if [ -s "$suppressed" ]; then
            suppressed_count=$(wc -l < "$suppressed" | tr -d ' ')

            # Cap the embedded detail (which can include full annotation
            # diffs) across ALL suppressed entries combined, not per entry,
            # so a large upstream rewrite can't approach the 1 MiB step
            # summary limit.
            detail_lines=$(wc -l < "$suppressed_detail" | tr -d ' ')
            detail_capped=$(mktemp)
            if [ "$detail_lines" -gt 200 ]; then
              head -n 200 "$suppressed_detail" > "$detail_capped"
              echo "" >> "$detail_capped"
              echo "_(truncated: $((detail_lines - 200)) more line(s) omitted)_" >> "$detail_capped"
            else
              cp "$suppressed_detail" "$detail_capped"
            fi

            {
              echo "### Spec drift: $suppressed_count suppressed (not actionable by any oracle gate)"
              echo
              echo '```'
              cat "$suppressed"
              echo '```'
              echo
              cat "$detail_capped"
              echo
            } >> "$GITHUB_STEP_SUMMARY"

            notice_count=0
            while IFS= read -r sline; do
              notice_count=$((notice_count + 1))
              if [ "$notice_count" -le 10 ]; then
                echo "::notice::spec-drift suppressed (not actionable): $sline"
              fi
            done < "$suppressed"
            if [ "$notice_count" -gt 10 ]; then
              echo "::notice::$((notice_count - 10)) more suppressed entries -- see the step summary"
            fi
          fi

          if [ "$verdict" = "non-actionable" ]; then
            {
              echo 'close_comment<<REPORT_EOF'
              echo "The mirrored trees (\`schemas/conformance\`, \`schemas/json/policy\`, \`schemas/parity\`) differ from the pinned revision (\`$head_sha\`), but only in ways \`conformance-oracle\`'s gates cannot act on -- see this run's step summary for detail. The pin itself remains behind. Closed automatically; this is not tracked as an open issue."
              echo REPORT_EOF
            } >> "$GITHUB_OUTPUT"
            echo "spec moved to $head_sha; differences exist but none is actionable -- not filing"
            exit 0
          fi

          # ---- verdict == yes: build the issue body -------------------------

          body=$(mktemp)
          {
            printf "The spec repo has moved ahead of the revision \`conformance-oracle\` is pinned to, and the change touches the trees that job consumes.\n\n"
            printf '| | |\n|---|---|\n'
            printf "| Pinned (\`SPEC_REV\` in \`ci.yml\`) | \`%s\` |\n" "$PINNED"
            printf "| Spec default branch | \`%s\` |\n\n" "$head_sha"
            printf "### Differences in the mirrored trees\n\n\`\`\`\n"
            cat "$actionable"
            printf "\`\`\`\n\n"
            if [ -s "$suppressed" ]; then
              suppressed_count=$(wc -l < "$suppressed" | tr -d ' ')
              printf "%s additional difference(s) were suppressed as not actionable by any oracle gate -- see the step summary on this run for detail.\n\n" "$suppressed_count"
            fi
            printf "### Commits since the pin\n\n"
            git -C spec-head log --oneline "$PINNED..$head_sha" \
              -- schemas/conformance schemas/json/policy schemas/parity 2>/dev/null | sed 's/^/- /' \
              || printf -- '- (could not enumerate; the pinned commit may not be an ancestor)\n'
            printf '\n'
          } > "$body"
          cat >> "$body" <<'MARKDOWN'
          ### What to do

          1. Vendor any changed or added fixture from `schemas/conformance/` into `tests/conformance/` **byte-identically** (never hand-edit them), and register new ones with `conformance_test!`.
          2. If `schemas/json/policy/**` changed, re-check the hand-written mirrors in `crates/macp-policy/src/registry.rs` (`validate_conditional_constraints`) and the `enum_lists_match_the_canonical_schemas` parity test.
          3. If `schemas/parity/contract.json` changed, re-vendor it byte-identically into `tests/parity/contract.json` (see `tests/parity/SOURCE.md`), re-run `cargo test --test parity_contract`, and re-run its prove-then-restore checks.
          4. Implement any new normative behaviour, then bump `SPEC_REV` in `.github/workflows/ci.yml` in the same PR.

          Bumping `SPEC_REV` without doing 1, 2, and 3 turns `conformance-oracle` red on every PR, which is the failure mode the pin exists to prevent. This issue refreshes itself daily and closes itself once the pin catches up.
          MARKDOWN

          {
            echo 'body<<REPORT_EOF'
            cat "$body"
            echo REPORT_EOF
          } >> "$GITHUB_OUTPUT"
        env:
          PINNED: ${{ steps.pin.outputs.rev }}

      - name: File or update the drift issue
        if: steps.diff.outputs.drift == 'yes'
        env:
          GH_TOKEN: ${{ github.token }}
          BODY: ${{ steps.diff.outputs.body }}
        run: |
          set -euo pipefail
          title="Spec drift: conformance-oracle pin is behind ${{ steps.diff.outputs.head_sha }}"
          # One issue at a time, refreshed in place, so a week of drift does not
          # produce seven issues.
          existing=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
                       --label spec-drift --limit 1 --json number -q '.[0].number // empty')
          if [ -n "$existing" ]; then
            gh issue edit "$existing" --repo "$GITHUB_REPOSITORY" --title "$title" --body "$BODY"
            echo "::notice::refreshed existing drift issue #$existing"
          else
            gh label create spec-drift --repo "$GITHUB_REPOSITORY" \
              --description "The conformance-oracle spec pin is behind upstream" \
              --color D4C5F9 2>/dev/null || true
            num=$(gh issue create --repo "$GITHUB_REPOSITORY" --title "$title" \
                    --body "$BODY" --label spec-drift | grep -oE '[0-9]+$')
            echo "::notice::filed drift issue #$num"
          fi

      - name: Close a stale drift issue once the pin catches up
        if: steps.diff.outputs.drift != 'yes'
        env:
          GH_TOKEN: ${{ github.token }}
          CLOSE_COMMENT: ${{ steps.diff.outputs.close_comment }}
        run: |
          set -euo pipefail
          existing=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
                       --label spec-drift --limit 1 --json number -q '.[0].number // empty')
          if [ -n "$existing" ]; then
            gh issue close "$existing" --repo "$GITHUB_REPOSITORY" \
              --comment "$CLOSE_COMMENT"
            echo "::notice::closed drift issue #$existing"
          else
            echo "no open drift issue; nothing to close"
          fi

      - name: Summary
        run: |
          {
            echo "### Spec drift watch"
            echo
            echo "- pinned: \`${{ steps.pin.outputs.rev }}\`"
            echo "- spec head: \`${{ steps.diff.outputs.head_sha }}\`"
            echo "- verdict: \`${{ steps.diff.outputs.drift }}\`"
          } >> "$GITHUB_STEP_SUMMARY"