diff --git a/CHANGELOG.md b/CHANGELOG.md index fdf729e..0f3811b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable distribution changes are documented here. This project follows semantic versioning for skill and plugin artifacts. +## Unreleased + +### Changed + +- Aligned the repository publisher mirror with the live founder-operated Openly Useful authority while Openly Useful LLC remains formation-pending. +- Made npm package readiness depend on direct founder-owner authorization, exact package/MCP namespaces, public-policy files, and deterministic release validation instead of LLC formation or provider marketplace review. +- Normalized both npm CLI `bin` maps and made the combined MCP build mark its bundled commands executable so npm preserves every published command without manifest correction. +- Kept npm account authentication at the actual registry boundary and retained marketplace, MCP Registry, deployment, and future LLC operation as separate workflows. + ## 1.2.0 - 2026-08-16 ### Added diff --git a/COMPLIANCE-PACKET.md b/COMPLIANCE-PACKET.md index af48d72..1a1b880 100644 --- a/COMPLIANCE-PACKET.md +++ b/COMPLIANCE-PACKET.md @@ -1,9 +1,10 @@ # Local compliance packet -Status: local identity, licensing, policy, and runtime-notice inputs are -implemented for release `1.2.0`. Public marketplace submission, package -publication, hosted operation, and business verification remain external -actions. This packet is not legal advice or publication authorization. +Status: identity, licensing, policy, runtime-notice, package, namespace, and +founder-authorization inputs are implemented for release `1.2.0`. npm package +publication is authorized but has not been performed. Marketplace submission, +MCP Registry submission, hosted operation, and provider review remain separate +external actions. This packet is not legal advice. ## Publisher and ownership record @@ -13,18 +14,22 @@ The repository mirrors the canonical Openly Useful publisher manifest in - Public publisher/developer brand: **Openly Useful**. - Planned legal entity: **Openly Useful LLC**. - Entity status: **formation-pending**. +- Current operator: **individual founder**, operating as Openly Useful. - Planned entity roles: publisher, operator, and licensee. - RunGlance authorship: **sole-author-confirmed**. - RunGlance copyright: personally owned by the individual founder. - Ownership transfer: **not required and not planned**. - Current open-source publication: founder-authorized. +- Current npm package publication: directly founder-owner authorized while + formation remains pending. - Future LLC publication: authorization documentation pending until after formation. -Openly Useful LLC must not be described as already formed or as the RunGlance -copyright owner. Its eventual publisher/operator/licensee role does not depend -on an assignment of ownership. Sole authorship and personal ownership are -owner-confirmed and are not public-activation gates. +Openly Useful LLC must not be described as already formed, as the current +operator, or as the RunGlance copyright owner. Its eventual +publisher/operator/licensee role does not depend on an assignment of ownership. +Sole authorship, personal ownership, and direct founder publication +authorization are owner-confirmed and do not depend on LLC formation. ## License and notices @@ -60,30 +65,26 @@ website, privacy, terms, and support values. Claude manifests carry the common publisher, homepage, repository, and license values; their physical skill copy also contains the canonical component metadata. -## External gates +## npm publication gate -The release may be locally distribution-ready while public publication remains -blocked by external state: +The fail-closed npm gate requires: -1. Complete and verify Openly Useful LLC formation before identifying it as the - active legal operator. -2. After formation, document the founder's authorization for LLC publication, - and verify the public repository and every policy/support URL anonymously. -3. Complete provider business/developer verification and domain-namespace - authentication. -4. Review the generated archives and checksums from the exact release commit. -5. Authorize each package publication, MCP Registry entry, marketplace - submission, and deployment separately. +1. Direct founder-owner authorization effective during formation. +2. Exact `@openly-useful` package and `org.openlyuseful` MCP contracts. +3. Canonical public policy files and authority metadata. +4. Current license, notices, generated wrappers, tests, and deterministic plans. -No IP assignment, ownership transfer, or ownership verification appears in -this gate list. The founder's personal ownership and sole authorship are already -confirmed, and transfer is neither required nor planned. +npm account authentication is not part of the static readiness claim. The +registry enforces it separately at the actual publish request. + +Provider review does not block npm. Marketplace, MCP Registry, deployment, and +future LLC operation each remain separately controlled. No IP assignment, +ownership transfer, or ownership verification appears in the npm gate. ## Completion criteria -`node scripts/release-check.mjs --json` must report valid local inputs and -`distributionReady: true`. While the publisher manifest records -`formation-pending`, it must continue to report `publishReady: false` and an -`entity-and-external-verification` gate. No local check creates or verifies an -external account, entity filing, registry entry, marketplace listing, or -deployment. +`node scripts/release-check.mjs --json` must report `valid: true`, +`distributionReady: true`, and `publishReady: true`; both package +`prepublishOnly` scripts must pass without publishing. No local check creates or +authenticates an npm account, files an entity, submits a registry or marketplace +listing, or deploys a service. diff --git a/PROVENANCE.md b/PROVENANCE.md index 0341b76..874d6e9 100644 --- a/PROVENANCE.md +++ b/PROVENANCE.md @@ -15,10 +15,12 @@ or ownership transfer to Openly Useful or to the planned Openly Useful LLC is required or represented by this repository. Current open-source publication is founder-authorized. -Openly Useful is the publisher and developer brand. Openly Useful LLC remains -formation-pending and must not be described as formed, active, or as the owner -or operator. A future LLC may publish, operate, and license the project only -under separately documented founder authorization after formation. +Openly Useful is the publisher and developer brand and is currently operated by +the individual founder. The founder-owner directly authorizes source and npm +package publication while formation is pending. Openly Useful LLC remains +formation-pending and must not be described as formed, active, as the current +operator, or as the owner. A future LLC may later publish, operate, and license +the project under separately documented founder authorization. The canonical public repository target for both products in release 1.2.0 is . Product packages, MCP Registry diff --git a/README.md b/README.md index 5c046b9..2449ac5 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ The `packages/mcp` commands apply only when that optional package is present. `n - `dist/server/index.js` - `dist/.openai/hosting.json` -`scripts/release-check.mjs` validates wrapper drift, version agreement, host-specific marketplace and manifest shapes, the MCP bundle boundary, package safety, changelog coverage, and deterministic archive plans. It reports public-publication gates separately from repository distribution readiness. Both publishable MCP packages also run `scripts/assert-publish-ready.mjs` at the `prepublishOnly` boundary, so `npm publish` fails closed until formation, authorization, namespace verification, live policy verification, and blocker clearance are all recorded. The test suite also extracts both plugin archives into isolated temporary directories and performs a pinned MCP handshake plus readiness and activity tool calls without installing dependencies. +`scripts/release-check.mjs` validates wrapper drift, version agreement, host-specific marketplace and manifest shapes, the MCP bundle boundary, package safety, changelog coverage, deterministic archive plans, package identities, namespaces, public-policy files, and founder publication authorization. Both publishable MCP packages run `scripts/assert-publish-ready.mjs` at the `prepublishOnly` boundary. npm publication is founder-authorized while LLC formation remains pending; the registry still enforces account authentication at the actual publish request, and provider marketplace review is a separate workflow that does not block npm. The test suite also extracts both plugin archives into isolated temporary directories and performs a pinned MCP handshake plus readiness and activity tool calls without installing dependencies. Third-party notices are generated from the pinned runtime dependency graphs rather than the full development toolchain: @@ -187,15 +187,19 @@ The distribution version is recorded in `VERSION`, both canonical component meta `publisher/publisher.json` is the repository mirror/consumer of . Openly Useful is the publisher/developer brand. Openly Useful LLC is the planned publisher, operator, and licensee, but remains formation-pending and must not be described as already formed. The `.org` identity is the canonical open-source, publisher, policy, security, and support surface; `.com` is the studio/commercial identity. Component metadata points to the canonical public repository and . -RunGlance was solely authored by and remains personally owned by the founder. Current open-source publication is founder-authorized. No IP assignment, ownership transfer, or ownership verification is required for activation; the future LLC can publish, operate, and license RunGlance after formation and after its founder authorization is documented, without becoming the copyright owner. +RunGlance was solely authored by and remains personally owned by the founder. Openly Useful is currently operated by the individual founder, who directly authorizes open-source and npm package publication while LLC formation remains pending. No IP assignment, ownership transfer, or ownership verification is required; a future LLC can later publish, operate, and license RunGlance under documented founder authorization without becoming the copyright owner. -Local distribution readiness and public activation are separate. `distributionReady` may be true when source, generated wrappers, license/notices, policies, metadata, and deterministic packages validate. While the publisher record remains `formation-pending`, `publishReady` must remain false. Public activation still requires: +Local distribution readiness and npm package readiness remain separate signals. `distributionReady` covers source, generated wrappers, license/notices, policies, metadata, and deterministic packages. `publishReady` additionally requires: -1. Openly Useful LLC formation and documentation of the founder's authorization for its publishing role; -2. anonymous reachability checks for the public repository and policy/support URLs; -3. provider business/developer verification and domain/namespace authentication; -4. review of generated archives and checksums from the exact release commit; and -5. separate authorization for each package publication, registry entry, marketplace submission, or deployment. +1. direct founder-owner authorization effective while LLC formation is pending; +2. exact npm package and MCP namespace contracts; +3. the canonical privacy, terms, security, and support policy files; +4. current license, notices, generated wrappers, tests, and deterministic package plans. + +Account authentication is not fabricated by the static readiness result. npm +enforces it separately when an actual registry request is made. + +OpenAI/Claude provider review, marketplace submission, MCP Registry submission, deployment, and future LLC operation remain separate workflows. None is inferred from npm readiness, and provider review does not block npm publication. The repository contains the unmodified Apache License 2.0 text and a deterministic third-party notice bundle. Apache-2.0 does not require a project-specific copyright-holder/year placeholder in the license text. diff --git a/RELEASE-POLICY-DECISIONS.md b/RELEASE-POLICY-DECISIONS.md index 472267f..865d47a 100644 --- a/RELEASE-POLICY-DECISIONS.md +++ b/RELEASE-POLICY-DECISIONS.md @@ -1,9 +1,10 @@ # Local release and monitoring policy record -Status: approved local preparation decisions recorded. External monitoring, -publication, marketplace, and business-verification actions remain inactive. -This record does not authorize deployment, installation, scheduling, -notification, or publication. +Status: founder-authorized open-source and npm package publication is recorded. +External monitoring, marketplace submission, MCP Registry submission, +deployment, and provider-review actions remain separate. This record does not +authorize installation, scheduling, notification, deployment, or provider +submission. ## Publisher, authorship, and entity boundary @@ -11,17 +12,19 @@ notification, or publication. component and provider metadata. - **Openly Useful LLC** is the planned publisher/operator/licensee and remains `formation-pending`. +- Openly Useful is currently operated by the individual founder. - RunGlance was solely authored by, and remains personally owned by, the founder. - No copyright assignment or ownership transfer is required or planned. -- The future entity may publish, operate, and license RunGlance under founder - authorization without becoming its copyright owner. +- The founder-owner directly authorizes current source and npm package + publication while formation is pending. The future entity may later publish, + operate, and license RunGlance under founder authorization without becoming + its copyright owner. - Sole authorship and personal ownership are owner-confirmed facts, not public-activation gates. -- Public marketplace, registry, package, and commercial activation remains - pending entity formation, documentation of founder authorization for future - LLC publication, provider business verification, public URL reachability, - and separate authorization. +- Provider marketplace review does not block npm package publication. + Marketplace submission, MCP Registry submission, deployment, and commercial + activation retain separate authorization and validation paths. ## Approved monitoring preparation parameters @@ -81,6 +84,7 @@ escalation procedure. The public-safe publisher mirror contains brand, policy, namespace, planned entity-role, and non-identifying ownership-status facts. Private legal filings, personal identifiers, account credentials, and restricted contact records must -not enter archives. No entry here authorizes a commit, push, merge, deployment, -installation, schedule, publication, filing, purchase, outreach, or account -change. +not enter archives. This record documents the founder's current source and npm +package publication authorization; it does not itself initiate a commit, push, +merge, deployment, installation, schedule, registry request, filing, purchase, +outreach, or account change. diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 09258f3..aaf5fc6 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -6,7 +6,7 @@ Optional local, read-only companions for Project Status readiness and standalone The publishable Project Status package identity is `@openly-useful/project-status-mcp`, with official MCP Registry name `org.openlyuseful/project-status`. This source package deliberately retains both the `project-status-mcp` and `runglance-mcp` local bins so existing plugin builds and tests keep working. Public RunGlance packaging is owned by the sibling `packages/runglance-mcp` bundle and uses its independent package and registry identity. -Neither package nor either MCP Registry record is published by this repository. External publication remains gated on formation of the planned publisher, explicit authorization, namespace verification, and public policy verification. +The founder-owner directly authorizes npm package publication while Openly Useful LLC formation remains pending. The package's `prepublishOnly` gate validates the founder authorization, package/MCP namespaces, public-policy files, licensing, generated wrappers, tests, and deterministic release inputs. npm account authentication is enforced by the registry at the actual publish request; provider marketplace review is separate and does not block npm. This subpackage targets Node.js 20+, the stable MCP TypeScript SDK v2 split packages, the 2026-07-28 protocol, and Zod v4 Standard Schemas. It serves stdio only and never diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 1dccb84..52eb7a2 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -7,8 +7,8 @@ "license": "Apache-2.0", "type": "module", "bin": { - "project-status-mcp": "./dist/index.js", - "runglance-mcp": "./dist/runglance-index.js" + "project-status-mcp": "dist/index.js", + "runglance-mcp": "dist/runglance-index.js" }, "files": [ "dist/index.js", @@ -17,7 +17,7 @@ "LICENSE" ], "scripts": { - "build": "node scripts/sync-license.mjs && tsc -p tsconfig.json && esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/index.js --charset=utf8 --legal-comments=eof && esbuild src/runglance-index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/runglance-index.js --charset=utf8 --legal-comments=eof", + "build": "node scripts/sync-license.mjs && tsc -p tsconfig.json && esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/index.js --charset=utf8 --legal-comments=eof && esbuild src/runglance-index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/runglance-index.js --charset=utf8 --legal-comments=eof && node scripts/finalize-build.mjs", "typecheck": "tsc -p tsconfig.json --noEmit", "test": "node --test tests/*.test.mjs", "prepack": "npm run build && npm test", diff --git a/packages/mcp/scripts/finalize-build.mjs b/packages/mcp/scripts/finalize-build.mjs new file mode 100644 index 0000000..b7564b4 --- /dev/null +++ b/packages/mcp/scripts/finalize-build.mjs @@ -0,0 +1,12 @@ +#!/usr/bin/env node + +import { chmodSync, existsSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageRoot = resolve(fileURLToPath(new URL("..", import.meta.url))); +for (const relativePath of ["dist/index.js", "dist/runglance-index.js"]) { + const output = resolve(packageRoot, relativePath); + if (!existsSync(output)) throw new Error(`Missing MCP executable: ${relativePath}`); + chmodSync(output, 0o755); +} diff --git a/packages/mcp/tests/identity.test.mjs b/packages/mcp/tests/identity.test.mjs index a04a908..853cc96 100644 --- a/packages/mcp/tests/identity.test.mjs +++ b/packages/mcp/tests/identity.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; +import { readFileSync, statSync } from "node:fs"; import { join, resolve } from "node:path"; import test from "node:test"; import { fileURLToPath } from "node:url"; @@ -28,9 +28,14 @@ test("Project Status npm and MCP Registry identities agree", () => { test("combined source package preserves both existing local bins", () => { const packageJson = json(join(packageRoot, "package.json")); assert.deepEqual(packageJson.bin, { - "project-status-mcp": "./dist/index.js", - "runglance-mcp": "./dist/runglance-index.js", + "project-status-mcp": "dist/index.js", + "runglance-mcp": "dist/runglance-index.js", }); + for (const path of Object.values(packageJson.bin)) { + const output = join(packageRoot, path); + assert.ok(readFileSync(output, "utf8").startsWith("#!/usr/bin/env node\n")); + assert.notEqual(statSync(output).mode & 0o111, 0); + } }); test("published package carries the owner-approved root license", () => { diff --git a/packages/runglance-mcp/README.md b/packages/runglance-mcp/README.md index 7718dfd..c1823f0 100644 --- a/packages/runglance-mcp/README.md +++ b/packages/runglance-mcp/README.md @@ -17,4 +17,4 @@ node dist/index.js --help The shared pinned TypeScript/esbuild toolchain under `../mcp` must be present in a source checkout. The published bundle has no runtime dependency installation step. -Nothing in this package authorizes npm publication or MCP Registry submission. External publication remains gated on formation of the planned publisher, explicit publisher authorization, namespace verification, and public policy verification. +The founder-owner directly authorizes npm package publication while Openly Useful LLC formation remains pending. The package's `prepublishOnly` gate validates the founder authorization, package/MCP namespaces, public-policy files, licensing, generated wrappers, tests, and deterministic release inputs. npm account authentication is enforced by the registry at the actual publish request; provider marketplace review and MCP Registry submission remain separate workflows. diff --git a/packages/runglance-mcp/package.json b/packages/runglance-mcp/package.json index 564accc..4d53b17 100644 --- a/packages/runglance-mcp/package.json +++ b/packages/runglance-mcp/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "type": "module", "bin": { - "runglance-mcp": "./dist/index.js" + "runglance-mcp": "dist/index.js" }, "files": [ "dist/index.js", diff --git a/packages/runglance-mcp/tests/identity.test.mjs b/packages/runglance-mcp/tests/identity.test.mjs index 61d9695..539d66c 100644 --- a/packages/runglance-mcp/tests/identity.test.mjs +++ b/packages/runglance-mcp/tests/identity.test.mjs @@ -23,6 +23,7 @@ test("RunGlance npm and MCP Registry identities agree", () => { assert.equal(serverJson.packages[0].version, packageJson.version); assert.equal(serverJson.packages[0].transport.type, "stdio"); assert.equal(serverJson.$schema, "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json"); + assert.deepEqual(packageJson.bin, { "runglance-mcp": "dist/index.js" }); }); test("standalone package and combined source package have distinct identities", () => { diff --git a/publisher/publisher.json b/publisher/publisher.json index af3ad59..04061db 100644 --- a/publisher/publisher.json +++ b/publisher/publisher.json @@ -8,6 +8,11 @@ "plannedName": "Openly Useful LLC", "activeName": null, "status": "formation-pending", + "currentOperator": { + "type": "founder-individual", + "displayName": "Founder of Openly Useful", + "operatingAs": "Openly Useful" + }, "plannedRoles": [ "publisher", "operator", @@ -37,22 +42,30 @@ "security": "https://openlyuseful.org/security", "support": "https://openlyuseful.org/support" }, + "policyMirrors": { + "privacy": "https://github.com/Openly-Useful/openlyuseful.org/blob/main/legal/privacy.html", + "terms": "https://github.com/Openly-Useful/openlyuseful.org/blob/main/legal/terms.html", + "security": "https://github.com/Openly-Useful/openlyuseful.org/blob/main/security.html", + "support": "https://github.com/Openly-Useful/openlyuseful.org/blob/main/support.html" + }, + "authorityManifestMirror": "https://github.com/Openly-Useful/openlyuseful.org/blob/main/publisher/manifest.json", "publication": { "localGenerationAllowed": true, "localTestingAllowed": true, - "externalPublicationAllowed": false, - "authorization": "withheld", + "externalPublicationAllowed": true, + "authorization": "granted", + "authorizationBasis": "founder-owner-direct", + "effectiveWhileFormationPending": true, "blockingRequirements": [ - "formation-active", - "publisher-authorization", "namespace-verification", - "public-policy-url-verification" + "provider-account-authentication", + "provider-review" ] }, "artifactPolicy": { "authorityEndpoint": "This manifest is the published authority endpoint for Openly Useful publisher and marketplace verification. It is projected from the governed editable publisher source.", "derivation": "Provider-specific skills, MCP manifests, packages, and marketplace listings must derive publisher identity, domains, policy URLs, contacts, and namespaces from this published authority endpoint.", - "activation": "The planned legal entity must not be represented as formed, active, or the operator until formation and required publisher verification are complete." + "activation": "Openly Useful is founder-operated while Openly Useful LLC formation is pending. External source and registry publication is authorized by the founder-owner. The planned LLC must not be represented as formed, active, or the operator until formation is accepted; later LLC operation does not require a transfer of RunGlance ownership." }, "repositoryContext": { "repositories": { @@ -68,5 +81,5 @@ "currentOpenSourcePublication": "founder-authorized", "futureEntityPublishing": "documentation-pending-after-formation" }, - "lastUpdated": "2026-08-16" + "lastUpdated": "2026-08-23" } diff --git a/scripts/assert-publish-ready.mjs b/scripts/assert-publish-ready.mjs index d2c9258..39308ce 100644 --- a/scripts/assert-publish-ready.mjs +++ b/scripts/assert-publish-ready.mjs @@ -15,14 +15,14 @@ export function assertPublishReady() { throw new Error([ "PUBLICATION BLOCKED: the canonical Openly Useful release gate is not satisfied.", ...reasons.map((reason) => `- ${reason}`), - "Complete formation, publisher authorization, namespace verification, live public-policy URL verification, and clear blockingRequirements before publishing.", + "Confirm the founder-owner authorization, package and namespace contracts, and public-policy files before publishing. npm account authentication is enforced by the registry; provider review is a separate workflow.", ].join("\n")); } export async function main(argv = process.argv.slice(2)) { if (argv.length !== 0) throw new Error("Usage: assert-publish-ready.mjs"); const result = assertPublishReady(); - process.stdout.write(`PUBLICATION READY ${result.version}\n`); + process.stdout.write(`NPM PUBLICATION READY ${result.version}\n`); return 0; } diff --git a/scripts/release-check.mjs b/scripts/release-check.mjs index 543f8c0..bb7894f 100644 --- a/scripts/release-check.mjs +++ b/scripts/release-check.mjs @@ -175,12 +175,19 @@ export function validateMcpDistributionIdentity(version, errors, root = releaseR registryPath: join(root, "mcp-registry", "project-status", "server.json"), packageName: "@openly-useful/project-status-mcp", mcpName: "org.openlyuseful/project-status", + bin: { + "project-status-mcp": "dist/index.js", + "runglance-mcp": "dist/runglance-index.js", + }, + files: ["dist/index.js", "dist/runglance-index.js", "README.md", "LICENSE"], }, runGlance: { packagePath: join(root, "packages", "runglance-mcp", "package.json"), registryPath: join(root, "mcp-registry", "runglance", "server.json"), packageName: "@openly-useful/runglance-mcp", mcpName: "org.openlyuseful/runglance", + bin: { "runglance-mcp": "dist/index.js" }, + files: ["dist/index.js", "README.md", "LICENSE"], }, }; const result = {}; @@ -203,6 +210,10 @@ export function validateMcpDistributionIdentity(version, errors, root = releaseR if (packageManifest.mcpName !== expected.mcpName) errors.push(`${key} package mcpName must be ${expected.mcpName}`); if (packageManifest.version !== version) errors.push(`${key} package version differs from VERSION`); if (packageManifest.license !== "Apache-2.0") errors.push(`${key} package license must be Apache-2.0`); + if (packageManifest.private === true) errors.push(`${key} package cannot be private`); + if (packageManifest.publishConfig?.access !== "public") errors.push(`${key} package publishConfig.access must be public`); + if (JSON.stringify(packageManifest.bin) !== JSON.stringify(expected.bin)) errors.push(`${key} package bin contract is invalid`); + if (JSON.stringify(packageManifest.files) !== JSON.stringify(expected.files)) errors.push(`${key} package files allowlist is invalid`); if (packageManifest.repository?.url !== `git+${repository}.git`) errors.push(`${key} package repository is invalid`); if (packageManifest.bugs !== "https://openlyuseful.org/support") errors.push(`${key} package support URL is invalid`); if (registryManifest.name !== expected.mcpName) errors.push(`${key} registry name must match package mcpName`); @@ -222,19 +233,26 @@ export function validateMcpDistributionIdentity(version, errors, root = releaseR return result; } -export function externalActivationSatisfied(publisher) { +export function founderPublicationAuthorizationSatisfied(publisher) { return Boolean( - publisher?.legal?.status === "active" - && publisher?.legal?.activeName === publisher?.legal?.plannedName - && publisher?.repositoryContext?.futureEntityPublishing === "documented" + publisher?.legal?.status === "formation-pending" + && publisher?.legal?.activeName === null + && publisher?.legal?.currentOperator?.type === "founder-individual" && publisher?.publication?.externalPublicationAllowed === true - && publisher?.publication?.authorization === "authorized" + && publisher?.publication?.authorization === "granted" + && publisher?.publication?.authorizationBasis === "founder-owner-direct" + && publisher?.publication?.effectiveWhileFormationPending === true && Array.isArray(publisher?.publication?.blockingRequirements) - && publisher.publication.blockingRequirements.length === 0 + && !publisher.publication.blockingRequirements.includes("formation-active") + && !publisher.publication.blockingRequirements.includes("publisher-authorization") ); } -function releaseGates(release) { +// Backwards-compatible export for existing release consumers. External package +// publication is currently authorized by the founder-owner, not by an active LLC. +export const externalActivationSatisfied = founderPublicationAuthorizationSatisfied; + +function releaseGates(release, mcpDistributions) { const licensePath = join(releaseRoot, "LICENSE"); const licenseDigest = existsSync(licensePath) && statSync(licensePath).isFile() ? createHash("sha256").update(readFileSync(licensePath)).digest("hex") @@ -281,7 +299,13 @@ function releaseGates(release) { && release.publisher?.repositoryContext?.runGlanceCopyright?.transferRequired === false && release.publisher?.repositoryContext?.currentOpenSourcePublication === "founder-authorized", ); - const externalActivationComplete = externalActivationSatisfied(release.publisher); + const packageNamespaceContractsValid = Boolean( + mcpDistributions?.projectStatus?.packageName === `${release.publisher?.namespaces?.npm}/project-status-mcp` + && mcpDistributions?.runGlance?.packageName === `${release.publisher?.namespaces?.npm}/runglance-mcp` + && mcpDistributions?.projectStatus?.mcpName === `${release.publisher?.namespaces?.openSourceMcp}/project-status` + && mcpDistributions?.runGlance?.mcpName === `${release.publisher?.namespaces?.openSourceMcp}/runglance` + ); + const founderPublicationAuthorized = founderPublicationAuthorizationSatisfied(release.publisher); return [ { id: "apache-2.0-license", @@ -309,6 +333,13 @@ function releaseGates(release) { ? "Both products use Openly Useful as the publisher/developer brand and reference the canonical publisher mirror." : "Publisher metadata is incomplete or inconsistent across Project Status and RunGlance.", }, + { + id: "package-and-namespace-contracts", + status: packageNamespaceContractsValid ? "satisfied" : "pending_implementation", + detail: packageNamespaceContractsValid + ? "Both npm package names and MCP identities match the canonical Openly Useful namespaces." + : "The publishable package or MCP identities do not match the canonical Openly Useful namespaces.", + }, { id: "founder-record-and-open-source-authorization", status: founderRecordConfirmed ? "satisfied" : "pending_implementation", @@ -317,11 +348,11 @@ function releaseGates(release) { : "The owner-confirmed founder record or current open-source publication authorization is not encoded correctly.", }, { - id: "entity-and-external-verification", - status: externalActivationComplete ? "satisfied" : "pending_external_verification", - detail: externalActivationComplete - ? "Publisher entity formation, future-entity publishing authorization, external verification, and blocker clearance are recorded as complete." - : "Openly Useful LLC formation, documentation of its future publishing authorization, provider/business verification, public URL reachability, and external activation remain pending. IP assignment, ownership transfer, and ownership verification are not required.", + id: "founder-authorized-package-publication", + status: founderPublicationAuthorized ? "satisfied" : "pending_authorization", + detail: founderPublicationAuthorized + ? "The founder-owner directly authorizes package publication while LLC formation remains pending; provider review is a separate workflow and does not block npm." + : "Package publication requires direct founder-owner authorization that remains effective while LLC formation is pending.", }, ]; } @@ -355,7 +386,7 @@ export function checkRelease() { if (!readme.includes("skill/project-status")) errors.push("README.md must identify the canonical skill source"); if (!readme.includes("release-sync.mjs")) errors.push("README.md must document generated-wrapper synchronization"); } - const gates = releaseGates(release); + const gates = releaseGates(release, mcpDistributions); const uniqueErrors = [...new Set(errors)]; return { valid: uniqueErrors.length === 0, @@ -388,7 +419,7 @@ function format(result) { `RELEASE CHECK ${result.valid ? "OK" : "FAILED"}`, `Version: ${result.version ?? "unknown"}`, `Distribution packages: ${result.distributionReady ? "ready" : "not ready"}`, - `Public publication: ${result.publishReady ? "ready" : "waiting on external verification and activation"}`, + `npm package publication: ${result.publishReady ? "ready" : "waiting on package, namespace, policy, or founder-authorization validation"}`, `MCP companion: ${result.mcpIncluded ? "included" : "not built; omitted"}`, ...result.releaseGates.map((gate) => `- ${gate.id}: ${gate.status} — ${gate.detail}`), ...result.errors.map((error) => `ERROR: ${error}`), diff --git a/scripts/release-sync.mjs b/scripts/release-sync.mjs index f8144e4..b1e883f 100644 --- a/scripts/release-sync.mjs +++ b/scripts/release-sync.mjs @@ -151,8 +151,12 @@ export function publisherErrors(publisher) { } if (publisher.organization?.github !== "https://github.com/Openly-Useful") errors.push("publisher organization GitHub URL is invalid"); if (publisher.legal?.plannedName !== "Openly Useful LLC") errors.push("publisher planned legal entity must be Openly Useful LLC"); - if (!["formation-pending", "active"].includes(publisher.legal?.status)) errors.push("publisher legal entity status must be formation-pending or active"); - if (publisher.legal?.status === "formation-pending" && publisher.legal?.activeName !== null) errors.push("formation-pending publisher cannot have an active legal name"); + if (publisher.legal?.status !== "formation-pending") errors.push("publisher legal entity status must remain formation-pending until formation is accepted"); + if (publisher.legal?.activeName !== null) errors.push("formation-pending publisher cannot have an active legal name"); + const currentOperator = publisher.legal?.currentOperator; + if (currentOperator?.type !== "founder-individual") errors.push("publisher current operator must be founder-individual"); + if (currentOperator?.displayName !== "Founder of Openly Useful") errors.push("publisher current operator display name is invalid"); + if (currentOperator?.operatingAs !== "Openly Useful") errors.push("publisher current operator must operate as Openly Useful"); const plannedRoles = publisher.legal?.plannedRoles; if (!Array.isArray(plannedRoles) || JSON.stringify([...plannedRoles].sort()) !== JSON.stringify(["licensee", "operator", "publisher"])) { errors.push("publisher planned roles must be licensee, operator, and publisher"); @@ -177,6 +181,18 @@ export function publisherErrors(publisher) { for (const [field, expected] of Object.entries(expectedPolicies)) { if (publisher.policies?.[field] !== expected) errors.push(`publisher policies.${field} must be ${expected}`); } + const expectedPolicyMirrors = { + privacy: "https://github.com/Openly-Useful/openlyuseful.org/blob/main/legal/privacy.html", + terms: "https://github.com/Openly-Useful/openlyuseful.org/blob/main/legal/terms.html", + security: "https://github.com/Openly-Useful/openlyuseful.org/blob/main/security.html", + support: "https://github.com/Openly-Useful/openlyuseful.org/blob/main/support.html", + }; + for (const [field, expected] of Object.entries(expectedPolicyMirrors)) { + if (publisher.policyMirrors?.[field] !== expected) errors.push(`publisher policyMirrors.${field} must be ${expected}`); + } + if (publisher.authorityManifestMirror !== "https://github.com/Openly-Useful/openlyuseful.org/blob/main/publisher/manifest.json") { + errors.push("publisher authorityManifestMirror is invalid"); + } if (publisher.contacts?.public !== "hello@openlyuseful.org") errors.push("publisher public contact is invalid"); if (publisher.contacts?.routing !== "Use the email subject to route publishing, security, legal, and support requests.") errors.push("publisher contact routing is invalid"); if (publisher.namespaces?.openSourceMcp !== "org.openlyuseful") errors.push("publisher open-source MCP namespace must be org.openlyuseful"); @@ -187,12 +203,12 @@ export function publisherErrors(publisher) { if (publisher.repositoryContext?.repositories?.runGlance !== repository) errors.push("publisher RunGlance repository is invalid"); if (publisher.publication?.localGenerationAllowed !== true) errors.push("publisher must allow local generation"); if (publisher.publication?.localTestingAllowed !== true) errors.push("publisher must allow local testing"); - if (typeof publisher.publication?.externalPublicationAllowed !== "boolean") errors.push("publisher externalPublicationAllowed must be boolean"); - if (!["withheld", "authorized"].includes(publisher.publication?.authorization)) errors.push("publisher publication authorization is invalid"); - if (publisher.legal?.status === "formation-pending" && publisher.publication?.externalPublicationAllowed !== false) errors.push("external publication must remain disabled while formation is pending"); - if (publisher.legal?.status === "formation-pending" && publisher.publication?.authorization !== "withheld") errors.push("publication authorization must remain withheld while formation is pending"); + if (publisher.publication?.externalPublicationAllowed !== true) errors.push("publisher external publication must be founder-authorized"); + if (publisher.publication?.authorization !== "granted") errors.push("publisher publication authorization must be granted"); + if (publisher.publication?.authorizationBasis !== "founder-owner-direct") errors.push("publisher authorization basis must be founder-owner-direct"); + if (publisher.publication?.effectiveWhileFormationPending !== true) errors.push("founder publication authorization must remain effective while formation is pending"); const blockers = publisher.publication?.blockingRequirements; - const allowedBlockers = ["formation-active", "namespace-verification", "public-policy-url-verification", "publisher-authorization"]; + const allowedBlockers = ["namespace-verification", "provider-account-authentication", "provider-review"]; if (!Array.isArray(blockers)) { errors.push("publisher blockingRequirements must be an array"); } else { @@ -201,29 +217,8 @@ export function publisherErrors(publisher) { for (const blocker of uniqueBlockers) { if (!allowedBlockers.includes(blocker)) errors.push(`publisher blockingRequirements contains an unknown requirement: ${blocker}`); } - if (publisher.legal?.status === "formation-pending" - && JSON.stringify([...uniqueBlockers].sort()) !== JSON.stringify([...allowedBlockers].sort())) { - errors.push("formation-pending publisher blocking requirements are incomplete"); - } - if (publisher.legal?.status === "active" && uniqueBlockers.includes("formation-active")) { - errors.push("active publisher cannot retain the formation-active blocker"); - } - const activationAuthorized = publisher.publication?.externalPublicationAllowed === true - && publisher.publication?.authorization === "authorized"; - if (activationAuthorized && uniqueBlockers.length !== 0) { - errors.push("authorized external publication requires all blocking requirements to be cleared"); - } - if (publisher.publication?.externalPublicationAllowed === true && publisher.publication?.authorization !== "authorized") { - errors.push("external publication cannot be enabled without publisher authorization"); - } - if (publisher.publication?.authorization === "authorized" && publisher.publication?.externalPublicationAllowed !== true) { - errors.push("publisher authorization and external publication activation must advance together"); - } - if (activationAuthorized && publisher.legal?.status !== "active") { - errors.push("external publication cannot be activated before the publisher entity is active"); - } - if (activationAuthorized && publisher.repositoryContext?.futureEntityPublishing !== "documented") { - errors.push("external publication requires documented future-entity publishing authorization"); + if (uniqueBlockers.includes("formation-active") || uniqueBlockers.includes("publisher-authorization")) { + errors.push("LLC formation and separate publisher authorization cannot block founder-authorized publication"); } } for (const field of ["authorityEndpoint", "derivation", "activation"]) { diff --git a/tests/publisher-contract.test.mjs b/tests/publisher-contract.test.mjs index 50c9cea..dd6ad4e 100644 --- a/tests/publisher-contract.test.mjs +++ b/tests/publisher-contract.test.mjs @@ -15,6 +15,12 @@ test("publisher mirror records the formation-pending one-entity boundary without assert.equal(publisher.displayName, "Openly Useful"); assert.equal(publisher.legal.plannedName, "Openly Useful LLC"); assert.equal(publisher.legal.status, "formation-pending"); + assert.equal(publisher.legal.activeName, null); + assert.deepEqual(publisher.legal.currentOperator, { + type: "founder-individual", + displayName: "Founder of Openly Useful", + operatingAs: "Openly Useful", + }); assert.deepEqual([...publisher.legal.plannedRoles].sort(), ["licensee", "operator", "publisher"]); assert.deepEqual(publisher.repositoryContext.runGlanceCopyright, { authorshipStatus: "sole-author-confirmed", @@ -24,8 +30,15 @@ test("publisher mirror records the formation-pending one-entity boundary without }); assert.equal(publisher.repositoryContext.currentOpenSourcePublication, "founder-authorized"); assert.equal(publisher.repositoryContext.futureEntityPublishing, "documentation-pending-after-formation"); - assert.equal(publisher.publication.externalPublicationAllowed, false); - assert.equal(publisher.publication.authorization, "withheld"); + assert.equal(publisher.publication.externalPublicationAllowed, true); + assert.equal(publisher.publication.authorization, "granted"); + assert.equal(publisher.publication.authorizationBasis, "founder-owner-direct"); + assert.equal(publisher.publication.effectiveWhileFormationPending, true); + assert.deepEqual(publisher.publication.blockingRequirements, [ + "namespace-verification", + "provider-account-authentication", + "provider-review", + ]); }); test("publisher and component metadata use reachable public endpoints and the routed contact", () => { @@ -36,6 +49,8 @@ test("publisher and component metadata use reachable public endpoints and the ro }); assert.equal(publisher.policies.support, "https://openlyuseful.org/support"); assert.equal(publisher.policies.security, "https://openlyuseful.org/security"); + assert.equal(publisher.authorityManifestMirror, "https://github.com/Openly-Useful/openlyuseful.org/blob/main/publisher/manifest.json"); + assert.equal(publisher.policyMirrors.privacy, "https://github.com/Openly-Useful/openlyuseful.org/blob/main/legal/privacy.html"); for (const product of ["project-status", "runglance"]) { const metadata = JSON.parse(readFileSync(join(root, "skill", product, "assets", "package-metadata.json"), "utf8")); @@ -54,14 +69,23 @@ test("publisher and component metadata use reachable public endpoints and the ro } }); -test("publisher validator rejects an ownership transfer or premature entity authorization claim", () => { +test("publisher validator rejects an ownership transfer or invalid founder authorization claim", () => { const transferred = structuredClone(publisher); transferred.repositoryContext.runGlanceCopyright.transferRequired = true; assert.match(publisherErrors(transferred).join("\n"), /transferRequired must be false/); - const premature = structuredClone(publisher); - premature.repositoryContext.futureEntityPublishing = "founder-authorized-license"; - assert.match(publisherErrors(premature).join("\n"), /future entity publishing authorization/); + const wrongBasis = structuredClone(publisher); + wrongBasis.publication.authorizationBasis = "future-llc"; + assert.match(publisherErrors(wrongBasis).join("\n"), /authorization basis must be founder-owner-direct/); + + const prematureEntity = structuredClone(publisher); + prematureEntity.legal.status = "active"; + prematureEntity.legal.activeName = prematureEntity.legal.plannedName; + assert.match(publisherErrors(prematureEntity).join("\n"), /must remain formation-pending/); + + const entityBlocked = structuredClone(publisher); + entityBlocked.publication.blockingRequirements.push("formation-active", "publisher-authorization"); + assert.match(publisherErrors(entityBlocked).join("\n"), /LLC formation and separate publisher authorization cannot block founder-authorized publication/); }); test("LICENSE is the exact Apache License 2.0 reference text", () => { diff --git a/tests/release-plugin.test.mjs b/tests/release-plugin.test.mjs index 9082671..3a8b453 100644 --- a/tests/release-plugin.test.mjs +++ b/tests/release-plugin.test.mjs @@ -5,7 +5,7 @@ import { join } from "node:path"; import { spawnSync } from "node:child_process"; import test from "node:test"; import { fileURLToPath } from "node:url"; -import { checkRelease, externalActivationSatisfied, validateCompanionVersions, validateMcpDistributionIdentity } from "../scripts/release-check.mjs"; +import { checkRelease, founderPublicationAuthorizationSatisfied, validateCompanionVersions, validateMcpDistributionIdentity } from "../scripts/release-check.mjs"; import { bundledEntrypointErrors, inspectReleaseState, publisherErrors } from "../scripts/release-sync.mjs"; const root = fileURLToPath(new URL("..", import.meta.url)); @@ -119,7 +119,7 @@ test("release sync check is read-only and clean", () => { assert.equal(after.valid, true, after.errors.join("\n")); }); -test("release check separates local distribution readiness from external activation", () => { +test("release check separates distribution and npm readiness from provider workflows", () => { const result = checkRelease(); assert.equal(result.valid, true, result.errors.join("\n")); assert.equal(result.distributionReady, true); @@ -147,47 +147,52 @@ test("release check separates local distribution readiness from external activat const noticesGate = result.releaseGates.find((gate) => gate.id === "third-party-notices"); const policyGate = result.releaseGates.find((gate) => gate.id === "public-policy-files"); const publisherGate = result.releaseGates.find((gate) => gate.id === "publisher-contract"); + const packageGate = result.releaseGates.find((gate) => gate.id === "package-and-namespace-contracts"); const founderGate = result.releaseGates.find((gate) => gate.id === "founder-record-and-open-source-authorization"); - const externalGate = result.releaseGates.find((gate) => gate.id === "entity-and-external-verification"); - for (const gate of [licenseGate, noticesGate, policyGate, publisherGate, founderGate]) { + const authorizationGate = result.releaseGates.find((gate) => gate.id === "founder-authorized-package-publication"); + for (const gate of [licenseGate, noticesGate, policyGate, publisherGate, packageGate, founderGate, authorizationGate]) { assert.equal(gate?.status, "satisfied", JSON.stringify(gate)); } - assert.equal(externalGate?.status, "pending_external_verification"); - assert.match(externalGate.detail, /IP assignment, ownership transfer, and ownership verification are not required/); - assert.equal(result.publishReady, false); + assert.match(authorizationGate.detail, /provider review is a separate workflow and does not block npm/); + assert.equal(result.publishReady, true); }); -test("publisher activation is fail-closed until every blocker is cleared", () => { +test("founder package authorization is effective while LLC formation and provider review remain pending", () => { const pending = json(join(root, "publisher", "publisher.json")); - assert.equal(externalActivationSatisfied(pending), false); + assert.equal(pending.legal.status, "formation-pending"); + assert.equal(pending.legal.activeName, null); + assert.ok(pending.publication.blockingRequirements.includes("provider-review")); + assert.equal(founderPublicationAuthorizationSatisfied(pending), true); assert.deepEqual(publisherErrors(pending), []); - const active = structuredClone(pending); - active.legal.status = "active"; - active.legal.activeName = active.legal.plannedName; - active.repositoryContext.futureEntityPublishing = "documented"; - active.publication.externalPublicationAllowed = true; - active.publication.authorization = "authorized"; - active.publication.blockingRequirements = []; - assert.deepEqual(publisherErrors(active), []); - assert.equal(externalActivationSatisfied(active), true); + const revoked = structuredClone(pending); + revoked.publication.authorization = "withheld"; + assert.equal(founderPublicationAuthorizationSatisfied(revoked), false); + assert.match(publisherErrors(revoked).join("\n"), /authorization must be granted/); - active.publication.blockingRequirements = ["namespace-verification"]; - assert.equal(externalActivationSatisfied(active), false); - assert.match(publisherErrors(active).join("\n"), /requires all blocking requirements to be cleared/); + const entityBlocked = structuredClone(pending); + entityBlocked.publication.blockingRequirements.push("formation-active"); + assert.equal(founderPublicationAuthorizationSatisfied(entityBlocked), false); + assert.match(publisherErrors(entityBlocked).join("\n"), /LLC formation and separate publisher authorization cannot block founder-authorized publication/); }); -test("npm publication entry points invoke the canonical fail-closed release assertion", () => { +test("both npm prepublishOnly entry points pass the canonical founder-authorized release assertion", () => { for (const packagePath of ["packages/mcp/package.json", "packages/runglance-mcp/package.json"]) { assert.equal(json(join(root, packagePath)).scripts.prepublishOnly, "node ../../scripts/assert-publish-ready.mjs"); + const packageRoot = join(root, packagePath, ".."); + const result = spawnSync("npm", ["run", "prepublishOnly", "--silent"], { + cwd: packageRoot, + encoding: "utf8", + }); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.match(result.stdout, /NPM PUBLICATION READY/); } const result = spawnSync(process.execPath, [join(root, "scripts", "assert-publish-ready.mjs")], { cwd: root, encoding: "utf8", }); - assert.equal(result.status, 1); - assert.match(result.stderr, /PUBLICATION BLOCKED/); - assert.match(result.stderr, /entity-and-external-verification: pending_external_verification/); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.match(result.stdout, /NPM PUBLICATION READY/); }); test("release check rejects companion package version drift", (context) => { @@ -234,6 +239,12 @@ test("release validation rejects MCP package and registry identity drift", (cont ["project-status", "mcp", "@openly-useful/project-status-mcp", "org.openlyuseful/project-status"], ["runglance", "runglance-mcp", "@openly-useful/runglance-mcp", "org.openlyuseful/runglance"], ]) { + const bin = packageDirectory === "mcp" + ? { "project-status-mcp": "dist/index.js", "runglance-mcp": "dist/runglance-index.js" } + : { "runglance-mcp": "dist/index.js" }; + const files = packageDirectory === "mcp" + ? ["dist/index.js", "dist/runglance-index.js", "README.md", "LICENSE"] + : ["dist/index.js", "README.md", "LICENSE"]; mkdirSync(join(fixtureRoot, "packages", packageDirectory), { recursive: true }); mkdirSync(join(fixtureRoot, "mcp-registry", component), { recursive: true }); writeFileSync(join(fixtureRoot, "packages", packageDirectory, "package.json"), `${JSON.stringify({ @@ -241,6 +252,9 @@ test("release validation rejects MCP package and registry identity drift", (cont version, mcpName, license: "Apache-2.0", + bin, + files, + publishConfig: { access: "public" }, repository: { url: "git+https://github.com/Openly-Useful/project-status.git" }, bugs: "https://openlyuseful.org/support", })}\n`);