Skip to content

[S · locating read] Where does plugin unpack happen — the landing surface for the ruled #11331 integrity verifier #13455

Closed

Description

@claude

S 级定位读数卡,由 domain:spec 席按 #11331 的 2026-08-30T12:32Z 裁决(选项 A·enforce,verbatim「同意」)第 1 步立卡。⛔ 本卡不写实现——只回答一个问题,答案决定 enforce 实施卡的车道与条款②面。

问题

插件包的 unpack(解包/落盘)究竟发生在哪个面——digest 校验器该装在哪里?

候选:

  • framework install 侧(packages/cli(线索:cli/src/utils/osplugin.ts 有 unpack 形词)、packages/core 装载链、runtime);
  • cloud worker 侧(⚠️ 本板 cloud 仓权限已移除,⛔ 不做 cloud 读取——若 framework 侧读数指向「解包不在本仓」,如实报告并停,cloud 半边归 cloud 路由类等维护者)。

要交付的读数(全部 file:line 实证)

  1. 插件 tarball/artifact 从「包文件」变成「磁盘文件树」的每一条路径(cli install、runtime 装载、marketplace 拉取的本仓半边);
  2. 每条路径上现有的完整性检查读数(预期:零——manifest.integrity declares per-file artifact digests the spec says the runtime re-verifies at unpack — nothing computes them and nothing checks them #11331 前提 3);
  3. manifest.integrity 的 digests 在本仓的全部读者(预期:零读者,只有 CLI 写者 computeIntegrity);
  4. 结论:校验器落点建议(cli / core / 不在本仓)+ 该落点是否触 packages/spec 契约面(决定实施卡条款②)。

背景

三段链路已建成两段:os plugin build 无条件算 SRI digest 写 manifest(git grep -n "computeIntegrity" -- packages/cli/);cloud 发布时原样持久化(#12400 在案读数)。缺的是第三段:验。#11331 的 RULED 评论是裁决权威。

Filed by session session_01KX8wnyjStaZcuMyAMNsy3N.


Generated by Claude Code

Activity

  1. added
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    pm:queueReady for the PM dispatch loop
    domain:specDomain lane — seat card indexed by label:pm:seat
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 30, 2026
  2. claude commented on Aug 30, 2026

    @claude
    ContributorAuthor

    Claim: domain:spec seat (executive PM), dispatching to an os-dev subagent — step 1 of the #11331 ruling (2026-08-30T12:32Z, option A·enforce).

    • Session: session_01KX8wnyjStaZcuMyAMNsy3N
    • Branch: none expected — read-only locating card; the deliverable is a report comment (an instrument PR only if the dev finds a zero-cost census hook worth pinning, which is NOT required).
    • Domain: domain:spec per the ruling (落点定后实施卡可能跨车道).
    • Container & model: claude-opus. --tier N/A (no intended diff).
    • Clause-②: no diff ⇒ no limbs.
    • Constraint: ⛔ no cloud reads (access removed); if the framework-side reading concludes unpack is not in this repo, report and stop — the cloud half joins the cloud-routing class.

    Generated by Claude Code

  3. os-project-manager commented on Aug 30, 2026

    @os-project-manager
    Collaborator

    Dev claim (os-dev seat, session session_01KX8wnyjStaZcuMyAMNsy3N): starting the locating reading. Read-only card — no branch, no PR expected; the deliverable is the four-part reading posted back here as os-dev-report.


    Generated by Claude Code

  4. os-project-manager commented on Aug 30, 2026

    @os-project-manager
    Collaborator

    os-dev-report

    S-size locating reading for #13455. Read-only card: no branch, no diff, no PR. Measured on origin/main @ 3e31c269 in a throwaway read-only worktree. All four deliverables below are file:line evidence.

    Headline: plugin unpack does not exist in this repository. No .osplugin archive is ever expanded onto disk anywhere in objectstack. ADR-0025 §3.5 steps 4–5 (download+verify → materialize) are unimplemented, and the ADR says so in its own status line (docs/adr/0025-plugin-package-distribution.md:3). The verifier the ruling ordered therefore has no existing framework surface to attach to — its primary leg belongs to the cloud control plane. What framework CAN carry is a smaller, real leg, described in §4.


    1. Every path where a plugin package becomes bytes on disk (or structured content)

    Six paths measured. Only P1 writes an archive; P3 and P4 write files; none of them unpacks a file tree.

    P1 — .osplugin PRODUCER (files → archive; the write side, for context)

    Chain: os plugin build → stage archive files → hash → compile manifest → pack → write.

    • packages/cli/src/commands/plugin/build.ts:209 — const integrity = computeIntegrity(files);
    • packages/cli/src/commands/plugin/build.ts:210 — const compiledManifest = { ...manifest, main: 'dist/index.mjs', integrity };
    • packages/cli/src/utils/osplugin.ts:61-69 — computeIntegrity(files), archive-relative POSIX path → sha256-BASE64, excluding objectstack.plugin.json and SIGNATURE, keys sorted.
    • packages/cli/src/commands/plugin/build.ts:219 — const blob = createTarGz(files); → :222 await writeFile(outPath, blob);
    • packages/cli/src/utils/osplugin.ts:132-134 — createTarGz (ustar + gzip).

    P2 — the ONLY .osplugin read path in the repo (in-memory, one entry, never to disk)

    Chain: os plugin publish → read artifact bytes → gunzip+untar in memory → pull exactly one entry.

    • packages/cli/src/commands/plugin/publish.ts:77 — const bytes = new Uint8Array(await readFile(artifactPath));
    • packages/cli/src/commands/plugin/publish.ts:84 — manifest = readOspluginManifest(bytes);
    • packages/cli/src/utils/osplugin.ts:163-167 — readOspluginManifest → readTarGz(blob).find(f = the entry named objectstack.plugin.json
    • packages/cli/src/utils/osplugin.ts:155-157 — readTarGz → readTar(gunzipSync(...))
    • packages/cli/src/utils/osplugin.ts:140-152 — readTar (in-memory array of path/data records)

    This is the closest thing in the repo to an unpack, and it is deliberately narrow: the full file tree IS materialized in memory by readTarGz at :156, then everything except objectstack.plugin.json is discarded at :164. Nothing is written to disk. No caller of readTarGz / readTar / readOspluginManifest exists outside packages/cli/test/.

    P3 — marketplace pull, framework half (the real "remote package → disk file" path)

    Chain: os package install ID → HTTP POST to a running runtime → runtime fetches a manifest → runtime persists one JSON file.

    • packages/cli/src/commands/package/install.ts:170 — POST to RUNTIME/api/v1/marketplace/install-local
    • packages/cloud-connection/src/marketplace-install-local-plugin.ts:622 — private handleInstall
    • packages/cloud-connection/src/marketplace-install-local-plugin.ts:688 — builds PUBLIC_BASE/packages/ID/versions/VERSION/manifest.json
    • packages/cloud-connection/src/marketplace-install-local-plugin.ts:711 — const resp = await fetch(attempt.url, { headers }); → :726 payload = await resp.json();
    • packages/cloud-connection/src/marketplace-install-local-plugin.ts:858 and :895 — this.ledger.write(entry);
    • packages/cloud-connection/src/local-manifest-source.ts:243-246 — write() → mkdirSync + writeFileSync(...), landing at CWD/.objectstack/installed-packages/SAFE_ID.json (documented at marketplace-install-local-plugin.ts:40).
    • Rehydrate leg at boot: marketplace-install-local-plugin.ts:216-217 → this.rehydrate(ctx); reads back via local-manifest-source.ts:195-231.

    Critical for this card: what lands is a single JSON metadata manifest, not an archive and not a file tree. There is no per-file surface here for manifest.integrity (whose keys are archive-relative paths) to address. This path can never be the .osplugin unpack the spec sentence describes.

    P4 — artifact-pinned boot (the one materialization in the repo that DOES verify a digest)

    Chain: OS_ARTIFACT_URL → fetch → hash → compare against the #sha256=HEX64 fragment pin → write to a local cache → boot from the local path.

    • packages/cli/src/commands/serve.ts:1971-1990 — resolves OS_ARTIFACT_URL through resolveArtifactReference
    • packages/runtime/src/artifact-reference.ts:558 — resolveArtifactReference
    • packages/runtime/src/artifact-reference.ts:628 — const actual = sha256Hex(bytes); (also :571 file leg, :611 cache leg)
    • packages/runtime/src/artifact-reference.ts:505-520 — mismatch refusal naming expected AND actual
    • packages/runtime/src/artifact-reference.ts:393-398 — atomic writeFileSync + renameSync into artifactCacheDir (:370, :375-376)
    • Policy table in the module header: packages/runtime/src/artifact-reference.ts:27-33

    Again a single JSON artifact, and the check is a whole-file sha256 pin carried in the URL fragment — a different mechanism on a different field from manifest.integrity. It is nonetheless the strongest in-repo precedent for what "verify, then materialize, refuse loudly on mismatch" looks like here.

    P5 — packages/core load chain: no bytes, no unpack, by design

    • packages/core/src/plugin-loader.ts:142 — async loadPlugin(plugin: Plugin) takes an already-constructed in-memory module.
    • packages/core/src/plugin-loader.ts:411-417 — the file states the boundary itself: artifact verification "is performed against the .osplugin artifact bytes + version identity at materialize/install time … By the time a plugin reaches loadPlugin() it is an in-memory module with no artifact bytes".
    • There is no dynamic import of an installed-plugin directory anywhere in packages/core/src (the only await import( is plugin-signature-verifier.ts:300, importing crypto).

    P6 — runtime package install: a manifest object off a JSON body

    • packages/runtime/src/domains/packages.ts:281-315 — POST /packages reads body.manifest, then protocol.installPackage({ manifest, settings }) or registry.installPackage(manifest, settings). No blob, no archive.

    NOT PRESENT (checked explicitly, with controls)

    • ADR-0025 §3.5 step 5's materialize dir OS_HOME/plugins/ENV/ID/VERSION/ — zero hits; the only resolveObjectStackHome() consumers are serve.ts:525, serve.ts:1976, default-host.ts:133, package-state-store.ts:38.
    • No archive-extraction dependency in any workspace package.json: tar, tar-stream, adm-zip, unzipper, decompress → zero. (Control: the same pathspec finds "name" in every workspace package.json.)
    • apps/ contains only docs — zero occurrences of osplugin / computeIntegrity / integrity there.
    • ⚠️ Probe hygiene for whoever re-runs this: the pathspec packages/*/src is blind in this repo's git (git grep -ln computeIntegrity -- 'packages/*/src' returns nothing while the file exists). Every probe above uses -- packages plus a filter. A zero taken with the starred pathspec is a false zero.

    2. Existing integrity checks on each path — the expected zero is CONFIRMED for the digest map

    Path Integrity check present?
    P1 build Write only. Computes and embeds; verifies nothing.
    P2 publish read None. readTarGz/readTar do no verification of any kind; the ustar header checksum is written on the way out (osplugin.ts:105-109) and is not re-checked on the way in.
    P3 marketplace install-local None. A digest-word probe (sha256, checksum, digest, hash) over marketplace-install-local-plugin.ts (1816 lines) and local-manifest-source.ts (259 lines) returns zero lines.
    P4 artifact-pinned boot Yes, but a different mechanism and a different field — whole-file sha256 URL-fragment pin, artifact-reference.ts:628 + :505-520. Unpinned references get no verification at all (artifact-reference.ts:29).
    P5 core load Signature STRING well-formedness only — plugin-loader.ts:405-428 calls parseSignature and throws only on a malformed ed25519:KEYID:BASE64URL shape. No bytes are hashed.
    P6 runtime install None.

    Adjacent machinery that exists and is NOT reached — this matters for the landing decision, because the artifact-security story looks staffed and is not:

    • packages/core/src/security/plugin-artifact-signature.ts:200 — verifyPluginArtifact, the designated install-time two-chain verifier, has zero non-test callers. Its only non-test reference in the repo is the re-export at packages/core/src/security/index.ts:34.
    • packages/core/src/security/plugin-signature-verifier.ts:72 — PluginSignatureVerifier likewise: zero non-test callers, only the re-export at security/index.ts:14.
    • os plugin publish does compute a whole-artifact digest — packages/cli/src/commands/plugin/publish.ts:78 sha256Hex(bytes) → shipped as artifact_checksum at :134 — but it is sent to the server for the server to check. Nothing in this repo verifies it.

    So the repo has three artifact-security seams (per-file digests, publisher/platform signatures, whole-artifact checksum) and enforces none of them locally.


    3. Readers of manifest.integrity in this repo

    Zero non-test readers. Confirmed, with controls.

    • Sole producer: packages/cli/src/utils/osplugin.ts:61-69 (computeIntegrity).
    • Sole write site: packages/cli/src/commands/plugin/build.ts:209-210; the entry count is echoed to the console at build.ts:228 (an echo of the map's size, not a read of its contents).
    • Every .integrity property access repo-wide, excluding CHANGELOGs: exactly one, and it is a test — packages/cli/test/osplugin.test.ts:130.
    • Destructuring / bracket forms over cli, core, runtime, rest, objectql, metadata-protocol, cloud-connection sources: zero, apart from the write at build.ts:209-210.
    • Controls held: the same probe finds live sibling-key reads — manifest.packaging at packages/cli/src/commands/plugin/build.ts:126, and manifest.contributes.kinds at packages/objectql/src/engine.ts:4837-4839. The zero is about the key, not about the probe.

    Declaration and claim sites — the text the ruling's step 2 must correct:

    • packages/spec/src/kernel/manifest.zod.ts:100-109 — PluginIntegritySchema, TSDoc: "Re-verified by the runtime when it unpacks the .osplugin (ADR §3.5 step 5)."
    • packages/spec/src/kernel/manifest.zod.ts:610-614 — the integrity: field, TSDoc: "verified at install/load time when the runtime unpacks the .osplugin."
    • packages/spec/liveness/manifest.json:265-269 — row props/integrity, status: "dead", verifiedAt: 2026-08-23, evidenceScope: "cross-repo"; its note carries the same "nothing computes them at build time and nothing re-verifies them at unpack" wording, the first half of which was already falsified on 2026-08-24.

    4. Conclusion — where the verifier lands, and what it means for the implementation card

    4a. Landing recommendation: split, and the enforce leg is NOT in this repo

    The unpack the ruled verifier must guard is not in objectstack. No path here expands an .osplugin into a file tree; P3 and P4 write single JSON files; P5 receives in-memory modules. Attaching a per-file digest verifier to any current framework path would require inventing the unpack first — ADR-0025 §3.5 steps 4–7 wholesale (download, verify, materialize to a per-environment dir, register, load). That is not an S/M enforce card, that is the unimplemented half of ADR-0025.

    Per this card's scope limit I stop at the repo boundary and did not read cloud. Reporting the framework-side reading as instructed: the digest-verification landing surface is not in this repo; the cloud half belongs to the cloud routing lane.

    Framework leg that IS available now, and is worth landing regardless of where the cloud leg goes — ranked:

    1. os plugin publish preflight self-check (recommended; cheap and real). publish.ts:84 already has the complete file tree in memory from readTarGz at osplugin.ts:156 and throws it away. A verifyIntegrity(files, manifest.integrity) call there closes a genuine gap: today os plugin publish will happily upload an artifact whose dist/ bytes no longer match the digests its own manifest declares (hand-edited artifact, truncated file, corrupt disk). It costs one function next to computeIntegrity and one call site, and it makes the producer/verifier pair symmetric inside the CLI that owns both. It does not, and must not claim to, be the unpack-time check the spec sentence promises.
    2. Publish the canonical verifier as a shared contract, mirroring the signature precedent. packages/core/src/security/plugin-artifact-signature.ts:3-25 declares itself byte-for-byte compatible with cloud's package-signing.ts. A verifyIntegrityMap living beside it in packages/core/src/security/ gives the cloud unpack one canonical implementation to mirror instead of a second hand-rolled one — exactly the drift the signature module exists to prevent. Take this leg if the cloud implementation card wants a shared definition.
    3. Not recommended: building the unpack in framework to give the verifier a home. That is ADR-0025 §3.5 in full, a different and much larger card, and nothing pulls it today — no framework path installs an .osplugin.

    Do not let the framework leg alone be recorded as the ruling discharged. Landing (1) makes the shipped sentence less false, not true: the spec still promises re-verification "by the runtime when it unpacks", and after (1) the digests would be checked by the publisher, before upload, on the machine that just computed them. If the cloud leg is not scheduled, the honest same-PR text is the ruling's fallback-C wording, not a claim of enforcement.

    4b. Does the landing touch the packages/spec/src contract face?

    The verifier itself: no. It reads an already-declared optional field. No new key, no shape change, no schema edit — manifest.integrity is declared at manifest.zod.ts:610-614 and needs nothing added.

    The ruling's step-2 text correction: yes, it touches packages/spec/src, but as prose, not as contract shape. Two TSDoc/describe sentences at manifest.zod.ts:100-109 and :610-614 state a re-verification that does not happen. Measured consequence for clause ②: packages/spec/authorable-surface.base.json carries keys only, not describe text — verified, the string Per-file content digests does not appear in that file, and the single re-verif hit inside it is the file's own self-description. So a describe-text correction moves no authorable-surface baseline and is not a key-surface change.

    My reading for clause ②: it hits on file path, not on contract shape. The implementation card edits packages/spec/src/kernel/manifest.zod.ts, so a path-triggered clause ② fires, while a content-triggered "authorable contract face changed" test does not. The card should state which of the two it means; the ruling's own wording ("若实施落点在 framework 且触 spec 契约面,条款②按内容命中走复审链") reads as content-triggered, and by content this is a prose correction.

    Also in packages/spec but outside src/: the ledger row at packages/spec/liveness/manifest.json:265-269. One consequence to put in the implementation card explicitly: if the verifier lands in cloud, this repo's integrity row stays status: "dead" — that verdict is a per-repo reader census, and a cloud-side verifier adds no objectstack reader. The correction owed is then to the row's note prose only: drop the falsified "nothing computes them at build time", restate the verify side accurately, and name where enforcement lives. Flipping the row's status on the strength of a cloud verifier would be a false ledger entry. Only options (1) or (2) create a real in-repo reader, and even then the status change must be argued, not assumed.

    4c. Suggested lane and size for the implementation card

    • Cloud unpack verifier (the ruled enforce leg proper): NOT this repo. Route to the cloud lane. Cannot be sized from here.
    • Framework leg — domain:spec lane (it edits packages/spec/src/kernel/manifest.zod.ts and packages/spec/liveness/manifest.json), size S–M:
      • verifyIntegrity(files, integrity) next to computeIntegrity in packages/cli/src/utils/osplugin.ts, or in packages/core/src/security/ if the shared-contract route is taken — roughly 40 lines plus a rejection envelope;
      • one call site at packages/cli/src/commands/plugin/publish.ts:84;
      • unit tests: match, single-file mismatch, missing entry, extra file, absent map (which must stay permissive — the field is .optional());
      • the two spec TSDoc corrections plus the liveness/manifest.json note correction, same PR per the ruling;
      • a changeset (user-visible: os plugin publish gains a refusal).
    • Gates that leg owes, derived from the touched paths rather than copied from a dispatch list: pnpm --filter @objectstack/cli test, pnpm --filter @objectstack/spec check:liveness, pnpm --filter @objectstack/spec check:authorable-surface, plus check:error-code-casing only if a new error code is introduced. On that last point, note the precedent at packages/runtime/src/artifact-reference.ts:68-73: refusals that can never reach an HTTP response envelope are deliberately NOT registered in ERROR_CODE_LEDGER. An os plugin publish preflight refusal is exactly that shape and should follow the same rule — which keeps the implementation clear of packages/spec/src/api/error-code-ledger.zod.ts and of a second contract-face question.
    • A decision the implementation card must carry rather than assume: whether an absent map is permissive (the field is optional, and every artifact built before the digest writer landed has none) or strict for artifact_kind: 'plugin'. Strict-by-default would reject every pre-computeIntegrity artifact.

    Method / probe log

    Read-only worktree at origin/main 3e31c269; no edits, no branch, no PR. Probes run: computeIntegrity call census; readTarGz / readTar / readOspluginManifest caller census; repo-wide unpack-family word probe (unpack, untar, extractTar, extractArchive, gunzip, node-tar, tar.x); .integrity property-access probe with two live-sibling controls; archive-dependency probe over every workspace package.json with a "name" control; resolveObjectStackHome consumer census; verifyPluginArtifact / PluginSignatureVerifier caller census; digest-word probe over the install-local plugin and its ledger. One blind pathspec was caught by a control and discarded (noted in §1). No build and no test run — this card changes nothing, so there is nothing to verify green.


    Generated by Claude Code

  5. claude commented on Aug 30, 2026

    @claude
    ContributorAuthor

    PM verdict: ACCEPT — locating card complete, closing. (session_01KX8wnyjStaZcuMyAMNsy3N)

    The reading is decisive and controls held throughout (positive-control probes for manifest.packaging / contributes.kinds; the starred-pathspec false-zero was caught by a control and re-measured). Zero edits, zero PR, read-only worktree removed — exactly what an S locating card owes.

    What the reading establishes (recorded for #11331's step 2):

    1. There is no unpack in this repo. ADR-0025 §3.5 steps 4–7 are unimplemented; no tar/unzip dependency exists in any workspace package. The only .osplugin read path (os plugin publish) materializes the tree in memory and discards it.
    2. Zero existing digest checks on every plugin path; manifest.integrity has zero non-test readers. Adjacent verifiers (verifyPluginArtifact, PluginSignatureVerifier) have zero non-test callers.
    3. The ruled enforce leg proper lands in the cloud control plane, not here — routed accordingly on manifest.integrity declares per-file artifact digests the spec says the runtime re-verifies at unpack — nothing computes them and nothing checks them #11331.
    4. A real framework leg exists: an integrity self-check in the os plugin publish preflight (publish.ts:84), which already holds the full tree in memory. Today publish uploads artifacts whose bytes may no longer match their own declared digests. This leg does NOT discharge the ruling and will not be recorded as doing so.

    Step-2 filing proceeds on #11331. Probe-hygiene note (starred pathspec packages/*/src is blind in this repo's git) is worth an AGENTS.md line — leaving that to triage as a finding, not widening this card.


    Generated by Claude Code

  6. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

domain:specDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions