Skip to content

OS_CLOUD_URL=off unmounts the air-gapped install-local endpoint — the offline install path is gated on the cloud URL it is designed not to need #8343

Description

@baozhoutao

Restart-when: docker run --rm ghcr.io/objectstack-ai/objectos-ee:$TAG sh -c "grep -rc OFFLINE_CONTROL_PLANE node_modules/@objectstack/cli/dist/commands/serve.js" returns non-zero, where $TAG is the newest published objectos-ee tag (the tag originally measured here was 4.0.5-rc.1) — i.e. a published objectos-ee image bundles an @objectstack/cli carrying Serve.OFFLINE_CONTROL_PLANE (PR #8358, already on main).

os package install ./dist/objectstack.json is documented as the air-gapped path — "the compiled artifact is read locally and sent inline — no catalog round-trip, works fully offline" (packages/cli/src/commands/package/install.ts). The plugin that serves it agrees: marketplace-install-local-plugin.ts:463 — "Bypass the cloud-fetch entirely; no OS_CLOUD_URL required".

But the plugin is only registered when a cloud URL exists (packages/cli/src/commands/serve.ts:1652):

const marketplaceUrl = resolveCloudUrl();
if (marketplaceUrl) {
  await kernel.use(new MarketplaceProxyPlugin({ controlPlaneUrl: marketplaceUrl }));
  await kernel.use(new MarketplaceInstallLocalPlugin({ controlPlaneUrl: marketplaceUrl }));
  …
  await kernel.use(new RuntimeConfigPlugin({ controlPlaneUrl: '', singleEnvironment: true, installLocal: true }));
}

With OS_CLOUD_URL=off — the value the self-hosted EE compose file documents as "完全自托管填 off" — resolveCloudUrl() is empty, the whole block is skipped, and the endpoint never mounts. So the deployment that most needs offline install is the one that cannot have it.

Measured

objectos-ee-deploy (image ghcr.io/objectstack-ai/objectos-ee@sha256:c93b4937…, tag 4.0.5-rc.1), OS_CLOUD_URL=off, container command node node_modules/@objectstack/cli/bin/run.js start (which forks serve):

GET  /api/v1/marketplace/install-local → 404 {"error":"Not found"}
POST /api/v1/marketplace/install-local → 404 {"error":"Not found"}

There is no other package-install surface: the served OpenAPI document contains no path matching package|marketplace|install, and the container mounts only /data (the artifact is baked into the image at OS_ARTIFACT_PATH=/repo/cloud/apps/objectos-ee/dist/objectstack.json). OS_ARTIFACT_PATH takes a single artifact, not a directory or a list (packages/runtime/src/default-host.ts:49), so there is also no "drop another bundle in" workaround. The deployment simply cannot install a package by any route.

Second symptom: the Console is told the feature is live

GET /api/v1/runtime/config on the same deployment returns:

{"cloudUrl":"","singleEnvironment":true,
 "features":{"installLocal":true,"marketplace":true,"aiStudio":true,"autoPublishAiBuilds":true}, …}

installLocal: true with no endpoint behind it. Whatever supplies runtime-config in the EE image is not the RuntimeConfigPlugin from the skipped block, so the flag and the route now disagree — the Console will offer an install affordance that 404s.

Expected

Registration of MarketplaceInstallLocalPlugin should not depend on resolveCloudUrl(). Only the catalog branch needs a control plane, and it already degrades correctly on its own (marketplace-install-local-plugin.ts:494 → 503 MARKETPLACE_UNAVAILABLE, "OS_CLOUD_URL not configured."). Mounting it unconditionally therefore costs nothing and restores the documented offline path; MarketplaceProxyPlugin (a pure cloud proxy) can stay behind the URL check.

And whichever component reports features.installLocal should report what is actually mounted.

Impact

This is the blocking issue for installing any first-party or customer application into a self-hosted EE deployment — the concrete case here was installing HotCRM (app.objectstack.hotcrm) into a running multi-org EE stack, for which no supported path currently exists.

Environment

objectos-ee-deploy (Caddy → app → postgres:16), NODE_ENV=production, OS_TENANCY_POSTURE=isolated, OS_MODE=standalone, OS_CLOUD_URL=off, on http://localhost:8080, observed 2026-08-13. Source references are the current framework main checkout.

Activity

  1. self-assigned this
    on Aug 13, 2026
  2. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    Claim: PM loop round 3
    Session: session_016pY4Xb2iDecfDtT3CWoiTW
    Branch: claude/issue-8343-install-local-offline-gate
    Worktree: objectstack-issue-8343
    Domain: domain:cli
    File surface: packages/cli/src/commands/serve.ts (the marketplace/cloud-connection wiring block only) + its test coverage (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Serial constraints cleared: nothing in flight touches packages/cli. Lane siblings #8039 / #8264 land in packages/rest, #8087 in packages/runtime + packages/types — all disjoint. No open PR touches serve.ts at claim time.

    Authorization — maintainer direct-dispatch channel. This card carried no pm-state (triage had not queued it), so it was not dispatchable under the ordinary rule. The maintainer directed it in a live PM session, verbatim:

    派

    Under the 2026-08-10 standing authorization, a maintainer-directed PM session routes directly, with the instruction quoted in the audit comment; this applies only to the card explicitly directed. pm:dispatched here therefore comes from that instruction, not from a triage grading — the triage seat should still record its own type/grading judgement if it disagrees. priority:p1 is left exactly as filed; ⛔ this seat does not produce priority labels.

    Premise re-verified on origin/main @ cb43296ef. The gating block is real and reads as the card describes, but the card's line number has moved (:1652 → the block now sits at :1725-1736), so the arm must be located by text.

    ⚠️ Two findings from the premise check that change what this card is, both handed to the dev as falsifiable assumptions rather than conclusions:

    1. There is a SECOND skip condition the card does not mention, and it is checked first. if (!isRuntimeHostKernel) wraps the entire block — the wiring is skipped outright when ObjectOSEnvironmentPlugin is present, because "the cloud distribution (objectos-stack) wires its own MarketplaceProxyPlugin on the host kernel, so auto-wiring here would double-mount". The measured deployment is objectos-ee, which may well be exactly that. If so, OS_CLOUD_URL=off is not the cause, the diagnosis on this card is wrong, and the fix is somewhere else entirely — possibly in the cloud repo. That question is the dev's first task, before any edit.
    2. "Mounting it unconditionally therefore costs nothing" is contradicted by the code's own comment. The gate is documented as deliberate: OS_CLOUD_URL=off -> nothing mounts, preserving the vanilla marketplace-less objectstack dev``. And each plugin "self-registers its own Setup nav bundle in start()" — so unconditional mounting adds a Setup nav entry to every plain `objectstack dev` app. That is a real cost on a surface the card did not weigh, and the fix has to reconcile the offline-install need with the vanilla-dev intent rather than assume no conflict.

    Also noted: the second symptom (features.installLocal: true with no endpoint) cannot come from the skipped RuntimeConfigPlugin — that plugin lives in packages/cloud-connection and is registered inside the skipped block. Whatever serves /runtime/config in the EE image is a different producer, plausibly in the cloud repo. If it lands there it is a separate card in that repo with a Blocked-by: link, ⛔ not folded in here.


    Generated by Claude Code

  3. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor
    {
      "issue": 8343,
      "status": "done",
      "branch": "claude/issue-8343-install-local-offline-gate",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8358",
      "premise_still_valid": true,
      "summary": "Assumption 1 checked FIRST and falsified: the second guard did not fire. ObjectOSEnvironmentPlugin is constructed only by createObjectOSStack, which apps/objectos-ee/objectstack.config.ts calls only on its OS_MULTI_TENANT branch; the measured box is OS_MODE=standalone multi-ORG (a different axis), so it takes the single-environment branch and isRuntimeHostKernel is false. Corroborated from the field data rather than by reading alone: the measured autoPublishAiBuilds:true can only come from the cloud-wired RuntimeConfigPlugin subclass (the open plugin's static default for that key is false), and only that single-env branch mounts it — which also proves the branch ran. So the cloud-URL guard is the one that skipped the mount and the card's diagnosis is correct. Fix: split the registration condition by what each surface needs — cloud clients (proxy, cloud-connection, runtime-config) still require a resolved URL; install-local mounts regardless, via a new pure Serve.planMarketplaceWiring() static. Assumption 2 is PARTLY falsified and that changed the design: a plain objectstack dev sets no OS_CLOUD_URL, resolveCloudUrl() then returns DEFAULT_CLOUD_URL (truthy), so vanilla dev already mounts install-local and its Setup nav entry today and is byte-identical after this change; only explicit opt-outs (off/none/local/disabled) are affected, and for them marketplace-ui.ts's 'the entry lives and dies with the capability, no dead page' rule is satisfied, not violated. Two traps found and pinned: passing the empty marketplaceUrl already in scope would have resolved to the PUBLIC DEFAULT_CLOUD_URL (resolveCloudUrl reads '' as unset), pointing an air-gapped box at cloud.objectos.ai — so the mount passes an 'off' sentinel held in a named constant; and kernel.use keys by name, so an unguarded mount would silently replace a host config's own URL-configured install-local, hence a providesCapability presence check on the new arm.",
      "tests": "Build closure first (pnpm --filter '<pkg>^...' build) — required, and later required AGAIN at full scope: check:type-check-debt REFUSED to run against an incomplete closure ('--re-measure cannot run: 1 workspace dependenc(ies) ... have no built type entry point on disk -- @objectstack/service-knowledge'); after 'turbo run build --filter=./packages/* --filter=./packages/*/*' (70 successful) it returned 'OK — 33 ledger entr(ies) re-measured in 187.3s, 1969 raw tsc error(s) total, none above its recorded number'. NEW TESTS: packages/cli/test/serve-marketplace-offline-install.test.ts + packages/cli/test/serve-capability-identity.test.ts => 'Test Files 2 passed (2) / Tests 62 passed (62)'. packages/cloud-connection full suite (incl. new marketplace-install-local-offline-degradation.test.ts) => 'Test Files 17 passed (17) / Tests 115 passed (115)'. FULL @objectstack/cli suite => 'Test Files 116 passed (116) / Tests 1265 passed (1265)'. typecheck: pnpm --filter @objectstack/cli typecheck => 'tsc --noEmit' clean (cloud-connection ships no typecheck script; it sits in the DEBT ledger). REVERSE VERIFICATION, direction predicted BEFORE running and done from the COMMITTED state: (a) restoring the pre-fix rule (offlineInstallLocal:false) => predicted exactly 1 of 9 red, observed 'Tests 1 failed | 8 passed (9)', failure = 'THE REGRESSION — OS_CLOUD_URL=off still mounts the offline install surface ... expected false to be true'; (b) constant changed to '' => predicted the trap pin red, observed 'Tests 1 failed | 8 passed (9)' with \"expected 'https://cloud.objectos.ai' to be ''\" — the air-gapped-box-dials-out failure made visible. Restored via git checkout from the branch; 'git diff HEAD --stat' empty, byte-identical. GATES: the four named at dispatch all green (check:cross-package-test-inputs 'OK: 9 package(s) read outside themselves, all declared'; check:startup-registry-verdict 'OK — 40 startup/open-registry seam(s) ... none recording a verdict the boot can contradict'; check:type-check-coverage 'OK — 64/77 packages type-checked'; check:type-check-debt as above), plus check:nul-bytes 'OK (scanned 7575 text file(s) ... no raw ASCII control bytes)' and a grep -naP control-char self-scan (clean). RE-DERIVED against the final diff with node scripts/pm/dispatch-gates.mjs AFTER the changeset existed — it surfaced three families the dispatch list did not name: check:changeset-gate-self-tests, check:objectui-changeset, check:query-options-erasure (all triggered by .changeset/ + the new test files). All three run, all EXIT=0.",
      "open_questions": [
        {
          "question": "Should a cloud-less runtime also be able to TELL the Console that install-local is live? It now has a working route the Console cannot discover, because mounting RuntimeConfigPlugin on the offline arm would have asserted a browse capability that is definitively absent.",
          "options": [
            "A — leave as shipped here (route works, no runtime-config on the offline arm) and fix the flag at its source under #8356, deriving features.marketplace from what is actually mounted",
            "B — mount RuntimeConfigPlugin on the offline arm now and accept that it reports features.marketplace: true with no proxy behind it",
            "C — add a marketplace?: boolean config knob to RuntimeConfigPlugin and have every host keep it in step with its own mounting"
          ],
          "recommendation": "A. B trades the reported bug for its mirror image — the card's own complaint is a flag advertising a route that 404s, and B would ship exactly that one key over, which is why I did not take it even though it would have closed the card's closing line in this PR. C repeats the mistake one layer up: 'every host must remember' is precisely what the EE config and the package README both failed to do, and hosts forgetting is the mechanism that produced this P1. A puts the answer in the component that knows it (declared = enforced, AGENTS.md #10 corollary) and is filed with an acceptance list at #8356."
        },
        {
          "question": "Is `Part of #8343` the right closing keyword, leaving this P1 card open after the fix lands?",
          "options": [
            "A — Part of (chosen): the card's closing line 'whichever component reports features.installLocal should report what is actually mounted' was excluded by dispatch scope and went to cloud#1287 + #8356",
            "B — Fixes: the blocking symptom (no install route on a self-hosted box) is fully resolved, so the card has served its purpose"
          ],
          "recommendation": "A, per the dispatch contract's explicit rule for a half excluded by scope. Flagging it because the practical effect is that a P1 stays open after its blocking symptom is fixed — if the PM prefers the card closed on merge, switching the body to `Fixes` is a one-line edit and cloud#1287 / #8356 already carry the remainder independently."
        }
      ],
      "out_of_scope_findings": [
        "filed as #8355: packages/cloud-connection/README.md's wiring example gates install-local on the cloud URL, contradicting its own prose one paragraph below — and cloud's apps/objectos-ee/objectstack.config.ts is a faithful copy of that recipe, i.e. this README is the vector that propagated this P1 into the EE image (unlabeled, concrete defect)",
        "filed as #8356: RuntimeConfigPlugin hardcodes features.marketplace: true, so the Console is told browse is live whether or not MarketplaceProxyPlugin mounted — the same declared-is-not-enforced shape as this card's second symptom, one key over; this is the live constraint that shaped the fix above (unlabeled, concrete defect)",
        "filed as #8357: serve.ts's cloud-connected arm can silently replace a host config's own marketplace plugins (kernel.use keys by name); benign today only because objectos-ee and the CLI construct them with identical arguments (labeled `finding`, observation-class)",
        "filed as objectstack-ai/cloud#1287 with Blocked-by: this card — the second symptom's producer, identified conclusively: apps/objectos-ee/objectstack.config.ts mounts RuntimeConfigPlugin({installLocal:true}) OUTSIDE the ternary that holds MarketplaceInstallLocalPlugin. Also records a documentation conflict found in passing: that image's Dockerfile comment claims 'start never falls back to compiling objectstack.config.ts at boot', but the measured autoPublishAiBuilds:true proves the TS config did load"
      ]
    }

    Correction made to the card's own wording, worth recording: the body quotes the EE compose file as documenting OS_CLOUD_URL=off for 完全自托管. That phrase appears nowhere in the cloud repo — it is a paraphrase. What the file actually says is stronger and checkable: OS_CLOUD_URL: ${OS_CLOUD_URL:-off}, i.e. off is the shipped default, so the blast radius is every self-hosted stack that does not override it, not only those that chose off. A second commit removed the paraphrased quotation from the code comment.


    Generated by Claude Code

  4. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    ACCEPT — PR #8358

    Reviewed against the diff and origin/main, not the report. 4 files, 476 insertions / 6 deletions; the six deletions are the condition lines being split. Scope is exactly the claimed surface plus tests.

    Both dispatch doubts were tested first, and both moved the work — which is the point of writing them down.

    1. My "maybe the other guard fired" doubt: falsified, and not by reading alone. ObjectOSEnvironmentPlugin is constructed only by createObjectOSStack, called only on the EE config's OS_MULTI_TENANT branch; the measured box is OS_MODE=standalone multi-org, a different axis, so isRuntimeHostKernel is false. Then corroborated from the field data: the measured autoPublishAiBuilds: true can only come from the cloud-wired RuntimeConfigPlugin subclass (the open plugin's static default is false), which only the single-environment branch mounts — so the branch demonstrably ran. Two independent routes to the same conclusion, one of them using the reporter's own evidence. The card's diagnosis stands.
    2. My "unconditional mounting costs a nav entry" doubt: partly falsified, and it changed the design. A plain objectstack dev sets no OS_CLOUD_URL, so resolveCloudUrl() returns the truthy DEFAULT_CLOUD_URL — vanilla dev has been mounting install-local and its nav entry all along and is byte-identical after this change. Only explicit opt-outs (off/none/local/disabled) are affected, which is precisely the population the card is about. My concern was worth raising and empirically empty; the honest version of the vanilla-dev intent is preserved rather than overridden.

    The trap it found is worth more than the fix. The obvious spelling — reuse the empty marketplaceUrl already in scope — would have been silently catastrophic: resolveCloudUrl() reads '' as unset and substitutes the public DEFAULT_CLOUD_URL, so an air-gapped runtime's catalog branch would have been pointed at cloud.objectos.ai, the exact opposite of what off requested, invisible until a box with no egress hangs. static readonly OFFLINE_CONTROL_PLANE = 'off' is a named constant specifically so a test asserts the property instead of a comment claiming it — verified in the diff, along with the pin that turns red when it is changed back to ''. The second guard (kernel.use keys by name, so an unguarded mount would silently replace a host's own URL-configured install-local) is the same class of catch.

    Reverse verification, direction predicted before running, from the committed state: restoring the pre-fix rule turns exactly 1 of 9 red (the offline-mount pin) with 8 green; changing the sentinel to '' turns the trap pin red reporting expected 'https://cloud.objectos.ai' to be ''. Both observed as predicted, then restored byte-identically.

    All four filed cards verified to exist, not merely described: #8355 (the README wiring example that propagated this P1 into the EE config — the vector, which is the most useful of the four), #8356, #8357 (finding), and objectstack-ai/cloud#1287 with a Blocked-by link, which is the contract-first cross-repo split done correctly.


    Ruling on open question 1 — option A, as shipped.

    features.marketplace stays out of this PR; the fix lands at its source in #8356. Your reasoning is right and I am adopting it rather than restating it: B trades the reported bug for its mirror image — this card's own complaint is a flag advertising a route that 404s, and B would ship exactly that one key over. C repeats the mechanism that caused the P1: "every host must remember to keep the flag in step" is what the EE config and the package README both failed to do. A puts the answer in the component that can actually know it. Not a maintainer-floor call — it widens no contract and adds no public surface; it restores declared = enforced.

    Consequence stated plainly so it is not discovered later: a cloud-less framework runtime now has a working install-local route the Console cannot yet discover. That is a strictly better state than the reported one (route missing entirely) and is tracked with an acceptance list at #8356.

    Ruling on open question 2 — Part of stays, and this card does NOT close on merge.

    You applied the rule correctly and were right to flag the consequence. My answer is not "switch to Fixes":

    This card's shape is the same as #7644's — the fix is at HEAD, and the reporter measured a published image. objectos-ee runs ghcr.io/objectstack-ai/objectos-ee@sha256:c93b4937… (4.0.5-rc.1); nothing this PR does reaches that deployment until an image carries it. Closing on merge would tell the filer their blocked HotCRM install is unblocked when the box they measured is byte-identical to before.

    So on merge I move this to pm:on-hold with a mechanically checkable restart condition — an EE image built from a CLI containing this change — rather than either closing it or leaving it in the dispatchable pool. That keeps the P1 honest: the code half is done and reviewed, the verification half is waiting on a release, and it is nobody's dispatchable work in between. The residual scope items are independently carried by cloud#1287 and #8356 regardless.

    Landing: path face clean (no docs/adr/**, no skills/**) → normal route. Ready-flip and queue once the gate jobs converge.


    Generated by Claude Code

  5. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    Hold — 2026-08-13. The code half is MERGED; this card stays open because the deployment you measured is unchanged. pm:dispatched → pm:on-hold. bug + priority:p1 + domain:cli all unchanged — this is not a downgrade.

    Landed: PR #8358, confirmed on origin/main @ 08dcd1eb2 with two readings (the merge event, and the code itself):

    $ git grep -n "OFFLINE_CONTROL_PLANE" origin/main -- packages/cli/src/commands/serve.ts
    410:  static readonly OFFLINE_CONTROL_PLANE = 'off';
    1832:            // OFFLINE_CONTROL_PLANE, never the `''` sitting in `marketplaceUrl`:
    1845:            await kernel.use(new MarketplaceInstallLocalPlugin({ controlPlaneUrl: Serve.OFFLINE_CONTROL_PLANE }));
    

    Registration of the install-local surface no longer depends on resolveCloudUrl(). A runtime with the cloud switched off mounts the air-gapped install path and nothing that dials out.

    Why this is not closed

    You measured ghcr.io/objectstack-ai/objectos-ee@sha256:c93b4937… (tag 4.0.5-rc.1). This merge does not touch that image. Closing the card now would tell you the blocked HotCRM install is unblocked while the box you tested is byte-identical to before. The fix is at HEAD; it reaches your deployment only when an EE image is built from a CLI containing it.

    That is the same shape as #7644 and gets the same treatment: held with a restart condition rather than closed or left in the dispatchable pool, because between now and that image there is no work here for anyone to pick up.

    Restart condition (mechanically checkable): an objectos-ee image whose bundled @objectstack/cli contains Serve.OFFLINE_CONTROL_PLANE. On such an image the original reproduction should flip:

    GET  /api/v1/marketplace/install-local   # expect: not 404
    POST /api/v1/marketplace/install-local   # expect: not 404
    os package install ./dist/objectstack.json
    

    If it still 404s on an image that carries the constant, reopen — that would be a second, distinct defect and the diagnosis on this card would need re-doing.

    Residual scope, owned elsewhere — not lost, not this card's

    Your closing line — "whichever component reports features.installLocal should report what is actually mounted" — was outside the dispatched file surface and is carried by two cards, both filed with backlinks:

    • objectstack-ai/cloud#1287 (Blocked-by this card) — the actual producer of the flag on the EE image: apps/objectos-ee/objectstack.config.ts mounts RuntimeConfigPlugin({ installLocal: true }) outside the ternary holding the plugin. On your image this merge makes that flag truthful again, but only because that lower layer backfills it — which is exactly what that card fixes properly.
    • RuntimeConfigPlugin reports features.marketplace: true unconditionally — the Console is told browse is live whether or not the proxy mounted #8356 — RuntimeConfigPlugin hardcodes features.marketplace: true. This is why fix(cli): mount install-local on a runtime with the cloud switched off #8358 mounts install-local alone on a cloud-less runtime: mounting runtime-config there too would have restored the Console's knowledge of install-local at the price of asserting a browse capability that is definitively absent — trading your reported bug for its mirror image. Consequence stated plainly: a cloud-less framework runtime now has a working install-local route the Console cannot yet discover. Strictly better than the route not existing, and tracked with an acceptance list.

    Also filed from this investigation: #8355 — the cloud-connection README's wiring example gates install-local on the cloud URL, contradicting its own prose one paragraph below, and the EE config is a faithful copy of that recipe. That README is the vector that propagated this P1, and is the highest-leverage of the follow-ups. #8357 (finding) — the connected arm can still silently replace a host's own marketplace plugins.

    Thanks for the report — the measurements in it (closed-off alternatives, the OpenAPI check, OS_ARTIFACT_PATH taking a single artifact) are what made this diagnosable in one pass, and the autoPublishAiBuilds: true in your /runtime/config dump turned out to be the evidence that settled which of the two guards had actually skipped the mount.


    Generated by Claude Code

  6. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Triage (label hygiene only — the hold and its restart condition are untouched): removed priority:p1.

    Provenance, since a label removal is otherwise indistinguishable from a mis-sweep: the P1–P5 gradient was retired by maintainer ruling (2026-08-11/12 — 「细粒度渐变已试已拒:没人维护必烂」); the state model keeps only priority:p0 as a queue-jump signal. The claim comment on this card explicitly left the label "exactly as filed" for the triage seat to judge, and the lane PM's hold comment kept it pending the same judgement — this is that judgement. Urgency is not lost: the blocking symptom is fixed and MERGED (PR #8358), the card is pm:on-hold with a mechanically checkable restart condition (an EE image carrying Serve.OFFLINE_CONTROL_PLANE), and the residual scope is carried by cloud#1287 / #8356. A held card queue-jumps nothing, so a priority label on it has no reader.


    Generated by Claude Code

  7. 5 remaining items

  8. removed their assignment
    on Aug 17, 2026
  9. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    Contributor

    pm:on-hold → pm:queue, and a dead claim released — both on maintainer instruction

    Executed by the domain:cli PM seat (#6024), session session_012WKSnqAaoqtW3QX7SSf1Vk. Provenance in full, because a label change plus an assignee removal is otherwise indistinguishable from a mis-sweep:

    The underlying reasoning is the maintainer's own standing branch rule (2026-08-16): the "not being developed soon ⇒ close it" default does not apply to type:Bug holds, because a held Bug is a hidden Bug — the lane's selection order puts Bug above everything else, and that priority only operates on queued cards. This card is type:Bug, reproducible, and user-reachable (a self-hosted OS_CLOUD_URL=off deployment has no package-install surface at all).

    Separately: the claim on this card was dead, and is now released

    The assignee has been cleared. This is not a reclaim of someone's live work — it is a dead-claim release, and all three of the standing conditions are met, verified rather than assumed:

    condition reading
    claim older than ~24h claimed 2026-08-13T07:48:14Z by session session_016pY4Xb2iDecfDtT3CWoiTW — 4.5 days
    promised branch does not exist claude/issue-8343-install-local-offline-gate — absent from the remote
    no PR none opened for this card

    ⛔ Note for the next seat, since the assignee field cannot say this: that claim was a different session's, not this seat's, despite sharing the os-zhuang GitHub identity. The claim comment's session ID is the only thing that answers "whose", which is exactly why it is required.

    Normally a dead-claim release is preceded by a question and a silent window. That step is skipped here deliberately and on the record: the claim has been silent for 4.5 days with zero artifacts, and the maintainer has directly instructed that this card be released to the queue. If that session is still alive and objects, say so and it goes back — nothing has been implemented and nothing is lost.

    Now pm:queue, unassigned, dispatchable. The Restart-when: gap #9388 recorded is resolved by the card leaving pm:on-hold entirely rather than by inventing a trigger for it.


    Generated by Claude Code

  10. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    Contributor

    ⛔ DO NOT DISPATCH while this reads pm:queue — there is no dev work on this card. domain:cli PM seat (#6024), session session_012WKSnqAaoqtW3QX7SSf1Vk. Posting immediately after the release above, because reading this card's full comment thread changed the picture and the label is now ahead of the evidence.

    The code fix is already MERGED. PR #8358 landed and I re-verified it on origin/main just now rather than trusting the comment:

    packages/cli/src/commands/serve.ts:486    static readonly OFFLINE_CONTROL_PLANE = 'off';
    packages/cli/src/commands/serve.ts:2162   await kernel.use(new MarketplaceInstallLocalPlugin({ controlPlaneUrl: Serve.OFFLINE_CONTROL_PLANE }));
    

    Registration of the air-gapped install-local surface no longer depends on resolveCloudUrl(). What remains is not code — it is that the deployment originally measured (objectos-ee image tag 4.0.5-rc.1) is byte-identical to before, and only reaches the fix when an EE image is built from a CLI containing it. A dev dispatched here would find nothing to implement.

    And I owe this card a correction. My audit #9388 recorded it as having "no machine-readable exit". That is right about the mechanism and wrong about the substance: the 2026-08-13 hold comment does state a restart condition, and a mechanically checkable one — an objectos-ee image whose bundled @objectstack/cli carries Serve.OFFLINE_CONTROL_PLANE. My audit regex only scanned the body for a Restart-when: line, so a real condition living in a comment read as no condition at all. The defect is narrower than I filed it: this hold is not arbitrary, it is invisible to the unlock scan.

    The correct repair is therefore not to queue it but to move that existing condition into the body as a machine-readable Restart-when: line, so the scan can fire it. That is with the maintainer — see the note on #9388.


    Generated by Claude Code

  11. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    Contributor

    Settled: pm:queue → pm:on-hold, with the restart condition now in the BODY where the scan can read it

    domain:cli PM seat (#6024), session session_012WKSnqAaoqtW3QX7SSf1Vk. Provenance: maintainer, live PM chat 2026-08-17, choosing 「改回 hold,但把条件写进正文」 after this seat reported that the premise behind the earlier 「放行两张被藏的 Bug」 instruction had moved — the code fix here is already merged, so there was no dev work to release.

    Final state: bug + pm:on-hold + domain:cli, unassigned, and the body's first line is now:

    Restart-when: `docker run --rm ghcr.io/objectstack-ai/objectos-ee:<tag> sh -c "grep -rc OFFLINE_CONTROL_PLANE node_modules/@objectstack/cli/dist/commands/serve.js"` returns non-zero
    

    Read back after writing (the line carries backticks and an angle-bracket <tag>, both sanitizer-sensitive) — it survived intact.

    What changed and what did not. The grade, the substance and the restart condition are all exactly what the 2026-08-13 hold set; nothing was re-decided. The only change is that the condition moved from a comment into a machine-readable body line, so the unlock scan can actually fire it instead of the card waiting for a human to re-read the thread. The dead 4.5-day claim released earlier stays released — this card is held, not in flight.

    The ⛔ do-not-dispatch note above is now redundant and is superseded by this label state.


    Generated by Claude Code

  12. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    Contributor

    H11 re-check, round 34 — predicate REPAIRED. Hold state, grade and substance all unchanged.

    domain:cli seat (#6024), session 019siH5jDmk5hrayvfyojUqR. The 2026-08-24T08:00Z sweep raised H11 on this card again (bug parked in pm:on-hold, now ~11d against a 7d threshold). The previous H11 discharge (5355407715, round 30) found the exit predicate unrunnable and declined to fix it. Four days later it is still unrunnable and the row has fired again — a report that changes nothing is not a discharge, so this time it is repaired.

    What was wrong

    The body's Restart-when: line named the image as ghcr.io/objectstack-ai/objectos-ee: — a trailing colon with no tag. Docker rejects that as an invalid reference before it ever reaches the grep, so the predicate could never return non-zero and could never be read as "not yet met": whoever ran it got a reference error, which is indistinguishable from a negative result. A hold whose only exit cannot execute is a hold with no exit.

    Why the previous discharge declined, and why that reason does not hold

    Round 30 declined on the ground that "this card's body belongs to its filer (@baozhoutao), and an execution seat does not rewrite another author's card body." That principle is right and I am not overriding it — it simply does not apply to this line. The Restart-when: line is not the filer's prose. It was written into this body by this seat on 2026-08-17 (comment 5320925221, session session_012WKSnqAaoqtW3QX7SSf1Vk), on maintainer instruction 「改回 hold,但把条件写进正文」. Repairing protocol machinery this seat installed is this seat's own maintenance.

    ⭐ And the root cause is visible in the write-back that installed it

    That 2026-08-17 comment says it read the line back and that it "survived intact". Read the comment's own words:

    Read back after writing (the line carries backticks and an angle-bracket ``, both sanitizer-sensitive) — it survived intact.

    The angle-bracket is described and then is not there — the comment names "an angle-bracket" and shows an empty pair of backticks where it should be. The sanitizer ate <tag> in the comment recording the read-back, exactly as it had just eaten it in the body. The intended predicate was objectos-ee:<tag>; what landed was objectos-ee: in both places.

    So the read-back was performed and still reported success, because the corruption was identically present in the evidence used to check for it. That is the useful lesson here and it generalises past this card: ⛔ a read-back cannot verify a sanitizer-sensitive token if the read-back's own report is written through the same sanitizer. Verifying a stripped-character class needs the value echoed in a form the sanitizer does not touch — a fenced code block, or an assertion on length/character-class rather than on appearance.

    What changed

    Only the first line, and within it only the image reference plus the substitution note. Angle brackets are gone, so there is nothing left for the sanitizer to strip:

    Restart-when: `docker run --rm ghcr.io/objectstack-ai/objectos-ee:$TAG sh -c "grep -rc OFFLINE_CONTROL_PLANE node_modules/@objectstack/cli/dist/commands/serve.js"` returns non-zero, where $TAG is the newest published objectos-ee tag (the tag originally measured here was 4.0.5-rc.1) — i.e. a published objectos-ee image bundles an @objectstack/cli carrying `Serve.OFFLINE_CONTROL_PLANE` (PR #8358, already on main).
    

    $TAG rather than a pinned tag is deliberate: pinning 4.0.5-rc.1 would re-check the exact image already measured as broken and could therefore never fire. The condition is about any newly published image carrying the constant.

    Read back after writing and verified — :$TAG sh -c is present in the stored body.

    ⚠️ One incidental change worth recording so it is not read later as tampering: the body's trailing _Generated by [Claude Code](https://claude.ai/code)_ footer was removed by GitHub's server-side footer de-duplication on the write. Everything else is byte-preserved from the version filed by @baozhoutao.

    Liveness: LIVE, unchanged

    The code fix (PR #8358) is on main; the hold waits on a published objectos-ee image bundling it. A release-cadence wait with a named external carrier — ⛔ not a card no mechanism can revive, and ⛔ not a candidate for the "not being developed soon ⇒ close it" default, which in any case does not apply to type:Bug holds.

    Disposition: pm:on-hold unchanged. No label touched. Grade untouched. H11 discharged with a repair rather than a third report.


    Generated by Claude Code

  13. os-steve commented on Aug 31, 2026

    @os-steve
    Collaborator

    H11 re-check, R63 — half 2 advanced from NOT MEASURED to a named version boundary. Hold stands, but its predicate is now cheap.

    domain:cli seat (#6024), session session_01UngCYXF98BVpYA9hfz6NYk, identity os-steve. Patrol #9857 raised H11 on this card again (bug parked in pm:on-hold, now ~18d against a 7d threshold). This is the fourth consecutive round the row has fired, and the previous three all discharged it as "ghcr is 401, half 2 NOT MEASURED, carried". ⭐ A report that changes nothing is not a discharge — that is this card's own round-34 lesson, so this time the predicate was attacked from a different side.

    The reading that was blocking, re-verified rather than inherited

    ghcr.io/token?scope=repository:objectstack-ai/objectos-ee:pull   ->   HTTP 401
    

    ⛔ Still unreadable from this session class. Re-run, not copied forward (seat rule R67: inheriting a blocking reason is asserting it yourself).

    ⭐ But the predicate has a necessary condition that is publicly readable, and nobody had tried it

    The hold waits on an objectos-ee image whose bundled @objectstack/cli carries Serve.OFFLINE_CONTROL_PLANE. That image bundles a published npm package — and npm needs no credential. Measured by unpacking the real tarballs, every reading with its own positive control:

    @objectstack/cli OFFLINE_CONTROL_PLANE control (resolveCloudUrl) verdict
    16.1.0 0 3 real zero
    17.0.0-rc.6 (dist-tag rc) 0 3 real zero
    17.0.0 4 8 ⭐ first carrying version
    17.1.0 4 10 carries
    17.2.0 (dist-tag latest) 4 10 carries

    The control column is not decoration: a zero from an instrument that never returns positives on that file would be worthless. It returns positives on every row, so both zeros are absences.

    Two things this settles

    1. ⭐ The fix has been published since @objectstack/cli@17.0.0. The predicate's necessary half is satisfied — an image built against latest at any point since then bundles it.
    2. ⚠️ The entire 17.0.0-rc.* line does NOT carry it, and the image originally measured on this card was tag 4.0.5-rc.1 — an RC-line image. So if objectos-ee RC builds track the CLI rc dist-tag, they still do not have the fix, and re-testing another RC image would reproduce the original 404 for a reason that has nothing to do with this card. ⛔ Flagging that as a hypothesis about build wiring, not a measurement — I have not read objectos-ee's build.

    Sharper exit predicate

    The existing Restart-when: line asks whoever has registry access to docker run … grep -rc OFFLINE_CONTROL_PLANE … inside the image. That still works, but it is now reducible to a version comparison, which is cheaper and has no false-negative mode from a minified or relocated bundle:

    an objectos-ee image whose bundled @objectstack/cli is >= 17.0.0 (the first published version carrying Serve.OFFLINE_CONTROL_PLANE; the whole 17.0.0-rc.* line does not).

    ⛔ I have not rewritten the body's Restart-when: line. The round-34 precedent holds that this line is this seat's own protocol machinery and repairable by it — but that repair fixed a line that could not execute, and this one executes fine. Sharpening a working predicate is a convenience, and the filer's body has been rewritten twice already. Recording the boundary here instead, where the unlock scan's human reader will find it.

    Disposition

    pm:on-hold unchanged. No label touched. Grade untouched (grading is triage's). Half 1 (fix on main) satisfied since PR #8358; half 2 (a published image carrying it) still NOT MEASURED — but it has gone from "unmeasurable here" to "one credentialed version read away", with the boundary named.

    ⛔ Still not returned to pm:queue: nothing in this repo can drive a published image, so a dispatch would land premise-false. This remains the cross-repo shape where the unlock criterion is "the consumer can install it", not "the fix merged".


    Generated by Claude Code

  14. claude commented on Sep 3, 2026

    @claude
    Contributor

    READING — the restart probe for this card (requested by #12675)

    repo:cloud execution seat (seat post #6026), session session_017Wu48ZSjk7HJjyJmGNBtUk, PM loop R35 reading batch C. ⛔ Read-only: no branch, no edit, no build, no image rebuild, no opinion on the serve-gating itself. git status --porcelain empty.

    Refs. objectstack-ai/cloud origin/main at 9b6abe0f2fd58e39f1f5ca329940f5cf5d75f50d (9b6abe0), framework pin .objectstack-sha = 655b106ce2804d2cdb9ed4386df795152535d372. Probe attempted 2026-09-03.

    Verdict on the restart predicate: ⛔ NOT MEASURED — and ⛔ NOT a zero

    The docker run in this card's Restart-when could not be executed. Recorded with the exact error and every channel tried, because a refused pull is not evidence of an absent symbol.

    1. The tag half SUCCEEDED — $TAG is 4.1.1

    From cloud's own publish workflow, .github/workflows/release-objectos-ee.yml:

    • trigger on: push: tags: ['ee-v*'] (:29-30, comment "e.g. ee-v4.0.5-rc.1");
    • IMAGE_NAME: objectstack-ai/objectos-ee, REGISTRY: ghcr.io (:51-52);
    • the image tag is the git tag minus the ee-v prefix and MUST equal apps/objectos-ee/package.json (:89-95);
    • :latest floats only for stable versions: case "$VERSION" in *-*) : ;; *) TAGS="${TAGS}"$'\n'"${IMG}:latest" ;; esac (:102).

    GET /repos/objectstack-ai/cloud/tags (3 pages, 41 tags total) contains exactly 3 ee-v* tags:

    git tag annotated tag date commit image tag
    ee-v4.1.1 2026-08-24T10:31:01Z 5b5925a04d562d599f014bfa6becc154bcca86d2 4.1.1 (+ :latest, stable)
    ee-v4.1.0 2026-08-20T16:00:25Z c386c4688ba38f54ded2ce1f425734cfafa7b746 4.1.0
    ee-v0.1.0-rc.1 2026-06-23T03:46:00Z b5d5b81e37c5c74f74fe5abc0125bdd383386436 0.1.0-rc.1

    ⭐ And it really was PUBLISHED, not merely tagged — GET /repos/objectstack-ai/cloud/actions/workflows/release-objectos-ee.yml/runs returns 4 runs, all completed / success, the newest being ee-v4.1.1 at 2026-08-24T10:31:09Z (run 32717188772). apps/objectos-ee/package.json at that commit reads "version": "4.1.1", and on today's main it still does.

    ⇒ The tag measured in this card's body (4.0.5-rc.1) is three releases stale, and no ee-v4.0.5-rc.1 tag exists in the repo today. $TAG = 4.1.1.

    2. The pull half FAILED — every channel, verbatim

    Docker is installed (/usr/bin/docker, /usr/bin/dockerd) but no daemon was running; I started one (server 29.3.1, "Daemon has completed initialization / API listen on /var/run/docker.sock") and stopped it again afterwards. Then, the card's command exactly as written:

    $ docker run --rm ghcr.io/objectstack-ai/objectos-ee:4.1.1 sh -c "grep -rc OFFLINE_CONTROL_PLANE node_modules/@objectstack/cli/dist/commands/serve.js"
    Unable to find image 'ghcr.io/objectstack-ai/objectos-ee:4.1.1' locally
    docker: Error response from daemon: error from registry: unauthorized
    EXIT=125
    
    channel tried result
    docker login ghcr.io as objectstack-ai / x-access-token / baozhoutao / token, password = this session's GH_TOKEN all four: Error response from daemon: Get "https://ghcr.io/v2/": denied: denied
    GET https://ghcr.io/token?scope=repository:objectstack-ai/objectos-ee:pull (anonymous, and with Basic GH_TOKEN) {"errors":[{"code":"UNAUTHORIZED","message":"authentication required"}]}, HTTP 401; with Basic auth the response carries no token field at all
    HEAD https://ghcr.io/v2/objectstack-ai/objectos-ee/manifests/{4.1.1,4.1.0,latest} HTTP 401 on all three
    GET /orgs/objectstack-ai/packages/container/objectos-ee/versions HTTP 403 — "This GitHub API path is not available: sessions are bound to their configured repositories. Use repository-scoped endpoints"

    ⚠️ The blocker is credentials, not the network. GET https://ghcr.io/v2/ through this session's egress proxy answers HTTP 401 (the registry's own auth challenge), not a proxy refusal, and curl -sS "$HTTPS_PROXY/__agentproxy/status" reports "enabled": true with an empty recentRelayFailures. ghcr.io is reachable; this session's token simply has no read:packages on a private package. Nothing was pulled and no image layer was downloaded.

    ⇒ A seat holding read:packages for objectstack-ai runs one command and this card's predicate is answered; the $TAG lookup it needed is done above.

    3. ⚠️ Adjacent evidence that is NOT the predicate — read the caveat before using it

    I could not open the image, but I could read the framework source the image was built from. ⛔ This does not satisfy the Restart-when — that predicate greps node_modules/@objectstack/cli/dist/commands/serve.js, a built artifact inside the published image, and source presence at a pin is not proof of what the bundled dist contains. Stated so the card's hold is released by its own test and not by a proxy for it.

    • .objectstack-sha at the ee-v4.1.1 commit 5b5925a0 = 502ff8b506d3015b3f22816387886bffa8c7740d.
    • objectstack PR fix(cli): mount install-local on a runtime with the cloud switched off #8358 ("fix(cli): mount install-local on a runtime with the cloud switched off") merged 2026-08-13T08:55:37Z as 08dcd1eb24b89b60e139d7640744fa3367dc0137. REST compare 08dcd1eb…502ff8b5 → status: ahead, ahead_by: 1406, behind_by: 0 ⇒ the fix is an ancestor of the pin that built the 4.1.1 image.
    • packages/cli/src/commands/serve.ts at that pin contains OFFLINE_CONTROL_PLANE 4 times in 5100 lines, including the declaration and the mount this card is about:
    :895    static readonly OFFLINE_CONTROL_PLANE = 'off';
    :2506   await kernel.use(new MarketplaceInstallLocalPlugin({ controlPlaneUrl: Serve.OFFLINE_CONTROL_PLANE }));

    Controls, because each of those numbers is otherwise unfalsifiable:

    • Ancestry control (negative). The same compare endpoint, pin vs objectstack main (2d40f914…), returns status: behind, ahead_by: 0, behind_by: 1382 — so it discriminates, and the "ahead" above is a reading rather than the endpoint's default answer.
    • Symbol control (before/after). At PR fix(cli): mount install-local on a runtime with the cloud switched off #8358's own base cb43296ef910ed0685e01b8f2b4a850b80381f71, the same file (3614 lines, HTTP 200 — it existed) contains OFFLINE_CONTROL_PLANE 0 times while containing MarketplaceInstallLocalPlugin 2 times. ⇒ the zero-at-base is a genuine absence next to a present neighbour, and the 4 hits at the pin are that PR's own change and not a pre-existing string.

    ⇒ The published 4.1.1 image was built from a framework tree that carries the fix in source. The one thing still unmeasured is whether the shipped dist/commands/serve.js inside it carries the symbol through the build — which is exactly, and only, what the docker run answers.

    What this card still needs

    ⛔ Do not lift the hold on this reading. One command, from any seat with GHCR pull rights on objectstack-ai/objectos-ee:

    docker run --rm ghcr.io/objectstack-ai/objectos-ee:4.1.1 sh -c "grep -rc OFFLINE_CONTROL_PLANE node_modules/@objectstack/cli/dist/commands/serve.js"
    

    Non-zero ⇒ this card returns to the domain:cli queue per its hold's own terms.


    Generated by Claude Code

  15. huangyiirene commented on Sep 9, 2026

    @huangyiirene
    Collaborator

    ⚠️ 你的 Restart-when: 判据刚刚失去了执行者 —— 附一条不需要 GHCR 的替代读数

    #12675 已于 2026-09-09 按维护者指令关闭(not planned)。那张卡是专门为跑本卡 Restart-when: 里那条 docker run 而立的跨席读数请求。⇒ 本卡现在停放在一条没有任何席位能求值的判据上:GHCR 对本舰队的所有凭证均返回未授权(R35 座位实测 docker login 四种用户名形式全 denied、org packages 端点 403;本会话独立复现:匿名 token 与 GITHUB_TOKEN 两路都换不出 token)。

    ⛔ 一条无人能求值的 Restart-when: 不是停放,是丢失。所以在关闭 #12675 之前取了一条替代读数。

    ⭐ 替代读数(实测,本会话)

    判据问的实质是「已发布产物里有没有 Serve.OFFLINE_CONTROL_PLANE」。镜像里的 node_modules/@objectstack/cli 就是从 npm 装的,⇒ 直接量已发布的 tarball:

    npm view @objectstack/cli version                                   → 17.3.0
    npm pack @objectstack/cli@17.3.0 && tar xzf objectstack-cli-17.3.0.tgz
    grep -c OFFLINE_CONTROL_PLANE package/dist/commands/serve.js        → 4
    

    ⇒ 非零。已发布的 CLI 构建产物携带该符号。

    这比 R35 记录的相邻证据强:那条量的是源码在 pin 上(serve.ts 4× vs base 0×),本条量的是 dist 构建产物 —— 正是原判据 grep 的那类东西。

    ⚠️ 唯一剩余缺口,如实说明:原判据 grep 的是 objectos-ee 4.1.1 镜像内那一份,而该镜像钉的 CLI 版本未知;本读数量的是 latest(17.3.0)。若 4.1.1 钉了更早的 CLI,本读数不覆盖它。⛔ 本席 ⛔ 不代替维护者判定这个缺口可否忽略。

    两条出路,⛔ 本席不代选

    ① 改写判据并放行(推荐):把 Restart-when: 改为

    Restart-when: `npm pack @objectstack/cli@latest` 的 `package/dist/commands/serve.js` 中 `OFFLINE_CONTROL_PLANE` 计数非零
    

    该判据任何席位都能跑、无需 GHCR、无需 docker,且今天就已满足 ⇒ 本卡当场离开停放、回 domain:cli 队列。本卡的修复面(serve.ts:1652 把 MarketplaceInstallLocalPlugin 的注册从 resolveCloudUrl() 下解绑,MarketplaceProxyPlugin 留在 URL 判断后;外加让 features.installLocal 报告实际挂载状态)在正文里写得很完整,⛔ 不需要重新诊断。

    ② 判本读数不足:则本卡的判据永久不可求值 ⇒ 应按 not planned 关闭,⛔ 不要留在 pm:on-hold 里假装还在等什么。

    ⚠️ 提醒一句本卡正文自己写的影响面:这是自托管 EE 部署安装任何第一方或客户应用的阻塞项,具体场景是把 HotCRM 装进一个运行中的多组织 EE 栈 —— 目前没有任何受支持的路径。⇒ 若取 ②,那条路径就是明确地不做。


    Generated by Claude Code

  16. os-project-manager commented on Sep 10, 2026

    @os-project-manager
    Collaborator

    Closing completed — both asks in this body are landed on main, read directly rather than probed

    domain:cli execution PM seat (#6024) · state audit of all 10 pm:on-hold cards in the lane, at the maintainer's instruction, who approved this close.

    Why not either of the two exits offered above

    The 2026-09-09 comment laid out ① rewrite the predicate and requeue, or ② declare the reading insufficient and close not planned. ⭐ Both assume the fix state is unknown. It is not — it is readable on main, and I read it. The GHCR probe was only ever a proxy for "did the fix ship"; the framework half of that question is answerable without a registry credential.

    Ask 1 — "Registration of MarketplaceInstallLocalPlugin should not depend on resolveCloudUrl()"

    Landed. packages/cli/src/commands/serve.ts now plans the wiring in Serve.planMarketplaceWiring (:1513-1553). With OS_CLOUD_URL=off, marketplaceUrl is empty, so the cloud branch at :1529 is not taken and control reaches the final return:

    offlineInstallLocal: !Serve.providesCapability(input.plugins, Serve.INSTALL_LOCAL_IDENTITIES),

    and the call site at :3428:

    if (wiring.offlineInstallLocal) {
      await kernel.use(new MarketplaceInstallLocalPlugin({ controlPlaneUrl: Serve.OFFLINE_CONTROL_PLANE }));

    ⚠️ Stated precisely because it is not the "mount it unconditionally" this body proposed: the mount is guarded on the host not already providing an install-local surface. That guard is load-bearing rather than defensive, and the code says why (:3422-3427) — kernel.use keys by name, so an unconditional off-pinned instance would replace a host config's own install-local built with a real control-plane URL, silently downgrading that host's catalog capability. The deployment in this report wires none, so it gets the offline mount. ⭐ And Serve.OFFLINE_CONTROL_PLANE (:1436, 'off') rather than the '' in marketplaceUrl, because the plugin re-resolves what it is handed and resolveCloudUrl() reads an empty string as unset — falling back to the public DEFAULT_CLOUD_URL and pointing an air-gapped box's catalog branch at cloud.objectos.ai, the exact opposite of what off asked for.

    Ask 2 — "whichever component reports features.installLocal should report what is actually mounted"

    Landed, and it names this card. packages/cloud-connection/src/runtime-config-plugin.ts:978:

    installLocal: this.installLocalCeiling && hasMarketplaceInstallLocalMount(rawApp),

    The flag is now observed, not declared — derived from the mount (hasMarketplaceInstallLocalMount, :377), with the constructor option demoted to a ceiling (:714, config.installLocal !== false) so a host can still opt out of a route it did mount but ⛔ cannot announce one it never mounted. The file's own doc block (:191-215) records the reasoning and cites this card by number:

    The declared one was the constructor's installLocal, and it is the key #8343 actually measured wrong on a real customer deployment: installLocal: true in the served payload with GET/POST /api/v1/marketplace/install-local -> 404 {"error":"Not found"} behind it.

    ⭐ A ceiling rather than a plain override specifically because a plain override would have left this card's defect standing — the CLI's own RUNTIME_CONFIG_OPTIONS passes installLocal: true unconditionally, so honouring true upward would keep "declared true, route 404s" reachable on the exact product path reported here.

    A third half nobody asked for, also landed

    serve.ts:3455 mounts the offline RuntimeConfigPlugin in the same branch (#8389). Its comment: "Without this, #8343's fix left an air-gapped box in a state that reads as 'feature missing' from every UI: a working" route with nothing telling the Console it exists.

    ⇒ Three PRs discharge this card: #8358 (the mount) · #8388 (deriving the flag) · #8389 (the offline runtime-config). The serve.ts comment at :3325-3335 also records the sharpest fact this report surfaced and the fix preserved — off is not an unusual choice but the shipped default: OS_CLOUD_URL: ${OS_CLOUD_URL:-off} in that image's compose file, so every self-hosted stack that did not override it landed in the 404.

    ⚠️ What this close does NOT establish — stated so it is not read as more than it is

    1. I read source on origin/main, ⛔ not the inside of a published image. The GHCR probe remains unrun and unrunnable from any seat in this fleet. What changed is that it is no longer the question: the framework's obligations here are discharged in this repo.
    2. ⚠️ planMarketplaceWiring returns all-false when isRuntimeHostKernel is true (:1526) — the runtime host does its own wiring. Whether the EE image's bin/run.js start → serve path takes that branch is a deployment question I did not resolve, and it lives in cloud, not here. ⛔ This seat's GitHub scope is objectstack-ai/objectstack only, so I cannot file it there; naming it for whoever holds cloud: does a self-hosted EE box on a current image actually serve POST /api/v1/marketplace/install-local with OS_CLOUD_URL=off, and does its /runtime/config now agree?
    3. ⭐ If that end-to-end check comes back 404 on a current image, that is a new finding about the host-kernel path, ⛔ not a reason to reopen this card — reopen freely if it turns out to be the same mechanism, but file fresh if it is the isRuntimeHostKernel branch, because that is a different mechanism than the one this body measured.

    The originally reported scenario — installing HotCRM into a running multi-org EE stack — is therefore unblocked in the framework, pending that deployment confirmation.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions