@manifest-network/manifest-agent-core 0.14.0 → 0.15.0
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/dist/close-lease.d.ts.map +1 -1
- package/dist/close-lease.js +15 -3
- package/dist/close-lease.js.map +1 -1
- package/dist/deploy-app.d.ts +2 -2
- package/dist/deploy-app.d.ts.map +1 -1
- package/dist/deploy-app.js +82 -35
- package/dist/deploy-app.js.map +1 -1
- package/dist/deploy-app.test-d.d.ts +1 -0
- package/dist/deploy-app.test-d.js +11 -0
- package/dist/deploy-app.test-d.js.map +1 -0
- package/dist/guarded-fetch.d.ts +2 -0
- package/dist/guarded-fetch.js +2 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -2
- package/dist/internals/cancellation.d.ts +57 -0
- package/dist/internals/cancellation.d.ts.map +1 -0
- package/dist/internals/cancellation.js +79 -0
- package/dist/internals/cancellation.js.map +1 -0
- package/dist/internals/inspect-image.js +1 -1
- package/dist/internals/inspect-image.js.map +1 -1
- package/dist/internals/render-intent-recap.d.ts +13 -11
- package/dist/internals/render-intent-recap.d.ts.map +1 -1
- package/dist/internals/render-intent-recap.js +5 -4
- package/dist/internals/render-intent-recap.js.map +1 -1
- package/dist/internals/spec-normalize.d.ts +34 -28
- package/dist/internals/spec-normalize.d.ts.map +1 -1
- package/dist/internals/spec-normalize.js +28 -22
- package/dist/internals/spec-normalize.js.map +1 -1
- package/dist/manage-domain.d.ts.map +1 -1
- package/dist/manage-domain.js +34 -8
- package/dist/manage-domain.js.map +1 -1
- package/dist/node_modules/@vitest/pretty-format/dist/index.js +888 -0
- package/dist/node_modules/@vitest/pretty-format/dist/index.js.map +1 -0
- package/dist/node_modules/@vitest/runner/dist/chunk-artifact.js +1500 -0
- package/dist/node_modules/@vitest/runner/dist/chunk-artifact.js.map +1 -0
- package/dist/node_modules/@vitest/runner/dist/index.js +1 -0
- package/dist/node_modules/@vitest/runner/dist/utils.js +1 -0
- package/dist/node_modules/@vitest/utils/dist/chunk-pathe.M-eThtNZ.js +82 -0
- package/dist/node_modules/@vitest/utils/dist/chunk-pathe.M-eThtNZ.js.map +1 -0
- package/dist/node_modules/@vitest/utils/dist/display.js +558 -0
- package/dist/node_modules/@vitest/utils/dist/display.js.map +1 -0
- package/dist/node_modules/@vitest/utils/dist/helpers.js +68 -0
- package/dist/node_modules/@vitest/utils/dist/helpers.js.map +1 -0
- package/dist/node_modules/@vitest/utils/dist/source-map.js +95 -0
- package/dist/node_modules/@vitest/utils/dist/source-map.js.map +1 -0
- package/dist/node_modules/@vitest/utils/dist/timers.js +20 -0
- package/dist/node_modules/@vitest/utils/dist/timers.js.map +1 -0
- package/dist/node_modules/tinyrainbow/dist/index.js +86 -0
- package/dist/node_modules/tinyrainbow/dist/index.js.map +1 -0
- package/dist/node_modules/vite/dist/node/module-runner.js +22 -0
- package/dist/node_modules/vite/dist/node/module-runner.js.map +1 -0
- package/dist/node_modules/vitest/dist/index.js +6 -0
- package/dist/troubleshoot.d.ts.map +1 -1
- package/dist/troubleshoot.js +11 -1
- package/dist/troubleshoot.js.map +1 -1
- package/dist/types.d.ts +30 -51
- package/dist/types.d.ts.map +1 -1
- package/package.json +12 -6
- package/dist/internals/build-fred-input.d.ts +0 -46
- package/dist/internals/build-fred-input.d.ts.map +0 -1
- package/dist/internals/build-fred-input.js +0 -160
- package/dist/internals/build-fred-input.js.map +0 -1
package/dist/deploy-app.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy-app.js","names":["fredDeployApp","decodeLeaseState"],"sources":["../src/deploy-app.ts"],"sourcesContent":["/**\n * Public entry point: orchestrate a Manifest-Network app deployment from\n * a typed `DeploySpec` through the plan/confirm/broadcast/save flow.\n *\n * Architect's α-locked composition (post-PR-3 sub-plan Q1):\n *\n * Happy path: `fredDeployApp` (workspace MCP-tool function) is called\n * atomically for create-lease + manifest upload + (optional) set-\n * item-custom-domain. agent-core wraps the call with planning, user\n * confirmation, progress events, and post-success persistence.\n *\n * Recovery path: when fred's atomic deployApp throws or the lease\n * reaches a non-recoverable state, agent-core renders a recovery\n * prompt (typed `RecoveryOption[]`), invokes `onFailure`, and\n * dispatches the user's `RecoveryChoice` to inline closures that\n * call core's decomposed primitives (`setItemCustomDomain` for\n * `retry_set_domain`; `stopApp` for `close_lease`).\n *\n * E-hybrid runtime-context (post-PR-3 sub-plan Q5):\n *\n * `opts: DeployAppOptions` carries `clientManager` (chain ops),\n * `walletProvider` (ADR-036 auth-token construction), optional\n * `fetchFn` (HTTP override for fred's upload), and the chain-data /\n * denomMap injection for humanization. agent-core composes the\n * auth-token callbacks internally from `walletProvider` so callers\n * don't need to know about ADR-036 plumbing.\n *\n * Auth-callback construction follows fred's `AuthTokenService` pattern\n * (verified against `packages/fred/src/http/auth.ts` per TL2.1 silent-\n * fix discipline):\n *\n * 1. `timestamps.next()` → monotonic replay-safe timestamp.\n * 2. `createSignMessage(address, leaseUuid, timestamp)` → message.\n * 3. `walletProvider.signArbitrary(address, message)` → `{ pub_key,\n * signature }` (cosmjs convention; `pub_key.value` is base64).\n * 4. `createAuthToken(address, leaseUuid, timestamp, pub_key.value,\n * signature[, metaHashHex])` → token string.\n */\n\nimport {\n cosmosEstimateFee,\n ManifestMCPError,\n ManifestMCPErrorCode,\n resolveSku,\n type SkuCandidate,\n setItemCustomDomain,\n stopApp,\n} from '@manifest-network/manifest-mcp-core';\nimport {\n AuthTimestampTracker,\n buildManifestPreview,\n type ConnectionDetails,\n checkDeploymentReadiness,\n createAuthToken,\n createLeaseDataSignMessage,\n createSignMessage,\n type DeployAppResult as FredDeployAppResult,\n type FredLeaseStatus,\n fetchActiveLease,\n deployApp as fredDeployApp,\n pollLeaseUntilReady,\n resolveProviderUrl,\n uploadLeaseData,\n waitForAppReady,\n} from '@manifest-network/manifest-mcp-fred';\nimport {\n buildFredDeployInput,\n buildManifestPreviewInput,\n} from './internals/build-fred-input.js';\nimport { classifyDeployError } from './internals/classify-deploy-error.js';\nimport {\n classifyDeployResponse,\n type DeployResponseShape,\n} from './internals/classify-deploy-response.js';\nimport {\n extractRunningEndpoints,\n formatEndpointAsUrl,\n normalizeFredUrl,\n} from './internals/connection.js';\nimport { evaluateReadinessFromFredResponse } from './internals/evaluate-readiness-from-fred.js';\nimport {\n EMPTY_DENOM_MAP,\n loadChainDenomMap,\n} from './internals/humanize-denom.js';\nimport { decode as decodeLeaseState } from './internals/lease-state.js';\nimport { renderDeploymentPlan } from './internals/render-deployment-plan.js';\nimport { renderIntentRecap } from './internals/render-intent-recap.js';\nimport { renderPartialSuccessPrompt } from './internals/render-partial-success-prompt.js';\nimport {\n isStackSpec,\n summarizeSpec,\n validateSpec,\n} from './internals/spec-normalize.js';\nimport type {\n DenomMap,\n DeployAppCallbacks,\n DeployAppOptions,\n DeployResult,\n DeploySpec,\n FailureEnvelope,\n FeeEstimate,\n LeaseStateName,\n Plan,\n Readiness,\n RecoveryChoice,\n RecoveryOption,\n RecoveryOptionId,\n ServiceDef,\n SingleServiceSpec,\n StackSpec,\n} from './types.js';\n\n/**\n * Orchestrate a deployment. See module-level docstring for the architect-\n * locked composition + E-hybrid runtime-context contract.\n *\n * @throws `ManifestMCPError(INVALID_CONFIG)` for spec / wallet validation.\n * @throws `ManifestMCPError(OPERATION_CANCELLED)` when `onConfirm` returns\n * `'no'` or `onPlan` returns `'cancel'` (deliberate user cancellation —\n * ENG-272).\n *\n * Errors from fred's broadcast or core's recovery primitives surface as\n * typed `ManifestMCPError`s. Partial-success failures with applicable\n * recovery options route through `onFailure(envelope, options)` — the\n * callback's return value drives recovery dispatch via the inline\n * closures in `dispatchRecovery`. Non-partial or inform-only failures\n * (no recovery choices to present, per `handleBroadcastFailure`'s F3\n * branch) throw directly as `ManifestMCPError(TX_FAILED)` without\n * invoking `onFailure`.\n */\nexport async function deployApp(\n spec: DeploySpec,\n callbacks: DeployAppCallbacks,\n opts: DeployAppOptions,\n): Promise<DeployResult> {\n // --- Input validation -----------------------------------------------\n try {\n validateSpec(spec);\n } catch (err) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n err instanceof Error ? err.message : `Invalid spec: ${String(err)}`,\n );\n }\n if (typeof opts.walletProvider.signArbitrary !== 'function') {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n 'opts.walletProvider must implement signArbitrary for ADR-036 auth tokens.',\n );\n }\n\n // --- Address-source consistency guard -------------------------------\n // Copilot review fix (PR #58 r3248900328): `opts.walletProvider` and\n // `opts.clientManager` are independently-injected runtime objects.\n // The readiness check + ADR-036 auth-token signing read the address\n // from `walletProvider`; fred's atomic `deployApp` (create-lease +\n // manifest upload) reads it from `clientManager`. If the two are\n // bound to different wallets (misconfiguration / copy-paste in\n // host-surface composition / multi-tenant test rig), readiness is\n // evaluated for wallet A while create-lease + upload execute as\n // wallet B — orphaning a lease on wallet B with auth tokens signed\n // by wallet A (provider auth-fails after the chain tx confirms).\n // Resolve both up-front, fail fast on mismatch, then reuse the\n // single value as the canonical `tenantAddress` for the rest of\n // the orchestration.\n const walletAddress = await opts.walletProvider.getAddress();\n const clientAddress = await opts.clientManager.getAddress();\n if (walletAddress !== clientAddress) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `opts.walletProvider and opts.clientManager are bound to different addresses ` +\n `(walletProvider=${walletAddress}, clientManager=${clientAddress}); they must reference the same wallet ` +\n `to avoid creating an orphaned lease on the clientManager wallet when ADR-036 auth (signed by walletProvider) fails.`,\n );\n }\n const tenantAddress = walletAddress;\n\n // --- Resolve denom map for humanization -----------------------------\n // I/O at orchestrator boundary (Path-Bii principle): callers may\n // pre-load via `denomMap`, point at `chainDataFile`, or omit both\n // (no-op map → raw on-chain denom rendering downstream).\n const denomMap: DenomMap =\n opts.denomMap ??\n (opts.chainDataFile\n ? await loadChainDenomMap(opts.chainDataFile)\n : EMPTY_DENOM_MAP);\n\n // --- Active-chain detection -----------------------------------------\n // The active chain (testnet / mainnet) drives intent-recap's mainnet\n // warning + parts of the Plan rendering. CosmosClientManager exposes\n // the bound chainId; we map to the canonical user-facing name.\n const chainId = opts.clientManager.getConfig().chainId;\n const activeChain: 'testnet' | 'mainnet' = /mainnet|main/i.test(chainId)\n ? 'mainnet'\n : 'testnet';\n\n // --- Readiness evaluation -------------------------------------------\n // fred's checkDeploymentReadiness takes (queryClient, address, input).\n // `tenantAddress` was resolved + validated as consistent across\n // walletProvider/clientManager in the address-source guard above.\n const queryClient = await opts.clientManager.getQueryClient();\n\n // --- SKU pin resolution (ENG-258) -----------------------------------\n // Resolve the requested `size` to a single concrete (skuUuid,\n // providerUuid) pin ONCE, BEFORE readiness/plan/fee/broadcast, so all\n // four reference the same SKU. Ambiguity routes through `onResolveSku`\n // (interactive) or re-throws SKU_AMBIGUOUS (headless). Defined as a\n // closure so it captures `queryClient` + `callbacks` and can be reused\n // by the post-edit re-plan branch (an edit can change size/provider).\n //\n // Returns `{ pin, elicited }` where `elicited` is true ONLY when\n // `onResolveSku` was actually invoked (i.e. an ambiguous-name\n // interactivity happened). The caller uses `elicited` to decide whether\n // to stamp the chosen pin onto `confirmedSpec` — stamping is only\n // needed to suppress a re-elicit on the post-edit re-plan; non-elicited\n // resolutions (unique-name or UUID-direct) don't need it and shouldn't\n // carry a stale pin if the user later issues a `replace_spec` edit that\n // changes the size.\n const resolvePin = async (\n s: DeploySpec,\n ): Promise<{ pin: SkuCandidate; elicited: boolean }> => {\n const providerUuid = requestedProviderUuid(s);\n const skuUuid = requestedSkuUuid(s);\n try {\n const pin = await resolveSku(queryClient, {\n size: requestedSize(s),\n ...(providerUuid !== undefined ? { providerUuid } : {}),\n ...(skuUuid !== undefined ? { skuUuid } : {}),\n });\n return { pin, elicited: false };\n } catch (err) {\n if (\n err instanceof ManifestMCPError &&\n err.code === ManifestMCPErrorCode.SKU_AMBIGUOUS &&\n callbacks.onResolveSku\n ) {\n const candidates = (err.details?.candidates as SkuCandidate[]) ?? [];\n callbacks.onProgress?.({ kind: 'sku_ambiguous', candidates });\n const pick = await callbacks.onResolveSku(candidates);\n const pin = await resolveSku(queryClient, {\n size: requestedSize(s),\n skuUuid: pick.skuUuid,\n providerUuid: pick.providerUuid,\n });\n return { pin, elicited: true };\n }\n throw err;\n }\n };\n let { pin: pinned, elicited: pinElicited } = await resolvePin(spec);\n\n // FIX 1 (ENG-258 review): `pinned.name` is the RESOLVED SKU's on-chain\n // name. When a deploy is pinned by `skuUuid` (or by provider) whose\n // on-chain name differs from the user's requested `size` (or size was\n // omitted → defaulted to 'small'), `requestedSize(spec)` no longer\n // matches the resolved SKU. fred's `evaluateReadiness` gate\n // (`skuCandidates.some(c => c.name === inputs.size)`) would then fail\n // and block a valid pin. Thread `pinned.name` — not `requestedSize` —\n // as the canonical SKU name into every downstream consumer (readiness,\n // plan render, fred input, persisted manifest). `requestedSize(spec)`\n // survives ONLY as the input to `resolvePin` (the user's request).\n const readinessRaw = await checkDeploymentReadiness(\n queryClient,\n tenantAddress,\n {\n image: primaryImage(spec),\n size: pinned.name,\n providerUuid: pinned.providerUuid,\n skuUuid: pinned.skuUuid,\n },\n );\n // `readiness` is `let`-bound because the post-edit recompute branch\n // re-evaluates it against the edited spec (Copilot r3267373084 — see\n // the recall block inside the `onPlan` `verdict !== 'confirm'` arm).\n let readiness: Readiness = evaluateReadinessFromFredResponse(\n readinessRaw,\n opts.clientManager.getConfig().gasPrice ?? '1umfx',\n denomMap,\n tenantAddress,\n );\n callbacks.onProgress?.({ kind: 'readiness_evaluated', readiness });\n if (readiness.status === 'block') {\n const envelope: FailureEnvelope = {\n outcome: 'failed',\n reason: `Readiness check failed: ${readiness.reasons.join('; ')}`,\n };\n // F3 fix: align with verify-recover's pattern — inform-only branch\n // (no recovery choices available) throws directly without calling\n // onFailure. Caller surfaces the error via the thrown\n // ManifestMCPError; no choice to present.\n void envelope;\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `Readiness check failed: ${readiness.reasons.join('; ')}`,\n );\n }\n\n // --- Plan assembly --------------------------------------------------\n // Build manifest preview (provides meta_hash for Plan + later save).\n // These are `let`-bound because the onPlan callback may return a\n // PlanEdit that triggers a re-plan (C2 fix below — single-iteration\n // plan-edit must recompute preview/summary/fees/block against the\n // edited spec; otherwise the manifest persistence at step 16 uses\n // the stale pre-edit preview).\n let preview = await buildManifestPreview(\n buildManifestPreviewInput(spec, requestedSize(spec)),\n );\n\n // Fee estimation for create-lease (always) + set-item-custom-domain\n // (when customDomain set). Lean port: cosmosEstimateFee invocation\n // details encapsulated in a helper to keep this fn focused on flow.\n let summary = summarizeSpec(spec);\n let fees = await estimateFees(\n opts,\n spec,\n preview.meta_hash_hex,\n pinned.skuUuid,\n );\n let plan: Plan = { summary, readiness, fees };\n\n // --- Render plan + onPlan callback ----------------------------------\n // FIX 2 (ENG-258 review): stamp the resolved pin identity onto the\n // working spec ONLY when an ambiguous SKU was resolved interactively\n // via `onResolveSku` (i.e. `pinElicited === true`). The stamp prevents\n // re-eliciting on the post-edit re-plan: an `edit_env` edit spreads\n // the prior spec and preserves the stamped skuUuid/providerUuid, so\n // the by-UUID second resolve skips the ambiguity entirely.\n //\n // When the SKU was resolved by a UNIQUE name or by explicit UUID\n // (non-elicited paths), the original `spec` already carries the right\n // identity — stamping is unnecessary and risks locking in a stale pin\n // if a `replace_spec` edit changes `size` (that edit replaces the\n // spec wholesale anyway, but avoiding the stamp keeps the non-elicited\n // path behaviorally minimal).\n let confirmedSpec: DeploySpec = pinElicited\n ? { ...spec, skuUuid: pinned.skuUuid, providerUuid: pinned.providerUuid }\n : spec;\n const block = renderDeploymentPlan({\n plan,\n denomMap,\n image: primaryImage(spec),\n // FIX 1: show the RESOLVED SKU name (honest when pinned by uuid).\n size: pinned.name,\n metaHash: preview.meta_hash_hex,\n customDomain: customDomainOf(spec),\n customDomainService: customDomainServiceOf(spec),\n providerUuid: pinned.providerUuid,\n });\n callbacks.onProgress?.({ kind: 'deployment_plan_rendered', block });\n if (callbacks.onPlan) {\n const verdict = await callbacks.onPlan(plan);\n if (verdict === 'cancel') {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.OPERATION_CANCELLED,\n 'User cancelled deployment at plan step.',\n );\n }\n if (verdict !== 'confirm') {\n // PlanEdit — apply edits to the spec, then re-plan against the\n // edited spec so downstream consumers (intent recap, fred input,\n // manifest persistence) all see the post-edit values.\n //\n // C2 fix (post-edit propagation gap): the prior single-iteration\n // implementation updated `confirmedSpec` but kept `preview` /\n // `summary` / `fees` / `plan` based on the original spec, which\n // caused the manifest persistence at step 16 to record the stale\n // pre-edit `meta_hash_hex` / `manifest_json` while fred's\n // deployApp broadcast used the edited spec — a real mismatch.\n // Re-planning closes the gap. Multi-iteration plan-edit (loop\n // back to onPlan with the new plan) remains a PR-3.x follow-up;\n // this fix addresses single-iteration freshness only.\n confirmedSpec = applyPlanEdit(confirmedSpec, verdict);\n // Copilot review fix (PR #58 r3249684686): re-validate the post-\n // edit spec at the agent-core boundary. `validateSpec` runs once\n // on the original input at the top of `deployApp`; without this\n // second invocation a `replace_spec` edit returning an invalid\n // spec (portless single-service, out-of-range port, stack-\n // without-services, stack-with-customDomain-missing-serviceName,\n // etc.) flows through to `buildManifestPreview` / fred's\n // broadcast and surfaces only as a mid-orchestration error.\n // Placed BEFORE the recompute so we don't spend a\n // `buildManifestPreview` round-trip on a known-bad spec. Wraps\n // `TypeError` from `validateSpec` into `INVALID_CONFIG` to match\n // the initial-input-validation convention at the top of this fn.\n try {\n validateSpec(confirmedSpec);\n } catch (err) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n err instanceof Error\n ? `Post-edit spec failed validation: ${err.message}`\n : `Post-edit spec failed validation: ${String(err)}`,\n );\n }\n // Copilot review fix (PR #58 r3267373084): readiness recall.\n // The original-spec `readiness` (captured pre-`onPlan`) gates\n // SKU + credit-balance pre-flight; a `replace_spec` /\n // `edit_env` edit that changes `image` or `size` can produce a\n // different readiness outcome. Without this recall, the\n // post-edit `plan` still carries the original-spec readiness,\n // which mis-renders the plan and may bypass a `status: 'block'`\n // condition specific to the edited shape.\n //\n // ENG-185 #1 (sub-PR B): the always-`'ok'` stub\n // `evaluateReadinessFromRaw` has been replaced by\n // `evaluateReadinessFromFredResponse` (the canonical evaluator\n // wired through the snake_case → camelCase translator). Both\n // call sites now fire the `status === 'block'` short-circuit\n // correctly (initial-spec L207 + post-edit recall below).\n // Re-resolve the SKU pin for the edited spec (ENG-258): an edit can\n // change `size` / `providerUuid`, so the pin threaded into the\n // post-edit readiness/fee/plan/broadcast must reflect the edit.\n // Track elicitation again: if the post-edit resolve is also\n // interactive, stamp the new pin so a further re-plan won't\n // re-elicit for the same choice.\n ({ pin: pinned, elicited: pinElicited } =\n await resolvePin(confirmedSpec));\n if (pinElicited) {\n confirmedSpec = {\n ...confirmedSpec,\n skuUuid: pinned.skuUuid,\n providerUuid: pinned.providerUuid,\n };\n }\n const editedReadinessRaw = await checkDeploymentReadiness(\n queryClient,\n tenantAddress,\n {\n image: primaryImage(confirmedSpec),\n // FIX 1: canonical resolved SKU name, not the user's requested size.\n size: pinned.name,\n providerUuid: pinned.providerUuid,\n skuUuid: pinned.skuUuid,\n },\n );\n readiness = evaluateReadinessFromFredResponse(\n editedReadinessRaw,\n opts.clientManager.getConfig().gasPrice ?? '1umfx',\n denomMap,\n tenantAddress,\n );\n callbacks.onProgress?.({ kind: 'readiness_evaluated', readiness });\n if (readiness.status === 'block') {\n // Same fail-fast as the original-spec readiness gate above.\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `Post-edit readiness check failed: ${readiness.reasons.join('; ')}`,\n );\n }\n preview = await buildManifestPreview(\n buildManifestPreviewInput(confirmedSpec, requestedSize(confirmedSpec)),\n );\n summary = summarizeSpec(confirmedSpec);\n fees = await estimateFees(\n opts,\n confirmedSpec,\n preview.meta_hash_hex,\n pinned.skuUuid,\n );\n plan = { summary, readiness, fees };\n // Copilot review fix (PR #58 r3237308843): the pre-edit\n // `deployment_plan_rendered` event already fired with the original\n // spec's block. After applying the edit + recomputing preview /\n // summary / fees / plan, re-render and emit a fresh block so\n // consumers see the post-edit plan alongside the post-edit intent\n // recap. Without this re-emit, the event stream is inconsistent\n // with the user's confirmation surface and the persisted manifest.\n const editedBlock = renderDeploymentPlan({\n plan,\n denomMap,\n image: primaryImage(confirmedSpec),\n // FIX 1: canonical resolved SKU name, not the user's requested size.\n size: pinned.name,\n metaHash: preview.meta_hash_hex,\n customDomain: customDomainOf(confirmedSpec),\n customDomainService: customDomainServiceOf(confirmedSpec),\n providerUuid: pinned.providerUuid,\n });\n callbacks.onProgress?.({\n kind: 'deployment_plan_rendered',\n block: editedBlock,\n });\n }\n }\n\n // --- Intent recap + onConfirm callback ------------------------------\n const recapText = renderIntentRecap({ spec: confirmedSpec, activeChain });\n const recapBlock = { text: recapText };\n if (callbacks.onConfirm) {\n const yesNo = await callbacks.onConfirm(recapBlock);\n if (yesNo !== 'yes') {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.OPERATION_CANCELLED,\n 'User declined to proceed at intent-recap step.',\n );\n }\n }\n callbacks.onProgress?.({ kind: 'user_confirmed' });\n\n // --- Compose ADR-036 auth callbacks (E-hybrid: agent-core internalizes) ---\n const signArbitrary = opts.walletProvider.signArbitrary.bind(\n opts.walletProvider,\n );\n const timestamps = new AuthTimestampTracker();\n const getAuthToken = async (\n address: string,\n leaseUuid: string,\n ): Promise<string> => {\n const ts = await timestamps.next();\n const message = createSignMessage(address, leaseUuid, ts);\n const { pub_key, signature } = await signArbitrary(address, message);\n return createAuthToken(address, leaseUuid, ts, pub_key.value, signature);\n };\n const getLeaseDataAuthToken = async (\n address: string,\n leaseUuid: string,\n metaHashHex: string,\n ): Promise<string> => {\n const ts = await timestamps.next();\n const message = createLeaseDataSignMessage(leaseUuid, metaHashHex, ts);\n const { pub_key, signature } = await signArbitrary(address, message);\n return createAuthToken(\n address,\n leaseUuid,\n ts,\n pub_key.value,\n signature,\n metaHashHex,\n );\n };\n\n // --- Broadcast: fred's atomic deployApp (architect α-locked) -------\n callbacks.onProgress?.({ kind: 'deploy_app_broadcast' });\n // FIX 1: pass the RESOLVED SKU name so fred records the lease item with\n // the on-chain SKU's name, consistent with the readiness/plan above.\n const fredInput = buildFredDeployInput(confirmedSpec, pinned.name, {\n skuUuid: pinned.skuUuid,\n providerUuid: pinned.providerUuid,\n });\n let fredResult: FredDeployAppResult;\n try {\n fredResult = await fredDeployApp(\n opts.clientManager,\n getAuthToken,\n getLeaseDataAuthToken,\n fredInput,\n opts.fetchFn,\n );\n } catch (err) {\n // ENG-185 sub-PR E: thread a `RecoveryContext` so the\n // `retry_set_domain` branch can decompose the deploy into\n // `setItemCustomDomain` + `uploadLeaseData` + `pollLeaseUntilReady`.\n // Captured values mirror what fred's atomic `deployApp` had: the\n // ADR-036 auth closures, the manifest payload + hash, and the chain\n // identity (for downstream `tryPersistManifest`).\n const recoveryCtx: RecoveryContext = {\n manifestJson: preview.manifest_json,\n metaHash: preview.meta_hash_hex,\n getAuthToken,\n getLeaseDataAuthToken,\n tenantAddress,\n chainId,\n denomMap,\n // FIX 1: thread the RESOLVED SKU name so the recovery path's\n // manifest persistence records the same name the broadcast used.\n skuName: pinned.name,\n };\n return await handleBroadcastFailure(\n err,\n confirmedSpec,\n callbacks,\n opts,\n recoveryCtx,\n );\n }\n\n // Live-state + live-connection trackers (Copilot fix-3, post-PR-D):\n // the pre-fix code merged `pollResult.state` (a JSON-encoded string from\n // `waitForAppReady`) into `fredResult.state` (numeric `LeaseState`) via\n // a width-erasing cast, hiding a type mismatch. Same for `pollResult.status`\n // (`FredLeaseStatus`) → `fredResult.connection` (`ConnectionDetails`):\n // runtime worked (duck-typed reads via `extractRunningEndpoints` /\n // `hasRunningInstances` / `decodeLeaseState`, all of which accept the\n // wider shape), but the type contract was violated.\n //\n // The fix: track the FINAL (post-poll if applicable, else initial)\n // state + connection in two separate locals with HONEST types. Each\n // upstream source has a typed slot:\n // - `fredResult.state` (`LeaseState`) when no polling fired.\n // - `pollResult.status.state` (`LeaseState`) when polling did fire —\n // NOTE: `pollResult.state` is the STRING form (JSON-encoded), wrong\n // source. The numeric form lives one level deeper.\n // - `fredResult.connection` (`ConnectionDetails | undefined`) initial.\n // - `pollResult.status` (`FredLeaseStatus`) post-poll.\n let liveState: FredDeployAppResult['state'] | undefined = fredResult.state;\n let liveConnection: ConnectionDetails | FredLeaseStatus | undefined =\n fredResult.connection;\n\n // --- Classify happy-path result + full routing (ENG-185 sub-PR D) -\n // Architect's α-lock: fred returns after tx + manifest upload succeed,\n // NOT after the app is observably running. So `'needs_wait'` IS an\n // expected happy-path return shape (lease created, manifest uploaded,\n // container not yet started by the provider) — and `'failed'` covers\n // the terminal-state-on-return edge (e.g. REJECTED, when the chain\n // invalidated the lease between create and return).\n //\n // Routing (per architect's Q7 pseudocode):\n // - `'failed'` → throw TX_FAILED with the classifier's\n // `errorSummary` (F3 pattern, no onFailure for\n // this kind of failure — there's no recovery\n // choice once fred returns a terminal-state\n // response from a successful broadcast).\n // - `'needs_wait'` → poll `wait_for_app_ready`, emit\n // `polling_for_readiness` events per onProgress\n // sample, then RE-classify the post-poll result\n // (Defense #2 — rare provider race where\n // pollLeaseUntilReady exits on state==ACTIVE\n // without a running instance). On post-poll\n // success, merge the polled fields back into\n // `fredResult` so downstream DeployResult\n // construction sees the final state/connection.\n // - `'active'` → fall through to `app_ready_confirmed` + persist.\n let classification = classifyDeployResponse(fredResult);\n callbacks.onProgress?.({\n kind: 'deploy_response_classified',\n outcome: classification.outcome,\n });\n\n if (classification.outcome === 'failed') {\n // F3 pattern (mirrors handleBroadcastFailure's empty-options path):\n // throw TX_FAILED directly; no onFailure invocation. The envelope\n // is constructed for a future logging hook but otherwise unused.\n const reason =\n classification.errorSummary ??\n `fred deployApp returned failed outcome for lease ${\n classification.leaseUuid ?? '<no-uuid>'\n }`;\n const envelope: FailureEnvelope = { outcome: 'failed', reason };\n void envelope;\n throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);\n }\n\n if (classification.outcome === 'needs_wait') {\n // Defense #1: classifier guarantees leaseUuid when needs_wait\n // (`classify-deploy-response.ts`: !leaseUuid → outcome='failed'),\n // but the TS type doesn't narrow it. Defensive throw documents\n // the invariant for future maintainers + catches any classifier\n // regression that would break the assumption.\n if (!classification.leaseUuid) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n 'Internal invariant: classifier returned needs_wait without leaseUuid.',\n );\n }\n const leaseUuid = classification.leaseUuid;\n // queryClient is already bound at L193 (function-level); reuse it.\n // (Copilot #1 fix: removed shadowing redeclaration. CosmosClientManager\n // keys its query client as a singleton so there was no behavioral\n // difference, but shadowing is a maintenance trap.)\n const pollStartMs = Date.now();\n let attempt = 0;\n\n let pollResult: Awaited<ReturnType<typeof waitForAppReady>>;\n try {\n pollResult = await waitForAppReady(\n queryClient,\n tenantAddress,\n leaseUuid,\n getAuthToken,\n {\n timeoutMs: opts.waitForReadyTimeoutMs ?? 480_000,\n onProgress: (status) => {\n attempt += 1;\n const stateName = decodeLeaseState(status.state);\n callbacks.onProgress?.({\n kind: 'polling_for_readiness',\n leaseUuid,\n attempt,\n elapsedMs: Date.now() - pollStartMs,\n ...(stateName !== undefined ? { state: stateName } : {}),\n });\n },\n },\n opts.fetchFn,\n );\n } catch (err) {\n // ProviderApiError / timeout / TerminalChainStateError → F3 route.\n const reason =\n err instanceof Error\n ? `wait_for_app_ready failed for lease ${leaseUuid}: ${err.message}`\n : `wait_for_app_ready failed for lease ${leaseUuid}: ${String(err)}`;\n const envelope: FailureEnvelope = { outcome: 'failed', reason };\n void envelope;\n throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);\n }\n\n // Defense #2: re-classify post-poll. `pollLeaseUntilReady` exits on\n // state==ACTIVE but doesn't check running-instances; a rare provider-\n // side race could leave us at ACTIVE with no instances → outcome\n // 'needs_wait' on the re-classify. We treat that as TX_FAILED rather\n // than misleadingly emitting app_ready_confirmed + onComplete on a\n // non-running deploy.\n const postPollResponse: DeployResponseShape = {\n lease_uuid: pollResult.lease_uuid,\n provider_uuid: pollResult.provider_uuid,\n provider_url: pollResult.provider_url,\n state: pollResult.state,\n connection: pollResult.status,\n };\n classification = classifyDeployResponse(postPollResponse);\n if (classification.outcome !== 'active') {\n // Copilot fix-6: include `leaseUuid` in the fallback message so\n // log/user-report correlation matches the sibling\n // `waitForAppReady` catch path at L548-550. Diagnostic consistency\n // invariant — locked in by the Defense #2 test's\n // `expect(...).toContain(leaseUuid)` assertion. The\n // `errorSummary` path is unaffected; the classifier already\n // includes leaseUuid in its terminal-state summary\n // (`classify-deploy-response.ts:120`), but errorSummary fires only\n // for `outcome === 'failed'`. The no-errorSummary fallback\n // (this branch) fires when post-poll outcome is `'needs_wait'`\n // (Defense #2's race scenario) — that's the gap we're closing.\n const reason =\n classification.errorSummary ??\n `wait_for_app_ready returned for lease ${leaseUuid} but post-poll classifier outcome is ${classification.outcome}`;\n throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);\n }\n\n // Merge post-poll fields back into `fredResult` so downstream\n // DeployResult construction sees the final lease/provider identity.\n // The string fields (lease_uuid / provider_uuid / provider_url) align\n // typewise with their FredDeployAppResult counterparts. The state +\n // connection fields are NOT merged here — they go into `liveState`\n // and `liveConnection` so each carries the type that matches its\n // upstream source (no width-erasing casts). See the live-tracker\n // declarations above for the full rationale.\n fredResult = {\n ...fredResult,\n lease_uuid: pollResult.lease_uuid,\n provider_uuid: pollResult.provider_uuid,\n provider_url: pollResult.provider_url,\n };\n liveState = pollResult.status.state;\n liveConnection = pollResult.status;\n }\n\n // 'active' (initial OR post-poll merge): emit + fall through to persist.\n callbacks.onProgress?.({\n kind: 'app_ready_confirmed',\n leaseUuid: fredResult.lease_uuid,\n });\n\n // --- Persist manifest (best-effort; save-fail still emits success) -\n const persistedPath = await tryPersistManifest({\n leaseUuid: fredResult.lease_uuid,\n image: primaryImage(confirmedSpec),\n // FIX 1: persist the RESOLVED SKU name (matches what was broadcast).\n size: pinned.name,\n metaHash: preview.meta_hash_hex,\n chainId,\n manifestJson: preview.manifest_json,\n customDomain: fredResult.custom_domain,\n customDomainService: fredResult.service_name,\n dataDir: opts.dataDir,\n callbacks,\n });\n\n // --- Build typed DeployResult --------------------------------------\n // F1 fix: decode lease state via the canonical lease-state.decode()\n // (handles int + LEASE_STATE_* string + undefined paths exhaustively).\n //\n // C3 fix (defensive bias correction; checklist item #16): distinguish\n // absent state (undefined → default ACTIVE as defense-in-depth against\n // legacy/mocked shapes that bypass fred's required-state contract —\n // fred itself always sets `state` in `DeployAppResult`)\n // from UNRECOGNIZED state (decode returned undefined for a value that\n // WAS provided → likely a terminal/unknown chain emission that must\n // NOT be silently classified as ACTIVE). For the unrecognized case,\n // throw `INVALID_CONFIG` so callers see the empirical mismatch\n // instead of consuming a misleading ACTIVE.\n // Reads via `liveState` (Copilot fix-3): carries the post-poll\n // `pollResult.status.state` (numeric `LeaseState`) when the needs_wait\n // branch fired; falls back to `fredResult.state` for the direct-active\n // path. Effective type is `LeaseState | undefined` — numeric only after\n // the fix-3 type-tightening. The `undefined` branch handles the C3\n // defense-in-depth case above (legacy/mocked shapes that bypass fred's\n // required-state contract). The numeric branch decodes the enum via\n // `decodeLeaseState`; the `decoded === undefined` arm catches\n // UNRECOGNIZED enum values (defense-in-depth against future chain\n // emissions that add new states beyond the current `LeaseStateName`\n // union).\n let leaseStateDecoded: LeaseStateName;\n if (liveState === undefined) {\n leaseStateDecoded = 'LEASE_STATE_ACTIVE';\n } else {\n const decoded = decodeLeaseState(liveState);\n if (decoded === undefined) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `Unrecognized lease state from fred deployApp response: ${String(liveState)}. Cannot safely classify; refusing to silently coerce to ACTIVE.`,\n );\n }\n leaseStateDecoded = decoded;\n }\n\n // F4 fix: derive `urls` from `extractRunningEndpoints(connection)` for\n // multi-FQDN dedup (matches CJS pipeline behavior). fred's\n // `result.url` is a single derived URL; the full connection payload\n // exposes the canonical instance list.\n //\n // Copilot review fix (PR #58 r3249097136): when no FQDN can be\n // extracted from `connection`, fall back to `fredResult.url` THROUGH\n // the shared `normalizeFredUrl` helper. Raw values like\n // `'app.example.com:443'` now surface as\n // `'https://app.example.com:443/'`, matching the classifier's\n // (`classify-deploy-response.ts`) and renderer's (`format-success.ts`)\n // handling. Empty / scheme-less inputs are normalized consistently.\n // Reads via `liveConnection` (Copilot fix-3): carries\n // `pollResult.status` (FredLeaseStatus) when the needs_wait branch\n // fired, falls back to `fredResult.connection` (ConnectionDetails)\n // for the direct-active path. `extractRunningEndpoints` takes\n // `unknown` and walks `instances` / `services.*.instances` — both\n // shapes are accepted at runtime.\n const endpointUrls =\n extractRunningEndpoints(liveConnection).map(formatEndpointAsUrl);\n const fallbackUrl =\n typeof fredResult.url === 'string' ? normalizeFredUrl(fredResult.url) : '';\n const result: DeployResult = {\n leaseUuid: fredResult.lease_uuid,\n providerUuid: fredResult.provider_uuid,\n leaseState: leaseStateDecoded,\n urls:\n endpointUrls.length > 0\n ? endpointUrls\n : fallbackUrl.length > 0\n ? [fallbackUrl]\n : [],\n ...(fredResult.custom_domain\n ? { customDomain: fredResult.custom_domain }\n : {}),\n manifestPath: persistedPath ?? '',\n };\n callbacks.onProgress?.({ kind: 'success_rendered', result });\n callbacks.onComplete?.(result);\n return result;\n}\n\n// --- Helpers ---------------------------------------------------------\n\nfunction primaryImage(spec: DeploySpec): string {\n if (isStackSpec(spec)) {\n for (const svc of Object.values(spec.services)) {\n if (svc?.image) return svc.image;\n }\n return '';\n }\n return (spec as SingleServiceSpec).image ?? '';\n}\n\nfunction requestedSize(spec: DeploySpec): string {\n // ENG-275: `size` is a first-class optional field on both DeploySpec\n // variants (`types.ts`). This helper centralizes the default: a\n // non-empty string wins; absent / empty falls back to 'small'. The\n // `typeof` guard still degrades a non-string value smuggled in by an\n // `unknown`-cast (e.g. JSON.parse) caller to the safe default.\n const recorded = spec.size;\n return typeof recorded === 'string' && recorded.length > 0\n ? recorded\n : 'small';\n}\n\n/**\n * SKU disambiguator intent helpers. `providerUuid` / `skuUuid` are\n * first-class optional fields on both `DeploySpec` variants (ENG-296,\n * mirroring ENG-275's typed `size`). Returns `undefined` for absent /\n * empty values so `resolveSku` only narrows when a real disambiguator is\n * supplied.\n */\nfunction requestedProviderUuid(spec: DeploySpec): string | undefined {\n const v = spec.providerUuid;\n return typeof v === 'string' && v.length > 0 ? v : undefined;\n}\nfunction requestedSkuUuid(spec: DeploySpec): string | undefined {\n const v = spec.skuUuid;\n return typeof v === 'string' && v.length > 0 ? v : undefined;\n}\n\nfunction customDomainOf(spec: DeploySpec): string | undefined {\n return (spec as { customDomain?: string }).customDomain;\n}\n\nfunction customDomainServiceOf(spec: DeploySpec): string | undefined {\n if (isStackSpec(spec)) return (spec as StackSpec).serviceName;\n return undefined;\n}\n\nasync function estimateFees(\n opts: DeployAppOptions,\n spec: DeploySpec,\n metaHashHex: string, // SHA-256 hex digest of the canonical manifest JSON; threaded into create-lease estimate via the `--meta-hash` flag (mirrors fred's deploy path at packages/fred/src/tools/deployApp.ts:363)\n skuUuid: string, // ENG-258: pre-resolved SKU pin from the orchestrator; no second lookup here.\n): Promise<Plan['fees']> {\n // PR 3 fix-3 (B-narrowed-trimmed per architect ratification):\n // - REAL cosmosEstimateFee for create-lease (criterion-blocking).\n // - SET-DOMAIN emits `{notEstimated: true, reason}` sentinel (the\n // frozen-contract escape hatch designed for pre-broadcast lease-\n // UUID unavailability per ENG-128). Per ENG-185 #3 sub-PR C\n // (architect's verdict B): the chain rejects placeholder-UUID\n // simulation of `MsgSetItemCustomDomain` (keeper's `GetLease()`\n // fails first with ErrLeaseNotFound), so the sentinel is the\n // PERMANENT shape — not a TODO.\n\n // ENG-258: `skuUuid` is now a pre-resolved parameter (the orchestrator\n // resolves the pin ONCE via core's `resolveSku` so plan, fee, and\n // broadcast share one SKU). The prior in-function second lookup is gone.\n\n // ENG-185 #3 sub-PR C: mirror fred's deploy-time item creation verbatim\n // (`packages/fred/src/tools/deployApp.ts:336-341`). Stack specs create\n // ONE lease item per service (each with `${skuUuid}:1:${name}`); legacy\n // single-service specs create one bare `${skuUuid}:1`. The prior gate\n // on `spec.serviceName` underestimated multi-service stacks (only the\n // domain-target service was billed) and accidentally collapsed stacks\n // WITHOUT customDomain to legacy-mode args (`spec.serviceName` is only\n // set alongside customDomain — bug 2).\n //\n // Storage SKU items (fred's `input.storage` path) are deliberately NOT\n // handled here — agent-core's `DeploySpec` has no `storage` field,\n // unlike fred's input contract.\n const itemArgs: string[] = isStackSpec(spec)\n ? Object.keys(spec.services).map((name) => `${skuUuid}:1:${name}`)\n : [`${skuUuid}:1`];\n\n let createLeaseEstimate: Awaited<ReturnType<typeof cosmosEstimateFee>>;\n try {\n createLeaseEstimate = await cosmosEstimateFee(\n opts.clientManager,\n 'billing',\n 'create-lease',\n ['--meta-hash', metaHashHex, ...itemArgs],\n );\n } catch (err) {\n // Wrap the underlying failure with an agent-core-boundary message\n // for caller diagnostics. `core`'s `cosmosEstimateFee` (per\n // `packages/core/src/cosmos.ts`) throws across multiple sites with\n // different codes: `INVALID_CONFIG` for missing `gasPrice`,\n // `UNSUPPORTED_TX` for invalid module/subcommand,\n // `SIMULATION_FAILED` for actual simulation issues.\n //\n // Copilot review fix (PR #58 r3250192834): preserve the original\n // code when the underlying threw a typed `ManifestMCPError`;\n // fall back to `SIMULATION_FAILED` only for untyped failures.\n // The prior comment claimed code-preservation but the code\n // unconditionally cast to `SIMULATION_FAILED`.\n const msg = `Failed to estimate create-lease fee: ${err instanceof Error ? err.message : String(err)}`;\n if (err instanceof ManifestMCPError) {\n throw new ManifestMCPError(err.code, msg);\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.SIMULATION_FAILED, msg);\n }\n\n // FeeEstimateResult shape (per packages/core/src/types.ts):\n // { module, subcommand, gasEstimate: string, fee: { gas: string, amount: Coin[] } }\n // Map to typed `FeeEstimate { coins: Coin[], gas: number }` (Path-C\n // revision per a62cfd1).\n //\n // Copilot review fix (PR #58 r3250192734): use `fee.gas` (post-\n // `gasMultiplier`), NOT `gasEstimate` (raw simulation gas). The\n // `coins` were priced at `fee.gas`; displaying `gasEstimate` shows\n // a number ~33% lower than the price reflects under the default\n // 1.5x multiplier (per CLAUDE.md `COSMOS_GAS_MULTIPLIER`), creating\n // a visible inconsistency in the rendered plan.\n const createLease: FeeEstimate = {\n coins: createLeaseEstimate.fee.amount.map((c) => ({\n denom: c.denom,\n amount: c.amount,\n })),\n gas: Number(createLeaseEstimate.fee.gas),\n };\n\n // set-domain: emit `{notEstimated: true, reason}` sentinel per\n // architect-ratified counter-proposal + ENG-185 #3 verdict B (the\n // chain rejects placeholder-UUID simulation of `MsgSetItemCustomDomain`\n // — keeper's `GetLease()` runs first and fails with ErrLeaseNotFound;\n // verified against manifest-ledger v2.1.0). The frozen-contract type\n // includes this discriminated variant precisely for this case.\n //\n // Reason string mirrors the canonical form already pinned in\n // `internals/render-deployment-plan.test.ts` so producer + renderer\n // share the same wording.\n const hasDomain = typeof customDomainOf(spec) === 'string';\n return {\n createLease,\n ...(hasDomain\n ? {\n setDomain: {\n notEstimated: true,\n reason: 'no representative lease for pre-broadcast simulation',\n },\n }\n : {}),\n };\n}\n\nfunction applyPlanEdit(\n spec: DeploySpec,\n edit: Exclude<\n Awaited<ReturnType<NonNullable<DeployAppCallbacks['onPlan']>>>,\n 'confirm' | 'cancel'\n >,\n): DeploySpec {\n // PR 3 single-iteration: replace_spec replaces; edit_env merges env keys\n // into the matching service (or single-service spec).\n if (edit.kind === 'replace_spec') return edit.spec;\n if (edit.kind === 'edit_env') {\n // Copilot review fix (PR #58 r3266642610): the prior implementation\n // silently no-op'd two stack-spec cases (missing `edit.service` or\n // unknown service name), returning the unchanged spec while the\n // callback caller perceived the edit as applied. Worst case: deploy\n // proceeds with wrong env vars / secrets without an error signal.\n // Fail-fast at the boundary instead, so the user's `onPlan` callback\n // gets a clear `INVALID_CONFIG` for misuse. Uses\n // `Object.keys().includes()` for the membership check — matches\n // Fix 16's cross-package symmetry with fred (avoids prototype-chain\n // bypass via `'constructor'` / `'toString'` / etc.).\n if (isStackSpec(spec)) {\n if (edit.service === undefined) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n 'applyPlanEdit: edit_env on a stack spec requires `service` identifying which service to edit.',\n );\n }\n if (!Object.keys(spec.services).includes(edit.service)) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `applyPlanEdit: edit_env \\`service\\` \"${edit.service}\" is not a key in \\`services\\` (got: [${Object.keys(spec.services).join(', ')}]).`,\n );\n }\n const svc = spec.services[edit.service];\n // Membership check above guarantees `svc` is defined; the\n // non-null assertion documents that — TS narrows it after the\n // `includes` check, but the runtime invariant is in the\n // membership check.\n return {\n ...spec,\n services: {\n ...spec.services,\n [edit.service]: {\n ...(svc as ServiceDef),\n env: { ...((svc as ServiceDef).env ?? {}), ...edit.env },\n },\n },\n };\n }\n const single = spec as SingleServiceSpec;\n return { ...single, env: { ...(single.env ?? {}), ...edit.env } };\n }\n return spec;\n}\n\n/**\n * Recovery-path execution context. Threaded from `deployApp`'s enclosing\n * scope through `handleBroadcastFailure` → `dispatchRecovery` → the\n * per-choice closures. Internal (not exported, not in `types.ts`); each\n * field's upstream source lives in `deployApp`'s scope when the broadcast\n * failure surfaces, so the context is just a parameter bundle, not a\n * stateful object.\n *\n * Added by ENG-185 sub-PR E so the `retry_set_domain` branch can\n * decompose the deploy: it needs the manifest payload + hash for\n * `uploadLeaseData`, the auth closures for the upload + poll, and the\n * tenant/chain identity for `pollLeaseUntilReady` + downstream\n * `tryPersistManifest`. Other recovery branches (`salvage_without_domain`,\n * `cancel_lease`, `close_lease`) currently ignore the context — they\n * route through `stopApp` or a bare throw — but the widened signature\n * keeps future expansions cheap.\n */\ninterface RecoveryContext {\n manifestJson: string;\n metaHash: string;\n getAuthToken: (address: string, leaseUuid: string) => Promise<string>;\n getLeaseDataAuthToken: (\n address: string,\n leaseUuid: string,\n metaHashHex: string,\n ) => Promise<string>;\n tenantAddress: string;\n chainId: string;\n denomMap: DenomMap;\n /**\n * Resolved on-chain SKU name (FIX 1, ENG-258 review). The recovery\n * path's manifest persistence records this — not the user's requested\n * `size` — so a deploy pinned by `skuUuid` persists the actual SKU name.\n */\n skuName: string;\n}\n\nasync function handleBroadcastFailure(\n err: unknown,\n spec: DeploySpec,\n callbacks: DeployAppCallbacks,\n opts: DeployAppOptions,\n ctx: RecoveryContext,\n): Promise<DeployResult> {\n const requestedCustomDomain = customDomainOf(spec);\n\n // F2 fix: classify-deploy-error.ts is the canonical classifier — it\n // anchors the `PARTIAL_PREFIX` match, supports `{ error: {...} }`\n // SDK-wrapping envelopes, and threads `expectedCustomDomain` for\n // downstream rendering. Earlier inline `parsePartialSuccess` was a\n // reduced-robustness duplicate; replaced here per QA F2.\n const classified = classifyDeployError(err, {\n ...(requestedCustomDomain\n ? { expectedCustomDomain: requestedCustomDomain }\n : {}),\n });\n\n if (classified.outcome === 'partially_succeeded' && classified.leaseUuid) {\n const envelope: FailureEnvelope = {\n outcome: 'partially_succeeded',\n leaseUuid: classified.leaseUuid,\n ...(requestedCustomDomain ? { requestedCustomDomain } : {}),\n reason: classified.reason,\n };\n // CJS-parity: the lease was just created so it's typically PENDING.\n // The classifier doesn't decode state from the error envelope (the\n // chain emits state asynchronously after the create-lease tx); the\n // user prompt's \"state: <name>\" line is informational.\n const promptPayload = renderPartialSuccessPrompt({\n leaseUuid: classified.leaseUuid,\n decodedState: 'LEASE_STATE_PENDING',\n reason: classified.reason,\n ...(requestedCustomDomain ? { requestedCustomDomain } : {}),\n });\n const options: RecoveryOption[] = promptPayload.options.map((id) => ({\n id,\n label: recoveryOptionLabel(id),\n description: recoveryOptionDescription(id),\n }));\n // F3 fix: align with verify-recover's pattern — only invoke\n // onFailure when there's a choice to present. Empty options means\n // inform-only path; we throw instead of prompting.\n if (options.length > 0 && callbacks.onFailure !== undefined) {\n // Route β (ENG-185 #7): the rendered prompt body would otherwise be\n // dropped (only `options` flow into `onFailure`). Ride it on a\n // ProgressEvent emitted exactly once, immediately before the\n // (single) `onFailure` call — so it never fires on the inform-only\n // throw path below and `onFailure` stays invoked exactly once.\n callbacks.onProgress?.({\n kind: 'partial_success_prompt_rendered',\n prompt: promptPayload.prompt,\n leaseUuid: envelope.leaseUuid,\n });\n const choice = await callbacks.onFailure(envelope, options);\n return await dispatchRecovery(\n choice,\n envelope,\n spec,\n opts,\n callbacks,\n ctx,\n );\n }\n throw new ManifestMCPError(\n ManifestMCPErrorCode.TX_FAILED,\n classified.reason,\n );\n }\n\n // Non-partial failure: surface as `outcome: 'failed'` envelope.\n // F3 fix: skip onFailure when options is empty (inform-only path);\n // throw directly. Caller can still surface the error via the thrown\n // ManifestMCPError if they need to react.\n const envelope: FailureEnvelope = {\n outcome: 'failed',\n reason: classified.reason,\n };\n // Intentionally NOT invoking callbacks.onFailure?.(envelope, []) here\n // per F3 — no recovery choice to present.\n void envelope; // retained for future logging hook if needed\n throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, classified.reason);\n}\n\nasync function dispatchRecovery(\n choice: RecoveryChoice,\n envelope: FailureEnvelope,\n spec: DeploySpec,\n opts: DeployAppOptions,\n callbacks: DeployAppCallbacks,\n ctx: RecoveryContext,\n): Promise<DeployResult> {\n // Inline closures per gate-2 verdict (no separate strategy module).\n const leaseUuid =\n envelope.outcome === 'partially_succeeded' ? envelope.leaseUuid : '';\n switch (choice.id) {\n case 'retry_set_domain':\n return await retrySetDomainAndComplete(\n leaseUuid,\n spec,\n opts,\n callbacks,\n ctx,\n );\n case 'salvage_without_domain':\n throw new ManifestMCPError(\n ManifestMCPErrorCode.TX_FAILED,\n `salvage_without_domain: lease ${leaseUuid} retained without domain; caller should re-run troubleshootDeployment.`,\n );\n case 'cancel_lease':\n case 'close_lease': {\n await stopApp(opts.clientManager, leaseUuid);\n throw new ManifestMCPError(\n ManifestMCPErrorCode.TX_FAILED,\n `${choice.id}: lease ${leaseUuid} closed.`,\n );\n }\n }\n throw new ManifestMCPError(\n ManifestMCPErrorCode.TX_FAILED,\n `Unknown recovery option: ${(choice as RecoveryChoice).id}`,\n );\n}\n\n/**\n * `retry_set_domain` recovery: decompose the deploy after the partial-\n * success failure. ENG-185 sub-PR E.\n *\n * Steps (mirrors fred's atomic `deployApp` minus the create-lease tx,\n * which already succeeded):\n * 1. `setItemCustomDomain` — broadcast the domain claim against the\n * pre-existing lease. Stack specs thread `serviceName` so the\n * tx targets the named lease item.\n * 2. `fetchActiveLease` + `resolveProviderUrl` — look up the provider\n * URL from the on-chain lease record (the partial-success error\n * envelope only carries `leaseUuid`).\n * 3. `uploadLeaseData` — push the manifest payload to the provider.\n * Uses the ADR-036 lease-data auth token (signed against the\n * manifest's meta-hash).\n * 4. `pollLeaseUntilReady` — poll until the provider reports ACTIVE +\n * running. Uses the LOWER-LEVEL primitive (not `waitForAppReady`)\n * so the already-resolved `providerApiUrl` and auth-token closure\n * pass through directly — no redundant on-chain queries (Copilot\n * fix-1, PR #71). Reuses D's canonical polling-emission pattern:\n * `onProgress` closure translates each `FredLeaseStatus` sample\n * into a typed `polling_for_readiness` ProgressEvent, default\n * 480_000ms timeout overridable via `opts.waitForReadyTimeoutMs`.\n * 5. Defense #2 parity (post-poll re-classify) — guard the\n * ACTIVE-with-no-instances race per D's pattern.\n * 6. Persist manifest (best-effort) + build typed `DeployResult` +\n * emit `app_ready_confirmed` + `success_rendered` + onComplete.\n *\n * Failure paths (sibling-parity wraps — every catch site surfaces\n * `retry_set_domain <primitive-name> failed for lease ${leaseUuid}:\n * ${err.message}` in the thrown message, matching D's L548-550 style).\n * Error-code policy: typed `ManifestMCPError`s flow through with their\n * original code preserved (precedent at `estimateFees` — see the\n * `cosmosEstimateFee` catch block); untyped errors default to\n * `TX_FAILED`. The post-poll re-classify path likewise prefixes BOTH\n * the errorSummary-set and the no-errorSummary branches with\n * `retry_set_domain` + leaseUuid (Copilot fix-4, PR #71):\n * - `setItemCustomDomain` throws → wrap with prefix + leaseUuid +\n * code preservation. Most likely cause: chain rejected the\n * set-item-custom-domain tx (FQDN validation, reserved-suffix\n * match, lease not active, etc.).\n * - `fetchActiveLease` / `resolveProviderUrl` throw → wrap with\n * prefix + leaseUuid.\n * - `uploadLeaseData` throws → wrap with prefix + leaseUuid.\n * - `pollLeaseUntilReady` throws → wrap with prefix + leaseUuid.\n * - Post-poll re-classify outcome !== 'active' → wrap both\n * branches: errorSummary-set (terminal-state response) AND\n * no-errorSummary fallback (ACTIVE-with-no-instances Defense #2\n * race) carry prefix + leaseUuid.\n */\nasync function retrySetDomainAndComplete(\n leaseUuid: string,\n spec: DeploySpec,\n opts: DeployAppOptions,\n callbacks: DeployAppCallbacks,\n ctx: RecoveryContext,\n): Promise<DeployResult> {\n const domain = customDomainOf(spec);\n if (!domain) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n 'retry_set_domain requires a customDomain in spec.',\n );\n }\n // C6 fix (preserved from pre-E impl): pass `serviceName` for stack\n // leases so the set-item-custom-domain tx targets the named service\n // item, not the default single-item lease.\n const serviceName = customDomainServiceOf(spec);\n const setItemOpts = serviceName ? { serviceName } : undefined;\n try {\n await setItemCustomDomain(\n opts.clientManager,\n leaseUuid,\n domain,\n setItemOpts,\n );\n } catch (err) {\n // Copilot fix-3 (PR #71): sibling-parity wrap. Every throw site in\n // this helper now surfaces `retry_set_domain` + leaseUuid in the\n // message for log/user-report correlation, matching the\n // fetchActiveLease/uploadLeaseData/pollLeaseUntilReady wraps below.\n // Preserve the original ManifestMCPError code when applicable\n // (precedent at `estimateFees` — see the cosmosEstimateFee catch\n // block); fall back to TX_FAILED for untyped errors.\n // Upstream traceability (Copilot fix-6, PR #71): `setItemCustomDomain`\n // from `core/src/tools/setItemCustomDomain.ts:63,69` genuinely throws\n // `ManifestMCPError(INVALID_CONFIG)` for validation failures — the\n // typed branch here is LIVE for the canonical chain-side errors\n // (FQDN shape, reserved-suffix match, etc.).\n const reason =\n err instanceof Error\n ? `retry_set_domain set-item-custom-domain failed for lease ${leaseUuid}: ${err.message}`\n : `retry_set_domain set-item-custom-domain failed for lease ${leaseUuid}: ${String(err)}`;\n const code =\n err instanceof ManifestMCPError\n ? err.code\n : ManifestMCPErrorCode.TX_FAILED;\n throw new ManifestMCPError(code, reason);\n }\n\n // Resolve the lease + provider URL via on-chain queries. The\n // partial-success envelope only carried `leaseUuid` — fred's atomic\n // deployApp already had providerUuid in scope, but here we recover it.\n // BOTH values are hoisted to outer scope so the poll + DeployResult\n // build below can reuse them WITHOUT re-running the on-chain queries\n // (Copilot fix-1, PR #71: switching from `waitForAppReady` to the\n // lower-level `pollLeaseUntilReady` removes the 2 redundant queries\n // that `waitForAppReady`'s internal `fetchActiveLease` +\n // `resolveProviderUrl` calls would otherwise add per recovery).\n const queryClient = await opts.clientManager.getQueryClient();\n let lease: Awaited<ReturnType<typeof fetchActiveLease>>;\n let providerApiUrl: string;\n try {\n lease = await fetchActiveLease(\n queryClient,\n leaseUuid,\n 'cannot complete retry_set_domain',\n );\n providerApiUrl = await resolveProviderUrl(queryClient, lease.providerUuid);\n } catch (err) {\n // Copilot fix-5 (PR #71): preserve typed ManifestMCPError codes\n // (matches the L1147 setItemCustomDomain precedent + the L818\n // estimateFees precedent). Honors fixup-4's JSDoc claim.\n // Upstream traceability (Copilot fix-6, PR #71):\n // - `fetchActiveLease` throws `ManifestMCPError(QUERY_FAILED)` at\n // `fred/src/tools/fetchActiveLease.ts:23,35` (lease not found\n // on chain + lease-not-active).\n // - `resolveProviderUrl` throws `ManifestMCPError(QUERY_FAILED)`\n // at `fred/src/tools/resolveLeaseProvider.ts:13,25,36` (empty\n // providerUuid + missing apiUrl + chain query failure).\n // - Either can also surface `ProviderApiError` (validateProviderUrl\n // path); untyped → TX_FAILED fallback.\n // Typed branch is LIVE for the canonical chain-side errors at this\n // catch — both upstream call sites genuinely emit ManifestMCPError.\n const reason =\n err instanceof Error\n ? `retry_set_domain failed to resolve provider for lease ${leaseUuid}: ${err.message}`\n : `retry_set_domain failed to resolve provider for lease ${leaseUuid}: ${String(err)}`;\n const code =\n err instanceof ManifestMCPError\n ? err.code\n : ManifestMCPErrorCode.TX_FAILED;\n throw new ManifestMCPError(code, reason);\n }\n\n // Upload the manifest payload via the ADR-036 lease-data auth token\n // (signed against the manifest's meta-hash).\n const manifestBytes = new TextEncoder().encode(ctx.manifestJson);\n try {\n const leaseDataAuthToken = await ctx.getLeaseDataAuthToken(\n ctx.tenantAddress,\n leaseUuid,\n ctx.metaHash,\n );\n await uploadLeaseData(\n providerApiUrl,\n leaseUuid,\n manifestBytes,\n leaseDataAuthToken,\n opts.fetchFn,\n );\n } catch (err) {\n // Copilot fix-5 (PR #71): preserve typed ManifestMCPError codes.\n // Upstream traceability (Copilot fix-6, PR #71): fred's\n // `uploadLeaseData` does NOT throw typed `ManifestMCPError` — both\n // its underlying `validateProviderUrl` (`fred/src/http/provider.ts:14`)\n // and the wrapped `checkedFetch` surface throw `ProviderApiError`,\n // which is NOT a `ManifestMCPError`. So this `instanceof\n // ManifestMCPError` check is effectively a no-op for the typical\n // fred path — the typed branch is dead code today for this catch.\n // Pattern is kept for symmetry with the L1196/L1284 sites + safety\n // against future deps that DO throw typed errors (e.g. a hypothetical\n // core dependency in the upload path). For the typical fred-only\n // case, the fallback `TX_FAILED` is what surfaces.\n const reason =\n err instanceof Error\n ? `retry_set_domain manifest upload failed for lease ${leaseUuid}: ${err.message}`\n : `retry_set_domain manifest upload failed for lease ${leaseUuid}: ${String(err)}`;\n const code =\n err instanceof ManifestMCPError\n ? err.code\n : ManifestMCPErrorCode.TX_FAILED;\n throw new ManifestMCPError(code, reason);\n }\n\n // Poll until the provider reports ACTIVE + running. Uses the LOWER-\n // LEVEL `pollLeaseUntilReady` directly (Copilot fix-1, PR #71) — not\n // `waitForAppReady` — so the already-resolved `providerApiUrl` and\n // auth-token closure pass through without re-running the on-chain\n // `fetchActiveLease` + `resolveProviderUrl` calls that\n // `waitForAppReady` would do internally. Saves ~2 queries (and ~2-6s\n // of avoidable latency) per recovery.\n //\n // The `onProgress` closure + `state?` discriminator-spread idiom +\n // `opts.waitForReadyTimeoutMs ?? 480_000` default mirror D's\n // canonical polling pattern verbatim.\n const pollStartMs = Date.now();\n let attempt = 0;\n let pollResult: Awaited<ReturnType<typeof pollLeaseUntilReady>>;\n try {\n pollResult = await pollLeaseUntilReady(\n providerApiUrl,\n leaseUuid,\n () => ctx.getAuthToken(ctx.tenantAddress, leaseUuid),\n {\n timeoutMs: opts.waitForReadyTimeoutMs ?? 480_000,\n onProgress: (status) => {\n attempt += 1;\n const stateName = decodeLeaseState(status.state);\n callbacks.onProgress?.({\n kind: 'polling_for_readiness',\n leaseUuid,\n attempt,\n elapsedMs: Date.now() - pollStartMs,\n ...(stateName !== undefined ? { state: stateName } : {}),\n });\n },\n },\n opts.fetchFn,\n );\n } catch (err) {\n // Names the actual primitive being awaited (post-fixup-1 +\n // fixup-4 consistency): `pollLeaseUntilReady`, not the higher-level\n // `waitForAppReady`. Matches the post-poll re-classify fallback's\n // wording at L1287 + the UNRECOGNIZED-state message at L1308 — all\n // three sites consistently name the primitive that's actually\n // running in this helper. Copilot fix-5 (PR #71): preserve typed\n // ManifestMCPError codes.\n // Upstream traceability (Copilot fix-6, PR #71): fred's\n // `pollLeaseUntilReady` does NOT throw typed `ManifestMCPError` —\n // its terminal-state path throws `TerminalChainStateError` which\n // `extends ProviderApiError` (`fred/src/http/fred.ts:278`), and its\n // timeout/HTTP paths throw `ProviderApiError` directly. Neither is\n // a `ManifestMCPError`. So this `instanceof ManifestMCPError` check\n // is effectively a no-op for the typical fred path — typed branch\n // is dead code today for this catch. Pattern is kept for symmetry\n // with the L1196/L1228 sites + safety against future deps that DO\n // throw typed errors. For the typical fred-only case, the fallback\n // `TX_FAILED` is what surfaces.\n const reason =\n err instanceof Error\n ? `retry_set_domain pollLeaseUntilReady failed for lease ${leaseUuid}: ${err.message}`\n : `retry_set_domain pollLeaseUntilReady failed for lease ${leaseUuid}: ${String(err)}`;\n const code =\n err instanceof ManifestMCPError\n ? err.code\n : ManifestMCPErrorCode.TX_FAILED;\n throw new ManifestMCPError(code, reason);\n }\n\n // Defense #2 parity (from D): re-classify the post-poll response and\n // refuse to declare success if the classifier doesn't see ACTIVE +\n // running instances. Catches the rare provider race where\n // `pollLeaseUntilReady` exits on state==ACTIVE but instances are empty.\n //\n // pollResult IS a `FredLeaseStatus` directly (no `WaitForAppReadyResult`\n // wrapping — that's what the refactor unlocked). The lease/provider\n // identity fields below come from the already-resolved values, NOT\n // from a (no-longer-existing) nested response object.\n const postPollResponse: DeployResponseShape = {\n lease_uuid: leaseUuid,\n provider_uuid: lease.providerUuid,\n provider_url: providerApiUrl,\n state: pollResult.state,\n connection: pollResult,\n };\n const classification = classifyDeployResponse(postPollResponse);\n if (classification.outcome !== 'active') {\n // Copilot fix-4 (PR #71): sibling-parity for BOTH branches. The\n // pre-fix `??` collapsed errorSummary (set when the post-poll\n // classifier produces 'failed' with a terminal-state response)\n // directly into the throw — no `retry_set_domain` prefix, no\n // leaseUuid. Both branches now carry the prefix + leaseUuid, and\n // the no-errorSummary fallback names the actual primitive\n // (`pollLeaseUntilReady` post-fixup-1, not `wait_for_app_ready`).\n const reason =\n classification.errorSummary !== undefined\n ? `retry_set_domain post-poll re-classification failed for lease ${leaseUuid}: ${classification.errorSummary}`\n : `retry_set_domain: pollLeaseUntilReady returned for lease ${leaseUuid} but post-poll classifier outcome is ${classification.outcome}`;\n throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);\n }\n\n callbacks.onProgress?.({ kind: 'app_ready_confirmed', leaseUuid });\n\n // Persist manifest (best-effort; save-fail still emits success — same\n // contract as D's happy path).\n const persistedPath = await tryPersistManifest({\n leaseUuid,\n image: primaryImage(spec),\n // FIX 1: persist the RESOLVED SKU name (matches what was broadcast).\n size: ctx.skuName,\n metaHash: ctx.metaHash,\n chainId: ctx.chainId,\n manifestJson: ctx.manifestJson,\n customDomain: domain,\n customDomainService: serviceName,\n dataDir: opts.dataDir,\n callbacks,\n });\n\n // Build DeployResult. State decoding + urls extraction mirror the\n // happy-path block in `deployApp` verbatim. After the Copilot fix-1\n // refactor, `pollResult` IS a `FredLeaseStatus` (no wrapping), so\n // `liveState` reads from `pollResult.state` directly (numeric\n // `LeaseState`) and `extractRunningEndpoints` walks `pollResult` itself.\n const liveState = pollResult.state;\n let leaseStateDecoded: LeaseStateName;\n const decoded = decodeLeaseState(liveState);\n if (decoded === undefined) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `Unrecognized lease state from pollLeaseUntilReady response: ${String(liveState)}. Cannot safely classify; refusing to silently coerce to ACTIVE.`,\n );\n }\n leaseStateDecoded = decoded;\n const endpointUrls =\n extractRunningEndpoints(pollResult).map(formatEndpointAsUrl);\n // Lease + provider identity come from the already-resolved values,\n // not from a (no-longer-existing) wrapping response object.\n const result: DeployResult = {\n leaseUuid,\n providerUuid: lease.providerUuid,\n leaseState: leaseStateDecoded,\n urls: endpointUrls,\n customDomain: domain,\n manifestPath: persistedPath ?? '',\n };\n callbacks.onProgress?.({ kind: 'success_rendered', result });\n callbacks.onComplete?.(result);\n return result;\n}\n\nfunction recoveryOptionLabel(id: RecoveryOptionId): string {\n switch (id) {\n case 'retry_set_domain':\n return 'Retry set-domain + upload';\n case 'salvage_without_domain':\n return 'Salvage without domain';\n case 'cancel_lease':\n return 'Cancel the lease';\n case 'close_lease':\n return 'Cancel or close the lease';\n }\n}\n\nfunction recoveryOptionDescription(id: RecoveryOptionId): string {\n switch (id) {\n case 'retry_set_domain':\n return 'Retry the set-domain transaction against the already-created lease.';\n case 'salvage_without_domain':\n return 'Keep the lease without the requested custom domain.';\n case 'cancel_lease':\n return 'Submit a cancel-lease transaction (pre-active terminal).';\n case 'close_lease':\n return 'Submit a close-lease transaction (post-active or pre-active terminal).';\n }\n}\n\ninterface PersistArgs {\n leaseUuid: string;\n image: string;\n size: string;\n metaHash: string;\n chainId: string;\n manifestJson: string;\n customDomain?: string;\n customDomainService?: string;\n dataDir?: string;\n callbacks: DeployAppCallbacks;\n}\n\nasync function tryPersistManifest(\n args: PersistArgs,\n): Promise<string | undefined> {\n if (!args.dataDir) return undefined;\n try {\n // Dynamic import keeps save-manifest's `node:fs` dep out of the\n // platform-neutral build path until needed.\n const { saveManifest } = await import('./internals/save-manifest.js');\n const result = await saveManifest({\n leaseUuid: args.leaseUuid,\n image: args.image,\n size: args.size,\n metaHash: args.metaHash,\n chainId: args.chainId,\n manifestJson: args.manifestJson,\n dataDir: args.dataDir,\n ...(args.customDomain ? { customDomain: args.customDomain } : {}),\n ...(args.customDomainService\n ? { customDomainServiceName: args.customDomainService }\n : {}),\n });\n args.callbacks.onProgress?.({\n kind: 'manifest_saved',\n leaseUuid: args.leaseUuid,\n manifestPath: result.manifestPath,\n });\n return result.manifestPath;\n } catch {\n // Step-16 contract: save-fail still returns success but `onProgress\n // (manifest_saved)` is NOT emitted; result.manifestPath stays empty.\n return undefined;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"deploy-app.js","names":["previewInput","editedPreviewInput","fredDeployApp","decodeLeaseState"],"sources":["../src/deploy-app.ts"],"sourcesContent":["/**\n * Public entry point: orchestrate a Manifest-Network app deployment from\n * a typed `AppDeploySpec` through the plan/confirm/broadcast/save flow.\n *\n * Architect's α-locked composition (post-PR-3 sub-plan Q1):\n *\n * Happy path: `fredDeployApp` (workspace MCP-tool function) is called\n * atomically for create-lease + manifest upload + (optional) set-\n * item-custom-domain. agent-core wraps the call with planning, user\n * confirmation, progress events, and post-success persistence.\n *\n * Recovery path: when fred's atomic deployApp throws or the lease\n * reaches a non-recoverable state, agent-core renders a recovery\n * prompt (typed `RecoveryOption[]`), invokes `onFailure`, and\n * dispatches the user's `RecoveryChoice` to inline closures that\n * call core's decomposed primitives (`setItemCustomDomain` for\n * `retry_set_domain`; `stopApp` for `close_lease`).\n *\n * E-hybrid runtime-context (post-PR-3 sub-plan Q5):\n *\n * `opts: DeployAppOptions` carries `clientManager` (chain ops),\n * `walletProvider` (ADR-036 auth-token construction), optional\n * `fetchFn` (HTTP override for fred's upload), and the chain-data /\n * denomMap injection for humanization. agent-core composes the\n * auth-token callbacks internally from `walletProvider` so callers\n * don't need to know about ADR-036 plumbing.\n *\n * Auth-callback construction follows fred's `AuthTokenService` pattern\n * (verified against `packages/fred/src/http/auth.ts` per TL2.1 silent-\n * fix discipline):\n *\n * 1. `timestamps.next()` → monotonic replay-safe timestamp.\n * 2. `createSignMessage(address, leaseUuid, timestamp)` → message.\n * 3. `walletProvider.signArbitrary(address, message)` → `{ pub_key,\n * signature }` (cosmjs convention; `pub_key.value` is base64).\n * 4. `createAuthToken(address, leaseUuid, timestamp, pub_key.value,\n * signature[, metaHashHex])` → token string.\n */\n\nimport {\n asLeaseUuid,\n asProviderUuid,\n cosmosEstimateFee,\n ManifestMCPError,\n ManifestMCPErrorCode,\n noopLogger,\n parseFqdn,\n parseLeaseUuid,\n type ReadCtx,\n resolveSku,\n type SkuCandidate,\n setItemCustomDomain,\n stopApp,\n} from '@manifest-network/manifest-mcp-core';\nimport {\n AuthTimestampTracker,\n type BuildManifestPreviewInput,\n buildManifestPreview,\n type ConnectionDetails,\n checkDeploymentReadiness,\n createAuthToken,\n createLeaseDataSignMessage,\n createSignMessage,\n type DeployAppResult as FredDeployAppResult,\n type FredLeaseStatus,\n type FredReadCtx,\n fetchActiveLease,\n deployApp as fredDeployApp,\n pollLeaseUntilReady,\n resolveProviderUrl,\n uploadLeaseData,\n waitForAppReady,\n} from '@manifest-network/manifest-mcp-fred';\nimport { makeCancellationScope } from './internals/cancellation.js';\nimport { classifyDeployError } from './internals/classify-deploy-error.js';\nimport {\n classifyDeployResponse,\n type DeployResponseShape,\n} from './internals/classify-deploy-response.js';\nimport {\n extractRunningEndpoints,\n formatEndpointAsUrl,\n normalizeFredUrl,\n} from './internals/connection.js';\nimport { evaluateReadinessFromFredResponse } from './internals/evaluate-readiness-from-fred.js';\nimport {\n EMPTY_DENOM_MAP,\n loadChainDenomMap,\n} from './internals/humanize-denom.js';\nimport { decode as decodeLeaseState } from './internals/lease-state.js';\nimport { renderDeploymentPlan } from './internals/render-deployment-plan.js';\nimport { renderIntentRecap } from './internals/render-intent-recap.js';\nimport { renderPartialSuccessPrompt } from './internals/render-partial-success-prompt.js';\nimport {\n isStackSpec,\n summarizeSpec,\n validateSpec,\n} from './internals/spec-normalize.js';\nimport type {\n AppDeploySpec,\n DenomMap,\n DeployAppCallbacks,\n DeployAppOptions,\n DeployResult,\n FailureEnvelope,\n FeeEstimate,\n LeaseStateName,\n Plan,\n Readiness,\n RecoveryChoice,\n RecoveryOption,\n RecoveryOptionId,\n ServiceConfig,\n} from './types.js';\n\n/**\n * Orchestrate a deployment. See module-level docstring for the architect-\n * locked composition + E-hybrid runtime-context contract.\n *\n * @throws `ManifestMCPError(INVALID_CONFIG)` for spec / wallet validation.\n * @throws `ManifestMCPError(OPERATION_CANCELLED)` when `onConfirm` returns\n * `'no'` or `onPlan` returns `'cancel'` (deliberate user cancellation —\n * ENG-272).\n *\n * Errors from fred's broadcast or core's recovery primitives surface as\n * typed `ManifestMCPError`s. Partial-success failures with applicable\n * recovery options route through `onFailure(envelope, options)` — the\n * callback's return value drives recovery dispatch via the inline\n * closures in `dispatchRecovery`. Non-partial or inform-only failures\n * (no recovery choices to present, per `handleBroadcastFailure`'s F3\n * branch) throw directly as `ManifestMCPError(TX_FAILED)` without\n * invoking `onFailure`.\n */\nexport async function deployApp(\n spec: AppDeploySpec,\n callbacks: DeployAppCallbacks,\n opts: DeployAppOptions,\n): Promise<DeployResult> {\n // --- Input validation -----------------------------------------------\n try {\n validateSpec(spec);\n } catch (err) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n err instanceof Error ? err.message : `Invalid spec: ${String(err)}`,\n );\n }\n if (typeof opts.walletProvider.signArbitrary !== 'function') {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n 'opts.walletProvider must implement signArbitrary for ADR-036 auth tokens.',\n );\n }\n\n // Cancellation seam (ENG-310 / D4, shared via internals/cancellation.ts — ENG-374).\n // Race ONLY pre-broadcast interactive callbacks (onResolveSku/onPlan/onConfirm);\n // post-broadcast awaits route into recovery (D4.6), never cancel. `signal` is\n // also forwarded into fred's DeployCallOptions below.\n const { signal, throwIfCancelled, race } = makeCancellationScope({\n opts,\n onProgress: callbacks.onProgress,\n opLabel: 'Deployment',\n broadcasts: true,\n });\n throwIfCancelled();\n\n // --- Address-source consistency guard -------------------------------\n // Copilot review fix (PR #58 r3248900328): `opts.walletProvider` and\n // `opts.clientManager` are independently-injected runtime objects.\n // The readiness check + ADR-036 auth-token signing read the address\n // from `walletProvider`; fred's atomic `deployApp` (create-lease +\n // manifest upload) reads it from `clientManager`. If the two are\n // bound to different wallets (misconfiguration / copy-paste in\n // host-surface composition / multi-tenant test rig), readiness is\n // evaluated for wallet A while create-lease + upload execute as\n // wallet B — orphaning a lease on wallet B with auth tokens signed\n // by wallet A (provider auth-fails after the chain tx confirms).\n // Resolve both up-front, fail fast on mismatch, then reuse the\n // single value as the canonical `tenantAddress` for the rest of\n // the orchestration.\n const walletAddress = await opts.walletProvider.getAddress();\n const clientAddress = await opts.clientManager.getAddress();\n if (walletAddress !== clientAddress) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `opts.walletProvider and opts.clientManager are bound to different addresses ` +\n `(walletProvider=${walletAddress}, clientManager=${clientAddress}); they must reference the same wallet ` +\n `to avoid creating an orphaned lease on the clientManager wallet when ADR-036 auth (signed by walletProvider) fails.`,\n );\n }\n const tenantAddress = walletAddress;\n\n // --- Resolve denom map for humanization -----------------------------\n // I/O at orchestrator boundary (Path-Bii principle): callers may\n // pre-load via `denomMap`, point at `chainDataFile`, or omit both\n // (no-op map → raw on-chain denom rendering downstream).\n const denomMap: DenomMap =\n opts.denomMap ??\n (opts.chainDataFile\n ? await loadChainDenomMap(opts.chainDataFile)\n : EMPTY_DENOM_MAP);\n\n // --- Active-chain detection -----------------------------------------\n // The active chain (testnet / mainnet) drives intent-recap's mainnet\n // warning + parts of the Plan rendering. CosmosClientManager exposes\n // the bound chainId; we map to the canonical user-facing name.\n const chainId = opts.clientManager.getConfig().chainId;\n const activeChain: 'testnet' | 'mainnet' = /mainnet|main/i.test(chainId)\n ? 'mainnet'\n : 'testnet';\n\n // --- Readiness evaluation -------------------------------------------\n // fred's checkDeploymentReadiness takes (ctx: ReadCtx, address, input),\n // where ReadCtx = { query, chain, logger } (spec §5.4). It forwards the\n // ctx to core's getBalance → withReadSignal → ctx.chain.acquireRateLimit,\n // so the ctx MUST carry the clientManager as `chain` (a bare queryClient\n // would crash). `tenantAddress` was resolved + validated as consistent\n // across walletProvider/clientManager in the address-source guard above.\n const queryClient = await opts.clientManager.getQueryClient();\n const readCtx: ReadCtx = {\n query: queryClient,\n chain: opts.clientManager,\n logger: noopLogger,\n };\n\n // --- SKU pin resolution (ENG-258) -----------------------------------\n // Resolve the requested `size` to a single concrete (skuUuid,\n // providerUuid) pin ONCE, BEFORE readiness/plan/fee/broadcast, so all\n // four reference the same SKU. Ambiguity routes through `onResolveSku`\n // (interactive) or re-throws SKU_AMBIGUOUS (headless). Defined as a\n // closure so it captures `readCtx` + `callbacks` and can be reused\n // by the post-edit re-plan branch (an edit can change size/provider).\n //\n // Returns `{ pin, elicited }` where `elicited` is true ONLY when\n // `onResolveSku` was actually invoked (i.e. an ambiguous-name\n // interactivity happened). The caller uses `elicited` to decide whether\n // to stamp the chosen pin onto `confirmedSpec` — stamping is only\n // needed to suppress a re-elicit on the post-edit re-plan; non-elicited\n // resolutions (unique-name or UUID-direct) don't need it and shouldn't\n // carry a stale pin if the user later issues a `replace_spec` edit that\n // changes the size.\n const resolvePin = async (\n s: AppDeploySpec,\n ): Promise<{ pin: SkuCandidate; elicited: boolean }> => {\n const providerUuid = requestedProviderUuid(s);\n const skuUuid = requestedSkuUuid(s);\n try {\n const pin = await resolveSku(readCtx, {\n size: requestedSize(s),\n ...(providerUuid !== undefined ? { providerUuid } : {}),\n ...(skuUuid !== undefined ? { skuUuid } : {}),\n });\n return { pin, elicited: false };\n } catch (err) {\n if (\n err instanceof ManifestMCPError &&\n err.code === ManifestMCPErrorCode.SKU_AMBIGUOUS &&\n callbacks.onResolveSku\n ) {\n const candidates = (err.details?.candidates as SkuCandidate[]) ?? [];\n callbacks.onProgress?.({ kind: 'sku_ambiguous', candidates });\n const pick = await race(callbacks.onResolveSku(candidates));\n const pin = await resolveSku(readCtx, {\n size: requestedSize(s),\n skuUuid: pick.skuUuid,\n providerUuid: pick.providerUuid,\n });\n return { pin, elicited: true };\n }\n throw err;\n }\n };\n let { pin: pinned, elicited: pinElicited } = await resolvePin(spec);\n\n // FIX 1 (ENG-258 review): `pinned.name` is the RESOLVED SKU's on-chain\n // name. When a deploy is pinned by `skuUuid` (or by provider) whose\n // on-chain name differs from the user's requested `size` (or size was\n // omitted → defaulted to 'small'), `requestedSize(spec)` no longer\n // matches the resolved SKU. fred's `evaluateReadiness` gate\n // (`skuCandidates.some(c => c.name === inputs.size)`) would then fail\n // and block a valid pin. Thread `pinned.name` — not `requestedSize` —\n // as the canonical SKU name into every downstream consumer (readiness,\n // plan render, fred input, persisted manifest). `requestedSize(spec)`\n // survives ONLY as the input to `resolvePin` (the user's request).\n const readinessRaw = await checkDeploymentReadiness(readCtx, tenantAddress, {\n image: primaryImage(spec),\n size: pinned.name,\n providerUuid: pinned.providerUuid,\n skuUuid: pinned.skuUuid,\n });\n // `readiness` is `let`-bound because the post-edit recompute branch\n // re-evaluates it against the edited spec (Copilot r3267373084 — see\n // the recall block inside the `onPlan` `verdict !== 'confirm'` arm).\n let readiness: Readiness = evaluateReadinessFromFredResponse(\n readinessRaw,\n opts.clientManager.getConfig().gasPrice ?? '1umfx',\n denomMap,\n tenantAddress,\n );\n callbacks.onProgress?.({ kind: 'readiness_evaluated', readiness });\n if (readiness.status === 'block') {\n const envelope: FailureEnvelope = {\n outcome: 'failed',\n reason: `Readiness check failed: ${readiness.reasons.join('; ')}`,\n };\n // F3 fix: align with verify-recover's pattern — inform-only branch\n // (no recovery choices available) throws directly without calling\n // onFailure. Caller surfaces the error via the thrown\n // ManifestMCPError; no choice to present.\n void envelope;\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `Readiness check failed: ${readiness.reasons.join('; ')}`,\n );\n }\n\n // --- Plan assembly --------------------------------------------------\n // Build manifest preview (provides meta_hash for Plan + later save).\n // These are `let`-bound because the onPlan callback may return a\n // PlanEdit that triggers a re-plan (C2 fix below — single-iteration\n // plan-edit must recompute preview/summary/fees/block against the\n // edited spec; otherwise the manifest persistence at step 16 uses\n // the stale pre-edit preview).\n // `buildManifestPreview` reads ONLY the manifest STRUCTURED_FIELDS and\n // ignores size/customDomain/serviceName/skuUuid/providerUuid — so passing\n // the spec (via a variable, not a fresh literal, to dodge excess-property\n // checks) is meta-hash-safe and keeps preview ≡ deploy by construction\n // (both build the manifest from the same STRUCTURED_FIELDS). D3 / ENG-310.\n const previewInput: BuildManifestPreviewInput = spec;\n let preview = await buildManifestPreview(previewInput);\n\n // Fee estimation for create-lease (always) + set-item-custom-domain\n // (when customDomain set). Lean port: cosmosEstimateFee invocation\n // details encapsulated in a helper to keep this fn focused on flow.\n let summary = summarizeSpec(spec);\n let fees = await estimateFees(\n opts,\n spec,\n preview.meta_hash_hex,\n pinned.skuUuid,\n );\n let plan: Plan = { summary, readiness, fees };\n\n // --- Render plan + onPlan callback ----------------------------------\n // FIX 2 (ENG-258 review): stamp the resolved pin identity onto the\n // working spec ONLY when an ambiguous SKU was resolved interactively\n // via `onResolveSku` (i.e. `pinElicited === true`). The stamp prevents\n // re-eliciting on the post-edit re-plan: an `edit_env` edit spreads\n // the prior spec and preserves the stamped skuUuid/providerUuid, so\n // the by-UUID second resolve skips the ambiguity entirely.\n //\n // When the SKU was resolved by a UNIQUE name or by explicit UUID\n // (non-elicited paths), the original `spec` already carries the right\n // identity — stamping is unnecessary and risks locking in a stale pin\n // if a `replace_spec` edit changes `size` (that edit replaces the\n // spec wholesale anyway, but avoiding the stamp keeps the non-elicited\n // path behaviorally minimal).\n let confirmedSpec: AppDeploySpec = pinElicited\n ? { ...spec, skuUuid: pinned.skuUuid, providerUuid: pinned.providerUuid }\n : spec;\n const block = renderDeploymentPlan({\n plan,\n denomMap,\n image: primaryImage(spec),\n // FIX 1: show the RESOLVED SKU name (honest when pinned by uuid).\n size: pinned.name,\n metaHash: preview.meta_hash_hex,\n customDomain: customDomainOf(spec),\n customDomainService: customDomainServiceOf(spec),\n providerUuid: pinned.providerUuid,\n });\n callbacks.onProgress?.({ kind: 'deployment_plan_rendered', block });\n if (callbacks.onPlan) {\n const verdict = await race(callbacks.onPlan(plan));\n if (verdict === 'cancel') {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.OPERATION_CANCELLED,\n 'User cancelled deployment at plan step.',\n );\n }\n if (verdict !== 'confirm') {\n // PlanEdit — apply edits to the spec, then re-plan against the\n // edited spec so downstream consumers (intent recap, fred input,\n // manifest persistence) all see the post-edit values.\n //\n // C2 fix (post-edit propagation gap): the prior single-iteration\n // implementation updated `confirmedSpec` but kept `preview` /\n // `summary` / `fees` / `plan` based on the original spec, which\n // caused the manifest persistence at step 16 to record the stale\n // pre-edit `meta_hash_hex` / `manifest_json` while fred's\n // deployApp broadcast used the edited spec — a real mismatch.\n // Re-planning closes the gap. Multi-iteration plan-edit (loop\n // back to onPlan with the new plan) remains a PR-3.x follow-up;\n // this fix addresses single-iteration freshness only.\n confirmedSpec = applyPlanEdit(confirmedSpec, verdict);\n // Copilot review fix (PR #58 r3249684686): re-validate the post-\n // edit spec at the agent-core boundary. `validateSpec` runs once\n // on the original input at the top of `deployApp`; without this\n // second invocation a `replace_spec` edit returning an invalid\n // spec (portless single-service, out-of-range port, stack-\n // without-services, stack-with-customDomain-missing-serviceName,\n // etc.) flows through to `buildManifestPreview` / fred's\n // broadcast and surfaces only as a mid-orchestration error.\n // Placed BEFORE the recompute so we don't spend a\n // `buildManifestPreview` round-trip on a known-bad spec. Wraps\n // `TypeError` from `validateSpec` into `INVALID_CONFIG` to match\n // the initial-input-validation convention at the top of this fn.\n try {\n validateSpec(confirmedSpec);\n } catch (err) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n err instanceof Error\n ? `Post-edit spec failed validation: ${err.message}`\n : `Post-edit spec failed validation: ${String(err)}`,\n );\n }\n // Copilot review fix (PR #58 r3267373084): readiness recall.\n // The original-spec `readiness` (captured pre-`onPlan`) gates\n // SKU + credit-balance pre-flight; a `replace_spec` /\n // `edit_env` edit that changes `image` or `size` can produce a\n // different readiness outcome. Without this recall, the\n // post-edit `plan` still carries the original-spec readiness,\n // which mis-renders the plan and may bypass a `status: 'block'`\n // condition specific to the edited shape.\n //\n // ENG-185 #1 (sub-PR B): the always-`'ok'` stub\n // `evaluateReadinessFromRaw` has been replaced by\n // `evaluateReadinessFromFredResponse` (the canonical evaluator\n // wired through the snake_case → camelCase translator). Both\n // call sites now fire the `status === 'block'` short-circuit\n // correctly (initial-spec L207 + post-edit recall below).\n // Re-resolve the SKU pin for the edited spec (ENG-258): an edit can\n // change `size` / `providerUuid`, so the pin threaded into the\n // post-edit readiness/fee/plan/broadcast must reflect the edit.\n // Track elicitation again: if the post-edit resolve is also\n // interactive, stamp the new pin so a further re-plan won't\n // re-elicit for the same choice.\n ({ pin: pinned, elicited: pinElicited } =\n await resolvePin(confirmedSpec));\n if (pinElicited) {\n confirmedSpec = {\n ...confirmedSpec,\n skuUuid: pinned.skuUuid,\n providerUuid: pinned.providerUuid,\n };\n }\n const editedReadinessRaw = await checkDeploymentReadiness(\n readCtx,\n tenantAddress,\n {\n image: primaryImage(confirmedSpec),\n // FIX 1: canonical resolved SKU name, not the user's requested size.\n size: pinned.name,\n providerUuid: pinned.providerUuid,\n skuUuid: pinned.skuUuid,\n },\n );\n readiness = evaluateReadinessFromFredResponse(\n editedReadinessRaw,\n opts.clientManager.getConfig().gasPrice ?? '1umfx',\n denomMap,\n tenantAddress,\n );\n callbacks.onProgress?.({ kind: 'readiness_evaluated', readiness });\n if (readiness.status === 'block') {\n // Same fail-fast as the original-spec readiness gate above.\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `Post-edit readiness check failed: ${readiness.reasons.join('; ')}`,\n );\n }\n const editedPreviewInput: BuildManifestPreviewInput = confirmedSpec;\n preview = await buildManifestPreview(editedPreviewInput);\n summary = summarizeSpec(confirmedSpec);\n fees = await estimateFees(\n opts,\n confirmedSpec,\n preview.meta_hash_hex,\n pinned.skuUuid,\n );\n plan = { summary, readiness, fees };\n // Copilot review fix (PR #58 r3237308843): the pre-edit\n // `deployment_plan_rendered` event already fired with the original\n // spec's block. After applying the edit + recomputing preview /\n // summary / fees / plan, re-render and emit a fresh block so\n // consumers see the post-edit plan alongside the post-edit intent\n // recap. Without this re-emit, the event stream is inconsistent\n // with the user's confirmation surface and the persisted manifest.\n const editedBlock = renderDeploymentPlan({\n plan,\n denomMap,\n image: primaryImage(confirmedSpec),\n // FIX 1: canonical resolved SKU name, not the user's requested size.\n size: pinned.name,\n metaHash: preview.meta_hash_hex,\n customDomain: customDomainOf(confirmedSpec),\n customDomainService: customDomainServiceOf(confirmedSpec),\n providerUuid: pinned.providerUuid,\n });\n callbacks.onProgress?.({\n kind: 'deployment_plan_rendered',\n block: editedBlock,\n });\n }\n }\n\n // --- Intent recap + onConfirm callback ------------------------------\n const recapText = renderIntentRecap({ spec: confirmedSpec, activeChain });\n const recapBlock = { text: recapText };\n if (callbacks.onConfirm) {\n const yesNo = await race(callbacks.onConfirm(recapBlock));\n if (yesNo !== 'yes') {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.OPERATION_CANCELLED,\n 'User declined to proceed at intent-recap step.',\n );\n }\n }\n callbacks.onProgress?.({ kind: 'user_confirmed' });\n\n // --- Compose ADR-036 auth callbacks (E-hybrid: agent-core internalizes) ---\n const signArbitrary = opts.walletProvider.signArbitrary.bind(\n opts.walletProvider,\n );\n const timestamps = new AuthTimestampTracker();\n const getAuthToken = async (\n address: string,\n leaseUuid: string,\n ): Promise<string> => {\n const ts = await timestamps.next();\n const message = createSignMessage(address, leaseUuid, ts);\n const { pub_key, signature } = await signArbitrary(address, message);\n return createAuthToken(address, leaseUuid, ts, pub_key.value, signature);\n };\n const getLeaseDataAuthToken = async (\n address: string,\n leaseUuid: string,\n metaHashHex: string,\n ): Promise<string> => {\n const ts = await timestamps.next();\n const message = createLeaseDataSignMessage(leaseUuid, metaHashHex, ts);\n const { pub_key, signature } = await signArbitrary(address, message);\n return createAuthToken(\n address,\n leaseUuid,\n ts,\n pub_key.value,\n signature,\n metaHashHex,\n );\n };\n\n // --- Broadcast: fred's atomic deployApp (architect α-locked) -------\n // Last pre-broadcast cancellation boundary: once `fredDeployApp` is called\n // the tx may commit on-chain, so this is the final point an abort can cancel\n // cleanly (no lease created). After this, the signal only bounds fred's own\n // await (via `abortSignal`) — a post-broadcast abort routes into recovery\n // (tx MAY STILL COMMIT → re-query; see core's withTxConfirmation contract).\n throwIfCancelled();\n callbacks.onProgress?.({ kind: 'deploy_app_broadcast' });\n // D3 / ENG-310: loss-free broadcast. A SHALLOW spread of `confirmedSpec`\n // forwards every rich AppDeploySpec field (user/tmpfs/health_check/\n // stop_grace_period/init/expose/labels/storage/depends_on, plus the stack\n // services map verbatim) — the old build-fred-input mapper silently\n // dropped them. We then stamp the RESOLVED SKU identity (thread resolved\n // identity, not raw hints): `size` becomes the on-chain SKU name (so fred\n // records the lease item consistently with the readiness/plan above), and\n // skuUuid/providerUuid become the authoritative pin.\n const fredInput: AppDeploySpec = {\n ...confirmedSpec,\n size: pinned.name,\n skuUuid: pinned.skuUuid,\n providerUuid: pinned.providerUuid,\n };\n let fredResult: FredDeployAppResult;\n try {\n fredResult = await fredDeployApp(\n {\n query: queryClient,\n chain: opts.clientManager,\n fetch: opts.fetchFn ?? globalThis.fetch,\n logger: noopLogger,\n providerAuth: {\n providerToken: (i) => getAuthToken(i.address, i.leaseUuid),\n leaseDataToken: (i) =>\n getLeaseDataAuthToken(i.address, i.leaseUuid, i.metaHashHex),\n },\n },\n fredInput,\n // Forward the effective signal so fred's own await is abort-bounded\n // (it surfaces OPERATION_CANCELLED with the tx-MAY-have-committed\n // semantics; agent-core does NOT race this — D4.6).\n signal ? { abortSignal: signal } : {},\n );\n } catch (err) {\n // ENG-185 sub-PR E: thread a `RecoveryContext` so the\n // `retry_set_domain` branch can decompose the deploy into\n // `setItemCustomDomain` + `uploadLeaseData` + `pollLeaseUntilReady`.\n // Captured values mirror what fred's atomic `deployApp` had: the\n // ADR-036 auth closures, the manifest payload + hash, and the chain\n // identity (for downstream `tryPersistManifest`).\n const recoveryCtx: RecoveryContext = {\n manifestJson: preview.manifest_json,\n metaHash: preview.meta_hash_hex,\n getAuthToken,\n getLeaseDataAuthToken,\n tenantAddress,\n chainId,\n denomMap,\n // FIX 1: thread the RESOLVED SKU name so the recovery path's\n // manifest persistence records the same name the broadcast used.\n skuName: pinned.name,\n };\n return await handleBroadcastFailure(\n err,\n confirmedSpec,\n callbacks,\n opts,\n recoveryCtx,\n );\n }\n\n // Live-state + live-connection trackers (Copilot fix-3, post-PR-D):\n // the pre-fix code merged `pollResult.state` (a JSON-encoded string from\n // `waitForAppReady`) into `fredResult.state` (numeric `LeaseState`) via\n // a width-erasing cast, hiding a type mismatch. Same for `pollResult.status`\n // (`FredLeaseStatus`) → `fredResult.connection` (`ConnectionDetails`):\n // runtime worked (duck-typed reads via `extractRunningEndpoints` /\n // `hasRunningInstances` / `decodeLeaseState`, all of which accept the\n // wider shape), but the type contract was violated.\n //\n // The fix: track the FINAL (post-poll if applicable, else initial)\n // state + connection in two separate locals with HONEST types. Each\n // upstream source has a typed slot:\n // - `fredResult.state` (`LeaseState`) when no polling fired.\n // - `pollResult.status.state` (`LeaseState`) when polling did fire —\n // NOTE: `pollResult.state` is the STRING form (JSON-encoded), wrong\n // source. The numeric form lives one level deeper.\n // - `fredResult.connection` (`ConnectionDetails | undefined`) initial.\n // - `pollResult.status` (`FredLeaseStatus`) post-poll.\n let liveState: FredDeployAppResult['state'] | undefined = fredResult.state;\n let liveConnection: ConnectionDetails | FredLeaseStatus | undefined =\n fredResult.connection;\n\n // --- Classify happy-path result + full routing (ENG-185 sub-PR D) -\n // Architect's α-lock: fred returns after tx + manifest upload succeed,\n // NOT after the app is observably running. So `'needs_wait'` IS an\n // expected happy-path return shape (lease created, manifest uploaded,\n // container not yet started by the provider) — and `'failed'` covers\n // the terminal-state-on-return edge (e.g. REJECTED, when the chain\n // invalidated the lease between create and return).\n //\n // Routing (per architect's Q7 pseudocode):\n // - `'failed'` → throw TX_FAILED with the classifier's\n // `errorSummary` (F3 pattern, no onFailure for\n // this kind of failure — there's no recovery\n // choice once fred returns a terminal-state\n // response from a successful broadcast).\n // - `'needs_wait'` → poll `wait_for_app_ready`, emit\n // `polling_for_readiness` events per onProgress\n // sample, then RE-classify the post-poll result\n // (Defense #2 — rare provider race where\n // pollLeaseUntilReady exits on state==ACTIVE\n // without a running instance). On post-poll\n // success, merge the polled fields back into\n // `fredResult` so downstream DeployResult\n // construction sees the final state/connection.\n // - `'active'` → fall through to `app_ready_confirmed` + persist.\n let classification = classifyDeployResponse(fredResult);\n callbacks.onProgress?.({\n kind: 'deploy_response_classified',\n outcome: classification.outcome,\n });\n\n if (classification.outcome === 'failed') {\n // F3 pattern (mirrors handleBroadcastFailure's empty-options path):\n // throw TX_FAILED directly; no onFailure invocation. The envelope\n // is constructed for a future logging hook but otherwise unused.\n const reason =\n classification.errorSummary ??\n `fred deployApp returned failed outcome for lease ${\n classification.leaseUuid ?? '<no-uuid>'\n }`;\n const envelope: FailureEnvelope = { outcome: 'failed', reason };\n void envelope;\n throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);\n }\n\n if (classification.outcome === 'needs_wait') {\n // Defense #1: classifier guarantees leaseUuid when needs_wait\n // (`classify-deploy-response.ts`: !leaseUuid → outcome='failed'),\n // but the TS type doesn't narrow it. Defensive throw documents\n // the invariant for future maintainers + catches any classifier\n // regression that would break the assumption.\n if (!classification.leaseUuid) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n 'Internal invariant: classifier returned needs_wait without leaseUuid.',\n );\n }\n const leaseUuid = classification.leaseUuid;\n // queryClient is already bound at L193 (function-level); reuse it.\n // (Copilot #1 fix: removed shadowing redeclaration. CosmosClientManager\n // keys its query client as a singleton so there was no behavioral\n // difference, but shadowing is a maintenance trap.)\n const pollStartMs = Date.now();\n let attempt = 0;\n\n let pollResult: Awaited<ReturnType<typeof waitForAppReady>>;\n try {\n pollResult = await waitForAppReady(\n {\n query: queryClient,\n chain: opts.clientManager,\n fetch: opts.fetchFn ?? globalThis.fetch,\n logger: noopLogger,\n providerAuth: {\n providerToken: (i) => getAuthToken(i.address, i.leaseUuid),\n leaseDataToken: (i) =>\n getLeaseDataAuthToken(i.address, i.leaseUuid, i.metaHashHex),\n },\n },\n { address: tenantAddress, leaseUuid },\n {\n timeoutMs: opts.waitForReadyTimeoutMs ?? 480_000,\n onProgress: (status) => {\n attempt += 1;\n const stateName = decodeLeaseState(status.state);\n callbacks.onProgress?.({\n kind: 'polling_for_readiness',\n leaseUuid,\n attempt,\n elapsedMs: Date.now() - pollStartMs,\n ...(stateName !== undefined ? { state: stateName } : {}),\n });\n },\n },\n );\n } catch (err) {\n // ProviderApiError / timeout / TerminalChainStateError → F3 route.\n const reason =\n err instanceof Error\n ? `wait_for_app_ready failed for lease ${leaseUuid}: ${err.message}`\n : `wait_for_app_ready failed for lease ${leaseUuid}: ${String(err)}`;\n const envelope: FailureEnvelope = { outcome: 'failed', reason };\n void envelope;\n throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);\n }\n\n // Defense #2: re-classify post-poll. `pollLeaseUntilReady` exits on\n // state==ACTIVE but doesn't check running-instances; a rare provider-\n // side race could leave us at ACTIVE with no instances → outcome\n // 'needs_wait' on the re-classify. We treat that as TX_FAILED rather\n // than misleadingly emitting app_ready_confirmed + onComplete on a\n // non-running deploy.\n const postPollResponse: DeployResponseShape = {\n lease_uuid: pollResult.lease_uuid,\n provider_uuid: pollResult.provider_uuid,\n provider_url: pollResult.provider_url,\n state: pollResult.state,\n connection: pollResult.status,\n };\n classification = classifyDeployResponse(postPollResponse);\n if (classification.outcome !== 'active') {\n // Copilot fix-6: include `leaseUuid` in the fallback message so\n // log/user-report correlation matches the sibling\n // `waitForAppReady` catch path at L548-550. Diagnostic consistency\n // invariant — locked in by the Defense #2 test's\n // `expect(...).toContain(leaseUuid)` assertion. The\n // `errorSummary` path is unaffected; the classifier already\n // includes leaseUuid in its terminal-state summary\n // (`classify-deploy-response.ts:120`), but errorSummary fires only\n // for `outcome === 'failed'`. The no-errorSummary fallback\n // (this branch) fires when post-poll outcome is `'needs_wait'`\n // (Defense #2's race scenario) — that's the gap we're closing.\n const reason =\n classification.errorSummary ??\n `wait_for_app_ready returned for lease ${leaseUuid} but post-poll classifier outcome is ${classification.outcome}`;\n throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);\n }\n\n // Merge post-poll fields back into `fredResult` so downstream\n // DeployResult construction sees the final lease/provider identity.\n // lease_uuid / provider_uuid are chain-polled, trusted ids — they are\n // trust-cast (asLeaseUuid / asProviderUuid) to satisfy DeployResult's\n // branded wire types (brands erase at runtime; zero behavior change).\n // provider_url stays a plain string. The state + connection fields are\n // NOT merged here — they go into `liveState` and `liveConnection` so\n // each carries the type that matches its upstream source (no\n // width-erasing casts). See the live-tracker declarations above for\n // the full rationale.\n fredResult = {\n ...fredResult,\n lease_uuid: asLeaseUuid(pollResult.lease_uuid),\n provider_uuid: asProviderUuid(pollResult.provider_uuid),\n provider_url: pollResult.provider_url,\n };\n liveState = pollResult.status.state;\n liveConnection = pollResult.status;\n }\n\n // 'active' (initial OR post-poll merge): emit + fall through to persist.\n callbacks.onProgress?.({\n kind: 'app_ready_confirmed',\n leaseUuid: fredResult.lease_uuid,\n });\n\n // --- Persist manifest (best-effort; save-fail still emits success) -\n const persistedPath = await tryPersistManifest({\n leaseUuid: fredResult.lease_uuid,\n image: primaryImage(confirmedSpec),\n // FIX 1: persist the RESOLVED SKU name (matches what was broadcast).\n size: pinned.name,\n metaHash: preview.meta_hash_hex,\n chainId,\n manifestJson: preview.manifest_json,\n customDomain: fredResult.custom_domain,\n customDomainService: fredResult.service_name,\n dataDir: opts.dataDir,\n callbacks,\n });\n\n // --- Build typed DeployResult --------------------------------------\n // F1 fix: decode lease state via the canonical lease-state.decode()\n // (handles int + LEASE_STATE_* string + undefined paths exhaustively).\n //\n // C3 fix (defensive bias correction; checklist item #16): distinguish\n // absent state (undefined → default ACTIVE as defense-in-depth against\n // legacy/mocked shapes that bypass fred's required-state contract —\n // fred itself always sets `state` in `DeployAppResult`)\n // from UNRECOGNIZED state (decode returned undefined for a value that\n // WAS provided → likely a terminal/unknown chain emission that must\n // NOT be silently classified as ACTIVE). For the unrecognized case,\n // throw `INVALID_CONFIG` so callers see the empirical mismatch\n // instead of consuming a misleading ACTIVE.\n // Reads via `liveState` (Copilot fix-3): carries the post-poll\n // `pollResult.status.state` (numeric `LeaseState`) when the needs_wait\n // branch fired; falls back to `fredResult.state` for the direct-active\n // path. Effective type is `LeaseState | undefined` — numeric only after\n // the fix-3 type-tightening. The `undefined` branch handles the C3\n // defense-in-depth case above (legacy/mocked shapes that bypass fred's\n // required-state contract). The numeric branch decodes the enum via\n // `decodeLeaseState`; the `decoded === undefined` arm catches\n // UNRECOGNIZED enum values (defense-in-depth against future chain\n // emissions that add new states beyond the current `LeaseStateName`\n // union).\n let leaseStateDecoded: LeaseStateName;\n if (liveState === undefined) {\n leaseStateDecoded = 'LEASE_STATE_ACTIVE';\n } else {\n const decoded = decodeLeaseState(liveState);\n if (decoded === undefined) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `Unrecognized lease state from fred deployApp response: ${String(liveState)}. Cannot safely classify; refusing to silently coerce to ACTIVE.`,\n );\n }\n leaseStateDecoded = decoded;\n }\n\n // F4 fix: derive `urls` from `extractRunningEndpoints(connection)` for\n // multi-FQDN dedup (matches CJS pipeline behavior). fred's\n // `result.url` is a single derived URL; the full connection payload\n // exposes the canonical instance list.\n //\n // Copilot review fix (PR #58 r3249097136): when no FQDN can be\n // extracted from `connection`, fall back to `fredResult.url` THROUGH\n // the shared `normalizeFredUrl` helper. Raw values like\n // `'app.example.com:443'` now surface as\n // `'https://app.example.com:443/'`, matching the classifier's\n // (`classify-deploy-response.ts`) and renderer's (`format-success.ts`)\n // handling. Empty / scheme-less inputs are normalized consistently.\n // Reads via `liveConnection` (Copilot fix-3): carries\n // `pollResult.status` (FredLeaseStatus) when the needs_wait branch\n // fired, falls back to `fredResult.connection` (ConnectionDetails)\n // for the direct-active path. `extractRunningEndpoints` takes\n // `unknown` and walks `instances` / `services.*.instances` — both\n // shapes are accepted at runtime.\n const endpointUrls =\n extractRunningEndpoints(liveConnection).map(formatEndpointAsUrl);\n const fallbackUrl =\n typeof fredResult.url === 'string' ? normalizeFredUrl(fredResult.url) : '';\n const result: DeployResult = {\n leaseUuid: fredResult.lease_uuid,\n providerUuid: fredResult.provider_uuid,\n leaseState: leaseStateDecoded,\n urls:\n endpointUrls.length > 0\n ? endpointUrls\n : fallbackUrl.length > 0\n ? [fallbackUrl]\n : [],\n ...(fredResult.custom_domain\n ? { customDomain: fredResult.custom_domain }\n : {}),\n manifestPath: persistedPath ?? '',\n };\n callbacks.onProgress?.({ kind: 'success_rendered', result });\n callbacks.onComplete?.(result);\n return result;\n}\n\n// --- Helpers ---------------------------------------------------------\n\nfunction primaryImage(spec: AppDeploySpec): string {\n if (isStackSpec(spec)) {\n for (const svc of Object.values(spec.services)) {\n if (svc?.image) return svc.image;\n }\n return '';\n }\n return spec.image ?? '';\n}\n\nfunction requestedSize(spec: AppDeploySpec): string {\n // ENG-310: `size` is a REQUIRED field on the canonical AppDeploySpec —\n // the prior silent `'small'` default is gone (callers must pass an\n // explicit tier or pin `skuUuid`). This helper returns the user's\n // requested size verbatim; it feeds ONLY `resolvePin` (the SKU lookup\n // input). Downstream consumers use `pinned.name` (the RESOLVED SKU name).\n return spec.size;\n}\n\n/**\n * SKU disambiguator intent helpers. `providerUuid` / `skuUuid` are\n * first-class optional fields on `AppDeploySpec` (ENG-296, mirroring\n * ENG-275's typed `size`). Returns `undefined` for absent / empty values\n * so `resolveSku` only narrows when a real disambiguator is supplied.\n */\nfunction requestedProviderUuid(spec: AppDeploySpec): string | undefined {\n const v = spec.providerUuid;\n return typeof v === 'string' && v.length > 0 ? v : undefined;\n}\nfunction requestedSkuUuid(spec: AppDeploySpec): string | undefined {\n const v = spec.skuUuid;\n return typeof v === 'string' && v.length > 0 ? v : undefined;\n}\n\nfunction customDomainOf(spec: AppDeploySpec): string | undefined {\n return spec.customDomain;\n}\n\nfunction customDomainServiceOf(spec: AppDeploySpec): string | undefined {\n if (isStackSpec(spec)) return spec.serviceName;\n return undefined;\n}\n\nasync function estimateFees(\n opts: DeployAppOptions,\n spec: AppDeploySpec,\n metaHashHex: string, // SHA-256 hex digest of the canonical manifest JSON; threaded into create-lease estimate via the `--meta-hash` flag (mirrors fred's deploy path at packages/fred/src/tools/deployApp.ts:363)\n skuUuid: string, // ENG-258: pre-resolved SKU pin from the orchestrator; no second lookup here.\n): Promise<Plan['fees']> {\n // PR 3 fix-3 (B-narrowed-trimmed per architect ratification):\n // - REAL cosmosEstimateFee for create-lease (criterion-blocking).\n // - SET-DOMAIN emits `{notEstimated: true, reason}` sentinel (the\n // frozen-contract escape hatch designed for pre-broadcast lease-\n // UUID unavailability per ENG-128). Per ENG-185 #3 sub-PR C\n // (architect's verdict B): the chain rejects placeholder-UUID\n // simulation of `MsgSetItemCustomDomain` (keeper's `GetLease()`\n // fails first with ErrLeaseNotFound), so the sentinel is the\n // PERMANENT shape — not a TODO.\n\n // ENG-258: `skuUuid` is now a pre-resolved parameter (the orchestrator\n // resolves the pin ONCE via core's `resolveSku` so plan, fee, and\n // broadcast share one SKU). The prior in-function second lookup is gone.\n\n // ENG-185 #3 sub-PR C: mirror fred's deploy-time item creation verbatim\n // (`packages/fred/src/tools/deployApp.ts:336-341`). Stack specs create\n // ONE lease item per service (each with `${skuUuid}:1:${name}`); legacy\n // single-service specs create one bare `${skuUuid}:1`. The prior gate\n // on `spec.serviceName` underestimated multi-service stacks (only the\n // domain-target service was billed) and accidentally collapsed stacks\n // WITHOUT customDomain to legacy-mode args (`spec.serviceName` is only\n // set alongside customDomain — bug 2).\n //\n // Storage-SKU fee estimation is OUT OF SCOPE for ENG-310 (tracked\n // separately). The canonical `AppDeploySpec` now HAS a `storage?` field,\n // and the loss-free broadcast spread DOES forward it to fred — but this\n // agent-core fee estimate still bills only the compute SKU item(s); it\n // does not add a storage item. A pre-existing gap, now visible.\n const itemArgs: string[] = isStackSpec(spec)\n ? Object.keys(spec.services).map((name) => `${skuUuid}:1:${name}`)\n : [`${skuUuid}:1`];\n\n let createLeaseEstimate: Awaited<ReturnType<typeof cosmosEstimateFee>>;\n try {\n createLeaseEstimate = await cosmosEstimateFee(\n opts.clientManager,\n 'billing',\n 'create-lease',\n ['--meta-hash', metaHashHex, ...itemArgs],\n );\n } catch (err) {\n // Wrap the underlying failure with an agent-core-boundary message\n // for caller diagnostics. `core`'s `cosmosEstimateFee` (per\n // `packages/core/src/cosmos.ts`) throws across multiple sites with\n // different codes: `INVALID_CONFIG` for missing `gasPrice`,\n // `UNSUPPORTED_TX` for invalid module/subcommand,\n // `SIMULATION_FAILED` for actual simulation issues.\n //\n // Copilot review fix (PR #58 r3250192834): preserve the original\n // code when the underlying threw a typed `ManifestMCPError`;\n // fall back to `SIMULATION_FAILED` only for untyped failures.\n // The prior comment claimed code-preservation but the code\n // unconditionally cast to `SIMULATION_FAILED`.\n const msg = `Failed to estimate create-lease fee: ${err instanceof Error ? err.message : String(err)}`;\n if (err instanceof ManifestMCPError) {\n throw new ManifestMCPError(err.code, msg);\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.SIMULATION_FAILED, msg);\n }\n\n // FeeEstimateResult shape (per packages/core/src/types.ts):\n // { module, subcommand, gasEstimate: string, fee: { gas: string, amount: Coin[] } }\n // Map to typed `FeeEstimate { coins: Coin[], gas: number }` (Path-C\n // revision per a62cfd1).\n //\n // Copilot review fix (PR #58 r3250192734): use `fee.gas` (post-\n // `gasMultiplier`), NOT `gasEstimate` (raw simulation gas). The\n // `coins` were priced at `fee.gas`; displaying `gasEstimate` shows\n // a number ~33% lower than the price reflects under the default\n // 1.5x multiplier (per CLAUDE.md `COSMOS_GAS_MULTIPLIER`), creating\n // a visible inconsistency in the rendered plan.\n const createLease: FeeEstimate = {\n coins: createLeaseEstimate.fee.amount.map((c) => ({\n denom: c.denom,\n amount: c.amount,\n })),\n gas: Number(createLeaseEstimate.fee.gas),\n };\n\n // set-domain: emit `{notEstimated: true, reason}` sentinel per\n // architect-ratified counter-proposal + ENG-185 #3 verdict B (the\n // chain rejects placeholder-UUID simulation of `MsgSetItemCustomDomain`\n // — keeper's `GetLease()` runs first and fails with ErrLeaseNotFound;\n // verified against manifest-ledger v2.1.0). The frozen-contract type\n // includes this discriminated variant precisely for this case.\n //\n // Reason string mirrors the canonical form already pinned in\n // `internals/render-deployment-plan.test.ts` so producer + renderer\n // share the same wording.\n const hasDomain = typeof customDomainOf(spec) === 'string';\n return {\n createLease,\n ...(hasDomain\n ? {\n setDomain: {\n notEstimated: true,\n reason: 'no representative lease for pre-broadcast simulation',\n },\n }\n : {}),\n };\n}\n\nfunction applyPlanEdit(\n spec: AppDeploySpec,\n edit: Exclude<\n Awaited<ReturnType<NonNullable<DeployAppCallbacks['onPlan']>>>,\n 'confirm' | 'cancel'\n >,\n): AppDeploySpec {\n // PR 3 single-iteration: replace_spec replaces; edit_env merges env keys\n // into the matching service (or single-service spec).\n if (edit.kind === 'replace_spec') return edit.spec;\n if (edit.kind === 'edit_env') {\n // Copilot review fix (PR #58 r3266642610): the prior implementation\n // silently no-op'd two stack-spec cases (missing `edit.service` or\n // unknown service name), returning the unchanged spec while the\n // callback caller perceived the edit as applied. Worst case: deploy\n // proceeds with wrong env vars / secrets without an error signal.\n // Fail-fast at the boundary instead, so the user's `onPlan` callback\n // gets a clear `INVALID_CONFIG` for misuse. Uses\n // `Object.keys().includes()` for the membership check — matches\n // Fix 16's cross-package symmetry with fred (avoids prototype-chain\n // bypass via `'constructor'` / `'toString'` / etc.).\n if (isStackSpec(spec)) {\n if (edit.service === undefined) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n 'applyPlanEdit: edit_env on a stack spec requires `service` identifying which service to edit.',\n );\n }\n if (!Object.keys(spec.services).includes(edit.service)) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `applyPlanEdit: edit_env \\`service\\` \"${edit.service}\" is not a key in \\`services\\` (got: [${Object.keys(spec.services).join(', ')}]).`,\n );\n }\n const svc = spec.services[edit.service];\n // Membership check above guarantees `svc` is defined; the\n // non-null assertion documents that — TS narrows it after the\n // `includes` check, but the runtime invariant is in the\n // membership check.\n return {\n ...spec,\n services: {\n ...spec.services,\n [edit.service]: {\n ...(svc as ServiceConfig),\n env: { ...((svc as ServiceConfig).env ?? {}), ...edit.env },\n },\n },\n };\n }\n return { ...spec, env: { ...(spec.env ?? {}), ...edit.env } };\n }\n return spec;\n}\n\n/**\n * Recovery-path execution context. Threaded from `deployApp`'s enclosing\n * scope through `handleBroadcastFailure` → `dispatchRecovery` → the\n * per-choice closures. Internal (not exported, not in `types.ts`); each\n * field's upstream source lives in `deployApp`'s scope when the broadcast\n * failure surfaces, so the context is just a parameter bundle, not a\n * stateful object.\n *\n * Added by ENG-185 sub-PR E so the `retry_set_domain` branch can\n * decompose the deploy: it needs the manifest payload + hash for\n * `uploadLeaseData`, the auth closures for the upload + poll, and the\n * tenant/chain identity for `pollLeaseUntilReady` + downstream\n * `tryPersistManifest`. Other recovery branches (`salvage_without_domain`,\n * `cancel_lease`, `close_lease`) currently ignore the context — they\n * route through `stopApp` or a bare throw — but the widened signature\n * keeps future expansions cheap.\n */\ninterface RecoveryContext {\n manifestJson: string;\n metaHash: string;\n getAuthToken: (address: string, leaseUuid: string) => Promise<string>;\n getLeaseDataAuthToken: (\n address: string,\n leaseUuid: string,\n metaHashHex: string,\n ) => Promise<string>;\n tenantAddress: string;\n chainId: string;\n denomMap: DenomMap;\n /**\n * Resolved on-chain SKU name (FIX 1, ENG-258 review). The recovery\n * path's manifest persistence records this — not the user's requested\n * `size` — so a deploy pinned by `skuUuid` persists the actual SKU name.\n */\n skuName: string;\n}\n\nasync function handleBroadcastFailure(\n err: unknown,\n spec: AppDeploySpec,\n callbacks: DeployAppCallbacks,\n opts: DeployAppOptions,\n ctx: RecoveryContext,\n): Promise<DeployResult> {\n const requestedCustomDomain = customDomainOf(spec);\n\n // F2 fix: classify-deploy-error.ts is the canonical classifier — it\n // anchors the `PARTIAL_PREFIX` match, supports `{ error: {...} }`\n // SDK-wrapping envelopes, and threads `expectedCustomDomain` for\n // downstream rendering. Earlier inline `parsePartialSuccess` was a\n // reduced-robustness duplicate; replaced here per QA F2.\n const classified = classifyDeployError(err, {\n ...(requestedCustomDomain\n ? { expectedCustomDomain: requestedCustomDomain }\n : {}),\n });\n\n if (classified.outcome === 'partially_succeeded' && classified.leaseUuid) {\n const envelope: FailureEnvelope = {\n outcome: 'partially_succeeded',\n leaseUuid: classified.leaseUuid,\n ...(requestedCustomDomain ? { requestedCustomDomain } : {}),\n reason: classified.reason,\n };\n // CJS-parity: the lease was just created so it's typically PENDING.\n // The classifier doesn't decode state from the error envelope (the\n // chain emits state asynchronously after the create-lease tx); the\n // user prompt's \"state: <name>\" line is informational.\n const promptPayload = renderPartialSuccessPrompt({\n leaseUuid: classified.leaseUuid,\n decodedState: 'LEASE_STATE_PENDING',\n reason: classified.reason,\n ...(requestedCustomDomain ? { requestedCustomDomain } : {}),\n });\n const options: RecoveryOption[] = promptPayload.options.map((id) => ({\n id,\n label: recoveryOptionLabel(id),\n description: recoveryOptionDescription(id),\n }));\n // F3 fix: align with verify-recover's pattern — only invoke\n // onFailure when there's a choice to present. Empty options means\n // inform-only path; we throw instead of prompting.\n if (options.length > 0 && callbacks.onFailure !== undefined) {\n // Route β (ENG-185 #7): the rendered prompt body would otherwise be\n // dropped (only `options` flow into `onFailure`). Ride it on a\n // ProgressEvent emitted exactly once, immediately before the\n // (single) `onFailure` call — so it never fires on the inform-only\n // throw path below and `onFailure` stays invoked exactly once.\n callbacks.onProgress?.({\n kind: 'partial_success_prompt_rendered',\n prompt: promptPayload.prompt,\n leaseUuid: envelope.leaseUuid,\n });\n const choice = await callbacks.onFailure(envelope, options);\n return await dispatchRecovery(\n choice,\n envelope,\n spec,\n opts,\n callbacks,\n ctx,\n );\n }\n throw new ManifestMCPError(\n ManifestMCPErrorCode.TX_FAILED,\n classified.reason,\n );\n }\n\n // Non-partial failure: surface as `outcome: 'failed'` envelope.\n // F3 fix: skip onFailure when options is empty (inform-only path);\n // throw directly. Caller can still surface the error via the thrown\n // ManifestMCPError if they need to react.\n const envelope: FailureEnvelope = {\n outcome: 'failed',\n reason: classified.reason,\n };\n // Intentionally NOT invoking callbacks.onFailure?.(envelope, []) here\n // per F3 — no recovery choice to present.\n void envelope; // retained for future logging hook if needed\n throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, classified.reason);\n}\n\nasync function dispatchRecovery(\n choice: RecoveryChoice,\n envelope: FailureEnvelope,\n spec: AppDeploySpec,\n opts: DeployAppOptions,\n callbacks: DeployAppCallbacks,\n ctx: RecoveryContext,\n): Promise<DeployResult> {\n // Inline closures per gate-2 verdict (no separate strategy module).\n const leaseUuid =\n envelope.outcome === 'partially_succeeded' ? envelope.leaseUuid : '';\n switch (choice.id) {\n case 'retry_set_domain':\n return await retrySetDomainAndComplete(\n leaseUuid,\n spec,\n opts,\n callbacks,\n ctx,\n );\n case 'salvage_without_domain':\n throw new ManifestMCPError(\n ManifestMCPErrorCode.TX_FAILED,\n `salvage_without_domain: lease ${leaseUuid} retained without domain; caller should re-run troubleshootDeployment.`,\n );\n case 'cancel_lease':\n case 'close_lease': {\n await stopApp(\n { chain: opts.clientManager, logger: noopLogger },\n { leaseUuid: parseLeaseUuid(leaseUuid) },\n );\n throw new ManifestMCPError(\n ManifestMCPErrorCode.TX_FAILED,\n `${choice.id}: lease ${leaseUuid} closed.`,\n );\n }\n }\n throw new ManifestMCPError(\n ManifestMCPErrorCode.TX_FAILED,\n `Unknown recovery option: ${(choice as RecoveryChoice).id}`,\n );\n}\n\n/**\n * `retry_set_domain` recovery: decompose the deploy after the partial-\n * success failure. ENG-185 sub-PR E.\n *\n * Steps (mirrors fred's atomic `deployApp` minus the create-lease tx,\n * which already succeeded):\n * 1. `setItemCustomDomain` — broadcast the domain claim against the\n * pre-existing lease. Stack specs thread `serviceName` so the\n * tx targets the named lease item.\n * 2. `fetchActiveLease` + `resolveProviderUrl` — look up the provider\n * URL from the on-chain lease record (the partial-success error\n * envelope only carries `leaseUuid`).\n * 3. `uploadLeaseData` — push the manifest payload to the provider.\n * Uses the ADR-036 lease-data auth token (signed against the\n * manifest's meta-hash).\n * 4. `pollLeaseUntilReady` — poll until the provider reports ACTIVE +\n * running. Uses the LOWER-LEVEL primitive (not `waitForAppReady`)\n * so the already-resolved `providerApiUrl` and auth-token closure\n * pass through directly — no redundant on-chain queries (Copilot\n * fix-1, PR #71). Reuses D's canonical polling-emission pattern:\n * `onProgress` closure translates each `FredLeaseStatus` sample\n * into a typed `polling_for_readiness` ProgressEvent, default\n * 480_000ms timeout overridable via `opts.waitForReadyTimeoutMs`.\n * 5. Defense #2 parity (post-poll re-classify) — guard the\n * ACTIVE-with-no-instances race per D's pattern.\n * 6. Persist manifest (best-effort) + build typed `DeployResult` +\n * emit `app_ready_confirmed` + `success_rendered` + onComplete.\n *\n * Failure paths (sibling-parity wraps — every catch site surfaces\n * `retry_set_domain <primitive-name> failed for lease ${leaseUuid}:\n * ${err.message}` in the thrown message, matching D's L548-550 style).\n * Error-code policy: typed `ManifestMCPError`s flow through with their\n * original code preserved (precedent at `estimateFees` — see the\n * `cosmosEstimateFee` catch block); untyped errors default to\n * `TX_FAILED`. The post-poll re-classify path likewise prefixes BOTH\n * the errorSummary-set and the no-errorSummary branches with\n * `retry_set_domain` + leaseUuid (Copilot fix-4, PR #71):\n * - `setItemCustomDomain` throws → wrap with prefix + leaseUuid +\n * code preservation. Most likely cause: chain rejected the\n * set-item-custom-domain tx (FQDN validation, reserved-suffix\n * match, lease not active, etc.).\n * - `fetchActiveLease` / `resolveProviderUrl` throw → wrap with\n * prefix + leaseUuid.\n * - `uploadLeaseData` throws → wrap with prefix + leaseUuid.\n * - `pollLeaseUntilReady` throws → wrap with prefix + leaseUuid.\n * - Post-poll re-classify outcome !== 'active' → wrap both\n * branches: errorSummary-set (terminal-state response) AND\n * no-errorSummary fallback (ACTIVE-with-no-instances Defense #2\n * race) carry prefix + leaseUuid.\n */\nasync function retrySetDomainAndComplete(\n leaseUuid: string,\n spec: AppDeploySpec,\n opts: DeployAppOptions,\n callbacks: DeployAppCallbacks,\n ctx: RecoveryContext,\n): Promise<DeployResult> {\n const domain = customDomainOf(spec);\n if (!domain) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n 'retry_set_domain requires a customDomain in spec.',\n );\n }\n // C6 fix (preserved from pre-E impl): pass `serviceName` for stack\n // leases so the set-item-custom-domain tx targets the named service\n // item, not the default single-item lease.\n const serviceName = customDomainServiceOf(spec);\n try {\n await setItemCustomDomain(\n { chain: opts.clientManager, logger: noopLogger },\n {\n leaseUuid: parseLeaseUuid(leaseUuid),\n customDomain: parseFqdn(domain),\n serviceName,\n },\n );\n } catch (err) {\n // Copilot fix-3 (PR #71): sibling-parity wrap. Every throw site in\n // this helper now surfaces `retry_set_domain` + leaseUuid in the\n // message for log/user-report correlation, matching the\n // fetchActiveLease/uploadLeaseData/pollLeaseUntilReady wraps below.\n // Preserve the original ManifestMCPError code when applicable\n // (precedent at `estimateFees` — see the cosmosEstimateFee catch\n // block); fall back to TX_FAILED for untyped errors.\n // Upstream traceability (Copilot fix-6, PR #71): `setItemCustomDomain`\n // from `core/src/tools/setItemCustomDomain.ts:63,69` genuinely throws\n // `ManifestMCPError(INVALID_CONFIG)` for validation failures — the\n // typed branch here is LIVE for the canonical chain-side errors\n // (FQDN shape, reserved-suffix match, etc.).\n const reason =\n err instanceof Error\n ? `retry_set_domain set-item-custom-domain failed for lease ${leaseUuid}: ${err.message}`\n : `retry_set_domain set-item-custom-domain failed for lease ${leaseUuid}: ${String(err)}`;\n const code =\n err instanceof ManifestMCPError\n ? err.code\n : ManifestMCPErrorCode.TX_FAILED;\n throw new ManifestMCPError(code, reason);\n }\n\n // Resolve the lease + provider URL via on-chain queries. The\n // partial-success envelope only carried `leaseUuid` — fred's atomic\n // deployApp already had providerUuid in scope, but here we recover it.\n // BOTH values are hoisted to outer scope so the poll + DeployResult\n // build below can reuse them WITHOUT re-running the on-chain queries\n // (Copilot fix-1, PR #71: switching from `waitForAppReady` to the\n // lower-level `pollLeaseUntilReady` removes the 2 redundant queries\n // that `waitForAppReady`'s internal `fetchActiveLease` +\n // `resolveProviderUrl` calls would otherwise add per recovery).\n const queryClient = await opts.clientManager.getQueryClient();\n const readCtx: FredReadCtx = {\n query: queryClient,\n chain: opts.clientManager,\n fetch: opts.fetchFn ?? globalThis.fetch,\n logger: noopLogger,\n };\n let lease: Awaited<ReturnType<typeof fetchActiveLease>>;\n let providerApiUrl: string;\n try {\n lease = await fetchActiveLease(\n readCtx,\n leaseUuid,\n 'cannot complete retry_set_domain',\n );\n providerApiUrl = await resolveProviderUrl(readCtx, lease.providerUuid);\n } catch (err) {\n // Copilot fix-5 (PR #71): preserve typed ManifestMCPError codes\n // (matches the L1147 setItemCustomDomain precedent + the L818\n // estimateFees precedent). Honors fixup-4's JSDoc claim.\n // Upstream traceability (Copilot fix-6, PR #71):\n // - `fetchActiveLease` throws `ManifestMCPError(QUERY_FAILED)` at\n // `fred/src/tools/fetchActiveLease.ts:23,35` (lease not found\n // on chain + lease-not-active).\n // - `resolveProviderUrl` throws `ManifestMCPError(QUERY_FAILED)`\n // at `fred/src/tools/resolveLeaseProvider.ts:13,25,36` (empty\n // providerUuid + missing apiUrl + chain query failure).\n // - Either can also surface `ProviderApiError` (validateProviderUrl\n // path); untyped → TX_FAILED fallback.\n // Typed branch is LIVE for the canonical chain-side errors at this\n // catch — both upstream call sites genuinely emit ManifestMCPError.\n const reason =\n err instanceof Error\n ? `retry_set_domain failed to resolve provider for lease ${leaseUuid}: ${err.message}`\n : `retry_set_domain failed to resolve provider for lease ${leaseUuid}: ${String(err)}`;\n const code =\n err instanceof ManifestMCPError\n ? err.code\n : ManifestMCPErrorCode.TX_FAILED;\n throw new ManifestMCPError(code, reason);\n }\n\n // Upload the manifest payload via the ADR-036 lease-data auth token\n // (signed against the manifest's meta-hash).\n const manifestBytes = new TextEncoder().encode(ctx.manifestJson);\n try {\n const leaseDataAuthToken = await ctx.getLeaseDataAuthToken(\n ctx.tenantAddress,\n leaseUuid,\n ctx.metaHash,\n );\n await uploadLeaseData(\n providerApiUrl,\n leaseUuid,\n manifestBytes,\n leaseDataAuthToken,\n opts.fetchFn,\n );\n } catch (err) {\n // Copilot fix-5 (PR #71): preserve typed ManifestMCPError codes.\n // Upstream traceability (Copilot fix-6, PR #71): fred's\n // `uploadLeaseData` does NOT throw typed `ManifestMCPError` — both\n // its underlying `validateProviderUrl` (`fred/src/http/provider.ts:14`)\n // and the wrapped `checkedFetch` surface throw `ProviderApiError`,\n // which is NOT a `ManifestMCPError`. So this `instanceof\n // ManifestMCPError` check is effectively a no-op for the typical\n // fred path — the typed branch is dead code today for this catch.\n // Pattern is kept for symmetry with the L1196/L1284 sites + safety\n // against future deps that DO throw typed errors (e.g. a hypothetical\n // core dependency in the upload path). For the typical fred-only\n // case, the fallback `TX_FAILED` is what surfaces.\n const reason =\n err instanceof Error\n ? `retry_set_domain manifest upload failed for lease ${leaseUuid}: ${err.message}`\n : `retry_set_domain manifest upload failed for lease ${leaseUuid}: ${String(err)}`;\n const code =\n err instanceof ManifestMCPError\n ? err.code\n : ManifestMCPErrorCode.TX_FAILED;\n throw new ManifestMCPError(code, reason);\n }\n\n // Poll until the provider reports ACTIVE + running. Uses the LOWER-\n // LEVEL `pollLeaseUntilReady` directly (Copilot fix-1, PR #71) — not\n // `waitForAppReady` — so the already-resolved `providerApiUrl` and\n // auth-token closure pass through without re-running the on-chain\n // `fetchActiveLease` + `resolveProviderUrl` calls that\n // `waitForAppReady` would do internally. Saves ~2 queries (and ~2-6s\n // of avoidable latency) per recovery.\n //\n // The `onProgress` closure + `state?` discriminator-spread idiom +\n // `opts.waitForReadyTimeoutMs ?? 480_000` default mirror D's\n // canonical polling pattern verbatim.\n const pollStartMs = Date.now();\n let attempt = 0;\n let pollResult: Awaited<ReturnType<typeof pollLeaseUntilReady>>;\n try {\n pollResult = await pollLeaseUntilReady(\n providerApiUrl,\n leaseUuid,\n () => ctx.getAuthToken(ctx.tenantAddress, leaseUuid),\n {\n timeoutMs: opts.waitForReadyTimeoutMs ?? 480_000,\n onProgress: (status) => {\n attempt += 1;\n const stateName = decodeLeaseState(status.state);\n callbacks.onProgress?.({\n kind: 'polling_for_readiness',\n leaseUuid,\n attempt,\n elapsedMs: Date.now() - pollStartMs,\n ...(stateName !== undefined ? { state: stateName } : {}),\n });\n },\n },\n opts.fetchFn,\n );\n } catch (err) {\n // Names the actual primitive being awaited (post-fixup-1 +\n // fixup-4 consistency): `pollLeaseUntilReady`, not the higher-level\n // `waitForAppReady`. Matches the post-poll re-classify fallback's\n // wording at L1287 + the UNRECOGNIZED-state message at L1308 — all\n // three sites consistently name the primitive that's actually\n // running in this helper. Copilot fix-5 (PR #71): preserve typed\n // ManifestMCPError codes.\n // Upstream traceability (Copilot fix-6, PR #71): fred's\n // `pollLeaseUntilReady` does NOT throw typed `ManifestMCPError` —\n // its terminal-state path throws `TerminalChainStateError` which\n // `extends ProviderApiError` (`fred/src/http/fred.ts:278`), and its\n // timeout/HTTP paths throw `ProviderApiError` directly. Neither is\n // a `ManifestMCPError`. So this `instanceof ManifestMCPError` check\n // is effectively a no-op for the typical fred path — typed branch\n // is dead code today for this catch. Pattern is kept for symmetry\n // with the L1196/L1228 sites + safety against future deps that DO\n // throw typed errors. For the typical fred-only case, the fallback\n // `TX_FAILED` is what surfaces.\n const reason =\n err instanceof Error\n ? `retry_set_domain pollLeaseUntilReady failed for lease ${leaseUuid}: ${err.message}`\n : `retry_set_domain pollLeaseUntilReady failed for lease ${leaseUuid}: ${String(err)}`;\n const code =\n err instanceof ManifestMCPError\n ? err.code\n : ManifestMCPErrorCode.TX_FAILED;\n throw new ManifestMCPError(code, reason);\n }\n\n // Defense #2 parity (from D): re-classify the post-poll response and\n // refuse to declare success if the classifier doesn't see ACTIVE +\n // running instances. Catches the rare provider race where\n // `pollLeaseUntilReady` exits on state==ACTIVE but instances are empty.\n //\n // pollResult IS a `FredLeaseStatus` directly (no `WaitForAppReadyResult`\n // wrapping — that's what the refactor unlocked). The lease/provider\n // identity fields below come from the already-resolved values, NOT\n // from a (no-longer-existing) nested response object.\n const postPollResponse: DeployResponseShape = {\n lease_uuid: leaseUuid,\n provider_uuid: lease.providerUuid,\n provider_url: providerApiUrl,\n state: pollResult.state,\n connection: pollResult,\n };\n const classification = classifyDeployResponse(postPollResponse);\n if (classification.outcome !== 'active') {\n // Copilot fix-4 (PR #71): sibling-parity for BOTH branches. The\n // pre-fix `??` collapsed errorSummary (set when the post-poll\n // classifier produces 'failed' with a terminal-state response)\n // directly into the throw — no `retry_set_domain` prefix, no\n // leaseUuid. Both branches now carry the prefix + leaseUuid, and\n // the no-errorSummary fallback names the actual primitive\n // (`pollLeaseUntilReady` post-fixup-1, not `wait_for_app_ready`).\n const reason =\n classification.errorSummary !== undefined\n ? `retry_set_domain post-poll re-classification failed for lease ${leaseUuid}: ${classification.errorSummary}`\n : `retry_set_domain: pollLeaseUntilReady returned for lease ${leaseUuid} but post-poll classifier outcome is ${classification.outcome}`;\n throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);\n }\n\n callbacks.onProgress?.({ kind: 'app_ready_confirmed', leaseUuid });\n\n // Persist manifest (best-effort; save-fail still emits success — same\n // contract as D's happy path).\n const persistedPath = await tryPersistManifest({\n leaseUuid,\n image: primaryImage(spec),\n // FIX 1: persist the RESOLVED SKU name (matches what was broadcast).\n size: ctx.skuName,\n metaHash: ctx.metaHash,\n chainId: ctx.chainId,\n manifestJson: ctx.manifestJson,\n customDomain: domain,\n customDomainService: serviceName,\n dataDir: opts.dataDir,\n callbacks,\n });\n\n // Build DeployResult. State decoding + urls extraction mirror the\n // happy-path block in `deployApp` verbatim. After the Copilot fix-1\n // refactor, `pollResult` IS a `FredLeaseStatus` (no wrapping), so\n // `liveState` reads from `pollResult.state` directly (numeric\n // `LeaseState`) and `extractRunningEndpoints` walks `pollResult` itself.\n const liveState = pollResult.state;\n let leaseStateDecoded: LeaseStateName;\n const decoded = decodeLeaseState(liveState);\n if (decoded === undefined) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `Unrecognized lease state from pollLeaseUntilReady response: ${String(liveState)}. Cannot safely classify; refusing to silently coerce to ACTIVE.`,\n );\n }\n leaseStateDecoded = decoded;\n const endpointUrls =\n extractRunningEndpoints(pollResult).map(formatEndpointAsUrl);\n // Lease + provider identity come from the already-resolved values,\n // not from a (no-longer-existing) wrapping response object.\n const result: DeployResult = {\n leaseUuid,\n providerUuid: lease.providerUuid,\n leaseState: leaseStateDecoded,\n urls: endpointUrls,\n customDomain: domain,\n manifestPath: persistedPath ?? '',\n };\n callbacks.onProgress?.({ kind: 'success_rendered', result });\n callbacks.onComplete?.(result);\n return result;\n}\n\nfunction recoveryOptionLabel(id: RecoveryOptionId): string {\n switch (id) {\n case 'retry_set_domain':\n return 'Retry set-domain + upload';\n case 'salvage_without_domain':\n return 'Salvage without domain';\n case 'cancel_lease':\n return 'Cancel the lease';\n case 'close_lease':\n return 'Cancel or close the lease';\n }\n}\n\nfunction recoveryOptionDescription(id: RecoveryOptionId): string {\n switch (id) {\n case 'retry_set_domain':\n return 'Retry the set-domain transaction against the already-created lease.';\n case 'salvage_without_domain':\n return 'Keep the lease without the requested custom domain.';\n case 'cancel_lease':\n return 'Submit a cancel-lease transaction (pre-active terminal).';\n case 'close_lease':\n return 'Submit a close-lease transaction (post-active or pre-active terminal).';\n }\n}\n\ninterface PersistArgs {\n leaseUuid: string;\n image: string;\n size: string;\n metaHash: string;\n chainId: string;\n manifestJson: string;\n customDomain?: string;\n customDomainService?: string;\n dataDir?: string;\n callbacks: DeployAppCallbacks;\n}\n\nasync function tryPersistManifest(\n args: PersistArgs,\n): Promise<string | undefined> {\n if (!args.dataDir) return undefined;\n try {\n // Dynamic import keeps save-manifest's `node:fs` dep out of the\n // platform-neutral build path until needed.\n const { saveManifest } = await import('./internals/save-manifest.js');\n const result = await saveManifest({\n leaseUuid: args.leaseUuid,\n image: args.image,\n size: args.size,\n metaHash: args.metaHash,\n chainId: args.chainId,\n manifestJson: args.manifestJson,\n dataDir: args.dataDir,\n ...(args.customDomain ? { customDomain: args.customDomain } : {}),\n ...(args.customDomainService\n ? { customDomainServiceName: args.customDomainService }\n : {}),\n });\n args.callbacks.onProgress?.({\n kind: 'manifest_saved',\n leaseUuid: args.leaseUuid,\n manifestPath: result.manifestPath,\n });\n return result.manifestPath;\n } catch {\n // Step-16 contract: save-fail still returns success but `onProgress\n // (manifest_saved)` is NOT emitted; result.manifestPath stays empty.\n return undefined;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { describe, it } from "./node_modules/@vitest/runner/dist/chunk-artifact.js";
|
|
2
|
+
import { expectTypeOf } from "./node_modules/vitest/dist/index.js";
|
|
3
|
+
//#region src/deploy-app.test-d.ts
|
|
4
|
+
describe("deployApp input type (ENG-310)", () => {
|
|
5
|
+
it("is exactly AppDeploySpec", () => {
|
|
6
|
+
expectTypeOf().toEqualTypeOf();
|
|
7
|
+
});
|
|
8
|
+
});
|
|
9
|
+
//#endregion
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=deploy-app.test-d.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deploy-app.test-d.js","names":[],"sources":["../src/deploy-app.test-d.ts"],"sourcesContent":["import type { AppDeploySpec } from '@manifest-network/manifest-mcp-core';\nimport { describe, expectTypeOf, it } from 'vitest';\nimport type { deployApp } from './deploy-app.js';\n\n// Belt-and-suspenders: hard `tsc --noEmit` error too (not only vitest --typecheck).\ntype Equals<A, B> =\n (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2\n ? true\n : false;\ntype Expect<T extends true> = T;\n// `true satisfies …` is the hard compile-time equivalence proof: a mismatch\n// makes `Equals<…>` resolve to `false`, and `true satisfies Expect<false>`\n// fails `tsc --noEmit` (noUnusedLocals-clean — no standalone alias to flag,\n// no export to trip biome's noExportsInTest). Belt-and-suspenders alongside\n// the `expectTypeOf` runtime-typecheck assertion below.\ntrue satisfies Expect<Equals<Parameters<typeof deployApp>[0], AppDeploySpec>>;\n\ndescribe('deployApp input type (ENG-310)', () => {\n it('is exactly AppDeploySpec', () => {\n expectTypeOf<\n Parameters<typeof deployApp>[0]\n >().toEqualTypeOf<AppDeploySpec>();\n });\n});\n"],"mappings":";;;AAiBA,SAAS,wCAAwC;CAC/C,GAAG,kCAAkC;EACnC,aAEE,EAAE,cAA6B;CACnC,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { AgentCoreRuntime, CloseLeaseArgs, CloseLeaseCallbacks, CloseLeaseOptions, CloseLeaseResult, Coin, CosmosClientManager, DenomLookup, DenomMap, DeployAppCallbacks, DeployAppOptions, DeployResult,
|
|
1
|
+
import { AgentCoreRuntime, AppDeploySpec, CloseLeaseArgs, CloseLeaseCallbacks, CloseLeaseOptions, CloseLeaseResult, Coin, CosmosClientManager, DenomLookup, DenomMap, DeployAppCallbacks, DeployAppOptions, DeployResult, DeploymentPlanBlock, FailureEnvelope, FeeEstimate, LeaseStateName, ManageDomainArgs, ManageDomainCallbacks, ManageDomainOptions, ManageDomainResult, Plan, PlanEdit, PlanFees, ProgressEvent, Readiness, ReadinessAction, RecoveryChoice, RecoveryOption, RecoveryOptionId, ServiceConfig, SkuCandidate, SpecSummary, TroubleshootArgs, TroubleshootCallbacks, TroubleshootOptions, TroubleshootReport, WalletProvider } from "./types.js";
|
|
2
2
|
import { closeLease } from "./close-lease.js";
|
|
3
3
|
import { deployApp } from "./deploy-app.js";
|
|
4
|
-
import { GuardedFetch, createGuardedFetch } from "./internals/guarded-fetch.js";
|
|
5
4
|
import { loadChainDenomMap } from "./internals/humanize-denom.js";
|
|
6
5
|
import { manageDomain } from "./manage-domain.js";
|
|
7
6
|
import { troubleshootDeployment } from "./troubleshoot.js";
|
|
8
|
-
export { AgentCoreRuntime, CloseLeaseArgs, CloseLeaseCallbacks, CloseLeaseOptions, CloseLeaseResult, Coin, type CosmosClientManager, DenomLookup, DenomMap, DeployAppCallbacks, DeployAppOptions, DeployResult,
|
|
7
|
+
export { AgentCoreRuntime, type AppDeploySpec, CloseLeaseArgs, CloseLeaseCallbacks, CloseLeaseOptions, CloseLeaseResult, Coin, type CosmosClientManager, DenomLookup, DenomMap, DeployAppCallbacks, DeployAppOptions, DeployResult, DeploymentPlanBlock, FailureEnvelope, FeeEstimate, LeaseStateName, ManageDomainArgs, ManageDomainCallbacks, ManageDomainOptions, ManageDomainResult, Plan, PlanEdit, PlanFees, ProgressEvent, Readiness, ReadinessAction, RecoveryChoice, RecoveryOption, RecoveryOptionId, type ServiceConfig, type SkuCandidate, SpecSummary, TroubleshootArgs, TroubleshootCallbacks, TroubleshootOptions, TroubleshootReport, type WalletProvider, closeLease, deployApp, loadChainDenomMap, manageDomain, troubleshootDeployment };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { closeLease } from "./close-lease.js";
|
|
2
2
|
import { loadChainDenomMap } from "./internals/humanize-denom.js";
|
|
3
3
|
import { deployApp } from "./deploy-app.js";
|
|
4
|
-
import { createGuardedFetch } from "./internals/guarded-fetch.js";
|
|
5
4
|
import { manageDomain } from "./manage-domain.js";
|
|
6
5
|
import { troubleshootDeployment } from "./troubleshoot.js";
|
|
7
|
-
export { closeLease,
|
|
6
|
+
export { closeLease, deployApp, loadChainDenomMap, manageDomain, troubleshootDeployment };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ProgressEvent } from "../types.js";
|
|
2
|
+
import { ManifestMCPError } from "@manifest-network/manifest-mcp-core";
|
|
3
|
+
|
|
4
|
+
//#region src/internals/cancellation.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Build the structured cancellation error for an aborted/timed-out PRE-broadcast
|
|
7
|
+
* await (or a stopped-awaiting read). `OPERATION_CANCELLED` is non-retryable and
|
|
8
|
+
* keeps the abort path consistent with the SDK error model; the original
|
|
9
|
+
* `AbortError`/`TimeoutError` reason is preserved in the message + details.
|
|
10
|
+
*
|
|
11
|
+
* - `broadcasts: true` → mutating flows (deploy / domain-set / lease-close):
|
|
12
|
+
* the abort happened before any tx was sent.
|
|
13
|
+
* - `broadcasts: false` → read-only flows (troubleshoot / domain-lookup): there
|
|
14
|
+
* is no broadcast to reference; we merely stopped awaiting the query.
|
|
15
|
+
*/
|
|
16
|
+
declare function cancelledError(reason: unknown, opLabel: string, broadcasts: boolean): ManifestMCPError;
|
|
17
|
+
/**
|
|
18
|
+
* Race a pending promise against an `AbortSignal`. Copies the executor + swallow
|
|
19
|
+
* shape from core's `internals/tx-confirmation.ts:withTxConfirmation`: the losing
|
|
20
|
+
* branch's eventual rejection is swallowed (no unhandled rejection) and the abort
|
|
21
|
+
* listener is added `{ once: true }` and removed in `.finally`. The rejection
|
|
22
|
+
* error is produced by the injected `makeError`, so the primitive stays
|
|
23
|
+
* operation-agnostic. Manifestjs queries take no `AbortSignal`, so for read-only
|
|
24
|
+
* callers this does NOT cancel the RPC — it only stops AWAITING it.
|
|
25
|
+
*/
|
|
26
|
+
declare function raceAbort<T>(promise: Promise<T>, signal: AbortSignal, makeError: (reason: unknown) => ManifestMCPError): Promise<T>;
|
|
27
|
+
/** Per-call cancellation seam shared by all four agent-core orchestrators. */
|
|
28
|
+
interface CancellationScope {
|
|
29
|
+
/** Effective signal (caller `signal` composed with `timeout`), or `undefined`. */
|
|
30
|
+
signal: AbortSignal | undefined;
|
|
31
|
+
/** Throw `OPERATION_CANCELLED` (emitting `cancelled` once) if already aborted. */
|
|
32
|
+
throwIfCancelled: () => void;
|
|
33
|
+
/**
|
|
34
|
+
* Race a pre-broadcast callback or a read query against the signal. A no-op
|
|
35
|
+
* passthrough when no signal is present. On abort it emits `cancelled` once
|
|
36
|
+
* and throws `OPERATION_CANCELLED`.
|
|
37
|
+
*/
|
|
38
|
+
race: <T>(p: Promise<T>) => Promise<T>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build the per-call cancellation seam: captures the resolved signal, a once-guard
|
|
42
|
+
* for the terminal `cancelled` progress event, and the operation label /
|
|
43
|
+
* broadcast-ness for the error message. PURE at construction — call
|
|
44
|
+
* `throwIfCancelled()` explicitly for the already-aborted short-circuit.
|
|
45
|
+
*/
|
|
46
|
+
declare function makeCancellationScope(args: {
|
|
47
|
+
opts: {
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
timeout?: number;
|
|
50
|
+
};
|
|
51
|
+
onProgress: ((event: ProgressEvent) => void) | undefined;
|
|
52
|
+
opLabel: string;
|
|
53
|
+
broadcasts: boolean;
|
|
54
|
+
}): CancellationScope;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { CancellationScope, cancelledError, makeCancellationScope, raceAbort };
|
|
57
|
+
//# sourceMappingURL=cancellation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cancellation.d.ts","names":[],"sources":["../../src/internals/cancellation.ts"],"mappings":";;;;;;AAkBA;;;;;;;;;iBAAgB,cAAA,CACd,MAAA,WACA,OAAA,UACA,UAAA,YACC,gBAAgB;AAuBnB;;;;;;;;;AAAA,iBAAgB,SAAA,IACd,OAAA,EAAS,OAAA,CAAQ,CAAA,GACjB,MAAA,EAAQ,WAAA,EACR,SAAA,GAAY,MAAA,cAAoB,gBAAA,GAC/B,OAAA,CAAQ,CAAA;;UAgBM,iBAAA;EAnBN;EAqBT,MAAA,EAAQ,WAAA;EArBR;EAuBA,gBAAA;EAtBA;;;;;EA4BA,IAAA,MAAU,CAAA,EAAG,OAAA,CAAQ,CAAA,MAAO,OAAA,CAAQ,CAAA;AAAA;AA1B1B;AAgBZ;;;;;AAhBY,iBAmCI,qBAAA,CAAsB,IAAA;EACpC,IAAA;IAAQ,MAAA,GAAS,WAAA;IAAa,OAAA;EAAA;EAC9B,UAAA,IAAc,KAAA,EAAO,aAAA;EACrB,OAAA;EACA,UAAA;AAAA,IACE,iBAAA"}
|