Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/27123-integrity-reverified-at-unpack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@objectstack/spec': patch
'@objectstack/core': patch
---

docs(spec, core): the plugin manifest's `integrity` map is described as re-verified at unpack by `os plugin install`, and the platform-object registry lists `sys_trusted_key` (#27123)

Clause-②: no — no authorable key or accepted value changes. `PLATFORM_OBJECTS_BY_PACKAGE['platform-objects']` gains the `sys_trusted_key` row its maintenance contract requires for an object `@objectstack/platform-objects` now registers (a list of platform-object names, not an authorable surface); the rest is prose: the TSDoc of `PluginIntegritySchema` and the manifest's `integrity` field, and comments in `@objectstack/core`'s security module, with no `.describe()` text changed
Metadata-protocol: none

The TSDoc said nothing in the repo re-verified the map after publish and that there was no `os plugin install`. `os plugin install` now re-hashes the unpacked files against the map before it installs anything, and refuses the install on a mismatch, so the text says that. An absent map is still a permissive pass. The liveness ledger's `integrity` row moves from `dead` to `live` on that reader.

`sys_trusted_key` joins the `platform-objects` group of `PLATFORM_OBJECTS_BY_PACKAGE`, so `PLATFORM_PROVIDED_OBJECT_NAMES` counts it as platform-provided like its siblings.
25 changes: 25 additions & 0 deletions .changeset/27123-local-plugin-install.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'@objectstack/cli': patch
'@objectstack/runtime': patch
'@objectstack/platform-objects': patch
---

feat(cli, runtime, platform-objects): `os plugin install ./x.osplugin` installs a signed plugin artifact locally, after verifying its signature against the environment's trusted publisher keys and its files against the manifest's `integrity` map (#27123)

Clause-②: no — no key, accepted value or export of `@objectstack/spec` changes; the new object and commands are platform code, not authorable metadata
Metadata-protocol: none

The first slice of ADR-0025's local install loop (§3.5 M1). Before this, a built and signed `.osplugin` had no command that installed it.

- **`os plugin keygen --out PATH`** writes a publisher Ed25519 key pair: `PATH.key.pem` (private, mode `0600`, stays with the publisher) and `PATH.pub.pem`, and prints the key id and the public key's fingerprint (`sha256:` + hex of the SPKI DER bytes). It never overwrites a key file without `--force`.
- **`os plugin trust add PUB.pem --key-id ID`**, **`trust list`**, **`trust revoke ID`** manage the environment's trusted keys in the new deployment-level object `sys_trusted_key` (registered by `PlatformObjectsPlugin`, beside `sys_secret`; no tenant column; `manage_platform_settings`; the API offers reads only). The writes run in system context and are recorded by the audit mirror. Only a public key is accepted: a PEM holding a private key, or a key that is not Ed25519, is refused. One key id names one key and one key carries one key id. A revoked row stays, and neither its key id nor its key is trusted again.
- **`os plugin install ARTIFACT`** reads the archive and refuses, by name and with exit status 1, before anything is written:
- an entry that could land outside the install directory, two entries that differ only in letter case, or a file that is also the directory of another entry (`artifact_entries_refused`);
- a file whose digest is not the one the manifest's `integrity` map declares, a declared file that is missing, or a packaged file the map does not name (`integrity_mismatch`);
- a missing signature (`signature_missing`), one that is there but cannot be read (`signature_unreadable`), a malformed one (`signature_malformed`), a key id the environment does not trust (`signature_key_untrusted`) or has revoked (`signature_key_revoked`), a trusted-key row whose fingerprint is not its key's (`trusted_key_invalid`), and a signature that does not verify over the artifact's bytes (`signature_invalid`);
- an installations file it cannot read (`record_unreadable`), which a write would clobber, and an install directory that already holds other bytes no installation of this project put there (`install_dir_taken`): two projects sharing one `OS_HOME` and environment share the directory, and one does not overwrite the other's install.

It then unpacks the artifact to `<OS_HOME>/plugins/<env>/<id>/<version>/`, keeping the original `artifact.osplugin` and `artifact.osplugin.sig` beside the files, and writes an installation record (the version, the key id and fingerprint it verified against) to `<OS_HOME>/plugin-installations/`, keyed by environment and project like the package disable-state store. A swap that fails puts the earlier install of the same version back. `--trust-unsigned` installs an artifact that has no signature, with a warning on stderr; it never makes a present signature that fails verification pass. `--json` prints one document.
- **Not yet.** `os serve` does not load installed plugins: the boot-time loader is the next slice of the same loop.

`@objectstack/runtime` exports the two stores both sides read: `addTrustedKey`, `listTrustedKeys`, `revokeTrustedKey`, `resolveTrustedKey`, `readPublisherPublicKey`, `publicKeyFingerprint`, and `pluginInstallDir`, `loadPluginInstallations`, `recordPluginInstallation`.
38 changes: 23 additions & 15 deletions content/docs/protocol/kernel/lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ partially implemented in the control plane and exposed only on hosted plans.
Treat those command snippets as design intent: the `os` CLI has no `upgrade`
command, so no `objectstack upgrade …` line on this page is runnable.

**Code-plugin installation is not implemented.** The `os plugin` group is
`build`, `sign` and `publish` only; there is no `os plugin install`, `enable`
or `test`. [ADR-0025](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0025-plugin-package-distribution.md)
records the install half (download, verify, materialize, load) as not yet
implemented; see [Plugin Installation](#plugin-installation) for what an author
does today.
**Code-plugin installation is local and partial.** `os plugin install
./x.osplugin` verifies a local artifact's signature against the environment's
trusted keys and its per-file integrity, then unpacks it; nothing loads an
installed plugin at boot yet, and there is no `os plugin enable` or `test`.
[ADR-0025](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0025-plugin-package-distribution.md)
§3.5 M1 records the loader as the next step; see
[Plugin Installation](#plugin-installation) for what an author does today.
</Callout>

ObjectStack manages the complete **lifecycle** of the platform runtime—from initial boot through schema evolution and graceful shutdown, and describes the design intent for plugin installation, upgrades, and rollbacks. The boot sequence is implemented as described; the installation and upgrade flows are design intent (see the callout above).
Expand Down Expand Up @@ -195,11 +196,15 @@ class AppPlugin implements Plugin {
## Plugin Installation

<Callout type="warn">
**Not implemented.** Installing a code plugin is a design that
**Partly implemented.** Installing a code plugin is a design that
[ADR-0025](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0025-plugin-package-distribution.md)
records but the platform does not yet ship: the install half (download,
verify, materialize, load) has no command and no runtime path. The sections
below up to *Upgrades* describe that design, not a command you can run. See
records and the platform ships only in part: `os plugin install ./x.osplugin`
verifies a local artifact (its signature against the environment's trusted
keys, its files against the manifest's `integrity` map), unpacks it and
records the installation, writing nothing until every check passes. Download
from the cloud, loading an installed plugin at boot, and the install hooks and
registry below have no command and no runtime path. The sections below up to
*Upgrades* describe that design, not a command you can run. See
[What an author does today](#what-an-author-does-today).
</Callout>

Expand Down Expand Up @@ -369,7 +374,9 @@ transitions (this page, above), not code hooks; boot-time code goes in

### What an author does today

There is no install command for a code plugin. What exists, per the
The one install command for a code plugin is `os plugin install`, for a
signed artifact on local disk, and nothing loads what it installs yet. What
exists, per the
[CLI reference](https://github.com/objectstack-ai/objectstack/blob/main/packages/cli/README.md):

- **Runtime plugins** declared in `objectstack.config.ts` `plugins` are loaded
Expand All @@ -381,9 +388,10 @@ There is no install command for a code plugin. What exists, per the
publishing with `os package publish … --env <env-id> --install`.
- **Code-bearing plugins** (a directory carrying an `objectstack.plugin.json`
manifest) are packaged and shipped with `os plugin build`, `os plugin sign`
and `os plugin publish`. The group stops there: ADR-0025 records the install
half as not yet implemented, so a published `.osplugin` has no `os` command
that installs it.
and `os plugin publish`. A signed `.osplugin` on local disk is installed with
`os plugin install` once its publisher key is trusted (`os plugin keygen`,
`os plugin trust add`); `os serve` does not load installed plugins yet, and a
published `.osplugin` has no `os` command that fetches it from the cloud.

See [Plugin System](/docs/plugins) for how kernel plugins are created and
registered.
Expand Down Expand Up @@ -912,7 +920,7 @@ Plugin authors must document breaking changes in CHANGELOG.md.

ObjectStack lifecycle management provides:
- **Deterministic Boot:** 7-phase boot sequence with clear error handling
- **Atomic Installations (design):** ADR-0025 calls for transactional, all-or-nothing plugin installs; code-plugin installation is not implemented yet
- **Atomic Installations (design):** ADR-0025 calls for transactional, all-or-nothing plugin installs; `os plugin install` verifies a local artifact in full before it writes anything, but nothing loads an installed plugin at boot yet
- **Zero-Downtime Upgrades:** Blue-green and rolling strategies for production
- **Automatic Rollback:** Failed upgrades auto-revert to previous version
- **Health Monitoring:** Built-in health checks validate system state
Expand Down
7 changes: 5 additions & 2 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,18 @@ in there with `--email` / `--password` (env `OS_RUNTIME_EMAIL` /

Runtime plugins (declared in `objectstack.config.ts` `plugins`) are loaded automatically by `os serve` / `os dev`. Runtime plugins are bundled into the build artifact. To distribute a build, publish it as a package with `os package publish` (see [Cloud — publish & install](#cloud--publish--install)); the `os environments bind <id> --artifact dist/objectstack.json` path still binds an artifact directly into an environment without going through the package registry.

A code-bearing plugin — a directory carrying an `objectstack.plugin.json` manifest — is packaged and shipped through the `os plugin` command group (ADR-0025 §3.4, build → sign → publish):
A code-bearing plugin — a directory carrying an `objectstack.plugin.json` manifest — is packaged and shipped through the `os plugin` command group (ADR-0025 §3.4, build → sign → publish), and installed locally from a signed artifact (ADR-0025 §3.5 M1, keygen → trust → install):

| Command | Description |
|---------|-------------|
| `os plugin build [dir]` | Compile a plugin into a signed-ready `.osplugin` artifact (`--entry`, `--out`, `--minify`) |
| `os plugin sign <artifact> --key <pem>` | Sign a built `.osplugin` with a publisher Ed25519 key, writing a detached `<artifact>.sig` |
| `os plugin publish [artifact]` | Publish a signed `.osplugin` to ObjectOS Cloud |
| `os plugin keygen --out <path>` | Generate a publisher Ed25519 key pair: `<path>.key.pem` (private, stays with you) and `<path>.pub.pem` |
| `os plugin trust add <pub.pem> --key-id <id>` | Trust a publisher public key in this environment's database (`sys_trusted_key`); `trust list` shows the keys, `trust revoke <id>` revokes one |
| `os plugin install <artifact>` | Verify a local `.osplugin`'s detached signature against the trusted keys and its per-file `integrity`, then unpack it to `<OS_HOME>/plugins/<env>/<id>/<version>/`; `--trust-unsigned` is the explicit dev override for an unsigned artifact |

The group has no `install`: ADR-0025 records the code-plugin install half (download, verify, materialize, load) as not yet implemented. `os plugin` (singular) is unrelated to `os plugins` (plural), oclif's plugin manager, which this package does not ship — see [`os plugins` and `os help`](#os-plugins-and-os-help-not-commands).
`os plugin install` refuses, by name, an unknown or revoked key, a signature over other bytes, a missing signature and an integrity mismatch. `os serve` does not load installed plugins yet: loading them at boot is the next step of ADR-0025's M1 install loop. `os plugin` (singular) is unrelated to `os plugins` (plural), oclif's plugin manager, which this package does not ship — see [`os plugins` and `os help`](#os-plugins-and-os-help-not-commands).

### Quality

Expand Down
Loading
Loading