Repository navigation
feat(spec)!: an install through POST /api/v1/packages declares manifest.scope project only, so an authored install cannot lock itself - #24160
Conversation
Claude-Session: https://claude.ai/code/session_015p3xNTnaiNHYkAHoQ4y5Cg Co-authored-by: Claude <noreply@anthropic.com>
…; the door locates the bare-form refusal through the union's own branch Claude-Session: https://claude.ai/code/session_015p3xNTnaiNHYkAHoQ4y5Cg Co-authored-by: Claude <noreply@anthropic.com>
…d the self-locked-row remedy Claude-Session: https://claude.ai/code/session_015p3xNTnaiNHYkAHoQ4y5Cg Co-authored-by: Claude <noreply@anthropic.com>
… reference docs and the dropped-refinements totals for PackageInstallManifestSchema Claude-Session: https://claude.ai/code/session_015p3xNTnaiNHYkAHoQ4y5Cg Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 148 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a06778a3e733547a5ce2d2ded37d05296862515f && git checkout a06778a3e733547a5ce2d2ded37d05296862515f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c40506aac02df70aad25588a19a629fa2efa4c62 b78d15a5b13af7e6de0a5da6e0c45d4d5256a972 && git checkout -B drift-repro c40506aac02df70aad25588a19a629fa2efa4c62 && git merge --no-ff b78d15a5b13af7e6de0a5da6e0c45d4d5256a972
node scripts/docs-audit/affected-docs.mjs --json c40506aac02df70aad25588a19a629fa2efa4c62
|
席位核对ACCEPT · The seat's own review at
The dev's open question, answered by the seat: A — keep the one-line Producer the dev could not measure, measured here: Studio (objectui) at the console pin Out of scope, where each goes:
Generated by Claude Code |
Contract reviewServed-tier: Inputs read: card #24062's body and all five comments (grade ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #24062
Clause-②: no (narrowing)
An install through
POST /api/v1/packagesmay now declaremanifest.scopeprojectonly, or leave it out.cloudandsystemare refused at parse time with400/VALIDATION_ERROR, on both body forms and on either overwrite spelling, and the refusal says what to write instead. So an authored install can no longer lock itself.ManifestSchemais unchanged for every other producer.Session:
session_015p3xNTnaiNHYkAHoQ4y5Cg(theos-devcloud run dispatched by thedomain:spec#2seat, claim6230547268). Base9f024d8a2c, headb78d15a5.What changed
packages/spec/src/api/package-api.zod.tsPackageInstallManifestSchema(plus the typesPackageInstallManifestandPackageInstallManifestParsed). It isManifestSchema.extend({ scope })withscope: z.enum(['project'], { error }).default('project'). Every other key, the strict close, its unknown-key prescription and the retired-key tombstones ride the.extend()unchanged; the authorable-surface shard lists the same key set, tombstones included.packageInstallScopeRefusal(input): the one refusal sentence. It names the value sent, whycloud/systemlock the package, and the remedy: "Removescope(it defaults toproject), or writescope: 'project'."PackageInstallRequestSchema.manifestand the bare branch ofPackageInstallBodySchemaboth now usePackageInstallManifestSchema. The docblocks that namedManifestSchemafor those two positions now name the new schema.overwritedescribe gains: "an id the deployment ships read-only (code-booted, or scope cloud / system) is refused 422 WRITABLE_PACKAGE_REQUIRED". I re-read PR fix(runtime)!: POST /packages refuses an overwrite over a read-only package with 422 WRITABLE_PACKAGE_REQUIRED #24055's door onmain:requireWritablePackageruns after the 409, for both overwrite spellings, atpackages.ts(thereadOnlyleg).packages/runtime/src/domains/packages.ts: one line plus its comment and import, a deviation from the dispatch's "no runtime source change" expectation, measured and reported below.installBodyRefusalre-parses the bare form throughPackageInstallManifestSchema, the union's own bare branch, instead ofManifestSchema.package-api.test.ts, and the door pins as §9 ofpackages-install-body-contract.test.ts.apishards ofapi-surface,authorable-surface,authorable-defaults,json-schema.manifest,declaration-mapandexport-origins;content/docs/references/api/package-api.mdxandreferences/index.mdx. Plus one hand-edited ledger,dropped-refinements.baseline.json: the build printed the entry for the new published schemaapi/PackageInstallManifest(the samenavigationContributionssiteapi/PackageInstallRequestalready declares), and the header totals 235 to 236 and 766 to 767, whichscripts/dropped-refinements.test.tsasserts. Delta vsmain: 3 exports added, 0 removed..changeset/24062-authored-install-scope-project.md:@objectstack/specminor(BREAKING narrowing, launch-window convention, PR fix(runtime)!: POST /packages refuses an overwrite over a read-only package with 422 WRITABLE_PACKAGE_REQUIRED #24055's62f959dd62precedent) and@objectstack/runtimepatch. It carries FROM / TO, theClause-②line, and the ADR-0087 dispositionnot-required (no-migration-prescription). The only conversion that could exist (rewritecloud/systemtoproject) is the silent rewrite this card refuses.Accept set, before and after
{ manifest },scope: 'cloud'or'system'201, authored row stored with that scope400VALIDATION_ERROR, nothing installed; issueinvalid_valueatmanifest.scopescope: 'cloud'or'system'201400VALIDATION_ERROR, nothing installed; issue atscopeoverwrite: trueor?overwrite=true, platform scope201on a fresh id; over an authored project row it re-locked that row400, ahead of the 409 and the read-only gate; an existing row keeps its manifestscope: 'project', either form201201scope, either form201, stored as sent (noscopekey)201, stored as sent: the door still stores what it was sent, so absence stays absentManifestSchema.safeParsewithcloud/system/projectMeasured: the door needed one line, and why I made it rather than stopping
A2 assumed narrowing the declaration makes the door refuse with no runtime change. That holds for the verdict and fails for the sentence. Probe on the built
dist, before the runtime line:zod reports the failed union as one
invalid_union, andinstallBodyRefusallocates the failure by re-parsing the form the caller wrote. For the bare form it re-parsed withManifestSchema, which admitscloud. That re-parse passed, so the door fell back to the union's issues, and the bare-form refusal would have read(body): Invalid input: a400that does not say what to write. The door's own header says the re-read schemas are "the declarations@objectstack/specpublishes for this door; nothing here restates them", andManifestSchemais no longer that branch. So the fix re-reads the branch the union actually carries. It adds no door-side verdict: which bodies are refused is still the declaration's decision alone. The ablation below shows the line is load-bearing. If the seat would rather hold the runtime file out, the alternative is the bare-form refusal without a prescription.Producers measured (A3)
git grepon9f024d8a2cfor every sender ofPOST /api/v1/packagesand every parser ofPackageInstallRequestSchema/PackageInstallBodySchemaoutsidepackages/spec:@objectstack/clientpackages.install(manifest, options)forwards the caller's manifest, and sets noscopeof its own.packages/runtime/src/domains/packages.tsis the door itself.packages/client/src/readme-package-install-example.test.tsandpackages/runtime/src/domains/packages-install-body-contract.test.tsare the two test parsers. Neither fixture carries a platform scope; both stay green unchanged.packages/cli/src): no sender of this door.metadata-protocolduplicatePackagecallsinstallPackagein-process, not over this door, and stripsscopefrom the copy already ("the copy is scope-less"): the same rule this card declares.scope: 'system'/'cloud'manifests (plugin-auth, plugin-security, organizations, the service plugins, the apps) reach the registry by code boot (registerApp/registry.installPackage(…, 'code')), never through these schemas.../objectui): NOT MEASURED. The sibling checkout is not in this container andpackages/console/distis absent. Studio creates bases through this door. If it ever sent a platform scope, the new400names the fix.No first-party producer sends
cloud/systemthrough these schemas, so the narrowing breaks nothing first-party.Fixtures triaged (A9)
None needed re-spelling. Every test that installs a
scope: 'system'/'cloud'package seeds the registry directly and never passes through the door or the narrowed schemas:packages-readonly-gate,packages-writable-verdict,packages-serializable-response,packages-install-overwrite-readonly(its authored cloud-scoped row is exactly the already-locked shape, still meaningful), andmetadata-protocol'sget-meta-items-package-writable. §8's existingscope: 'tenant'row still answers400namingmanifest.scope.Rows that already locked themselves (A7): a documented operator delete, no code remedy
service-packagestart()re-installs everysys_packagesrow asauthoredwith its stored manifest (packages/services/service-package/src/index.ts, the hydration loop'sregistry.installPackage(…, 'authored')).isWritablePackagereadsmanifest.scopeoff the registry row. So a row stored withcloud/systembefore this PR is still read-only after it and after every restart.DELETE,PATCH …/disableand the overwrite door keep refusing it422; this PR changes nothing for such a row.DELETE FROM sys_packages WHERE id = 'PACKAGE_ID'on the environment's database, then restart. No API door can do it, by ADR-0070 D2's own design: there is noisSystemescape hatch.service-packagehydration.sys_metadataitems such a package owned once its row is deleted.Tests and evidence (head
b78d15a5)VERDICT command-exit 0.--project local --maxWorkers=2, full: 690/692 files on the first run. The 2 red were generated-ledger freshness (dropped-refinementstotals;export-originsmissing the two new exports), both fixed by regeneration; thenscripts/dropped-refinements.test.ts+scripts/export-origins.test.ts: 51/51 passed.src/api/package-api.test.ts+package-install-one-authority.test.ts: 105/105.check:generated: "All 14 generated artifacts are up to date".VERDICT command-exit 0.src/domains/packages*.test.ts: 28 files, 559 tests passed.readme-package-install-example,client.test,return-type-precision: 3 files, 220 tests passed.scripts/ablation-replace.mjs: the bare-branch re-parse was put back toManifestSchema. Result: exactly the 4 BARE §9 cases red ("4 failed / 40 passed"), all WRAPPED cases green. Restore: blob equals HEAD3168f2e89ab3,git diff HEADempty.dist): the enum was widened back tocloud/system/projectwith a marker value.ablation-dist-preflight: "marker present in 10 built files". Runtime contract: 11 failed / 33 passed (every §9 refusal, both forms, both overwrite spellings, the existing-row overwrite); spec: 2 failed / 90 passed. Restored, rebuilt,--absentpreflight exit 0, "working tree clean against HEAD".check-adr-0087-registration: one declared-breaking changeset,[BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription), exit 0.check-changeset-no-major: "This diff introduces nomajorbump", exit 0.node scripts/pm/check-governed-merges.mjs --branch claude/issue-24062-authored-install-scope: "0 of 14 path(s) hit the register … NOT governed"; 412 changed lines. Noskills/**path moved.dispatch-gates --commands, 110): all run; 108 exit 0. Two exit 3, PREREQUISITE NOT MET, read as NOT MEASURED and left to CI:check:skill-examples(packages/client-react/distnot built here) andcheck:dual-build-cjs-loads(dists outside this closure).--ranreconciliation: "110 derived famil(ies) accounted for — 108 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)".check-plugin-teardown-shape --self-testfirst refused on the shallow clone; after fetching its pinned fixture commit: "48 cases pass".mainmoved 3 commits past the base (135e19e20,1fee13d4c,c40506aac). None touches a path in this diff, so I did not merge them; CI judges the merge ref.Acceptance notes
domain:engine) is not addressed here. If it makes the protocol'sinstallPackageparse what the door parses, it inherits this narrowing only by parsingPackageInstallManifestSchema(or the body union), notManifestSchema. Noted for that card, not filed.scope, the value sent,scope: 'project') besidecodeandstatus, never in full.Generated by Claude Code