Skip to content
This repository was archived by the owner on Sep 7, 2026. It is now read-only.
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
17 changes: 13 additions & 4 deletions .github/workflows/README-RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,23 @@ Separate from cutting a release, the pinned `software-agent-sdk` / agent-server
image (`config.agentServerImage` in `package.json`, plus the
`integration-tests.yml`, `AGENTS.md`, and `README.md` mirrors) is kept current
by the **SDK's own release automation**: when `software-agent-sdk` publishes a
new release, it opens a `bump-agent-server-X.Y.Z` PR here (once the matching
agent-server image is published to GHCR). That PR's CI + integration tests
validate the client against the new server. This is independent of the npm
package version and publishes nothing.
new release, it waits for the matching GHCR image and exact release
`openapi.json`, then opens a `bump-agent-server-X.Y.Z` PR containing the pin and
generated contract changes plus an API-change summary. That PR's required
pinned-regeneration, typecheck, and integration CI validate the client against
the new server. This is independent of the npm package version and publishes
nothing.

The weekly `agent-server-sdk-main-audit.yml` workflow separately exports one
exact SDK `main` commit and opens or updates an informational issue for upcoming
drift. It records the full commit SHA and never makes ordinary client PRs depend
on the moving branch.

## Workflow Files

- `.github/workflows/pr.yml` — conventional PR-title lint + label (reusable)
- `.github/workflows/release.yml` — release-please release line (reusable)
- `.github/workflows/npm-publish.yml` — npmjs.org publish on `release: published`
- `.github/workflows/publish-github-packages.yml` — GitHub Packages publish on `release: published` (and manual recovery)
- `.github/workflows/agent-server-sdk-main-audit.yml` — informational exact-SHA
preview of unreleased SDK contract drift
200 changes: 200 additions & 0 deletions .github/workflows/agent-server-sdk-main-audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
name: Agent Server SDK main drift audit

on:
schedule:
- cron: '17 7 * * 1'
workflow_dispatch:

permissions:
contents: read
issues: write

concurrency:
group: agent-server-sdk-main-audit
cancel-in-progress: true

jobs:
audit:
runs-on: ubuntu-24.04
timeout-minutes: 45

steps:
- name: Checkout TypeScript client
uses: actions/checkout@v4

- name: Resolve exact SDK main commit
id: sdk
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
SHA=$(gh api repos/OpenHands/software-agent-sdk/commits/main --jq .sha)
if ! [[ "$SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo "Unexpected SDK commit SHA: $SHA"
exit 1
fi
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
echo "short=${SHA::12}" >> "$GITHUB_OUTPUT"
echo "Auditing OpenHands/software-agent-sdk@$SHA"

- name: Checkout exact SDK commit
uses: actions/checkout@v4
with:
repository: OpenHands/software-agent-sdk
ref: ${{ steps.sdk.outputs.sha }}
path: sdk
fetch-depth: 1

- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with:
version: latest
python-version: '3.13'
enable-cache: false

- name: Export exact SDK main OpenAPI
env:
OPENHANDS_SUPPRESS_BANNER: '1'
run: |
set -euo pipefail
uv sync --directory sdk --frozen --dev
(
cd sdk
uv run python .github/scripts/export_agent_server_openapi.py \
--output "$RUNNER_TEMP/sdk-main-openapi.json"
)

- name: Use Node.js 22.12
uses: actions/setup-node@v6
with:
node-version: 22.12
cache: 'npm'

- name: Install client dependencies
run: npm ci

- name: Generate candidate and summarize drift
id: drift
env:
AGENT_SERVER_OPENAPI_PATH: ${{ runner.temp }}/sdk-main-openapi.json
AGENT_SERVER_SCHEMA_SOURCE: OpenHands/software-agent-sdk@${{ steps.sdk.outputs.sha }}
run: |
set -euo pipefail
cp src/generated/agent-server-schema.ts \
"$RUNNER_TEMP/pinned-agent-server-schema.ts"
npm run generate:agent-server-api
npm run summarize:agent-server-api -- \
--before "$RUNNER_TEMP/pinned-agent-server-schema.ts" \
--after src/generated/agent-server-schema.ts \
--output "$RUNNER_TEMP/sdk-main-api-summary.md"
if cmp -s \
"$RUNNER_TEMP/pinned-agent-server-schema.ts" \
src/generated/agent-server-schema.ts; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi

- name: Render exact-SHA drift report
env:
SDK_SHA: ${{ steps.sdk.outputs.sha }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
node scripts/render-agent-server-drift-report.mjs \
--sdk-sha "$SDK_SHA" \
--summary "$RUNNER_TEMP/sdk-main-api-summary.md" \
--package package.json \
--run-url "$RUN_URL" \
--output "$RUNNER_TEMP/sdk-main-drift-report.md"

- name: Upload exact audit inputs and candidate
if: steps.drift.outputs.changed == 'true'
uses: actions/upload-artifact@v4
with:
name: agent-server-sdk-main-${{ steps.sdk.outputs.short }}
path: |
${{ runner.temp }}/sdk-main-openapi.json
${{ runner.temp }}/sdk-main-api-summary.md
${{ runner.temp }}/sdk-main-drift-report.md
src/generated/agent-server-schema.ts
retention-days: 14
if-no-files-found: error

- name: Open, update, or resolve drift issue
uses: actions/github-script@v8
env:
SDK_SHA: ${{ steps.sdk.outputs.sha }}
DRIFT_CHANGED: ${{ steps.drift.outputs.changed }}
DRIFT_REPORT_PATH: ${{ runner.temp }}/sdk-main-drift-report.md
with:
script: |
const fs = require('fs');
const marker = '<!-- agent-server-sdk-main-drift -->';
const title = '[audit] Agent Server SDK main contract drift';
const { owner, repo } = context.repo;
const sha = process.env.SDK_SHA;
const changed = process.env.DRIFT_CHANGED === 'true';
const testedCommit =
`https://github.com/OpenHands/software-agent-sdk/commit/${sha}`;
const body = fs.readFileSync(process.env.DRIFT_REPORT_PATH, 'utf8');

const issues = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: 'open',
per_page: 100,
});
const existing = issues.find(
(issue) => !issue.pull_request && issue.body?.includes(marker)
);

if (changed) {
if (existing) {
await github.rest.issues.update({
owner,
repo,
issue_number: existing.number,
title,
body,
});
core.info(`Updated drift issue #${existing.number} for SDK ${sha}`);
} else {
const created = await github.rest.issues.create({
owner,
repo,
title,
body,
});
core.info(`Opened drift issue #${created.data.number} for SDK ${sha}`);
}
} else if (existing) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: existing.number,
body:
`Resolved by exact audit of [\`${sha}\`](${testedCommit}); ` +
`the generated SDK-main contract matches the checked-in pin.`,
});
await github.rest.issues.update({
owner,
repo,
issue_number: existing.number,
state: 'closed',
});
core.info(`Closed resolved drift issue #${existing.number}`);
} else {
core.info(`No drift at exact SDK commit ${sha}`);
}

- name: Record exact audited commit
env:
SDK_SHA: ${{ steps.sdk.outputs.sha }}
DRIFT_CHANGED: ${{ steps.drift.outputs.changed }}
run: |
{
echo "## Agent Server SDK main audit"
echo ""
echo "- Exact SDK commit: \`$SDK_SHA\`"
echo "- Generated drift: \`$DRIFT_CHANGED\`"
} >> "$GITHUB_STEP_SUMMARY"
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Test Agent Server contract tooling
run: npm run test:agent-server-api-tooling

- name: Check generated Agent Server contract
run: npm run check:agent-server-api

Expand Down
8 changes: 5 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ src/workspace/
- `createWorkspace({ type, options })` - Explicit type selection
- `createWorkspaceAuto(options)` - Auto-detect based on presence of `host` option


**Ergonomic API note**: Keep `Workspace`/`RemoteWorkspace` as the main user-facing entry point for remote execution. Lower-level bash endpoint access should hang off `workspace.bash`, while direct endpoint-specific clients belong in the secondary `@openhands/typescript-client/clients` entrypoint instead of the root SDK surface.

### LLM Architecture
Expand Down Expand Up @@ -224,7 +223,6 @@ await conversation.close();

**Ergonomic API note**: Keep `ConversationManager` as the main server-scoped entry point. Server/LLM/settings/skills/tools/VSCode/desktop operations should be reachable through manager namespaces such as `manager.server`, `manager.llm`, and `manager.desktop`; ACP-specific operations should be reachable via `manager.acp`.


- `createConversation({ type, agent, workspace, options })` - Explicit type selection
- `createConversationAuto(agent, workspace, options)` - Auto-detect based on workspace type

Expand Down Expand Up @@ -337,7 +335,11 @@ The generated transport contract in
`npm run check:agent-server-api` to enforce a clean regeneration. Released SDK
versions use the `openapi.json` release artifact. Legacy releases without the
artifact are exported from an isolated temporary container of the exact pinned
image. The generator never reads from an unpinned `latest` endpoint.
image. Run `npm run test:agent-server-api-tooling` when changing generation or
drift automation. The scheduled `agent-server-sdk-main-audit` records the exact
SDK commit it tests and reports upcoming drift informationally; ordinary PR CI
never reads from that moving target. The generator never reads from an unpinned
`latest` endpoint.

## Local Setup and Validation

Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ release configured by `package.json` → `config.agentServerImage`.
```bash
npm run generate:agent-server-api
npm run check:agent-server-api
npm run test:agent-server-api-tooling
```

The generator downloads the matching SDK release's `openapi.json`. For older
Expand All @@ -69,6 +70,11 @@ SDK development, `AGENT_SERVER_OPENAPI_PATH=/path/to/openapi.json` can supply a
local candidate contract while retaining the pinned image metadata in the
generated file.

Pinned PR CI is reproducible and required. A separate weekly
`agent-server-sdk-main-audit` workflow exports the contract from one exact SDK
`main` commit, records that SHA, and opens or updates an informational drift
issue. Ordinary client PRs never generate from the moving SDK branch.

## Quick Start

### Start an AgentServer
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@
"scripts": {
"build": "tsc && node ./scripts/copy-json-assets.mjs && node ./scripts/rewrite-relative-imports.mjs",
"generate:agent-server-api": "node ./scripts/generate-agent-server-api.mjs",
"check:agent-server-api": "npm run generate:agent-server-api && git diff --exit-code -- src/generated/agent-server-schema.ts",
"check:agent-server-api": "node ./scripts/check-agent-server-api.mjs",
"summarize:agent-server-api": "node ./scripts/summarize-agent-server-api.mjs",
"test:agent-server-api-tooling": "node ./scripts/test-agent-server-api-tooling.mjs",
"lint": "eslint src/**/*.ts",
"lint:fix": "eslint src/**/*.ts --fix",
"format": "prettier --write \"src/**/*.ts\" \"examples/**/*.ts\" \"scripts/**/*.mjs\"",
Expand Down
35 changes: 35 additions & 0 deletions scripts/check-agent-server-api.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { spawnSync } from 'node:child_process';
import { readFile } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const generatedFile = resolve(
repositoryRoot,
process.env.AGENT_SERVER_GENERATED_OUTPUT ?? 'src/generated/agent-server-schema.ts'
);
const generator = join(repositoryRoot, 'scripts/generate-agent-server-api.mjs');

const before = await readFile(generatedFile, 'utf8');
const result = spawnSync(process.execPath, [generator], {
cwd: repositoryRoot,
env: process.env,
stdio: 'inherit',
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
process.exit(result.status ?? 1);
}

const after = await readFile(generatedFile, 'utf8');
if (before !== after) {
process.stderr.write(
`${generatedFile} was stale and has been regenerated. ` +
'Commit the generated contract diff.\n'
);
process.exit(1);
}

process.stdout.write('Checked-in Agent Server contract is current.\n');
8 changes: 6 additions & 2 deletions scripts/generate-agent-server-api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ import { format, resolveConfig } from 'prettier';

const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const packageJsonPath = join(repositoryRoot, 'package.json');
const generatedFile = join(repositoryRoot, 'src/generated/agent-server-schema.ts');
const generatedFile = resolve(
repositoryRoot,
process.env.AGENT_SERVER_GENERATED_OUTPUT ?? 'src/generated/agent-server-schema.ts'
);
const explicitSchemaPath = process.env.AGENT_SERVER_OPENAPI_PATH;
const explicitSchemaSource = process.env.AGENT_SERVER_SCHEMA_SOURCE;

function getPinnedAgentServer(packageJson) {
const image = packageJson.config?.agentServerImage;
Expand Down Expand Up @@ -166,7 +170,7 @@ async function main() {
const generated = await readFile(join(generatorOutput, 'types.gen.ts'), 'utf8');
const header = [
'// Generated file. Do not edit by hand.',
`// Source image: ${image}`,
`// Source: ${explicitSchemaSource ?? image}`,
`// Regenerate with: npm run generate:agent-server-api`,
'',
].join('\n');
Expand Down
Loading
Loading