Skip to content

Commit 7c01d30

Browse files
committed
Merge origin/main (6d7f4df) into the S8 branch
#23250 moved two counts in the generated tenant-audit census page and its counts ledger. Both took main's side here to commit the merge; the next commit regenerates them from the merged tree. Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-Authored-By: Claude <noreply@anthropic.com>
2 parents b22a9ee + 6d7f4df commit 7c01d30

37 files changed

Lines changed: 3681 additions & 492 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"create-objectstack": minor
3+
---
4+
5+
feat(create-objectstack): a scaffolded project installs its AI skills from the `@objectstack/skills` package at its own `@objectstack/spec` version, never from the repository's `main` branch
6+
7+
Clause-②: no
8+
9+
- **The project depends on the catalog.** The blank template's `devDependencies` gain `@objectstack/skills`, at the same `^<version>` as every other `@objectstack/*` dependency, so a new project's dependency set changes: the skills are the ones written for the `@objectstack/spec` it installs.
10+
- **The install reads `node_modules`.** After `<pm> install`, the scaffolder runs `npx -y skills@1.7.2 experimental_sync --agent claude-code --copy -y`, which copies the installed package's catalog into `.claude/skills/` as real files, once, beside a `skills-lock.json` that names the package as every skill's source. The GitHub `npx skills add objectstack-ai/objectstack/skills …` command is gone from the run, its summary and the `AGENTS.md` and `README.md` it writes into the project.
11+
- **The summary says what happened.** It names the package version installed and prints the same command for re-installing after an upgrade of the `@objectstack/*` packages, and for any other agent runtime. When the install leaves no catalog to read, or a skill did not come from the package, the run says so instead of reporting the skills installed.
12+
- **The flags keep their meaning.** `--skip-skills` skips the skills step. `--skip-install` installs no skills, because nothing is in `node_modules` yet; the summary names `<pm> install` and then the sync command.
13+
- **To move a project created earlier onto the package:** add `@objectstack/skills` to `devDependencies` at your `@objectstack/spec` range and run your package manager's install. Remove the skills `skills add` installed — `npx skills remove objectstack-data -y` and so on for each `objectstack-*` skill, or `npx skills remove --all` if you installed no others — because the sync skips a skill it did not install and leaves the old copy in place. Then run `npx skills@1.7.2 experimental_sync --agent claude-code --copy -y` (name your own runtime after `--agent`).
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/service-mailbox': minor
3+
---
4+
5+
Inbound mail has an ingest seam: `MailIngestService`, the kernel service `mailbox`, turns one raw RFC 822 message into one `sys_mail_message` row, associates it at ingest with the records whose `mailAssociation` email fields hold a participant's address, and re-matches a mailbox on request
6+
7+
Clause-②: yes (the package gains an exported ingest entry point it does not export today; a widening of the public surface)
8+
9+
- **`ingest({ mailboxId, raw, receivedAt? }, context)`** reads one message and answers `stored`, `duplicate` or `suppressed`. Only the system context may call it: a message enters a mailbox from its mail source, never from a user.
10+
- **Participants.** The addresses of From, Sender, Reply-To, To and Cc, plus the same five headers of a message forwarded as an attachment (a `message/rfc822` part of the received message). The mailbox's own address is left out. The set is stored on the row in `match_addresses`, a JSON list of `{ address, source }` where `source` is `outer` or `attachment`. A forward quoted inline in the body is not read yet, and `inline` is declared so a later reader of it can store its addresses.
11+
- **Association.** The message is linked, one `sys_mail_association` row each (`matched_by: 'ingest'`), to every record of every object with an `email` field declaring `mailAssociation: true` whose value equals a participant address (ASCII case-insensitive), inside the mailbox's organization only. A flagged object with no organization column is skipped and logged once. No app code and no resolver service is involved. A message nothing matches is stored `association_state: 'unassociated'`: the restricted pool.
12+
- **Dedupe.** One row per message per mailbox. The key is `mid:` plus the SHA-256 of the Message-ID, or `hdr:` plus the SHA-256 of the header block as received for a message without one. A second fetch answers `duplicate` and writes nothing, and the unique key settles a concurrent pair.
13+
- **Threading.** `message_id`, `in_reply_to` and `reference_ids` hold the RFC 5322 ids with their angle brackets stripped.
14+
- **Suppression.** No row is written for an auto-reply (`Auto-Submitted` other than `no`, `Precedence: bulk`, `junk` or `list`, `X-Autoreply` or `X-Autorespond` other than `no`) or a bounce (`multipart/report; report-type=delivery-status`).
15+
- **`rematch({ mailboxId, messageIds? }, context)`** replays each message's stored participant set against the records that exist now. It adds the missing links (`matched_by: 'rematch'`) and moves a pool message to `associated` once it has one. It never removes a link. A stored set that is not the stored shape is reported in `unreadable` and not matched. The system context may run it, and so may the mailbox's owner and a `manage_mailboxes` holder in the mailbox's organization. Anyone else is refused with `PERMISSION_DENIED` (403), and another organization's mailbox answers `RECORD_NOT_FOUND` (404).
16+
- **New exports:** `MailIngestService`, `MAILBOX_SERVICE`, `MailboxServiceError` (`code` and `status`), `MAIL_MATCH_ADDRESS_SOURCES`, `MAIL_SUPPRESSION_REASONS`, and the types `MailIngestInput`, `MailIngestResult`, `MailRematchInput`, `MailRematchResult`, `MailboxCallerContext`, `MailIngestEngine`, `MailIngestLogger`, `MailAssociationMatch`, `MailMatchAddress`, `MailMatchAddressSource`, `MailSuppressionReason`, `MailboxServiceErrorCode` and `RawMailMessage`. `MailboxServicePlugin` registers the service in `init()`.
17+
- **New dependencies:** `postal-mime` (MIT-0, no dependencies of its own, ES module and CommonJS builds) reads the MIME structure, the encoded words and the address lists. `@objectstack/types`, already installed through `@objectstack/core`, supplies the keyset walk the re-match pages with.
18+
- **Not yet:** no mail source feeds the seam (the IMAP connector ships separately), and attachments are not stored, so a message forwarded as an attachment keeps the forwarder's own text and not the forwarded message's body.

‎.github/workflows/scaffold-e2e.yml‎

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,14 @@ jobs:
235235
236236
- name: 'Skills boundary: only the curated skills/ catalog installs'
237237
# 15.1 third-party eval: the repo-internal dogfood-verification skill
238-
# (.claude/skills/) leaked into scaffolded projects. Two leak paths,
239-
# two assertions against THIS checkout:
240-
# 1. The scaffolder/docs command is `skills add …/objectstack/skills
241-
# --all` — the /skills subpath is the hard boundary, because the
242-
# skills CLI's --all implies --skill '*' which INCLUDES
238+
# (.claude/skills/) leaked into scaffolded projects. The scaffolder no
239+
# longer reads GitHub — it syncs the installed @objectstack/skills
240+
# package, asserted by the step after the pinned shape below — so
241+
# these two assertions now guard the GitHub path the docs keep as the
242+
# `next` channel, against THIS checkout:
243+
# 1. The documented command is `skills add …/objectstack/skills …` —
244+
# the /skills subpath is the hard boundary, because the skills
245+
# CLI's --all implies --skill '*' which INCLUDES
243246
# metadata.internal skills. Install from the subpath and assert
244247
# set-equality with the curated catalog.
245248
# 2. Interactive `skills add …/framework` (no --all) relies on
@@ -293,8 +296,10 @@ jobs:
293296
# never RUNNING — a scaffolder that stops calling it, or an install path
294297
# that no longer leaves a resolvable CLI behind.
295298
#
296-
# `--skip-skills` is kept: skills install after the pin, so dropping it
297-
# would only add a network fetch, never coverage.
299+
# No `--skip-skills` either: the scaffolder's skills step reads the
300+
# catalog out of the node_modules this same install wrote, so this is
301+
# the one project in the job whose skills a real user would get. The
302+
# step after this one asserts them.
298303
#
299304
# The unpublished-version window is the one degradation, and it is
300305
# evidenced rather than blanket. The install step above carries the
@@ -337,7 +342,7 @@ jobs:
337342
# still catches a genuinely broken scaffolder install.
338343
run: |
339344
cd "$RUNNER_TEMP"
340-
node "$GITHUB_WORKSPACE/packages/create-objectstack/bin/create-objectstack.js" pinned-probe --skip-skills
345+
node "$GITHUB_WORKSPACE/packages/create-objectstack/bin/create-objectstack.js" pinned-probe
341346
cd pinned-probe
342347
343348
if [ ! -f node_modules/@objectstack/cli/package.json ]; then
@@ -377,6 +382,58 @@ jobs:
377382
fi
378383
echo "pinned as expected: FROM tag == resolved CLI ($RESOLVED)"
379384
385+
- name: 'Skills: the scaffolder installed exactly the @objectstack/skills catalog'
386+
# The pinned-shape project above was scaffolded the way a real user
387+
# does it, skills included. The scaffolder syncs the catalog out of the
388+
# @objectstack/skills package its own install put in node_modules — at
389+
# the range it gives every @objectstack/* dependency, so the version of
390+
# @objectstack/spec — into .claude/skills/ as real files (the measured
391+
# shapes live in packages/create-objectstack/src/skills-install.ts).
392+
# So the catalog this step compares against is the package's, read off
393+
# the registry-installed node_modules, never this checkout's skills/:
394+
# the same skill names, the same count, copied rather than linked.
395+
#
396+
# The unpublished-version window degrades the way the pinned-shape step
397+
# does, and only that window: with nothing installed at all, that step
398+
# has already classified the registry answer and failed on any other
399+
# cause, so this step asserts the degraded shape — no skills from
400+
# anywhere — instead of nothing. An install that resolved the CLI but
401+
# no @objectstack/skills is a template that lost its dependency: red.
402+
run: |
403+
cd "$RUNNER_TEMP/pinned-probe"
404+
PKG=node_modules/@objectstack/skills
405+
if [ ! -f "$PKG/package.json" ]; then
406+
if [ -f node_modules/@objectstack/cli/package.json ]; then
407+
echo "::error::the scaffolder's install resolved @objectstack/cli but no @objectstack/skills, so this project got no skills catalog at its own version — the template no longer depends on the package"
408+
exit 1
409+
fi
410+
if [ -e .claude/skills ]; then
411+
echo "::error::nothing was installed, yet .claude/skills exists — the scaffolder installed skills from a source other than the @objectstack/skills package"
412+
exit 1
413+
fi
414+
echo "::warning::no @objectstack/skills is installed — the unpublished-version window the pinned-shape step classified. Catalog EQUALITY is SKIPPED for this run; the degraded shape (no skills from any source) was asserted."
415+
exit 0
416+
fi
417+
418+
skills_in() { for d in "$1"/*/; do [ -f "${d}SKILL.md" ] && [ ! -L "${d%/}" ] && basename "$d"; done | sort; }
419+
expected=$(skills_in "$PKG/dist/skills")
420+
installed=$(skills_in .claude/skills)
421+
echo "@objectstack/skills@$(node -p "require('./$PKG/package.json').version") catalog ($(echo "$expected" | grep -c .) skills):"; echo "$expected"
422+
echo "installed in .claude/skills ($(echo "$installed" | grep -c .) skills):"; echo "$installed"
423+
if [ -z "$expected" ]; then
424+
echo "::error::the installed @objectstack/skills holds no dist/skills/<skill>/SKILL.md — the layout the scaffolder reads has moved"
425+
exit 1
426+
fi
427+
if [ "$installed" != "$expected" ]; then
428+
echo "::error::the skills the scaffolder installed are not the installed package's catalog (names and counts above)"
429+
exit 1
430+
fi
431+
if [ -e .agents ] || [ -n "$(find .claude -type l)" ]; then
432+
echo "::error::the scaffolder linked the catalog instead of copying it — a clone of this project would hold dangling links into node_modules"
433+
exit 1
434+
fi
435+
echo "skills as expected: .claude/skills == the installed @objectstack/skills catalog"
436+
380437
- name: Build official runtime image from this checkout
381438
# The scaffolded app's Dockerfile builds FROM
382439
# ghcr.io/objectstack-ai/objectstack. Build that base HERE from

‎content/docs/permissions/system-context.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ One of the largest consumers of the flag;
170170
| 45 | Comment access hooks return early (insert + update + delete, and the read AST), and so do the activity and audit-log read gates (their read AST), the activity field redaction and the audit-log field redaction (the rows a read serves), and the query guard over both objects' value-bearing columns (the read AST) | plugin-audit | Get: the whole activity row and the whole `sys_audit_log` row, its before/after snapshots included, on `find` / `findOne` — the audit writer, each read gate's own pre-scan and the activity redaction's — and a filter, sort or grouping by those columns. Lose: comment visibility scoping, the narrowing of `sys_activity` and of `sys_audit_log` to rows whose parent record the caller can read, the redaction of a parent field's value from an activity row's text and recorded change and from a ledger row's before/after snapshots, the withholding of an activity row recording an update whose every changed field the reader is withheld (on `count` and `aggregate` too, so a total agrees with the rows), and the refusal of a query over those columns for a reader withheld a field of the objects it can reach | `packages/plugins/plugin-audit/src/comment-access-hooks.ts#installCommentAccessHooks`, `#installCommentReadVisibility`, `packages/plugins/plugin-audit/src/activity-read-visibility.ts#installActivityReadVisibility`, `packages/plugins/plugin-audit/src/audit-log-read-visibility.ts#installAuditLogReadVisibility`, `packages/plugins/plugin-audit/src/activity-field-redaction.ts#installActivityFieldRedaction`, `packages/plugins/plugin-audit/src/audit-log-field-redaction.ts#installAuditLogFieldRedaction`, `packages/plugins/plugin-audit/src/parent-field-query-guard.ts#installParentFieldQueryGuard` |
171171
| 46 | Knowledge search returns hits unfiltered | service-knowledge | Lose: the permission filter over search results | `packages/services/service-knowledge/src/knowledge-service.ts#applyPermissionFilter` |
172172
| 47 | The inbound-mail read gates return early (the read AST) | service-mailbox | Get: every `sys_mail_message` and `sys_mail_association` row on `find` / `findOne` — the ingest writer and each gate's own pre-scan, which read the association and mailbox rows a decision needs. Lose: the narrowing of a message to callers who can read a record it is associated with or who own its mailbox (so the unassociated pool is restricted), and of an association to callers who can read the record it names (on `count` and `aggregate` too, so a total agrees with the rows) | `packages/services/service-mailbox/src/mail-read-visibility.ts#gateMiddleware` |
173+
| 47b | The inbound-mail ingest seam: `ingest` admits the system context ALONE, and `rematch` admits it without the organization, owner or `manage_mailboxes` check | service-mailbox | Get: a mail source hands a mailbox its messages, and server code re-matches any mailbox. Lose: nothing a user held — every other caller of `ingest` is refused (`PERMISSION_DENIED`), because an ingested message is shown on every record its correspondents' addresses match and a user who could hand the seam a message could write one in a correspondent's name; every other caller of `rematch` must be in the mailbox's organization and own the mailbox or hold `manage_mailboxes` | `packages/services/service-mailbox/src/ingest/mail-ingest-service.ts#ingest`, `#authorizeRematch` |
173174

174175
### 5. Actions, metadata plane, provenance, the organization wall
175176

0 commit comments

Comments
 (0)