You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit e2b7d13
Browse filesBrowse the repository at this point in the historyBrowse files
**BREAKING** for authored metadata — `ObjectSchema.fields` refuses a key named `__proto__`, `constructor` or `prototype`, and `AssignmentConfigSchema.assignments` (the `assignment` flow node's variable map) refuses a key named `__proto__` — both refused with a named, located error at parse time, rather than silently accepted and then silently mishandled (objectstack#17852, objectstack#18847).
6
+
7
+
## Why
8
+
9
+
zod's `z.record()` skips a `__proto__` own key entirely, above its own key schema — the record parser's `if (key === "__proto__") continue;` runs before `def.keyType._zod.run`, so no key grammar (a regex, `.refine()`, `.superRefine()`, even a key schema that rejects every string) can ever see that key. A document whose `fields` (or `assignments`) carried a `__proto__` own key — which `JSON.parse` produces routinely — used to parse as SUCCESS with that key silently missing from the output: the validator accepted a document and handed back a *different* document. `os build` writes the release artifact from that returned document, so the failure shape is success, silent, and irreversible into the shipped artifact.
10
+
11
+
Two independent mechanisms close this, one per name class, because they are not reachable the same way:
12
+
13
+
-`__proto__` is refused by a **pre-parse guard** that reads the raw input's own keys before the record ever parses, at both `ObjectSchema.fields` and `AssignmentConfigSchema.assignments`.
14
+
-`constructor` and `prototype` — which, unlike `__proto__`, DO reach the key schema unskipped — are refused by `ObjectSchema.fields`' own key grammar (they were ordinary lowercase words its regex already admitted). They are **not** refused at `AssignmentConfigSchema.assignments`: that slot's key type carries no grammar at all (`z.string().min(1)`), both names are legal flow-VARIABLE names measured to survive parse intact today, and no ruling narrows that slot's accept set for them — only its `__proto__` half moves.
15
+
16
+
Measured: zero authored use of any of the three names as a `fields` key or an `assignments` variable name, across this repo, `examples/` and `objectui`.
17
+
18
+
## Known gap, left open on purpose
19
+
20
+
The guard runs at parse time only. It does not project into the published JSON Schema (`packages/spec/json-schema/**`) — the general gap that closes is tracked separately (objectstack#18670) and stays open after this change.
21
+
22
+
Clause-②: yes (narrowing)
23
+
24
+
<!-- adr-0087: not-required (no-migration-prescription) zero authored use of `__proto__`, `constructor` or `prototype` as a `fields` key or an `assignments` variable name across this repo, examples/ and objectui — nobody has anything to rewrite, so there is no prescription to give. -->
The in-process install primitive honours `enableOnInstall` instead of ignoring it (#19277).
6
+
7
+
`InstallPackageRequestSchema.enableOnInstall` (`kernel/package-registry.zod.ts`) is the request contract of `ObjectStackProtocol.installPackage` / `MetadataProtocol.installPackage`. The implementation read `request.manifest` and `request.settings` and nothing else, so a caller that asked for `enableOnInstall: false` got an ENABLED install — no refusal, no warning, no effect. That is a declared option the runtime did not deliver, which ADR-0049 (enforce-or-remove) and Prime Directive #10 refuse outright. Ruling batch #153 item 5 letter 1 (#18605) kept this declaration as a COPY of the HTTP request key with the same meaning, so the disposition is enforce, not retire.
8
+
9
+
The primitive now applies the same rule the HTTP door applies (maintainer ruling batch #157 item 5 letter C, 「缺省 = 保持,有旗 = 设置」), through the same registry verbs `PATCH /packages/:id/enable` and `PATCH /packages/:id/disable` use:
10
+
11
+
```text
12
+
enableOnInstall: true ⇒ enablePackage — clears a disable, including a boot-seeded one
13
+
enableOnInstall: false ⇒ disablePackage — the row and its `status` both move
14
+
enableOnInstall absent ⇒ no lifecycle call at all; the row the registry returned stands
15
+
```
16
+
17
+
Absent is a third state, not a synonym for `true`: on a FRESH id the registry still lands the package enabled (the declared default), and on an EXISTING row it preserves whatever that row says (#18877). A non-boolean value is read as absent rather than coerced.
18
+
19
+
⚠️ **What this seam does not write, stated rather than implied.** The runtime's durable disabled-package file is keyed by environment (`setPackageDisabled(environmentId, id, disabled)`, `@objectstack/runtime`), and an `InstallPackageRequest` carries no environment, so that record cannot be written from here — the HTTP door owns that half and writes it from the row it returned. `enableOnInstall` through the in-process primitive therefore moves the registry row, which is what every in-process reader serves from, for the life of the process; a caller that needs the choice replayed after a restart goes through the door that owns the durable record.
20
+
21
+
No behaviour changes for any caller on the tree: measured across `packages/**`, `examples/**` and `apps/**`, no existing call site sets the key — the HTTP door deliberately calls `installPackage({ manifest, settings })` and performs the flip itself, and `duplicatePackage` passes `{ manifest }` alone. The change is observable only to a caller that sets the key, which until now got silence.
0 commit comments