Repository navigation
Description
Context
objectstack start today boots from the project's local dist/objectstack.json. There is no way to point a runtime at an artifact by reference — an HTTP(S) URL or an explicit file location — via an environment variable.
That capability is the missing half of a deployment model where the runtime image and the app artifact are two independent release axes: a fixed runtime container + one env var naming the artifact = a running app; upgrading the app is an env change + restart, never an image rebuild. The motivating case is the cloud's unified SaaS run mode for hosted marketplace apps (EE image + artifact ref + OS_TENANCY_POSTURE=isolated, see objectstack-ai/cloud#1292), but the mechanism benefits every self-host/community deployment equally: "run any marketplace app anywhere with one env var."
Boundary note ("open mechanism, closed intelligence"): fetching and booting an artifact is a mechanism and belongs in the open framework. The commercial gate is unaffected — walled tenancy postures remain entitled via @objectstack/organizations regardless of how the artifact arrives. Registry-based resolution (OS_PACKAGE_REF, signing enforcement, entitlements) is deliberately not part of this issue.
Requirement
One env var, one value — no companion knobs:
OS_ARTIFACT_URL=https://…/hotcrm-2.2.2.json— fetched at boot.OS_ARTIFACT_URL=file:///…/objectstack.json— read directly (volume-mount workflow).- Optional integrity pin, SRI-style, inside the URL fragment:
…/hotcrm-2.2.2.json#sha256=<hex>. Fragments are client-side by standard (never sent to the server); when present, the runtime verifies the content hash before booting. There is deliberately no separateOS_ARTIFACT_SHA256variable.
Honored by the production boot path (objectstack start / serve); when set, it overrides the implicit dist/objectstack.json lookup.
MVP posture: verification only happens when the fragment is present. The recommended production discipline (immutable version-named objects, CI-only write access to the artifact host, hash pinned via fragment) is documentation, not code.
Acceptance
- A runtime container +
OS_ARTIFACT_URLboots the referenced artifact with no project checkout present. - No
#sha256=fragment → no verification; a fetch/read failure fails the boot loudly (container orchestration retries). No cache-fallback logic. #sha256=fragment present → content hash verified before boot; mismatch → refuse loudly, naming expected vs actual. A fetch failure MAY fall back to a locally cached copy only when the cache matches the pinned hash (loud warning).- The artifact's
engines.protocolis validated against the runtime; incompatible → refuse loudly (the safety belt of the two-axis split). - Migration policy: safe migrations run at boot; a destructive migration (
os migrate --allow-destructiveclass) → refuse to boot with a clear operator message. Never skip silently. - Secrets discipline: the URL may carry auth material (pre-signed URLs) — never echoed into logs or HTTP responses unredacted.
Out of scope
OS_PACKAGE_REF(marketplace registry resolution, signature enforcement, entitlements) — a later layer on top of this one; adds a boot-time marketplace dependency.- Multi-tenant fleet / hostname-routing concerns — this is the single-instance boot path.
- Cloud-side adoption (EE image docs, retiring the dev/test
OS_ARTIFACT_PATHknob inapps/objectos-ee) — tracked in objectstack-ai/cloud#1292.
Estimated effort: 1–1.5 days.
Activity
hotlong commentedon Aug 13, 2026
Triage: lands in
packages/cli(production boot path ofobjectstack start/serve) ⇒domain:cli,pm:queue, type Feature. Rationale: maintainer-authored with full acceptance criteria — the build decision is already made by its author, so this does not enter the decision inbox; triage only routes. Cross-repo note: cloud-side adoption is tracked in objectstack-ai/cloud#1292 and is explicitly out of scope here; no Blocked-by in either direction (this card is the upstream). Consumer-facing surface is one env var (OS_ARTIFACT_URL,#sha256=fragment pin);engines.protocolvalidation and migration-policy refusals per the acceptance list. Size/model suggestion: M–L, opus; secrets-redaction acceptance item (#6) deserves an explicit test.Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
Claimed and dispatched by the
domain:cliseat (#6024).session_01P7vaLs7bhBPi9m3JyzkhDjclaude/issue-8368-artifact-pinned-bootmode:subagent(M–L per triage)标题写的是
OS_ARTIFACT_URL (+ OS_ARTIFACT_SHA256),正文的 Requirement 段明确写着:正文赢。完整性校验值走 URL fragment(
…/hotcrm-2.2.2.json#sha256=<hex>),SRI 风格,fragment 按标准不发给服务端。只实现一个环境变量。 照标题实现两个,交付的就是维护者明确拒绝过的形状 —— 这是本卡最容易踩的一处,提前挑明。串行状态(派工时测量)
packages/cli/src/commands/serve.ts当前无人占用 —— 18 个开放 PR 里没有一个动packages/cli,本车道唯一在飞的 #8373/#8487 落在packages/rest。你独占这个文件。⛔ 但 #8357 是本车道
pm:queue上一张关于serve.ts的 finding(cloud-connected marketplace arm 会静默替换 host 自己的 marketplace 插件)。你会在同一个文件里读到它。不要顺手修 —— 它是独立卡片,捎带修会让两张卡的评审边界糊掉,也会让 #8357 变成一张无法独立反向验证的卡。看到就绕开。交付要求
六条 acceptance 全部要有测量证据,其中两条按经验最容易被做成「不可能失败的检查」:
其余:#2 无 fragment 则不校验、取不到就响亮失败(不做 cache fallback);#4
engines.protocol与运行时不兼容则响亮拒绝;#5 安全迁移照常在启动时跑,破坏性迁移(os migrate --allow-destructive那一类)拒绝启动并给出清晰的运维消息,绝不静默跳过。反向验证按惯例:先预测方向再跑,把预测和实测一起写进 PR。
范围外(正文已列,重申)
OS_PACKAGE_REF/ registry 解析 / 签名强制 / entitlements;多租户 fleet 与 hostname 路由;cloud 侧接入(objectstack-ai/cloud#1292)。本卡是上游,不等它。需要 changeset。⛔ 不要碰
content/docs/releases/。Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
{ "issue": 8368, "status": "done", "branch": "claude/issue-8368-artifact-pinned-boot", "pr": "https://github.com/objectstack-ai/objectstack/pull/8526", "premise_still_valid": true, "summary": "Implemented artifact-pinned boot as ONE env var (OS_ARTIFACT_URL) with the SRI-style integrity pin inside the URL fragment — the body's shape, not the title's; nothing found contradicted the body, so no escalation. The mechanism landed in packages/runtime/src/artifact-reference.ts (parse, fetch/read, verify, cache, protocol handshake, redaction), with packages/cli/src/utils/artifact-boot-migration.ts for the boot migration policy and wiring in serve.ts + start.ts. Two things worth the PM's attention: (a) OS_ARTIFACT_URL had to beat OS_ARTIFACT_PATH, not just the implicit dist/ lookup, because the official runtime image sets ENV OS_ARTIFACT_PATH=/srv/app/objectstack.json — without that precedence acceptance #1 is unreachable in the exact deployment it was designed for; (b) acceptance #4 was ALREADY partly enforced downstream by AppPlugin.assertProtocolCompat, so what this PR adds is a pre-boot refusal that names the reference — the e2e assertion is written to require that placement and Ablation C confirms it discriminates. Also found and fixed an unplanned defect in my own design: fetch refuses to construct a request from a URL carrying userinfo, so https://user:token@host/app.json was unusable; the credential now moves into an Authorization: Basic header.", "tests": "packages/runtime: 152 files / 2357 tests pass (`pnpm --filter @objectstack/runtime test`). packages/cli: 119 files / 1294 tests pass. New: artifact-reference.test.ts (40), artifact-boot-migration.test.ts (11), artifact-pinned-boot.e2e.test.ts (7, real `os serve` children in provably empty dirs, OS_MIGRATE_AND_EXIT=1). Typecheck clean on both packages. Gates run on a FULL built closure (70 turbo build tasks): check:type-check-debt (re-measure OK, 33 entries, none above recorded), check:type-check-coverage, check:nul-bytes, check:error-code-casing, check:startup-registry-verdict, check:cross-package-test-inputs, check:query-options-erasure, check:changeset-gate-self-tests, check:doc-anchors, check:doc-authoring, check:adr-anchors, check:empty-changeset, check:objectui-changeset, check:release-notes, check:published-files — all pass; ESLint clean. check:type-check-debt DID go red mid-run (+29 raw errors on @objectstack/runtime TEST_DEBT, recorded 227 -> 256); the ledger was NOT raised — all 29 came from `.catch((e) => e as ArtifactReferenceError)` widening to `Error | Resolved`, replaced by a typed refusalOf() helper (which also makes a wrongly-succeeding call fail as itself), re-measured back to exactly 227. REVERSE VERIFICATION, predictions recorded before running: Ablation A (drop the remote integrity check) predicted 3 unit + 2 e2e red, measured exactly 3 + 2, and the file:// mismatch test correctly stayed green — but one prediction was WRONG: I said the secrets e2e would fail at its positive control; it failed one line earlier at expect(code).not.toBe(0), because without verification the mismatched artifact simply boots successfully. Ablation B (give the unpinned path a cache fallback) predicted exactly 1 red, measured exactly 1; second-order check — with the ablation still applied I deleted the planted cache and the test went GREEN again, proving it discriminates because of the plant rather than because the network was cut. Ablation C (protocol refusal to a no-op) predicted 2 unit red plus an e2e where the exit-code assertion stays green (AppPlugin still refuses) and only the placement assertion goes red; measured exactly that, on exactly that line.", "open_questions": [], "out_of_scope_findings": [] }Notes for the PM, outside the JSON:
packages/cli/src/commands/serve.ts. That file is touched, but most of the change is inpackages/runtime(newartifact-reference.ts) and a newpackages/cli/src/utils/artifact-boot-migration.ts. Worth knowing if the queue is used to order the next card.serve.ts's cloud-connected marketplace arm was read and deliberately left alone.in_progressat the time of this report. All named gate families were run locally on a full built closure first.Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
已落地并结单 —— PR #8526 merged。
domain:cli座位 (#6024) 评分。落地探针取 PR 新增的两个文件:
packages/runtime/src/artifact-reference.ts与packages/cli/src/utils/artifact-boot-migration.ts,在refs/heads/main上均存在;同批目录列举里的既有文件作正对照。评级:ACCEPT
标题与正文的矛盾按正文实现。 只有一个环境变量,校验值走 URL fragment。PR 还补上了一条派工时没说透的理由:两个变量会让「URL 更新了、hash 没更新」成为一个可达状态,fragment 形状让这个状态拼不出来。这比「因为正文这么写」更有说服力。
它纠正了卡片的验收条款 #4
验收 #4 要求「
engines.protocol与运行时不兼容则响亮拒绝」。dev 测出来:这在本 PR 之前就已经为真 ——AppPlugin.init对每个加载的 bundle 都跑assertProtocolCompat。toContain('Cannot boot from OS_ARTIFACT_URL')」,实测正是那一行。它记录了一个预测失败
Ablation A 里 dev 预测 secrets e2e 会红在正对照那行,实测红在前一行的退出码断言 —— 因为去掉校验后不匹配的制品直接启动成功,失败在到达正对照之前就冒出来了。方向性结论(secrets 断言不是这个测试的承重部分)成立,具体预测不成立,写进 PR 而不是抹平。这正是反向验证该有的样子:预测的价值在于它可以错。
Ablation B 还做了二阶测量:保持 ablation 不动、删掉预置的缓存,测试重新变绿 —— 证明它是因为那个 plant 才有区分力,而不是因为网被切了。这是「这个测试很锐利」与「这个测试只是红了」之间的差别。
其余做对的地方
OS_ARTIFACT_PATH的优先级不是修饰,是让验收 Add metamodel interfaces for ObjectQL/ObjectUI contract #1 可达的前提:官方运行时镜像里ENV OS_ARTIFACT_PATH=/srv/app/objectstack.json恒被设置,在不带 app 的容器上它永远指向一个不存在的文件。没有这条优先级,这个特性会恰好在它被设计使用的地方以「OS_ARTIFACT_PATH 指定的制品不存在」拒绝。e2e 每个子进程都设这个变量专门钉住它。fetch拒绝携带 userinfo 的 URL —— 这是被 secrets 测试的正对照逼出来的发现,不是读文档读出来的。凭据移进Authorization: Basic头,顺带也修对了归属:请求行里的凭据会落进制品主机的访问日志。MetadataPlugin/ artifact-source 记录 / 任何日志行;并且所有消息统一擦洗(包括fetch内部产生的、惯例携带整个 URL 的那些)。每条「不包含」断言都配了同批正对照,断言同一段捕获文本包含同一 URL 的 host 和 path。check:type-check-debt中途确实红了(+29),账本未抬 —— 29 个错误同源于.catch((e) => e as ArtifactReferenceError)的联合类型,用refusalOf()helper 在源头解决,顺带让测试更严(错误地成功的调用现在会以自身身份失败)。重新测回记录值 227。ERROR_CODE_LEDGER并说明了理由:这里所有拒绝都发生在 HTTP server 绑定之前,注册会造出账本自己定义为缺陷的「不可发射行」。范围
size/xl(+2320),但落点分布合理:机制在packages/runtime(开放框架的机制),策略在packages/cli,没有加 entitlement 检查 —— 围栏租户形态仍经@objectstack/organizations授权,与制品如何到达无关。这与卡片的「开放机制、封闭智能」边界注一致。范围外均未触碰:
OS_PACKAGE_REF/ registry 解析 / 签名强制 / entitlements;多租户 fleet 与 hostname 路由;cloud 侧接入(objectstack-ai/cloud#1292 —— 本卡是上游,不等它)。Generated by Claude Code
feat(cli,runtime): artifact-pinned boot — OS_ARTIFACT_URL with an SRI…