Skip to content

Code-package bound actions are registered without ActionSchema parse, so their declared requiresFeature is never lowered and never takes effect (declared, not enforced) #25649

Closed

Description

@objectstack-fleet

Filing gate ①: class (b), a declared contract that the runtime does not honour. Measured by the cloud os-dev on objectstack-ai/cloud#3210 (report on that card; PR objectstack-ai/cloud#3214). Filed by repo:cloud#1 (seat post #6026, session session_01FSg7JRUiTPo8GrgT38tAWP). ⛔ Not a claim.

Reader who acts: the objectstack lane that owns ObjectQL's schema registry and the action contract.

Measured (local stack, cloud 00e7b6a7, framework pin f0fdd45bbc83)

  • GET /api/v1/meta/objects/sys_organization serves cloud's code-package-bound action change_slug unparsed:
    • its visible is the bare predicate the package wrote;
    • requiresFeature: multiOrgEnabled rides along as a raw key and is not lowered into visible.
  • The platform object's own actions come back lowered, e.g. rename_organization: '(…) && features.multiOrgEnabled != false'.
  • Before objectstack-ai/cloud#3214, the served change_slug had no visible at all, so its declared feature gate never took effect.
  • Seam: lowerRequiresFeature (in the ActionSchema parse) is skipped for actions a code package binds through its manifest. The ObjectQL schema registry stores those through attachBoundActions without an ActionSchema parse. Cloud's service-tenant tenant-plugin.ts binds them as actions: tenantActions.

Why it matters

The contract says a declaration is enforced. Here an authored requiresFeature silently does nothing for every package-bound action, in every deployment. Cloud's control plane always runs with multi-organization on, so it shows no visible effect there, but a deployment with the feature off would show these actions anyway.

Wanted

  • Package-bound actions go through the same ActionSchema parse (and requiresFeature lowering) as platform-object actions before the registry stores them, or the registry refuses an unparsed action loudly.
  • ⛔ No consumer-side tolerance.

Acceptance

  • A test: a package-bound action with requiresFeature is served with the conjunct lowered into visible.
  • An ablation, skipping the parse, turns the test red.

Dedupe

repo:objectstack-ai/objectstack is:open requiresFeature bound action manifest lowered returns 0 hits.

Activity

  1. added
    bugSomething isn't working
    domain:serverDomain lane — seat card indexed by label:pm:seat
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    on Oct 24, 2026
  2. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p2 · domain:server · area:records · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-24T13:57Z

    ⛔ Not a claim, ⛔ not a dispatch.

    • p2 and domain:server stand. The seam is the ObjectQL registry's attachBoundActions, which skips the ActionSchema parse, so lowerRequiresFeature never runs. This is declared but not enforced, on a visibility gate, not on an authorization door.
    • ⛔ No consumer-side tolerance, as the card says. The fix parses package-bound actions as platform actions are parsed, or refuses an unparsed action loudly. It gets a test plus an ablation.

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01JVTd3PprStGa26aez5omLB
    Account: sunhaolin (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-25649-bound-action-schema-parse
    Worktree: objectstack-issue-25649
    Domain: domain:server
    Seat: domain:server#2
    File surface: packages/objectql/src/registry.ts (the code-package action item path that feeds attachBoundActions / boundCodeActions) or the registration site that stores those items unparsed, tests, one changeset. ⛔ Not packages/spec/src/** (lowerRequiresFeature and ActionSchema are used as they are), ⛔ not packages/objectql/src/engine.ts (#25522's split is in flight there) (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: TIER_DEFAULT (dispatch-gates --tier: "no path-derived mandate … the tier stays the PM's per-card judgment call"), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: ObjectQL's schema registry stores a code package's bound action items without the ActionSchema parse, so lowerRequiresFeature never lowers requiresFeature into visible | the parse and the lowering already exist and run for platform-object actions | every deployment that serves a package-bound action with requiresFeature; measured by cloud's os-dev on objectstack-ai/cloud#3210 (change_slug on sys_organization) (triage, 6385089695)
    Thread-read: 6385089695
    Serial constraints cleared: no open PR touches packages/objectql/src/registry.ts (every open PR's file list read this fire). #25522's open slice PR #25636 moves bodies out of engine.ts only; this card stays out of it.
    Premise: re-read on origin/main 9dac9eb8d6: attachBoundActions attaches boundCodeActions() entries by reference from this.metadata.get('action'), with no parse on that path; lowerRequiresFeature lives in packages/spec/src/kernel/public-auth-features.ts as a zod transform.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 25649,
      "status": "done",
      "branch": "claude/issue-25649-bound-action-schema-parse",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25699",
      "session": "session_01JVTd3PprStGa26aez5omLB",
      "premise_still_valid": true,
      "summary": "Premise held on origin/main 702b44f8c3: attachBoundActions attached boundCodeActions() items by reference, and no ActionSchema parse ran on any code-package action. Every such action enters through SchemaRegistry.registerItem (registerApp, nested plugins, metadata facade, the ObjectQL plugin's metadata-service sync). Platform-object actions are parsed at the producer (ObjectSchema.create), not in the registry. Fix: registerItem now stores ActionSchema.parse(item) for type 'action' when a packageId is passed and isCodeArtifactBody holds, running after applyProtection. A body that does not parse is refused with INVALID_METADATA/422; the message names the action and package, and issues[] carries path/message/code. attachBoundActions is unchanged: rung-1 precedence and dedupe by name hold, and the attachment is still the same object rung 2 resolves (toBe pinned). The bare slot (no packageId: sys_metadata hydration, write-through) is not judged. Measured parse effects: requiresFeature and requiresMembershipReach are lowered into visible and both keys deleted; defaults are filled (type, refreshAfter=false); a CEL string shorthand becomes {dialect, source}; undeclared keys are refused (strict); the ADR-0010 envelope (_packageId, _packageVersion, _provenance, _lock) passes through; parse is idempotent. In-repo narrowing scan: 81 top-level actions (app-crm 1, app-todo 8, app-showcase allActions 72) give 0 refused and 0 changed (key-sorted JSON), and no top-level actions collection exists in plugin/service/runtime source. So Clause-2 stays 'no' and the title has no '!'. The refusal reached 7 test fixtures carrying off-spec action bodies. Each was supplied its missing declaration (listed in the PR body); none pinned the removed behaviour.",
      "tests": "New tests in packages/objectql/src/bound-action-foreign-object.test.ts, through ObjectQL.registerApp and protocol.getMetaItem(type object), the read behind GET /api/v1/meta/objects/:name: (1) bound action with requiresFeature multiOrgEnabled is served with visible {cel, \"(record.name != '') && features.multiOrgEnabled != false\"}, no requiresFeature key, equal to the platform object's own action of the same declaration, and toBe the registry item; (2) no own visible gives the gate alone; (3) control: the platform object's own action is served exactly as ObjectSchema.create produced it; (4) refusal: requiresFeature with visible:false gives code INVALID_METADATA, status 422, a message naming change_slug and com.test.binder, issues containing path requiresFeature, and nothing stored or attached. Ablation via scripts/ablation-replace.mjs wrap mode (the test imports ./registry.js from src, so there is no dist leg): anchor 'item = parsePackageAction(item, baseName, packageId) as T;' x1 to x0, blob 0aa44217 to 07883fa6, 'Tests 3 failed | 15 passed (18)' (lowering, gate-alone and refusal red; control green). Restore: blob 0aa44217 == HEAD and git diff HEAD empty. Runs: objectql local full at 108ef87ee5 'Test Files 466 passed (466) / Tests 8704 passed (8704)'; objectql typecheck exit 0 at d1e5a3868b ('check:test-typecheck: OK'); rest full 'Test Files 325 passed (325)' and metadata-protocol full 'Test Files 263 passed | 3 skipped' at d1e5a3868b; runtime local full at 06381ddde3 'Test Files 1 failed | 440 passed (441)', the red being stored-metadata-reader-contexts.pin.test.ts, which was then triaged; final runtime rerun of both triaged files at 89ecd31788 'Test Files 2 passed / Tests 171 passed'; runtime typecheck exit 0 at 89ecd31788; eslint --no-inline-config over the touched files exit 0. Each run's inner exit code was echoed by its wrapper script under the verify lock.",
      "gates": "Local, final head 89ecd31788: dispatch-gates --commands --repo objectstack-ai/objectstack derived 74 families. All 74 ran, exit codes captured before any pipe; all exit 0. check:engine-split-ratio --days 90 first exited 2 (shallow-clone prerequisite) and exited 0 after git fetch --shallow-since=2026-07-19. --ran reconciliation: '74 derived, 74 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). Per-family list: node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0; node scripts/check-adr-0087-registration.mjs --self-test :: exit 0; node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0; node scripts/check-changeset-no-major.mjs --self-test :: exit 0; node scripts/check-ci-filter-parity.mjs :: exit 0; node scripts/check-closing-keyword-parity.mjs :: exit 0; node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0; node scripts/check-comment-mask-adoption.mjs :: exit 0; node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0; node scripts/check-comment-mask-corpus.mjs :: exit 0; node scripts/check-dts-emitted.mjs --self-test :: exit 0; node scripts/check-empty-changeset.mjs --base origin/main :: exit 0; node scripts/check-empty-changeset.mjs --self-test :: exit 0; node scripts/check-engine-split-ratio.mjs --days 90 :: exit 0; node scripts/check-engine-split-ratio.mjs --self-test :: exit 0; node scripts/check-issue-citations.mjs :: exit 0; node scripts/check-keyed-text-bounds.mjs :: exit 0; node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0; node scripts/check-platform-object-tenancy-census.mjs :: exit 0; node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0; node scripts/check-plugin-teardown-shape.mjs :: exit 0; node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0; node scripts/check-position-name-fold-loaders.mjs :: exit 0; node scripts/check-position-name-fold-loaders.mjs --self-test :: exit 0; node scripts/check-registry-log-declared.mjs :: exit 0; node scripts/check-registry-log-declared.mjs --self-test :: exit 0; node scripts/check-rest-log-declared.mjs :: exit 0; node scripts/check-rest-log-declared.mjs --self-test :: exit 0; node scripts/check-rest-log-spy-declared.mjs :: exit 0; node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0; node scripts/check-system-context-census.mjs :: exit 0; node scripts/check-system-context-census.mjs --self-test :: exit 0; node scripts/check-undeclared-dep-imports.mjs :: exit 0; node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0; node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0; node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0; node scripts/docs-audit/check-affected-docs.mjs :: exit 0; node scripts/docs-audit/check-drift-comment.mjs :: exit 0; node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0; node scripts/release-pending-publish.mjs --self-test :: exit 0; pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0; pnpm check:changeset-gate-self-tests :: exit 0; pnpm check:closed-ceremony-text :: exit 0; pnpm check:cross-package-test-inputs :: exit 0; pnpm check:dispatcher-error-vocabulary :: exit 0; pnpm check:doc-authoring :: exit 0; pnpm check:driver-memory-census :: exit 0; pnpm check:dts-closure :: exit 0; pnpm check:dual-build-cjs-loads :: exit 0; pnpm check:durability-log-level :: exit 0; pnpm check:engine-double-contract :: exit 0; pnpm check:error-status-conformance :: exit 0; pnpm check:gitlink-declared :: exit 0; pnpm check:issue-citations :: exit 0; pnpm check:lean-entry-closure :: exit 0; pnpm check:logger-receiver-detach :: exit 0; pnpm check:nul-bytes :: exit 0; pnpm check:objectql-double-limit :: exit 0; pnpm check:objectui-changeset :: exit 0; pnpm check:org-identifier :: exit 0; pnpm check:page-declaration-shape :: exit 0; pnpm check:pm-changeset-deadline-census :: exit 0; pnpm check:published-files :: exit 0; pnpm check:query-options-erasure :: exit 0; pnpm check:refd-timer-probe :: exit 0; pnpm check:route-envelope :: exit 0; pnpm check:slot-lookup :: exit 0; pnpm check:sourcemap-no-sources-content :: exit 0; pnpm check:test-source-alias :: exit 0; pnpm check:tier-file-adoption :: exit 0; pnpm check:type-check-coverage :: exit 0; pnpm check:type-check-debt :: exit 0; pnpm check:watch-hint-literal :: exit 0; pnpm check:where-matcher :: exit 0. CI on PR head 89ecd31788 at report time: every required and lint/typecheck run in_progress (Lint · ESLint, Lint · PM dispatch-gates, Lint · repo & tooling gates, Lint · docs & code guards, Lint · CI, release & engine gates, Type Check · workspace/source gates/consumer gates/debt ledger, Governed Surface Queue Guard, Check Changeset); 'No other open PR may claim the same issue' success; Test Core/Build Core not yet listed. NOT MEASURED: CI conclusions, reason: still running when this report was written.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0",
      "api_writes": "2 relay writes as objectstack-fleet[bot]: (1) pr_create stroke fw-20261024T173851Z-754af7, POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/25699/assignees [sunhaolin], read-back identical (8335 bytes); (2) this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/25649/comments via post-stamped.mjs. git push is not counted.",
      "files_changed": [
        ".changeset/25649-bound-action-schema-parse.md",
        "packages/objectql/src/registry.ts",
        "packages/objectql/src/bound-action-foreign-object.test.ts",
        "packages/objectql/src/bound-action-collision-report.test.ts",
        "packages/objectql/src/engine-nested-plugin-collections.test.ts",
        "packages/objectql/src/package-disable-enforcement.test.ts",
        "packages/objectql/src/registry-package-version-stamp.test.ts",
        "packages/objectql/src/registry.test.ts",
        "packages/runtime/src/domains/mcp-package-object-discovery.test.ts",
        "packages/runtime/src/stored-metadata-reader-contexts.pin.test.ts"
      ],
      "line_budget": "n/a — no skills/** or ratcheted ledger touched; diff 213 insertions, 16 deletions over 10 files",
      "deviations": [
        "File surface widened past the claim: two packages/runtime test fixtures (mcp-package-object-discovery.test.ts, stored-metadata-reader-contexts.pin.test.ts) and five objectql test fixtures, all off-spec action bodies the new refusal reached. Fixture triage by the rule's consumer radius; no runtime source touched.",
        "Merged origin/main (5eeb02c614, which changed packages/spec/src/ui/action.zod.ts) into the branch before opening the PR, then reinstalled and rebuilt the closure.",
        "The first objectql full run used vitest run without --project local, so it also ran the CI-owned repo project; it was superseded by the --project local run quoted in tests.",
        "Ran the full rest and metadata-protocol suites (a git pathspec glob produced empty file lists). This is a superset of the scoped sweep.",
        "Deepened the shared clone (git fetch --shallow-since=2026-07-19 origin main) so check:engine-split-ratio could measure; this changes the shallow boundary for every worktree of this clone.",
        "The runtime full local suite was not rerun at the final head; only the two triaged runtime files were rerun there (declared narrowing; CI Test Core runs the full set)."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "class: b · reach: protocol.getMetaItem({type:'object'}) in-process (the read behind GET /api/v1/meta/objects/:name), measured with a throwaway probe: an object registered through registerApp from a raw manifest, carrying its own action {name:'gated', type:'url', requiresFeature:'multiOrgEnabled'}, is served as [{name:'gated',label:'G',type:'url',target:'https://example.com',requiresFeature:'multiOrgEnabled'}], requiresFeature raw and no visible. Not measured over HTTP. · evidence: SchemaRegistry.registerItem only diagnoses an object against ObjectSchema (validate() result discarded, #3903 'deliberately not a gate'), so an object's own actions are stored unparsed unless the producer parsed them (ObjectSchema.create / strict defineStack). Seam: spec:ActionSchema requiresFeature lowering (lowerRequiresFeature) → runtime:SchemaRegistry.registerObject / registerItem('object') → served by resolveObject/finishServedObject. Same declared-not-enforced family as #25649; the seat decides whether this is a sibling card or folds into a family close-out card · dedupe words: requiresFeature object actions raw manifest unparsed, registerObject ActionSchema lowering, strict false stack object action visible",
        "carrier: none (承接者:无) · noted in PR Acceptance notes only: a tenant overlay row of a packaged action is registered in the bare slot and is not parsed by this change; whether the metadata save door stores the parsed body or the authored one was not measured"
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #25699 → e44a1ffc4 · domain:server seat 2 · sunhaolin · session_01JVTd3PprStGa26aez5omLB · 2026-10-25T03:59Z

    reading result
    PR #25699 object merged: true, merge commit e44a1ffc4d6b743a2f93c0950add57a7827d2dea, merged 2026-10-25T03:59:16Z through the merge queue
    origin/main e44a1ffc4 is an ancestor of origin/main (e44a1ffc4d), 1 parent(s)
    queue branch no gh-readonly-queue/main/* head for PR 25699 remains
    • State: closed completed by Fixes #25649 on merge. pm:dispatched stripped in this act.
    • Delivered: a code package's action is stored as its ActionSchema parse, so a bound action's requiresFeature / requiresMembershipReach is lowered into visible as on platform actions; an action that does not parse is refused at registration (422 INVALID_METADATA). Declared Clause-②: no (narrowing), patch, BREAKING; the CONTRACT_REVIEW_TIER review was the director seat's (record 6388028905, PASS).
    • Carried: an object's own actions registered from a raw manifest stay unparsed (the dev's boundary note); a sibling card is filed from this seat's sign-off.
    • Dev: a local subagent; no cloud session to archive.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions