Repository navigation
content(blog): can you prove who opened a customer record, including an AI agent? record-view auditing (slot 2026-10-26) - #284
Open
objectstack-fleet[bot] wants to merge 2 commits into
Conversation
…an AI agent? record-view auditing on 17.7.0 (slot 2026-10-26)
New post in all eight locales (zh-Hant generated by pnpm gen:zh-hant) with an
English-text cover and diagram plus the zh pair. Proof: examples/app-crm at tag
@objectstack/cli@17.7.0 on the published 17.7.0 packages with
new AuditPlugin({ readAudit: { objects: ['crm_contact'] } }) in plugins; five
reads (REST detail by a person, MCP get_record by an API-key agent, a list, an
unlisted object, a refused read) left exactly two read rows in sys_audit_log.
Back-links with updated: 2026-10-26 on eu-ai-act-runtime-audit (8 locales),
when-ai-agent-deletes-production-database (8) and
objectos-agent-permission-boundaries (en, zh-Hans, zh-Hant regenerated).
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…d clear the diagram stop lines Covers (en, zh): ledger row bands widened to x=14..642, columns at 28/150/212/392/512 with ids shortened to four characters, so every column gap is at least 20px and every value sits at least 14px inside its band; the two footnote lines moved up 4-6px for 14px bottom clearance. Diagrams (en, zh): the three WHERE C, D AND E STOP lines shortened to keep 50px or more of clearance to the card edge; the two ledger row cards grown from 110px to 116px so the last line keeps 14px below it. Clearance scan over the gate's own --verbose widths: 0 findings (16 single-glyph badge chips exempt by rule); svg:lint OK on all four; re-rendered at native size on light and dark and looked at. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #274
Slot: Mon 2026-10-26, on the shared calendar on #267. Merge on the slot day only. Seat review. The seat reviewed the post and its diagrams at native size. Cover-column and stop-line tightness was fixed in
27d6efb, after which a clearance scan reports 0 findings and the seat re-viewed the cover. The edges the card claimed all hold on 17.7.0, and the MCPget_recordpath is measured on HTTP. ACCEPT.Slot Mon 2026-10-26 (
date: 2026-10-26, moved from the card's 10-28 by the seat's calendar change during the run). Merge on the slot day only; if the slot moves again, re-date the post and move the three back-linkedupdatedvalues with it.What this adds
content/blog/prove-who-opened-a-customer-record-view-auditing/— the post in all eight locales (index.mdx,zh-Hans,ja,ko,de,es,frauthored;zh-Hantproduced bypnpm gen:zh-hant), cover pair (cover-en.svg,cover.svg) and one diagram pair (which-reads-land-en.svg,which-reads-land.svg). The diagram adds what the cover lacks: the four-condition rule and where reads C, D and E stop.updated: 2026-10-26, one closing sentence each:eu-ai-act-runtime-audit(all eight locales; its existingupdatedmoved from 2026-10-15),when-ai-agent-deletes-production-database(all eight),objectos-agent-permission-boundaries(en, zh-Hans; zh-Hant regenerated). Links to the glossary termaudit-trailfrom every locale (/en/or/zh-Hans/form where the locale has no glossary; the cross-locale rule incontent-lintaccepts that).it, topicgovernance. Canonical T4 closes every locale verbatim fromsrc/lib/positioning.ts(standalone form).Proof artifact — produced on the published
@objectstack/cli17.7.0examples/app-crmexported from tag@objectstack/cli@17.7.0(commit4e4e881427), every@objectstack/*dependency pinned to the published 17.7.0 packages (cli,runtime,spec,service-i18n,driver-sql,formula,objectql, plus@objectstack/plugin-audit17.7.0 added as a dependency, since pnpm's strict layout does not expose the CLI's transitive copy to the app),plugins: [new AuditPlugin({ readAudit: { objects: ['crm_contact'] } })]added toobjectstack.config.ts, booted withobjectstack dev --compile --no-watch --port 49200. Logs and raw responses:/tmp/claude-0/-home-user/2224823d-3484-535e-94a5-c071ba6af395/scratchpad/issue-274/proof/(server1.log,transcript.log,readA.json,readB.json,ledger.json,where.log,phase.sh,where.mjs).Boot:
Runtime: 1 plugins;Plugins: 37 loadedincludingAudit, com.objectstack.audit; the warningPlugin superseded: 'com.objectstack.audit' — the later registration (v1.0.0) REPLACED the earlier one (v1.0.0)…(server1.log:98);objectstack.telemetry.dbcreated besideobjectstack.db.Actors (admin API):
dana@example.comandagent-svc@example.comeach given asys_user_positionrow withposition: 'sales_rep';nobody@example.comwith none. API keyosk_xtVIackn…minted byagent-svcviaPOST /api/v1/keys.sys_audit_log?GET /api/v1/data/crm_contact/h7UV6RJfS5ykgOH-user_id PdP4MCXuWr4khHoyN1R4uw7U8G4LtsCv,created_at 02:33:05.512Zx-api-key)tools/call get_record { objectName: "crm_contact", recordId: "HwvcWKZh3n1TM55l" }on/api/v1/mcpuser_id S8My5629W2MZy813Pcx0Hgpyq1DiNk5i,created_at 02:33:05.618ZGET /api/v1/data/crm_contactGET /api/v1/data/crm_account/eSLGuoysxgUk5mvOGET /api/v1/data/crm_contact/h7UV6RJfS5ykgOH-PERMISSION_DENIEDAccess denied: operation 'findOne' on object 'crm_contact' is not permitted for positions [org_member, everyone]Ledger before:
{"records":[],"total":0}. Five seconds after the reads, as admin,GET /api/v1/data/sys_audit_log?filter={"action":"read"}→"total": 2; both rows:action: read,old_value: null,new_value: null,ip_address: null,user_agent: null,actor=user_id,organization_id/tenant_id=org_mvbs6g8ek7hnfdcf(the viewer's session org; the seeded contact's ownorganization_idis null),updated_at2.0 s aftercreated_at(the batch drain). The same query as Dana → 403PERMISSION_DENIEDonsys_audit_log.better-sqlite3from the app's own store:objectstack.db → no such table: sys_audit_log;objectstack.telemetry.db → create 40, login 5, read 2, update 4.Card inference confirmed by measurement: the card said MCP
get_recordshould qualify (inferred from the predicate rule). It does on the HTTP transport (row B), because/api/v1/mcp→buildMcpBridge.get→callData('get')→protocol.getData→engine.findOne(object, { where: { id } }). The card's edges all held: list never logged, no field values, no IP, no refused reads.Sources (tag
@objectstack/cli@17.7.0unless noted; line numbers at the tag)content/docs/permissions/record-view-auditing.mdx—:7-9"answers the question that comes after it … who actually opened it, and when?";:12-13"off until a deployment names the objects it wants recorded … its scope has real edges";:45-51the threereadAuditkeys, defaults 50 / 2000, "Empty registers no hook at all";:63-68"There is no object-metadata key and no environment variable.enable.auditReadsdoes not exist … a declaration a reviewer mistakes for coverage is worse than an absent feature";:74-114underos serve, auto-registration "before it walksplugins", thePlugin supersededblock and "is the opt-in working, not a misconfiguration";:118-127"There is no stack-config key either." and "requires: ['audit']is not a substitute — it … constructs the plugin with no options and offers nowhere to name objects";:132-147the two conditions ("It materialized exactly one record", "Its predicate pinned the primary key",findOne(object, { where: { id } }),$or/$notdisqualify, depth 8);:150-153"List and search reads are never recorded — including a list read that happened to return exactly one record … 'record-detail views', not 'reads'";:156-172the column table ("The instant the record was viewed, not the instant its batch drained"; "falling back touser_id"; "The viewed record's own organization, falling back to the viewer's session organization"; "Alwaysnull") and the org-B wall reason;:174-188the four non-records ("No field values, ever." … "ahead of its field masking"; "Not what the viewer actually saw."; "No IP address and no user agent."; "Nothing about refused or failed reads.");:190-199the two boundaries (session.isSystem, "sudo()keeps the caller's user id", "a row naming nobody only adds noise");:201-222Record Views under Diagnostics, append-only,view_all_audit_log, "retained hot for 90 days, then archived for seven years";:224-255failure posture ("enqueues and returns", 10,000 ceiling, "once per cause", "Both failures are invisible from everywhere else … confident, wrong, short answer", telemetry "sibling SQLite file");:268canonical source. Diff tag →origin/main(570c7a2d95): only thegetLocaleparagraph added and the "only key" sentence reworded (main:45-46,:61-64); every quoted sentence is identical on both.content/docs/kernel/runtime-services/audit-service.mdx:11-22— the three ledger surfaces; "Record views do not travel through it either …afterFindhook, under a per-object opt-in given at construction".content/docs/api/data-api.mdx:21— thefilterquery parameter (JSON).content/docs/ai/connect-mcp.mdx:97-134— API keys,POST /api/v1/keys, thex-api-keyheader;:153-167— "for a full-authority local agent, mint the key on a platform-admin or dedicated service identity; for a scoped one, mint it on a user with exactly the access it should have";:288-305the shellinitialize/tools/list/tools/callform used.content/docs/permissions/authentication.mdx:931-946—POST /api/v1/auth/admin/create-userbody;content/docs/permissions/administrator-guide.mdx:100-108—sys_user_position= user + position;packages/plugins/plugin-security/src/objects/sys-user-position.object.ts:78-84—user_id,position.packages/plugins/plugin-audit/src/read-audit.ts—:347-359extractDetailReadId(array/null never a view);:364-389findIdPin($or/$not→ null,$andwalked, depth 8);:683if (session.isSystem === true) return;;:690if (!userId && !actor) return;;:647-648old_value: null, new_value: null;:657-658actor = event.actor ?? event.userId;:727eng.registerHook('afterFind', auditRead, { object: [...auditedObjects], packageId });:577-583the "no such table … DIFFERENT datasource …os devprovisions one by default as a SIBLING SQLite file" message.packages/plugins/plugin-audit/src/audit-plugin.ts:234-250—readAudit.objectswiring;:247ctx.logger.info('AuditPlugin: record-view auditing installed on N object(s) …')(why the defaultwarnboot shows only the superseded line;packages/cli/src/commands/dev.ts:228"defaultwarn").packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts:94-109— therecord_viewsview (columns,action in ['read'], empty state "Record-view auditing is opt-in per object");:238actor,:317old_value,:325new_value,:334ip_address,:342user_agent,:350tenant_id.packages/cli/src/commands/serve.ts:4314-4323— "Registered with NO options … put its ownnew AuditPlugin({ readAudit: … })in the stack'spluginsarray, which this file registers further down — AFTER this line";:4390-4392await kernel.use(new AuditPlugin());:4433-4440the supersedingkernel.use(pluginToLoad)in thepluginsloop;packages/core/src/plugin-registration.ts:97thePlugin superseded:text.packages/mcp/src/mcp-http-tools.ts:883-907— theget_recordtool (bridge.get(objectName, recordId));packages/runtime/src/domains/mcp.ts:609-611— HTTP bridgeget→callData('get', { object, id });packages/runtime/src/action-execution.ts:240-242—protocol.getData(...);packages/metadata-protocol/src/protocol.ts:13243-13297—getDatabuildswhere: { id: request.id }and callsthis.engine.findOne(:13283).packages/rest/src/rest-server.ts:8964—GET /data/:object/:id→p.getData(getRequest).examples/app-crm/src/security/sales-positions.ts:44—crm_contact: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false };examples/app-crm/src/data/index.ts:25-27— the three seeded contacts.content/docs/operate/audit-logs.mdx:25-28atorigin/main76988dad4— "The object is read-only and immutable" (linked ashttps://docs.objectos.ai/docs/operate/audit-logs, the form published posts use).Plugin superseded,Access denied, ledger rows and SQLite outputs are the 17.7.0 process's own output (proof/server1.log,proof/transcript.log,proof/where.log).Gates
pnpm content:lintdate/updatedin the future (slot post) —clint2.logpnpm content:lint --publishedclint-pub.logpnpm svg:lint(the four new SVGs)svglint-en3.log,svglint-zh1.log); withinpnpm build: 122 SVGs, 2691 boxes, OKpnpm check(underos-verify-lock.sh)VERDICT command-exit 0; 0 errors, 0 warnings, 0 hints (160 files) —check.logpnpm build(under the lock; second run, on the committed content)VERDICT command-exit 0; 920 pages in 48.97 s;--distprose (494 pages), CJK breaks, quotes (13149 marks), links (99460) all pass; tree clean afterwards (git statusempty) —build2.logpnpm seo:smokeseo.logpnpm gen:zh-hant/zh-Hant/; one locative warning (账本里) resolved by rewording the zh-Hans source to账本中—gen3.logFirst
pnpm buildfailed oncontent-lint --dist: ja, zh-Hans and zh-Hant rendered 10 literal**each, a bold span ending。**directly followed by a letter is not right-flanking in CommonMark. Moved the full stop outside eight such spans in zh-Hans and ja, regenerated zh-Hant, rebuilt green (build.log→build2.log).Browser check (looked at, not only asserted)
astro previewon 127.0.0.1:49205; Playwright with the preinstalled Chromium. en and zh-Hans article plus/en/blog/at 1366×625, 1440×750, 1512×830, 1920×945, 390×844 and 360×800, light and dark, lazy images scrolled into view first: 210/210 assertions pass (HTTP 200, no horizontal overflow, no element past the viewport outside a scroll container, no repeated image, cover rendered, every body image loaded) —shots.log,results.json. Viewport crops at the hero, the opening ledger table, the diagram, the column table and the closing section were taken for six locale/viewport/scheme combinations and inspected (hero 1440 light, 1920 dark, zh-Hans 1366 light; ledger table 390 dark and 1440 light; diagram 390 dark and 1440 light; zh-Hans column table 360 dark; closing 1440 light). The blog list was scrolled to the new card at 1366 light, 390 dark, 1920 dark and zh-Hans 1440 light: first position, cover,Oct 26, 2026/2026年10月26日, tags; at 390 dark the lazy cover had not loaded at first measurement and was re-measured after load (natural width 267). The six back-linked pages checked (en × 3, zh-Hans, ja, zh-Hant) all renderUpdated Oct 26, 2026(datetime="2026-10-26") and the new link resolves with 200. Every new SVG was rendered at native size (1200×675, 1200×720) on light and dark; one defect found and fixed before commit (the English cover'sold / newandipcolumns collided — "null · nulbull"; columns re-spaced, gate re-run, re-rendered and looked at). Screenshots:/tmp/claude-0/-home-user/2224823d-3484-535e-94a5-c071ba6af395/scratchpad/www/shots-274/(svg/,crops/,listcard-*,backlink-*,results.json).Seat fixup (second commit
27d6efb, on top of2121cd0)Three visual findings from the seat's review, fixed in both language variants:
cover-en.svg,cover.svg): the ledger row bands widened to x=14..642 and the five columns re-spaced to x=28 / 150 / 212 / 392 / 512 with the ids shortened to four characters (PdP4…,S8My…,h7UV…,Hwvc…). By the gate's own widths every column gap is now ≥ 20px (created_at→user_id21.2,user_id→object · record20.0,object · record→old / new20.4,old / new→ip · user agent27.6) and the last value ends 37.6px inside the band; the left edge sits 14px inside. The two footnote lines moved up 4–6px (bottom clearance 10.1 → 14.1).which-reads-land-en.svg,which-reads-land.svg): the threeWHERE C, D AND E STOPlines shortened ("C fails 3 — a list, never recorded", "D fails 1 — crm_account not named", "E fails 2 — the hook never reached"; zh likewise), now ≥ 50px from the card edge; the two ledger row cards grown 110 → 116px so the last line keeps 14px below it (was 8.4).scratchpad/issue-274/clearance-scan.py, over the gate's--verboseoutput, Arial/Liberation metrics): flags any side under 12px to the shape under a text and any two texts on one baseline closer than 16px. Result on all four SVGs: 0 findings across 218 text boxes; 16 single-glyph digits centred in 28px/40px badge chips are exempt by an explicit rule (a chip that narrow cannot give 12px on every side; the gate confirms none overflows) —clearance-scan2.log. First pass had found the six real items above plus the 48 chip sides —clearance-scan1.log.pnpm svg:lintOK (4 SVGs, 218 boxes, 0 findings);pnpm checkunder the lockVERDICT command-exit 0, 0 errors / 0 warnings / 0 hints (160 files) —check-r2.log;pnpm buildunder the lockVERDICT command-exit 0, 920 pages in 53.62 s,--distprose / CJK / quotes / links all pass, tree clean afterwards —build-r2.log;pnpm seo:smokepass (910 pages, 99450 links) —seo-r2.log. All four SVGs re-rendered at native size on light and dark (shots-274/svg/*.png) and looked at: columns separate, stop lines clear, last row line clear.Acceptance notes
datevalues and the nineteen back-linkedupdatedvalues carry 2026-10-26. AGENTS.md's "never more than one post a day" holds as far as this worktree can see: the sibling Post (slot Thu 2026-10-22): How Do You Let Anonymous Visitors Submit a Form Into a Governed Business App? One Public Door, Five Fields Wide #272 branch now carries 2026-10-22 and Post (slot Sun 2026-10-25): How Do You Import a Spreadsheet From an Old System Into an AI-Built App? Import Mappings, Dry Runs, and the Three Ways a Row Can Fail to Match #273 2026-10-27.readAudit.objects; E, a refused read) plus the non-admin ledger query and the SQLite file probe. Additions, not substitutions.typescript ^6.0.3andvitest; the proof app pinstypescript ^5.9.3(what the offline store has) and dropsvitest(unused by the proof). Neither touches the runtime under test.npm create objectstack@latestwas not itself run; the proof used the CRM example at the tag with dependencies pinned to the published 17.7.0 packages, which the card lists as the first option.get(packages/mcp/src/stdio-data-bridge.ts:171-179) resolves a record withengine.find(object, { where: { id }, limit: 1 }), an array result, so by the documented rule a stdioget_recordwould leave no view row while the HTTP one does. Not measured (stdio not run); the post speaks only of the HTTP transport. Noted, not filed; carrier: none.os devboot the only visible opt-in signal is the superseded warning, since the "installed on N object(s)" line isinfo(audit-plugin.ts:247) anddevdefaults towarn(dev.ts:228); the doc already says the warning "will not always" appear (stacks with their ownAuthPlugin). Observation only; the post states it.维护者速读(草稿)
改了什么。 新增一篇博文(八个语言版本、封面与一张示意图),回答「能否证明是谁打开了一条客户记录——包括 AI Agent」:在已发布的 17.7.0 上,一个人经 REST 的详情读取与一个 Agent 经 MCP
get_record的读取各在sys_audit_log留下一行read;列表、未登记对象、被拒绝的读取一行都没有;并逐条核对一行查看记录永远不记的四件事(字段值、看到了什么、IP/UA、被拒读取)。三篇已发布文章各加一句回链并设updated: 2026-10-26。为什么改。 #267 日更第一批的第 10-26 档;此前没有任何文章覆盖读取审计及其范围边界。文中每个数字与日志行都来自 17.7.0 实测,文档引文逐行核对到标签。
风险与代价(含回滚)。 内容类改动,不触及站点代码;门禁与六视口浏览器检查全部通过。回滚只需 revert 这一个提交。注意:文章如实指出在默认
os dev日志级别下只能看到 superseded 警告,以及 Agent 的行只记主体不记通道(需把 key 铸在专用身份上)——这两点是对读者的提醒,不是缺陷。席位意见。 (留空)
你要做的。 档期日合并;若档期再变,先改
date与三篇文章的updated再合并。Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr