Skip to content

chore(studio): build the SDK from the checkout permanently - #2288

Merged
Nashon-Steffen merged 2 commits into
mainfrom
Nashon-Steffen/mecatl-studio-ci/cd
Oct 13, 2026
Merged

Nashon-Steffen merged 2 commits into
mainfrom
Nashon-Steffen/mecatl-studio-ci/cd

Conversation

@Nashon-Steffen

Copy link
Copy Markdown
Contributor

Summary

Since #2219, Studio's BFF has used the checked-out sdk/typescript through a file: dependency. That PR called this temporary, to last until an SDK release included the execution-template contract. SDK v0.5.0 (#2284) now includes it. This PR makes the checkout dependency the permanent design instead of switching back to npm.

Why not go back to npm:

  • A Studio image could pair main's daemon contract with an older released SDK.
  • Any change spanning the proto, SDK and Studio would need an SDK release before Studio could build. That's the "one or two releases per image" problem.

Why this matters now: continuous builds from main to the infra dev cluster need Studio, its SDK and the daemon it targets to come from one commit.

What changes:

  • Guard: apps/server/src/sdk-source.test.ts fails if the dependency moves to a registry version. I checked it fails when the dependency is set to 0.5.0.
  • About page: in a built image, the SDK version carries the Studio build after a +, e.g. 0.5.0+v0.0.48. The package version alone can predate the source. Local source-mode runs still show the plain version.
  • Comments and docs:
    • The "temporary" wording is replaced in apps/Dockerfile, apps/README.md and ci.yml.
    • The outdated "released SDK only" rule in docs/architecture/api-surface.md is updated.
    • user-docs/operating/studio.md explains the About version format.
  • Biome: the rule against importing the SDK by relative path stays, with a reworded message. Studio still uses only the SDK's public exports. The SDK's own package.test.ts and declarations.test.ts already check the npm tarball on every SDK change.
  • Cleanup:
    • Removed the SDK 0.4.0 minimumReleaseAgeExclude entry from apps/pnpm-workspace.yaml.
    • Removed the Renovate rule for adopting SDK releases in apps/. Renovate skips file: dependencies, so the rule did nothing.

Issue relationship

Relates to #2219 (follow-up to its temporary checkout-SDK dependency)

Type of change

  • Bug fix
  • New feature
  • Refactoring (no behavior change)
  • Dependency update
  • Documentation
  • Other (describe): About's SDK version gains a +<studio build> suffix in built images

Test plan

Baseline checks

  • Linting (task lint): one gosec G703 finding in environment/microvm/launch_ownership_darwin.go. That file is macOS-only and this PR has no Go changes.

  • Fast offline test suite (task test): covered by task test:race below.

  • Pre-review race suite (task test:race): three macOS-specific failures in code this PR doesn't touch:

    • TestMecatedCLICompositionValidatesMicroVMDefaultAfterReadiness, twice (unix socket under /dev/fd).
    • TestRepositoryGuestAuthenticationRejectsCompetingConnectorBeforeDisclosure (a timing limit).

    Everything else passed. Linux CI is authoritative.

  • Offline demo (go run ./cmd/mecademo): shows the tool call, the permission ask and approval, and the result.

  • Markdown changed: task docs:check passed with 0 broken links or anchors. The full task docs stopped at docs:grpc-ref because buf isn't installed locally, and that step only regenerates the gRPC reference.

  • User docs/user-facing behavior changed: site build (task site:build)

  • Guarded engine API affected: compatibility check (task api:check)

  • Intentional engine API change: task api:update + engine/CHANGELOG.md

Studio: task studio:lint studio:typecheck studio:test studio:generated-check passed (server 197/197, web 806/806, no contract drift).

Changes

File Change
apps/server/src/sdk-source.test.ts New guard for the file: SDK dependency
apps/server/src/sdk-version.ts, app.ts (+ tests) reportedSdkVersion stamps the SDK version with the Studio build
apps/Dockerfile, apps/README.md, .github/workflows/ci.yml, apps/web/e2e/chat-approval.spec.ts Comments now give the reason for building from source instead of calling it temporary
docs/architecture/api-surface.md Replace the "released SDK only" rule
user-docs/operating/studio.md Explain About's SDK version format
apps/biome.json Reword the relative-import message
apps/pnpm-workspace.yaml, renovate.json Remove the obsolete npm SDK settings

User-facing change

In a published Studio image, Settings → About shows the SDK version with the Studio build appended, e.g. 0.5.0+v0.0.48. Nothing else changes.

Special notes for reviewers

  • This reverses the published-SDK-only rule that was written down deliberately. @JAORMX, since feat: add execution templates with platform-managed identity #2219 marked the dependency as temporary, please confirm you weren't planning to switch back to npm.
  • The /api/v1/runtime sdkVersion field keeps its type (string). Only its value format changes in built images. Studio is early access, and the field is display-only in About and the support summary.

🤖 Generated with Claude Code

Studio's BFF has used the checked-out sdk/typescript through a file:
dependency since #2219, described as temporary until an SDK release
carried the execution-template contract. SDK v0.5.0 now does, but
returning to npm would let a Studio image pair main's daemon contract
with an older released SDK and would require an SDK release before any
change spanning proto, SDK, and Studio could ship. Keep the checkout
dependency as the design: Studio, its SDK, and the daemon it targets
always come from one commit, which continuous builds from main need.

- Guard the file: dependency with sdk-source.test.ts.
- Stamp About's SDK version with the baked Studio build
  (0.5.0+<build>), since the package version can predate the source.
- Replace the "temporary" wording in the Dockerfile, README, CI, and
  architecture docs; keep Biome's package-name-only import rule.
- Drop the SDK 0.4.0 release-age exclusion and the Renovate rule for
  adopting SDK releases in apps/ (Renovate skips file: dependencies).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@Nashon-Steffen Nashon-Steffen left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-Agent Consensus Review

Agents consulted: architecture/CI, code quality/API, docs/comment hygiene, test coverage, codex (gpt-5.5)

Consensus Summary

# Finding Consensus Severity Action
1 Biome relative-import rule misses deep sdk/ imports the docs now rely on 9/10 MEDIUM Fix
2 Apps lockfile resolves the SDK's deps at older versions than the SDK pins; the guard can't see it 8/10 MEDIUM Fix
3 "Published SDK" rationale left in CI scripts and BFF wording 9/10 LOW Fix
4 User docs say every image carries the +build suffix; only release images do 9/10 LOW Fix
5 "Daemon it targets … one commit" reads as a runtime guarantee 7/10 LOW Fix
6 Empty injected studioBuildId yields 0.5.0+ 7/10 LOW Fix
7 sdkVersion now embeds studioBuildId, which is already reported beside it 7/10 INFO Discuss
8 studio:image task description says context = apps/ 7/10 LOW Fix

Overall

This makes the file: SDK dependency from #2219 permanent: Studio, its SDK, and the daemon contract build from one commit, so a proto→SDK→Studio change doesn't wait on an SDK release. The PR adds a guard test, adds a +<studio build> stamp on About's SDK version, and updates the docs to match. The reasoning holds up, dependencies still point one way (apps → SDK), and the cleanup (the minimumReleaseAgeExclude entry, the no-op Renovate rule) is correct.

None of the findings blocks the approach. Two need fixing. Making the dependency permanent leaves two jobs that the published tarball used to do implicitly. The first is keeping the BFF on the SDK's public exports. The docs now credit a Biome rule with this, but the rule only matches four exact specifiers: ../../../sdk/typescript/src/index.js passes lint (checked against Biome 2.5.14 with this config). The second is resolving the SDK's own dependencies at the versions the SDK pins. apps/pnpm-lock.yaml still resolves @bufbuild/protobuf 2.15.0 and @types/node 24.13.3 while sdk/typescript/package.json pins 2.16.0 and 24.19.1, and --frozen-lockfile doesn't catch it for a file: directory dependency. That drift is already on main, but this PR is what commits to the design, so it's the natural place to close it. The rest is wording.

For the continuous main → dev builds that motivate this PR: the +build stamp only appears when the build passes STUDIO_BUILD_ID. Today only release.yml does. A dev pipeline that doesn't pass it will show the plain package version, which is the ambiguity the stamp exists to remove.

Outside the diff

  • "Published SDK" rationale (9/10, docs + architecture + code quality + codex). .github/scripts/relevant-changes-workflow_test.sh:147 says "Studio consumes the PUBLISHED SDK, so it must NOT be tied to sdk relevance". The assertion is still right, but only because studio_relevant already includes sdk/typescript/* (relevant-changes.sh:102), and the stated reason invites someone to drop that path. Suggested wording: "studio_relevant already covers sdk/typescript/*, so the job needs no separate sdk_relevant term." .github/scripts/macos-relevant-changes.sh:44-45 gives the same stale reason for excluding apps/*. The exclusion is still correct because nothing in a macOS job imports apps/. Lower priority: apps/server/src/mecatl/inspection.ts:11 and the chat.test.ts test names at 103/539/621 say "published SDK" where they mean the public exports. "Public SDK" would be unambiguous.
  • apps/Taskfile.yml:126 (7/10, codex + docs): the image task description says (context = apps/), but docker build … .. builds from the repository root. The Dockerfile comment edited here now states the root context, so the two disagree in task --list.

Documentation

  • user-docs/operating/studio.md:136-140: scope the suffix to published images and rewrap to the site's 80-column prose width (inline suggestion).
  • docs/architecture/api-surface.md:278-282: "the daemon it targets ship from one commit". The same phrasing is in apps/Dockerfile:10-12 and apps/README.md:31-33 (inline suggestion). Line 271 still calls apps/ a "self-contained pnpm workspace", which is no longer quite true with a permanent file:../../sdk dependency.

Generated with Claude Code

Comment thread apps/biome.json Outdated
Comment on lines 26 to 31
"paths": {
"../sdk": "Studio consumes only the published @stacklok-oss/mecatl-sdk.",
"../../sdk": "Studio consumes only the published @stacklok-oss/mecatl-sdk.",
"../../../sdk": "Studio consumes only the published @stacklok-oss/mecatl-sdk.",
"../../../../sdk/typescript": "Studio consumes only the published @stacklok-oss/mecatl-sdk."
"../sdk": "Import the SDK by package name (@stacklok-oss/mecatl-sdk), never by relative path.",
"../../sdk": "Import the SDK by package name (@stacklok-oss/mecatl-sdk), never by relative path.",
"../../../sdk": "Import the SDK by package name (@stacklok-oss/mecatl-sdk), never by relative path.",
"../../../../sdk/typescript": "Import the SDK by package name (@stacklok-oss/mecatl-sdk), never by relative path."
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Biome relative-import rule misses deep sdk/ imports (9/10, MEDIUM; architecture, code quality, docs)

paths matches the whole specifier exactly. With this config on Biome 2.5.14, ../../../sdk and ../../../../sdk/typescript are rejected, but ../../../sdk/typescript/src/index.js and ../../../../sdk/typescript/dist/index.js pass. Those are the imports someone would actually write from server/src/. apps/README.md and api-surface.md now cite this rule as what keeps the BFF on the public exports, and with the SDK in the same checkout it's the only thing doing so.

A patterns group flags all four forms. I checked that it still allows @stacklok-oss/mecatl-sdk, @stacklok-oss/mecatl-sdk/node, and ./sdk-version.js, and no current apps/ import matches it:

Suggested change
"paths": {
"../sdk": "Studio consumes only the published @stacklok-oss/mecatl-sdk.",
"../../sdk": "Studio consumes only the published @stacklok-oss/mecatl-sdk.",
"../../../sdk": "Studio consumes only the published @stacklok-oss/mecatl-sdk.",
"../../../../sdk/typescript": "Studio consumes only the published @stacklok-oss/mecatl-sdk."
"../sdk": "Import the SDK by package name (@stacklok-oss/mecatl-sdk), never by relative path.",
"../../sdk": "Import the SDK by package name (@stacklok-oss/mecatl-sdk), never by relative path.",
"../../../sdk": "Import the SDK by package name (@stacklok-oss/mecatl-sdk), never by relative path.",
"../../../../sdk/typescript": "Import the SDK by package name (@stacklok-oss/mecatl-sdk), never by relative path."
}
"patterns": [
{
"group": ["**/sdk", "**/sdk/**"],
"message": "Import the SDK by package name (@stacklok-oss/mecatl-sdk), never by relative path."
}
]

readFileSync(new URL("../package.json", import.meta.url), "utf8"),
) as { dependencies: Record<string, string> };
expect(manifest.dependencies["@stacklok-oss/mecatl-sdk"]).toBe("file:../../sdk/typescript");
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lockfile resolves the SDK's deps at older versions than the SDK pins (8/10, MEDIUM; architecture, tests)

apps/pnpm-lock.yaml resolves @stacklok-oss/mecatl-sdk@file:../sdk/typescript with @bufbuild/protobuf 2.15.0 and @types/node 24.13.3, but sdk/typescript/package.json pins 2.16.0 and 24.19.1. pnpm install --frozen-lockfile doesn't compare a file: directory dependency's manifest against the lockfile snapshot, so CI stays green and the image runs SDK code tested on protobuf-es 2.16.0 against runtime 2.15.0. Renovate's SDK group bumps sdk/typescript/package.json without touching the apps lockfile, so this will keep drifting. It's already on main, but it undercuts "one commit", and this guard only checks the package.json specifier.

Suggest regenerating apps/pnpm-lock.yaml, then guarding against recurrence. One option: extend this test to compare the lockfile's '@stacklok-oss/mecatl-sdk@file:../sdk/typescript' snapshot dependencies with sdk/typescript/package.json dependencies. Another: a CI step that runs pnpm install --lockfile-only in apps/ and fails on a diff.

Comment thread user-docs/operating/studio.md Outdated
Comment on lines 136 to 140
**About** identifies the Studio image, installed SDK, and connected daemon
builds. Ask users for **Copy support summary** when diagnosing a deployment
builds. Studio compiles the SDK from the same source as its image, so in an image
the SDK version ends with the Studio build after a `+`, such as
`0.5.0+v0.0.48`. Ask users for **Copy support summary** when diagnosing a deployment
issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suffix appears only in release images (9/10, LOW; docs, architecture, code quality)

Only release.yml passes STUDIO_BUILD_ID. CI no-push images, task studio:image, and any dev build that doesn't pass the build arg show the plain package version. Lines 137 and 139 are also past the site's 80-column proseWrap. Suggested scope and rewrap:

Suggested change
**About** identifies the Studio image, installed SDK, and connected daemon
builds. Ask users for **Copy support summary** when diagnosing a deployment
builds. Studio compiles the SDK from the same source as its image, so in an image
the SDK version ends with the Studio build after a `+`, such as
`0.5.0+v0.0.48`. Ask users for **Copy support summary** when diagnosing a deployment
issue.
**About** identifies the Studio image, installed SDK, and connected daemon
builds. In a published image, the SDK version ends with the Studio release it
was built from, after a `+`, such as `0.5.0+v0.0.48`. Images built without a
release tag show only the SDK package version. Ask users for **Copy support
summary** when diagnosing a deployment issue.

Comment thread docs/architecture/api-surface.md Outdated
Comment on lines +278 to +282
and is the only part that talks to Mecatl. It builds the SDK from the same checkout
through a `file:` dependency, never a registry release, so Studio, the SDK, and the
daemon it targets ship from one commit and a change spanning all three needs no
intermediate SDK release. It still imports the SDK only by package name, through the
same exports as any other consumer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"The daemon it targets … ship from one commit" reads as a runtime guarantee (7/10, LOW; docs, architecture)

In external mode a deployed Studio connects to whatever daemon the operator runs. What is guaranteed is that Studio is built against its own commit's daemon contract, which is how sdk-source.test.ts and the commit message put it. The same phrasing is in apps/Dockerfile:10-12 ("always share one commit") and apps/README.md:31-33.

Suggested change
and is the only part that talks to Mecatl. It builds the SDK from the same checkout
through a `file:` dependency, never a registry release, so Studio, the SDK, and the
daemon it targets ship from one commit and a change spanning all three needs no
intermediate SDK release. It still imports the SDK only by package name, through the
same exports as any other consumer.
and is the only part that talks to Mecatl. It builds the SDK from the same checkout
through a `file:` dependency, never a registry release, so Studio, the SDK, and the
daemon contract it targets are built from one commit and a change spanning all three
needs no intermediate SDK release. It still imports the SDK only by package name,
through the same exports as any other consumer.

Comment thread apps/server/src/sdk-version.ts Outdated
Comment on lines +30 to +32
return version === undefined || studioBuildId === undefined
? version
: `${version}+${studioBuildId}`;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty build ID yields 0.5.0+ (7/10, LOW; code quality, tests)

build-info.ts normalizes an empty baked ID to undefined, but createApp({ studioBuildId: "" }) passes "" straight through (?? keeps it), which yields 0.5.0+. Only tests inject it today. Treating empty like absent here is cheap, along with a reportedSdkVersion("0.5.0", "") case:

Suggested change
return version === undefined || studioBuildId === undefined
? version
: `${version}+${studioBuildId}`;
return version === undefined || !studioBuildId
? version
: `${version}+${studioBuildId}`;

* Studio compiles the SDK from its own checkout, so the SDK's package version
* can predate the source in a build. A baked Studio build ID names that source.
*/
export function reportedSdkVersion(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sdkVersion now embeds studioBuildId (7/10, INFO, discuss; code quality, tests, architecture)

In a release image, About and the support summary show Studio build: v0.0.48 next to SDK version: 0.5.0+v0.0.48. The upside is that a copied SDK line identifies its source on its own. The cost is that sdkVersion is no longer purely the package version, so anything that later compares it against npm versions gets a mixed value. If the suffix stays, a one-line .describe() on sdkVersion in apps/contracts/src/schemas/runtime.ts would document the format for API readers. Otherwise, keep sdkVersion as the bare package version and let About explain that the SDK is built from the Studio source.

Making the file: SDK dependency permanent leaves two jobs the published
tarball used to do implicitly.

- Biome's relative-import rule matched four exact specifiers, so a deep
  import such as ../../../sdk/typescript/src/index.js passed lint. Use a
  **/sdk and **/sdk/** pattern group instead.
- A frozen install does not compare a file: dependency's manifest with
  its lockfile snapshot, so apps/pnpm-lock.yaml still installed the SDK
  with @bufbuild/protobuf 2.15.0 and @types/node 24.13.3 against the
  SDK's 2.16.0 and 24.19.1 pins. Refresh the lockfile and have
  sdk-source.test.ts compare the installed versions with the SDK's pins.

Also treat an empty Studio build ID as absent in reportedSdkVersion,
scope the About suffix in user docs to published images, describe the
one-commit guarantee as the daemon contract rather than the deployed
daemon, retire the remaining "published SDK" wording in the CI
classifiers and BFF, and correct the studio:image build context.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Nashon-Steffen
Nashon-Steffen merged commit 73795a6 into main Oct 13, 2026
38 checks passed
@Nashon-Steffen
Nashon-Steffen deleted the Nashon-Steffen/mecatl-studio-ci/cd branch October 13, 2026 19:56
@toolhive-release-app toolhive-release-app Bot mentioned this pull request Oct 20, 2026
2 tasks

This branch was successfully deployed

1 active deployment
Preview — f86f0fe5 Deployed Oct 13, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants