Skip to content

plugin-audit: object definitions are cached for the process lifetime, so enabling files on an object at runtime still answers 403 FILES_DISABLED on attachments until a restart #24565

Closed

Description

@objectstack-fleet

Filing gate: ① — reproducible product defect with a named location (a finding of class (a) reproducible defect)
Path: access-security.parent-and-organization-write-guards
type: Bug (a declared metadata change — enable.files — does not take effect at runtime; the relay channel sets no issue type, so it is pre-filled here for triage)
Checklist item priority: P1 item, but this is a side finding outside its authored clauses (the item does not fail on it) — no inheritance; severity medium for triage to grade
reach: PUT /api/v1/meta/object/qa_kid adding enable.files:true → 200 and read-back shows it, then POST /api/v1/data/sys_attachment on that object → 403 FILES_DISABLED until the server restarts (measured twice on a live boot at 5b14b7f)
Acting reader: the domain:services lane seat, at dispatch — the fix lands in packages/plugins/plugin-audit (audit-writers.ts object-definition cache)
Dedupe: search_issues "plugin-audit FILES_DISABLED stale object definition cache enable.files restart" (open + closed) → 2 hits, both closed, neither this defect (#22913 audit DDL log level, #5202 upload-session audit rows)

QA-source: #24558 · access-security.parent-and-organization-write-guards · side finding (not an authored clause)

Found by the 18.0 release QA sweep (subject 5b14b7f3) while driving the item above; reproduced twice. Full run evidence: #24558.

What is wrong

plugin-audit caches each object definition the first time it looks one up and never invalidates the cache (audit-writers.ts, getObjectDef → objectDefCache). An object whose enable.files is switched on through the metadata door keeps answering as if files were disabled for every attachment write until the server restarts. The metadata read-back already shows the new value, so nothing tells the author why the write is refused.

Reproduction

  1. Boot the showcase (objectstack dev --seed-admin), sign in as admin.
  2. PUT /api/v1/meta/object/qa_kid — a minimal object with no enable block → 200.
  3. POST /api/v1/data/sys_attachment with parent_object: "qa_kid" (plus a parent id and file fields) → 403 FILES_DISABLED (correct at this point; this lookup populates the cache).
  4. PUT /api/v1/meta/object/qa_kid again with enable: { files: true } → 200; GET /api/v1/meta/object/qa_kid shows enable.files: true.
  5. Repeat step 3 → expected: admitted; actual: still 403 FILES_DISABLED. Restarting the server makes the same request succeed.

Control: an object authored with enable.files: true from the start, and never looked up before, admits the attachment.

Mechanism / fix direction

The cache needs invalidation on object metadata change (the ObjectQL registry already emits object-metadata events — the same channel other consumers subscribe to), or the lookup should resolve per call against the registry.

Severity: medium (functional; a runtime metadata change that silently does not take effect).


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: p2 · bug · domain:services · area:files · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-19T14:08Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch. The 18.0 QA sweep found this at 5b14b7f (or the hotcrm next.5 bump). Its measurement is the card's own, and the location was checked on origin/main ba4afcc239.

    • p2. A declared metadata change (enable.files) does not take effect until a restart. That is a correctness defect with a workaround (restart), and not a release item. It is a side finding with no inheritance.
    • Lane domain:services: packages/plugins/plugin-audit (the object-definition cache in audit-writers.ts). The fix invalidates on the metadata change event rather than holding definitions for the process lifetime. Pin it with a live toggle and no restart.
    • Not a protocol change: a stale cache. What the door accepts is unchanged.

    Generated by Claude Code

  2. added
    area:filesFiles — upload, download, signed URLs, access derived from the parent record
    bugSomething isn't working
    and removed on Oct 19, 2026
  3. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 5 · 2026-10-19T21:27Z
    Session: session_01LR1cD9JqgkeqY7k9rrZH8s
    Account: os-warren (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24565-audit-def-cache-follows-metadata
    Worktree: objectstack-issue-24565
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    File surface: packages/plugins/plugin-audit/src/audit-writers.ts (installAuditWriters' object- and field-definition caches and their invalidation), the plugin-audit file that wires installAuditWriters if the subscription must live there, new test files beside them; a changeset; ⛔ not packages/objectql/**, not packages/metadata/**, not packages/spec/** (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus (the default tier; dispatch-gates --tier printed no path-derived mandate for this surface)
    Clause-②: no
    Responsibility: plugin-audit's definition caches (this lane) | a declared metadata change takes effect without a restart; the door answers what the object declares now (triage 6269217384) | every author who toggles enable.files (or other object metadata the writers read) at runtime (card)
    Thread-read: 6269217384
    Serial constraints cleared: no open PR touches plugin-audit/src/audit-writers.ts (a file scan of every open PR at this claim). #24343 (epic, pm:blocked, nothing in flight per 6266806376) concerns the framework writers' organisation stamping, not these caches. #24341's PR #24623 edits plugin-audit/src/objects/sys-audit-log.object.ts, a different file.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24565,
      "status": "done",
      "branch": "claude/issue-24565-audit-def-cache-follows-metadata",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24676",
      "session": "session_01LR1cD9JqgkeqY7k9rrZH8s (subagent run; the dispatching seat's harness id)",
      "premise_still_valid": true,
      "summary": "installAuditWriters' two process-lifetime copies of engine.getSchema (objectDefCache, fieldDefsCache) are gone: getObjectDef reads the registered definition on every call and getFieldDefs reads .fields off it, so every reader (FILES_DISABLED/FEEDS_DISABLED gates, the ledger mask and internal omission, enable.activities, milestones, titles, labels) follows a metadata-door change with no restart. Reproduced first on a real kernel at f89d29e38: after protocol.saveMetaItem set enable.files true (getSchema read true), the same sys_attachment insert still answered FILES_DISABLED 403. The same read now also decides the ledger's field mask after a runtime field change (case d). The route was dropping the copy, not event invalidation, on the four axes and a measured cost; see deviations.",
      "tests": "New packages/plugins/plugin-audit/src/audit-writers.definition-follows-metadata.integration.test.ts (ObjectKernel + ObjectQLPlugin with its in-house protocol + AuditPlugin + SqliteWasmDriver; every change through protocol.saveMetaItem): (a) toggle ON admits the same attach, (b) toggle OFF refuses again, (c) control authored-from-start admits, (d) a password field added after the first audited write is masked in sys_audit_log.new_value. On 30a57ba9d: 'Test Files 1 passed (1) / Tests 4 passed (4)'. Whole package on 56218f119: 'pnpm --filter @objectstack/plugin-audit exec vitest run --maxWorkers=2' gave 'Test Files 58 passed (58) / Tests 934 passed (934)', lock VERDICT command-exit 0. 'pnpm --filter @objectstack/plugin-audit typecheck' on 30a57ba9d: exit 0, 'check:test-typecheck: OK ... 0 error(s)'. Ablation, on committed 56218f119, via scripts/ablation-replace.mjs in wrap mode with a restore trap; no dist leg, because the subject is imported relatively from src. The held copy was put back into getObjectDef: anchor 1 to 0, blob e5840a3a6b5b to 8bfd1f9e7ff2. Direction: red as expected. (a) 'expected { code: FILES_DISABLED, status: 403 } to deeply equal { admitted: true }', (b) 'expected { admitted: true } to deeply equal { code: FILES_DISABLED, status: 403 }', (d) red at its masking assertion; control (c) stayed green. Restore was proven by the tool (blob after restore == HEAD blob e5840a3a6b5b, git diff HEAD empty). Cost probe, shared box: getSchema 32-44 ns/call against 7 ns for Map.get. Calls per audited insert went 16 to 23 and per update 9 to 25. Mean audited insert was 3301 us before and 3077 us after (300 inserts each).",
      "gates": "Local, all under scripts/pm/os-verify-lock.sh with the exit captured before any pipe: closure build 'turbo run build --filter=@objectstack/plugin-audit^...' VERDICT command-exit 0 (15/15); 'pnpm --filter @objectstack/plugin-audit build' exit 0; package test exit 0; typecheck exit 0. Derived union 'node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack' gave 70 commands, rerun in full on the final head 30a57ba9d: all 70 ':: exit 0'. 'dispatch-gates --ran' exit 0: 'Run reconciliation - 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN'. First pass, on 56218f119: check:query-options-erasure exit 1 (test surface 236 to 237, caused by this PR's 'as any' option bags; fixed in 30a57ba9d, back to 236 and exit 0); check:dual-build-cjs-loads and check:i18n exit 3 (PREREQUISITE NOT MET, no workspace dist), both exit 0 on the final pass. CI on head 30a57ba9d at report time: completed success = Auto Label, Check Documentation Links, Check PR Size, Dogfood Regression Gate, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, filter. Skipped by design = Build Docs, Console Pin Gate, Dogfood Regression Gate (matrix), Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance. in_progress = Build Core, Check Changeset, Governed Surface Queue Guard, Test Core 1-6/6, Type Check (workspace, source gates, consumer gates, debt ledger), Lint (ESLint, repo & tooling, docs & code guards, CI release & engine, PM dispatch-gates, scoped x3), Flag docs affected by code changes. test:repo was not run locally and is left to CI.",
      "mcp_calls": "0 - no MCP GitHub tool called (reads via gh api REST GETs: issue 24565, its comments, pull 24676, check-runs x2)",
      "api_writes": "2 relay strokes, both POST /repos/objectstack-ai/objectstack/dispatches (HTTP 204). Stroke 1 (pr_create) executed as objectstack-fleet[bot]: POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/24676/assignees [os-warren], read back identical (7891 bytes). Stroke 2 (comment): POST /repos/objectstack-ai/objectstack/issues/24565/comments (this report). Plus 3 git pushes of the branch (not REST): empty-branch probe, 56218f119 and 30a57ba9d.",
      "line_budget": "231 (additions 205 + deletions 26, vs merge base f89d29e38)",
      "files_changed": [
        "packages/plugins/plugin-audit/src/audit-writers.ts",
        "packages/plugins/plugin-audit/src/audit-writers.definition-follows-metadata.integration.test.ts",
        ".changeset/24565-audit-def-cache-follows-metadata.md"
      ],
      "deviations": [
        "Route: A3 (drop the copy and read engine.getSchema on every call) rather than the triage's wording 'invalidates on the metadata change event'. I am not silently choosing a side: the dispatch offered A3 as the alternative to be chosen on the four axes. Measured A2: on the door's save only protocol.onMetadataMutation fires, after the registry holds the new definition; metadata:reloaded does NOT fire there; this composition's metadata service has no subscribe. An invalidation therefore follows only the channels it subscribes to, while the other doors (per-item publish, package publish, dev reload with notify false, a peer replica's replay, a delete, a plugin's own registerObject) announce elsewhere or not at all. The registry already memoizes the merged body behind its own invalidation points, so a read costs two map lookups (cost measured in tests). The package already reads per call where the definition decides (declaresActivityStampColumns, currentMilestoneWatched). No TTL. The four-axis reasoning is in the PR body.",
        "The pin drives protocol.saveMetaItem, the producer behind PUT /api/v1/meta/object/:name, one frame below HTTP, on ObjectKernel + ObjectQLPlugin (in-house protocol) + AuditPlugin over SqliteWasmDriver, not driver-sql: plugin-audit's devDependencies carry only driver-sqlite-wasm, and package.json is outside the claim's file surface.",
        "The second reader is pinned with a password field, not a secret one. A secret write needs a CryptoProvider the harness does not wire (measured: 'Cannot persist secret field \"qa_vault.api_key\": no CryptoProvider is registered'). The mask predicate (collectMaskedReadFields over the same getObjectDef read) is shared by both types.",
        "sys_attachment in the test is a stand-in under the real name (service-storage owns the definition; the gate reads only parent_object).",
        "The whole package suite ran on 56218f119. The final commit 30a57ba9d touches only the new test file (option bags typed with satisfies to clear check:query-options-erasure). On 30a57ba9d the new file and typecheck were rerun, and so was the whole 70-command union.",
        "The package test was run as 'pnpm --filter @objectstack/plugin-audit exec vitest run --maxWorkers=2', the package's 'vitest run' script with the worker cap.",
        "The A2/A3 measurements came from a scratch probe file (zz-probe-24565.test.ts) inside the worktree. It was deleted before the first commit and never committed.",
        "Stray file: a backgrounding slip wrote a 5-byte PID file at /pkg-test.pid (filesystem root, outside any repository). The harness's safety check refused my 'rm -f /pkg-test.pid', so it is left for the user to delete."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "class: a · reach: exception: security · held off GitHub for the maintainer (#9387); the seat carries it.",
        "carrier: 承接者:无 · noted in the PR's Acceptance notes, not filed (read-only inference, no door measured): same family, other package. @objectstack/metadata-core createFieldPresenceProbe (field-set cache) and createResolver (organization-column cache) also hold a per-object answer for the process lifetime, on the docblock premise 'object schemas are static after registration'. They are used by the audit, approvals and automation-run writers, and are outside this card's file surface."
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24676 comment 6279377225 · domain:services seat 2 · 2026-10-19T23:10Z · head ce67ae20d9. Landing waits for every check on the head.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 2 · os-warren · session_01LR1cD9JqgkeqY7k9rrZH8s · 2026-10-20T00:02Z

    Reading Value
    PR #24676 merged through the merge queue: auto_merge_enabled 2026-10-19T23:36Z → added_to_merge_queue 2026-10-19T23:38Z → merged 2026-10-20T00:02Z
    origin/main merge commit 5018fc303d is an ancestor (git merge-base --is-ancestor, exit 0)
    This card closed completed by the PR's Fixes #24565; this act clears pm:dispatched and the assignee

    ACCEPT 6279377225 is on the PR. A security note on rows written before this fix is held off GitHub for the maintainer (#9387).


    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

No one assigned

    Labels

    area:filesFiles — upload, download, signed URLs, access derived from the parent recordbugSomething isn't workingdomain:servicespriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions