Repository navigation
Commit 258f4e6
fix(runtime)!: REST's /actions door refuses an action on an object whose owning package is disabled 404 OBJECT_PACKAGE_DISABLED, before any handler runs (#25305)
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuXquM9v2r8ymXj6TzynfjMF)_
Fixes #25296
Clause-②: no (narrowing)
Metadata-protocol: none
REST's `POST /api/v1/actions/OBJECT/NAME` now asks, before it reads the
action's declaration, whether the object it names is served. An object
whose owning package is disabled is refused `404
OBJECT_PACKAGE_DISABLED`, with the error the protocol raises for it on
the data plane and on `GET /api/v1/meta/object/NAME`, before any gate,
record load or handler. The question is asked through the by-name object
read the data verbs' exposure gate already makes
(`readExposureDeclaration`), so neither the package predicate nor the
refusal is copied.
## The defect, measured on base `5801a4f2c9`
The `/actions` door (`packages/runtime/src/domains/actions.ts`, the one
owner of the route) resolved the action's declaration through
`resolveRouteActionDeclaration`, which reads `ql.getSchema(OBJECT)` and
the registry's `action` items. A disabled package's objects stay
registered there, so the declaration resolved and the door dispatched,
while every other door treats the package as gone.
Real kernels (`ObjectKernel` + `MetadataPlugin` + `ObjectQLPlugin` over
a better-sqlite3 `SqlDriver`), in the two shapes the #25120 rig pins
(`env_local` with `createStandaloneStack`'s options, and no
`environmentId`), driven through the runtime dispatcher's `dispatch()`.
The fixture package, registered through the `manifest` service during
the boot and then disabled (`registry.disablePackage`): one object
carrying a script action `ping` (its handler registered), a declarative
update `mark_done`, a script action `admin_ping` gated by
`requiredPermissions: ['manage_metadata']`, and a top-level declarative
update `bound_done` bound to the object.
| request, as a signed-in caller without `manage_metadata` | base, both
shapes | this PR, both shapes |
| :-- | :-- | :-- |
| `GET /data/OBJECT` (the data plane, unchanged) | `404
OBJECT_PACKAGE_DISABLED` | same |
| `POST /actions/OBJECT/ping` | **`200 { pong: true }`, the handler
ran** | `404 OBJECT_PACKAGE_DISABLED` |
| `POST /actions/OBJECT/ping/r_any` | **`200`, the handler ran** | `404
OBJECT_PACKAGE_DISABLED` |
| `POST /actions/OBJECT/mark_done` with a `recordId` | refused only at
the record load | `404 OBJECT_PACKAGE_DISABLED` |
| `POST /actions/OBJECT/bound_done` with a `recordId` | refused only at
the record load | `404 OBJECT_PACKAGE_DISABLED` |
| `POST /actions/OBJECT/admin_ping` | `403` (the D4 gate read the
declaration) | `404 OBJECT_PACKAGE_DISABLED` |
| `POST /actions/OBJECT/qa_nowhere` | the "no declaration" `404` (no
code) | `404 OBJECT_PACKAGE_DISABLED` |
| the same `ping`, anonymous | `401` | `401` (the anonymous baseline
stays first) |
| the same `ping`, after `registry.enablePackage` | `200` | `200` |
The base column is the pins' main leg below.
## What changed
- **`packages/runtime/src/action-execution.ts`.**
`refuseDisabledPackageObject(deps, requestContext, object, scopeId?)`:
it asks `readExposureDeclaration` (the protocol's by-name object read on
its execution face, which `refuseUnexposedDataAction` already asks) and
rethrows only the protocol's `OBJECT_PACKAGE_DISABLED` refusal
(`isObjectPackageDisabledRefusal`). Any other answer is not its to give:
an absent object is the declaration resolution's to answer, and so is an
unreadable metadata plane (ADR-0110 D3). Not published by `index.ts`.
- **`packages/runtime/src/domains/actions.ts`.** `handleActionsRequest`
asks it once the engine is resolved and the activation write door has
had its turn, before the declaration resolution; a refusal is answered
through `deps.errorFromThrown(err, 404)`, the exit every other domain
catch uses, so the envelope carries the protocol error's own `code` and
`status`. An object-less invocation (`POST /actions/global/NAME`, `POST
/actions//NAME`) names no object and is not asked about.
- **`.changeset/25296-rest-actions-disabled-package.md`.** A `patch` for
`@objectstack/runtime`, with the BREAKING banner and the ADR-0087
disposition `not-required (no-migration-prescription)`; both declaration
lines are in the body.
## Contract
Every input whose answer changes, old to new, on `POST
/api/v1/actions/OBJECT/NAME[/RECORD]` where OBJECT's owning package is
disabled, on every kernel shape:
1. **A script action with a registered handler.** Old: `200`, the
handler's result (its body ran, `isSystem`-elevated as a script body
always is). New: `404 OBJECT_PACKAGE_DISABLED`; the handler is not
called.
2. **A declarative update (embedded, or a top-level action bound to the
object).** Old: refused at the record load. New: `404
OBJECT_PACKAGE_DISABLED` before the record load.
3. **An action gated by `requiredPermissions` the caller lacks.** Old:
`403`. New: `404 OBJECT_PACKAGE_DISABLED`. The data plane also answers
the package refusal ahead of any permission check, and the refusal names
the object, which the data plane already discloses to the same caller.
4. **An action switched off in the activation ledger.** Old: `409
ACTION_DISABLED` (after D4). New: `404 OBJECT_PACKAGE_DISABLED`. Not
pinned separately: the gate sits ahead of the ledger consult by
construction, as it sits ahead of D4 in case 3.
5. **A name nothing declares.** Old: the "no declaration" `404` with its
prescription. New: `404 OBJECT_PACKAGE_DISABLED`.
6. **A `flow` action.** Old: dispatched through the automation service
(or `503` without one). New: `404 OBJECT_PACKAGE_DISABLED`. Not pinned
separately: the rig has no automation service, and the gate runs before
the type dispatch for every type.
Unchanged: the anonymous baseline (`401`) and the `405` stay first; an
enabled package's actions run as before behind every gate (pinned as the
control on both shapes); re-enabling the package restores its actions at
once, because nothing records the refusal (pinned); an object-less
action; the activation door (`POST /actions/_activation/OBJECT/NAME`); a
host whose protocol has no by-name read (no refusal, as before). The
cost is one by-name object read per invocation on an object route, the
read the data verbs' gate already makes per call.
## Pins
- **`packages/runtime/src/domains/actions-disabled-package.test.ts`,
new, 24 cases on real kernels.** Per shape: the ENABLED control (`ping`
answers `200 { pong: true }` and its handler runs once; `mark_done` and
`bound_done` update a record seeded through `POST /data/OBJECT`, `200`;
`admin_ping` answers `403`; an undeclared name answers `404` with no
package code), and the DISABLED package (precondition: `GET
/data/OBJECT` answers `404 OBJECT_PACKAGE_DISABLED`; each of the six
invocations of the table answers `404 OBJECT_PACKAGE_DISABLED` with the
handler never called and the protocol's `getData` / `updateData` and the
engine's `executeAction` never reached; the anonymous caller still
answers `401`; after `enablePackage`, `ping` runs again). A new file,
not the #25120 rig file, which the #25297 card holds.
**Red before, green after.** The directions were predicted before each
run. Both mutated legs ran the pin file under the verify lock on
`99d375e8` (this PR's head); the test imports the runtime's source, so
no `dist/` is involved.
| leg | mutation | result | which failed |
| :-- | :-- | :-- | :-- |
| main | the base tree's `action-execution.ts` and `domains/actions.ts`
(`git restore --source=5801a4f2`, worktree only), restored by `git
checkout HEAD`, `git diff HEAD` empty | 12 failed, 12 passed | the six
refusal cases on both shapes |
| helper | `refuseDisabledPackageObject` swallows the refusal too
(`scripts/ablation-replace.mjs`: anchor 1 to 0, blob `9e202cd4e8a7` to
`0711946f42aa`; restored blob equals HEAD, `git diff HEAD` empty) | 12
failed, 12 passed | the same twelve |
| fix | none | 24 passed | none |
## Local verification
On `99d375e84c`, over `origin/main` `5801a4f2c9`. Every heavy run went
through the verify lock; each line cites its `VERDICT`.
- `pnpm exec turbo run build --filter='@objectstack/dogfood^...'
--filter=@objectstack/driver-turso --concurrency=3`: `VERDICT
command-exit 0`, 66 of 66 tasks (the runtime's closure included;
`driver-turso` because the runtime's test typecheck reads it).
- The pin file (`pnpm --filter @objectstack/runtime exec vitest run
--maxWorkers=2 src/domains/actions-disabled-package.test.ts`): `VERDICT
command-exit 0`, 24 passed.
- `@objectstack/runtime` unit suite (`pnpm --filter @objectstack/runtime
test`): `VERDICT command-exit 0`; Test Files 437 passed (437); Tests
6952 passed, 19 skipped (6971).
- `pnpm --filter @objectstack/runtime typecheck`: `VERDICT command-exit
0` (`tsc --noEmit`, and `check:test-typecheck` OK). A first run before
`driver-turso` was built answered one TS2307 in
`organization-ownership-boot-guard.turso-remote.kernel.test.ts` (a
missing dist, not this diff).
- `eslint --no-inline-config` on the three changed source and test
files: exit 0.
- `node scripts/pm/dispatch-gates.mjs --commands`: 68 families derived,
each run, every one `exit 0`. Two first answered `exit 3`
(prerequisites, not verdicts): `check-plugin-teardown-shape --self-test`
until its pinned fixture commit was fetched, and
`check:dual-build-cjs-loads` until the eight dists it named were built.
`check:type-check-debt` ran under the lock.
- `--ran` reconciliation: `68 derived, 68 run, 0 NOT-MEASURED (a DERIVED
zero — all 68 recorded an exit code and none of them is 3)`.
- The full dogfood battery is owed once on the final head (the `!` and
the `(narrowing)` arm); its counts follow as a PR comment.
## Acceptance notes
- **Measured, not changed here: a disabled package's object-less action
still runs through REST.** A top-level action with no `objectName`,
shipped by the package (`type: 'script'`, its handler registered under
`global`), answered `200` with its handler's result on `POST
/actions/global/NAME` and `POST /actions//NAME` after
`registry.disablePackage`, on both shapes, while `GET
/api/v1/meta/action/NAME` answered `404 RESOURCE_NOT_FOUND`. Scratch
probe over this PR's rig, not committed. The card's shape is the object
route and its `OBJECT_PACKAGE_DISABLED` answer; an object-less action
has no object to refuse, and which answer it owes (the "no declaration"
`404`, as the by-name `/meta` read answers it, is the likely reading) is
the next card's to settle. The #25297 card moves MCP's object-less
actions onto the merged action listing, which omits them.
- **`refuseAmbiguousActionActivation` was not needed.** The card asked
whether this fix needs the activation-ambiguity check to read the served
listing. It does not: the fix reads no collection, and the check serves
the activation door, which this PR leaves alone. Whether that check sees
a package object's actions on an `environmentId` kernel is unchanged and
unmeasured here.
- **The activation door is untouched.** Flipping a disabled package's
action in the activation ledger still works; it writes a ledger row and
runs nothing.
- **Branch name.** This PR's branch is `claude/project-thread-vlqmr4`,
the name the session was given, not `claude/issue-25296-…`; the
Duplicate Fix Guard may say so. The claim (comment 6352124313) names it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_013txgcKJ9EWEZJhQ41e7kZg)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_013txgcKJ9EWEZJhQ41e7kZg
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 882372d commit 258f4e6
4 files changed
Lines changed: 342 additions & 0 deletions
File tree
- .changeset
- packages/runtime/src
- domains
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
382 | 382 | | |
383 | 383 | | |
384 | 384 | | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
385 | 412 | | |
386 | 413 | | |
387 | 414 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
0 commit comments