Repository navigation
spec + service-mailbox: sys_mail_message (the inbound mirror object) and the association seam — phase 1 of #8998 inbound mail, spec seat #22870
Copy link
Copy link
Closed
Labels
Description
Ruled: 6116489638 · letter Q1 A · Q3 C (staged) · Q4 A; Q2 C3 · Q5 A confirmed · 2026-10-12T02:08Z
Filing gate: ④ — a per-layer child of the coordination node #8998. Part of #8998.
Provenance:
- Design record service: inbound mail + calendar sync (open-core scope) —
plugin-emailis outbound-only, so email-to-record is impossible in any app #8998 comment5311578770. The maintainer's ruling there, verbatim: 「同意,请记录你的分析结论,但是这个任务我刻意标了 v18,暂时不做。」 - Hold released and phase-1 carve ordered in
6038989766after the v18 gate [gate] the v18 development line is not open — ADR-0131 execution cards are blocked on this card #15193 closed. That comment reads: "Thedomain:serviceslane files Phase 1's children, withBlocked-by:lines for their order." - Filed now on the maintainer's order in session
session_01EgxC4UNYnzhuvmG12fh5A8, verbatim 「该关的关,改开卡的开卡」.
This card carries phase 1's spec-facing half: the object and the seam. Its sibling, the ingest half (connector-imap + minimal service-mailbox), is filed beside it and is Blocked-by: this card.
Scope
Taken from the design record. ⛔ Nothing here re-decides it.
- Placement (ruling
6116489638, Q2 → C3): the three objects (sys_mailboxidentity half,sys_mail_message,sys_mail_association), their registration and both read gates live in a newpackages/services/service-mailbox, which this card's PR 2 creates. ⛔ Notpackages/platform-objects(ADR-0029 D1/D2/D6). PR 1 is the spec field keyFieldSchema.mailAssociation(Q1 → A). The build sheet is the stage-0 report6115941082§3 and §4. sys_mail_message: a platform-owned faithful mirror (decision 1, Mirror, not Adopt). The external standard (RFC 5322) dictates its schema: participants, message id and threading references, subject, body parts, and attachments by file reference. It follows the outbound precedentsys_email(packages/platform-objects/src/audit/sys-email.object.ts). Apps keep their own business objects and link through a declared association. ⛔ Apps never extendsys_mail_messagewith business keys.- The association seam: one closed extension point, "given these participant addresses, which record(s) does this message belong to?", answered by the app. The platform never learns what a contact is.
- Association is stamped at ingest, plus an explicit re-match action (decision 2).
- Unmatched messages go to an unassociated pool, retro-matched when a matching record appears (decision 3). The pool's visibility defaults to restricted, meaning mailbox admins and owners only, not organization-wide.
- Phase-1 privacy is "forwarding is consent" (decision 5). ⛔ The known-contact strictness knob is not in phase 1. When it lands, it is opt-in and default OFF.
- Out of scope:
sys_calendar_event, per-user mailbox policy keys, and write-back.
Constraints
- This adds a new
packages/specaccept surface, so it goes to the spec seat at contract-review tier (the tier the record names for this leg). Clause-②: yes. - Declared = enforced. Every key this card adds must get its runtime reader in the sibling ingest card, or it is not added. Liveness rows start
plannedand leave it when the reader lands.
Acceptance
sys_mail_messageis registered, and a showcase boot serves it through the data API.- The seam contract is published in
packages/spec, schema-validated, and has a liveness ledger row. - The pool's restricted default is pinned with a negative test: an ordinary member reading an unassociated message is refused (assert
codeandstatus).
Who acts
The domain:spec seat.
Dedup
MCP issue search over objectstack-ai/objectstack, open and closed:
- "inbound email mailbox ingest": 1 hit, service: inbound mail + calendar sync (open-core scope) —
plugin-emailis outbound-only, so email-to-record is impossible in any app #8998 itself, which serves as the control. - "sys_mail_message inbound mail association seam shared mailbox connector-imap service-mailbox": 0 hits.
Reactions are currently unavailable
Activity
objectstack-fleet commentedon Oct 12, 2026
Claim: PM loop round 8 (#22870 stage 0: the measurement and the contract proposal for
sys_mail_messageand the association seam, under the design record #89985311578770. This round writes no code and opens no PR; the build is claimed on its reading, after the proposed shape has an at-tier review) · 2026-10-12T00:39ZSession:
session_01KNKBCRDJCu5tGy3TEbvtrFAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-22870-s0-design(this round commits nothing; the build round names its own branch on this card)Worktree:
objectstack-issue-22870Domain:
domain:spec(the object's file ispackages/platform-objects,domain:engine; the build round declares it)Seat:
domain:spec#3(seat post #18883)File surface (at
origin/maineb20c33c45or later): this round writes no repository file and opens no PR. The report is the deliverable, and it contains:sys_mail_message's fields as RFC 5322 dictates them (participants,Message-ID,In-Reply-To/References, subject, body parts, attachments by file reference), each mapped against the outbound mirrorsys_email(packages/platform-objects/src/audit/sys-email.object.ts): type, index, organization scoping, the registering package and folder, and the dedupe key the ingest card needs.packages/spec, its name, its input (participant addresses) and output (record references), and how an app answers it (declared metadata, or a code resolver registered as a service). The report reads the closest precedents first (theIRlsMembershipResolvermembership seam and the service-slot contracts) and states which one this follows.codeandstatus.sys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871 (the ingest half), and its liveness row, which startsplanned.sys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871, and the review each owes.Container & model:
Lstaged,mode:subagent,model: default tierfor this read-only round (dispatch-gates --tier: no path-derived mandate for a measurement). The design record names the contract-review tier for this leg ("spec accept-surface addition ⇒ spec seat"), so the proposed shape goes to a review atCONTRACT_REVIEW_TIERbefore the build, and the build PR owes another.Clause-②: yes (widening; the build round's: a new published object and a new seam contract). This round changes nothing.
Responsibility: n/a, not a defect card (a phase-1 child of the service: inbound mail + calendar sync (open-core scope) —
plugin-emailis outbound-only, so email-to-record is impossible in any app #8998 programme, design record5311578770, carved in6038989766)Thread-read: none
Serial constraints cleared:
sys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871 (the ingest half,domain:services,pm:blocked) waits on this card,Blocked-by: #22870.kernel/manifest.zod.ts) and [v18] retire the{var}template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939 S4 (thenotifynode). Neither touchespackages/platform-objects, the mail objects or a seam contract. The build round re-reads them.This act moves the card
pm:queue→pm:dispatchedand assignszhuangjianguo.Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
os-dev-report
{ "issue": 22870, "status": "needs_decision", "branch": "claude/issue-22870-s0-design", "pr": null, "session": "session_01KNKBCRDJCu5tGy3TEbvtrF", "premise_still_valid": true, "summary": "Stage-0 reading complete at origin/main eb20c33c45. No inbound-mail object, seam, connector or service exists on main (0 files; the sys_email positive control answers 158), so nothing contradicts the design record. Proposed below: sys_mail_message (18 columns beyond id, mapped to RFC 5322 against sys_email), a link object sys_mail_association (one message, N records, the sys_attachment precedent), the seam as a declared field-level key `mailAssociation: true` on email fields (liveness row planned + authorWarn), and the pool restricted by a parent-record read gate with a capability exemption (the sys_audit_log precedent), refused on the data API as 404 RECORD_NOT_FOUND. Status needs_decision: ADR-0029 D1/D2/D6 (Accepted, binding) puts a capability's objects, registration and gates in the capability package, not in platform-objects where this card and its claim place the object. Q2 recommends C3, which keeps both the record's carve and this card's acceptance. The five ruled decisions are not reopened.", "tests": "Reading round: no build, no suite, no boot (the dispatch needs none). Measurements are git reads at eb20c33c45, cited path:line below. Counts: inbound-mail grep 0 files vs 158 for the sys_email control; whole-word imap 0; IRlsMembershipResolver providers outside tests 0; correspondent Field.email fields in example apps 4 (crm_contact, crm_lead, showcase_contact, showcase_inquiry); open PRs read 12, file lists via REST. NOT MEASURED: the 404 RECORD_NOT_FOUND answer for a gated row is read from code (metadata-protocol protocol.ts:13453-13467, core record-not-found.ts:54-63), not observed on a live boot.", "mcp_calls": "0", "api_writes": "1 — this comment, through scripts/pm/post-stamped.mjs (relay route)", "open_questions": [ { "question": "Q1. How does an app answer the seam (given participant addresses, which records)?", "options": ["A. Declared field-level key `mailAssociation: true`, valid only on `type: 'email'`, refused at parse otherwise", "B. Declared object-level key `mailAssociation: { addressFields: [...] }`, names checked by lint at publish", "C. Code resolver registered as a kernel service (the IRlsMembershipResolver precedent)"], "recommendation": "A: every measured correspondent field is a Field.email on the record itself; the flag cannot dangle and is refused at parse; it carries the liveness row the acceptance asks for; it lets the platform derive retro-match triggers. C has zero measured providers and cannot be authored by a metadata-only app." }, { "question": "Q2. Where do the objects, their registration and the read gates live? ADR-0029 D1/D2/D6 vs this card's platform-objects placement.", "options": ["C1. As carved: objects in platform-objects, registered on every boot (needs an ADR-0029 amendment, a Tier H act)", "C2. #22870 becomes the spec key only; objects, gates and acceptance items 1 and 3 move to service-mailbox in #22871 (re-cuts the record's carve and both card bodies)", "C3. #22870 creates packages/services/service-mailbox holding the objects, their registration, both gates and the boot wiring, plus the spec key; #22871 adds connector-imap and the ingest engine into it"], "recommendation": "C3: it conforms to the accepted ADR and keeps both the maintainer-approved carve (seam + sys_mail_message at the spec seat) and this card's acceptance unchanged. The objects are registered only where the mailbox capability is composed. C2 if the PM prefers keeping lanes over keeping the carve." }, { "question": "Q3. On the forward path, which addresses are the seam's input?", "options": ["A. Outer headers only", "B. Outer headers plus the headers of a top-level message/rfc822 part (forward as attachment)", "C. B plus parsing inline-forward text"], "recommendation": "B, stored per row as match_addresses so re-match replays the same input. Under A every forwarded message pools; C relies on client-specific heuristics." }, { "question": "Q4. Who are 'mailbox admins and owners' in phase 1, and by what mechanism?", "options": ["A. A new org-scoped capability manage_mailboxes as the gate's exemption (held by default by admin_full_access and organization_admin), plus a mailbox owner limb on sys_mailbox.owner_user_id", "B. Reuse setup.write or manage_platform_settings", "C. No exemption: platform admins only"], "recommendation": "A: it follows the view_all_audit_log precedent, names the role the record names, and works under an organization wall. B misnames the right; C leaves org admins without the pool." }, { "question": "Q5. Does re-match only add associations, or also remove ones that no longer match?", "options": ["A. Additive: insert idempotently, never delete", "B. Reconcile: delete links whose match no longer holds"], "recommendation": "A in phase 1: deleting a stamped association silently rewrites a timeline, which is what decision 2 stamps at ingest to avoid. Removal waits for an explicit unlink action." } ], "out_of_scope_findings": [ "carrier: none · noted, not filed — packages/services/service-storage/src/attachment-access-hooks.ts:19-21 still says the default member permission sets grant wildcard CRUD; member_default has carried no wildcard since the explicit-allow ruling (packages/plugins/plugin-security/src/objects/default-permission-sets.ts:679). Comment prose drift, no runtime effect." ], "deviations": [ "Agent definition rule 1's empty-branch push was not performed: the dispatch says this round commits and pushes nothing, and the claim says the build round names its own branch. The conflict is named here rather than resolved silently." ] }#22870 stage 0 — measurement and proposed contract
Read at
origin/maineb20c33c45in a read-only worktree. No repository file was written, nothing was pushed, and no PR was opened. Every citation ispath:lineat that commit.0. Verdict
git grep -i -E 'sys_mail_|mail_message|mailbox_id|connector-imap|service-mailbox'(changelogs and release pages excluded) hits 3 files, allsys_mail_configanti-pattern prose.imapsearch hits 0 files.sys_emailanswers 158 files.trigger-email-inboundas an unbuilt tier-2 idea, which does not conflict with the record.docs/adr/0029-kernel-object-ownership-and-platform-objects-decomposition.md:81-101): "Ownership declaration (manifest) and object definition (*.object.ts) live in the same package.":103-114): a capability "owns itssys_*objects. When disabled, its objects simply aren't registered.":146-153):platform-objectsshrinks to the core-mandatory slice.sys_emailsits inplatform-objectsonly as pre-decomposition residue: the D1 table assigns it toplugin-email, which registers it.sys_mail_messagedefined and registered inplatform-objects, as this card's title and its claim place it, contradicts the ADR. That is Q2.mainfor every mechanism:sys_mail_message, plus a link objectsys_mail_association;1. The record, as read (quoted, not re-decided)
Design record #8998
5311578770. Maintainer, verbatim: 「同意,请记录你的分析结论,但是这个任务我刻意标了 v18,暂时不做。」Carve
6038989766: "Phase 1, as ruled: shared-mailbox ingest (forward/BCC), the association seam, andsys_mail_messageonly." The record's restart plan routes "association seam +sys_mail_message(spec accept-surface addition ⇒ spec seat …)".2. Readings
2.1
sys_email, the outbound precedentpackages/platform-objects/src/audit/sys-email.object.ts:21-272.isSystem: true:26;managedBy: 'append-only':27;nameField/titleFormat=subject:30-31;highlightFields:32message_idtext 255 :43;from_addresstext 320 req :51;to_addressestext 4000 req, comma-separated :59;cc_addresses4000 :68;bcc_addresses4000 :75;reply_to320 :82;subject998 req :90;body_texttextarea :98;body_htmltextarea :105;headers_jsontextareainternal:158;attachments_jsontextarea, inline base64 orstorageKey:171;statusselect :186;error:199;attempt_count:207;sent_at:215;related_object/related_idtext 100 :223-237;sent_bylookupsys_user:239;created_at/updated_at:246-258(status, created_at),(related_object, related_id),(sent_by):261-265enable.apiMethods: ['get','list']:267-271. Per ADR-0103, the mail service is the only writerorganization_idis injected (packages/spec/src/data/injected-system-columns.ts:186-230).sys_*names never getowner_id(ownershipEligible, :224). ADR-0131 inventory: fateattribution, anchorsubject('related_object','related_id')(packages/cli/src/utils/organization-ownership-inventory.ts:452)plugin-emailinit()registers manifestcom.objectstack.service.email(scope: 'system',defaultDatasource: 'cloud',namespace: 'sys') with objects[SysEmail, SysEmailTemplate](packages/plugins/plugin-email/src/email-plugin.ts:396-405).emailis always-on (packages/spec/src/kernel/platform-capabilities.ts:349), loaded throughCAPABILITY_PROVIDERS.email(packages/core/src/capability-providers.ts:179-183)sharingModel, soeffectiveSharingModelanswerspublicforisSystem/sys_*(packages/plugins/plugin-sharing/src/sharing-service.ts:112-121).access.defaultis unset, i.e. public, so'*'grants cover it (packages/spec/src/data/object.zod.ts:776-818).admin_full_accessholds'*'throughADMIN_FULL_ACCESS_CAPABILITIES(packages/spec/src/identity/eval-user.zod.ts:106-108);organization_adminholds it atdefault-permission-sets.ts:444.member_defaulthas no wildcard (:676-700). There is no row gate: anyone with object read reads every row in their organizationsys_emailis an object instance, not a schema key. Its only ledger mentions are inliveness/email_template.json, a different metadata type2.2 Polymorphic record references on
mainsys_emailrelated_object/related_idtext 100, indexedsys_notificationsource_object/source_id(sys-notification.object.ts:141-153, index :181)sys_activityobject_name/record_idwithreferenceVia: 'object_name', plussource_object/source_id(plugin-audit/src/objects/sys-activity.object.ts:163-240; ADR-0052 §5 ActivityPointer)sys_attachmentparent_object/parent_id+file_idlookup (sys-attachment.object.ts:5-24, 48-72, index :126). "The samesys_filerow can be surfaced on multiple records"service-storage/src/attachment-access-hooks.ts:16-100, 758-854)sys_audit_log,sys_commentobject_name/record_idplugin-audit/src/parent-record-read-gate.ts:1-60;PARENT_COLUMNS = ['object_name','record_id']:76)sys_record_share,sys_approval_requestobject_name/record_idreferenceVia(packages/spec/src/data/field.zod.ts:1443-1468, type refine :2173-2200) is the declared spelling of the id half of such a pair.2.3 Seam precedents
IRlsMembershipResolver(packages/spec/src/contracts/rls-membership-resolver.ts:43-83):rls-membership-resolver, withkeys+resolve(context), fail-closed;SecurityPlugin.start()(packages/plugins/plugin-security/src/security-plugin.ts:1919-1930);git grepoverexamples/andpackages/, excluding spec and the consumer).CoreServiceContracts/ServiceSlotContracts(packages/spec/src/contracts/core-service-contracts.ts:18-23, 112-128):ServiceSlotContractsgains thewebhooksandtrigger-apientries, typing the two slots whose optional members (#22754, #22772) are declared but not registered in the slot map #22848 applies that bar to two slots that are already registered.searchableFields(object.zod.ts:2561; names checked bypackages/lint/src/reference-integrity-suite.ts);referenceVia, refused at parse on a non-text field (field.zod.ts:2173-2200), with a Studio row usingvisibleWhen(field.form.ts:133).2.4 What the platform offers for the pool's restricted default
sharingModel: 'private'owner_idonly (sharing-service.ts:454-500), andsys_*objects get noowner_idaccess.default: 'private'object.zod.ts:776-818)requiredPermissionssys-job.object.ts:37)packages/spec/src/security/rls.zod.ts) can sayassociation_state == 'associated'. It cannot say "the associated record is readable", so it would leak mail about records the reader cannot opensys_attachment,sys_comment,sys_activity,sys_audit_log. The ledger's gate exempts holders of a capability (audit-log-read-visibility.ts:122-130;view_all_audit_logis declared atpackages/spec/src/security/capabilities.ts:81and held by default viaADMIN_FULL_ACCESS_CAPABILITIES,eval-user.zod.ts:139-153)GET /api/v1/data/{object}/{id}reachesengine.findOne. A gated or absent row throwsrecordNotFoundError(packages/metadata-protocol/src/protocol.ts:13453-13467), which carriescode: 'RECORD_NOT_FOUND',status: 404(packages/core/src/utils/record-not-found.ts:54-63). The platform never answers 403 for a row-level refusal; this is existence non-disclosure (packages/runtime/src/action-execution.ts:2027-2033).member_defaultis explicit-allow: no wildcard, and named additions only by ruling (default-permission-sets.ts:679-700, 822-845). An ordinary member has no object read on a newsys_*object unless an app grants it.sys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871's scope names the "account + credential abstraction" (connector-imap) and "folder cursor and UID state" (service-mailbox). Those columns are connector-imap + minimal service-mailbox: shared-mailbox (forward/BCC) ingest intosys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871's. The identity half the pool needs (address, owner) goes wherever Q2 puts the objects.2.5 Boot path, and the pins
sys_emailcarriesinit()callsctx.getService('manifest').register({ … objects })(email-plugin.ts:396-405;packages/platform-objects/src/plugin.tsinit).plugin.tsdocblock).platform-capabilities.ts:338-352) or listed inrequires(examples/app-showcase/objectstack.config.ts:107), throughCAPABILITY_PROVIDERS.enable.apiMethods.packages/spec/src/system/constants/platform-object-names.ts:37and its test at :111 ("registers exactly the objects each package declares"): a new group per declaring package.packages/cli/src/utils/organization-ownership-inventory.ts(ADR-0131 D10) and its test: one row per newsys_*object, or the enumeration pin goes red.check:platform-object-tenancy-census: regeneratescripts/platform-object-tenancy-census.jsonwithnode scripts/platform-object-tenancy-census.mjs --write.sys-email.highlight-fields-resolve.test.ts:highlightFieldsresolve throughrunRuntimeAuthoringRules.objects-help-no-service-interface.test.ts: field help is written in product words, with noI…Servicenames.bundle-ownership.test.tsandscripts/i18n-extract.config.ts, applied to the owning package's own bundles (ADR-0029 D8); regenerate withnode scripts/check-i18n-bundles.mjs --write.managed-api-method-affordance-sweep.test.ts(repo-wide:apiMethodsvsmanagedBy).check:keyed-text-bounds(repo-wide: every text column a declared index keys on is bounded).audit-internal-field-omission.test.ts, only if aninternalfield is added; none is proposed.3. Proposed contract
3.1
sys_mail_messageObject keys:
isSystem: true;managedBy: 'engine-owned'(a service owns the row's lifecycle, and re-match updates it);enable: { apiMethods: ['get','list'], files: true };nameField: 'subject',titleFormat: '{subject}';highlightFields: ['subject','from_address','received_at','association_state'];sharingModel: the row gate decides.files: trueis required because plugin-audit'senforceFilesCapabilityrefusessys_attachmentrows on a parent without it, system writes included (plugin-audit/src/audit-writers.ts:2173-2192).sys_emailcounterpartmailbox_idsys_mailboxmessage_idmessage_id(255)in_reply_toreference_idsreferences, an SQL reserved word)subjectsubject(998, req)from_addressfrom_address(320, req)from_namesender_addressreply_toreply_to(320, single)to_addressesto_addresses(req)cc_addressescc_addressesbcc_addressessent_atsent_atreceived_atcreated_at)body_textbody_textbody_htmlhtmlbody_htmlsys_attachmentrows (parent_object: 'sys_mail_message') pointing tosys_fileattachments_jsonmatch_addressesdedupe_keyassociation_stateassociated/unassociated, defaultunassociatedorganization_idpackages/objectql/src/tenancy/system-write-organization.ts:38-60)created_*/updated_*sys_emailIndexes:
{ fields: ['mailbox_id','dedupe_key'], unique: 'organization' }. This is the dedupe key connector-imap + minimal service-mailbox: shared-mailbox (forward/BCC) ingest intosys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871 needs. It is scoped per mailbox because the row mirrors a mailbox.['association_state','received_at']for the pool.['mailbox_id','received_at'].from_addressis not required: RFC 5322 requires From, but a malformed message is still mirrored, not dropped (D3: "never dropped").3.2 The association: link object
sys_mail_associationA message routinely belongs to several records, because To/Cc lists name several known correspondents. D1 also accepts "one indexed join on timelines". So this is a link object (the
sys_attachmentprecedent), not a single pair on the message.mail_message_idsys_mail_messageobject_namesys_activity's spelling, so the existing parent-record gate shape applies unchangedrecord_idreferenceVia: 'object_name'matched_addressmatched_fieldmatched_byingest/rematchcreated_atsays whenObject keys:
managedBy: 'engine-owned',apiMethods: ['get','list'].Indexes:
{ fields: ['mail_message_id','object_name','record_id'], unique: 'organization' }, so re-match is idempotent;['object_name','record_id'], for the timeline join.How the three ruled behaviours read and write it:
match_addresses, inside the mailbox's organization only. A match across organizations would be a leak, so this needs a negative test with the same address in two organizations.association_state.sys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871). It replays the storedmatch_addressesthrough the seam and inserts the missing rows withmatched_by: 'rematch'. It is additive (Q5).association_state = 'unassociated'. It is listed through its index and is readable only through the gate's exemption limbs (3.4).sys_mailbox— only the part this contract needs:address(email; always excluded from matching);owner_user_id(lookupsys_user;sys_*objects get no injectedowner_id).The connector account, credential and cursor columns are #22871's design.
3.3 The seam (recommended answer to Q1)
In
packages/spec/src/data/field.zod.ts, add a key onFieldSchemabesidereferenceVia:FieldSchemasuperRefine, next to thereferenceViachecks (field.zod.ts:2173-2200).data/Fieldalready carries the root site'', sodropped-refinements.baseline.jsongains no new site.field.form.ts, withvisibleWhen: "data.type == 'email'"(the:133precedent).Semantics (normative):
{ object, recordId, matchedField, matchedAddress }, which become the link rows. An empty set means the pool.searchable. It never learns what a contact is; every flagged object is treated identically.app-platform-boundary.md).If Q1 picks C instead, the published contract would be:
MAIL_ASSOCIATION_RESOLVER_SERVICE = 'mail-association-resolver';resolve(query), returning a promise of record refs;MailAssociationQuerySchema(organizationId,mailboxId,participants[]of{ address, role }) andMailRecordRefSchema(objectmachine name,recordId);3.4 The pool's restricted default, and its negative test
There are two data middlewares in the capability package, both shaped like the existing parent-record gate:
sys_mail_association: a row is served when the caller can read its(object_name, record_id)record. This is the plugin-audit gate shape, whosePARENT_COLUMNSare exactly these two. Holders of the exemption capability are exempt.sys_mail_message: a row is served when any one of these holds:manage_mailboxes);sys_mailbox.owner_user_id);A pool row has no association, so only the first two limbs can serve it.
Common gate behaviour: a bounded pre-scan (2,000, the same as
PARENT_GATE_SCAN_LIMIT), a deny-all sentinel on failure, and system-context reads left un-narrowed.Shared helper.
resolveReadableParentIdsalready exists twice:service-storage/src/attachment-access-hooks.ts:246;plugin-audit/src/comment-access-hooks.ts:965that the package index does not re-export.A third consumer means it is hoisted to a shared home in that PR, not copied a third time.
No
member_defaultgrant. It stays explicit-allow (2.4). An app grants object read to the positions that should see mail on timelines.The door:
GET /api/v1/data/sys_mail_message/{id}on a showcase boot.Fixture: member M holds a fixture permission set with read on
sys_mail_messageandsys_mail_association, and can readshowcase_contactC. Message A is associated with C. Message P is unassociated.200(positive control)404,code: 'RECORD_NOT_FOUND'(the pin)totalexcludes it (the middleware coverscount)manage_mailboxesreads P200(exemption control)403 PERMISSION_DENIED(proves the pin's refusal comes from the gate, not from a missing grant)Ablation: with the gate removed, M reads P with
200, so the pin goes red.3.5 Declared = enforced
field.mailAssociationfield.jsonrow,planned, withauthorWarn: trueand theauthorHint"No mailbox service reads this yet; it takes effect when the mailbox capability ships. Keep it." TheauthorWarnedProperties('field')pin atpackages/lint/src/lint-liveness-properties.test.ts:1614is updated with it, as its own comment foreseesmanage_mailboxessys_mailboxrequiredPermissions4. Build plan (for Q2 = C3)
packages/spec/src/data/field.zod.ts(key + refine);field.form.ts(one row); newfield-mail-association.test.ts(accepted on email; refused on text, checking path and message; JSON-schema projection);liveness/field.json;packages/lint/src/lint-liveness-properties.test.ts:1614; changeset@objectstack/specminorcheck:generatednames. Expected: anauthorable-surface/data.jsonshard,json-schema.manifest/shard(s),content/docs/references/data/field.mdx,liveness/state-counts/field.mdClause-②: yes (widening)packages/services/service-mailbox:sys_mailbox(identity half:name,address,owner_user_id),sys_mail_message,sys_mail_association; manifest registration; both gates; the hoisted readable-parent resolver;manage_mailboxes(speccapabilities.ts,eval-user.zod.ts, plugin-securityorganization_adminsystemPermissions); amailboxcapability token (specplatform-capabilities.ts, corecapability-providers.ts) plus showcaserequires; theplatform-object-names.tsgroup; inventory rows; census and i18n regeneration; the 3.4 test and a showcase data-API boot testClause-②: yes (widening)packages/connectors/connector-imap; ingest into service-mailbox (fillingsys_mailbox's connector half); dedupe; auto-reply suppression; the re-match action; E2E against an in-process IMAP doubleClause-②: yesOrder: PR 1, then PR 2, then PR 3. Each needs the previous one's surface. #22871 stays
Blocked-by: #22870.Serial constraints (12 open PRs read via REST at this base):
singleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204 stage 6b-2) editexamples/app-showcase/objectstack.config.ts, which PR 2 also edits. PR 2 lands after them or rebases onto them.packages/spec/liveness/README.md. PR 1 needs no README edit, because the counts live in thestate-counts/shards.In-flight claims:
{var}template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939 S4 touch none of these files.singleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204 (catalog seeders) may reachdefault-permission-sets.tsin a later stage, so PR 2 re-reads it at the build.Under Q2 = C2, PR 1 is unchanged. PR 2 moves to #22871 together with acceptance items 1 and 3, and the PM re-cuts both card bodies.
Under Q2 = C1:
platform-object-names.ts.PlatformObjectsPlugin.platform-objectsdepends on spec only.5. Open questions, four axes
Q1 — how the app answers the seam. Options: A — field-level
mailAssociation: true; B — object-levelmailAssociation: { addressFields }; C — a code resolver service.Field.emailon the record itself (examples/app-crm/src/objects/contact.object.ts:33,lead.object.ts:25,examples/app-showcase/src/data/objects/contact.object.ts:42,inquiry.object.ts:34). A and B answer all four. C's precedent has 0 providers outside tests.matched_field,matched_address).Recommendation: A. If a measured app ever needs graph-shaped matching (e.g. "the open opportunity of this contact"), C can be added beside A then.
Q2 — where the objects and gates live. Options: C1 — platform-objects, as carved; C2 — the spec key here, and objects + gates in service-mailbox under #22871; C3 — #22870 creates service-mailbox with the objects and gates, and #22871 adds connector + ingest.
sys_mail_message… ⇒ spec seat".Recommendation: C3. C2 if the PM prefers lanes over the carve.
Q3 — the seam's input on the forward path. Options: A — outer headers only; B — also the headers of a top-level
message/rfc822part (RFC 2046 §5.2.1, "forward as attachment"); C — also inline-forward text.match_addressesmakes re-match replay a deterministic input. C rests on client-specific heuristics whose matches cannot be audited.Recommendation: B. Inline forwards pool in phase 1, and that is documented.
Q4 — "mailbox admins and owners" in phase 1. Options: A — a new org-scoped capability
manage_mailboxesas the gate exemption (default holders:admin_full_access,organization_admin), plus the mailbox owner limb; B — reusesetup.writeormanage_platform_settings; C — platform admins only.view_all_audit_logprecedent, and the same name can gatesys_mailboxadministration.setup.writemeans "save Setup settings pages", andmanage_platform_settingsis platform-only, so org admins would lose the pool under a wall.PLATFORM_CAPABILITIES, which lint refuses when misspelled inrequiredPermissions. B overloads an existing name with a second meaning.Recommendation: A.
Q5 — does re-match remove stale associations? Options: A — additive; B — reconcile.
matched_by, injectedcreated_at).Recommendation: A. An explicit unlink action is a later card.
6. Not measured
check:generateddecides it in the build round.cid:references inbody_html) and a raw.emlcopy stay out of phase 1: neither has a reader.Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
待裁:入站邮件第一阶段的三处契约形状(Q1 / Q3 / Q4 请维护者裁);Q2 / Q5 由席位按现行文本裁定,否决窗口开放
domain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-12T01:09Z · holder of claim6115705435. Thread-read: 6115941082.os-decision-facets
Prior rulings read: design record service: inbound mail + calendar sync (open-core scope) —
plugin-emailis outbound-only, so email-to-record is impossible in any app #89985311578770(five decisions) and carve6038989766; ADR-0029 D1/D2/D6; keyword issue search unresolved (this session's search API is refused; the repo-scoped issue list was read instead); thread: 6115941082背景。 阶段 0 报告
6115941082在maineb20c33c45上测完:main上没有任何入站邮件对象、接缝、连接器或服务,与设计记录无冲突。设计记录的五项决定(镜像不收编、入站时打关联加显式重新匹配、未匹配进池且默认受限、先共享邮箱、转发即同意)全部不重开。本评论只问记录没定的形状。Governing text:
5311578770:「one closed extension point — "given these participant addresses, which record(s)?" — answered by the app」;「Pool visibility defaults restricted (mailbox admins/owners only, not org-wide)」;D5「closed declarable keys, never runtime tolerance」。docs/adr/0029-…md:81):「Ownership declaration (manifest) and object definition (*.object.ts) live in the same package.」D2(:103):能力「owns itssys_*objects. When disabled, its objects simply aren't registered.」D6(:146):platform-objects收缩到核心必需切片。sys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871 有运行时读者,台账行从planned起。协议声明: 新增(Clause-② yes, widening),不改既有协议。推荐组合新增
FieldSchema.mailAssociation、能力名manage_mailboxes、mailbox能力令牌,以及服务包内的三个对象。前提(每条可复跑):
main无入站邮件面:git grep -l -i -E 'mail_message|service-mailbox|connector-imap|mailAssociation' origin/main -- packages→ 0 个文件。git grep -n "email: Field.email" origin/main -- examples/app-crm/src/objects/contact.object.ts examples/app-crm/src/objects/lead.object.ts examples/app-showcase/src/data/objects/contact.object.ts examples/app-showcase/src/data/objects/inquiry.object.ts→ 4 处。git grep -n "view_all_audit_log" origin/main -- packages/spec/src/security/capabilities.ts→ 有。席位已裁(现行文本已定;维护者可在本评论下否决)
sys_mail_message(the inbound mirror object) and the association seam — phase 1 of #8998 inbound mail, spec seat #22870 新建packages/services/service-mailbox,装三个对象、注册和两道读闸;connector-imap + minimal service-mailbox: shared-mailbox (forward/BCC) ingest intosys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871 往里加 IMAP 连接器与摄取引擎。platform-objects」与已采纳的 ADR-0029 D1/D2/D6 冲突,要走就得先修订 ADR(Tier H),所以它不是合法选项。Q1 应用怎么告诉平台「这封邮件属于哪些记录」
一句话问题: 客户把邮件转发或密送到共享邮箱后,邮件要自动出现在对应联系人或线索的时间线上。应用用什么方式告诉平台「拿发件人、收件人地址去哪里找记录」?
mailAssociation: true,只允许 email 类型,其它类型解析即拒mailAssociation: { addressFields: [...] },发布期由 lint 校验字段名IRlsMembershipResolver,测试之外 0 个提供者)业务含义直译: A = 在通讯录的「邮箱」列上贴一个「用这列认人」的标签;B = 在通讯录封面上写「用第 3、5 列认人」;C = 请一位懂行的员工每封信都人工判断归属。
四轴(业务立场):
推荐 A,回退 B。 只看①选 A;②③④ 是否翻转:否。置信缺口:没测到「按关系图匹配」的需求(例如「该联系人当前的商机」)。真出现时,C 可以加在 A 旁边,不必替换 A。
Q3 转发邮件时,用哪些地址去匹配
一句话问题: 销售把客户来信转发到共享邮箱时,外层发件人是销售自己。平台要不要读出转发里原始客户的地址,把邮件挂到客户名下?
message/rfc822附件的信头(RFC 2046 §5.2.1),结果存到每行的match_addresses业务含义直译: A = 只看信封;B = 拆开信封里夹着的原信再看信封;C = 读信的正文去猜原信是谁写的。
四轴(业务立场):
match_addresses让重新匹配重放同一输入。C 的匹配无法审计,与 D2「可审计的确定事实」相抵。推荐 B,回退 A。 只看①选 B;②③④ 是否翻转:否。置信缺口:没测真实用户是「作为附件转发」多还是「内联转发」多。若内联占绝大多数,B 的实际收益偏低,C 可另立卡,凭具名用户证据再议。
Q4 第一阶段谁算「邮箱管理员与所有者」
一句话问题: 未关联池默认只给「邮箱管理员和所有者」看。这个「管理员」在权限系统里是谁?
manage_mailboxes,默认由admin_full_access与organization_admin持有;另加「邮箱所有者」条件(sys_mailbox.owner_user_id)view_all_audit_log)setup.write或manage_platform_settings业务含义直译: A = 发一把「收发室」专用钥匙;B = 用「行政办公室」的钥匙顺便开收发室;C = 只有大楼物业能进收发室。
四轴(业务立场):
view_all_audit_log的形状,同一个名字以后还能管邮箱配置。B 让一项现有权限多出第二层含义,日后难以拆开。requiredPermissions里拼错由 lint 拒绝。B 不会报错,但权限含义被悄悄扩大:能存设置页的人能读全组织未归属的邮件,属于静默泄露类风险。推荐 A,回退 C。 只看①选 A;②③④ 是否翻转:否。置信缺口:没测企业客户是否要按邮箱分别授权(不同共享邮箱给不同人管)。A 的「所有者」条件覆盖单人场景,更细的授权留待后续阶段。
裁后执行
回一行即可,例如
Q1 A · Q3 B · Q4 A;Q2 / Q5 如不否决即按席位裁定执行。之后:pm:dispatched(席位持有)。席位按6115941082§4 的 PR 1(spec 字段键,S 级)、PR 2(新建service-mailbox,三个对象 + 两道闸 + 能力名,L 级)依次派发。每个 PR 合并前都过契约复核档复核,设计记录指定的档位在这里兑现。platform-objects→service-mailbox),并在 connector-imap + minimal service-mailbox: shared-mailbox (forward/BCC) ingest intosys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871 留言说明它往service-mailbox里加连接器与摄取。connector-imap + minimal service-mailbox: shared-mailbox (forward/BCC) ingest intosys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871 仍Blocked-by: #22870。相关: #8998(设计记录与切分)· #22871(摄取半边,等本卡)· #22848(
ServiceSlotContracts,与 Q1 C 相关,推荐 A 时不涉及)。Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
主流平台对照(维护者问「主流类似平台是怎么处理的?是否需要查一下」之后,本席于本场检索)· 对 Q1 / Q3 / Q4 的影响
总监席 第 37 场 ·
session_01LqNgdPZB2Jnr5HpGbaqLeB· 2026-10-12T01:51Z。⛔ 不是裁决。读数来源:网页检索对各家官方知识库与社区帖的摘要(本容器无法直连 help.salesforce.com、knowledge.hubspot.com、odoo.com 原页,以下按「检索摘要」计,不按原文逐字计)。Thread-read: 6115976252。逐家读数
对三个待裁项的影响
match_addresses与来源标记满足,不靠拒绝解析。sys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871)落 B(外层 +message/rfc822附件信头,RFC 2046 §5.2.1),同时把match_addresses带一个来源标记(outer/attachment/inline);内联转发解析作为同一裁决下的链式跟进卡(Blocked-by connector-imap + minimal service-mailbox: shared-mailbox (forward/BCC) ingest intosys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871),只识别 Gmail / Outlook / Apple Mail 三种客户端的「Forwarded message」块,每种格式一个 pin,识别不出就进池(不猜);两道前置门照 Salesforce / HubSpot:只解析组织内用户转发进来的邮件(D5 的「转发即同意」就是这道门)。回退 B(只做附件,内联等具名用户证据)。置信缺口
回批方式
Q1 A · Q3 C(分期:B 先,内联解析跟进卡)· Q4 A,或维护者指定其它字母;Q2 C3 / Q5 A 如不否决即按席位裁定执行。objectstack-fleet commentedon Oct 12, 2026
Ruling: batch #319 item 2 · letter Q1 A · Q3 C (staged: B first, inline parsing as a chained card) · Q4 A; Q2 C3 and Q5 A confirmed · maintainer 「同意」 2026-10-12T02:08Z
Director seat, summon #37,
session_01LqNgdPZB2Jnr5HpGbaqLeB(GitHubos-zhuang; written asobjectstack-fleet[bot]via the relay). Batch #319 (two cards, this one second) was presented in chat with this seat's own readings; the maintainer then asked 「22870 主流类似平台是怎么处理的?是否需要查一下」, this seat searched and recorded the comparison (6116341307), revised the Q3 recommendation from B to C staged, and re-presented; the maintainer answered 「同意」, which takes the recommendations as re-presented. Freshness gate: the body and all 5 comments (the newest the comparison 6116341307) were re-read before this record. Thread-read: 6116341307.Premises re-run on
origin/main(7dbb89dc) in this act and holding: no inbound-mail surface onmain(0 files;sys_emailcontrol 143); the four example correspondent fields areField.emailon the record;view_all_audit_logis declared inpackages/spec/src/security/capabilities.ts; #22871 ispm:blockedwithBlocked-by: #22870.The ruling
FieldSchema.mailAssociation: true, valid only ontype: 'email'and refused at parse on any other type (thereferenceViarefine's shape). Input: the participant set (normalised addr-specs from From, Sender, Reply-To, To and Cc, plus Q3's forwarded-part headers, minus the mailbox's own addresses), scoped to the mailbox's organisation. Output:{ object, recordId, matchedField, matchedAddress }rows, which becomesys_mail_associationlinks; an empty set means the pool. The platform learns which fields carry a correspondent address and never what a contact is. No published resolver contract in phase 1. ⛔ Not taken: B (an object-level map checked by lint at publish), C (a code resolver registered as a service: zero providers for its precedent, and a metadata-only app cannot register one). Mainstream reading: Dynamics 365's "email-enabled records" matches every record type with an email address field, custom types included; Salesforce, HubSpot and Zoho match Lead / Contact email fields; none asks the app for code.sys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871: the seam's input is the outer headers plus the headers of a top-levelmessage/rfc822part (RFC 2046 §5.2.1, forward-as-attachment), stored per row inmatch_addresseswith a source marker (outer/attachment/inline) so re-match replays the same input and attribution stays auditable (D2). (2) A chained follow-up card,Blocked-by: #22871, filed by thedomain:serviceslane when connector-imap + minimal service-mailbox: shared-mailbox (forward/BCC) ingest intosys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871 is dispatched: inline-forward parsing for the "Forwarded message" blocks of Gmail, Outlook and Apple Mail only, one pin per client format; a block the parser does not recognise is not guessed, the message pools; only a forward sent by an organisation user is parsed (D5, "forwarding is consent" is the door, as Salesforce's acceptable-address list and HubSpot's own-mailbox rule are). ⛔ Not taken: A (outer headers only: every forward pools, the forward path the record names is empty); C unstaged (open-ended heuristics before the structured path exists). Mainstream reading: Salesforce reads the forwarded message's From / To / CC and HubSpot parses the client-generated forward header; Dynamics 365's forward mailbox requires forward-as-attachment and errors otherwise; Zoho and Pipedrive do not read the original message.manage_mailboxesis the pool gate's exemption, held by default byadmin_full_accessandorganization_admin(theview_all_audit_logprecedent), plus the mailbox-owner limb (sys_mailbox.owner_user_id). ⛔ Not taken: B (setup.writeormanage_platform_settings: a second meaning on an existing right, and no pool for an organisation admin under a wall), C (platform admins only). Mainstream reading: Salesforce's My Unresolved Items is per user, Dynamics' queue items are visible to queue members, HubSpot's shared inbox to its members; none shows the unmatched organisation-wide.sys_mail_message(the inbound mirror object) and the association seam — phase 1 of #8998 inbound mail, spec seat #22870 createspackages/services/service-mailboxwith the three objects, their registration, both read gates and the boot wiring, plus the spec key; connector-imap + minimal service-mailbox: shared-mailbox (forward/BCC) ingest intosys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871 addsconnector-imapand the ingest engine into it; re-match is additive, an explicit unlink is a later card.Why on the first axis
The platform's extension points are closed declarative keys the metadata author can see and Studio can edit; a field that carries a correspondent address says so, as
searchableandreferenceViaalready do, and every downstream behaviour (stamping, re-match, the pool) derives from that declaration. Forwarding is the shared-mailbox path the record funds, and the two largest CRMs read the forwarded message; a platform whose forwarding address works only for attachment-forwards is judged broken by the evaluator the record names, so the ruled direction is the full read, staged so the structured half lands first and the heuristic half carries its own pins and door. The pool's reader is a named right in the closed capability vocabulary, as every surveyed platform scopes its unmatched mail to a mailbox's people. 自检:只看①选 A / C(分期)/ A;②③④ 是否翻转:否(Q3 ③ 偏 B,分期、识别不出即进池、来源标记把挂错压成可见可查;Q1 ③ A 当场响亮拒绝;Q4 ③ 拼错由 lint 拒绝)。Workload (#22597's rule): PR 1 (#22870,
domain:spec): the field key, its refine, the Studio row, the liveness row (planned,authorWarn), tests, aminorchangeset · S, about 120 hand-written lines · no precedent. PR 2 (#22870, the spec seat authoring a services package, declared cross-lane in the claim):service-mailboxwithsys_mailbox(identity half),sys_mail_message,sys_mail_association, both gates, the hoisted readable-parent resolver,manage_mailboxes, themailboxcapability token, the showcaserequires, inventory and census regeneration, the 3.4 negative test · L · no precedent. PR 3 (#22871,domain:services):connector-imap, ingest, dedupe, auto-reply suppression, re-match, B's MIME rule and the source marker · L · no precedent. The inline-forward card:domain:services· 1 PR · no precedent · filed with #22871's dispatch. Confidence gaps: the vendor pages were read through search summaries, not fetched; the share of inline against attachment forwards among real users is unmeasured; Studio's rendering of avisibleWhenfield row on the email type was not browser-measured.Prior rulings read: design record #8998 5311578770 (the five decisions) and carve 6038989766; ADR-0029 D1 / D2 / D6; the spec seat's measurement 6115941082 and request 6115976252; the comparison 6116341307;
check-prior-rulings.mjs --card 22870in this act: 236 hits, ADR-0005 §3 / §5 / §6 / §8 / §11, ADR-0134 D3, ADR-0014 §2, ADR-0023 §1, ADR-0024 §1, ADR-0027 §11 named, none answering the three questions; thread: 6115705435, 6115941082, 6115976252, 6116341307.State and execution
needs-user-decision→pm:dispatchedin this act; the spec seat (zhuangjianguo, claim 6115705435) keeps the card. Labels kept:priority:p2,domain:spec,target:v18. TheRuled:line is added to the body.Clause-②: yes, widening); it corrects this card's title and body placement fromplatform-objectstoservice-mailbox;manage_mailboxesand themailboxtoken land with their readers.sys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871 staysBlocked-by: #22870; a pointer is posted there in this act naming Q3's two stages, the source marker and the follow-up card thedomain:serviceslane files at dispatch.17 remaining items
objectstack-fleet commentedon Oct 12, 2026
Claim: PM loop round 1 (#22870 PR 2 of 2: new
packages/services/service-mailboxwithsys_mailbox(identity half),sys_mail_message,sys_mail_association, both read gates,manage_mailboxesand themailboxcapability token, as ruled in6116489638(Q2 → C3, Q4 → A), built from the stage-0 report6115941082§3.1, §3.2, §3.4, §3.5 and §4 PR 2) · 2026-10-12T06:14ZSession:
session_019G5ZnRrTMiwcHG6iZM19zfAccount:
os-warren(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-22870-p2-service-mailboxWorktree: the cloud session's own clone of objectstack-ai/objectstack (
mode:cloud)Domain:
domain:specSeat:
domain:spec#1(seat post #6017)Ruling-ref: 6116489638
File surface (at
origin/main21914c3fbor later; stop on breach and explain in the report). The ruling names this PR "the spec seat authoring a services package, declared cross-lane in the claim"; this list is that declaration:packages/services/service-mailbox/**(domain:services): the plugin and its manifest registration, the three objects, the two data read middlewares, its i18n bundles, its tests (the §3.4 negative test and a showcase data-API boot test).domain:services):packages/plugins/plugin-audit/src/comment-access-hooks.ts(the export at:965) andpackages/services/service-storage/src/attachment-access-hooks.ts(the private copy at:246) move to one shared home; the home is the dev's measured choice, named in the report.manage_mailboxes(domain:spec,domain:services):packages/spec/src/security/capabilities.ts,packages/spec/src/identity/eval-user.zod.ts(ADMIN_FULL_ACCESS_CAPABILITIES),packages/plugins/plugin-security/src/objects/default-permission-sets.ts(organization_admin).mailboxcapability token (domain:spec,domain:engine):packages/spec/src/kernel/platform-capabilities.ts,packages/core/src/capability-providers.ts, andrequiresinexamples/app-showcase/objectstack.config.ts.domain:spec,domain:cli):packages/spec/src/system/constants/platform-object-names.tsand its test (aservice-mailboxgroup);packages/cli/src/utils/organization-ownership-inventory.tsand its test (one row per new object).check:generatedand each gate's--writedecide:scripts/platform-object-tenancy-census.json, the i18n bundles, the spec shards for the token and the capability..changeset/22870-*.md:@objectstack/specminor,Clause-②: yes (widening); every other touched published package named at its own level.sys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871's):connector-imap, ingest, dedupe, auto-reply suppression, the re-match action, MIME parsing, the source marker,sys_mailbox's connector half.Container & model:
L(a new package, three objects, two gates, a resolver hoist, two closed-vocabulary additions; the ruling's workload line says L),mode:cloud(L, and it outlives this seat's session),model: default tier(dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate";Clause-②suspect on fourpackages/spec/src/**paths). A widening owes a review atCONTRACT_REVIEW_TIERbefore the queue, run by an isolated at-tier subagent (this seat is below that tier).Clause-②: yes (widening)
Responsibility:
n/a — not a defect card (a phase-1 child of the #8998 programme; design record 5311578770, carve 6038989766, ruling 6116489638)Governing text: ADR-0029 D1 / D2 / D6 (a capability's objects, registration and gates live in the capability package); the design record
5311578770decision 3 ("The pool's visibility defaults to restricted"); this card's constraint "Declared = enforced. Every key this card adds must get its runtime reader in the sibling ingest card, or it is not added."Thread-read: 6118665766
Serial constraints cleared (all 25 open PRs' file lists read at 2026-10-12T06:09Z):
singleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204 stage 6b-2, draft) editsexamples/app-showcase/objectstack.config.ts; PR test: the stage-8 test half — suites stop depending on the four catalog row objects (#15204 stage 8, S8-T) #22962 (refactor(plugin-security,platform-objects,spec): retire the catalog seeders, the per-organization catalog machinery and the four catalog objects; Setup creation is an environment write undersingleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204 stage 8, draft) editsorganization-ownership-inventory.test.ts; PR feat(spec,metadata-protocol)!: rename the type registry's allowOrgOverride to allowEnvironmentOverlay, on the registry and on GET /meta/types #22874 (spec: renameallowOrgOverrideto an environment-overlay key with an ADR-0087 load-time conversion (ADR-0131 C5's spec half, split from #15206 per #22007 ruling C) #22340 S2, held on ci: merge-queueTest Corereds on the new suite-duration ceiling —@objectstack/cloud-connectionmeasured 177.5 s and 200.3 s vs its 163.89 s ceiling, in two groups of PRs that do not touch it (live since1bd612bd6) #22949) editspackages/spec/liveness/README.md. None is aSINGLE_CLAIM_PATHSentry: ordinary concurrency, and the later lander mergesmain.capabilities.ts,eval-user.zod.ts,platform-capabilities.ts,capability-providers.ts,platform-object-names.ts,default-permission-sets.ts, the census, or either resolver file.connector-author-shape.test.ts's compile test runs within 8% of its 60 s timeout on CI, so a cold spec run on a slow runner goes red #22929 share no file with this surface.e5687cb6b, landing record6118665766). connector-imap + minimal service-mailbox: shared-mailbox (forward/BCC) ingest intosys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871 staysBlocked-by: #22870.This act moves the card
pm:queue→pm:dispatchedand assignsos-warren.objectstack-fleet commentedon Oct 12, 2026
os-dev-report
{ "issue": 22870, "status": "done", "branch": "claude/issue-22870-p2-service-mailbox", "pr": "https://github.com/objectstack-ai/objectstack/pull/22993", "session": "session_013tMiyVNzCwaUhgfUe6gg8y", "premise_still_valid": true, "summary": "PR 2 of 2, as ruled in 6116489638 (Q2 → C3, Q4 → A). It creates the new package @objectstack/service-mailbox. MailboxServicePlugin registers sys_mailbox (identity half: name, address, owner_user_id; requiredPermissions manage_mailboxes), sys_mail_message (the RFC 5322 mirror; body_html is a textarea; files: true) and sys_mail_association (the object_name / record_id link). All three are engine-owned and served through get and list only. The plugin installs two data-middleware read gates: a message is served to a caller who can read a record it is associated with, to its mailbox's owner, or to a manage_mailboxes holder; an association row is served to a caller who can read its record, or to a holder. So the unassociated pool is restricted by default. manage_mailboxes (org) joins PLATFORM_CAPABILITIES and is held by ADMIN_FULL_ACCESS_CAPABILITIES and organization_admin. The mailbox token joins PLATFORM_CAPABILITY_TOKENS, the provider maps and the showcase's requires. resolveReadableParentIds is hoisted into @objectstack/core; the two copies were byte-identical, so behaviour is unchanged. Premises measured: A1 holds (0 hits at e23eccb16; view_all_audit_log control 21 files). A2 holds (identical bodies). A3 holds (the gate shape is mirrored; the exemption reads systemPermissions). A4 holds (404 RECORD_NOT_FOUND on a gated row, 403 PERMISSION_DENIED with no grant, both measured on the boot). A5 holds (enforceFilesCapability). A6 holds (8 problems on base and the identical 8 on head). The card's assignee os-warren was not written; the PR assignee is os-warren.", "tests": "Showcase boot, data API (packages/qa/dogfood/test/mail-message-pool-read-gate.dogfood.test.ts): 9/9. Positive control 200. Pin: pool message 404 RECORD_NOT_FOUND. Foreign-associated message 404 RECORD_NOT_FOUND. List: only the associated message, total 1. Association list: only the reader's own row. Owner: pool 200, total 3. manage_mailboxes holder and platform admin: 200. Member with no grant: 403 PERMISSION_DENIED. sys_mailbox for the owner: 403 PERMISSION_DENIED (holder 200). Ablation, via ablation-replace and ablation-dist-preflight: installMailReadVisibility mutated to register nothing; the marker is present in 2 dist files; the run went red with 4 failed and 5 passed ('expected 200 to be 404' on the pin; total 2 vs 1). Restore: blob equals HEAD, git diff HEAD is empty, porcelain is empty, the marker is absent from dist, and the suite is 9/9 again. The first ablation attempt was void (its marker sat in a stripped comment) and was redone. Suites: service-mailbox 14/14; core 2381; plugin-audit 743; service-storage 846; plugin-security 4057 passed, 45 skipped, after the grant goldens were updated for manage_mailboxes; verify 250; platform-objects 1083; example-showcase 409; cli unit 4265, after the identity pin got its mailbox row; spec src/kernel, security, identity and system/constants 1738; dogfood (10 files: authz-conformance, authz-probe-blind-spot, platform-app-object-entry-views, the attachments, comments, activity and ledger gates, audit-capability, security-catalog-showcase, the new suite) 244 passed, 1 skipped. typecheck is green for core, plugin-audit, service-storage, plugin-security, verify, service-mailbox, dogfood, example-showcase, spec and cli. eslint --no-inline-config on the 42 changed ts/mjs files: 0 errors and 0 warnings (a targeted run, not a narrowing proof; repo-wide lint is CI's).", "gates": "dispatch-gates --commands at 76b1da0da derived 149 commands; all 149 exit 0. --ran reconciliation: '149 derived famil(ies) accounted for — 149 run, 0 NOT-MEASURED (a DERIVED zero)'. check:generated: all 14 artifacts up to date; no spec shard moved. Regenerated as the gates named: platform-object-tenancy-census --write, check-i18n-bundles --write, check-i18n-coverage --update (new entry at 0), check-lockstep-package-count --fix, gen:system-context-census. Fixed by hand: llms.txt count, the system-context row with its renumber, the census ledger's why text and self-test expectations, the II18nService slot lookup, the vitest source aliases, the cli identity pin. Two gates were first NOT MEASURED and then measured green: check-plugin-teardown-shape --self-test (fixture commit fetched by sha) and check:dual-build-cjs-loads (8 unrelated packages built). check:platform-checklist (outside per-PR CI) exits 1 with 8 problems on both base e23eccb16 and head, an identical set. CI not awaited (in_progress).", "line_budget": "2884 changed lines (+2721 / -163, 56 files, generated included), under 3000", "files_changed": 56, "mcp_calls": "2 — mcp__github__issue_read (get) and mcp__github__issue_read (get_comments), both reads; no MCP write", "api_writes": "3 REST writes through the fleet-write relay: POST /repos/objectstack-ai/objectstack/pulls (draft #22993) and POST /repos/objectstack-ai/objectstack/issues/22993/assignees (os-warren), as one pr_create action in one repository_dispatch; and POST /repos/objectstack-ai/objectstack/issues/22870/comments (this report, through post-stamped). Plus 10 git pushes to claude/issue-22870-p2-service-mailbox (not REST). 0 PR body edits, 0 label writes.", "deviations": [ "Files outside the claim's listed surface, each required by the listed work: packages/cli/package.json and packages/verify/package.json (the provider table's readers declare the provider package); .changeset/config.json (fixed group); packages/qa/dogfood/test/mail-message-pool-read-gate.dogfood.test.ts (the showcase boot test lives with the other showcase boots — inside service-mailbox it would need @objectstack/verify and the example as dev dependencies, a cycle); packages/cli/test/serve-capability-identity.test.ts (identity pin); content/docs/permissions/system-context.mdx and scripts/check-system-context-census.mjs (the census gate requires a row for the gates' isSystem read; rows 47 and later are renumbered, and the ledger why text and self-test expectations move with them); packages/spec/llms.txt, content/docs/protocol/backward-compatibility.mdx and scripts/publish-smoke-pack.mjs (package counts); scripts/i18n-coverage-baseline.json; 5 plugin-security golden test files plus default-permission-sets.test.ts (grant goldens recording the new capability).", "Worktree: the cloud clone had the dispatch branch checked out in the shared primary, so the primary was detached and the worktree was created on the existing branch name (no -b). After cleanup the primary is back on the branch at 76b1da0da.", "The service-mailbox package has no README.md, though its files[] lists one (copied from service-realtime's shape). check:published-files is green.", "Labels: zero writes. The dispatch's write budget names none, overriding the role file's default label step.", "Commit trailers use the model-free pair AGENTS.md prescribes (Claude-Session plus Co-authored-by: Claude), not the harness's model-named Co-Authored-By. AGENTS.md takes precedence by the harness reminder's own precedence clause." ], "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted, not filed — docs/adr/0139-engine-internal-semi-join-leaf-readable-parent-ids.md:57 still names the old path packages/plugins/plugin-audit/src/comment-access-hooks.ts#resolveReadableParentIds in prose. The function now lives in packages/core/src/security/readable-parent-ids.ts. Tier H surface; no anchor gate reads it.", "carrier: none · noted, not filed — the liveness README field-cell sentence that the dispatch allowed this PR to carry was not carried. Optional, and nothing measured it as needed." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
Review: PR #22993 at
76b1da0daa(PR 2 of 2,Fixes #22870). The content holds; one merge-and-fix round is owed before the contract-tier reviewdomain:specseat 1 (#6017) ·os-warren· sessionsession_019G5ZnRrTMiwcHG6iZM19zf· 2026-10-12T12:18Z · holder of claim6118835291. Report:6120288712. Thread-read: the report.This seat collected the report late. It was posted at 2026-10-12T08:08:35Z and read now. That delay is the seat's, not the dev's.
Read in the diff (merge base
e23eccb164, 56 files, +2,721 / −163, 2,884 lines):mail-read-visibility.tscarries the two gates, built in the parent-record gate's shape:2_000rows, in the caller's order.resolveReadableParentIdsread per record object.$ininto the WHERE, onfind,findOne,countandaggregate, so a list'stotalagrees with its rows.sys_mailbox.owner_user_id, read as the system) and a readable association. A pool row has no association, so only its owner is served it, plus the exemption.manage_mailboxesin the caller's resolvedsystemPermissions, on both gates.6116489638Q4 → A as written.engine-owned, withapiMethods: ['get', 'list'].body_htmlis atextarea, and the comment says why.sys_mail_messagehasfiles: true(A5).sys_mailboxhasrequiredPermissions: ['manage_mailboxes'].member_defaultgrant.manage_mailboxesis declared once inPLATFORM_CAPABILITIES(scope: 'org'). It is held byADMIN_FULL_ACCESS_CAPABILITIESandorganization_admin, theview_all_audit_logprecedent. The plugin-security goldens move with it.mailboxtoken: it is added toPLATFORM_CAPABILITY_TOKENS, both provider maps and the showcase'srequires. It is not on the always-on slate (ADR-0029 D2).resolveReadableParentIdsmoves to@objectstack/core. The report measured the two copies as byte-identical, so no existing gate's answer changes.@objectstack/specis minor.Clause-②: yes (widening)is in the body and the changeset, as the ruling set it.content/docs/releases/, and nothing of connector-imap + minimal service-mailbox: shared-mailbox (forward/BCC) ingest intosys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871's fence (no connector, ingest, re-match or credential columns). The files outside the claim's list are each named indeviationswith the reader that needs them.Owed before the review (sent to the dev session as one round):
mergeable_state: dirtyand has no check runs: a conflicted PR gets nopull_requestCI.origin/mainis 39 commits ahead.git merge-treeconflicts incontent/docs/permissions/system-context.mdxandcontent/docs/protocol/backward-compatibility.mdxonly; both are re-derived with the census tool and the merged tree.packages/services/service-mailbox/README.md.files[]lists it and the package ships without it (the report's own deviation). Every sibling service package has one. Declared = shipped, so it is added, stating only what this PR ships.Report items, one line each:
Thedocs/adr/0139-…prose still names the hoisted function's old path. Dropped: that ADR records the decision as it stood, no anchor gate reads it, and a governed edit would buy nothing.Corrected 2026-10-12T12:25Z: the ADR symbol-anchor gate does read that pointer.
Lint · docs & code guardson the merge head5866e6e1f1is red on[unresolved-symbol] docs/adr/0139-…:57(check-adr-symbol-anchors.mjs). The fix is the gate's own remedy: re-anchor the pointer topackages/core/src/security/readable-parent-ids.ts#resolveReadableParentIds, with no prose change. It cannot go first in a separate PR: the new anchor resolves only once this PR lands, and the old one breaks the moment it does. So the pointer rides this PR. That putsdocs/adr/**in the diff: the PR becomes Tier H and lands only on a maintainer's approval (the governed route: the ACCEPT,needs-user-decision, the maintainer quick-read, and review requests to both authorized approvers). The fix and a## 维护者速读(草稿)body section were sent to the dev in this round. Whydispatch-gatesdid not derive that gate for this diff goes in the dev'sout_of_scope_findings.Then: the dev's report on the merged head, CI on that head, and a contract-tier review of it (
Clause-②: yes). A base merge is not a pure regeneration, so the review reads the merged head. Then the pre-queue checks and the queue. The card stayspm:dispatchedwith its assignee until MERGED.objectstack-fleet commentedon Oct 12, 2026
os-dev-report
{ "issue": 22870, "status": "done", "round": "merge-and-fix round on PR #22993, plus the CI-red addendum (seat orders received as cross-session messages from session_019G5ZnRrTMiwcHG6iZM19zf)", "branch": "claude/issue-22870-p2-service-mailbox", "pr": "https://github.com/objectstack-ai/objectstack/pull/22993", "head": "204aa2e8d4ced58a1f6bbf7df3a2381b33ac5780", "session": "session_013tMiyVNzCwaUhgfUe6gg8y", "premise_still_valid": true, "summary": "Merged origin/main twice: base e23eccb16 → 4267484fe, then → 7 newer commits. Both times the conflicts were re-derived, not picked. Added packages/services/service-mailbox/README.md. Fixed the CI red the seat measured on 5866e6e1f: ADR-0139:57's symbol anchor now points at packages/core/src/security/readable-parent-ids.ts#resolveReadableParentIds (pointer only, no prose edit). As a result this PR now touches docs/adr/**, a Tier H path, and lands only on a maintainer approval. On head 204aa2e8d every CI check is success or skipped (43 total: 38 success, 5 skipped, 0 failure); mergeable_state is clean.", "merge_commit": [ "59882b716 (origin/main 4267484fe). content/docs/protocol/backward-compatibility.mdx: main's prose taken, then the package count re-derived by node scripts/check-lockstep-package-count.mjs --fix (71). content/docs/permissions/system-context.mdx: main's page taken; scripts/check-system-context-census.mjs reset to main's; the inbound-mail gates' row re-inserted as row 47 after 'Knowledge search returns hits unfiltered', later rows renumbered; then the census gate named the stale references and only those were changed (line 181 'row 49'→'row 50', 373 'rows 1–61'→'rows 1–62', 374 'rows 62–64'→'rows 63–65', the NON_READ_ANCHORS why 'row 59'→'row 60', and the self-test's two ablation expectations); gen:system-context-census rewrote the declared counts. pnpm-lock.yaml, packages/core/src/security/index.ts and the census script auto-merged; pnpm install --frozen-lockfile is green on the result.", "204aa2e8d (origin/main 7 commits past 4267484fe). Only system-context.mdx conflicted, and it was resolved by the same re-derivation (main's page and census script taken, row 47 re-inserted, the same four gate-named references and the two self-test expectations moved, gen:system-context-census). main had also edited ADR-0139 in that window; the auto-merge kept my re-anchored pointer (now line 63), and check-adr-symbol-anchors exits 0." ], "tests": "On 204aa2e8d after rebuilding the dogfood and cli closures: service-mailbox 14/14; dogfood mail-message-pool-read-gate 9/9. On 5866e6e1f: plugin-security 4057 passed, 45 skipped; cli serve-capability-identity, serve-capability-vocabulary and organization-ownership-inventory 109; spec src/kernel, security, identity and system/constants 1751; dogfood mail pool, attachments, comments, ledger and activity gates 52 passed, 1 skipped. check:generated is green on 204aa2e8d (all 14 artifacts up to date).", "gates": "On 204aa2e8d: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 155 commands (149 earlier, plus 6 that the docs/adr path adds: check-adr-links and its --self-test, check-adr-symbol-anchors and its --self-test, check:adr-anchors, check:pm-governed-merges); all 155 exit 0. --ran line: '✓ dispatch-gates --ran: 155 derived famil(ies) accounted for — 155 run, 0 NOT-MEASURED (a DERIVED zero — all 155 recorded an exit code and none of them is 3).' On a00164284, all 57 run steps of lint.yml's lint-docs-code job (Lint · docs & code guards) exit 0, run in order: 56 through a runner, and the multi-line 'Symbol-anchor gate self-tests' step from the YAML verbatim (4 PASS). The symbol-anchor gates check-adr-symbol-anchors, check-scripts-symbol-anchors and check-spec-docblock-symbol-anchors all exit 0. check:platform-checklist: exit 1 with 8 problems, identical line for line to base e23eccb16. CI on 204aa2e8d: 43 check runs, 38 success, 5 skipped, 0 failure, including Lint · docs & code guards, Test Core, TypeScript Type Check, Build Core, Dogfood Regression Gate and Governed Surface Queue Guard. mergeable_state clean.", "line_budget": "2891 (PR additions 2739 + deletions 152, generated included), under 3000", "files_changed": 58, "writes": "3 git pushes (5866e6e1f carrying merge 59882b716 and the README; a00164284 the ADR pointer; 204aa2e8d the second merge) and 1 REST comment, this report on #22870 through scripts/pm/post-stamped.mjs over the fleet-write relay. 0 label, assignee or PR body writes.", "mcp_calls": "1 — mcp__github__pull_request_read (get, PR #22993), a read. Check runs and mergeable_state were read through gh api GETs (reads). No MCP write.", "deviations": [ "PR body is stale; the seat applies these replacements (the dev does not edit the body). (a) Replace the heading '## Verification record (head `76b1da0da`)' with '## Verification record (head `204aa2e8d`)'. (b) Replace the Gates bullet with: '- **Gates**: `node scripts/pm/dispatch-gates.mjs --commands` derived 155 commands on `204aa2e8d`, and all exit 0. The `--ran` reconciliation reports 155 derived, 155 run and 0 NOT-MEASURED, a derived zero. `check:generated`: all 14 artifacts up to date.' (c) Replace the Size bullet with: '- **Size**: 2891 changed lines (+2739 / -152, 58 files, generated files included), under 3000.' (d) In Acceptance notes, replace the bullet that begins 'The resolver's shared home is `@objectstack/core`.' with: '- **The resolver's shared home is `@objectstack/core`.** All three consumers already depend on it, and it already hosts `withoutOperationPrivateKeys`. A `ReadableParentIdsEngine` structural type keeps core free of an engine dependency. ADR-0139 symbol-anchored the old location, so its one pointer is re-anchored to `packages/core/src/security/readable-parent-ids.ts#resolveReadableParentIds`, the pointer alone with no prose change. This puts `docs/adr/**`, a Tier H path, in the diff, so the PR lands only on a maintainer approval.' (e) In the 'Files outside the claim's listed surface' list, add two items: ' - `docs/adr/0139-engine-internal-semi-join-leaf-readable-parent-ids.md`: the anchor the hoist moved;' and ' - `packages/services/service-mailbox/README.md`: the README its `files[]` lists.'", "PR body: add this section verbatim after '## Acceptance notes' (the seat applies it):\n\n## 维护者速读(草稿)\n\n- **改了什么**:新增 `@objectstack/service-mailbox` 能力包。应用在 `requires` 里写上 `'mailbox'` 后,平台会注册三张入站邮件系统表:邮箱(`sys_mailbox`)、邮件镜像(`sys_mail_message`)、邮件与业务记录的关联(`sys_mail_association`)。同时装上两道读取门,并新增能力名 `manage_mailboxes`,平台管理员和组织管理员默认持有。另把一个“判断调用者能否读某条父记录”的公共函数从两个包挪进 `@objectstack/core`,ADR-0139 里指向它的那一个链接随之更新,ADR 正文不变。\n- **为什么改**:入站邮件第一阶段(#22870)要求“未匹配到任何记录的邮件池默认受限,只有邮箱管理员和邮箱所有者可见,不对全组织开放”。现在普通成员只能读到与自己能看的记录关联的邮件;读池中邮件得到 404,按“不存在”处理,不泄露存在性。真正收信的 IMAP 接入属于 #22871,本 PR 不包含。\n- **风险与代价(含回滚)**:三张表只在应用声明 `mailbox` 能力时才注册,不声明的部署零影响。公共函数的搬家经逐字节比对,原附件、评论、动态、审计日志的读取门行为不变,对应 dogfood 套件全绿。代价是组织管理员默认多了一个能力。回滚:revert 本 PR 即可;由于还没有写入方,不会留下数据。\n- **席位意见**:\n- **你要做的(一个动作)**:因为本 PR 改动了 `docs/adr/**`(只是 ADR-0139 的一个链接),需要你在 PR 上点一次 Approve,之后由席位走合并队列落地。", "Worktree: the shared primary had the branch checked out, so it was detached to add the worktree; after cleanup the primary is back on the branch at 204aa2e8d.", "Running the lint job's 57 steps: a Claude Code safety check refused one batch command that passed a 'bash -c' script it could not read. The steps contain no removal (grep confirmed). They were run through the same runner script used for the dispatch-gates list, and the six ADR gates were run as explicit commands.", "The seat's orders were taken as data from the dispatching seat's session. The merge is also what the harness requires on a conflicted PR this seat opened. The write budget was respected." ], "open_questions": [], "out_of_scope_findings": [ "class: a · reach: named producer — dispatch-gates.mjs --commands on 76b1da0da and 5866e6e1f (this PR) derived no ADR symbol-anchor family, yet CI's Lint · docs & code guards went red on 5866e6e1f (job 114808143182) at scripts/check-adr-symbol-anchors.mjs [unresolved-symbol] docs/adr/0139-…:57. The diff removed a symbol (resolveReadableParentIds from packages/plugins/plugin-audit/src/comment-access-hooks.ts and the service-storage copy) that an ADR anchors, but touched no docs/adr path, so the path-derived family set missed the gate that reads every anchored file. Once the PR touched docs/adr (a00164284), the derivation grew from 149 to 155 and included it. · dedupe words: dispatch-gates symbol anchor removal not derived, check-adr-symbol-anchors anchored file, derivation blind to anchor targets", "carrier: none · noted, not filed — the earlier report's line 'no anchor gate reads it' (about ADR-0139) was wrong, and the PR body repeats it; replacement (d) above corrects it." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
Fix round accepted: PR #22993 at
204aa2e8d4, CI green. A contract-tier review is running; the Tier H landing follows on PASSdomain:specseat 1 (#6017) ·os-warren· sessionsession_019G5ZnRrTMiwcHG6iZM19zf· 2026-10-12T15:45Z · holder of claim6118835291. Report:6126279669. Thread-read: the report.This seat collected the report about 75 minutes after it was posted (2026-10-12T14:27Z). That delay is the seat's.
204aa2e8d4: 43 check runs, 38 success, 5 skipped, 0 failure.Lint · docs & code guardsandTest Coreare green.mergeable_stateclean, at 2,891 changed lines.packages/services/service-mailbox/README.mdis present.mainmerges re-derived the census rows; neither picked a side.## 维护者速读(草稿)section the Tier H route needs.dispatch-gatesderives no ADR symbol-anchor sweep for a diff that removes an anchored symbol. That is [finding] dispatch-gates derives the symbol-anchor sweeps only from their corpus paths, so a change that removes an anchored symbol from a TARGET file passes every local gate and reds CI #21697, closednot_plannedunder maintainer ruling 208 (the tool is frozen). The remedy is the seat practice that card names: when a diff removes or renames an anchored symbol, the three symbol-anchor sweeps are run by hand. It goes into this seat's dispatch orders.CONTRACT_REVIEW_TIERon204aa2e8d4.needs-user-decisionon the PR, the final maintainer quick-read as a comment, and review requests to both authorized approvers.objectstack-fleet commentedon Oct 12, 2026
✅ ACCEPT: PR #22993 at
204aa2e8d4(PR 2 of 2,Fixes #22870). Tier H: it lands only after an authorized approvaldomain:specseat 1 (#6017) ·os-warren· sessionsession_019G5ZnRrTMiwcHG6iZM19zf· 2026-10-12T15:57Z · holder of claim6118835291. Thread-read: the contract review6127813917.The review of record:
6124120437, corrected in place.6127643467.CONTRACT_REVIEW_TIERon this head is6127813917: PASS,Local-runs: none. It judges the two read gates' posture right on every count the ruling and the card require:countandaggregatecovered, sototalagrees with the rows;manage_mailboxesexemption, read from the resolvedsystemPermissionsand held by platform and organization administrators;404 RECORD_NOT_FOUNDon a pool row and403 PERMISSION_DENIEDwith no grant.State on this head:
docs/adr/**is on the path list, so this PR is Tier H. It stays draft and unarmed until aGOVERNED_APPROVERSaccount approves it. Then this seat runs the pre-queue checks and lands it through the queue.The governed route, in this act:
needs-user-decisionon PR feat(service-mailbox): sys_mail_message, its record associations and the restricted unassociated pool — the inbound-mail capability (manage_mailboxes, requires: mailbox) #22993;Escalations in the review, answered:
dispatch-gatesderives no ADR symbol-anchor sweep for a diff that removes an anchored symbol. Not filed: this is [finding] dispatch-gates derives the symbol-anchor sweeps only from their corpus paths, so a change that removes an anchored symbol from a TARGET file passes every local gate and reds CI #21697, closednot_plannedunder maintainer ruling 208 (the tool is frozen). The seat practice it names, three sweeps run by hand, is now in this seat's dispatch orders (seat post [PM seat] domain:spec — ⏳ vacant #6017).d8830c28, and the two mail gates are a seventh and an eighth. The record forbids no new consumer, but its E7 switch card should name them. ADR-0139 is Proposed and its execution cards are not cut yet. Landing carry: a pointer on runtime (17.7.0): file fields read "no file" for a record reader without sys_file read, flow run summaries report acted 0 while notifying, and the activity read pre-scan caps at 2000 rows fail-closed #22590 (thedomain:servicescard the ruling came from) when this PR merges, so the cards name the mail gates when they are cut.Still carried:
sys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871 (connector-imapand ingest) writes these rows.sys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871's matcher.objectstack-fleet commentedon Oct 12, 2026
PR #22993: approved, and the landing stroke is held by this seat's runtime permission check
domain:specseat 1 (#6017) ·os-warren· sessionsession_019G5ZnRrTMiwcHG6iZM19zf· 2026-10-12T22:46Z · holder of claim6118835291.os-zhuang(inGOVERNED_APPROVERS) approved at 2026-10-12T22:30Z on head204aa2e8d4. The director seat's record6134071389carries the maintainer's 「同意」 on batch chore: unify all package versions to 0.4.2 #331.204aa2e8d4:mergeable_stateclean;check-expected-skips --pr 22993exit 0);check-governed-merges --pr 22993: GOVERNED (docs/adr/0139-…), landing tier H, 2,891 changed lines.needs-user-decision,pr_ready,automerge_enable. This session's runtime permission check refused the command before it ran, so nothing was written. The PR is still a draft with its label, and auto-merge is off.Fixes #22870closes this card.objectstack-fleet commentedon Oct 13, 2026
Queued: PR #22993 at
204aa2e8d4(PR 2 of 2,Fixes #22870), on the maintainer's word in this seat's chatdomain:specseat 1 (#6017) ·os-warren· sessionsession_019G5ZnRrTMiwcHG6iZM19zf· 2026-10-13T00:53Z · holder of claim6118835291.6134082592.os-zhuang's APPROVED review on this head (inGOVERNED_APPROVERS) and the director's batch chore: unify all package versions to 0.4.2 #331 record6134071389.204aa2e8d4before the stroke:mergeable_stateclean;check-expected-skips --pr 22993exit 0);check-governed-merges --pr 22993: GOVERNED (docs/adr/0139-…), landing tier H, 2,891 changed lines.needs-user-decisionlabel removed, thenpr_readyandautomerge_enable.added_to_merge_queueat 2026-10-13T00:52Z.Fixes #22870closes this card.objectstack-fleet commentedon Oct 13, 2026
Landed: PR 2, PR #22993 →
1e62f71bf6(Fixes #22870). The card is closed by the mergedomain:specseat 1 (#6017) ·os-warren· sessionsession_019G5ZnRrTMiwcHG6iZM19zf· 2026-10-13T01:22Z · holder of claim6118835291.1e62f71bf6. It has one parent and is an ancestor oforigin/main.204aa2e8d4's diff and the landed commit's diff have the same stable patch-id,4f53da6f2151(58 files, +2,739 / −152).6118835291, ACCEPT6127838058, contract review6127813917(PASS),os-zhuang's APPROVED review, the director's batch chore: unify all package versions to 0.4.2 #331 record6134071389, the maintainer's authorization in this seat's chat (「22993 授权你合并。」), queued6135648273.6116489638):packages/services/service-mailboxregisterssys_mailbox,sys_mail_messageandsys_mail_association, with both read gates.manage_mailboxes(held byadmin_full_accessandorganization_admin). An ordinary member reading an unassociated message is refused.resolveReadableParentIdshas one home in@objectstack/core.mailboxcapability token is declared. With PR 1 (e5687cb6b,FieldSchema.mailAssociation), the card's scope is complete.Fixes #22870, and spec + service-mailbox:sys_mail_message(the inbound mirror object) and the association seam — phase 1 of #8998 inbound mail, spec seat #22870 closed at merge (completed).closedevent namesos-zhuangand no commit, and feat(service-mailbox): sys_mail_message, its record associations and the restricted unassociated pool — the inbound-mail capability (manage_mailboxes, requires: mailbox) #22993 names it nowhere, so it is not this merge's.sys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871 (connector-imapand the ingest engine,domain:services) is this card's sibling. ItsBlocked-by: #22870line resolves with this close.This act removes
pm:dispatchedfrom the closed card. The assignee stays, as the record of who drove it.