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
10 changes: 6 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ name: CI
on:
push:
branches: ["**"]
tags-ignore: ["v*"]
# Release tags are validated by their dedicated publish workflows; CI
# stays a branch/pull-request gate.
tags-ignore: ["v*", "sdk-v*", "dotnet-v*", "shared/v*", "packages/executor-sdks/golang/v*"]
pull_request:

concurrency:
Expand Down Expand Up @@ -96,13 +98,13 @@ jobs:
run: pnpm install --frozen-lockfile

- name: Build SDK
run: pnpm --filter @neuron/sdk build
run: pnpm --filter @neuronruntime/sdk build

- name: Test SDK
run: pnpm --filter @neuron/sdk test
run: pnpm --filter @neuronruntime/sdk test

- name: Typecheck SDK
run: pnpm --filter @neuron/sdk typecheck
run: pnpm --filter @neuronruntime/sdk typecheck

release-validation:
name: Release build matrix
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/executor-dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ name: Executor .NET
on:
push:
branches: ["**"]
tags-ignore: ["v*"]
# Release tags are validated by the publish workflow; this stays a
# branch/pull-request gate.
tags-ignore: ["v*", "dotnet-v*"]
pull_request:

concurrency:
Expand Down
38 changes: 15 additions & 23 deletions .github/workflows/publish-executor-dotnet.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
# Publish the Neuron.Executor .NET SDK to NuGet using Trusted Publishing (OIDC).
# Publish the Neuron.Executor .NET SDK to NuGet.
#
# No long-lived API key is stored in GitHub. The NuGet/login action exchanges a
# GitHub OIDC token for a short-lived NuGet API key, which is only valid for the
# duration of the job.
# Authentication uses a NuGet API key stored as a GitHub Actions secret in the
# `release` environment. The key is scoped to the Neuron.Executor package with
# push-only rights.
#
# One-time setup (required):
# 1. Create a Trusted Publishing policy on nuget.org:
# Account -> Trusted Publishing -> Add policy
# Publisher: GitHub Actions
# Repository owner: neuron-runtime
# Repository: neuron
# Workflow file: publish-executor-dotnet.yml (must match exactly)
# Environment: (leave empty unless a protected environment is used)
# 2. Add a repository *variable* (not a secret) named NUGET_USER holding your
# nuget.org username. It is an identifier, not a credential.
# GitHub -> Settings -> Secrets and variables -> Actions -> Variables
# 1. Create an API key on nuget.org: https://www.nuget.org → API keys →
# Generate Key. Scope it to the Neuron.Executor package with Push rights,
# and set an expiration you are willing to rotate.
# 2. In the GitHub repository: Settings → Environments → New environment
# named `release` (shared with the npm publish workflow).
# 3. Under the `release` environment's secrets, add `NUGET_API_KEY`
# holding the key from step 1.
#
# Release: push a tag named dotnet-v<semver>, e.g.
# `git tag dotnet-v0.1.0 && git push origin dotnet-v0.1.0`.
Expand All @@ -28,13 +25,13 @@ on:

permissions:
contents: read
id-token: write

jobs:
publish:
name: Publish Neuron.Executor
runs-on: ubuntu-latest
timeout-minutes: 25
environment: release
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -74,16 +71,11 @@ jobs:
-c Release --no-build -p:Version=${{ steps.version.outputs.value }}
-o "${{ runner.temp }}/pack"

# Exchange the GitHub OIDC token for a short-lived NuGet API key.
- name: NuGet login (OIDC)
uses: NuGet/login@v1
id: login
with:
user: ${{ vars.NUGET_USER }}

- name: Push to NuGet
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
run: >-
dotnet nuget push "${{ runner.temp }}/pack/*.nupkg"
--api-key "${{ steps.login.outputs.NUGET_API_KEY }}"
--api-key "$NUGET_API_KEY"
--source https://api.nuget.org/v3/index.json
--skip-duplicate
33 changes: 32 additions & 1 deletion .github/workflows/publish-go-sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@
# silently declare a module version. The proxy will serve the tag once CI is
# green.
#
# The executor SDK depends on the shared module, and consumers ignore a
# module's `replace` directives. Until the shared module is published, its
# go.mod carries a local `replace` for development; the workflow therefore
# refuses to publish the executor SDK while that replace (or the matching
# placeholder version) is present. The required sequence is:
#
# 1. git tag shared/v0.1.0 && git push origin shared/v0.1.0
# 2. merge the commit that removes the replace from
# packages/executor-sdks/golang/go.mod and pins the published
# github.com/neuron-runtime/neuron/shared version
# 3. git tag packages/executor-sdks/golang/v0.1.0 && git push origin
# packages/executor-sdks/golang/v0.1.0
#
# Release:
# git tag shared/v0.2.0 && git push origin shared/v0.2.0
# git tag packages/executor-sdks/golang/v0.2.0 && git push origin packages/executor-sdks/golang/v0.2.0
Expand Down Expand Up @@ -69,6 +82,24 @@ jobs:
echo "path=$path" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"

# Consumers ignore a module's `replace` directives, so the executor SDK
# is only publishable once its local shared replace is removed in favor
# of the published version. Tagging early would ship a module that
# cannot build for anyone outside this workspace — fail loudly instead.
- name: Publishable module check (executor SDK)
if: steps.module.outputs.path == 'packages/executor-sdks/golang'
shell: bash
run: |
go_mod="packages/executor-sdks/golang/go.mod"
if grep -qE '^[[:space:]]*replace[[:space:]]' "$go_mod" || grep -q 'v0.0.0-00010101000000-000000000000' "$go_mod"; then
echo "$go_mod still contains a replace directive or placeholder version." >&2
echo "Consumers ignore replace directives. Publish shared first:" >&2
echo " 1. tag and push shared/vX" >&2
echo " 2. merge the commit removing the replace and pinning the published shared version" >&2
echo " 3. tag and push packages/executor-sdks/golang/vX" >&2
exit 1
fi

- name: Formatting
run: |
files="$(gofmt -l "${{ steps.module.outputs.path }}")"
Expand All @@ -88,4 +119,4 @@ jobs:
- name: Summary
run: |
echo "### Published ${{ steps.module.outputs.path }} v${{ steps.module.outputs.version }}" >> "$GITHUB_STEP_SUMMARY"
echo "The Go module proxy will serve this tag: \`https://proxy.golang.org/github.com/!muhammad-!jay/neuron/${{ steps.module.outputs.path }}/@v/v${{ steps.module.outputs.version }}.info\`" >> "$GITHUB_STEP_SUMMARY"
echo "The Go module proxy will serve this tag: \`https://proxy.golang.org/github.com/neuron-runtime/neuron/${{ steps.module.outputs.path }}/@v/v${{ steps.module.outputs.version }}.info\`" >> "$GITHUB_STEP_SUMMARY"
35 changes: 18 additions & 17 deletions .github/workflows/publish-sdk.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
# Publish @neuron/sdk to npm using Trusted Publishing (OIDC).
# Publish @neuronruntime/sdk to npm.
#
# No tokens or secrets are stored in GitHub. npm verifies a short-lived OIDC
# token issued by GitHub Actions against the Trusted Publisher configured for
# the package on npmjs.com.
# Authentication uses an npm automation/granular access token stored as a
# GitHub Actions secret in the `release` environment. The workflow also
# publishes with --provenance (id-token: write), so the npm registry records a
# signed GitHub-backed attestation for every release.
#
# One-time setup (required, on npmjs.com):
# 1. Sign in to https://www.npmjs.com and open the @neuron/sdk package.
# 2. Settings -> Trusted Publisher -> GitHub Actions.
# 3. Publisher: GitHub
# Organization: neuron-runtime
# Repository: neuron
# Workflow name: publish-sdk.yml (must match this file exactly)
# Environment: (leave empty unless a protected environment is used)
# 4. Save.
# One-time setup (required):
# 1. Create an npm automation or granular access token with publish rights
# for the @neuronruntime scope: https://www.npmjs.com/settings → Access Tokens.
# 2. In the GitHub repository: Settings → Environments → New environment
# named `release`.
# 3. Under the `release` environment's secrets, add `NPM_TOKEN`
# holding the token from step 1.
#
# Release: push a tag named sdk-v<semver>, e.g. `git tag sdk-v0.1.0 && git push
# origin sdk-v0.1.0`. The tag version overrides package.json before publishing.
Expand All @@ -30,9 +29,10 @@ permissions:

jobs:
publish:
name: Publish @neuron/sdk
name: Publish @neuronruntime/sdk
runs-on: ubuntu-latest
timeout-minutes: 20
environment: release
steps:
- uses: actions/checkout@v4

Expand All @@ -46,15 +46,15 @@ jobs:
registry-url: "https://registry.npmjs.org"
cache: pnpm

# Trusted publishing requires npm >= 11.5.1.
# --provenance is supported from npm >= 9.5; use the latest.
- name: Upgrade npm
run: npm install -g npm@latest

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build SDK
run: pnpm --filter @neuron/sdk build
run: pnpm --filter @neuronruntime/sdk build

- name: Set version from tag
working-directory: packages/assembly-sdks/typescript
Expand All @@ -67,7 +67,8 @@ jobs:
fi
npm version "$version" --no-git-tag-version --allow-same-version

# OIDC is exchanged automatically; no NODE_AUTH_TOKEN is needed.
- name: Publish
working-directory: packages/assembly-sdks/typescript
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish --access public --provenance --ignore-scripts
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
- name: Publish GitHub release
env:
VERSION: ${{ github.ref_name }}
# gh reads GH_TOKEN; without it `gh release create` fails the
# authentication check even though contents: write is granted.
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
dist_dir="$RUNNER_TEMP/dist/$VERSION"
Expand Down
31 changes: 25 additions & 6 deletions application/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Flags:
-l, --lang string project authoring language (ts, typescript, yaml, yml) (default "ts")
```

`Target` is the directory to create (relative to the current directory). `neuron init` creates the directory and scaffolds a runnable project: a `neuron.config.json`, and — for the default TypeScript authoring surface — a `package.json` declaring `@neuron/sdk`, a `tsconfig.json`, a starter `assembly.ts`, and the canonical local capability runtime root `neuron/capabilityRuntimes/`.
`Target` is the directory to create (relative to the current directory). `neuron init` creates the directory and scaffolds a runnable project: a `neuron.config.json`, and — for the default TypeScript authoring surface — a `package.json` declaring `@neuronruntime/sdk`, a `tsconfig.json`, a starter `assembly.ts`, and the canonical local capability runtime root `neuron/capabilityRuntimes/`.

```bash
# Create ./my-assembly as a TypeScript project (default)
Expand Down Expand Up @@ -172,11 +172,12 @@ Usage:
neuron run [instance-id|assembly-key] [flags]

Flags:
--format string Output format for the Assembly result (json, text) (default "json")
--params string JSON params payload for execution (e.g., '{"key":"value"}')
--params-file string Path to JSON file with params payload
--port string Override local runtime port (default: auto-discover from project)
--remote string Remote Neuron runtime endpoint (e.g., http://host:port)
-c, --capability-runtime string run a Capability Runtime directly: a built-in core runtime (neuron:core:set) or an installed one (name[@version])
--format string Output format for the result (json, text) (default "json")
--params string JSON params payload for execution (e.g., '{"key":"value"}')
--params-file string Path to JSON file with params payload
--port string Override local runtime port (default: auto-discover from project)
--remote string Remote Neuron runtime endpoint (e.g., http://host:port)
```

`neuron run` is a client of the project runtime. It discovers the project's runtime endpoint (from `.neuron/` state or the project config), sends one synchronous execution request, and prints the Assembly result to stdout. It does not stream events or return execution handles — advanced execution inspection is exposed through the API rather than the normal `run` output.
Expand Down Expand Up @@ -211,6 +212,24 @@ neuron run --format text

stdout contains only the Assembly result in the selected `--format`; logs, diagnostics, and execution errors go to stderr, and a failed or cancelled execution exits non-zero. The result comes from one synchronous request — the command does not stream events, expose execution handles, or list execution history.

#### Direct Capability Runtime invocation

With `-c` / `--capability-runtime`, `neuron run` executes a single Capability Runtime instead of an Assembly. Direct invocation needs no project, no build, and no running project runtime:

```bash
# A built-in core runtime, executed in-process by a one-shot local N.O.R.E. child
neuron run -c neuron:core:set --params '{"message":"hi"}'

# Legacy bare core names canonicalize to the neuron:core namespace
neuron run -c set --params '{"message":"hi"}'

# An installed capability runtime — exact pin or constraint
neuron run -c example:echo --params '{"message":"hi"}'
neuron run -c example:echo@1.0.0 --params '{"message":"hi"}'
```

Core runtimes (`set`, `ai`, `command`, `delay`, `http`, `log` under `neuron:core:`) are compiled into N.O.R.E. and are never installed, so they take no version — `neuron:core:set@1.0.0` is rejected — and resolve without touching the store. Installed artifacts resolve from the machine-global store only and are never fetched or installed implicitly. `--capability-runtime` cannot be combined with a target argument, `--remote`, or `--port`; the result prints to stdout in `--format` exactly as Assembly mode does.

Starting, observing, streaming, and cancelling executions remain available as advanced API operations: starting an execution (optionally detached), streaming its events, and cancelling a specific execution.

### `neuron add`
Expand Down
4 changes: 2 additions & 2 deletions application/build/typescript/typescript.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import (
"github.com/neuron-runtime/neuron/application/process"
)

// SDKBin is the neuron-sdk executable name exposed by @neuron/sdk.
// SDKBin is the neuron-sdk executable name exposed by @neuronruntime/sdk.
const SDKBin = "neuron-sdk"

// ErrSDKNotFound reports that the neuron-sdk binary could not be located.
var ErrSDKNotFound = errors.New("neuron-sdk not found: install the @neuron/sdk workspace package (pnpm install) and run `pnpm build:sdk` first")
var ErrSDKNotFound = errors.New("neuron-sdk not found: install the @neuronruntime/sdk workspace package (pnpm install) and run `pnpm build:sdk` first")

// Builder delegates the TypeScript build to the neuron-sdk CLI.
type Builder struct {
Expand Down
37 changes: 29 additions & 8 deletions application/capabilityruntime/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,23 @@ func (i *Installer) Install(ctx context.Context, pkg *Package) (*InstallResult,
return nil, fmt.Errorf("package is nil")
}

if existing, err := i.Store.Get(ctx, pkg.Type, pkg.Version); err == nil && existing != nil {
if existing.Digest == pkg.Digest {
notify(i.Observer, func(o Observer) {
o.Installed(InstallResult{Installed: existing, AlreadyPresent: true})
})
return &InstallResult{Installed: existing, AlreadyPresent: true}, nil
// A locally sourced package may not carry a digest until its artifact is
// materialized. The conflict check therefore has to wait for the staged
// digest in that case; comparing an empty digest against the installed one
// would reject an idempotent reinstall of identical content.
var existing *Installed
if found, err := i.Store.Get(ctx, pkg.Type, pkg.Version); err == nil && found != nil {
if pkg.Digest != "" {
if found.Digest == pkg.Digest {
notify(i.Observer, func(o Observer) {
o.Installed(InstallResult{Installed: found, AlreadyPresent: true})
})
return &InstallResult{Installed: found, AlreadyPresent: true}, nil
}
return nil, fmt.Errorf("%w: %s@%s already installed with digest %s, refusing to replace with %s",
ErrIntegrityConflict, pkg.Type, pkg.Version, found.Digest, pkg.Digest)
}
return nil, fmt.Errorf("%w: %s@%s already installed with digest %s, refusing to replace with %s",
ErrIntegrityConflict, pkg.Type, pkg.Version, existing.Digest, pkg.Digest)
existing = found
}

notify(i.Observer, func(o Observer) { o.Installing(*pkg) })
Expand All @@ -80,6 +88,19 @@ func (i *Installer) Install(ctx context.Context, pkg *Package) (*InstallResult,
}
}

// The staged digest is now known, so a previously deferred store lookup can
// decide whether this is the same immutable artifact or a conflicting one.
if existing != nil {
if existing.Digest == pkg.Digest {
notify(i.Observer, func(o Observer) {
o.Installed(InstallResult{Installed: existing, AlreadyPresent: true})
})
return &InstallResult{Installed: existing, AlreadyPresent: true}, nil
}
return nil, fmt.Errorf("%w: %s@%s already installed with digest %s, refusing to replace with %s",
ErrIntegrityConflict, pkg.Type, pkg.Version, existing.Digest, pkg.Digest)
}

// 2. runtime.json must be present and authoritative.
manifestDir := stage
manifestPath := filepath.Join(stage, capabilityrt.ManifestFile)
Expand Down
Loading
Loading