diff --git a/.changeset/15932-plugin-security-scan-result-surface-retired.md b/.changeset/15932-plugin-security-scan-result-surface-retired.md new file mode 100644 index 00000000000..f3e068d3567 --- /dev/null +++ b/.changeset/15932-plugin-security-scan-result-surface-retired.md @@ -0,0 +1,98 @@ +--- +'@objectstack/spec': minor +--- + +feat(spec)!: retire the plugin-security scan-result surface — zero consumers after the scanner retirement (#15932) + +**BREAKING** — the plugin-security scan-result family is removed. ADR-0049 +enforce-or-remove; maintainer ruling 2026-09-07 (director seat, decision batch +#65), adopted verbatim 「同意」. + +This is the second half of the scanner retirement — issue 14919, a number since +deleted from the board, landed as PR #15930. That change retired `PluginSecurityScanner`, +the `@objectstack/core` class that shipped as a security control and returned +`status: "passed"` for every plugin it was ever handed. The **schemas** it fed +survived it — and that scanner's type-only import was their only importer of any +kind, so the family went from one type-only importer to **zero consumers** while +staying fully published: 27 authorable rows on `authorable-surface/kernel.json`, +six `api-surface` exports, two authorable defaults and two json-schema manifest +keys, with no `.parse` or `.safeParse` site against either schema anywhere. An +author could write any of it, be accepted, and get nothing. That is the +declared-not-enforced shape, one layer out from the class removed for the same +reason. "Declare an owner to enforce" was refused by name: it would rebuild the +scanner just retired. + +### FROM → TO + +| removed | what to write instead | +| --- | --- | +| `KernelSecurityScanResult`, `KernelSecurityScanResultParsed`, `KernelSecurityScanResultSchema` (exports) | nothing — delete the import. No replacement type exists. | +| `KernelSecurityVulnerability`, `KernelSecurityVulnerabilityParsed`, `KernelSecurityVulnerabilitySchema` (exports) | nothing — delete the import. No replacement type exists. | +| `PluginSecurityManifest.scanResults` | delete the key | +| `PluginSecurityManifest.vulnerabilities` | delete the key | +| `PluginQualityMetrics.securityScan` | delete the key | + +**The one-line fix: delete the keys and every import of the two types.** Plugin +security scanning is not a platform capability and there is no replacement +schema. What the platform does still enforce is unchanged: `permissions` and +`sandbox` on the same `PluginSecurityManifest`, and artifact provenance through +`verifyPluginArtifactIntegrity` and the plugin signature verifier — which tell +you an artifact is the one its publisher signed, and never that it is safe. For +dependency vulnerabilities use the tools built for it against your own project +(`npm audit` / `pnpm audit`, Dependabot, the GitHub Advisory Database, OSV), and +treat an unaudited third-party plugin as untrusted code. A publisher who used +`scanResults` to advertise diligence keeps the surviving `securityContact` and +`vulnerabilityDisclosure` blocks, which are contact terms rather than a verdict. + +⚠️ Runtime behaviour is deliberately **unchanged**. Nothing ever read any of +these keys, so deleting one removes no check that was running. A consumer that +gated on `securityScan.passed === true` was gating on nothing — the remediation +is to audit with a real tool, not to find a replacement key. + +### The retirement kit + +- The two **defs** leave the build whole — `RETIRED_DEFS_BY_MAJOR[18]` + (`kernel/KernelSecurityScanResult`, `kernel/KernelSecurityVulnerability`) — + because nothing parses them, so there is no author a tombstone could reach. +- The three **authorable keys** are `retiredKey()` tombstones registered in + `RETIRED_KEYS_BY_MAJOR[18]`. Neither carrying shape is `.strict()`, so a bare + deletion would strip an authored key in silence (ADR-0104): the tombstone is + audible in both channels — `tsc` (input type `never`) and the parse, which + raises the prescription itself. +- **No D2 conversion.** A plugin security manifest and a plugin registry entry + are package artifacts a publisher ships, never stack collection members and + never stored `sys_metadata` rows, so the chain has no seam that would see one + — the disposition the sibling `kernel-plugin-security-durations-unit-in-key` + entry already records for this same manifest. The D3 semantic entry + `plugin-security-scan-result-surface-retired` carries the judgement. +- `PluginSecurityManifest.vulnerabilities` is a **forced consequence**, not one + of the four names the ruling listed: it was the last authorable referent of + `KernelSecurityVulnerability` and could not outlive the def. +- **No deprecation window** (maintainer 2026-08-27: 「项目在创业阶段,用户也很少,短期不考虑渐进」). + +⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` +is published, so this is breaking for consumers no download, dependent or source +telemetry was consulted for — exactly as that retirement's own changeset says of its +three exports. That was an input to the ruling, not a reason to soften the removal. + +⛔ **Untouched, and not checked:** the marketplace `'scanning'` status +(`marketplace.zod.ts`). The ruling made it conditional on a producer grep of +`objectstack-ai/cloud`, and that repository was not reachable from the session +that executed this card, so it stays exactly as it is and its absence from this +diff is not evidence about it. + +⚠️ **The two members the ruling paired with it were ALREADY GONE** — measured on +this tree, not assumed. The incident `'malware'` type was a member of +`system/IncidentCategory`, and the whole incident-response family was retired by +#15513 (maintainer ruling 2026-09-05 — two days *before* the 2026-09-07 ruling +that made it conditional). `marketplace-admin.zod.ts` was deleted outright with +the cloud subpath (#16526). Both files return zero tree entries here, against a +lit control where `'scanning'` still returns a live declaration. So the +conditional question is **one** enum member wide, not three. + +`Clause-②: yes (narrowing)` — a published surface is removed: six exports leave +the built `.d.ts` and three authorable keys stop being writable, so the accept +set a consumer writes against narrows. Nothing is widened and nothing is +renamed. Contract-review tier. + + diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index a7a6966df63..47d11b4cc71 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,6 +1,6 @@ --- title: Protocol Reference -description: Every schema published by @objectstack/spec — 1533 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1531 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -25,7 +25,7 @@ counts are sums of the rows they head. Regenerate with | [Data Protocol](/docs/references/data) | 29 | 175 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | | [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. | | [Integration Protocol](/docs/references/integration) | 1 | 24 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. | -| [Kernel Protocol](/docs/references/kernel) | 30 | 159 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. | +| [Kernel Protocol](/docs/references/kernel) | 30 | 157 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. | | [Marketplace Protocol](/docs/references/marketplace) | 4 | 30 | The package & marketplace format — package identity and versions, listing, publish, review, search, install, template manifests. | | [QA Protocol](/docs/references/qa) | 1 | 8 | Declarative test suites — scenarios, steps, actions and assertions. | | [Security Protocol](/docs/references/security) | 5 | 30 | Permission sets, row-level security, sharing rules, tenancy posture. | @@ -33,7 +33,7 @@ counts are sums of the rows they head. Regenerate with | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 34 | 273 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | | [UI Protocol](/docs/references/ui) | 16 | 158 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **195** | **1533** | 14 protocol modules | +| **Total** | **195** | **1531** | 14 protocol modules | --- @@ -197,7 +197,7 @@ The single connector protocol (ADR-0097) — catalog descriptors and provider-bo ## Kernel Protocol -**Source:** `packages/spec/src/kernel/` · **Import:** `@objectstack/spec/kernel` · **30 pages, 159 schemas** +**Source:** `packages/spec/src/kernel/` · **Import:** `@objectstack/spec/kernel` · **30 pages, 157 schemas** Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. @@ -227,7 +227,7 @@ Plugin lifecycle and manifests, capabilities and security, metadata loading, ser | [`plugin-loading.zod.ts`](/docs/references/kernel/plugin-loading) | `PluginLoadingEvent`, `PluginLoadingState` | | [`plugin-registry.zod.ts`](/docs/references/kernel/plugin-registry) | `PluginInstallConfig`, `PluginQualityMetrics`, `PluginRegistryEntry`, `PluginSearchFilters`, `PluginStatistics`, `PluginVendor` | | [`plugin-security.zod.ts`](/docs/references/kernel/plugin-security) | `DependencyGraph`, `DependencyGraphNode`, `PackageDependencyConflict`, `PackageDependencyResolutionResult`, `PluginProvenance`, `PluginTrustScore`, `ResolvedPackageDependency`, `SBOM`, `SBOMEntry`, `SecurityPolicy`, `SecurityScanResult`, `SecurityVulnerability`, `VulnerabilitySeverity` | -| [`plugin-security-advanced.zod.ts`](/docs/references/kernel/plugin-security-advanced) | `KernelSecurityPolicy`, `KernelSecurityScanResult`, `KernelSecurityVulnerability`, `PermissionAction`, `PermissionScope`, `PluginPermission`, `PluginPermissionSet`, `PluginSecurityManifest`, `PluginTrustLevel`, `ResourceType`, `RuntimeConfig`, `SandboxConfig` | +| [`plugin-security-advanced.zod.ts`](/docs/references/kernel/plugin-security-advanced) | `KernelSecurityPolicy`, `PermissionAction`, `PermissionScope`, `PluginPermission`, `PluginPermissionSet`, `PluginSecurityManifest`, `PluginTrustLevel`, `ResourceType`, `RuntimeConfig`, `SandboxConfig` | | [`plugin-structure.zod.ts`](/docs/references/kernel/plugin-structure) | `OpsDomainModule`, `OpsFilePath`, `OpsPluginStructure` | | [`plugin-validator.zod.ts`](/docs/references/kernel/plugin-validator) | `PluginMetadata`, `ValidationError`, `ValidationResult`, `ValidationWarning` | | [`plugin-versioning.zod.ts`](/docs/references/kernel/plugin-versioning) | `BreakingChange`, `CompatibilityLevel`, `CompatibilityMatrixEntry`, `DependencyConflict`, `DeprecationNotice`, `MultiVersionSupport`, `PluginCompatibilityMatrix`, `PluginDependencyResolutionResult`, `PluginVersionMetadata`, `SemanticVersion`, `VersionConstraint` | diff --git a/content/docs/references/kernel/plugin-registry.mdx b/content/docs/references/kernel/plugin-registry.mdx index ff0125e8b62..04b9295cc2e 100644 --- a/content/docs/references/kernel/plugin-registry.mdx +++ b/content/docs/references/kernel/plugin-registry.mdx @@ -51,7 +51,7 @@ const result = PluginInstallConfigSchema.parse(data); | **testCoverage** | `number` | optional | | | **documentationScore** | `number` | optional | | | **codeQuality** | `number` | optional | | -| **securityScan** | `{ lastScanDate?: string; vulnerabilities?: object; passed: boolean }` | optional | | +| **securityScan** | `never` | optional | [REMOVED] `PluginQualityMetrics.securityScan` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — the block declared a last-scan date, per-severity vulnerability counts and a `passed` verdict, and no scanner, registry, installer or UI ever produced or read one, so a plugin could publish `passed: true` with nothing at all behind it. Delete the key. There is no replacement key: plugin security scanning is not a platform capability, and the scan-result family this block mirrored was retired with it. The sibling quality metrics — `testCoverage`, `documentationScore`, `codeQuality` and `conformanceTests` — are unchanged. Audit dependencies with a tool built for it (npm audit, pnpm audit, Dependabot, the GitHub Advisory Database, OSV). | | **conformanceTests** | `{ protocolId: string; passed: boolean; totalTests: integer; passedTests: integer; … }[]` | optional | | ### Nested Shape: `PluginQualityMetrics.conformanceTests[number]` @@ -85,7 +85,7 @@ const result = PluginInstallConfigSchema.parse(data); | **compatibility** | `{ minObjectStackVersion?: string; maxObjectStackVersion?: string; nodeVersion?: string; platforms?: Enum<'linux' \| 'darwin' \| 'win32' \| 'browser'>[] }` | optional | | | **links** | `{ homepage?: string; repository?: string; documentation?: string; bugs?: string; … }` | optional | | | **media** | `{ icon?: string; logo?: string; screenshots?: string[]; video?: string }` | optional | | -| **quality** | `{ testCoverage?: number; documentationScore?: number; codeQuality?: number; securityScan?: object; … }` | optional | | +| **quality** | `{ testCoverage?: number; documentationScore?: number; codeQuality?: number; conformanceTests?: object[] }` | optional | | | **statistics** | `{ downloads: integer; downloadsLastMonth: integer; activeInstallations: integer; ratings?: object; … }` | optional | | | **license** | `string` | optional | SPDX license identifier | | **pricing** | `{ model: Enum<'free' \| 'freemium' \| 'paid' \| 'enterprise'>; price?: number; currency?: string; billingPeriod?: Enum<'one-time' \| 'monthly' \| 'yearly'> }` | optional | | @@ -117,6 +117,16 @@ const result = PluginInstallConfigSchema.parse(data); | **extensionPoints** | `{ id: string; name: string; description?: string; type: Enum<'action' \| 'hook' \| 'widget' \| 'provider' \| 'transformer' \| 'validator' \| 'decorator'>; … }[]` | optional | Points where other plugins can extend this plugin | | **extensions** | `{ targetPluginId: string; extensionPointId: string; implementation: string; priority: integer }[]` | optional | Extensions contributed to other plugins | +### Nested Shape: `PluginRegistryEntry.quality` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **testCoverage** | `number` | optional | | +| **documentationScore** | `number` | optional | | +| **codeQuality** | `number` | optional | | +| **securityScan** | `never` | optional | [REMOVED] `PluginQualityMetrics.securityScan` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — the block declared a last-scan date, per-severity vulnerability counts and a `passed` verdict, and no scanner, registry, installer or UI ever produced or read one, so a plugin could publish `passed: true` with nothing at all behind it. Delete the key. There is no replacement key: plugin security scanning is not a platform capability, and the scan-result family this block mirrored was retired with it. The sibling quality metrics — `testCoverage`, `documentationScore`, `codeQuality` and `conformanceTests` — are unchanged. Audit dependencies with a tool built for it (npm audit, pnpm audit, Dependabot, the GitHub Advisory Database, OSV). | +| **conformanceTests** | `{ protocolId: string; passed: boolean; totalTests: integer; passedTests: integer; … }[]` | optional | | + --- diff --git a/content/docs/references/kernel/plugin-security-advanced.mdx b/content/docs/references/kernel/plugin-security-advanced.mdx index a2818598dce..353fabcd3f9 100644 --- a/content/docs/references/kernel/plugin-security-advanced.mdx +++ b/content/docs/references/kernel/plugin-security-advanced.mdx @@ -14,9 +14,16 @@ Features: - Fine-grained permission system - Resource access control - Sandboxing and isolation -- Security scanning and verification - Runtime security monitoring +⛔ NOT security scanning. The scan-result family — `KernelSecurityScanResult`, +`KernelSecurityVulnerability`, `PluginSecurityManifest.scanResults` and its +sibling list `.vulnerabilities` — was retired under ADR-0049 enforce-or-remove +(#15932), after the runtime scanner that was its last type-only importer went +the same way. That scanner retirement was issue 14919 — a number DELETED from +the board and no longer resolving; its live record is PR #15930, which landed +the removal. Nothing on this platform scans a plugin. + **Source:** `packages/spec/src/kernel/plugin-security-advanced.zod.ts` @@ -24,8 +31,8 @@ Features: ## TypeScript Usage ```typescript -import { KernelSecurityPolicySchema, KernelSecurityScanResultSchema, KernelSecurityVulnerabilitySchema, PermissionActionSchema, PermissionScopeSchema, PluginPermissionSchema, PluginPermissionSetSchema, PluginSecurityManifestSchema, PluginTrustLevelSchema, ResourceTypeSchema, RuntimeConfigSchema, SandboxConfigSchema } from '@objectstack/spec/kernel'; -import type { KernelSecurityPolicy, KernelSecurityScanResult, KernelSecurityVulnerability, PermissionAction, PermissionScope, PluginPermission, PluginPermissionSet, PluginSecurityManifest, PluginTrustLevel, ResourceType, RuntimeConfig, SandboxConfig } from '@objectstack/spec/kernel'; +import { KernelSecurityPolicySchema, PermissionActionSchema, PermissionScopeSchema, PluginPermissionSchema, PluginPermissionSetSchema, PluginSecurityManifestSchema, PluginTrustLevelSchema, ResourceTypeSchema, RuntimeConfigSchema, SandboxConfigSchema } from '@objectstack/spec/kernel'; +import type { KernelSecurityPolicy, PermissionAction, PermissionScope, PluginPermission, PluginPermissionSet, PluginSecurityManifest, PluginTrustLevel, ResourceType, RuntimeConfig, SandboxConfig } from '@objectstack/spec/kernel'; // Validate data const result = KernelSecurityPolicySchema.parse(data); @@ -93,62 +100,6 @@ const result = KernelSecurityPolicySchema.parse(data); | **retention** | `never` | optional | [REMOVED] `KernelSecurityPolicy.auditLog.retention` was renamed to `retentionDays` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `retentionDays`; the value (days) is unchanged. | ---- - -## KernelSecurityScanResult - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **timestamp** | `string` | ✅ | | -| **scanner** | `{ name: string; version: string }` | ✅ | | -| **status** | `Enum<'passed' \| 'failed' \| 'warning'>` | ✅ | | -| **vulnerabilities** | `{ cve?: string; id: string; severity: Enum<'critical' \| 'high' \| 'medium' \| 'low' \| 'info'>; category?: string; … }[]` | optional | | -| **codeIssues** | `{ severity: Enum<'error' \| 'warning' \| 'info'>; type: string; file: string; line?: integer; … }[]` | optional | | -| **dependencyVulnerabilities** | `{ package: string; version: string; vulnerability: object }[]` | optional | | -| **licenseCompliance** | `{ status: Enum<'compliant' \| 'non-compliant' \| 'unknown'>; issues?: object[] }` | optional | | -| **summary** | `{ totalVulnerabilities: integer; criticalCount: integer; highCount: integer; mediumCount: integer; … }` | ✅ | | - -### Nested Shape: `KernelSecurityScanResult.codeIssues[number]` - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | ✅ | | -| **type** | `string` | ✅ | Issue type (e.g., sql-injection, xss) | -| **file** | `string` | ✅ | | -| **line** | `integer` | optional | | -| **message** | `string` | ✅ | | -| **suggestion** | `string` | optional | | - - ---- - -## KernelSecurityVulnerability - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **cve** | `string` | optional | | -| **id** | `string` | ✅ | | -| **severity** | `Enum<'critical' \| 'high' \| 'medium' \| 'low' \| 'info'>` | ✅ | | -| **category** | `string` | optional | | -| **title** | `string` | ✅ | | -| **location** | `string` | optional | | -| **remediation** | `string` | optional | | -| **description** | `string` | ✅ | | -| **affectedVersions** | `string[]` | ✅ | | -| **fixedIn** | `string[]` | optional | | -| **cvssScore** | `number` | optional | | -| **exploitAvailable** | `boolean` | optional (default: `false`) | | -| **patchAvailable** | `boolean` | optional (default: `false`) | | -| **workaround** | `string` | optional | | -| **references** | `string[]` | optional | | -| **discoveredDate** | `string` | optional | | -| **publishedDate** | `string` | optional | | - - --- ## PermissionAction @@ -277,8 +228,8 @@ Scope of permission application | **permissions** | `{ permissions: object[]; groups?: object[]; defaultGrant?: Enum<'prompt' \| 'allow' \| 'deny' \| 'inherit'> }` | ✅ | | | **sandbox** | `{ enabled?: boolean; level?: Enum<'none' \| 'minimal' \| 'standard' \| 'strict' \| 'paranoid'>; runtime?: object; filesystem?: object; … }` | ✅ | | | **policy** | `{ csp?: object; cors?: object; rateLimit?: object; authentication?: object; … }` | optional | | -| **scanResults** | `{ timestamp: string; scanner: object; status: Enum<'passed' \| 'failed' \| 'warning'>; vulnerabilities?: object[]; … }[]` | optional | | -| **vulnerabilities** | `{ cve?: string; id: string; severity: Enum<'critical' \| 'high' \| 'medium' \| 'low' \| 'info'>; category?: string; … }[]` | optional | | +| **scanResults** | `never` | optional | [REMOVED] `PluginSecurityManifest.scanResults` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing on this platform ever produced, stored or read a security scan result, so an authored array parsed cleanly and changed nothing. Delete the key. There is no replacement key: plugin security scanning is not a platform capability. What this manifest still enforces is `permissions` and `sandbox`; artifact provenance is answered by the plugin signature verifier, which tells you an artifact is the one its publisher signed and never that it is safe. Audit dependencies with a tool built for it (npm audit, pnpm audit, Dependabot, the GitHub Advisory Database, OSV). | +| **vulnerabilities** | `never` | optional | [REMOVED] `PluginSecurityManifest.vulnerabilities` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — it was an array of `KernelSecurityVulnerability`, the other half of the scan-result family, and leaves with it: nothing ever wrote the list and nothing ever read it, so declaring a known vulnerability against a plugin warned nobody and blocked no install. Delete the key. There is no replacement key: plugin security scanning is not a platform capability. Publish vulnerability contact and disclosure terms through the surviving `securityContact` and `vulnerabilityDisclosure` blocks on this same manifest, and audit dependencies with a tool built for it (npm audit, pnpm audit, Dependabot, the GitHub Advisory Database, OSV). | | **codeSigning** | `{ signed: boolean; signature?: string; certificate?: string; algorithm?: string; … }` | optional | | | **certifications** | `{ name: string; issuer: string; issuedDate: string; expiryDate?: string; … }[]` | optional | | | **securityContact** | `{ email?: string; url?: string; pgpKey?: string }` | optional | | diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md index f00b4554d0b..f1e5d637489 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md @@ -260,7 +260,7 @@ directory rather than per file. | `api/` | 451 | | `identity/` | 32 | | `integration/` | 8 | -| `kernel/` | 257 | +| `kernel/` | 247 | | `marketplace/` | 29 | | `qa/` | 6 | | `shared/` | 20 | diff --git a/docs/qa/platform-checklist/FOLLOW-UPS.md b/docs/qa/platform-checklist/FOLLOW-UPS.md index 42b08ba8f63..27246027d7c 100644 --- a/docs/qa/platform-checklist/FOLLOW-UPS.md +++ b/docs/qa/platform-checklist/FOLLOW-UPS.md @@ -183,12 +183,38 @@ governance hole. | surface | evidence | the deadness, precisely | |---|---|---| -| `KernelSecurityScanResult` / `KernelSecurityVulnerability` / `PluginSecurityManifest.scanResults` (`packages/spec/src/kernel/plugin-security-advanced.zod.ts,476,625`) | no `.parse`/`.safeParse` site anywhere; **zero** consumers of any kind since #14919 retired the dead scanner that was the last type-only importer | 22 rows published to `packages/spec/authorable-surface/kernel.json` with zero authors and zero parsers. The whole `plugin-security-advanced` module has no runtime consumer. | -| `PluginQualityMetrics.securityScan` (`packages/spec/src/kernel/plugin-registry.zod.ts`) | spec self-test only | Nothing reads or writes it at runtime. | -| Marketplace/incident scan vocab (`marketplace.zod.ts` 'scanning' status, `marketplace-admin.zod.ts,193`, `incident-response.zod.ts` 'malware') | declared-only enum members, no producer in this repo | Cloud/EE surface. Same shape as the `'failed'`/`'expired'` upload statuses #7667 had to close: declared, published, no writer. | +| ~~`KernelSecurityScanResult` / `KernelSecurityVulnerability` / `PluginSecurityManifest.scanResults`~~ (`packages/spec/src/kernel/plugin-security-advanced.zod.ts`) | no `.parse`/`.safeParse` site anywhere; **zero** consumers of any kind since the scanner retirement (issue 14919, a number since deleted from the board; live record PR #15930) removed the dead scanner that was the last type-only importer | **CLOSED by removal, #15932** (ruling below). The count this row carried was **22**; the retirement measured **27** authorable rows for the named surface — 8 + 17 for the two defs, plus one each for `PluginSecurityManifest.scanResults` and `PluginQualityMetrics.securityScan`. The 22 is not reconciled, only superseded. | +| ~~`PluginQualityMetrics.securityScan`~~ (`packages/spec/src/kernel/plugin-registry.zod.ts`) | spec self-test only | **CLOSED by removal, #15932** — tombstoned with the family it mirrored. | +| Marketplace scan vocab — `marketplace.zod.ts` `'scanning'` status. ⛔ Still live, still unchecked. ⚠️ The two siblings this row used to name are GONE and the coordinates were stale: `marketplace-admin.zod.ts` left with the cloud subpath (#16526), and `incident-response.zod.ts` — which carried `'malware'` on `system/IncidentCategory` — was retired whole by #15513. Both verified as zero tree entries. | declared-only enum member, no producer in this repo | Cloud/EE surface. Same shape as the `'failed'`/`'expired'` upload statuses #7667 had to close: declared, published, no writer. | | MetadataPlugin FS scan + `metadata-fs` boot scan (`packages/metadata/src/plugin.ts,270` — `watch ?? false`; `packages/runtime/src/standalone-stack.ts` hard-off; `metadata-fs` unwired from any `os dev`/`os serve` lane) | unit-pinned in-package only | No reachable fixture from any shipped boot; if a future lane wires `metadata-fs`, the boot-scan/watcher dot-entry divergence is the risk to test first. | -The scanner row above was **CLOSED by removal** in #14919 (maintainer ruling, +**The first two rows above were CLOSED by removal in #15932** (maintainer ruling, +director seat, decision batch #65, 2026-09-07, adopted verbatim 「同意」): the two defs +left the build whole (`RETIRED_DEFS_BY_MAJOR[18]`), and the authorable carriers — +`PluginSecurityManifest.scanResults`, its sibling list `.vulnerabilities` and +`PluginQualityMetrics.securityScan` — are `retiredKey()` tombstones registered in +`RETIRED_KEYS_BY_MAJOR[18]`. "Declare an owner to enforce" was refused by name: it would +rebuild the scanner issue 14919 had just retired for the same reason. ⛔ Do not re-derive it. + +⚠️ **The third row is NOT closed and was NOT checked — and it is now ONE member, not +three.** The ruling made the marketplace `'scanning'` status and the incident +`'malware'` type conditional on a producer grep of `objectstack-ai/cloud`, and that +repository was not reachable from the session that executed #15932 — so `'scanning'` is +untouched, and its absence from a diff is not evidence about it. The ready-to-run command +is on #15932. + +⚠️ **But `'malware'` was already gone when that ruling was written**, which the ruling +could not have known. It was a member of `system/IncidentCategory`, and the whole +incident-response family was retired by #15513 — maintainer ruling 2026-09-05, **two days +before** the 2026-09-07 ruling that made it conditional (see +`incident-response-family-retired`). `marketplace-admin.zod.ts` left with the cloud +subpath (#16526). Verified on this tree by shape, not taken on report: both files return +zero tree entries on this branch and on `origin/main`, and no `*.zod.ts` names +`malware` at all — against a lit control where `'scanning'` returns a live declaration +in `marketplace.zod.ts`. ⇒ The cloud grep this row still owes is about **one** enum +member, and ⛔ that one is still genuinely unmeasured. + +The scanner row further down was **CLOSED by removal** in PR #15930, for issue 14919 (maintainer ruling, director summon #14, decision batch #42): the class, its barrel export, its `packages/core/examples/` demonstration and the `PHASE2_IMPLEMENTATION.md` section that advertised it are gone, and that section now states plainly that plugin security scanning diff --git a/packages/spec/api-surface/kernel.json b/packages/spec/api-surface/kernel.json index d298d04266f..951abcd5f5a 100644 --- a/packages/spec/api-surface/kernel.json +++ b/packages/spec/api-surface/kernel.json @@ -142,12 +142,6 @@ "KernelSecurityPolicy (type)", "KernelSecurityPolicyParsed (type)", "KernelSecurityPolicySchema (const)", - "KernelSecurityScanResult (type)", - "KernelSecurityScanResultParsed (type)", - "KernelSecurityScanResultSchema (const)", - "KernelSecurityVulnerability (type)", - "KernelSecurityVulnerabilityParsed (type)", - "KernelSecurityVulnerabilitySchema (const)", "LintableAuthoringCollection (interface)", "ListPackagesRequest (type)", "ListPackagesRequestSchema (const)", diff --git a/packages/spec/authorable-defaults/kernel.json b/packages/spec/authorable-defaults/kernel.json index 1f7d76063f4..e4997e515e6 100644 --- a/packages/spec/authorable-defaults/kernel.json +++ b/packages/spec/authorable-defaults/kernel.json @@ -51,8 +51,6 @@ "kernel/InstalledPackage:status = \"installed\"", "kernel/KernelContext:features = {}", "kernel/KernelContext:mode = \"production\"", - "kernel/KernelSecurityVulnerability:exploitAvailable = false", - "kernel/KernelSecurityVulnerability:patchAvailable = false", "kernel/Manifest:defaultDatasource = \"default\"", "kernel/Manifest:scope = \"project\"", "kernel/MetadataDiffItem:hasConflict = false", diff --git a/packages/spec/authorable-surface/kernel.json b/packages/spec/authorable-surface/kernel.json index 7b06132c37e..b2e931f40eb 100644 --- a/packages/spec/authorable-surface/kernel.json +++ b/packages/spec/authorable-surface/kernel.json @@ -251,31 +251,6 @@ "kernel/KernelSecurityPolicy:csp", "kernel/KernelSecurityPolicy:encryption", "kernel/KernelSecurityPolicy:rateLimit", - "kernel/KernelSecurityScanResult:codeIssues", - "kernel/KernelSecurityScanResult:dependencyVulnerabilities", - "kernel/KernelSecurityScanResult:licenseCompliance", - "kernel/KernelSecurityScanResult:scanner", - "kernel/KernelSecurityScanResult:status", - "kernel/KernelSecurityScanResult:summary", - "kernel/KernelSecurityScanResult:timestamp", - "kernel/KernelSecurityScanResult:vulnerabilities", - "kernel/KernelSecurityVulnerability:affectedVersions", - "kernel/KernelSecurityVulnerability:category", - "kernel/KernelSecurityVulnerability:cve", - "kernel/KernelSecurityVulnerability:cvssScore", - "kernel/KernelSecurityVulnerability:description", - "kernel/KernelSecurityVulnerability:discoveredDate", - "kernel/KernelSecurityVulnerability:exploitAvailable", - "kernel/KernelSecurityVulnerability:fixedIn", - "kernel/KernelSecurityVulnerability:id", - "kernel/KernelSecurityVulnerability:location", - "kernel/KernelSecurityVulnerability:patchAvailable", - "kernel/KernelSecurityVulnerability:publishedDate", - "kernel/KernelSecurityVulnerability:references", - "kernel/KernelSecurityVulnerability:remediation", - "kernel/KernelSecurityVulnerability:severity", - "kernel/KernelSecurityVulnerability:title", - "kernel/KernelSecurityVulnerability:workaround", "kernel/ListPackagesRequest:enabled", "kernel/ListPackagesRequest:status", "kernel/ListPackagesRequest:type", @@ -533,7 +508,7 @@ "kernel/PluginQualityMetrics:codeQuality", "kernel/PluginQualityMetrics:conformanceTests", "kernel/PluginQualityMetrics:documentationScore", - "kernel/PluginQualityMetrics:securityScan", + "kernel/PluginQualityMetrics:securityScan [RETIRED]", "kernel/PluginQualityMetrics:testCoverage", "kernel/PluginRegistryEntry:capabilities", "kernel/PluginRegistryEntry:category", @@ -574,10 +549,10 @@ "kernel/PluginSecurityManifest:pluginId", "kernel/PluginSecurityManifest:policy", "kernel/PluginSecurityManifest:sandbox", - "kernel/PluginSecurityManifest:scanResults", + "kernel/PluginSecurityManifest:scanResults [RETIRED]", "kernel/PluginSecurityManifest:securityContact", "kernel/PluginSecurityManifest:trustLevel", - "kernel/PluginSecurityManifest:vulnerabilities", + "kernel/PluginSecurityManifest:vulnerabilities [RETIRED]", "kernel/PluginSecurityManifest:vulnerabilityDisclosure", "kernel/PluginStartupResult:duration [RETIRED]", "kernel/PluginStartupResult:durationMs", diff --git a/packages/spec/declaration-map/kernel.json b/packages/spec/declaration-map/kernel.json index 10ff42c6b22..92f5db85d9f 100644 --- a/packages/spec/declaration-map/kernel.json +++ b/packages/spec/declaration-map/kernel.json @@ -95,10 +95,6 @@ "KernelContextSchema": "kernel/KernelContext", "KernelSecurityPolicy": "kernel/KernelSecurityPolicy", "KernelSecurityPolicySchema": "kernel/KernelSecurityPolicy", - "KernelSecurityScanResult": "kernel/KernelSecurityScanResult", - "KernelSecurityScanResultSchema": "kernel/KernelSecurityScanResult", - "KernelSecurityVulnerability": "kernel/KernelSecurityVulnerability", - "KernelSecurityVulnerabilitySchema": "kernel/KernelSecurityVulnerability", "ListPackagesRequest": "kernel/ListPackagesRequest", "ListPackagesRequestSchema": "kernel/ListPackagesRequest", "ListPackagesResponse": "kernel/ListPackagesResponse", diff --git a/packages/spec/export-origins/kernel.json b/packages/spec/export-origins/kernel.json index c66b7b3da0f..7ba8764d433 100644 --- a/packages/spec/export-origins/kernel.json +++ b/packages/spec/export-origins/kernel.json @@ -141,12 +141,6 @@ "KernelSecurityPolicy": "src/kernel/plugin-security-advanced.zod.ts#KernelSecurityPolicy (type)", "KernelSecurityPolicyParsed": "src/kernel/plugin-security-advanced.zod.ts#KernelSecurityPolicyParsed (type)", "KernelSecurityPolicySchema": "src/kernel/plugin-security-advanced.zod.ts#KernelSecurityPolicySchema (const)", - "KernelSecurityScanResult": "src/kernel/plugin-security-advanced.zod.ts#KernelSecurityScanResult (type)", - "KernelSecurityScanResultParsed": "src/kernel/plugin-security-advanced.zod.ts#KernelSecurityScanResultParsed (type)", - "KernelSecurityScanResultSchema": "src/kernel/plugin-security-advanced.zod.ts#KernelSecurityScanResultSchema (const)", - "KernelSecurityVulnerability": "src/kernel/plugin-security-advanced.zod.ts#KernelSecurityVulnerability (type)", - "KernelSecurityVulnerabilityParsed": "src/kernel/plugin-security-advanced.zod.ts#KernelSecurityVulnerabilityParsed (type)", - "KernelSecurityVulnerabilitySchema": "src/kernel/plugin-security-advanced.zod.ts#KernelSecurityVulnerabilitySchema (const)", "LintableAuthoringCollection": "src/kernel/metadata-authoring-lint.ts#LintableAuthoringCollection (interface)", "ListPackagesRequest": "src/kernel/package-registry.zod.ts#ListPackagesRequest (type)", "ListPackagesRequestSchema": "src/kernel/package-registry.zod.ts#ListPackagesRequestSchema (const)", diff --git a/packages/spec/json-schema.manifest/kernel.json b/packages/spec/json-schema.manifest/kernel.json index 4fc83d18c37..93d80ccb801 100644 --- a/packages/spec/json-schema.manifest/kernel.json +++ b/packages/spec/json-schema.manifest/kernel.json @@ -50,8 +50,6 @@ "kernel/InstalledPackage", "kernel/KernelContext", "kernel/KernelSecurityPolicy", - "kernel/KernelSecurityScanResult", - "kernel/KernelSecurityVulnerability", "kernel/ListPackagesRequest", "kernel/ListPackagesResponse", "kernel/Manifest", diff --git a/packages/spec/src/kernel/plugin-registry.test.ts b/packages/spec/src/kernel/plugin-registry.test.ts index 9b419f10e5b..34a433b6556 100644 --- a/packages/spec/src/kernel/plugin-registry.test.ts +++ b/packages/spec/src/kernel/plugin-registry.test.ts @@ -111,43 +111,12 @@ describe('Plugin Registry Schemas', () => { expect(() => PluginQualityMetricsSchema.parse({ codeQuality: -5 })).toThrow(); }); - it('should accept valid security scan', () => { - const metrics = PluginQualityMetricsSchema.parse({ - securityScan: { - lastScanDate: '2024-01-15T10:00:00Z', - vulnerabilities: { - critical: 0, - high: 1, - medium: 2, - low: 5, - }, - passed: true, - }, - }); - expect(metrics.securityScan?.passed).toBe(true); - expect(metrics.securityScan?.vulnerabilities?.high).toBe(1); - }); - - it('should apply defaults in security scan vulnerabilities', () => { - const metrics = PluginQualityMetricsSchema.parse({ - securityScan: { - vulnerabilities: {}, - }, - }); - expect(metrics.securityScan?.vulnerabilities?.critical).toBe(0); - expect(metrics.securityScan?.vulnerabilities?.high).toBe(0); - expect(metrics.securityScan?.vulnerabilities?.medium).toBe(0); - expect(metrics.securityScan?.vulnerabilities?.low).toBe(0); - expect(metrics.securityScan?.passed).toBe(false); - }); - - it('should reject negative vulnerability counts', () => { - expect(() => PluginQualityMetricsSchema.parse({ - securityScan: { - vulnerabilities: { critical: -1, high: 0, medium: 0, low: 0 }, - }, - })).toThrow(); - }); + // The three `securityScan` cases that stood here left with the key + // (#15932, ADR-0049 enforce-or-remove). Their replacement is not a + // narrower version of them: it is the refusal, pinned in + // `plugin-security-scan-result-retirement.test.ts` beside the rest of the + // family. ⛔ Do not re-add a "valid security scan" case — authoring the key + // is now a tsc error (input type `never`) and a parse error. it('should accept valid conformance tests', () => { const metrics = PluginQualityMetricsSchema.parse({ diff --git a/packages/spec/src/kernel/plugin-registry.zod.ts b/packages/spec/src/kernel/plugin-registry.zod.ts index 8517489c6b8..a2cfc0ba4d7 100644 --- a/packages/spec/src/kernel/plugin-registry.zod.ts +++ b/packages/spec/src/kernel/plugin-registry.zod.ts @@ -16,6 +16,19 @@ import { MAJOR_MINOR_PATCH_VERSION_PATTERN } from './version-grammar'; * Plugin Vendor Information */ import { lazySchema } from '../shared/lazy-schema'; +import { retiredKey } from '../shared/retired-key'; + +const SECURITY_SCAN_RETIRED = + '`PluginQualityMetrics.securityScan` was removed in @objectstack/spec 17 (ADR-0049 ' + + 'enforce-or-remove) — the block declared a last-scan date, per-severity vulnerability ' + + 'counts and a `passed` verdict, and no scanner, registry, installer or UI ever produced ' + + 'or read one, so a plugin could publish `passed: true` with nothing at all behind it. ' + + 'Delete the key. There is no replacement key: plugin security scanning is not a platform ' + + 'capability, and the scan-result family this block mirrored was retired with it. The ' + + 'sibling quality metrics — `testCoverage`, `documentationScore`, `codeQuality` and ' + + '`conformanceTests` — are unchanged. Audit dependencies with a tool built for it (npm ' + + 'audit, pnpm audit, Dependabot, the GitHub Advisory Database, OSV).'; + export const PluginVendorSchema = lazySchema(() => z.object({ /** * Vendor identifier (reverse domain notation) @@ -71,18 +84,15 @@ export const PluginQualityMetricsSchema = lazySchema(() => z.object({ codeQuality: z.number().min(0).max(100).optional(), /** - * Security scan status + * Tombstone: the plugin security-scan status is RETIRED (#15932, ADR-0049 + * enforce-or-remove), with the `plugin-security-advanced` scan-result family + * it mirrored. Not a bare deletion: `PluginQualityMetricsSchema` is not + * `.strict()`, so zod would strip an authored key in silence (ADR-0104). + * The key itself carried no default, so there is no materialized residue in + * previously built artifacts to accept (the inner defaults only ever fired + * for an author who wrote the block). */ - securityScan: z.object({ - lastScanDate: z.string().datetime().optional(), - vulnerabilities: z.object({ - critical: z.number().int().min(0).default(0), - high: z.number().int().min(0).default(0), - medium: z.number().int().min(0).default(0), - low: z.number().int().min(0).default(0), - }).optional(), - passed: z.boolean().default(false), - }).optional(), + securityScan: retiredKey(SECURITY_SCAN_RETIRED), /** * Conformance test results diff --git a/packages/spec/src/kernel/plugin-security-advanced.zod.ts b/packages/spec/src/kernel/plugin-security-advanced.zod.ts index 37b60343a7a..bf87e3c32fd 100644 --- a/packages/spec/src/kernel/plugin-security-advanced.zod.ts +++ b/packages/spec/src/kernel/plugin-security-advanced.zod.ts @@ -13,8 +13,15 @@ import { EvaluatedExpressionInputSchema } from '../shared/expression.zod'; * - Fine-grained permission system * - Resource access control * - Sandboxing and isolation - * - Security scanning and verification * - Runtime security monitoring + * + * ⛔ NOT security scanning. The scan-result family — `KernelSecurityScanResult`, + * `KernelSecurityVulnerability`, `PluginSecurityManifest.scanResults` and its + * sibling list `.vulnerabilities` — was retired under ADR-0049 enforce-or-remove + * (#15932), after the runtime scanner that was its last type-only importer went + * the same way. That scanner retirement was issue 14919 — a number DELETED from + * the board and no longer resolving; its live record is PR #15930, which landed + * the removal. Nothing on this platform scans a plugin. */ /** @@ -346,6 +353,30 @@ const DISCLOSURE_RESPONSE_TIME_RETIRED = + 'name on `PluginHealthReport.metrics` was milliseconds — which is the confusion the rule ' + 'exists to remove. Rename the key to `responseTimeHours`; the value (hours) is unchanged.'; +// The scan-result family's two tombstones (#15932, ADR-0049 enforce-or-remove). +// Their value types — `KernelSecurityScanResult` and `KernelSecurityVulnerability` — +// are gone from this build entirely; see the header note above. +const SCAN_RESULTS_RETIRED = + '`PluginSecurityManifest.scanResults` was removed in @objectstack/spec 17 (ADR-0049 ' + + 'enforce-or-remove) — nothing on this platform ever produced, stored or read a security ' + + 'scan result, so an authored array parsed cleanly and changed nothing. Delete the key. ' + + 'There is no replacement key: plugin security scanning is not a platform capability. What ' + + 'this manifest still enforces is `permissions` and `sandbox`; artifact provenance is ' + + 'answered by the plugin signature verifier, which tells you an artifact is the one its ' + + 'publisher signed and never that it is safe. Audit dependencies with a tool built for it ' + + '(npm audit, pnpm audit, Dependabot, the GitHub Advisory Database, OSV).'; + +const MANIFEST_VULNERABILITIES_RETIRED = + '`PluginSecurityManifest.vulnerabilities` was removed in @objectstack/spec 17 (ADR-0049 ' + + 'enforce-or-remove) — it was an array of `KernelSecurityVulnerability`, the other half of ' + + 'the scan-result family, and leaves with it: nothing ever wrote the list and nothing ever ' + + 'read it, so declaring a known vulnerability against a plugin warned nobody and blocked ' + + 'no install. Delete the key. There is no replacement key: plugin security scanning is not ' + + 'a platform capability. Publish vulnerability contact and disclosure terms through the ' + + 'surviving `securityContact` and `vulnerabilityDisclosure` blocks on this same manifest, ' + + 'and audit dependencies with a tool built for it (npm audit, pnpm audit, Dependabot, the ' + + 'GitHub Advisory Database, OSV).'; + /** * Sandbox Configuration * Defines how plugin is isolated @@ -434,171 +465,6 @@ export const SandboxConfigSchema = lazySchema(() => z.object({ }).optional(), })); -/** - * Security Vulnerability - * Represents a known security vulnerability - */ -export const KernelSecurityVulnerabilitySchema = lazySchema(() => z.object({ - /** - * CVE identifier - */ - cve: z.string().optional(), - - /** - * Vulnerability identifier - */ - id: z.string(), - - /** - * Severity level - */ - severity: z.enum(['critical', 'high', 'medium', 'low', 'info']), - - /** - * Category (e.g., SAST, DAST, Dependency) - */ - category: z.string().optional(), - - /** - * Title - */ - title: z.string(), - - /** - * Location of the vulnerability - */ - location: z.string().optional(), - - /** - * Remediation steps - */ - remediation: z.string().optional(), - - /** - * Description - */ - description: z.string(), - - /** - * Affected versions - */ - affectedVersions: z.array(z.string()), - - /** - * Fixed in versions - */ - fixedIn: z.array(z.string()).optional(), - - /** - * CVSS score - */ - cvssScore: z.number().min(0).max(10).optional(), - - /** - * Exploit availability - */ - exploitAvailable: z.boolean().default(false), - - /** - * Patch available - */ - patchAvailable: z.boolean().default(false), - - /** - * Workaround - */ - workaround: z.string().optional(), - - /** - * References - */ - references: z.array(z.string()).optional(), - - /** - * Discovered date - */ - discoveredDate: z.string().datetime().optional(), - - /** - * Published date - */ - publishedDate: z.string().datetime().optional(), -})); - -/** - * Security Scan Result - * Result of security scanning - */ -export const KernelSecurityScanResultSchema = lazySchema(() => z.object({ - /** - * Scan timestamp - */ - timestamp: z.string().datetime(), - - /** - * Scanner information - */ - scanner: z.object({ - name: z.string(), - version: z.string(), - }), - - /** - * Overall status - */ - status: z.enum(['passed', 'failed', 'warning']), - - /** - * Vulnerabilities found - */ - vulnerabilities: z.array(KernelSecurityVulnerabilitySchema).optional(), - - /** - * Code quality issues - */ - codeIssues: z.array(z.object({ - severity: z.enum(['error', 'warning', 'info']), - type: z.string().describe('Issue type (e.g., sql-injection, xss)'), - file: z.string(), - line: z.number().int().optional(), - message: z.string(), - suggestion: z.string().optional(), - })).optional(), - - /** - * Dependency vulnerabilities - */ - dependencyVulnerabilities: z.array(z.object({ - package: z.string(), - version: z.string(), - vulnerability: KernelSecurityVulnerabilitySchema, - })).optional(), - - /** - * License compliance - */ - licenseCompliance: z.object({ - status: z.enum(['compliant', 'non-compliant', 'unknown']), - issues: z.array(z.object({ - package: z.string(), - license: z.string(), - reason: z.string(), - })).optional(), - }).optional(), - - /** - * Summary statistics - */ - summary: z.object({ - totalVulnerabilities: z.number().int(), - criticalCount: z.number().int(), - highCount: z.number().int(), - mediumCount: z.number().int(), - lowCount: z.number().int(), - infoCount: z.number().int(), - }), -})); - /** * Security Policy * Defines security policies for plugin @@ -721,14 +587,18 @@ export const PluginSecurityManifestSchema = lazySchema(() => z.object({ policy: KernelSecurityPolicySchema.optional(), /** - * Security scan results + * Tombstone: the scan-result surface is RETIRED (#15932, ADR-0049 + * enforce-or-remove). `KernelSecurityScanResult` and + * `KernelSecurityVulnerability` left this build with it — the last importer + * of either type went with `PluginSecurityScanner` (issue 14919, a number + * deleted from the board; live record: PR #15930). Not a bare + * deletion: this shape is not `.strict()`, so zod would strip an authored + * key in silence (ADR-0104). */ - scanResults: z.array(KernelSecurityScanResultSchema).optional(), - - /** - * Known vulnerabilities - */ - vulnerabilities: z.array(KernelSecurityVulnerabilitySchema).optional(), + scanResults: retiredKey(SCAN_RESULTS_RETIRED), + + /** Tombstone for the sibling list — same retirement, same reasoning. */ + vulnerabilities: retiredKey(MANIFEST_VULNERABILITIES_RETIRED), /** * Code signing @@ -793,12 +663,6 @@ export type RuntimeConfigParsed = z.infer; export type SandboxConfig = z.input; /** Post-parse shape of {@link SandboxConfig} — defaults applied, transforms run (ADR-0122). */ export type SandboxConfigParsed = z.infer; -export type KernelSecurityVulnerability = z.input; -/** Post-parse shape of {@link KernelSecurityVulnerability} — defaults applied, transforms run (ADR-0122). */ -export type KernelSecurityVulnerabilityParsed = z.infer; -export type KernelSecurityScanResult = z.input; -/** Post-parse shape of {@link KernelSecurityScanResult} — defaults applied, transforms run (ADR-0122). */ -export type KernelSecurityScanResultParsed = z.infer; export type KernelSecurityPolicy = z.input; /** Post-parse shape of {@link KernelSecurityPolicy} — defaults applied, transforms run (ADR-0122). */ export type KernelSecurityPolicyParsed = z.infer; diff --git a/packages/spec/src/kernel/plugin-security-scan-result-retirement.test.ts b/packages/spec/src/kernel/plugin-security-scan-result-retirement.test.ts new file mode 100644 index 00000000000..33f307e149e --- /dev/null +++ b/packages/spec/src/kernel/plugin-security-scan-result-retirement.test.ts @@ -0,0 +1,139 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; + +import { PluginSecurityManifestSchema } from './plugin-security-advanced.zod'; +import { PluginQualityMetricsSchema } from './plugin-registry.zod'; + +// ─── [#15932] the plugin-security SCAN-RESULT family is REMOVED ─────────────── +// +// ADR-0049 enforce-or-remove; director seat, decision batch #65, 2026-09-07, +// maintainer verbatim 「同意」. Four members, one family: +// +// - `KernelSecurityScanResult` — whole def, out of the build +// - `KernelSecurityVulnerability` — whole def, out of the build +// - `PluginSecurityManifest.scanResults` — tombstoned key +// - `PluginQualityMetrics.securityScan` — tombstoned key +// +// plus `PluginSecurityManifest.vulnerabilities`, the last authorable referent of +// the second def and therefore a forced consequence of retiring it. +// +// This is the second half of the scanner retirement (issue 14919, a number since +// deleted from the board; live record PR #15930), which retired `PluginSecurityScanner` — the +// `@objectstack/core` class that shipped as a security control and returned +// `status: "passed"` for every plugin it was ever handed. That scanner was the +// family's ONLY importer of any kind (a type-only import), so its deletion moved +// these schemas from one type-only importer to zero consumers while they stayed +// fully published on the authorable surface. +// +// ⚠️ On the assertion set. A schema tombstone raises a `ZodError`, not the +// ADR-0112 envelope: its issues carry `code` and `path` and have no `status` +// field at all. So these pins assert the strongest set this surface really has — +// refusal, the issue `code`, the `path` naming WHICH key was refused, and the +// prescription text (where the wording is the contract, pin the wording). A bare +// `toThrow()` would stay green if the refusal moved to the wrong key or stopped +// carrying the fix. + +/** A manifest that is valid except for whatever an individual test adds. */ +const baseManifest = { + pluginId: 'com.example.plugin', + trustLevel: 'community', + permissions: { permissions: [] }, + sandbox: {}, +} as const; + +describe('[#15932] plugin-security scan-result retirement', () => { + it('REJECTS an authored `scanResults` array, naming the key and carrying the fix', () => { + const result = PluginSecurityManifestSchema.safeParse({ + ...baseManifest, + scanResults: [{ timestamp: '2026-01-15T10:00:00Z', status: 'passed' }], + }); + + expect(result.success).toBe(false); + if (result.success) return; // narrowing; the assertion above already failed + + const issue = result.error.issues.find((i) => i.path[0] === 'scanResults'); + expect(issue, 'the refusal must name `scanResults`').toBeDefined(); + expect(issue!.code).toBe('invalid_type'); + expect(issue!.path).toEqual(['scanResults']); + // The prescription IS the migration doc for whoever hits it — contract, not + // commentary. It must say the removal happened, and must NOT send the reader + // looking for a replacement key that does not exist. + expect(issue!.message).toMatch(/`PluginSecurityManifest\.scanResults`.*removed.*ADR-0049/s); + expect(issue!.message).toMatch(/Delete the key/s); + expect(issue!.message).toMatch(/no replacement key/s); + // An author who read a clean scan as evidence of safety has a security + // decision to revisit; silence here is what made the inert key dangerous. + expect(issue!.message).toMatch(/never that it is safe/s); + }); + + it('REJECTS the sibling `vulnerabilities` list, which left with its value type', () => { + const result = PluginSecurityManifestSchema.safeParse({ + ...baseManifest, + vulnerabilities: [{ id: 'V-1', severity: 'critical', title: 't', description: 'd', affectedVersions: [] }], + }); + + expect(result.success).toBe(false); + if (result.success) return; + + const issue = result.error.issues.find((i) => i.path[0] === 'vulnerabilities'); + expect(issue, 'the refusal must name `vulnerabilities`').toBeDefined(); + expect(issue!.code).toBe('invalid_type'); + expect(issue!.message).toMatch(/`PluginSecurityManifest\.vulnerabilities`.*removed/s); + expect(issue!.message).toMatch(/blocked\s+no install/s); + }); + + it('REJECTS `PluginQualityMetrics.securityScan`, the member that published a VERDICT', () => { + const result = PluginQualityMetricsSchema.safeParse({ + testCoverage: 85, + securityScan: { lastScanDate: '2026-01-15T10:00:00Z', passed: true }, + }); + + expect(result.success).toBe(false); + if (result.success) return; + + const issue = result.error.issues.find((i) => i.path[0] === 'securityScan'); + expect(issue, 'the refusal must name `securityScan`').toBeDefined(); + expect(issue!.code).toBe('invalid_type'); + expect(issue!.path).toEqual(['securityScan']); + expect(issue!.message).toMatch(/`PluginQualityMetrics\.securityScan`.*removed.*ADR-0049/s); + // The prescription must say plainly what the verdict was worth, because a + // consumer that gated on `passed: true` was gating on nothing. + expect(issue!.message).toMatch(/`passed: true` with nothing at all behind it/s); + expect(issue!.message).toMatch(/Delete the key/s); + }); + + it('parses cleanly once the keys are deleted, and grows no such property', () => { + // Absence must stay absence. Neither carrying shape is `.strict()`, so a bare + // deletion would have stripped an authored key in SILENCE (ADR-0104) — this + // pin plus the refusals above are what keep the tombstones honest. + const manifest = PluginSecurityManifestSchema.parse({ ...baseManifest }); + expect(manifest.pluginId).toBe('com.example.plugin'); + expect(manifest).not.toHaveProperty('scanResults'); + expect(manifest).not.toHaveProperty('vulnerabilities'); + + const metrics = PluginQualityMetricsSchema.parse({ testCoverage: 85, codeQuality: 75 }); + expect(metrics.testCoverage).toBe(85); + expect(metrics).not.toHaveProperty('securityScan'); + }); + + it('does not export the retired scan-result defs from ./kernel', async () => { + const kernel = await import('./index'); + for (const name of ['KernelSecurityScanResultSchema', 'KernelSecurityVulnerabilitySchema']) { + expect(kernel, `${name} must not be exported after #15932`).not.toHaveProperty(name); + } + + // Anti-vacuity: this pin means nothing unless the barrel really resolved and + // still exports the neighbours that SURVIVE. Three groups, each load-bearing: + // the manifest's own surviving security vocabulary; the sibling quality + // metrics; and — the scope fence — the SEPARATELY DECLARED, unprefixed pair + // in `plugin-security.zod.ts`, which is a different family with its own + // self-test and is deliberately NOT part of this retirement. + expect(kernel).toHaveProperty('PluginSecurityManifestSchema'); + expect(kernel).toHaveProperty('KernelSecurityPolicySchema'); + expect(kernel).toHaveProperty('SandboxConfigSchema'); + expect(kernel).toHaveProperty('PluginQualityMetricsSchema'); + expect(kernel).toHaveProperty('SecurityScanResultSchema'); + expect(kernel).toHaveProperty('SecurityVulnerabilitySchema'); + }); +}); diff --git a/packages/spec/src/migrations/entries/retired-defs/18.kernel__KernelSecurityScanResult.ts b/packages/spec/src/migrations/entries/retired-defs/18.kernel__KernelSecurityScanResult.ts new file mode 100644 index 00000000000..0c5f1261f89 --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-defs/18.kernel__KernelSecurityScanResult.ts @@ -0,0 +1,30 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #15932 — ADR-0049 enforce-or-remove (director seat, decision batch #65, +// 2026-09-07, maintainer verbatim 「同意」). `KernelSecurityScanResult` declared a +// complete scan report — timestamp, scanner name/version, a passed/failed/warning +// status, vulnerability and code-issue lists, dependency findings, license +// compliance and a six-number summary — and no layer ever emitted, stored, parsed +// or read one. Its only importer of any kind was `PluginSecurityScanner`, a +// type-only import from `packages/core/src/security/security-scanner.ts`, and +// PR #15930 deleted that file (it closed issue 14919, a number since deleted +// from the board and no longer resolving); the census after it landed put every remaining +// reference inside the declaring module itself, against a lit control (five hits +// for `PluginSecurityManifest` in the same file), so the zero is a reading. +// +// Whole-def retirement, not a tombstone: nothing parses this def, so there is no +// author to hand a prescription to and no `${defKey}:${name}` key leaving a live +// shape. `RETIRED_DEFS_BY_MAJOR[18]` plus the semantic entry +// `plugin-security-scan-result-surface-retired` ARE the declaration — the +// #11825 shape (its sibling in the pair this tree usually cites, issue 8715, is +// a number deleted from the board; 11825 is the half that still resolves). +// The two authorable carriers that pointed here, +// `PluginSecurityManifest.scanResults` and `.vulnerabilities`, are separately +// tombstoned and registered in `RETIRED_KEYS_BY_MAJOR[18]`. +// +// No D2 conversion: a plugin security manifest is a package artifact a publisher +// ships, never a stack collection member and never a stored `sys_metadata` row, +// so the conversion chain has no seam that would see one (the sibling +// `kernel/PluginSecurityManifest:vulnerabilityDisclosure.responseTime` entry +// records the same reasoning for the same schema). +export const entry = 'kernel/KernelSecurityScanResult'; diff --git a/packages/spec/src/migrations/entries/retired-defs/18.kernel__KernelSecurityVulnerability.ts b/packages/spec/src/migrations/entries/retired-defs/18.kernel__KernelSecurityVulnerability.ts new file mode 100644 index 00000000000..142f80b4e63 --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-defs/18.kernel__KernelSecurityVulnerability.ts @@ -0,0 +1,28 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #15932 — ADR-0049 enforce-or-remove (director seat, decision batch #65, +// 2026-09-07, maintainer verbatim 「同意」). The other half of the scan-result +// family: a CVE-shaped vulnerability record (severity, CVSS score, affected and +// fixed versions, exploit/patch availability, remediation, disclosure dates) that +// nothing ever constructed, validated or consulted. It reached this build through +// exactly three referents, all now gone — `KernelSecurityScanResult.vulnerabilities` +// and `.dependencyVulnerabilities[].vulnerability` (that def leaves in the same +// change) and `PluginSecurityManifest.vulnerabilities` (tombstoned). Retired +// together with `KernelSecurityScanResult` because declaring a vulnerability +// vocabulary with no scan to carry it reads as a capability, which is the +// ADR-0049 shape. +// +// Whole-def retirement for the reason its sibling entry records, and the same +// disposition: `RETIRED_DEFS_BY_MAJOR[18]` plus +// `plugin-security-scan-result-surface-retired`, no D2 conversion, no tombstone +// of its own. Its two authorable defaults — +// `KernelSecurityVulnerability:exploitAvailable = false` and +// `:patchAvailable = false` — leave `authorable-defaults/kernel.json` with the +// def; nothing ever parsed this schema, so no released toolchain ever +// materialized either value into an artifact and there is no residue to accept. +// +// ⛔ NOT in scope, and deliberately untouched: the unprefixed +// `SecurityVulnerabilitySchema` / `SecurityScanResultSchema` pair in the sibling +// module `kernel/plugin-security.zod.ts`. Those are separate defs with their own +// self-test and are outside this ruling. +export const entry = 'kernel/KernelSecurityVulnerability'; diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginQualityMetrics__securityScan.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginQualityMetrics__securityScan.ts new file mode 100644 index 00000000000..f521009ec56 --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginQualityMetrics__securityScan.ts @@ -0,0 +1,27 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #15932 — ADR-0049 enforce-or-remove (director seat, decision batch #65, +// 2026-09-07, maintainer verbatim 「同意」). `PluginQualityMetrics.securityScan` is +// the scan-result family's sibling on the plugin registry entry, named by the +// ruling alongside it: a last-scan date, per-severity vulnerability counts and a +// `passed` boolean, read by no scanner, registry, installer or UI. The census put +// every reference in `packages/spec/src/kernel/plugin-registry.test.ts` — the +// spec's own self-test and nothing else. +// +// It is the sharper half of the family for an author: `scanResults` published a +// report, but `securityScan.passed` published a VERDICT, so a plugin could ship +// `passed: true` with nothing at all behind it and a consumer reading the +// registry entry had no way to tell that from a real result. +// +// Tombstoned with `retiredKey()`: `PluginQualityMetricsSchema` is a plain +// `z.object`, so a bare deletion would strip an authored block in silence +// (ADR-0104). The key carried NO default of its own — the defaults inside it +// (`critical`/`high`/`medium`/`low = 0`, `passed = false`) fired only for an +// author who wrote the block — so `acceptRetiredDefaultResidue` is not owed: +// there is no value a released toolchain materialized into an artifact whose +// author never typed the key. +// +// No D2 conversion: a plugin registry entry is a published package artifact, not +// a stack collection member or a stored `sys_metadata` row. The ledger channel is +// `plugin-security-scan-result-surface-retired`. +export const entry = 'kernel/PluginQualityMetrics:securityScan'; diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginSecurityManifest__scanResults.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginSecurityManifest__scanResults.ts new file mode 100644 index 00000000000..fee45f4b620 --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginSecurityManifest__scanResults.ts @@ -0,0 +1,25 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #15932 — ADR-0049 enforce-or-remove (director seat, decision batch #65, +// 2026-09-07, maintainer verbatim 「同意」). `PluginSecurityManifest.scanResults` +// published an array of `KernelSecurityScanResult` on the authorable surface with +// zero authors and zero parsers: no `.parse`/`.safeParse` site existed anywhere +// against the scan-result schemas, so a publisher could declare a clean scan on a +// plugin manifest, be accepted, and get nothing — the declared-not-enforced shape +// Prime Directive #10 names, one layer out from the runtime scanner that PR +// #15930 removed for the same reason (that PR closed issue 14919, a number since +// deleted from the board and no longer resolving). +// +// Tombstoned with `retiredKey()`, not deleted: `PluginSecurityManifestSchema` is a +// plain `z.object`, not `.strict()`, so a bare deletion would strip an authored +// key in silence (ADR-0104) — swapping an inert declaration for an invisible one. +// The value type leaves this build entirely (`RETIRED_DEFS_BY_MAJOR[18]`, +// `kernel/KernelSecurityScanResult`). +// +// No D2 conversion: a security manifest is a package artifact a publisher ships, +// never a stack collection member and never a stored `sys_metadata` row, so the +// chain has no seam that would see one — the disposition the sibling +// `vulnerabilityDisclosure.responseTime` entry on this same schema already +// records. The prescription an author meets is the tombstone itself; the ledger +// channel is `plugin-security-scan-result-surface-retired`. +export const entry = 'kernel/PluginSecurityManifest:scanResults'; diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginSecurityManifest__vulnerabilities.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginSecurityManifest__vulnerabilities.ts new file mode 100644 index 00000000000..e71c5ea25e2 --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginSecurityManifest__vulnerabilities.ts @@ -0,0 +1,22 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #15932 — ADR-0049 enforce-or-remove, decision batch #65. +// `PluginSecurityManifest.vulnerabilities` was an array of +// `KernelSecurityVulnerability` and is this retirement's FORCED CONSEQUENCE +// rather than a name the ruling listed: it was the last authorable referent of a +// def the ruling retires by name, so it cannot survive the def, and keeping the +// def alive only to carry it would be keeping the retired family alive under a +// second name. It is inert on its own terms too — nothing ever wrote the list and +// nothing ever read it, so declaring a known vulnerability against a plugin +// warned nobody and blocked no install. +// +// ⚠️ This is the ONE key outside the four names the #15932 dispatch fenced +// (`KernelSecurityScanResult`, `KernelSecurityVulnerability`, +// `PluginSecurityManifest.scanResults`, `PluginQualityMetrics.securityScan`), and +// it is reported as such on the card and in the landing PR rather than absorbed +// silently. It is not a neighbour retired by proximity — the fence's stated +// concern — it is a referent of a named retiree. +// +// Tombstoned with `retiredKey()` for the reason its `scanResults` sibling records +// (non-strict shape, ADR-0104 silent strip). No D2 conversion, same reasoning. +export const entry = 'kernel/PluginSecurityManifest:vulnerabilities'; diff --git a/packages/spec/src/migrations/entries/semantic/18.plugin-security-scan-result-surface-retired.ts b/packages/spec/src/migrations/entries/semantic/18.plugin-security-scan-result-surface-retired.ts new file mode 100644 index 00000000000..c531c10f9f8 --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.plugin-security-scan-result-surface-retired.ts @@ -0,0 +1,95 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +export const entry: SemanticMigration = { + id: 'plugin-security-scan-result-surface-retired', + // No backticks in `surface` — build-upgrade-guide.ts renders it inside a code + // span AND a table cell. + surface: 'the plugin-security scan-result family: the defs ' + + 'KernelSecurityScanResult and KernelSecurityVulnerability ' + + '(kernel/plugin-security-advanced.zod.ts), their two authorable carriers on ' + + 'PluginSecurityManifest — scanResults and vulnerabilities — and the sibling ' + + 'verdict block PluginQualityMetrics.securityScan (kernel/plugin-registry.zod.ts)', + replacement: + 'nothing to re-declare — delete the keys and every import of the two types. Plugin ' + + 'security scanning is not a platform capability and there is no replacement schema. ' + + 'What the platform does still enforce, and what to reach for instead: `permissions` and ' + + '`sandbox` on the same PluginSecurityManifest are unchanged, and artifact provenance is ' + + 'answered by `verifyPluginArtifactIntegrity` and the plugin signature verifier — which ' + + 'tell you an artifact is the one its publisher signed, and never that it is safe. For ' + + 'dependency vulnerabilities use the tools built for it against your own project (npm ' + + 'audit / pnpm audit, Dependabot, the GitHub Advisory Database, OSV) and treat an ' + + 'unaudited third-party plugin as untrusted code. A publisher who used scanResults to ' + + 'advertise diligence keeps the surviving securityContact and vulnerabilityDisclosure ' + + 'blocks, which are contact terms rather than a verdict.', + reason: + 'ADR-0049 enforce-or-remove; maintainer ruling 2026-09-07 on #15932 (director seat, decision batch #65, adopted verbatim 「同意」). ' + + 'This is the second half of the scanner retirement — issue 14919, a number since ' + + 'DELETED from the board, landed as PR #15930. That card retired PluginSecurityScanner — a ' + + '@objectstack/core class that shipped as a SECURITY control and could not fail, whose ' + + 'verdict was status "passed" for every plugin it was ever handed. The SCHEMAS the ' + + 'scanner fed survived it, and the scanner had been their only importer of any kind (a ' + + 'type-only import in packages/core/src/security/security-scanner.ts), so the family went ' + + 'from one type-only importer to zero consumers while staying fully published: 27 ' + + 'authorable rows across kernel.json, six api-surface exports, two authorable defaults ' + + 'and two json-schema manifest keys. An author could write any of it, be accepted, and ' + + 'get nothing — declared-not-enforced, Prime Directive #10, one layer out from the class ' + + 'removed for the same reason. The census was taken on origin/main after that removal landed, ' + + 'with a lit control (five hits for PluginSecurityManifest inside the declaring module) ' + + 'proving the file greppable, and found no .parse or .safeParse site against either ' + + 'schema anywhere in packages/**. securityScan is the sharpest member: scanResults ' + + 'published a report, but securityScan.passed published a VERDICT, so a plugin could ' + + 'declare itself clean with nothing behind it. Route: the two defs leave the build whole ' + + '(RETIRED_DEFS_BY_MAJOR[18]) because nothing parses them and a prescription nobody can ' + + 'receive is not worth its cost; the three authorable keys are retiredKey() tombstones ' + + '(RETIRED_KEYS_BY_MAJOR[18]) because both carrying shapes are non-strict, where a bare ' + + 'deletion is a silent strip (ADR-0104). Why this entry and not a D2 conversion: a plugin ' + + 'security manifest and a plugin registry entry are package artifacts a publisher ships, ' + + 'never stack collection members and never stored sys_metadata rows, so the conversion ' + + 'chain has no seam that would see one — the disposition the sibling ' + + 'kernel-plugin-security-durations-unit-in-key entry already records for this same ' + + 'manifest. No deprecation window (maintainer 2026-08-27: 「项目在创业阶段,用户也很少,短期不考虑渐进」). ' + + 'Scope note, recorded rather than acted on: PluginSecurityManifest.vulnerabilities is a ' + + 'forced consequence rather than a name the ruling listed — it was the last authorable ' + + 'referent of KernelSecurityVulnerability and could not outlive the def. Two neighbours ' + + 'the ruling made CONDITIONAL are deliberately untouched here because the repository the ' + + 'condition names, objectstack-ai/cloud, is not reachable from the session that executed ' + + 'this: the marketplace "scanning" status stays exactly as it is — unremoved, and NOT ' + + 'recorded as checked. Its two siblings were MEASURED rather than assumed, and the ' + + 'record is corrected here: both were ALREADY GONE when that ruling was written. The ' + + 'incident "malware" type was a member of system/IncidentCategory, and the whole ' + + 'incident-response family was retired by #15513 (maintainer ruling 2026-09-05 — two ' + + 'days BEFORE the 2026-09-07 ruling that made it conditional); see ' + + 'incident-response-family-retired. And marketplace-admin.zod.ts was deleted outright ' + + 'with the cloud subpath (#16526); see cloud-subpath-retired. Verified on this tree by ' + + 'shape: both files return zero tree entries and no *.zod.ts names malware at all, ' + + 'against a lit control where "scanning" still returns a live declaration in ' + + 'marketplace.zod.ts. So the conditional question is ONE enum member wide, not three, ' + + 'and the objectstack-ai/cloud producer grep it still owes is that much smaller. ' + + '⚠️ The out-of-repo consumer population is NOT MEASURED. @objectstack/spec is published, ' + + 'so this removal is breaking for consumers no download, dependent or source telemetry ' + + 'was consulted for — accepted as an input to the ruling, exactly as that retirement states ' + + 'of its own three exports, and not a reason to soften the removal. #15932, PR #15930 ' + + '(for the deleted issue 14919), ADR-0049, ADR-0087.', + acceptanceCriteria: + 'No source imports KernelSecurityScanResult, KernelSecurityVulnerability or either ' + + 'Schema from @objectstack/spec/kernel: both defs are absent from the built kernel ' + + 'barrel and from api-surface/kernel.json, so a TypeScript consumer gets the refusal at ' + + 'compile time at the import site rather than a missing runtime value. Authoring ' + + 'PluginSecurityManifest.scanResults, PluginSecurityManifest.vulnerabilities or ' + + 'PluginQualityMetrics.securityScan fails to compile (input type `never`) and fails to ' + + 'parse with the tombstone prescription naming that key — verified by refusal pins that ' + + 'assert the issue code, the path naming WHICH key was refused, and the prescription ' + + 'text, plus a positive pin that the surrounding manifest still parses and grows no such ' + + 'property. ⚠️ Runtime behaviour is deliberately UNCHANGED and must be verified as such: ' + + 'nothing ever read any of these keys, so deleting one removes no check that was running. ' + + 'A publisher who believed a declared scanResults entry gated anything was never getting ' + + 'that gate; the remediation is to audit with a real tool, not to find a replacement key. ' + + 'The surviving neighbours must still parse and still be exported — permissions, sandbox, ' + + 'policy, codeSigning, certifications, securityContact and vulnerabilityDisclosure on the ' + + 'manifest, testCoverage/documentationScore/codeQuality/conformanceTests on the quality ' + + 'metrics, and the separately-declared SecurityScanResultSchema / ' + + 'SecurityVulnerabilitySchema in kernel/plugin-security.zod.ts, which this change does ' + + 'not touch.', +}; diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index f8ecd4f84a4..fa8f1e18e9d 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -10618,6 +10618,97 @@ const step18: MigrationStep = { + 'still carries `globs` keeps serving as stored data; clear it by deleting the key from ' + 'the source manifest and republishing.', }, + { + id: 'plugin-security-scan-result-surface-retired', + // No backticks in `surface` — build-upgrade-guide.ts renders it inside a code + // span AND a table cell. + surface: 'the plugin-security scan-result family: the defs ' + + 'KernelSecurityScanResult and KernelSecurityVulnerability ' + + '(kernel/plugin-security-advanced.zod.ts), their two authorable carriers on ' + + 'PluginSecurityManifest — scanResults and vulnerabilities — and the sibling ' + + 'verdict block PluginQualityMetrics.securityScan (kernel/plugin-registry.zod.ts)', + replacement: + 'nothing to re-declare — delete the keys and every import of the two types. Plugin ' + + 'security scanning is not a platform capability and there is no replacement schema. ' + + 'What the platform does still enforce, and what to reach for instead: `permissions` and ' + + '`sandbox` on the same PluginSecurityManifest are unchanged, and artifact provenance is ' + + 'answered by `verifyPluginArtifactIntegrity` and the plugin signature verifier — which ' + + 'tell you an artifact is the one its publisher signed, and never that it is safe. For ' + + 'dependency vulnerabilities use the tools built for it against your own project (npm ' + + 'audit / pnpm audit, Dependabot, the GitHub Advisory Database, OSV) and treat an ' + + 'unaudited third-party plugin as untrusted code. A publisher who used scanResults to ' + + 'advertise diligence keeps the surviving securityContact and vulnerabilityDisclosure ' + + 'blocks, which are contact terms rather than a verdict.', + reason: + 'ADR-0049 enforce-or-remove; maintainer ruling 2026-09-07 on #15932 (director seat, decision batch #65, adopted verbatim 「同意」). ' + + 'This is the second half of the scanner retirement — issue 14919, a number since ' + + 'DELETED from the board, landed as PR #15930. That card retired PluginSecurityScanner — a ' + + '@objectstack/core class that shipped as a SECURITY control and could not fail, whose ' + + 'verdict was status "passed" for every plugin it was ever handed. The SCHEMAS the ' + + 'scanner fed survived it, and the scanner had been their only importer of any kind (a ' + + 'type-only import in packages/core/src/security/security-scanner.ts), so the family went ' + + 'from one type-only importer to zero consumers while staying fully published: 27 ' + + 'authorable rows across kernel.json, six api-surface exports, two authorable defaults ' + + 'and two json-schema manifest keys. An author could write any of it, be accepted, and ' + + 'get nothing — declared-not-enforced, Prime Directive #10, one layer out from the class ' + + 'removed for the same reason. The census was taken on origin/main after that removal landed, ' + + 'with a lit control (five hits for PluginSecurityManifest inside the declaring module) ' + + 'proving the file greppable, and found no .parse or .safeParse site against either ' + + 'schema anywhere in packages/**. securityScan is the sharpest member: scanResults ' + + 'published a report, but securityScan.passed published a VERDICT, so a plugin could ' + + 'declare itself clean with nothing behind it. Route: the two defs leave the build whole ' + + '(RETIRED_DEFS_BY_MAJOR[18]) because nothing parses them and a prescription nobody can ' + + 'receive is not worth its cost; the three authorable keys are retiredKey() tombstones ' + + '(RETIRED_KEYS_BY_MAJOR[18]) because both carrying shapes are non-strict, where a bare ' + + 'deletion is a silent strip (ADR-0104). Why this entry and not a D2 conversion: a plugin ' + + 'security manifest and a plugin registry entry are package artifacts a publisher ships, ' + + 'never stack collection members and never stored sys_metadata rows, so the conversion ' + + 'chain has no seam that would see one — the disposition the sibling ' + + 'kernel-plugin-security-durations-unit-in-key entry already records for this same ' + + 'manifest. No deprecation window (maintainer 2026-08-27: 「项目在创业阶段,用户也很少,短期不考虑渐进」). ' + + 'Scope note, recorded rather than acted on: PluginSecurityManifest.vulnerabilities is a ' + + 'forced consequence rather than a name the ruling listed — it was the last authorable ' + + 'referent of KernelSecurityVulnerability and could not outlive the def. Two neighbours ' + + 'the ruling made CONDITIONAL are deliberately untouched here because the repository the ' + + 'condition names, objectstack-ai/cloud, is not reachable from the session that executed ' + + 'this: the marketplace "scanning" status stays exactly as it is — unremoved, and NOT ' + + 'recorded as checked. Its two siblings were MEASURED rather than assumed, and the ' + + 'record is corrected here: both were ALREADY GONE when that ruling was written. The ' + + 'incident "malware" type was a member of system/IncidentCategory, and the whole ' + + 'incident-response family was retired by #15513 (maintainer ruling 2026-09-05 — two ' + + 'days BEFORE the 2026-09-07 ruling that made it conditional); see ' + + 'incident-response-family-retired. And marketplace-admin.zod.ts was deleted outright ' + + 'with the cloud subpath (#16526); see cloud-subpath-retired. Verified on this tree by ' + + 'shape: both files return zero tree entries and no *.zod.ts names malware at all, ' + + 'against a lit control where "scanning" still returns a live declaration in ' + + 'marketplace.zod.ts. So the conditional question is ONE enum member wide, not three, ' + + 'and the objectstack-ai/cloud producer grep it still owes is that much smaller. ' + + '⚠️ The out-of-repo consumer population is NOT MEASURED. @objectstack/spec is published, ' + + 'so this removal is breaking for consumers no download, dependent or source telemetry ' + + 'was consulted for — accepted as an input to the ruling, exactly as that retirement states ' + + 'of its own three exports, and not a reason to soften the removal. #15932, PR #15930 ' + + '(for the deleted issue 14919), ADR-0049, ADR-0087.', + acceptanceCriteria: + 'No source imports KernelSecurityScanResult, KernelSecurityVulnerability or either ' + + 'Schema from @objectstack/spec/kernel: both defs are absent from the built kernel ' + + 'barrel and from api-surface/kernel.json, so a TypeScript consumer gets the refusal at ' + + 'compile time at the import site rather than a missing runtime value. Authoring ' + + 'PluginSecurityManifest.scanResults, PluginSecurityManifest.vulnerabilities or ' + + 'PluginQualityMetrics.securityScan fails to compile (input type `never`) and fails to ' + + 'parse with the tombstone prescription naming that key — verified by refusal pins that ' + + 'assert the issue code, the path naming WHICH key was refused, and the prescription ' + + 'text, plus a positive pin that the surrounding manifest still parses and grows no such ' + + 'property. ⚠️ Runtime behaviour is deliberately UNCHANGED and must be verified as such: ' + + 'nothing ever read any of these keys, so deleting one removes no check that was running. ' + + 'A publisher who believed a declared scanResults entry gated anything was never getting ' + + 'that gate; the remediation is to audit with a real tool, not to find a replacement key. ' + + 'The surviving neighbours must still parse and still be exported — permissions, sandbox, ' + + 'policy, codeSigning, certifications, securityContact and vulnerabilityDisclosure on the ' + + 'manifest, testCoverage/documentationScore/codeQuality/conformanceTests on the quality ' + + 'metrics, and the separately-declared SecurityScanResultSchema / ' + + 'SecurityVulnerabilitySchema in kernel/plugin-security.zod.ts, which this change does ' + + 'not touch.', + }, { id: 'plugin-security-scanner-retired', surface: @@ -14734,6 +14825,74 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // (`packages/core/src/health-monitor.ts`), never authored. See // `kernel-plugin-health-report-durations-unit-in-key`. 'kernel/PluginHealthReport:metrics.uptime', + // #15932 — ADR-0049 enforce-or-remove (director seat, decision batch #65, + // 2026-09-07, maintainer verbatim 「同意」). `PluginQualityMetrics.securityScan` is + // the scan-result family's sibling on the plugin registry entry, named by the + // ruling alongside it: a last-scan date, per-severity vulnerability counts and a + // `passed` boolean, read by no scanner, registry, installer or UI. The census put + // every reference in `packages/spec/src/kernel/plugin-registry.test.ts` — the + // spec's own self-test and nothing else. + // + // It is the sharper half of the family for an author: `scanResults` published a + // report, but `securityScan.passed` published a VERDICT, so a plugin could ship + // `passed: true` with nothing at all behind it and a consumer reading the + // registry entry had no way to tell that from a real result. + // + // Tombstoned with `retiredKey()`: `PluginQualityMetricsSchema` is a plain + // `z.object`, so a bare deletion would strip an authored block in silence + // (ADR-0104). The key carried NO default of its own — the defaults inside it + // (`critical`/`high`/`medium`/`low = 0`, `passed = false`) fired only for an + // author who wrote the block — so `acceptRetiredDefaultResidue` is not owed: + // there is no value a released toolchain materialized into an artifact whose + // author never typed the key. + // + // No D2 conversion: a plugin registry entry is a published package artifact, not + // a stack collection member or a stored `sys_metadata` row. The ledger channel is + // `plugin-security-scan-result-surface-retired`. + 'kernel/PluginQualityMetrics:securityScan', + // #15932 — ADR-0049 enforce-or-remove (director seat, decision batch #65, + // 2026-09-07, maintainer verbatim 「同意」). `PluginSecurityManifest.scanResults` + // published an array of `KernelSecurityScanResult` on the authorable surface with + // zero authors and zero parsers: no `.parse`/`.safeParse` site existed anywhere + // against the scan-result schemas, so a publisher could declare a clean scan on a + // plugin manifest, be accepted, and get nothing — the declared-not-enforced shape + // Prime Directive #10 names, one layer out from the runtime scanner that PR + // #15930 removed for the same reason (that PR closed issue 14919, a number since + // deleted from the board and no longer resolving). + // + // Tombstoned with `retiredKey()`, not deleted: `PluginSecurityManifestSchema` is a + // plain `z.object`, not `.strict()`, so a bare deletion would strip an authored + // key in silence (ADR-0104) — swapping an inert declaration for an invisible one. + // The value type leaves this build entirely (`RETIRED_DEFS_BY_MAJOR[18]`, + // `kernel/KernelSecurityScanResult`). + // + // No D2 conversion: a security manifest is a package artifact a publisher ships, + // never a stack collection member and never a stored `sys_metadata` row, so the + // chain has no seam that would see one — the disposition the sibling + // `vulnerabilityDisclosure.responseTime` entry on this same schema already + // records. The prescription an author meets is the tombstone itself; the ledger + // channel is `plugin-security-scan-result-surface-retired`. + 'kernel/PluginSecurityManifest:scanResults', + // #15932 — ADR-0049 enforce-or-remove, decision batch #65. + // `PluginSecurityManifest.vulnerabilities` was an array of + // `KernelSecurityVulnerability` and is this retirement's FORCED CONSEQUENCE + // rather than a name the ruling listed: it was the last authorable referent of a + // def the ruling retires by name, so it cannot survive the def, and keeping the + // def alive only to carry it would be keeping the retired family alive under a + // second name. It is inert on its own terms too — nothing ever wrote the list and + // nothing ever read it, so declaring a known vulnerability against a plugin + // warned nobody and blocked no install. + // + // ⚠️ This is the ONE key outside the four names the #15932 dispatch fenced + // (`KernelSecurityScanResult`, `KernelSecurityVulnerability`, + // `PluginSecurityManifest.scanResults`, `PluginQualityMetrics.securityScan`), and + // it is reported as such on the card and in the landing PR rather than absorbed + // silently. It is not a neighbour retired by proximity — the fence's stated + // concern — it is a referent of a named retiree. + // + // Tombstoned with `retiredKey()` for the reason its `scanResults` sibling records + // (non-strict shape, ADR-0104 silent strip). No D2 conversion, same reasoning. + 'kernel/PluginSecurityManifest:vulnerabilities', // #15678 (stack card 3/6 of #14478) — ruling B. // `PluginSecurityManifest.vulnerabilityDisclosure.responseTime` said "Expected // response time in hours" in prose and nothing else. Renamed to @@ -17213,6 +17372,60 @@ export const RETIRED_DEFS_BY_MAJOR: Readonly> // keeps emitting — see `18.kernel__PluginStartupResult__health.ts`. Route 3; // the D3 semantic entry `startup-orchestrator-retired` carries the record. 'kernel/HealthStatus', + // #15932 — ADR-0049 enforce-or-remove (director seat, decision batch #65, + // 2026-09-07, maintainer verbatim 「同意」). `KernelSecurityScanResult` declared a + // complete scan report — timestamp, scanner name/version, a passed/failed/warning + // status, vulnerability and code-issue lists, dependency findings, license + // compliance and a six-number summary — and no layer ever emitted, stored, parsed + // or read one. Its only importer of any kind was `PluginSecurityScanner`, a + // type-only import from `packages/core/src/security/security-scanner.ts`, and + // PR #15930 deleted that file (it closed issue 14919, a number since deleted + // from the board and no longer resolving); the census after it landed put every remaining + // reference inside the declaring module itself, against a lit control (five hits + // for `PluginSecurityManifest` in the same file), so the zero is a reading. + // + // Whole-def retirement, not a tombstone: nothing parses this def, so there is no + // author to hand a prescription to and no `${defKey}:${name}` key leaving a live + // shape. `RETIRED_DEFS_BY_MAJOR[18]` plus the semantic entry + // `plugin-security-scan-result-surface-retired` ARE the declaration — the + // #11825 shape (its sibling in the pair this tree usually cites, issue 8715, is + // a number deleted from the board; 11825 is the half that still resolves). + // The two authorable carriers that pointed here, + // `PluginSecurityManifest.scanResults` and `.vulnerabilities`, are separately + // tombstoned and registered in `RETIRED_KEYS_BY_MAJOR[18]`. + // + // No D2 conversion: a plugin security manifest is a package artifact a publisher + // ships, never a stack collection member and never a stored `sys_metadata` row, + // so the conversion chain has no seam that would see one (the sibling + // `kernel/PluginSecurityManifest:vulnerabilityDisclosure.responseTime` entry + // records the same reasoning for the same schema). + 'kernel/KernelSecurityScanResult', + // #15932 — ADR-0049 enforce-or-remove (director seat, decision batch #65, + // 2026-09-07, maintainer verbatim 「同意」). The other half of the scan-result + // family: a CVE-shaped vulnerability record (severity, CVSS score, affected and + // fixed versions, exploit/patch availability, remediation, disclosure dates) that + // nothing ever constructed, validated or consulted. It reached this build through + // exactly three referents, all now gone — `KernelSecurityScanResult.vulnerabilities` + // and `.dependencyVulnerabilities[].vulnerability` (that def leaves in the same + // change) and `PluginSecurityManifest.vulnerabilities` (tombstoned). Retired + // together with `KernelSecurityScanResult` because declaring a vulnerability + // vocabulary with no scan to carry it reads as a capability, which is the + // ADR-0049 shape. + // + // Whole-def retirement for the reason its sibling entry records, and the same + // disposition: `RETIRED_DEFS_BY_MAJOR[18]` plus + // `plugin-security-scan-result-surface-retired`, no D2 conversion, no tombstone + // of its own. Its two authorable defaults — + // `KernelSecurityVulnerability:exploitAvailable = false` and + // `:patchAvailable = false` — leave `authorable-defaults/kernel.json` with the + // def; nothing ever parsed this schema, so no released toolchain ever + // materialized either value into an artifact and there is no residue to accept. + // + // ⛔ NOT in scope, and deliberately untouched: the unprefixed + // `SecurityVulnerabilitySchema` / `SecurityScanResultSchema` pair in the sibling + // module `kernel/plugin-security.zod.ts`. Those are separate defs with their own + // self-test and are outside this ruling. + 'kernel/KernelSecurityVulnerability', // #13135 — ADR-0049 enforce-or-remove (maintainer ruling 2026-08-29 on // #12057: retirement adopted, re-scope rejected; re-charter #13135 executes // the widened surface). Part of the whole-module removal of