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
81 changes: 81 additions & 0 deletions .github/workflows/core-wheels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Published Core wheels
on:
workflow_call:
secrets:
LOOP_LIVE_CI_READ_KEY:
required: true
permissions:
contents: read
jobs:
runtime:
# Called only by the explicitly selected manual qualification mode.
if: github.event_name == 'workflow_dispatch'
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.13'
- uses: astral-sh/setup-uv@v6
with:
enable-cache: false
- name: Install and record the released host Core wheel
run: |
uv sync --locked --group dev
python scripts/release_qualification.py core-wheel --python .venv/bin/python --destination "$RUNNER_TEMP/host-core-wheel.json"
uv run --no-sync pytest tests/test_core_binary.py tests/test_runtime_updates.py tests/test_release_qualification.py tests/test_standalone.py -q
- name: Resolve the declared loop-live source
id: runtime-dependency
run: |
python -c 'import tomllib; p=tomllib.load(open("amplifier_web/runtime_deps/pyproject.toml", "rb")); print("revision=" + p["tool"]["uv"]["sources"]["amplifier-module-loop-live"]["rev"])' >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
with:
repository: microsoft/amplifier-module-loop-live
ref: ${{ steps.runtime-dependency.outputs.revision }}
path: .ci/loop-live
ssh-key: ${{ secrets.LOOP_LIVE_CI_READ_KEY }}
persist-credentials: false
- uses: actions/checkout@v4
with:
repository: microsoft/amplifier-bundle-recipes
ref: main
path: .ci/recipes
persist-credentials: false
- name: Resolve current components and install the released Core wheel
run: |
python -m pip install maturin
python scripts/release_qualification.py runtime-project --runtime "$RUNNER_TEMP/core-runtime"
uv add --project "$RUNNER_TEMP/core-runtime" --frozen --editable "$GITHUB_WORKSPACE/.ci/loop-live"
uv add --project "$RUNNER_TEMP/core-runtime" --frozen pytest pytest-asyncio 'amplifier-module-tool-delegate @ git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=modules/tool-delegate'
uv lock --project "$RUNNER_TEMP/core-runtime" --refresh --upgrade
python scripts/release_qualification.py cache-key --runtime "$RUNNER_TEMP/core-runtime"
uv sync --project "$RUNNER_TEMP/core-runtime" --locked
python scripts/release_qualification.py runtime-snapshot --runtime "$RUNNER_TEMP/core-runtime"
- name: Qualify real runtime behavior without model calls
run: |
UNIFIED_RUNTIME_PYTHON="$RUNNER_TEMP/core-runtime/.venv/bin/python" WARM_RECIPES_PATH="$GITHUB_WORKSPACE/.ci/recipes" "$RUNNER_TEMP/core-runtime/.venv/bin/python" -m pytest tests/test_app_guidance.py tests/test_host_children.py tests/test_host_components.py tests/test_runtime_module_cache.py -q
cp "$RUNNER_TEMP/core-runtime/qualified-runtime.json" "$RUNNER_TEMP/before-tests.json"
python scripts/release_qualification.py runtime-snapshot --runtime "$RUNNER_TEMP/core-runtime"
cmp "$RUNNER_TEMP/before-tests.json" "$RUNNER_TEMP/core-runtime/qualified-runtime.json"
git rev-parse HEAD > "$RUNNER_TEMP/app-revision.txt"
- name: Retain only dependency and binary qualification receipts
if: always()
uses: actions/upload-artifact@v4
with:
name: core-wheels-${{ matrix.os }}
path: |
${{ runner.temp }}/app-revision.txt
${{ runner.temp }}/host-core-wheel.json
${{ runner.temp }}/core-runtime/pyproject.toml
${{ runner.temp }}/core-runtime/uv.lock
${{ runner.temp }}/core-runtime/build-constraints.txt
${{ runner.temp }}/core-runtime/build-identity.json
${{ runner.temp }}/core-runtime/qualified-runtime.json
if-no-files-found: error
11 changes: 11 additions & 0 deletions .github/workflows/python-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ name: Python checks (on demand)
# Temporarily opt-in during rapid development; never a PR or release gate.
on:
workflow_dispatch:
inputs:
core_wheels:
description: Qualify published Core wheels and real runtime on both Linux architectures
type: boolean
default: false
permissions:
contents: read
jobs:
python:
if: ${{ !inputs.core_wheels }}
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
Expand All @@ -17,3 +23,8 @@ jobs:
- run: uv sync --locked --group dev --group artifacts
- name: Run the complete Python suite
run: uv run --no-sync pytest -q --tb=short
core-wheels:
if: ${{ inputs.core_wheels }}
uses: ./.github/workflows/core-wheels.yml
secrets:
LOOP_LIVE_CI_READ_KEY: ${{ secrets.LOOP_LIVE_CI_READ_KEY }}
9 changes: 7 additions & 2 deletions amplifier_web/runtime_deps/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description = "Standalone Foundation and loop-live runtime for Amplifier Unified
requires-python = ">=3.13"
dependencies = [
"amplifier-memory @ git+https://github.com/microsoft/amplifier-bundle-memory@main",
"amplifier-core>=1.6.1",
"amplifier-core>=2.0.1",
"amplifier-module-loop-live",
"amplifier-foundation",
"amplifier-module-loop-streaming",
Expand All @@ -29,13 +29,18 @@ dependencies = [
]

[tool.uv]
# Core publishes native wheels; never compile it while installing this app.
no-build-package = ["amplifier-core"]
# Follow the same Foundation branch while bundles resolve their modules.
override-dependencies = [
"amplifier-foundation @ git+https://github.com/microsoft/amplifier-foundation@main",
]

[[tool.uv.index]]
url = "https://pypi.org/simple"
default = true

[tool.uv.sources]
amplifier-core = { git = "https://github.com/microsoft/amplifier-core", rev = "main" }
amplifier-module-loop-live = { git = "https://github.com/microsoft/amplifier-module-loop-live", rev = "main" }
amplifier-foundation = { git = "https://github.com/microsoft/amplifier-foundation", rev = "main" }
amplifier-module-loop-streaming = { git = "https://github.com/microsoft/amplifier-module-loop-streaming", rev = "main" }
Expand Down
29 changes: 26 additions & 3 deletions amplifier_web/runtime_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,15 +150,36 @@ def inventory(home, *, installed=None):
for name, source in sorted(observed.items()):
ref = source.get('ref', '')
override = source.get('override', False)
eligible = bool(not override and ref and not pinned(ref) and source.get('current'))
registry_migration = core_registry_migration(name, source, baseline)
eligible = bool(not override and not registry_migration and ref and not pinned(ref) and source.get('current'))
rows.append({'id': 'runtime:' + name, 'package': name, 'kind': 'runtime dependency',
'label': safe_label(source['url']) if source.get('url') else name,
**source, 'ref': ref, 'status': 'not_checked' if eligible else 'local' if override else 'pinned',
**source, 'ref': ref, 'status': 'not_checked' if eligible or registry_migration else 'local' if override else 'pinned',
**({'registryMigration': True} if registry_migration else {}),
'eligible': eligible, 'usage': 'configured',
'usageEvidence': ['Conversation worker environment', source['provenance']]})
return rows


def core_registry_migration(name, source, baseline):
"""Replace only the old app-owned Core default in the next generation.

An installed fork, fixed ref, editable source or dirty cache remains protected.
The previous base manifest is policy evidence; frozen receipts stay untouched.
"""
if name != 'amplifier-core':
return False
current = tomllib.loads(manifest_path().read_text()).get('tool', {}).get('uv', {})
previous = tomllib.loads(baseline.decode()).get('tool', {}).get('uv', {}).get('sources', {}).get(name, {})
upstream = 'https://github.com/microsoft/amplifier-core'
return (name in current.get('no-build-package', [])
and name not in current.get('sources', {})
and previous.get('git') == upstream and (previous.get('rev') or previous.get('branch')) == 'main'
and not previous.get('subdirectory') and not previous.get('tag')
and source.get('url') == upstream and source.get('ref') == 'main'
and not source.get('subdirectory') and not source.get('override') and not source.get('cacheManaged'))


class ProtectedRuntimeSource(ValueError):
"""A fixed reason and package name, never source paths or exception text."""
def __init__(self, package, reason='protected-runtime-source'):
Expand Down Expand Up @@ -255,6 +276,8 @@ def augmented_manifest(content, rows):
name = row['package']
if row.get('override') and (name in declared or row.get('cacheManaged') or row.get('trackedSource')):
raise ProtectedRuntimeSource(name)
if row.get('registryMigration'):
continue
if row.get('override') or not row.get('url') or not row.get('ref'):
continue
if name in declared:
Expand Down Expand Up @@ -325,7 +348,7 @@ async def stage(manager, generation, candidates, *, finalize=True):
for row in selected:
if row.get('kind') == 'runtime dependency':
installed = baseline_sources.get(row['package'])
if not installed or any(installed.get(key, '') != row.get(key, '') for key in ('current', 'url', 'ref', 'subdirectory')):
if not installed or installed.get('registryMigration') or any(installed.get(key, '') != row.get(key, '') for key in ('current', 'url', 'ref', 'subdirectory')):
raise ValueError('Runtime dependencies changed since checking; check for updates again.')
from .updates import pinned
declared = tomllib.loads(content.decode()).get('tool', {}).get('uv', {}).get('sources', {})
Expand Down
35 changes: 35 additions & 0 deletions docs/validation/release-qualification.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,38 @@ qualification lanes.

An exact-candidate promotion system that reuses matching PR/merge qualification,
shared prebuilt wheels, and merge-policy changes remain separate proposals.

## Published Core binaries

The app host, worker project and Work acceptance project require
`amplifier-core>=2.0.1` from PyPI. This is a minimum supported release, not a
standing exact-version pin. Core's Git source override is removed. The uv
`no-build-package` policy forbids compiling Core; other source dependencies can
still build. The worker uses the same explicit public index as the host so an
outdated local mirror cannot silently keep it on an older Core release.

An existing worker's old app-owned Core Git-main default migrates only in a new
qualified environment generation. Its previous base manifest must prove that
default, and the installed source must match it. Forks, fixed refs and edited or
local sources are preserved; they are never treated as that default. Existing
locks and rollback receipts are unchanged. Installed-source augmentation must
not reintroduce the retired app-owned Core Git override.

The release's fresh source resolution, exact cache key and frozen runtime graph
checks remain intact. Rust/Maturin identity and build constraints remain because
other moving-source packages may require native builds. A Core policy/lock
change changes the cache key; no old Git-Core cache can make the registry
selection sticky. Core wheel metadata and its native extension hash now join
the runtime receipt, and qualification fails if amplifier-app-cli is installed.
No CLI companion is needed by Unified's runtime qualification.

To independently qualify a dependency change on both Linux architectures, run
the existing **Python checks (on demand)** workflow on the reviewed branch with
`core_wheels=true`. Its default remains the full Python suite. The Core mode
runs focused migration/receipt tests and all 24 real runtime checks unchanged
on Linux x86_64 and aarch64, using fresh moving-source resolution and published
Core wheels. It records the tested app revision, full lock, resolved source
identities, installed graph, wheel tags and native binary hashes. It has read-only
repository permissions, no publisher, and no production app access. The private
loop-live key remains confined to explicitly dispatched qualification; automatic
PR jobs do not receive it. macOS and Windows require separate platform evidence.
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ requires-python = ">=3.13"
# python-pam imports six but does not declare it in its distribution metadata.
dependencies = [
"amplifier-memory @ git+https://github.com/microsoft/amplifier-bundle-memory@main",
"amplifier-core>=1.6.1",
"amplifier-core>=2.0.1",
"amplifier-foundation @ git+https://github.com/microsoft/amplifier-foundation@main",
"aiohttp>=3.12,<4",
"packaging>=24",
Expand Down Expand Up @@ -55,6 +55,10 @@ exclude = ["/.ci/**"]
"examples/shell-reader" = "amplifier_web/bundle_data/examples/shell-reader"
"examples/shell-controls" = "amplifier_web/bundle_data/examples/shell-controls"

[tool.uv]
# Core publishes native wheels; never compile it while installing this app.
no-build-package = ["amplifier-core"]

[[tool.uv.index]]
url = "https://pypi.org/simple"
default = true
Expand Down
29 changes: 26 additions & 3 deletions scripts/release_qualification.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,32 @@ def graph(python):
return json.loads(run(str(python), '-I', '-c', probe))


def core_wheel(python):
"""Record the installed native binary without importing any CLI or app."""
probe = '''import hashlib,importlib.metadata as m,importlib.machinery as machinery,json,platform,sys
d=m.distribution('amplifier-core')
native=[p for p in d.files or [] if str(p).startswith('amplifier_core/_engine.') and any(str(p).endswith(s) for s in machinery.EXTENSION_SUFFIXES)]
print(json.dumps({'version':d.version,'direct':json.loads(d.read_text('direct_url.json') or 'null'),
'wheel':d.read_text('WHEEL'),'native':[{'name':str(p),'sha256':hashlib.sha256(d.locate_file(p).read_bytes()).hexdigest()} for p in native],
'python':sys.version,'machine':platform.machine(),
'cliInstalled':any(x.metadata['Name'].lower().replace('_','-')=='amplifier-app-cli' for x in m.distributions())}))'''
value = json.loads(run(str(python), '-I', '-c', probe))
if (value['direct'] is not None or not value['wheel'] or 'Root-Is-Purelib: false' not in value['wheel']
or len(value['native']) != 1 or value['cliInstalled']):
raise ValueError('Core must be a registry native wheel without amplifier-app-cli')
return value


def runtime_snapshot(root, runtime):
build = json.loads((runtime / 'build-identity.json').read_text())
if build != build_identity(root, runtime):
raise ValueError('Runtime source or toolchain changed after cache selection')
return {'build': digest(build), 'recipes': checkout_identity(root / '.ci/recipes'),
'graph': graph(runtime / '.venv/bin/python')}
value = {'build': digest(build), 'recipes': checkout_identity(root / '.ci/recipes'),
'graph': graph(runtime / '.venv/bin/python')}
policy = tomllib.loads((runtime / 'pyproject.toml').read_text()).get('tool', {}).get('uv', {})
if 'amplifier-core' in policy.get('no-build-package', []):
value['coreWheel'] = core_wheel(runtime / '.venv/bin/python')
return value


def receipt(root, evidence, expected, lane, destination, runtime=None, dist=None):
Expand Down Expand Up @@ -152,7 +172,7 @@ def verify_receipts(root, evidence, expected, receipts, dist):

def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('command', choices=('candidate', 'verify-candidate', 'runtime-project', 'cache-key', 'runtime-snapshot', 'receipt', 'verify-receipts'))
parser.add_argument('command', choices=('candidate', 'verify-candidate', 'runtime-project', 'cache-key', 'runtime-snapshot', 'core-wheel', 'receipt', 'verify-receipts'))
parser.add_argument('--evidence', type=Path)
parser.add_argument('--candidate')
parser.add_argument('--runtime', type=Path)
Expand All @@ -161,6 +181,7 @@ def main():
parser.add_argument('--receipts', type=Path)
parser.add_argument('--dist', type=Path, default=Path('dist'))
parser.add_argument('--output', type=Path)
parser.add_argument('--python', type=Path, default=Path(sys.executable))
args = parser.parse_args()
root = Path.cwd()
output = None
Expand All @@ -176,6 +197,8 @@ def main():
output = ('key', 'release-runtime-v1-' + digest(value))
elif args.command == 'runtime-snapshot':
write(args.runtime / 'qualified-runtime.json', runtime_snapshot(root, args.runtime))
elif args.command == 'core-wheel':
write(args.destination, core_wheel(args.python))
elif args.command == 'receipt':
receipt(root, args.evidence, args.candidate, args.lane, args.destination, args.runtime, args.dist)
else:
Expand Down
4 changes: 3 additions & 1 deletion scripts/work_profile/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "amplifier-work-acceptance"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
"amplifier-core>=1.6.1",
"amplifier-core>=2.0.1",
"amplifier-unified",
"amplifier-foundation @ git+https://github.com/microsoft/amplifier-foundation@main",
"amplifier-module-loop-live @ git+https://github.com/microsoft/amplifier-module-loop-live@main",
Expand All @@ -18,6 +18,8 @@ dev = ["pytest>=8", "pytest-asyncio>=0.24"]
browser = ["playwright>=1.55,<2"]

[tool.uv]
# Core publishes native wheels; never compile it while installing this app.
no-build-package = ["amplifier-core"]
package = false

[[tool.uv.index]]
Expand Down
Loading
Loading