Skip to content

Commit 99fc3a8

Browse files
committed
docs(deployment): the install modes page — installModes, what a managed install keeps, and what an unmanaged copy keeps and drops
A new authoring page beside Publish, Versioning & Preview: how a package declares installModes, how the installer picks a mode, what a managed install keeps (its locks bind), and what an unmanaged copy keeps and drops (no lock in either spelling; strippedLocks and the CLI's Locks removed lines name each one). Plus the runtime changeset for the refusal text, and the regenerated hand-written docs list. Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
1 parent a0e117c commit 99fc3a8

4 files changed

Lines changed: 239 additions & 0 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
The `/packages` refusals of a read-only package name the customisation that works today, instead of the retired organization overlay (ADR-0131 D6)
6+
7+
Clause-②: no
8+
9+
`DELETE`, `PATCH /disable`, `POST /publish` and `POST /revert` on a package provided by code or an installed app answer `422 WRITABLE_PACKAGE_REQUIRED`, and `POST /packages/:id/duplicate` on one answers `422 DUPLICATE_SOURCE_NOT_A_BASE`. Both refusals told the caller to customise the package "in place, with an ADR-0005 org overlay". ADR-0131 D6 retired the per-organization overlay axis: no metadata write lands organization-scoped any more, and the protocol refuses one that names an organization with `403 NOT_OVERRIDABLE`. The remedy named a mechanism that no longer exists.
10+
11+
Both refusals now name the customisation that works: customise what the package provides beside it, as environment-level state. Saving an item under its own name overlays it where the item's type takes an environment overlay and the package's lock does not refuse it. Any other save is refused, and that refusal names the route the item's type has, or the lock. The duplicate refusal also names the editable copy of a whole package: install it unmanaged instead (`installMode: 'unmanaged'`), where its manifest's `installModes` permits that.
12+
13+
Only the message text changes. Each refusal's `code`, status, `details`, condition and order are unchanged.
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
---
2+
title: Install modes — managed or an unmanaged copy
3+
navTitle: Install Modes
4+
description: Declare which install modes a package permits with installModes, then pick one at install time. What a managed install keeps, and what an unmanaged copy keeps and drops, the vendor's protection locks included.
5+
---
6+
7+
A package reaches a running runtime in one of two **install modes**
8+
([ADR-0131](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0131-total-organization-ownership-no-null-organization-id.md)
9+
D6). The package author declares which modes it permits; whoever installs it
10+
picks one of those.
11+
12+
| | **managed** (the default) | **unmanaged** |
13+
|:---|:---|:---|
14+
| What is installed | the package, registered as code | a copy of every metadata item, written once into the environment |
15+
| Who is the authority | the package | this environment: the copy is its own metadata |
16+
| Editing | nothing it ships is edited in place; you customise beside it | every copied item is edited like anything authored in Studio |
17+
| Later versions | install the next version | the copy follows no upgrade |
18+
| The vendor's protection locks | kept and enforced | not copied; the install lists each lock it drops |
19+
| Tenancy posture | any | refused where the deployment walls organizations (`group`, `isolated`) |
20+
21+
---
22+
23+
## Declare the modes a package permits
24+
25+
The manifest key is `installModes`:
26+
27+
```ts
28+
import { defineStack } from '@objectstack/spec';
29+
30+
export default defineStack({
31+
manifest: {
32+
id: 'com.acme.crm',
33+
namespace: 'crm',
34+
version: '1.0.0',
35+
type: 'app',
36+
name: 'Acme CRM',
37+
installModes: ['managed', 'unmanaged'],
38+
},
39+
// objects, views, dashboards, …
40+
});
41+
```
42+
43+
- **Leave it out and the package permits `managed` only.** The key has no
44+
parse-time default: absent means `['managed']`, and nothing is stamped into
45+
the stored manifest.
46+
- **Name at least one mode.** An empty list is refused; omit the key instead.
47+
A value other than `managed` or `unmanaged` is refused too.
48+
- **`unmanaged` alone is legal.** It suits a starter app a customer takes over
49+
wholesale. A deployment that walls organizations cannot install such a
50+
package at all.
51+
52+
### Permitting `unmanaged` is checked when you build
53+
54+
An unmanaged install copies every item through the same save door a Studio edit
55+
takes, or copies nothing; there is no partial copy. So `defineStack`, and with
56+
it `os validate` and `os build`, refuses a package that permits `unmanaged`
57+
while it carries something that door cannot write as environment metadata. The
58+
refusal is `STACK_UNMANAGED_INSTALL_UNCOPYABLE`, with one finding per key:
59+
60+
- items of a metadata type the metadata-type registry declares
61+
runtime-uncreatable (`allowRuntimeCreate: false`);
62+
- code: `functions`, `plugins` and the other keys that carry or describe code;
63+
- extensions of another package's items: `objectExtensions`,
64+
`picklistExtensions`, `navigationContributions`, `contributes`;
65+
- `packages`, which describes an artifact of several packages, while one
66+
unmanaged install copies one package.
67+
68+
The fix is to stop permitting `unmanaged`, or to move what cannot be copied
69+
out of the package. The install door judges the same body with the same
70+
classification, so a package that passes `os validate` is not refused there
71+
for a key you were never told about.
72+
73+
---
74+
75+
## Pick the mode at install time
76+
77+
```bash
78+
# managed, the default
79+
os package install ./dist/objectstack.json
80+
81+
# an unmanaged copy, from a local artifact or from the runtime's catalog
82+
os package install ./dist/objectstack.json --install-mode unmanaged
83+
os package install com.acme.crm --version 1.2.0 --install-mode unmanaged
84+
```
85+
86+
`--install-mode` sets the `installMode` field of
87+
`POST /api/v1/marketplace/install-local`; without it no field is sent and the
88+
runtime installs managed. The door judges the mode before it writes anything:
89+
90+
| The request | The answer |
91+
|:---|:---|
92+
| A mode the package's `installModes` does not permit | `422 INSTALL_MODE_NOT_PERMITTED`, naming what it permits |
93+
| `unmanaged` on a deployment whose requested tenancy posture walls organizations (`group`, `isolated`) | `409 UNMANAGED_INSTALL_WALLED_POSTURE`, naming the posture |
94+
| `managed`, while an unmanaged copy of the package holds its namespace | `409 RESOURCE_CONFLICT`, naming the copy's base |
95+
96+
A runtime that predates install modes ignores the field and installs the
97+
package managed. If you asked for `unmanaged`, the CLI says so and exits `1`.
98+
The install door itself, credentials included, is described under
99+
[`os package install`](/docs/deployment/cli#os-package-install).
100+
101+
---
102+
103+
## What a managed install keeps
104+
105+
The package is registered as code and stays the authority. Its install is
106+
recorded on the runtime host, so it is registered again at every boot, and a
107+
later version is installed over it.
108+
109+
**Nothing it ships is edited in place.** What a customer changes lives beside
110+
the package, as environment-level state:
111+
112+
- a `view`, `dashboard`, `report`, `translation` or `email_template` is
113+
overlaid by saving one under the same name, and removing the overlay brings
114+
the package's own back;
115+
- a flow is switched off, or cloned under a new name and changed there;
116+
- a save onto any other item the package ships is refused, and the refusal
117+
names the route its type has.
118+
119+
**The vendor's protection locks hold.** A package locks an item in either
120+
[ADR-0010](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0010-metadata-protection-model.md)
121+
spelling: the `_lock` envelope (`_lock`, `_lockReason`, `_lockDocsUrl`), or the
122+
author-facing `protection: { lock, reason, docsUrl }` block, which the install
123+
translates into the envelope with `_lockSource: 'package'`. The item is served
124+
locked with the vendor's reason, and the write doors refuse what the lock
125+
forbids with `403 ITEM_LOCKED`: `no-overlay` refuses a save, `no-delete` a
126+
delete, and `full` both.
127+
128+
---
129+
130+
## What an unmanaged copy keeps, and what it drops
131+
132+
The package's metadata is copied **once** into a new writable base,
133+
`<package id>-copy`, which keeps the package's namespace. Every item is written
134+
through the save door a Studio edit takes, environment-wide, and is served as
135+
environment-authored metadata: the next save edits it.
136+
137+
- **Translations are copied.** Each locale of each bundle becomes one
138+
`translation` item named `<namespace>_<locale>`, the locale lower-cased with
139+
its hyphens underscored (`crm_zh_cn`).
140+
- **Sample data is not loaded.** When the package declares any, the install
141+
answer says so, with `seeded.mode: 'skipped'`.
142+
- **Nothing is registered under the package's own id.** No install record,
143+
nothing re-registered at boot, nothing upgraded. A managed install of the
144+
package is refused while the copy holds its namespace, and a second unmanaged
145+
install is refused because the base already exists.
146+
- **All or nothing.** Every refusal is judged before the base exists, including
147+
a name the environment or a managed package already holds. A write that fails
148+
deletes the base, so there is never a half-copied app.
149+
150+
### The copy keeps no lock, in either spelling
151+
152+
The package permitted `unmanaged` through `installModes`, so its vendor already
153+
chose to let environments take editable copies. Once the package is no longer
154+
the authority, a lock whose source is the package has nothing to stand on. So
155+
every copied item is written:
156+
157+
- without `_lock`, `_lockReason`, `_lockSource` and `_lockDocsUrl`;
158+
- without its `protection` block, on a type whose schema declares one.
159+
160+
Every copied item is then served unlocked, and its next save is accepted.
161+
162+
Two cases are left to the save door exactly as before, and either refuses the
163+
whole install, which rolls back:
164+
165+
- a lock value the declarations refuse, such as `_lock: 'locked'`, a reason
166+
that is not a string, or a malformed `protection` block, is not stripped: the
167+
save door answers `422 INVALID_METADATA`;
168+
- a `protection` block on a type whose schema does not declare one (`action`,
169+
`hook`, `page`) is left in place, and the save door refuses it as an
170+
unrecognised key.
171+
172+
### The install lists each lock it drops
173+
174+
The install answer carries `strippedLocks`: one entry per dropped lock that
175+
binds (`no-overlay`, `no-delete` or `full`), in the order the items were
176+
written.
177+
178+
```json
179+
"strippedLocks": [
180+
{ "type": "dashboard", "name": "pipeline_dashboard", "lock": "full", "reason": "The pipeline figures feed the board pack — edit the source package instead." },
181+
{ "type": "app", "name": "crm_app", "lock": "no-delete" }
182+
]
183+
```
184+
185+
- `reason` is the vendor's text, verbatim, and is absent when the package gave
186+
none.
187+
- An item that carries both spellings is listed once, with the lock a managed
188+
install would bind: the `protection` block's.
189+
- A `none` lock declares nothing and is not listed.
190+
- The list is always present, and empty when the package locked nothing.
191+
192+
`os package install --install-mode unmanaged` prints the list under
193+
`Locks removed`, one line per lock as `<type>.<name>: <lock> — <reason>`, the
194+
dash and reason left off when there is none:
195+
196+
```text
197+
Locks removed: 2 — the package locked these items; the copy is editable
198+
dashboard.pipeline_dashboard: full — The pipeline figures feed the board pack — edit the source package instead.
199+
app.crm_app: no-delete
200+
```
201+
202+
Read it before you edit: it is why the vendor locked each item.
203+
204+
---
205+
206+
## Choosing what to permit
207+
208+
- **Permit `unmanaged`** when the package is a starting point a customer should
209+
be able to take over and change freely.
210+
- **Keep it managed only** when you must keep shipping upgrades to it, or when
211+
some item must never be edited by any customer. An unmanaged copy drops every
212+
lock, so a lock is no reason to permit the copy; it is a reason to withhold
213+
it.
214+
215+
## Related
216+
217+
- [Publish, Versioning & Preview](/docs/deployment/publish-and-preview): compile,
218+
publish and install a version
219+
- [`os package install`](/docs/deployment/cli#os-package-install): the flag-level
220+
reference
221+
- [Package manifest reference](/docs/references/kernel/manifest)
222+
- [Package lifecycle reference](/docs/references/api/package-lifecycle): the
223+
import's request and answer, `strippedLocks` included
224+
- [Tenancy Modes](/docs/deployment/tenancy-modes)

‎content/docs/deployment/meta.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"seed-tenancy-repair",
99
"single-project-mode",
1010
"publish-and-preview",
11+
"install-modes",
1112
"validating-metadata",
1213
"cli",
1314
"environment-variables",

‎scripts/docs-audit/handwritten-docs.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"content/docs/deployment/cli.mdx",
9595
"content/docs/deployment/environment-variables.mdx",
9696
"content/docs/deployment/index.mdx",
97+
"content/docs/deployment/install-modes.mdx",
9798
"content/docs/deployment/production-readiness.mdx",
9899
"content/docs/deployment/publish-and-preview.mdx",
99100
"content/docs/deployment/seed-tenancy-repair.mdx",

0 commit comments

Comments
 (0)