Skip to content

content(blog): how do you catch an AI's silent permission change before it ships? the access-matrix gate (slot 2026-10-23) - #282

Open
objectstack-fleet[bot] wants to merge 2 commits into
mainfrom
claude/pm-dispatch-objectos-ju9td1-issue-270
Open

objectstack-fleet[bot] wants to merge 2 commits into
mainfrom
claude/pm-dispatch-objectos-ju9td1-issue-270

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #270

Slot: Fri 2026-10-23, on the shared calendar (#267). Merge on the slot day only.

Seat review. The seat accepted the post as written. It also ruled option A on the review post, a minimal correction of a sentence that was false on a published page. os build was never green on the CRM example's own diff: the access-matrix gate has refused it since 14.7.0. The post's rule counts are untouched; they are the subject of the separate pending decision #208. This PR and PR #281 both set the review post's updated. At merge time the later slot wins. ACCEPT.

Post for slot Fri 2026-10-23 (slate 1 of the daily cadence, #267; the seat moved it from Sat 2026-10-24 mid-task, daily publishing starting the day after this report). Staged on lin/issue-270 from origin/main 3c5b6c6, two commits: 7a18f59 (the post) and ca29ec3 (the en-only correction of the review post, below), tip ca29ec3, worktree /home/user/www-issue-270. Not pushed by the dev; the seat pushes and merges on the slot day only.

What this PR adds

New post content/blog/catch-ai-silent-permission-change-access-matrix-gate/ in en, zh-Hans, zh-Hant (generated by pnpm gen:zh-hant), ja, ko, de, es and fr. date: 2026-10-23, no updated, topic: governance, audience: developer. Title as the card's working title: How Do You Catch an AI's Silent Permission Change Before It Ships? The Access-Matrix Snapshot Gate, on a 7-Entry CRM.

  • Ownable concept: effective capability, not raw flags. The number: the CRM's committed matrix has seven entries, and seven rows are every grant the app declares. The proof is one edit, one drift line, one changed JSON line.
  • Proof artifact (rule 5), real and reproducible: examples/app-crm at the tag @objectstack/cli@17.7.0, installed against the published 17.7.0 packages (not the monorepo), npx os --version = @objectstack/cli/17.7.0 linux-x64 node-v22.22.2. The allowDelete: false -> true flip on crm_opportunity passes npx os validate (49 author-time rules, Validation passed (684ms), exit 0, zero mentions of allowDelete, crm_sales_user or a matrix) and fails npx os compile with ✗ Access matrix drift (1 change) — capability changes must be reviewed / • 'crm_sales_user' gains delete on 'crm_opportunity', exit 1. npx os build fails identically (Build extends Compile). --json gives {"success":false,"error":"access matrix drift","changes":["'crm_sales_user' gains delete on 'crm_opportunity'"]}. --update-access-matrix rewrites the snapshot; git diff -U8 access-matrix.json is the one-line "delete": false -> true hunk; the next compile passes.
  • Second measurement backing the concept: modifyAllRecords: true added with allowDelete: false left in place yields Access matrix drift (2 changes): gains delete and gains Modify All Data on crm_opportunity; validate still passes (575ms).
  • Honesty (rule 7): a five-limit section (opt-in and off by default, including that the 17.7.0 scaffold template ships no access-matrix.json; compile/build only, never validate or lint; visible-not-impossible, an agent can regenerate the snapshot itself; scope is permission set x object; the steelman that a one-word flip is visible in the source diff anyway) plus the practical note that the flag rewrites the whole file and that the default three-line diff context cuts the row's permissionSet/object lines off.
  • Framing per the card's risk line: "one gate, opted in"; the review post's "No rule fires" sentence is quoted as still true of os validate; never says validate catches it.
  • Positioning: T4 (T4_STANDALONE) verbatim once per locale from src/lib/positioning.ts. E1 is not used (it names editions; the house bar says no edition naming while [Decision] Pricing headline: www shows $20 / $45 per AI seat "billed annually (about two months free)", docs License & Pricing shows $24 / $54 per month with no annual price — which is the source, and is the annual discount real? #214/[Decision] Where does "Get started" go? Neither site has an ObjectOS Cloud signup URL, so every plan CTA loops www → docs → www #215 are pending). No pricing, no signup URL, no token figures, no competitor claims.
  • CTA (rule 8): point your agent at the target (rules file or spec/MCP), npm create objectstack@latest, then npx os compile --update-access-matrix as the first commit and npx os compile in CI.
  • Internal links: how-to-review-ai-generated-app-changes (en only, so every locale links /en/), objectos-agent-permission-boundaries (zh-Hans/zh-Hant link their own locale), glossary reviewable-diff and declared-vs-enforced (zh links own locale, others /en/), how-to-point-your-agent-at-objectstack.
  • Assets: cover-en.svg / cover.svg (1200x675), two-commands-one-gate-en.svg / .svg (1200x640, the validate-vs-compile mechanism and the human loop), effective-not-raw-en.svg / .svg (1200x560, the fold and the two-line measurement). The zh pair uses the Chinese set, every other locale the English set. Each diagram adds what the cover lacks.

One published post is edited, on the seat's ruling (option A): see "Correction to a published post" below.

Claims and their sources

Claim in the post Source (objectstack at tag @objectstack/cli@17.7.0 = 4e4e881, unless noted)
Gate description, quoted whole: "AI drafts permission metadata on this platform; the one thing it must never do is silently change who can do what... any drift fails the build with a semantic explanation until a human regenerates the snapshot." content/docs/permissions/access-matrix.mdx:7-12 (identical at origin/main, diff empty)
Opt-in: access-matrix.json next to objectstack.config.ts; os compile --update-access-matrix; "No access-matrix.json → the gate is skipped (not opted in). Both examples/app-crm and examples/app-showcase are opted in." access-matrix.mdx:16-24; both files present at the tag (git cat-file -e)
"its diff IS the review artifact"; diff lines are semantic access-matrix.mdx:36, 39-43; printed verbatim by the run (proof/04-compile-modified.log)
Snapshot format, "the matrix reports effective capability, not the raw flags", each row carries the OWD access-matrix.mdx:45-72
Posture linter vs matrix gate vs explain engine access-matrix.mdx:74-85; content/docs/permissions/explain.mdx:106-108 ("the same evaluation run over representative (permission set × object) pairs; explain is the per-decision zoom lens")
"Not a registry rule: it reads (and with the flag, writes) a file next to the config rather than answering a question about the stack"; single read, missing file = not opted in; unconditional write packages/cli/src/commands/compile.ts:849-895
Build extends Compile packages/cli/src/commands/build.ts:3-6; measured proof/09-build-modified.log exit 1
The five drift-line shapes; "gains access to" is the new-entry form, "gains delete on" the bit-flip form; OWD line "(affects every principal)" packages/lint/src/build-access-matrix.ts:104-129
CRUD bits are the spec's fold; the two super-user columns are raw bits kept as context; sorted entries build-access-matrix.ts:47-68, 77-81
The fold: read = allowRead or viewAll or modifyAll; edit/delete/transfer = bit or modifyAll; create has no bypass; export = allowExport and read; "the same fold the runtime's own answer is built from" packages/spec/src/security/permission.zod.ts:179-224
"pointed the dangerous way round: a predicate hiding an action from the one administrator who holds the power to use it" permission.zod.ts:185-186
Seven entries, every crm_sales_user row "delete": false, guest_portal insert-only, OWD values examples/app-crm/access-matrix.json (82 lines, 7 entries)
The edit is line 45; crm_opportunity row before/after examples/app-crm/src/security/sales-positions.ts:39-57 (the card cites :50, which is origin/main numbering)
A fresh scaffold is not opted in git ls-tree -r @objectstack/cli@17.7.0 -- packages/create-objectstack has no access-matrix.json; the #230 writer's real 17.7.0 scaffold has none
"No rule fires. Both values are valid; both are enforceable" www content/blog/how-to-review-ai-generated-app-changes/index.mdx:112; measured true of validate (proof/03-validate-modified.log, 0 matches for allowDelete/crm_sales_user/access-matrix)
Every number and line in a text/json/diff code block the run's own output: scratchpad/issue-270/proof/00-14* (trimmed, never rewritten)
T4 src/lib/positioning.ts (T4_STANDALONE and each locale's t4)

The run, in order

Scratchpad issue-270/app/crm: git archive @objectstack/cli@17.7.0 examples/app-crm, workspace:* deps rewritten to 17.7.0 (runtime, service-i18n, spec, cli, driver-sql, formula, objectql), the monorepo-only paths rule and vitest removed, pnpm install (registry reachable), committed as 0b5e971 so git diff works.

  1. npx os compile baseline: → Checking access-matrix snapshot (ADR-0090 D6)... then ✓ Build complete (667ms), exit 0 (gate present, auto-pass).
  2. Line 45 allowDelete: false -> true. npx os validate: Running author-time rules (49), ✓ Validation passed (684ms), exit 0.
  3. npx os compile: ✗ Access matrix drift (1 change) / • 'crm_sales_user' gains delete on 'crm_opportunity' / If intended, re-run with --update-access-matrix..., exit 1. --json: success:false, error:"access matrix drift", one changes entry.
  4. npx os compile --update-access-matrix: Access matrix snapshot written to access-matrix.json (ADR-0090 D6) — review the diff., exit 0. git diff access-matrix.json: the one hunk at @@ -51,7 +51,7 @@; -U8 shows the row with its permissionSet/object keys. npx os compile again: ✓ Build complete (508ms).
  5. Snapshot restored, npx os build on the flipped source: same drift line, exit 1.
  6. Everything restored; modifyAllRecords: true added on the same row: npx os compile (2 changes) gains delete + gains Modify All Data, exit 1; npx os validate ✓ Validation passed (575ms). Restored (git diff HEAD empty).

Measured vs the card

  • The card's predicted drift line, 'crm_sales_user' gains access to 'crm_opportunity' (delete), is the new-entry form (build-access-matrix.ts:113). A bit flip on an existing row prints 'crm_sales_user' gains delete on 'crm_opportunity' (:118). The post quotes the measured line; the cover and both diagrams carry it verbatim.
  • Everything else the card's sources say holds on 17.7.0: compile-only, opt-in, semantic lines, effective bits, OWD per row. No needs_decision on the topic.

Gates, on ca29ec3 (scripts/pm/os-verify-lock.sh, slot www-270, VERDICT command-exit 0, held 84 s, waited 5 s, shared-box seconds; logs scratchpad/issue-270/gates5/*.log with .exit files; the identical chain was also green on 7a18f59 (gates4, the post alone) and on the earlier 2026-10-24-dated tip 4ebe660 (gates3))

Gate Exit Verdict line
pnpm content:lint 0 ✓ content lint passed (355 files, 46 glossary terms checked)
pnpm content:lint --published 0 same line
pnpm check 0 Result (160 files): 0 errors, 0 warnings, 0 hints
pnpm build 0 ✓ zh-Hant: generated 48, kept 0 hand-maintained, 8 with body links pointed at /zh-Hant/ · svg-text-fit: OK (124 SVGs, 2671 text boxes, 185 filtered elements, tolerance 2px, 1 allowlisted) · [build] 920 page(s) built in 48.81s · ✓ content lint --dist passed (494 built blog pages checked) · CJK line breaks, quotation marks (12811 curly marks, 0 wrong) and links (99459) passed · git status clean afterwards
pnpm seo:smoke 0 SEO smoke test passed (910 HTML pages checked; 594 sitemap URLs against 316 noindexed pages; 5679 canonical/hreflang annotations; 17397 prose blocks language-checked; 99449 internal links redirect-checked)
pnpm svg:lint (the six new files) 0 svg-text-fit: OK (6 SVGs, 198 text boxes, 2 filtered elements, tolerance 2px, 0 allowlisted); tightest box 6.4 px

Advisories for this post, all expected: Date is in the future: Fri Oct 23 2026 (x8); Title is N columns wide ... search results show about 60 (en 116, zh 86, ja 112, ko 108, de 160, es 175, fr 183: the ruled title); Description is N columns wide ... about 160 (en 228, zh 193, ja 245, ko 241, de 226, es 227, fr 229; every description is at or under the 230-character ceiling, after shortening de/es/fr). For the review post: Updated is in the future: 2026-10-23.

An earlier run on the same content (gates1) failed seo:smoke with 7 issues: the English doc quote rendered under lang="ja" etc. was judged not in its declared language. Fixed by wrapping that one blockquote in the seven translations in a blockquote carrying lang="en" (the smoke test's own remedy); the gate chain was re-run green.

Browser check (final build, scratchpad/issue-270/shots.mjs, results scratchpad/www/shots-270/results.json, 110 files)

dist/ of 7a18f59 served by astro preview on 127.0.0.1:49123, started and stopped by PID (confirmed gone, port closed). Playwright at /opt/node22/lib/node_modules/playwright, executablePath /opt/pw-browsers/chromium, no install.

  • The article in en and zh-Hans and the blog list in en and zh-Hans at 1366x625, 1440x750, 1512x830, 1920x945, 390x844 and 360x800, light and dark (colorScheme plus data-theme), lazy images scrolled into view first, fold and full-page shots: 48 of 48 pages HTTP 200, scrollWidth == clientWidth everywhere, no element past the viewport outside a scroll container, cover plus two body images on every article load with natural width, no repeated image, no page errors, both lists link the post (it is the first chronological card, ahead of the 2026-10-15 post), no time.updated rendered, JSON-LD datePublished == dateModified == 2026-10-23, rendered datetime="2026-10-23".
  • Six served SVGs opened as their own documents at native viewBox size in light and dark: content-type image/svg+xml, every text node's computed fill against the fill under it, lowest 4.59:1 (amber caps on cream), covers 5.06:1, 0 text nodes on a dark fill, 0 without a fill. The first run measured 4.13-4.43:1 for the small grey (#657872, the house value) on the cream and blush fills; the grey was darkened to #4b5d56 (6.17-7.00:1 on every fill used) before the final commit.
  • Looked at: post-en-1440x750-light-fold, post-en-1920x945-light-fold, post-en-390x844-dark-fold, post-en-1440x750-dark-full, post-zh-Hans-1440x750-dark-fold, list-en-1440x750-light-fold, list-zh-Hans-360x800-dark-fold, and the native renders of cover-en (light), cover (dark), both two-commands-one-gate (light), both effective-not-raw (light).

Correction to a published post (seat ruling: option A, 2026-10-17)

content/blog/how-to-review-ai-generated-app-changes/index.mdx, en only (the post has no other locale), commit ca29ec3, 3 lines changed. date unchanged; updated: 2026-10-15T10:00:00+08:00 becomes updated: 2026-10-23T10:00:00+08:00, the day the link goes live. The rule counts ("42 rules", "29 of them can emit error") are deliberately untouched: they wait on the separate version-labelling decision (#208).

Line 82, before:

Thirty-odd lines. Every key in it is real, every key parses, and os build is green. It carries three defects and inherits a fourth — ...

Line 82, after:

Thirty-odd lines. Every key in it is real, every key parses, and os validate is green (on the CRM example, os build is not: its committed access-matrix snapshot refuses the delete grant, as measured in the access-matrix snapshot gate post). It carries three defects and inherits a fourth — ...

Line 112 (Pass 2), before:

allowDelete: false → true. No rule fires. Both values are valid; both are enforceable; the runtime will enforce whichever one you merge.

Line 112, after:

allowDelete: false → true. No rule fires. One gate that is not a rule does fire when the app opts in, the committed access-matrix snapshot, and it fails os compile with a line naming who gains what. Both values are valid; both are enforceable; the runtime will enforce whichever one you merge.

Evidence that the old sentence was wrong (scratchpad/issue-270/proof/09-build-modified.log): the review post's own diff, applied to examples/app-crm at the tag @objectstack/cli@17.7.0 with the committed access-matrix.json in place, run with the published CLI:

$ npx os build
  → Checking access-matrix snapshot (ADR-0090 D6)...

  ✗ Access matrix drift (1 change) — capability changes must be reviewed
  • 'crm_sales_user' gains delete on 'crm_opportunity'
exit=1

and npx os validate on the same tree: ✓ Validation passed (684ms), exit 0 (proof/03-validate-modified.log). The claim was also false when published: examples/app-crm/access-matrix.json exists from at least @objectstack/cli@14.7.0 (2026-07-13) and the drift check is in compile.ts at 14.7.0, 17.2.0 and 17.3.0 (2026-09-04, the last tag before the post's 2026-09-08 date); the sentence entered in 9ca8238 (#153). "No rule fires" was and is true: validate prints no finding, and the snapshot gate is not a registry rule.

Browser check of the corrected post (scratchpad/issue-270/shots-review.mjs, dist/ of ca29ec3 on 127.0.0.1:49124, stopped by PID, port closed; shots scratchpad/www/shots-270/review-*): at 1440x750 and 390x844, light and dark, HTTP 200, time.updated renders "Updated Oct 23, 2026" with datetime="2026-10-23", both links to the new post render with non-zero boxes (link colour rgb(61, 88, 240) light / rgb(145, 163, 255) dark), no horizontal overflow, JSON-LD datePublished 2026-09-08T02:00:00.000Z, dateModified 2026-10-23T02:00:00.000Z. Looked at review-1440x750-light-fold, review-1440x750-dark-correction and review-390x844-light-correction.

Merge note from the seat: PR #281 (#271, slot 10-24) also back-links this post and sets its updated to 10-24; the seat resolves that conflict at merge time, later date wins.

Deviations

  • Drift-line wording differs from the card's prediction (see "Measured vs the card"); the real line is used everywhere.
  • The card cites sales-positions.ts:50; at the tag the line is 45 and the post says 45.
  • A second measurement (Modify All) was added beyond the card's proof artifact because it is the direct evidence for "effective capability, not raw flags".
  • The English doc quote in the seven translations is wrapped in a blockquote with lang="en" (first use of that pattern in content/blog), with the translation in parentheses after it; without it seo:smoke fails.
  • SVG small-text grey is #4b5d56 rather than the house #657872, after the native-size check measured the house value at 4.13-4.43:1 on the cream/blush fills used here.
  • E1 is not quoted (edition naming); T4 only.
  • de/es/fr descriptions were shortened to 226/227/229 characters to stay under the 230 ceiling.
  • The commit carries the repo's model-free trailer pair as instructed; the harness's model-named trailer was not used.
  • Slot moved from 2026-10-24 to 2026-10-23 by the seat mid-task: date changed in all seven sources, zh-Hant regenerated, commit amended, every gate and the browser check re-run on the new tip. The only updated stamped is the review post's, 2026-10-23, per the seat's option-A ruling.
  • GitHub reads: one gh api read of the card. No MCP calls, no REST writes, nothing posted.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr

hotlong and others added 2 commits October 17, 2026 02:25
… access-matrix snapshot gate on a 7-entry CRM

New post for the Sat 2026-10-24 slot, in en, zh-Hans, zh-Hant (generated by
pnpm gen:zh-hant), ja, ko, de, es and fr. date: 2026-10-24, topic governance,
audience developer. Every transcript was produced on the published
@objectstack/cli 17.7.0 against examples/app-crm at the tag
@objectstack/cli@17.7.0, installed against the published packages: the
allowDelete false -> true flip on crm_opportunity passes os validate with no
finding (49 author-time rules, exit 0) and fails os compile and os build with
"Access matrix drift (1 change)" and the line "'crm_sales_user' gains delete on
'crm_opportunity'" (exit 1); --update-access-matrix rewrites the snapshot and
the git diff is the one changed line. A second measurement, modifyAllRecords
true with allowDelete left false, yields two drift lines, which is the post's
"effective capability, not raw flags" concept.

Assets: cover-en.svg / cover.svg, two-commands-one-gate-en.svg / .svg,
effective-not-raw-en.svg / .svg; the zh pair uses the Chinese set, every other
locale the English set. No published post is edited; the review post's
"No rule fires" sentence stays as it is.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…he CRM diff; cross-link the access-matrix gate post

An en-only correction of how-to-review-ai-generated-app-changes, measured on
the published @objectstack/cli 17.7.0 against examples/app-crm at the tag:
the worked diff's allowDelete false -> true flip passes os validate (exit 0,
no finding) but fails os build and os compile with "Access matrix drift
(1 change)" / "'crm_sales_user' gains delete on 'crm_opportunity'" (exit 1),
because the CRM example ships a committed access-matrix.json. The post said
"os build is green"; it now says os validate is green, with a parenthetical
on the CRM example, and Pass 2 gains one sentence after "No rule fires."
naming the one gate that is not a rule, both linking the new post.

updated: 2026-10-23T10:00:00+08:00 (the day the link goes live); date is
unchanged. The rule counts (42 / 29) are deliberately untouched: they wait
on the separate version-labelling decision.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Post (slot Fri 2026-10-23): How Do You Catch an AI's Silent Permission Change Before It Ships? The Access-Matrix Snapshot Gate, on a 7-Entry CRM

1 participant