@promus/cli 0.24.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/bin/promus +33 -0
- package/package.json +51 -0
- package/src/commands/_agents.ts +14 -0
- package/src/commands/_inft-ref.ts +43 -0
- package/src/commands/_unlock.ts +74 -0
- package/src/commands/admin-autotopup-tick.ts +73 -0
- package/src/commands/admin.test.ts +34 -0
- package/src/commands/admin.ts +32 -0
- package/src/commands/balance.test.ts +10 -0
- package/src/commands/balance.ts +112 -0
- package/src/commands/chat-sandbox.tsx +520 -0
- package/src/commands/chat-telegram.ts +398 -0
- package/src/commands/chat.tsx +1916 -0
- package/src/commands/deploy.ts +204 -0
- package/src/commands/drain.ts +90 -0
- package/src/commands/gateway-logs.ts +47 -0
- package/src/commands/gateway-run.ts +54 -0
- package/src/commands/gateway-start.ts +218 -0
- package/src/commands/gateway-status.ts +88 -0
- package/src/commands/gateway-stop.ts +133 -0
- package/src/commands/gateway.ts +101 -0
- package/src/commands/init/cost.test.ts +169 -0
- package/src/commands/init/cost.ts +154 -0
- package/src/commands/init/funding-gate.ts +67 -0
- package/src/commands/init/model-picker.ts +81 -0
- package/src/commands/init/operator-picker.ts +263 -0
- package/src/commands/init/resume.ts +136 -0
- package/src/commands/init/sandbox-provision.test.ts +497 -0
- package/src/commands/init/sandbox-provision.ts +1177 -0
- package/src/commands/init/telegram-step.ts +229 -0
- package/src/commands/init/wizard-state.ts +95 -0
- package/src/commands/init.ts +612 -0
- package/src/commands/inspect.ts +529 -0
- package/src/commands/ledger.ts +176 -0
- package/src/commands/logs.ts +86 -0
- package/src/commands/migrate-keystore.ts +155 -0
- package/src/commands/model.ts +48 -0
- package/src/commands/pairing-approve.ts +114 -0
- package/src/commands/pairing-clear.ts +42 -0
- package/src/commands/pairing-list.ts +58 -0
- package/src/commands/pairing-revoke.ts +52 -0
- package/src/commands/pairing.test.ts +88 -0
- package/src/commands/pairing.ts +81 -0
- package/src/commands/pause.ts +99 -0
- package/src/commands/profile.ts +184 -0
- package/src/commands/restore.ts +221 -0
- package/src/commands/resume.ts +181 -0
- package/src/commands/status.ts +119 -0
- package/src/commands/sync.ts +147 -0
- package/src/commands/telegram-remove.ts +65 -0
- package/src/commands/telegram-setup.ts +74 -0
- package/src/commands/telegram-status.ts +89 -0
- package/src/commands/telegram.test.ts +50 -0
- package/src/commands/telegram.ts +44 -0
- package/src/commands/topup.ts +303 -0
- package/src/commands/transfer.test.ts +111 -0
- package/src/commands/transfer.ts +520 -0
- package/src/commands/upgrade.test.ts +137 -0
- package/src/commands/upgrade.ts +690 -0
- package/src/config/load.ts +35 -0
- package/src/config/render.test.ts +96 -0
- package/src/config/render.ts +110 -0
- package/src/index.ts +378 -0
- package/src/sandbox/client.test.ts +251 -0
- package/src/sandbox/client.ts +424 -0
- package/src/ui/app.tsx +677 -0
- package/src/ui/approval-summary.test.ts +154 -0
- package/src/ui/approval-summary.ts +34 -0
- package/src/ui/markdown-parse.ts +219 -0
- package/src/ui/markdown.test.ts +146 -0
- package/src/ui/markdown.tsx +37 -0
- package/src/ui/state.test.ts +74 -0
- package/src/ui/state.ts +198 -0
- package/src/util/bootstrap-mode.test.ts +40 -0
- package/src/util/bootstrap-mode.ts +25 -0
- package/src/util/bootstrap-progress-box.test.ts +190 -0
- package/src/util/bootstrap-progress-box.ts +378 -0
- package/src/util/brain-secrets.ts +96 -0
- package/src/util/cli-version.ts +28 -0
- package/src/util/format.test.ts +16 -0
- package/src/util/format.ts +11 -0
- package/src/util/gateway-spawn.test.ts +86 -0
- package/src/util/gateway-spawn.ts +128 -0
- package/src/util/gateway-version.test.ts +113 -0
- package/src/util/gateway-version.ts +154 -0
- package/src/util/github-releases.test.ts +116 -0
- package/src/util/github-releases.ts +79 -0
- package/src/util/profile-key.test.ts +60 -0
- package/src/util/profile-key.ts +25 -0
- package/src/util/ref-resolver.test.ts +77 -0
- package/src/util/ref-resolver.ts +55 -0
- package/src/util/silence-console.test.ts +53 -0
- package/src/util/silence-console.ts +40 -0
- package/src/util/telegram-secrets.test.ts +227 -0
- package/src/util/telegram-secrets.ts +223 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
import { cancel, confirm, intro, isCancel, log, note, outro, spinner } from '@clack/prompts'
|
|
2
|
+
import {
|
|
3
|
+
type PromusNetwork,
|
|
4
|
+
type PromusPlugin,
|
|
5
|
+
type OperatorSigner,
|
|
6
|
+
SANDBOX_PROVIDER_URL_GALILEO,
|
|
7
|
+
SandboxProviderClient,
|
|
8
|
+
iNFTAgentId,
|
|
9
|
+
} from '@promus/core'
|
|
10
|
+
import {
|
|
11
|
+
type BootstrapMode,
|
|
12
|
+
UPGRADE_DONE_MARKER,
|
|
13
|
+
UPGRADE_FAIL_KEYWORDS,
|
|
14
|
+
UPGRADE_FAIL_MARKER,
|
|
15
|
+
UPGRADE_PROGRESS_LOG,
|
|
16
|
+
UPGRADE_SUCCESS_MARKER_PREFIX,
|
|
17
|
+
buildUpgradeScript,
|
|
18
|
+
} from '@promus/gateway'
|
|
19
|
+
import type { Address, Hex } from 'viem'
|
|
20
|
+
import { findAndLoadConfig } from '../config/load'
|
|
21
|
+
import { writeConfigTs } from '../config/render'
|
|
22
|
+
import { SandboxClient } from '../sandbox/client'
|
|
23
|
+
import { BootstrapProgressController } from '../util/bootstrap-progress-box'
|
|
24
|
+
import { resolveCliVersion } from '../util/cli-version'
|
|
25
|
+
import { checkTagExists } from '../util/github-releases'
|
|
26
|
+
import { loadProfileScopeKeyHex } from '../util/profile-key'
|
|
27
|
+
import {
|
|
28
|
+
PROMUS_REPO_URL,
|
|
29
|
+
LATEST_KEYWORD,
|
|
30
|
+
type ResolvedRef,
|
|
31
|
+
expectedVersionFromRef,
|
|
32
|
+
formatResolvedRef,
|
|
33
|
+
resolvePromusRef,
|
|
34
|
+
} from '../util/ref-resolver'
|
|
35
|
+
import { loadTelegramHandoffSecrets } from '../util/telegram-secrets'
|
|
36
|
+
import { loadOrPickOperatorSigner } from './init/operator-picker'
|
|
37
|
+
import {
|
|
38
|
+
ensureSandboxStarted,
|
|
39
|
+
extractExecOutput,
|
|
40
|
+
handoffAgentToGateway,
|
|
41
|
+
makeExecRead,
|
|
42
|
+
preflightProviderDeposit,
|
|
43
|
+
publishSandboxEndpoint,
|
|
44
|
+
runSandboxProvision,
|
|
45
|
+
unlockAgentKeystore,
|
|
46
|
+
} from './init/sandbox-provision'
|
|
47
|
+
|
|
48
|
+
export type UpgradeMode = 'in-place' | 'reprovision'
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse the argv tail (everything AFTER the `upgrade` subcommand token) into
|
|
52
|
+
* {@link UpgradeOpts}. `--ref <val>` takes priority. Otherwise the first
|
|
53
|
+
* non-flag arg becomes the ref, so `promus upgrade latest` and
|
|
54
|
+
* `promus upgrade v0.17.8` work without `--ref`. Empty tail → undefined ref →
|
|
55
|
+
* command flow defaults to `latest` via GitHub API.
|
|
56
|
+
*/
|
|
57
|
+
export function parseUpgradeArgs(tail: readonly string[]): UpgradeOpts {
|
|
58
|
+
const refIdx = tail.indexOf('--ref')
|
|
59
|
+
const flagRef = refIdx >= 0 ? tail[refIdx + 1] : undefined
|
|
60
|
+
const positionalRef = tail.find(a => !a.startsWith('-') && a !== flagRef)
|
|
61
|
+
return {
|
|
62
|
+
ref: flagRef ?? positionalRef,
|
|
63
|
+
yes: tail.includes('--yes') || tail.includes('-y'),
|
|
64
|
+
reprovision: tail.includes('--reprovision'),
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface UpgradeOpts {
|
|
69
|
+
ref?: string
|
|
70
|
+
yes?: boolean
|
|
71
|
+
/**
|
|
72
|
+
* Opt into the heavy container-swap path. Default (false) is in-place. We
|
|
73
|
+
* default to in-place because promus's harness layer is unsealed
|
|
74
|
+
* (`feedback-promus-is-unsealed-currently.md`), so a fresh container buys
|
|
75
|
+
* no real attestation freshness. Heavy mode is reserved for the future
|
|
76
|
+
* when sealed mode + image-hash attestation are wired up.
|
|
77
|
+
*/
|
|
78
|
+
reprovision?: boolean
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* `promus upgrade`: roll the sandbox harness to a new git ref while preserving
|
|
83
|
+
* agent identity + memory.
|
|
84
|
+
*
|
|
85
|
+
* Default = in-place: `git fetch + checkout + bun install + harness restart`
|
|
86
|
+
* inside the existing Daytona container. ~30-60s downtime, $0 testnet cost,
|
|
87
|
+
* same sandbox UUID + endpoint.
|
|
88
|
+
*
|
|
89
|
+
* `--reprovision` (opt-in) = container swap: delete old sandbox + provision
|
|
90
|
+
* fresh + ECIES-handoff + publish new endpoint. ~2-5 min, ~0.9 0G testnet
|
|
91
|
+
* provider deposit. Reserved for sealed mode where attestation freshness is
|
|
92
|
+
* a load-bearing primitive.
|
|
93
|
+
*
|
|
94
|
+
* Both paths preserve: iNFT, agent EOA, encrypted keystore on 0G Storage,
|
|
95
|
+
* memory anchored on chain, 0G Compute ledger.
|
|
96
|
+
*/
|
|
97
|
+
export async function runUpgrade(opts: UpgradeOpts = {}): Promise<void> {
|
|
98
|
+
intro('promus upgrade')
|
|
99
|
+
|
|
100
|
+
const loaded = await findAndLoadConfig()
|
|
101
|
+
if (!loaded) {
|
|
102
|
+
cancel('No promus.config.ts found.')
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
const { config } = loaded
|
|
106
|
+
if (!config.identity.iNFT || !config.identity.agent) {
|
|
107
|
+
cancel('Config has no iNFT or agent.')
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
if (config.deployTarget !== 'sandbox' || !config.sandbox?.id || !config.sandbox.endpoint) {
|
|
111
|
+
cancel(
|
|
112
|
+
`Agent is not deployed to a sandbox. (deployTarget=${config.deployTarget ?? 'local'}). Run \`promus deploy\` first.`,
|
|
113
|
+
)
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
if (!config.brain.provider) {
|
|
117
|
+
cancel('Brain provider not configured. Run `promus model` first.')
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const mode: UpgradeMode = opts.reprovision ? 'reprovision' : 'in-place'
|
|
122
|
+
|
|
123
|
+
let resolved: ResolvedRef
|
|
124
|
+
try {
|
|
125
|
+
resolved = await resolvePromusRef(opts.ref)
|
|
126
|
+
} catch (e) {
|
|
127
|
+
cancel(
|
|
128
|
+
`could not resolve ref: ${(e as Error).message.slice(0, 200)}\nGitHub API may be unreachable. Pin a tag with \`--ref vX.Y.Z\` to skip the lookup.`,
|
|
129
|
+
)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Pre-flight tag visibility — closes the silent-success bug from 2026-05-03
|
|
134
|
+
// (see upgrade-silent-success-bug.md). Skip when we just resolved from
|
|
135
|
+
// `latest` (the API IS the source of truth) or for branch/SHA refs.
|
|
136
|
+
if (resolved.isTag && !resolved.resolvedFromLatest) {
|
|
137
|
+
try {
|
|
138
|
+
const exists = await checkTagExists(PROMUS_REPO_URL, resolved.ref)
|
|
139
|
+
if (!exists) {
|
|
140
|
+
cancel(
|
|
141
|
+
`Tag ${resolved.ref} is not visible on the remote yet (CI may still be propagating).\nTry again in 30s, or run \`promus upgrade ${LATEST_KEYWORD}\` to pick the most recent published release.`,
|
|
142
|
+
)
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
} catch (e) {
|
|
146
|
+
cancel(
|
|
147
|
+
`tag visibility check failed: ${(e as Error).message.slice(0, 200)}\nGitHub API may be unreachable. Set \`PROMUS_BOOTSTRAP_REF=main\` to skip tag verification for dev builds.`,
|
|
148
|
+
)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const refDisplay = formatResolvedRef(resolved)
|
|
154
|
+
|
|
155
|
+
if (!opts.yes) {
|
|
156
|
+
const message =
|
|
157
|
+
mode === 'reprovision'
|
|
158
|
+
? `Reprovision sandbox ${config.sandbox.id.slice(0, 8)} with a fresh container at ref=${refDisplay}? (~60-90s downtime, ~0.9 0G testnet)`
|
|
159
|
+
: `Upgrade sandbox ${config.sandbox.id.slice(0, 8)} in place to ref=${refDisplay}? (~30-60s downtime)`
|
|
160
|
+
const ok = await confirm({ message, initialValue: true })
|
|
161
|
+
if (isCancel(ok) || !ok) {
|
|
162
|
+
cancel('Aborted.')
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const contractAddress = config.identity.iNFT.contract as Address
|
|
168
|
+
const tokenId = BigInt(config.identity.iNFT.tokenId)
|
|
169
|
+
const agentAddress = config.identity.agent as Address
|
|
170
|
+
const sandboxId = config.sandbox.id
|
|
171
|
+
|
|
172
|
+
const operator = await loadOrPickOperatorSigner({
|
|
173
|
+
network: config.network,
|
|
174
|
+
hint: config.operator,
|
|
175
|
+
})
|
|
176
|
+
if (!operator) {
|
|
177
|
+
cancel('No operator wallet available; cannot decrypt keystore.')
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Pre-flight: Galileo deposit balance. The May 2 INSUFFICIENT_BALANCE event
|
|
182
|
+
// archived enigma; refusing up-front with a clear suggestion is much better
|
|
183
|
+
// UX than letting the upgrade run + fail mid-bootstrap.
|
|
184
|
+
if (!(await preflightProviderDeposit(operator))) {
|
|
185
|
+
await operator.close?.()
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const sUnlock = spinner()
|
|
190
|
+
sUnlock.start('Fetching keystore + decrypting via operator wallet')
|
|
191
|
+
let agentPrivkey: Hex
|
|
192
|
+
try {
|
|
193
|
+
agentPrivkey = await unlockAgentKeystore({
|
|
194
|
+
operator,
|
|
195
|
+
network: config.network,
|
|
196
|
+
contractAddress,
|
|
197
|
+
tokenId,
|
|
198
|
+
agentAddress,
|
|
199
|
+
})
|
|
200
|
+
sUnlock.stop('unlocked')
|
|
201
|
+
} catch (e) {
|
|
202
|
+
sUnlock.stop(`unlock failed: ${(e as Error).message.slice(0, 160)}`)
|
|
203
|
+
await operator.close?.()
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (mode === 'reprovision') {
|
|
208
|
+
await runReprovisionUpgrade({
|
|
209
|
+
operator,
|
|
210
|
+
agentPrivkey,
|
|
211
|
+
agentAddress,
|
|
212
|
+
contractAddress,
|
|
213
|
+
tokenId,
|
|
214
|
+
oldSandboxId: sandboxId,
|
|
215
|
+
config,
|
|
216
|
+
loadedPath: loaded.path,
|
|
217
|
+
resolved,
|
|
218
|
+
})
|
|
219
|
+
} else {
|
|
220
|
+
await runInPlaceUpgrade({
|
|
221
|
+
operator,
|
|
222
|
+
agentPrivkey,
|
|
223
|
+
agentAddress,
|
|
224
|
+
contractAddress,
|
|
225
|
+
tokenId,
|
|
226
|
+
sandboxId,
|
|
227
|
+
sandboxEndpoint: config.sandbox.endpoint,
|
|
228
|
+
iNFTNetwork: config.network,
|
|
229
|
+
brain: { provider: config.brain.provider as Address, model: config.brain.model ?? '' },
|
|
230
|
+
subname: config.subname,
|
|
231
|
+
plugins: config.plugins,
|
|
232
|
+
resolved,
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await operator.close?.()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
interface InPlaceUpgradeArgs {
|
|
240
|
+
operator: OperatorSigner
|
|
241
|
+
agentPrivkey: Hex
|
|
242
|
+
agentAddress: Address
|
|
243
|
+
contractAddress: Address
|
|
244
|
+
tokenId: bigint
|
|
245
|
+
sandboxId: string
|
|
246
|
+
sandboxEndpoint: string
|
|
247
|
+
iNFTNetwork: PromusNetwork
|
|
248
|
+
brain: { provider: Address; model: string }
|
|
249
|
+
/** Optional .0g subname forwarded into the harness handoff RuntimeConfig. */
|
|
250
|
+
subname?: string | null
|
|
251
|
+
/**
|
|
252
|
+
* Plugins enabled in the local config; threaded into the harness
|
|
253
|
+
* RuntimeConfig so the sandbox loads the same plugin set (telegram listener
|
|
254
|
+
* in particular). Without this the harness defaults to ['system','comms','onchain']
|
|
255
|
+
* and silently drops 'telegram' even when telegram-secrets are provisioned.
|
|
256
|
+
*/
|
|
257
|
+
plugins?: PromusPlugin[]
|
|
258
|
+
resolved: ResolvedRef
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function runInPlaceUpgrade(args: InPlaceUpgradeArgs): Promise<void> {
|
|
262
|
+
const operatorAccount = await args.operator.account()
|
|
263
|
+
const provider = new SandboxProviderClient({
|
|
264
|
+
endpoint: SANDBOX_PROVIDER_URL_GALILEO,
|
|
265
|
+
operator: operatorAccount,
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
const sBox = spinner()
|
|
269
|
+
sBox.start('Ensuring sandbox is started')
|
|
270
|
+
try {
|
|
271
|
+
await ensureSandboxStarted(provider, args.sandboxId, {
|
|
272
|
+
onProgress: msg => sBox.message(msg),
|
|
273
|
+
})
|
|
274
|
+
} catch (e) {
|
|
275
|
+
sBox.stop(`ensure-started failed: ${(e as Error).message.slice(0, 200)}`)
|
|
276
|
+
note(
|
|
277
|
+
[
|
|
278
|
+
'The sandbox could not be brought to started state.',
|
|
279
|
+
'If state is `error` or restore failed, run `promus upgrade --reprovision` to spin a fresh container.',
|
|
280
|
+
].join('\n'),
|
|
281
|
+
'recoverable',
|
|
282
|
+
)
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
sBox.message('probing container bootstrap mode')
|
|
287
|
+
const probedMode = await probeContainerBootstrapMode(provider, args.sandboxId)
|
|
288
|
+
if (!probedMode) {
|
|
289
|
+
sBox.stop('cannot determine container bootstrap mode (no promus install detected)')
|
|
290
|
+
note(
|
|
291
|
+
[
|
|
292
|
+
'Container has neither $HOME/promus/.git/ nor a global @promus/gateway binary.',
|
|
293
|
+
'The container may have been wiped or never bootstrapped successfully.',
|
|
294
|
+
'Try `promus upgrade --reprovision` to spin a fresh container.',
|
|
295
|
+
].join('\n'),
|
|
296
|
+
'recoverable',
|
|
297
|
+
)
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
const cliVersion = probedMode === 'npm' ? await resolveCliVersion() : undefined
|
|
301
|
+
sBox.message(`launching in-place upgrade to ref=${args.resolved.ref} (mode=${probedMode})`)
|
|
302
|
+
const { script } = buildUpgradeScript({
|
|
303
|
+
sandboxId: args.sandboxId,
|
|
304
|
+
operatorAddress: operatorAccount.address,
|
|
305
|
+
mode: probedMode,
|
|
306
|
+
ref: args.resolved.ref,
|
|
307
|
+
packageVersion: cliVersion,
|
|
308
|
+
})
|
|
309
|
+
let launchOut: string
|
|
310
|
+
try {
|
|
311
|
+
const launch = await provider.execInToolbox(args.sandboxId, { command: script, timeout: 60 })
|
|
312
|
+
launchOut = extractExecOutput(launch)
|
|
313
|
+
if (launch.exitCode !== 0) {
|
|
314
|
+
sBox.stop(`upgrade launch failed exitCode=${launch.exitCode}`)
|
|
315
|
+
note(launchOut.slice(0, 400), 'launch output')
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
} catch (e) {
|
|
319
|
+
sBox.stop(`execInToolbox failed: ${(e as Error).message.slice(0, 160)}`)
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Poll done/fail markers (mirror sandbox-provision Step 3 pattern).
|
|
324
|
+
sBox.message('upgrade running (git fetch + bun install + harness restart)')
|
|
325
|
+
const execRead = makeExecRead(provider, args.sandboxId)
|
|
326
|
+
// Lean poll: just FAIL + DONE markers (cheap `cat` of small files). The
|
|
327
|
+
// progress-log `tail` only attaches every 6th tick (~30s) since the
|
|
328
|
+
// consumer throttles its UX echo at 30s anyway. Saves ~5/6 of the
|
|
329
|
+
// signed-exec response payload through Daytona's HTTP channel.
|
|
330
|
+
const FAST_POLL = `echo --F--; cat ${UPGRADE_FAIL_MARKER} 2>/dev/null; echo --D--; cat ${UPGRADE_DONE_MARKER} 2>/dev/null`
|
|
331
|
+
const SLOW_POLL = `${FAST_POLL}; echo --P--; tail -n 1 ${UPGRADE_PROGRESS_LOG} 2>/dev/null`
|
|
332
|
+
const upgradeDeadline = Date.now() + 360_000 // 6 min ceiling for in-place
|
|
333
|
+
let tick = 0
|
|
334
|
+
let lastDone = ''
|
|
335
|
+
while (Date.now() < upgradeDeadline) {
|
|
336
|
+
const surfaceProgress = ++tick % 6 === 0
|
|
337
|
+
const out = await execRead(surfaceProgress ? SLOW_POLL : FAST_POLL)
|
|
338
|
+
const fail = sliceBetween(out, '--F--', '--D--')
|
|
339
|
+
const done = surfaceProgress ? sliceBetween(out, '--D--', '--P--') : sliceAfter(out, '--D--')
|
|
340
|
+
const failKeyword = UPGRADE_FAIL_KEYWORDS.find(k => fail.includes(k))
|
|
341
|
+
if (failKeyword) {
|
|
342
|
+
const log = await execRead(`tail -n 80 ${UPGRADE_PROGRESS_LOG} 2>/dev/null`)
|
|
343
|
+
sBox.stop(`upgrade-failed: ${failKeyword}`)
|
|
344
|
+
note(
|
|
345
|
+
[
|
|
346
|
+
`step failed: ${failKeyword}`,
|
|
347
|
+
'log tail:',
|
|
348
|
+
log.slice(-400),
|
|
349
|
+
'',
|
|
350
|
+
'You can retry with `promus upgrade` (the script is idempotent),',
|
|
351
|
+
'or fall back to `promus upgrade --reprovision` for a fresh container.',
|
|
352
|
+
].join('\n'),
|
|
353
|
+
'recoverable',
|
|
354
|
+
)
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
if (done.includes(UPGRADE_SUCCESS_MARKER_PREFIX)) {
|
|
358
|
+
lastDone = done
|
|
359
|
+
const pidLine =
|
|
360
|
+
done
|
|
361
|
+
.split('\n')
|
|
362
|
+
.find(l => l.includes(UPGRADE_SUCCESS_MARKER_PREFIX))
|
|
363
|
+
?.trim() ?? done.trim()
|
|
364
|
+
sBox.message(`upgrade complete (${pidLine})`)
|
|
365
|
+
break
|
|
366
|
+
}
|
|
367
|
+
if (surfaceProgress) {
|
|
368
|
+
const real = sliceAfter(out, '--P--').trim().split('\n').pop()
|
|
369
|
+
if (real) sBox.message(`upgrade: ${real}`)
|
|
370
|
+
}
|
|
371
|
+
await sleep(5000)
|
|
372
|
+
}
|
|
373
|
+
if (!lastDone.includes(UPGRADE_SUCCESS_MARKER_PREFIX)) {
|
|
374
|
+
const log = await execRead(`tail -n 80 ${UPGRADE_PROGRESS_LOG} 2>/dev/null`)
|
|
375
|
+
sBox.stop('upgrade timeout (6 min)')
|
|
376
|
+
note(`log tail:\n${log.slice(-400)}`, 'recoverable')
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Post-flight version verification — DONE marker only proves the inner
|
|
381
|
+
// script finished, not that git checkout moved HEAD. Read package.json
|
|
382
|
+
// from the container and compare. Skip for non-tag refs (no expectation).
|
|
383
|
+
const expected = expectedVersionFromRef(args.resolved)
|
|
384
|
+
if (expected !== null) {
|
|
385
|
+
const verifyPath =
|
|
386
|
+
probedMode === 'npm'
|
|
387
|
+
? '$HOME/.bun/install/global/node_modules/@promus/gateway/package.json'
|
|
388
|
+
: '$HOME/promus/packages/gateway/package.json'
|
|
389
|
+
const verifyOut = await execRead(`grep '"version"' ${verifyPath} | head -1`)
|
|
390
|
+
const m = verifyOut.match(/"version"\s*:\s*"([^"]+)"/)
|
|
391
|
+
if (!m) {
|
|
392
|
+
sBox.stop('post-flight verification failed: cannot parse package.json version')
|
|
393
|
+
note(
|
|
394
|
+
[
|
|
395
|
+
'The upgrade reported success but we could not read the deployed package.json.',
|
|
396
|
+
'Re-running `promus upgrade` should land cleanly. If this persists, file an issue.',
|
|
397
|
+
].join('\n'),
|
|
398
|
+
'recoverable',
|
|
399
|
+
)
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
const actual = m[1] ?? ''
|
|
403
|
+
if (actual !== expected) {
|
|
404
|
+
// v0.24.4: when npm `latest` is newer than the github release `latest`
|
|
405
|
+
// tag (common during a ship window where the tag was published seconds
|
|
406
|
+
// before the github release was cut), `npm install promus-cli@latest`
|
|
407
|
+
// pulls a NEWER version than `expected`. Treat newer-than-requested as
|
|
408
|
+
// a soft pass: print a note, continue handoff, don't bail out.
|
|
409
|
+
const cmpNewer = isSemverNewer(actual, expected)
|
|
410
|
+
if (cmpNewer) {
|
|
411
|
+
sBox.message(`harness landed ${actual} (newer than requested ${expected}); continuing`)
|
|
412
|
+
} else {
|
|
413
|
+
sBox.stop(`silent-success regression: expected ${expected}, got ${actual}`)
|
|
414
|
+
note(
|
|
415
|
+
[
|
|
416
|
+
`The harness reported success but is running ${actual} instead of ${expected}.`,
|
|
417
|
+
'This means git fetch may not have seen the tag yet. Re-running',
|
|
418
|
+
`\`promus upgrade --ref ${args.resolved.ref ?? 'latest'}\` should land it correctly,`,
|
|
419
|
+
'or `promus upgrade latest` to pick the most recent published release.',
|
|
420
|
+
].join('\n'),
|
|
421
|
+
'recoverable',
|
|
422
|
+
)
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
sBox.message(`verified harness version=${actual}`)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Re-handoff against the SAME endpoint (harness restarted with fresh keypair).
|
|
431
|
+
sBox.message('re-handing off agent privkey to restarted harness')
|
|
432
|
+
const sandboxClient = new SandboxClient({
|
|
433
|
+
endpoint: args.sandboxEndpoint,
|
|
434
|
+
sandboxId: args.sandboxId,
|
|
435
|
+
operator: operatorAccount,
|
|
436
|
+
})
|
|
437
|
+
const telegramSecretsPlain = await loadTelegramHandoffSecrets({
|
|
438
|
+
signer: args.operator,
|
|
439
|
+
agentAddress: args.agentAddress,
|
|
440
|
+
contractAddress: args.contractAddress,
|
|
441
|
+
tokenId: args.tokenId,
|
|
442
|
+
onNotice: msg => sBox.message(msg),
|
|
443
|
+
})
|
|
444
|
+
const inPlaceAgentId = iNFTAgentId({
|
|
445
|
+
contractAddress: args.contractAddress,
|
|
446
|
+
tokenId: args.tokenId,
|
|
447
|
+
})
|
|
448
|
+
const inPlaceProfileKeyHex = loadProfileScopeKeyHex(inPlaceAgentId)
|
|
449
|
+
if (!inPlaceProfileKeyHex) {
|
|
450
|
+
sBox.message('no cached PROFILE key; sandbox will boot without profile-slot anchoring')
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
await handoffAgentToGateway({
|
|
454
|
+
sandboxClient,
|
|
455
|
+
agentPrivkey: args.agentPrivkey,
|
|
456
|
+
agentAddress: args.agentAddress,
|
|
457
|
+
iNFTRef: { contract: args.contractAddress, tokenId: args.tokenId },
|
|
458
|
+
iNFTNetwork: args.iNFTNetwork,
|
|
459
|
+
brain: args.brain,
|
|
460
|
+
subname: args.subname,
|
|
461
|
+
plugins: args.plugins,
|
|
462
|
+
telegramSecrets: telegramSecretsPlain,
|
|
463
|
+
profileScopeKeyHex: inPlaceProfileKeyHex,
|
|
464
|
+
onProgress: msg => sBox.message(msg),
|
|
465
|
+
})
|
|
466
|
+
sBox.stop(`sandbox ${args.sandboxId.slice(0, 8)} ready @ ${args.sandboxEndpoint}`)
|
|
467
|
+
} catch (e) {
|
|
468
|
+
sBox.stop(`handoff failed: ${(e as Error).message.slice(0, 200)}`)
|
|
469
|
+
note(
|
|
470
|
+
[
|
|
471
|
+
'Container code rolled to the new ref but the agent privkey handoff did not complete.',
|
|
472
|
+
'The harness is back in Bootstrapping state. Re-run `promus upgrade` to retry the handoff,',
|
|
473
|
+
'or `promus upgrade --reprovision` to start fresh.',
|
|
474
|
+
].join('\n'),
|
|
475
|
+
'recoverable',
|
|
476
|
+
)
|
|
477
|
+
return
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
outro(
|
|
481
|
+
[
|
|
482
|
+
'',
|
|
483
|
+
` sandbox ${args.sandboxId} (unchanged)`,
|
|
484
|
+
` endpoint ${args.sandboxEndpoint} (unchanged)`,
|
|
485
|
+
` ref ${formatResolvedRef(args.resolved)}`,
|
|
486
|
+
'',
|
|
487
|
+
'Next: `promus` to chat (same harness endpoint, same agent EOA, new code)',
|
|
488
|
+
].join('\n'),
|
|
489
|
+
)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
interface ReprovisionUpgradeArgs {
|
|
493
|
+
operator: OperatorSigner
|
|
494
|
+
agentPrivkey: Hex
|
|
495
|
+
agentAddress: Address
|
|
496
|
+
contractAddress: Address
|
|
497
|
+
tokenId: bigint
|
|
498
|
+
oldSandboxId: string
|
|
499
|
+
config: NonNullable<Awaited<ReturnType<typeof findAndLoadConfig>>>['config']
|
|
500
|
+
loadedPath: string
|
|
501
|
+
resolved: ResolvedRef
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function runReprovisionUpgrade(args: ReprovisionUpgradeArgs): Promise<void> {
|
|
505
|
+
const operatorAccount = await args.operator.account()
|
|
506
|
+
const provider = new SandboxProviderClient({
|
|
507
|
+
endpoint: SANDBOX_PROVIDER_URL_GALILEO,
|
|
508
|
+
operator: operatorAccount,
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
const sDel = spinner()
|
|
512
|
+
sDel.start(`Deleting old sandbox ${args.oldSandboxId}`)
|
|
513
|
+
try {
|
|
514
|
+
await provider.deleteSandbox(args.oldSandboxId)
|
|
515
|
+
sDel.stop(`old sandbox ${args.oldSandboxId.slice(0, 8)} deleted`)
|
|
516
|
+
} catch (e) {
|
|
517
|
+
sDel.stop(`delete failed: ${(e as Error).message.slice(0, 160)}`)
|
|
518
|
+
note(
|
|
519
|
+
[
|
|
520
|
+
'Old sandbox could not be deleted but provisioning a fresh one is still safe.',
|
|
521
|
+
'You can manually delete the orphan via the provider dashboard later.',
|
|
522
|
+
].join('\n'),
|
|
523
|
+
'continuing',
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const sBox = spinner()
|
|
528
|
+
sBox.start('Provisioning fresh sandbox container')
|
|
529
|
+
const telegramSecretsPlain = await loadTelegramHandoffSecrets({
|
|
530
|
+
signer: args.operator,
|
|
531
|
+
agentAddress: args.agentAddress,
|
|
532
|
+
contractAddress: args.contractAddress,
|
|
533
|
+
tokenId: args.tokenId,
|
|
534
|
+
onNotice: msg => sBox.message(msg),
|
|
535
|
+
})
|
|
536
|
+
const reprovisionAgentId = iNFTAgentId({
|
|
537
|
+
contractAddress: args.contractAddress,
|
|
538
|
+
tokenId: args.tokenId,
|
|
539
|
+
})
|
|
540
|
+
const reprovisionProfileKeyHex = loadProfileScopeKeyHex(reprovisionAgentId)
|
|
541
|
+
if (!reprovisionProfileKeyHex) {
|
|
542
|
+
sBox.message('no cached PROFILE key; fresh sandbox will boot without profile-slot anchoring')
|
|
543
|
+
}
|
|
544
|
+
let sandboxResult: Awaited<ReturnType<typeof runSandboxProvision>>
|
|
545
|
+
const boxCtl = new BootstrapProgressController({
|
|
546
|
+
spinner: sBox,
|
|
547
|
+
cliVersion: await resolveCliVersion(),
|
|
548
|
+
startedMsg: 'fresh sandbox started, running bootstrap',
|
|
549
|
+
})
|
|
550
|
+
try {
|
|
551
|
+
sandboxResult = await runSandboxProvision({
|
|
552
|
+
operator: args.operator,
|
|
553
|
+
agentPrivkey: args.agentPrivkey,
|
|
554
|
+
agentAddress: args.agentAddress,
|
|
555
|
+
iNFTRef: { contract: args.contractAddress, tokenId: args.tokenId },
|
|
556
|
+
brain: {
|
|
557
|
+
provider: args.config.brain.provider as Address,
|
|
558
|
+
model: args.config.brain.model ?? '',
|
|
559
|
+
},
|
|
560
|
+
iNFTNetwork: args.config.network,
|
|
561
|
+
name: args.config.subname || 'promus',
|
|
562
|
+
ref: args.resolved.ref,
|
|
563
|
+
subname: args.config.subname,
|
|
564
|
+
plugins: args.config.plugins,
|
|
565
|
+
telegramSecrets: telegramSecretsPlain,
|
|
566
|
+
profileScopeKeyHex: reprovisionProfileKeyHex,
|
|
567
|
+
onProgress: boxCtl.onProgress,
|
|
568
|
+
onStageEvent: boxCtl.onStageEvent,
|
|
569
|
+
onTick: boxCtl.onTick,
|
|
570
|
+
})
|
|
571
|
+
boxCtl.finalize(`sandbox ${sandboxResult.sandboxId} ready @ ${sandboxResult.endpoint}`, msg =>
|
|
572
|
+
log.step(msg),
|
|
573
|
+
)
|
|
574
|
+
} catch (e) {
|
|
575
|
+
boxCtl.fail(`re-provision failed: ${(e as Error).message.slice(0, 200)}`, msg => log.error(msg))
|
|
576
|
+
note(
|
|
577
|
+
[
|
|
578
|
+
'Old sandbox was deleted but the new one did not provision.',
|
|
579
|
+
'Identity + funds + memory all safe on chain / 0G Storage.',
|
|
580
|
+
'Re-run `promus upgrade --reprovision` after fixing the issue, or `promus deploy` to start fresh.',
|
|
581
|
+
].join('\n'),
|
|
582
|
+
'recoverable (agent offline)',
|
|
583
|
+
)
|
|
584
|
+
return
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (args.config.subname) {
|
|
588
|
+
const sEp = spinner()
|
|
589
|
+
sEp.start(`Updating agent:endpoint on ${args.config.subname}.promus.0g`)
|
|
590
|
+
try {
|
|
591
|
+
await publishSandboxEndpoint({
|
|
592
|
+
subname: args.config.subname,
|
|
593
|
+
agentPrivkey: args.agentPrivkey,
|
|
594
|
+
endpoint: sandboxResult.endpoint,
|
|
595
|
+
})
|
|
596
|
+
sEp.stop('agent:endpoint updated')
|
|
597
|
+
} catch (e) {
|
|
598
|
+
sEp.stop(`agent:endpoint update failed: ${(e as Error).message.slice(0, 120)}`)
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const updated = {
|
|
603
|
+
...args.config,
|
|
604
|
+
sandbox: {
|
|
605
|
+
...args.config.sandbox,
|
|
606
|
+
id: sandboxResult.sandboxId,
|
|
607
|
+
providerAddress: sandboxResult.providerAddress,
|
|
608
|
+
endpoint: sandboxResult.endpoint,
|
|
609
|
+
snapshotName: sandboxResult.snapshotName,
|
|
610
|
+
},
|
|
611
|
+
}
|
|
612
|
+
await writeConfigTs(args.loadedPath, updated, { subname: updated.subname ?? null })
|
|
613
|
+
|
|
614
|
+
outro(
|
|
615
|
+
[
|
|
616
|
+
'',
|
|
617
|
+
` old sandbox ${args.oldSandboxId}`,
|
|
618
|
+
` new sandbox ${sandboxResult.sandboxId}`,
|
|
619
|
+
` endpoint ${sandboxResult.endpoint}`,
|
|
620
|
+
` ref ${formatResolvedRef(args.resolved)}`,
|
|
621
|
+
'',
|
|
622
|
+
'Next: `promus` to chat (now routes through the new harness)',
|
|
623
|
+
].join('\n'),
|
|
624
|
+
)
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* v0.24.4: compare two semver-shaped strings as `a > b`. Strips a leading `v`
|
|
629
|
+
* if present. Returns true when `a` is strictly newer than `b`. Used by the
|
|
630
|
+
* post-flight verifier so a newer-than-requested install (npm latest > github
|
|
631
|
+
* release latest during a ship window) doesn't fire the "silent-success
|
|
632
|
+
* regression" warning. Lightweight — does not handle prerelease tags.
|
|
633
|
+
*/
|
|
634
|
+
function isSemverNewer(a: string, b: string): boolean {
|
|
635
|
+
const parse = (s: string): number[] => {
|
|
636
|
+
const clean = s.replace(/^v/, '').split('-')[0] ?? ''
|
|
637
|
+
return clean.split('.').map(p => Number.parseInt(p, 10) || 0)
|
|
638
|
+
}
|
|
639
|
+
const pa = parse(a)
|
|
640
|
+
const pb = parse(b)
|
|
641
|
+
for (let i = 0; i < 3; i++) {
|
|
642
|
+
const av = pa[i] ?? 0
|
|
643
|
+
const bv = pb[i] ?? 0
|
|
644
|
+
if (av > bv) return true
|
|
645
|
+
if (av < bv) return false
|
|
646
|
+
}
|
|
647
|
+
return false
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function sliceBetween(s: string, start: string, end: string): string {
|
|
651
|
+
const i = s.indexOf(start)
|
|
652
|
+
if (i < 0) return ''
|
|
653
|
+
const j = s.indexOf(end, i + start.length)
|
|
654
|
+
if (j < 0) return s.slice(i + start.length)
|
|
655
|
+
return s.slice(i + start.length, j)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function sliceAfter(s: string, marker: string): string {
|
|
659
|
+
const i = s.indexOf(marker)
|
|
660
|
+
return i < 0 ? '' : s.slice(i + marker.length)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function sleep(ms: number): Promise<void> {
|
|
664
|
+
return new Promise(r => setTimeout(r, ms))
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Single execInToolbox round-trip that probes the container's bootstrap mode
|
|
669
|
+
* by checking filesystem state. Returns 'git' if `$HOME/promus/.git/` exists,
|
|
670
|
+
* 'npm' if global @promus/gateway binary exists, or null if neither.
|
|
671
|
+
*
|
|
672
|
+
* Used by `runInPlaceUpgrade` so the upgrade script ships only the path it
|
|
673
|
+
* actually needs (auto-detect inside the script blew the 5KB Daytona cap).
|
|
674
|
+
*/
|
|
675
|
+
export async function probeContainerBootstrapMode(
|
|
676
|
+
provider: SandboxProviderClient,
|
|
677
|
+
sandboxId: string,
|
|
678
|
+
): Promise<BootstrapMode | null> {
|
|
679
|
+
// Routed through makeExecRead so the `if [...]; then ...; fi` runs under
|
|
680
|
+
// a real bash. Daytona's exec is argv-only; without the wrap the probe
|
|
681
|
+
// tokenises `if` as argv[0] and returns empty. makeExecRead also swallows
|
|
682
|
+
// exec errors, returning '' on failure — matches the previous catch arm.
|
|
683
|
+
const execRead = makeExecRead(provider, sandboxId)
|
|
684
|
+
const out = await execRead(
|
|
685
|
+
`if [ -d "$HOME/promus/.git" ]; then echo MODE=git; elif [ -x "$HOME/.bun/install/global/node_modules/.bin/@promus/gateway" ]; then echo MODE=npm; else echo MODE=none; fi`,
|
|
686
|
+
)
|
|
687
|
+
if (out.includes('MODE=git')) return 'git'
|
|
688
|
+
if (out.includes('MODE=npm')) return 'npm'
|
|
689
|
+
return null
|
|
690
|
+
}
|