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
4 changes: 3 additions & 1 deletion docs/getting-started/self-hosted.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ The Session brings its own model provider; the installation default never applie

The installer brings its own pinned Node.js and Harness versions (listed in [`scripts/build-native-installer.mjs`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/scripts/build-native-installer.mjs)) and leaves other installations of those tools untouched. On a platform without a matching installer, the command fails.

The download is about 320 MiB. At its peak the installation needs about 2 GiB free on the disk that holds the Runtime home and the installation directory with one Harness selected (archive 316 MiB, unpacked installer 907 MiB, installed files about 560 MiB), and about 2.2 GiB with all three; the unpacked copy is removed when the command ends and the archive when the installation completes. The finished installation takes about 560 MiB with one Harness and about 940 MiB with all three.

The machine needs:

- HTTP or HTTPS access to Core, and to the release download host unless Core carries an offline copy of the installers;
Expand Down Expand Up @@ -69,7 +71,7 @@ The installer reports three results:
| **Daemon connection** | Core confirmed the daemon's authenticated connection |
| **Model configuration** | Not checked; the first Turn uses the Session's model provider |

Downloads retry temporary network failures up to three attempts and show progress in a terminal. Disk space is checked before downloading, extracting and copying components. If a download or installation is interrupted, rerun the command: it clears unfinished temporary copies while preserving completed components, credentials and the workspace. Download staging lives in `native-download` under the Runtime home; its small `download.lock` file remains for concurrency control. An active download or installation is never cleared by another run. If the command expires, copy a fresh one from the Session.
The installer checks the grant before it downloads anything, so an expired command costs no download. The archive download resumes after a reset, fails only when it stalls (no data for 60 seconds) and gives up after five attempts; rerun the command to continue where it stopped. A verified archive is kept in `native-download` under the Runtime home until the installation completes, so a rerun after a failure needs no new download; it is checked against Core's checksum on every run and discarded if it fails. Disk space is checked before downloading, extracting and copying components. If a download or installation is interrupted, rerun the command: it clears unfinished temporary copies while preserving completed components, credentials and the workspace. The small `download.lock` file in `native-download` remains for concurrency control. An active download or installation is never cleared by another run. If the command expires, copy a fresh one from the Session.

If the connection is not confirmed within 45 seconds, the installer prints the path of the daemon's log. The daemon keeps reconnecting. Fix the cause and run the install command again with the same installation directory, copying a fresh one from Web if it has expired: completed components and the credential are kept and a running daemon is reused. Do not remove the workspace or the Session to retry.

Expand Down
6 changes: 4 additions & 2 deletions docs/zh/getting-started/self-hosted.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "自托管执行器"
source: docs/getting-started/self-hosted.md
source_hash: 8603c51679351eefcaa3977c90af7658e1e86d62be7980baf42f1d72faedf044
source_hash: 1084921d73014c1c48c6b4c2df5fa356544ed3b046bf0bc90cb9dce85b7188f7
---

`self_hosted` Session 在应用拥有的机器上运行:工作站、虚拟机或你管理的沙箱。应用通过 `/v1` 创建 Session,并获得安装 `oac-daemon`、启动它并连接 Core 的命令。Web 在 Session 页面展示同一命令;Web 是可选的。Core 不创建、停止或回收这台机器。
Expand All @@ -20,6 +20,8 @@ Session 自带模型提供商;安装默认模型不适用([原因](../../../

安装程序自带固定版本的 Node.js 和 Harness(列于 [`scripts/build-native-installer.mjs`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/scripts/build-native-installer.mjs)),不修改这些工具的其他安装。在没有匹配安装程序的平台上,命令会失败。

下载约 320 MiB。安装高峰时,Runtime 主目录和安装目录所在磁盘需要约 2 GiB 可用空间(只选一个 Harness:归档 316 MiB、解包后的安装程序 907 MiB、安装后的文件约 560 MiB),三个 Harness 全选约 2.2 GiB;解包副本在命令结束时删除,归档在安装完成时删除。安装完成后占用约 560 MiB(一个 Harness)至约 940 MiB(全部三个)。

机器需要:

- 通过 HTTP 或 HTTPS 访问 Core,以及访问发布下载主机;如果 Core 已有安装程序的离线副本,则无需后者;
Expand Down Expand Up @@ -71,7 +73,7 @@ Session 自带模型提供商;安装默认模型不适用([原因](../../../
| **Daemon connection** | Core 已确认守护进程的认证连接 |
| **Model configuration** | 未检查;第一个 Turn 使用 Session 的模型提供商 |

临时网络故障会重试下载,最多尝试三次,并在终端展示进度。下载、解压和复制组件前会检查磁盘空间。如果下载或安装被中断,重新执行命令:它清理未完成的临时副本,同时保留已完成的组件、凭据和工作区。下载暂存位于 Runtime 主目录的 `native-download` 中;小型 `download.lock` 文件保留用于并发控制。其他运行不会清理仍在进行的下载或安装。命令过期时,从 Session 复制新命令。
安装程序在下载任何内容之前先检查授权,因此过期的命令不会白下载。归档下载在连接重置后会续传,只在传输停滞(60 秒没有数据)时失败,最多尝试五次;重新执行命令即可从中断处继续。校验通过的归档保存在 Runtime 主目录的 `native-download` 中,直到安装完成,因此失败后重跑无需重新下载;每次运行都会对照 Core 提供的校验和检查它,未通过则丢弃。下载、解压和复制组件前会检查磁盘空间。如果下载或安装被中断,重新执行命令:它清理未完成的临时副本,同时保留已完成的组件、凭据和工作区。`native-download` 中的小型 `download.lock` 文件保留用于并发控制。其他运行不会清理仍在进行的下载或安装。命令过期时,从 Session 复制新命令。

如果 45 秒内未确认连接,安装程序输出守护进程日志路径。守护进程继续重连。修复原因后,以同一安装目录重新执行安装命令;如果命令已过期,从 Web 复制新的。已完成的组件和凭据会保留,已运行的守护进程会复用。不要删除工作区或 Session 来重试。

Expand Down
68 changes: 52 additions & 16 deletions services/core/internal/nativeinstaller/assets/bootstrap.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -47,39 +47,48 @@ function Show-Bytes([string]$Label, [long]$Bytes, [long]$Total) {
$percent = if ($Total -gt 0) { [Math]::Min(100, [int](100.0 * $Bytes / $Total)) } else { -1 }
Write-Progress -Activity $Label -Status ("{0:N1} MiB" -f ($Bytes / 1MB)) -PercentComplete $percent
}
# Resume after resets and fail only when a transfer stalls, with no total time cap. A partial file is kept in the download directory for the next run.
function Get-NativeFile([string]$Uri, [string]$Destination, [long]$Limit) {
for ($attempt=1; $attempt -le 3; $attempt++) {
$response = $null; $inputStream = $null; $outputStream = $null; $retryable = $true
for ($attempt=1; $attempt -le 5; $attempt++) {
$request = $null; $response = $null; $inputStream = $null; $outputStream = $null; $retryable = $true
try {
[long]$offset = 0
if (Test-Path -LiteralPath $Destination) { $offset = (Get-Item -LiteralPath $Destination).Length }
$address = [Uri]$Uri
for ($redirect=0; $redirect -le 5; $redirect++) {
$response = $client.GetAsync($address, [Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult()
$request = [Net.Http.HttpRequestMessage]::new([Net.Http.HttpMethod]::Get, $address)
if ($offset -gt 0) { $request.Headers.Range = [Net.Http.Headers.RangeHeaderValue]::new($offset, $null) }
$response = $client.SendAsync($request, [Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult()
$request.Dispose(); $request = $null
$status = [int]$response.StatusCode
if ($status -notin @(301,302,303,307,308)) { break }
if ($redirect -eq 5 -or !$response.Headers.Location) { $retryable=$false; throw 'Installer download has too many or invalid redirects.' }
$next = [Uri]::new($address, $response.Headers.Location)
if ($next.Scheme -ne 'https' -or $next.UserInfo) { $retryable=$false; throw 'Installer download redirect must use HTTPS without credentials.' }
$response.Dispose(); $response=$null; $address=$next
}
if ($status -ne 200) {
if ($status -eq 416) { Remove-Item -LiteralPath $Destination -Force; throw 'The server cannot resume the earlier download; starting it again.' }
if ($status -notin @(200,206)) {
$retryable = $status -in @(408,429,500,502,503,504)
if ($status -eq 404) { throw 'This Core has no qualified installer for this platform.' }
throw "Installer download failed (HTTP $status). Check Core and the download host."
}
# A 200 answer ignores the Range header, so start the file again.
$mode = [IO.FileMode]::Append
if ($status -eq 200) { $mode = [IO.FileMode]::Create; $offset = 0 }
$total = $response.Content.Headers.ContentLength
if ($null -eq $total) { $total = 0 }
$retryable=$false
if ($total -gt $Limit) { throw 'Installer download exceeds the available space or metadata size limit.' }
if ($offset + $total -gt $Limit) { throw 'Installer download exceeds the available space or metadata size limit.' }
Assert-Space $total
$outputStream = [IO.File]::Open($Destination, [IO.FileMode]::Create, [IO.FileAccess]::Write, [IO.FileShare]::None)
$outputStream = [IO.File]::Open($Destination, $mode, [IO.FileAccess]::Write, [IO.FileShare]::None)
$retryable=$true
$inputStream = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
$buffer = New-Object byte[] 65536
[long]$received = 0
$clock = [Diagnostics.Stopwatch]::StartNew()
[long]$lastProgress=0
while ($true) {
if ($clock.Elapsed.TotalSeconds -gt 1200) { throw 'Installer download timed out; retry with a fresh command if it expired.' }
$cancel = [Threading.CancellationTokenSource]::new()
try {
$cancel.CancelAfter(60000)
Expand All @@ -90,28 +99,42 @@ function Get-NativeFile([string]$Uri, [string]$Destination, [long]$Limit) {
if ($read -eq 0) { break }
$received += $read
$retryable=$false
if ($received -gt $Limit) { throw 'Installer download exceeds the available space or metadata size limit.' }
if ($offset + $received -gt $Limit) { throw 'Installer download exceeds the available space or metadata size limit.' }
$outputStream.Write($buffer,0,$read)
$retryable=$true
if ($clock.ElapsedMilliseconds - $lastProgress -ge 200) { Show-Bytes 'Downloading installer' $received $total; $lastProgress=$clock.ElapsedMilliseconds }
if ($clock.ElapsedMilliseconds - $lastProgress -ge 200) { Show-Bytes 'Downloading installer' ($offset + $received) ($offset + $total); $lastProgress=$clock.ElapsedMilliseconds }
}
if ($total -gt 0 -and $received -ne $total) { throw 'Installer download was interrupted.' }
$retryable=$false
$outputStream.Flush($true)
return
} catch {
if (!$retryable) { throw }
if ($attempt -eq 3) { throw 'Installer download failed after three attempts. Check network, proxy and certificates, then retry.' }
Write-Host "Download interrupted; retrying ($attempt/2)..."
if ($attempt -eq 5) { throw 'Installer download failed after five attempts. Check the network to Core and GitHub and any proxy settings, then rerun the command; downloaded data is kept and resumed.' }
Write-Host "Download interrupted; retrying ($attempt/4)..."
Start-Sleep -Seconds $attempt
} finally {
if ($inputStream) { $inputStream.Dispose() }
if ($outputStream) { $outputStream.Dispose() }
if ($request) { $request.Dispose() }
if ($response) { $response.Dispose() }
if ($terminal) { Write-Progress -Activity 'Downloading installer' -Completed }
}
}
}
function Get-ArchiveHash([string]$Path) { (Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash.ToLowerInvariant() }
# Reject a rejected grant before any large download. Only a 401 stops here; any other answer is reported by the installer.
function Test-Grant {
if (!$env:OAC_INSTALL_GRANT) { return }
$rejected = $false
try {
$message = [Net.Http.HttpRequestMessage]::new([Net.Http.HttpMethod]::Post, ($Base -replace '/install/[^/]+$', '/installation'))
$message.Headers.Authorization = [Net.Http.Headers.AuthenticationHeaderValue]::new('Bearer', $env:OAC_INSTALL_GRANT)
$answer = $client.SendAsync($message).GetAwaiter().GetResult()
$rejected = [int]$answer.StatusCode -eq 401
} catch { } finally { if ($message) { $message.Dispose() }; if ($answer) { $answer.Dispose() } }
if ($rejected) { throw 'Installation authorization rejected or expired; copy a fresh command from the Session.' }
}
try {
Assert-OrdinaryPath $cache
[IO.Directory]::CreateDirectory($cache) | Out-Null
Expand All @@ -136,17 +159,29 @@ try {
$handler.AllowAutoRedirect = $false
$client = [Net.Http.HttpClient]::new($handler)
$client.Timeout = [TimeSpan]::FromSeconds(15)
Test-Grant
Assert-Space 0
Write-Host 'Downloading the installer matched to Core...'
$checksum = Join-Path $work 'checksum'
Get-NativeFile "$Base/windows-$architecture.sha256" $checksum 1024
$expected = [IO.File]::ReadAllText($checksum).Trim()
if ($expected -cnotmatch '^[0-9a-f]{64}$') { throw 'Core returned an invalid installer checksum.' }
$archive = Join-Path $work 'bundle.tar.gz'
$available = [OacNativeDownloadSpace]::Available($cache)
Get-NativeFile "$Base/windows-$architecture.tar.gz" $archive ($available-64MB)
Write-Host 'Verifying the installer archive...'
if ((Get-FileHash -Algorithm SHA256 $archive).Hash.ToLowerInvariant() -ne $expected) { throw 'Installer checksum mismatch; download again.' }
# The verified archive outlives this run so a later failure, such as a wrong option, needs no new download; it is removed once the installation completes.
$archive = Join-Path $cache "$expected.tar.gz"
Get-ChildItem -LiteralPath $cache -Filter '*.tar.gz*' -File | Where-Object { $_.Name -ne "$expected.tar.gz" -and $_.Name -ne "$expected.tar.gz.part" } | Remove-Item -Force
if (Test-Path -LiteralPath $archive) {
Write-Host 'Verifying the installer archive from an earlier run...'
if ((Get-ArchiveHash $archive) -ne $expected) { Remove-Item -LiteralPath $archive -Force; Write-Host 'The earlier archive failed verification and was discarded.' }
}
if (!(Test-Path -LiteralPath $archive)) {
$part = "$archive.part"
if ((Test-Path -LiteralPath $part) -and (Get-Item -LiteralPath $part).Length -gt 0) { Write-Host 'Resuming the earlier download...' }
$available = [OacNativeDownloadSpace]::Available($cache)
Get-NativeFile "$Base/windows-$architecture.tar.gz" $part ($available-64MB)
Write-Host 'Verifying the installer archive...'
if ((Get-ArchiveHash $part) -ne $expected) { Remove-Item -LiteralPath $part -Force; throw 'Installer checksum mismatch; the download was discarded. Rerun the command to download it again.' }
Move-Item -LiteralPath $part -Destination $archive
}
Write-Host 'Checking extraction space...'
$file = [IO.File]::OpenRead($archive)
$gzip = [IO.Compression.GZipStream]::new($file,[IO.Compression.CompressionMode]::Decompress)
Expand All @@ -165,6 +200,7 @@ try {
Write-Host 'Starting installation...'
& (Join-Path $bundle 'oac-daemon.exe') install --onboard-url $endpoint @InstallArguments
if ($LASTEXITCODE -ne 0) { throw 'Installation or connection failed; follow the installer guidance and retry.' }
Remove-Item -LiteralPath $archive -Force
} finally {
if ($client) { $client.Dispose() }
if ($lock) {
Expand Down
Loading
Loading