Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/oauth-registration-infer-native.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@executor-js/mcp-auth": patch
---

Dynamic client registrations that omit `application_type` are native when a callback is an RFC 8252 loopback or private-use one, and every callback is held to the native rules. A client that registers a web callback beside a loopback or private-use one is no longer refused.
25 changes: 25 additions & 0 deletions .github/workflows/fork-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,28 @@ jobs:
bun run "e2e:$suite"
echo "::endgroup::"
done

# Self-host scenarios for scoped-connection widening, OAuth registration, app
# User-Agent, and load-failure containment. Motel's collector build requires
# Bun 1.4.4, so this job does not share check's 1.3.11 pin. No extra secrets:
# the runner mints its own API key. CI disables the e2e prepare cache.
security-e2e:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: bun install --frozen-lockfile
- name: Install Playwright Chromium
run: bunx playwright install --with-deps chromium
- run: bun run e2e:prepare
- name: Security scenarios
run: |
bun run e2e:self-host -- --test-name 'application_type|fetch without a User-Agent names Executor|cannot create an all-apps connection|cannot widen a connection or mint|fails to load fails only its own reads'
4 changes: 1 addition & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,9 +304,7 @@ A pull request that changes the release paths (the `paths` in
`.github/workflows/release-artifacts.yml`) also runs "Executor releases". Gate its
merge on that workflow as on CI: wait for both
(`ci-wait --workflow CI --workflow "Executor releases"`) and do not merge while
either fails. Its packaging jobs fail when a native archive exceeds the npm budget
(`npmArchiveBudgetBytes` in `scripts/releases/config.ts`); an archive over it cannot be
published, so that failure blocks every later release until it is fixed.
either fails.

A failed e2e job uploads raw reports and server logs. Product database files,
runtime dependencies and private `actors.json` sessions are excluded.
Expand Down
23 changes: 15 additions & 8 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,19 +108,26 @@ The workflow refuses a channel that does not match the committed version.
1. Build and test every native runtime, desktop installer and Docker architecture.
2. Export the filtered source snapshot to the public repository's `v2-releases`
branch. Create `executor@<version>` on that public commit, never a private SHA.
3. Publish native npm variants, then the launcher with exact optional aliases.
Poll each public registry archive and compare its integrity before continuing.
4. Verify a clean `executor@<version>` installation from npm.
5. Upload installers, native packages, bundled Git source and SHA256SUMS.
6. Publish the tested Docker manifest as `:<version>` and `:beta`, then make the
3. Upload installers, native archives, the npm launcher, bundled Git source and
SHA256SUMS to the draft.
4. Publish the tested Docker manifest as `:<version>` and `:beta`, then make the
GitHub prerelease public with `latest=false`.
7. Point the desktop `beta` update channel at the public release assets.
5. Publish the npm launcher, which downloads the native archives from that public
release. It records each archive's SHA-256; publication checks them against
the build and the release's SHA256SUMS, then polls the registry archive and
compares its integrity.
6. Verify a clean `executor@<version>` installation from npm, including its first
download.
7. Point the Homebrew formula and the desktop `beta` update channel at the public
release assets.

The npm `latest` tag, Docker `latest`, and Executor 1 desktop updater stay
unchanged. A draft public release blocks accidental repeat publication of the
same version. After a partial failure, re-run the failed publish job. It resumes
only its own draft, whose target is this source's unchanged public snapshot, and
skips npm versions whose registry integrity matches the local archive. Any other
only its own release, whose target is this source's unchanged public snapshot:
a draft gets its assets again, and a release already public keeps them, which
npm publication then checks. It skips an npm launcher whose registry integrity
matches the local archive. Any other
existing release or npm version stops publication for inspection. Never
republish an accepted immutable npm version merely because its registry entry is
still propagating.
Expand Down
4 changes: 4 additions & 0 deletions apps/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# executor

## 2.0.0-beta.36

Nightly beta of the Executor local runtime, CLI, desktop, and self-host Docker image.

## 2.0.0-beta.35

Nightly beta of the Executor local runtime, CLI, desktop, and self-host Docker image.
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "executor",
"version": "2.0.0-beta.35",
"version": "2.0.0-beta.36",
"private": true,
"description": "Run Executor locally and connect your agents to your apps.",
"homepage": "https://executor.sh",
Expand Down
9 changes: 5 additions & 4 deletions apps/docs/content/run/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ browser and exits. Executor keeps running until you stop it; see
with its runtime, dashboard, Git and workerd beside it; no source checkout, Node
or Bun is needed to run it.

The npm package is a small launcher: npm installs the executable for your
platform as an optional dependency. If `executor` reports that its native
runtime is missing, npm skipped optional dependencies; install again with
`--include=optional`.
The npm package is a small launcher. The first time a version starts, it
downloads that release's archive for your platform from the public GitHub
release, checks it against the SHA-256 the package records, and keeps it in
`~/.executor/v2/launcher/<version>`. Set `EXECUTOR_DOWNLOAD_URL` to use a mirror
of the releases.

The install script keeps each release in `~/.executor/v2/runtime/versions/`,
points `~/.executor/v2/runtime/current` at the one in use and links
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/release-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export const ReleaseCommandProduct = Schema.Literals([
/** Concrete instructions generated from the same identity as the published artifacts. */
export const releaseCommands = {
cli: {
description: `With npm, on Node ${release.minimumNodeVersion} or newer. Executor carries its own runtime, so Node only starts it. Check that executor --version prints ${release.version} before starting. Supports macOS on Apple Silicon and Intel, Linux (glibc) on ARM64 and x64, and Windows on x64.`,
command: `${release.npmInstall} --include=optional\nexecutor --version\nexecutor`,
description: `With npm, on Node ${release.minimumNodeVersion} or newer. The package is a launcher that downloads Executor's own runtime for your platform on first run, so Node only starts it. Check that executor --version prints ${release.version} before starting. Supports macOS on Apple Silicon and Intel, Linux (glibc) on ARM64 and x64, and Windows on x64.`,
command: `${release.npmInstall}\nexecutor --version\nexecutor`,
},
"cli-script": {
description:
Expand Down
4 changes: 3 additions & 1 deletion apps/hosted/server/src/contracts/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
StartWorkflowFields,
ListWorkflowRuns,
WorkflowErrors,
WorkflowDefinitionErrors,
} from "@executor-js/sdk/core";
import {
OrganizationReference,
Expand All @@ -21,6 +22,7 @@ import {
const app = { organization: OrganizationReference, app: AppId };
const run = { ...app, run: WorkflowRunId };
const errors = [...WorkflowErrors, OrganizationForbidden] as const;
const definitionErrors = [...WorkflowDefinitionErrors, OrganizationForbidden] as const;
const path = "/api/organizations/:organization/apps/:app";
/**
* Members who can use the app read definitions and start runs with their accounts. Runs belong to
Expand All @@ -34,7 +36,7 @@ export const HostedWorkflows = HttpApiGroup.make("workflows")
withLegacyKeys({ version: "deployment" }),
),
success: Schema.Array(HostedWorkflow),
error: errors,
error: definitionErrors,
}),
)
.add(
Expand Down
3 changes: 2 additions & 1 deletion apps/local/server/src/contracts/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
StartWorkflowFields,
ListWorkflowRuns,
WorkflowErrors,
WorkflowDefinitionErrors,
WebhookSubscription,
WebhookErrors,
WebhookId,
Expand All @@ -34,7 +35,7 @@ export const DashboardWorkflows = HttpApiGroup.make("workflows")
withLegacyKeys({ version: "deployment" }),
),
success: Schema.Array(HostedWorkflow),
error: WorkflowErrors,
error: WorkflowDefinitionErrors,
}),
)
.add(
Expand Down
18 changes: 11 additions & 7 deletions apps/local/server/src/implementation/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ export interface UpgradeOptions {
}

/**
* Each package manager's global install. npm can skip the optional dependency that carries the
* native runtime, which leaves a launcher that cannot start, so it must be asked to include it.
* Each package manager's global install. It installs only the launcher, which downloads the
* release's runtime the first time it starts.
*/
const installArgs = (method: GlobalInstaller, version: string): ReadonlyArray<string> => {
const spec = `executor@${version}`;
switch (method) {
case "npm":
return ["install", "-g", spec, "--include=optional"];
return ["install", "-g", spec];
case "pnpm":
return ["add", "-g", spec];
case "bun":
Expand Down Expand Up @@ -402,7 +402,7 @@ const rollback = (options: UpgradeOptions, host: UpgradeHost) =>
if (Option.isNone(install))
return yield* failed(
"not-standalone",
"Only an install from the Executor install script keeps a previous version to roll back to. Install the version you want with the package manager you used, such as: npm install -g executor@<version> --include=optional. Nothing was changed.",
"Only an install from the Executor install script keeps a previous version to roll back to. Install the version you want with the package manager you used, such as: npm install -g executor@<version>. Nothing was changed.",
);
const previous = yield* linkedVersion(install.value.root, "previous");
if (Option.isNone(previous))
Expand Down Expand Up @@ -472,7 +472,7 @@ export const upgrade = (options: UpgradeOptions, host: UpgradeHost) =>
if (Option.isNone(current))
return yield* failed(
"development-build",
"This is a development build of Executor without a release version, so it cannot upgrade itself. Install a release with: npm install -g executor@beta --include=optional",
"This is a development build of Executor without a release version, so it cannot upgrade itself. Install a release with: npm install -g executor@beta",
);

const registry = yield* npmRegistry;
Expand Down Expand Up @@ -530,7 +530,7 @@ export const upgrade = (options: UpgradeOptions, host: UpgradeHost) =>
if (Option.isNone(install))
return yield* failed(
"not-standalone",
`${host.executable} is not in an install script's versions directory, so executor upgrade cannot replace it. Reinstall with the Executor install script, or with npm: npm install -g executor@${target} --include=optional. Nothing was changed.`,
`${host.executable} is not in an install script's versions directory, so executor upgrade cannot replace it. Reinstall with the Executor install script, or with npm: npm install -g executor@${target}. Nothing was changed.`,
);
if (compareReleaseVersions(target, current.value) < 0)
yield* confirm(
Expand Down Expand Up @@ -642,10 +642,14 @@ export const upgrade = (options: UpgradeOptions, host: UpgradeHost) =>
"verify-failed",
`${method} installed Executor v${target}, but ${detail} Run \`${host.platform === "win32" ? "where.exe executor" : "type -a executor"}\` to see which executable your shell starts, or reinstall with: ${command}`,
);
// The new launcher's first start downloads and checks its release's runtime.
yield* Console.log(
`Starting Executor v${target}. Its first start downloads the runtime for this platform.`,
);
const check = yield* runInstalled(
onPath(host.platform),
["--version"],
Duration.seconds(60),
Duration.minutes(15),
).pipe(Effect.result);
if (Result.isFailure(check))
return yield* verify("`executor --version` could not be started.");
Expand Down
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions e2e/test-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,17 @@ export const scenarios = plan({
cloud: na("Hosted Tools pages are covered through organization routes."),
},
},
appLoadFailureContainment: {
fixtures: "actors",
file: "app-load-failure-containment.spec.ts",
title:
"An app whose code fails to load fails only its own reads and names its error in every preview",
targets: {
"self-host": scheduled,
cloud: scheduled,
local: na("The hosted app overview and dashboard batches own this presentation."),
},
},
toolsErrorState: {
fixtures: "actors",
file: "tools-error-state.spec.ts",
Expand Down Expand Up @@ -2755,6 +2766,16 @@ export const scenarios = plan({
local: na("Exercises the hosted import and account connection APIs."),
},
},
appFetchUserAgent: {
fixtures: "actors",
file: "app-fetch-user-agent.spec.ts",
title: "An app's fetch without a User-Agent names Executor and keeps an app's own",
targets: {
"self-host": scheduled,
cloud: na("Uses a loopback upstream to exercise the shared outbound network."),
local: na("Exercises the hosted app deploy, account check and tool call APIs."),
},
},
mcpUserAgent: {
fixtures: "actors",
file: "mcp-user-agent.spec.ts",
Expand Down Expand Up @@ -4321,6 +4342,12 @@ export const scenarios = plan({
title: "OAuth resources are provisioned before client registration",
targets: { local: scheduled, "self-host": scheduled, cloud: scheduled },
},
oauthRegistrationApplicationType: {
file: "oauth-registration-application-type.spec.ts",
title:
"a registration without application_type is native for a loopback or private-use callback, held to the native rules, and web otherwise",
targets: { local: scheduled, "self-host": scheduled, cloud: scheduled },
},
apiDocumentPatterns: {
file: "api-document-patterns.spec.ts",
title: "the published API document keeps the string patterns requests must match",
Expand Down
Loading
Loading