feat: ADR-0025 plugin package distribution — framework F1–F5#1487
Merged
Conversation
Extend ManifestSchema with the authoritative plugin-distribution shapes
so the cloud control plane can drop its stopgap mirror and import the
canonical schemas.
- PluginPermissionsSchema: structured { services, hooks, network, fs }
(.strict()); ADR-0025 §3.2
- PluginEnginesSchema: { platform, protocol } (protocol-first, §3.10 #3)
- PluginRuntimeSchema: node | sandbox | worker (trust tier, §3.6)
- PluginPackagingSchema: bundled | manifest-deps (§3.3)
- PluginIntegritySchema: Record<path, digest> (§3.2)
ManifestSchema.permissions becomes a backward-compatible union of the
legacy string[] and the structured block; new optional runtime /
packaging / integrity / engines fields added. Legacy engine:{objectstack}
retained and superseded by engines.
Shapes match cloud's stopgap (service-cloud/src/plugin-artifact.ts) so
cloud's swap is a one-line import from @objectstack/spec/kernel.
Verified: tsc clean, 6609 spec tests pass, exports surface in
dist/kernel, runtime parse smoke (legacy + structured + strict reject).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the build half of the plugin distribution pipeline (ADR-0025 §3.4):
- src/utils/osplugin.ts — dependency-free packaging primitives:
- sriDigest(): canonical per-file integrity string `sha256-<base64>`
(matches ADR §3.2's example; the format cloud/runtime align to).
- computeIntegrity(): builds the manifest `integrity` map (excludes the
manifest itself + SIGNATURE; deterministic key order).
- createTar()/createTarGz(): reproducible ustar+gzip writer (mtime pinned
to 0, sorted entries) so any tar reader can unpack the artifact and
identical inputs yield byte-identical blobs.
- src/commands/plugin/build.ts — `os plugin build`:
1. validate objectstack.plugin.json against the canonical ManifestSchema
(@objectstack/spec/kernel — F1), failing fast with zod diagnostics;
2. esbuild-bundle the entry to dist/index.mjs, externalizing
@objectstack/* (and declared deps for packaging: manifest-deps);
3. compute per-file integrity + emit the compiled manifest;
4. pack dist/ (+assets, +package.json/lockfile for manifest-deps,
+SIGNATURE placeholder) into <id>-<version>.osplugin.
Signing is a separate step; this emits an unsigned artifact.
Tests (6, all green; full CLI suite 143 green): SRI vector, integrity
exclusion+ordering, ustar round-trip with valid checksums, gzip validity,
reproducibility, and an end-to-end build that bundles a fixture plugin and
reads the .osplugin back — exercising F1's schema through the CLI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(ADR-0025 F3)
Land the canonical signature half of the plugin distribution pipeline,
byte-for-byte aligned with the cloud control plane's package-signing so
the two never drift.
core/src/security/plugin-artifact-signature.ts — the shared Ed25519
detached-signature contract:
- format `ed25519:<keyId>:<base64url>`; sign/verify via node:crypto
(`sign(null,…)`/`verify(null,…)`), keyId as the rotation handle.
- verifyPublisherSignature(): publisher sig over raw artifact bytes,
keyId-resolved key, mirroring cloud's publish-time policy
(no sig → unverified-but-ok; malformed/unknown-key/mismatch → not ok).
- counterSignPayload()/verifyPlatformSignature(): platform counter-sign
over [package_id, version, blob_key, signature].join("\n") — identical
to cloud's payload.
- verifyPluginArtifact(): runs both trust chains at load time
(ADR §3.7); requirePlatform=false for first-party/local builds.
Exported from @objectstack/core/security.
cli plugin/sign.ts — `os plugin sign <artifact> --key <pem> [--key-id]`:
detached publisher signature over the EXACT artifact bytes (the bytes
cloud verifies at publish), written to a `<artifact>.sig` sidecar, with
a self-verify guard. Completes build → sign → publish.
plugin-loader.ts: replace the placeholder verifyPluginSignature with an
honest check — artifact-bytes/counter-sign verification belongs at
materialize time (no artifact bytes exist at loadPlugin()), so the loader
now validates signature well-formedness via parseSignature and fails fast
on a malformed value, pointing at verifyPluginArtifact for the real chains.
Verified: core 269 tests (incl. 14 signature: format, determinism, tamper,
cloud-contract alignment, publisher policy, counter-sign, combined chains,
KeyObject), cli 138 (incl. build→sign→verify e2e against the exact bytes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bridge the cloud control plane's persisted consent into runtime
enforcement. `PluginPermissionEnforcer.registerGrantedPermissions()` and
`buildPermissionsFromGrants()` turn the structured grant set cloud writes
to `sys_package_installation.granted_permissions`
(`{ services, hooks, network, fs }`, ADR §3.2) into the runtime
`PluginPermissions` bag that `SecurePluginContext` checks.
Matching: exact value, glob (`*` / `**`), or wildcard `*`; network grants
match the request URL's host; `fs` governs read and write; a null/empty
grant set denies everything (least privilege). This enforces what was
GRANTED at install, not merely what the manifest declared — the right
default for distributed third-party plugins.
Verified: 6 new tests (service/hook/fs/network matching, least-privilege
default, enforcer + SecurePluginContext gating); full core suite 275 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend UploadArtifactInput/Result so the IPackageService upload path
carries code-bearing `.osplugin` plugins alongside metadata packages,
aligned with the cloud control plane's publish flow:
- Input: `kind` ('metadata' | 'plugin'), detached `signature`
(`ed25519:<keyId>:<base64url>`), `expectedChecksum` (artifact sha256).
- Result: `versionId`, `listingStatus` (e.g. pending_review), and
`signatureVerified`.
All additive + optional — metadata packages are unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the framework-side prerequisites (F1–F5) that unblock the cloud control plane's plugin distribution work (cloud PD0–PD3 are already merged against a stopgap mirror of these shapes). Shapes/format/payloads are aligned byte-for-byte with cloud so the cloud swap is a one-line import.
Commits
feat(spec)— structured plugin manifest schema.ManifestSchemagains structuredpermissions {services,hooks,network,fs}(backward-compatible union with legacystring[]),runtime(node|sandbox|worker),packaging(bundled|manifest-deps),integrity, andengines {platform,protocol}(protocol-first); legacyengineretained. New schemas/types exported from@objectstack/spec/kernel— the authoritative shapes cloud mirrors.feat(cli)—os plugin build+.osplugin. esbuild bundle (externalizing@objectstack/*), per-fileintegrityassha256-<base64>(SRI) — the canonical format the runtime/cloud verify — and a dependency-free, reproducible ustar+gzip writer. Validates the manifest against the F1 schema.feat(core,cli)— Ed25519 signature contract +os plugin sign.core/security/plugin-artifact-signature.tsmirrors cloud'spackage-signing.tsexactly:ed25519:<keyId>:<base64url>, publisher sig over artifact bytes, platform counter-sign over[package_id,version,blob_key,signature].join("\n"), combinedverifyPluginArtifact(ADR §3.7).os plugin signemits a detached sidecar over the exact bytes cloud verifies.feat(core)— enforce install-timegranted_permissions.registerGrantedPermissions()/buildPermissionsFromGrants()turn cloud's persisted consent set into the runtimePluginPermissionsthatSecurePluginContextchecks (least-privilege default).feat(spec)— plugin fields on theuploadArtifactcontract (kind,signature,expectedChecksum; resultversionId/listingStatus/signatureVerified). Additive + optional.Verification
Landing in cloud (after merge)
pnpm bump-framework→ delete cloud'sSTOPGAPmirrors → import the F1 schemas from@objectstack/spec/kerneland re-export the shared F3 contract from@objectstack/core/security.🤖 Generated with Claude Code