@manifest-network/manifest-agent-core 0.9.0 → 0.11.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/README.md +1 -1
- package/dist/close-lease.d.ts +3 -2
- package/dist/close-lease.d.ts.map +1 -1
- package/dist/close-lease.js +4 -3
- package/dist/close-lease.js.map +1 -1
- package/dist/deploy-app.d.ts +3 -2
- package/dist/deploy-app.d.ts.map +1 -1
- package/dist/deploy-app.js +245 -77
- package/dist/deploy-app.js.map +1 -1
- package/dist/internals/build-fred-input.d.ts +38 -0
- package/dist/internals/build-fred-input.d.ts.map +1 -0
- package/dist/internals/build-fred-input.js +147 -0
- package/dist/internals/build-fred-input.js.map +1 -0
- package/dist/internals/evaluate-readiness-from-fred.d.ts +28 -0
- package/dist/internals/evaluate-readiness-from-fred.d.ts.map +1 -0
- package/dist/internals/evaluate-readiness-from-fred.js +94 -0
- package/dist/internals/evaluate-readiness-from-fred.js.map +1 -0
- package/dist/internals/format-success.js.map +1 -1
- package/dist/internals/guarded-fetch.d.ts +2 -138
- package/dist/internals/guarded-fetch.js +1 -241
- package/dist/internals/humanize-denom.js.map +1 -1
- package/dist/internals/inspect-image.js.map +1 -1
- package/dist/internals/lease-items.js +1 -4
- package/dist/internals/lease-items.js.map +1 -1
- package/dist/internals/render-deployment-plan.js.map +1 -1
- package/dist/internals/verify-recover.js.map +1 -1
- package/dist/manage-domain.d.ts +3 -2
- package/dist/manage-domain.d.ts.map +1 -1
- package/dist/manage-domain.js +4 -3
- package/dist/manage-domain.js.map +1 -1
- package/dist/troubleshoot.js.map +1 -1
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -5
- package/dist/internals/guarded-fetch.d.ts.map +0 -1
- package/dist/internals/guarded-fetch.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
TypeScript orchestration surface for Manifest agent flows. This package owns the deploy / manage-domain / troubleshoot / close-lease orchestration that host surfaces consume in lockstep — a bug fix in the core's recovery branch fixes every host surface simultaneously.
|
|
4
4
|
|
|
5
|
-
> **Status.**
|
|
5
|
+
> **Status.** Publicly published on npm as `@manifest-network/manifest-agent-core` (ENG-129). Consume via `npm install @manifest-network/manifest-agent-core`. The MCP-server adapter that wraps this orchestration surface via elicitation lives at [`@manifest-network/manifest-mcp-agent`](../agent/README.md).
|
|
6
6
|
|
|
7
7
|
See [ENG-127](https://linear.app/liftedinit/issue/ENG-127) for the broader initiative and [ENG-128](https://linear.app/liftedinit/issue/ENG-128) for the bootstrap PR.
|
|
8
8
|
|
package/dist/close-lease.d.ts
CHANGED
|
@@ -4,8 +4,9 @@ import { CloseLeaseArgs, CloseLeaseCallbacks, CloseLeaseOptions, CloseLeaseResul
|
|
|
4
4
|
/**
|
|
5
5
|
* Close a lease and verify it reached a terminal on-chain state.
|
|
6
6
|
*
|
|
7
|
-
* @throws `ManifestMCPError(INVALID_CONFIG)` for args validation
|
|
8
|
-
*
|
|
7
|
+
* @throws `ManifestMCPError(INVALID_CONFIG)` for args validation.
|
|
8
|
+
* @throws `ManifestMCPError(OPERATION_CANCELLED)` when `onConfirm` returns
|
|
9
|
+
* `'no'` (deliberate user cancellation — ENG-272).
|
|
9
10
|
* @throws `ManifestMCPError` (typically `TX_FAILED`) propagated as-is
|
|
10
11
|
* from the `stopApp()` broadcast step. Broadcast errors do NOT invoke
|
|
11
12
|
* `onFailure` — that callback is reserved for post-broadcast
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"close-lease.d.ts","names":[],"sources":["../src/close-lease.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"close-lease.d.ts","names":[],"sources":["../src/close-lease.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAgFsB,UAAA,CACpB,IAAA,EAAM,cAAA,EACN,SAAA,EAAW,mBAAA,EACX,IAAA,EAAM,iBAAA,GACL,OAAA,CAAQ,gBAAA"}
|
package/dist/close-lease.js
CHANGED
|
@@ -26,8 +26,9 @@ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
|
|
26
26
|
/**
|
|
27
27
|
* Close a lease and verify it reached a terminal on-chain state.
|
|
28
28
|
*
|
|
29
|
-
* @throws `ManifestMCPError(INVALID_CONFIG)` for args validation
|
|
30
|
-
*
|
|
29
|
+
* @throws `ManifestMCPError(INVALID_CONFIG)` for args validation.
|
|
30
|
+
* @throws `ManifestMCPError(OPERATION_CANCELLED)` when `onConfirm` returns
|
|
31
|
+
* `'no'` (deliberate user cancellation — ENG-272).
|
|
31
32
|
* @throws `ManifestMCPError` (typically `TX_FAILED`) propagated as-is
|
|
32
33
|
* from the `stopApp()` broadcast step. Broadcast errors do NOT invoke
|
|
33
34
|
* `onFailure` — that callback is reserved for post-broadcast
|
|
@@ -53,7 +54,7 @@ async function closeLease(args, callbacks, opts) {
|
|
|
53
54
|
validateArgs(args);
|
|
54
55
|
const block = renderConfirmationBlock(args);
|
|
55
56
|
if (callbacks.onConfirm) {
|
|
56
|
-
if (await callbacks.onConfirm(block) !== "yes") throw new ManifestMCPError(ManifestMCPErrorCode.
|
|
57
|
+
if (await callbacks.onConfirm(block) !== "yes") throw new ManifestMCPError(ManifestMCPErrorCode.OPERATION_CANCELLED, "User declined to proceed with close-lease.");
|
|
57
58
|
}
|
|
58
59
|
callbacks.onProgress?.({ kind: "user_confirmed" });
|
|
59
60
|
await stopApp(opts.clientManager, args.leaseUuid);
|
package/dist/close-lease.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"close-lease.js","names":["decodeLeaseState"],"sources":["../src/close-lease.ts"],"sourcesContent":["/**\n * Public entry point: orchestrate closing an existing lease via the\n * `close-lease` billing tx.\n *\n * Composition (mirrors `deploy-app.ts` / `manage-domain.ts`):\n *\n * 1. Validate args.\n * 2. Render a confirmation block + optionally consult `onConfirm`.\n * 3. Broadcast `stopApp` (which submits `MsgCloseLease`).\n * 4. Verify the post-broadcast on-chain state via `verifyAndRecover`\n * driving a direct `billing.v1.lease({ leaseUuid })` query +\n * `lease-state.decode` + `isTerminal`. Terminal states (CLOSED /\n * REJECTED / EXPIRED / INSUFFICIENT_FUNDS) count as success;\n * PENDING / ACTIVE map to the `pending_drift` branch; a chain\n * response with no lease (`{ lease: null }`) maps to the catch-all\n * `unclassified` branch.\n * 5. On verify-failure, invoke the simple-form `onFailure({ reason })`\n * then throw `ManifestMCPError(TX_FAILED)`. On success, emit\n * `onComplete` with the typed `CloseLeaseResult`.\n */\n\nimport {\n ManifestMCPError,\n ManifestMCPErrorCode,\n stopApp,\n} from '@manifest-network/manifest-mcp-core';\nimport {\n decode as decodeLeaseState,\n isTerminal,\n} from './internals/lease-state.js';\nimport {\n type VerificationSpec,\n verifyAndRecover,\n} from './internals/verify-recover.js';\nimport type {\n CloseLeaseArgs,\n CloseLeaseCallbacks,\n CloseLeaseOptions,\n CloseLeaseResult,\n DeploymentPlanBlock,\n LeaseStateName,\n} from './types.js';\n\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ntype CloseOutcome = 'terminal' | 'pending' | 'not_found';\n\ninterface CloseDiag {\n stateName?: LeaseStateName;\n reason?: string;\n}\n\n/**\n * Close a lease and verify it reached a terminal on-chain state.\n *\n * @throws `ManifestMCPError(INVALID_CONFIG)` for args validation or when\n * `onConfirm` returns `'no'`.\n * @throws `ManifestMCPError` (typically `TX_FAILED`) propagated as-is\n * from the `stopApp()` broadcast step. Broadcast errors do NOT invoke\n * `onFailure` — that callback is reserved for post-broadcast\n * verification failures. `stopApp` already raises a structured\n * `ManifestMCPError` from the core package; wrapping it again at this\n * layer would be redundant. Callers wanting to react to broadcast\n * errors should catch them at the call site.\n * @throws `ManifestMCPError(TX_FAILED)` when post-broadcast verification\n * reaches one of two failure modes (both with `onFailure({ reason })`\n * invoked first):\n * - the lease is still non-terminal (`pending_drift` branch — state\n * decoded as PENDING / ACTIVE / similar non-terminal); or\n * - the chain returns `{ lease: null }` post-close, so the lease is\n * not visible on-chain (`unclassified` branch).\n * @throws `ManifestMCPError(QUERY_FAILED)` when the post-broadcast verify\n * chain query (`billing.v1.lease`) raises a non-NotFound error\n * (RPC / transport / decoding failure). Wrapped inside the verifier\n * closure so the failure flows through `onFailure({ reason })` before\n * the throw. Structured `ManifestMCPError`s raised by the chain client\n * are re-thrown as-is (with `onFailure` invoked first).\n */\nexport async function closeLease(\n args: CloseLeaseArgs,\n callbacks: CloseLeaseCallbacks,\n opts: CloseLeaseOptions,\n): Promise<CloseLeaseResult> {\n validateArgs(args);\n\n const block = renderConfirmationBlock(args);\n if (callbacks.onConfirm) {\n const yesNo = await callbacks.onConfirm(block);\n if (yesNo !== 'yes') {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n 'User declined to proceed with close-lease.',\n );\n }\n }\n callbacks.onProgress?.({ kind: 'user_confirmed' });\n\n await stopApp(opts.clientManager, args.leaseUuid);\n\n // Direct single-lease query (Copilot review PR #60, comment 3275999624):\n // the previous `leasesByTenant` + page-1-only pagination would\n // false-`not_found` for tenants with >100 leases. `billing.v1.lease`\n // is the same query shape `troubleshoot.ts` already uses; it's\n // tenant-agnostic and bounded to a single lease.\n const spec: VerificationSpec<unknown, CloseOutcome, CloseDiag> = {\n verifier: async () => {\n // Wrap the chain call in try/catch (Copilot review PR #60,\n // comment 3276419264): if `billing.v1.lease` rejects (RPC down,\n // transport, structured `ManifestMCPError`), the error would\n // otherwise propagate OUT of `verifyAndRecover` and bypass the\n // post-verify `onFailure({ reason })` callback below. Mirror\n // the disambiguation pattern from `lookupDomain` (commit aaa5cc5)\n // and `troubleshootDeployment` (commit f1a4737): invoke\n // `onFailure` first, then re-throw `ManifestMCPError` as-is or\n // wrap plain errors as `QUERY_FAILED`.\n let result: unknown;\n try {\n const queryClient = await opts.clientManager.getQueryClient();\n result = await queryClient.liftedinit.billing.v1.lease({\n leaseUuid: args.leaseUuid,\n });\n } catch (err) {\n const reason = `Failed to query lease ${args.leaseUuid} during close-verify: ${\n err instanceof Error ? err.message : String(err)\n }`;\n if (callbacks.onFailure) {\n await callbacks.onFailure({ reason });\n }\n if (err instanceof ManifestMCPError) {\n throw err;\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.QUERY_FAILED, reason);\n }\n const lease = (result as { lease?: unknown })?.lease;\n if (lease === null || lease === undefined) {\n return {\n outcome: 'not_found' as const,\n diagnostic: {\n reason: `lease ${args.leaseUuid} not visible on chain after close`,\n },\n };\n }\n const rawState = (lease as { state?: unknown }).state;\n const stateName = decodeLeaseState(\n typeof rawState === 'number' || typeof rawState === 'string'\n ? rawState\n : undefined,\n );\n if (stateName === undefined) {\n return {\n outcome: 'pending' as const,\n diagnostic: {\n reason: `lease ${args.leaseUuid} state could not be decoded (raw=${String(rawState)})`,\n },\n };\n }\n return {\n outcome: (isTerminal(stateName) ? 'terminal' : 'pending') as\n | 'terminal'\n | 'pending',\n diagnostic: { stateName },\n };\n },\n successValues: ['terminal'],\n branches: {\n pending: {\n branchId: 'pending_drift',\n journalActionTags: ['close-lease-verify-pending'],\n buildFailureEnvelope: (d) => ({\n outcome: 'failed',\n reason:\n d.reason ??\n `close_lease tx accepted but state is still ${d.stateName ?? 'unknown'}.`,\n }),\n buildRecoveryOptions: () => [],\n },\n not_found: {\n branchId: 'unclassified',\n journalActionTags: ['close-lease-verify-not-found'],\n buildFailureEnvelope: (d) => ({\n outcome: 'failed',\n reason:\n d.reason ??\n `Lease ${args.leaseUuid} not visible on chain after close.`,\n }),\n buildRecoveryOptions: () => [],\n },\n },\n };\n\n const verifyResult = await verifyAndRecover(spec, undefined);\n\n if (verifyResult.result !== 'success') {\n const reason =\n verifyResult.failure?.reason ?? 'close-lease verification failed.';\n if (callbacks.onFailure) {\n await callbacks.onFailure({ reason });\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);\n }\n\n // Invariant: when `verifyAndRecover` returns success, the matched\n // outcome was `'terminal'`, and the verifier's `terminal` branch\n // ALWAYS sets `diagnostic.stateName` (see the spec above). A missing\n // `stateName` on the success path means the verifier invariant is\n // broken — likely a future refactor regression. The previous\n // implementation fell back to `'LEASE_STATE_CLOSED'` silently, which\n // would lie to the caller (Copilot review PR #60, comment 3276719603).\n // Fail loudly with a typed error instead. `TX_FAILED` is the closest\n // available code in `ManifestMCPErrorCode` (no `INTERNAL_ERROR`\n // variant); the message names the invariant explicitly.\n if (!verifyResult.diagnostic.stateName) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.TX_FAILED,\n `close-lease verifier invariant violated: success outcome reached without diagnostic.stateName for lease ${args.leaseUuid}`,\n );\n }\n const finalState: LeaseStateName = verifyResult.diagnostic.stateName;\n const result: CloseLeaseResult = {\n leaseUuid: args.leaseUuid,\n finalState,\n };\n callbacks.onComplete?.(result);\n return result;\n}\n\n// --- Helpers --------------------------------------------------------\n\nfunction validateArgs(args: CloseLeaseArgs): void {\n if (typeof args.leaseUuid !== 'string' || !args.leaseUuid.match(UUID_RE)) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `closeLease: leaseUuid must be a UUID; got \"${args.leaseUuid}\".`,\n );\n }\n}\n\nfunction renderConfirmationBlock(args: CloseLeaseArgs): DeploymentPlanBlock {\n // Image is not tracked in `CloseLeaseArgs` and `stopApp` doesn't return it;\n // surface the gap explicitly so reviewers/users see the missing context\n // rather than silently omitting an image field they'd expect.\n const text = [\n `Close lease ${args.leaseUuid}.`,\n ' Image: (image not recorded)',\n ' This is permanent — the lease cannot be reopened.',\n '',\n 'Proceed?',\n ].join('\\n');\n return { text };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAM,UACJ;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCF,eAAsB,WACpB,MACA,WACA,MAC2B;AAC3B,cAAa,KAAK;CAElB,MAAM,QAAQ,wBAAwB,KAAK;AAC3C,KAAI,UAAU;MACE,MAAM,UAAU,UAAU,MAAM,KAChC,MACZ,OAAM,IAAI,iBACR,qBAAqB,gBACrB,6CACD;;AAGL,WAAU,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAM,QAAQ,KAAK,eAAe,KAAK,UAAU;CA6FjD,MAAM,eAAe,MAAM,iBAtFsC;EAC/D,UAAU,YAAY;GAUpB,IAAI;AACJ,OAAI;AAEF,aAAS,OADW,MAAM,KAAK,cAAc,gBAAgB,EAClC,WAAW,QAAQ,GAAG,MAAM,EACrD,WAAW,KAAK,WACjB,CAAC;YACK,KAAK;IACZ,MAAM,SAAS,yBAAyB,KAAK,UAAU,wBACrD,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAElD,QAAI,UAAU,UACZ,OAAM,UAAU,UAAU,EAAE,QAAQ,CAAC;AAEvC,QAAI,eAAe,iBACjB,OAAM;AAER,UAAM,IAAI,iBAAiB,qBAAqB,cAAc,OAAO;;GAEvE,MAAM,QAAS,QAAgC;AAC/C,OAAI,UAAU,QAAQ,UAAU,KAAA,EAC9B,QAAO;IACL,SAAS;IACT,YAAY,EACV,QAAQ,SAAS,KAAK,UAAU,oCACjC;IACF;GAEH,MAAM,WAAY,MAA8B;GAChD,MAAM,YAAYA,OAChB,OAAO,aAAa,YAAY,OAAO,aAAa,WAChD,WACA,KAAA,EACL;AACD,OAAI,cAAc,KAAA,EAChB,QAAO;IACL,SAAS;IACT,YAAY,EACV,QAAQ,SAAS,KAAK,UAAU,mCAAmC,OAAO,SAAS,CAAC,IACrF;IACF;AAEH,UAAO;IACL,SAAU,WAAW,UAAU,GAAG,aAAa;IAG/C,YAAY,EAAE,WAAW;IAC1B;;EAEH,eAAe,CAAC,WAAW;EAC3B,UAAU;GACR,SAAS;IACP,UAAU;IACV,mBAAmB,CAAC,6BAA6B;IACjD,uBAAuB,OAAO;KAC5B,SAAS;KACT,QACE,EAAE,UACF,8CAA8C,EAAE,aAAa,UAAU;KAC1E;IACD,4BAA4B,EAAE;IAC/B;GACD,WAAW;IACT,UAAU;IACV,mBAAmB,CAAC,+BAA+B;IACnD,uBAAuB,OAAO;KAC5B,SAAS;KACT,QACE,EAAE,UACF,SAAS,KAAK,UAAU;KAC3B;IACD,4BAA4B,EAAE;IAC/B;GACF;EACF,EAEiD,KAAA,EAAU;AAE5D,KAAI,aAAa,WAAW,WAAW;EACrC,MAAM,SACJ,aAAa,SAAS,UAAU;AAClC,MAAI,UAAU,UACZ,OAAM,UAAU,UAAU,EAAE,QAAQ,CAAC;AAEvC,QAAM,IAAI,iBAAiB,qBAAqB,WAAW,OAAO;;AAapE,KAAI,CAAC,aAAa,WAAW,UAC3B,OAAM,IAAI,iBACR,qBAAqB,WACrB,2GAA2G,KAAK,YACjH;CAEH,MAAM,aAA6B,aAAa,WAAW;CAC3D,MAAM,SAA2B;EAC/B,WAAW,KAAK;EAChB;EACD;AACD,WAAU,aAAa,OAAO;AAC9B,QAAO;;AAKT,SAAS,aAAa,MAA4B;AAChD,KAAI,OAAO,KAAK,cAAc,YAAY,CAAC,KAAK,UAAU,MAAM,QAAQ,CACtE,OAAM,IAAI,iBACR,qBAAqB,gBACrB,8CAA8C,KAAK,UAAU,IAC9D;;AAIL,SAAS,wBAAwB,MAA2C;AAW1E,QAAO,EAAE,MAPI;EACX,eAAe,KAAK,UAAU;EAC9B;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK,EACG"}
|
|
1
|
+
{"version":3,"file":"close-lease.js","names":["decodeLeaseState"],"sources":["../src/close-lease.ts"],"sourcesContent":["/**\n * Public entry point: orchestrate closing an existing lease via the\n * `close-lease` billing tx.\n *\n * Composition (mirrors `deploy-app.ts` / `manage-domain.ts`):\n *\n * 1. Validate args.\n * 2. Render a confirmation block + optionally consult `onConfirm`.\n * 3. Broadcast `stopApp` (which submits `MsgCloseLease`).\n * 4. Verify the post-broadcast on-chain state via `verifyAndRecover`\n * driving a direct `billing.v1.lease({ leaseUuid })` query +\n * `lease-state.decode` + `isTerminal`. Terminal states (CLOSED /\n * REJECTED / EXPIRED / INSUFFICIENT_FUNDS) count as success;\n * PENDING / ACTIVE map to the `pending_drift` branch; a chain\n * response with no lease (`{ lease: null }`) maps to the catch-all\n * `unclassified` branch.\n * 5. On verify-failure, invoke the simple-form `onFailure({ reason })`\n * then throw `ManifestMCPError(TX_FAILED)`. On success, emit\n * `onComplete` with the typed `CloseLeaseResult`.\n */\n\nimport {\n ManifestMCPError,\n ManifestMCPErrorCode,\n stopApp,\n} from '@manifest-network/manifest-mcp-core';\nimport {\n decode as decodeLeaseState,\n isTerminal,\n} from './internals/lease-state.js';\nimport {\n type VerificationSpec,\n verifyAndRecover,\n} from './internals/verify-recover.js';\nimport type {\n CloseLeaseArgs,\n CloseLeaseCallbacks,\n CloseLeaseOptions,\n CloseLeaseResult,\n DeploymentPlanBlock,\n LeaseStateName,\n} from './types.js';\n\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ntype CloseOutcome = 'terminal' | 'pending' | 'not_found';\n\ninterface CloseDiag {\n stateName?: LeaseStateName;\n reason?: string;\n}\n\n/**\n * Close a lease and verify it reached a terminal on-chain state.\n *\n * @throws `ManifestMCPError(INVALID_CONFIG)` for args validation.\n * @throws `ManifestMCPError(OPERATION_CANCELLED)` when `onConfirm` returns\n * `'no'` (deliberate user cancellation — ENG-272).\n * @throws `ManifestMCPError` (typically `TX_FAILED`) propagated as-is\n * from the `stopApp()` broadcast step. Broadcast errors do NOT invoke\n * `onFailure` — that callback is reserved for post-broadcast\n * verification failures. `stopApp` already raises a structured\n * `ManifestMCPError` from the core package; wrapping it again at this\n * layer would be redundant. Callers wanting to react to broadcast\n * errors should catch them at the call site.\n * @throws `ManifestMCPError(TX_FAILED)` when post-broadcast verification\n * reaches one of two failure modes (both with `onFailure({ reason })`\n * invoked first):\n * - the lease is still non-terminal (`pending_drift` branch — state\n * decoded as PENDING / ACTIVE / similar non-terminal); or\n * - the chain returns `{ lease: null }` post-close, so the lease is\n * not visible on-chain (`unclassified` branch).\n * @throws `ManifestMCPError(QUERY_FAILED)` when the post-broadcast verify\n * chain query (`billing.v1.lease`) raises a non-NotFound error\n * (RPC / transport / decoding failure). Wrapped inside the verifier\n * closure so the failure flows through `onFailure({ reason })` before\n * the throw. Structured `ManifestMCPError`s raised by the chain client\n * are re-thrown as-is (with `onFailure` invoked first).\n */\nexport async function closeLease(\n args: CloseLeaseArgs,\n callbacks: CloseLeaseCallbacks,\n opts: CloseLeaseOptions,\n): Promise<CloseLeaseResult> {\n validateArgs(args);\n\n const block = renderConfirmationBlock(args);\n if (callbacks.onConfirm) {\n const yesNo = await callbacks.onConfirm(block);\n if (yesNo !== 'yes') {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.OPERATION_CANCELLED,\n 'User declined to proceed with close-lease.',\n );\n }\n }\n callbacks.onProgress?.({ kind: 'user_confirmed' });\n\n await stopApp(opts.clientManager, args.leaseUuid);\n\n // Direct single-lease query (Copilot review PR #60, comment 3275999624):\n // the previous `leasesByTenant` + page-1-only pagination would\n // false-`not_found` for tenants with >100 leases. `billing.v1.lease`\n // is the same query shape `troubleshoot.ts` already uses; it's\n // tenant-agnostic and bounded to a single lease.\n const spec: VerificationSpec<unknown, CloseOutcome, CloseDiag> = {\n verifier: async () => {\n // Wrap the chain call in try/catch (Copilot review PR #60,\n // comment 3276419264): if `billing.v1.lease` rejects (RPC down,\n // transport, structured `ManifestMCPError`), the error would\n // otherwise propagate OUT of `verifyAndRecover` and bypass the\n // post-verify `onFailure({ reason })` callback below. Mirror\n // the disambiguation pattern from `lookupDomain` (commit aaa5cc5)\n // and `troubleshootDeployment` (commit f1a4737): invoke\n // `onFailure` first, then re-throw `ManifestMCPError` as-is or\n // wrap plain errors as `QUERY_FAILED`.\n let result: unknown;\n try {\n const queryClient = await opts.clientManager.getQueryClient();\n result = await queryClient.liftedinit.billing.v1.lease({\n leaseUuid: args.leaseUuid,\n });\n } catch (err) {\n const reason = `Failed to query lease ${args.leaseUuid} during close-verify: ${\n err instanceof Error ? err.message : String(err)\n }`;\n if (callbacks.onFailure) {\n await callbacks.onFailure({ reason });\n }\n if (err instanceof ManifestMCPError) {\n throw err;\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.QUERY_FAILED, reason);\n }\n const lease = (result as { lease?: unknown })?.lease;\n if (lease === null || lease === undefined) {\n return {\n outcome: 'not_found' as const,\n diagnostic: {\n reason: `lease ${args.leaseUuid} not visible on chain after close`,\n },\n };\n }\n const rawState = (lease as { state?: unknown }).state;\n const stateName = decodeLeaseState(\n typeof rawState === 'number' || typeof rawState === 'string'\n ? rawState\n : undefined,\n );\n if (stateName === undefined) {\n return {\n outcome: 'pending' as const,\n diagnostic: {\n reason: `lease ${args.leaseUuid} state could not be decoded (raw=${String(rawState)})`,\n },\n };\n }\n return {\n outcome: (isTerminal(stateName) ? 'terminal' : 'pending') as\n | 'terminal'\n | 'pending',\n diagnostic: { stateName },\n };\n },\n successValues: ['terminal'],\n branches: {\n pending: {\n branchId: 'pending_drift',\n journalActionTags: ['close-lease-verify-pending'],\n buildFailureEnvelope: (d) => ({\n outcome: 'failed',\n reason:\n d.reason ??\n `close_lease tx accepted but state is still ${d.stateName ?? 'unknown'}.`,\n }),\n buildRecoveryOptions: () => [],\n },\n not_found: {\n branchId: 'unclassified',\n journalActionTags: ['close-lease-verify-not-found'],\n buildFailureEnvelope: (d) => ({\n outcome: 'failed',\n reason:\n d.reason ??\n `Lease ${args.leaseUuid} not visible on chain after close.`,\n }),\n buildRecoveryOptions: () => [],\n },\n },\n };\n\n const verifyResult = await verifyAndRecover(spec, undefined);\n\n if (verifyResult.result !== 'success') {\n const reason =\n verifyResult.failure?.reason ?? 'close-lease verification failed.';\n if (callbacks.onFailure) {\n await callbacks.onFailure({ reason });\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);\n }\n\n // Invariant: when `verifyAndRecover` returns success, the matched\n // outcome was `'terminal'`, and the verifier's `terminal` branch\n // ALWAYS sets `diagnostic.stateName` (see the spec above). A missing\n // `stateName` on the success path means the verifier invariant is\n // broken — likely a future refactor regression. The previous\n // implementation fell back to `'LEASE_STATE_CLOSED'` silently, which\n // would lie to the caller (Copilot review PR #60, comment 3276719603).\n // Fail loudly with a typed error instead. `TX_FAILED` is the closest\n // available code in `ManifestMCPErrorCode` (no `INTERNAL_ERROR`\n // variant); the message names the invariant explicitly.\n if (!verifyResult.diagnostic.stateName) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.TX_FAILED,\n `close-lease verifier invariant violated: success outcome reached without diagnostic.stateName for lease ${args.leaseUuid}`,\n );\n }\n const finalState: LeaseStateName = verifyResult.diagnostic.stateName;\n const result: CloseLeaseResult = {\n leaseUuid: args.leaseUuid,\n finalState,\n };\n callbacks.onComplete?.(result);\n return result;\n}\n\n// --- Helpers --------------------------------------------------------\n\nfunction validateArgs(args: CloseLeaseArgs): void {\n if (typeof args.leaseUuid !== 'string' || !args.leaseUuid.match(UUID_RE)) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `closeLease: leaseUuid must be a UUID; got \"${args.leaseUuid}\".`,\n );\n }\n}\n\nfunction renderConfirmationBlock(args: CloseLeaseArgs): DeploymentPlanBlock {\n // Image is not tracked in `CloseLeaseArgs` and `stopApp` doesn't return it;\n // surface the gap explicitly so reviewers/users see the missing context\n // rather than silently omitting an image field they'd expect.\n const text = [\n `Close lease ${args.leaseUuid}.`,\n ' Image: (image not recorded)',\n ' This is permanent — the lease cannot be reopened.',\n '',\n 'Proceed?',\n ].join('\\n');\n return { text };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAM,UACJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCF,eAAsB,WACpB,MACA,WACA,MAC2B;AAC3B,cAAa,KAAK;CAElB,MAAM,QAAQ,wBAAwB,KAAK;AAC3C,KAAI,UAAU;MAER,MADgB,UAAU,UAAU,MAAM,KAChC,MACZ,OAAM,IAAI,iBACR,qBAAqB,qBACrB,6CACD;;AAGL,WAAU,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAM,QAAQ,KAAK,eAAe,KAAK,UAAU;CA6FjD,MAAM,eAAe,MAAM,iBAAiB;EArF1C,UAAU,YAAY;GAUpB,IAAI;AACJ,OAAI;AAEF,aAAS,OAAM,MADW,KAAK,cAAc,gBAAgB,EAClC,WAAW,QAAQ,GAAG,MAAM,EACrD,WAAW,KAAK,WACjB,CAAC;YACK,KAAK;IACZ,MAAM,SAAS,yBAAyB,KAAK,UAAU,wBACrD,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAElD,QAAI,UAAU,UACZ,OAAM,UAAU,UAAU,EAAE,QAAQ,CAAC;AAEvC,QAAI,eAAe,iBACjB,OAAM;AAER,UAAM,IAAI,iBAAiB,qBAAqB,cAAc,OAAO;;GAEvE,MAAM,QAAS,QAAgC;AAC/C,OAAI,UAAU,QAAQ,UAAU,KAAA,EAC9B,QAAO;IACL,SAAS;IACT,YAAY,EACV,QAAQ,SAAS,KAAK,UAAU,oCACjC;IACF;GAEH,MAAM,WAAY,MAA8B;GAChD,MAAM,YAAYA,OAChB,OAAO,aAAa,YAAY,OAAO,aAAa,WAChD,WACA,KAAA,EACL;AACD,OAAI,cAAc,KAAA,EAChB,QAAO;IACL,SAAS;IACT,YAAY,EACV,QAAQ,SAAS,KAAK,UAAU,mCAAmC,OAAO,SAAS,CAAC,IACrF;IACF;AAEH,UAAO;IACL,SAAU,WAAW,UAAU,GAAG,aAAa;IAG/C,YAAY,EAAE,WAAW;IAC1B;;EAEH,eAAe,CAAC,WAAW;EAC3B,UAAU;GACR,SAAS;IACP,UAAU;IACV,mBAAmB,CAAC,6BAA6B;IACjD,uBAAuB,OAAO;KAC5B,SAAS;KACT,QACE,EAAE,UACF,8CAA8C,EAAE,aAAa,UAAU;KAC1E;IACD,4BAA4B,EAAE;IAC/B;GACD,WAAW;IACT,UAAU;IACV,mBAAmB,CAAC,+BAA+B;IACnD,uBAAuB,OAAO;KAC5B,SAAS;KACT,QACE,EAAE,UACF,SAAS,KAAK,UAAU;KAC3B;IACD,4BAA4B,EAAE;IAC/B;GACF;EAG6C,EAAE,KAAA,EAAU;AAE5D,KAAI,aAAa,WAAW,WAAW;EACrC,MAAM,SACJ,aAAa,SAAS,UAAU;AAClC,MAAI,UAAU,UACZ,OAAM,UAAU,UAAU,EAAE,QAAQ,CAAC;AAEvC,QAAM,IAAI,iBAAiB,qBAAqB,WAAW,OAAO;;AAapE,KAAI,CAAC,aAAa,WAAW,UAC3B,OAAM,IAAI,iBACR,qBAAqB,WACrB,2GAA2G,KAAK,YACjH;CAEH,MAAM,aAA6B,aAAa,WAAW;CAC3D,MAAM,SAA2B;EAC/B,WAAW,KAAK;EAChB;EACD;AACD,WAAU,aAAa,OAAO;AAC9B,QAAO;;AAKT,SAAS,aAAa,MAA4B;AAChD,KAAI,OAAO,KAAK,cAAc,YAAY,CAAC,KAAK,UAAU,MAAM,QAAQ,CACtE,OAAM,IAAI,iBACR,qBAAqB,gBACrB,8CAA8C,KAAK,UAAU,IAC9D;;AAIL,SAAS,wBAAwB,MAA2C;AAW1E,QAAO,EAAE,MAPI;EACX,eAAe,KAAK,UAAU;EAC9B;EACA;EACA;EACA;EACD,CAAC,KAAK,KACM,EAAE"}
|
package/dist/deploy-app.d.ts
CHANGED
|
@@ -6,8 +6,9 @@ import { DeployAppCallbacks, DeployAppOptions, DeployResult, DeploySpec } from "
|
|
|
6
6
|
* locked composition + E-hybrid runtime-context contract.
|
|
7
7
|
*
|
|
8
8
|
* @throws `ManifestMCPError(INVALID_CONFIG)` for spec / wallet validation.
|
|
9
|
-
* @throws `ManifestMCPError(
|
|
10
|
-
* `'no'` or `onPlan` returns `'cancel'
|
|
9
|
+
* @throws `ManifestMCPError(OPERATION_CANCELLED)` when `onConfirm` returns
|
|
10
|
+
* `'no'` or `onPlan` returns `'cancel'` (deliberate user cancellation —
|
|
11
|
+
* ENG-272).
|
|
11
12
|
*
|
|
12
13
|
* Errors from fred's broadcast or core's recovery primitives surface as
|
|
13
14
|
* typed `ManifestMCPError`s. Partial-success failures with applicable
|
package/dist/deploy-app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy-app.d.ts","names":[],"sources":["../src/deploy-app.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"deploy-app.d.ts","names":[],"sources":["../src/deploy-app.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;iBAiIsB,SAAA,CACpB,IAAA,EAAM,UAAA,EACN,SAAA,EAAW,kBAAA,EACX,IAAA,EAAM,gBAAA,GACL,OAAA,CAAQ,YAAA"}
|
package/dist/deploy-app.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { decode } from "./internals/lease-state.js";
|
|
2
|
+
import { isStackSpec, summarizeSpec, validateSpec } from "./internals/spec-normalize.js";
|
|
3
|
+
import { buildFredDeployInput, buildManifestPreviewInput } from "./internals/build-fred-input.js";
|
|
2
4
|
import { classifyDeployError } from "./internals/classify-deploy-error.js";
|
|
3
5
|
import { extractRunningEndpoints, formatEndpointAsUrl, normalizeFredUrl } from "./internals/connection.js";
|
|
4
6
|
import { classifyDeployResponse } from "./internals/classify-deploy-response.js";
|
|
5
|
-
import { findSkuUuid } from "./internals/find-sku-uuid.js";
|
|
6
7
|
import { EMPTY_DENOM_MAP, loadChainDenomMap } from "./internals/humanize-denom.js";
|
|
8
|
+
import { evaluateReadinessFromFredResponse } from "./internals/evaluate-readiness-from-fred.js";
|
|
9
|
+
import { findSkuUuid } from "./internals/find-sku-uuid.js";
|
|
7
10
|
import { renderDeploymentPlan } from "./internals/render-deployment-plan.js";
|
|
8
|
-
import { isStackSpec, summarizeSpec, validateSpec } from "./internals/spec-normalize.js";
|
|
9
11
|
import { renderIntentRecap } from "./internals/render-intent-recap.js";
|
|
10
12
|
import { renderPartialSuccessPrompt } from "./internals/render-partial-success-prompt.js";
|
|
11
13
|
import { ManifestMCPError, ManifestMCPErrorCode, cosmosEstimateFee, setItemCustomDomain, stopApp } from "@manifest-network/manifest-mcp-core";
|
|
12
|
-
import { AuthTimestampTracker, buildManifestPreview, checkDeploymentReadiness, createAuthToken, createLeaseDataSignMessage, createSignMessage, deployApp as deployApp$1 } from "@manifest-network/manifest-mcp-fred";
|
|
14
|
+
import { AuthTimestampTracker, buildManifestPreview, checkDeploymentReadiness, createAuthToken, createLeaseDataSignMessage, createSignMessage, deployApp as deployApp$1, fetchActiveLease, pollLeaseUntilReady, resolveProviderUrl, uploadLeaseData, waitForAppReady } from "@manifest-network/manifest-mcp-fred";
|
|
13
15
|
//#region src/deploy-app.ts
|
|
14
16
|
/**
|
|
15
17
|
* Public entry point: orchestrate a Manifest-Network app deployment from
|
|
@@ -54,8 +56,9 @@ import { AuthTimestampTracker, buildManifestPreview, checkDeploymentReadiness, c
|
|
|
54
56
|
* locked composition + E-hybrid runtime-context contract.
|
|
55
57
|
*
|
|
56
58
|
* @throws `ManifestMCPError(INVALID_CONFIG)` for spec / wallet validation.
|
|
57
|
-
* @throws `ManifestMCPError(
|
|
58
|
-
* `'no'` or `onPlan` returns `'cancel'
|
|
59
|
+
* @throws `ManifestMCPError(OPERATION_CANCELLED)` when `onConfirm` returns
|
|
60
|
+
* `'no'` or `onPlan` returns `'cancel'` (deliberate user cancellation —
|
|
61
|
+
* ENG-272).
|
|
59
62
|
*
|
|
60
63
|
* Errors from fred's broadcast or core's recovery primitives surface as
|
|
61
64
|
* typed `ManifestMCPError`s. Partial-success failures with applicable
|
|
@@ -81,10 +84,10 @@ async function deployApp(spec, callbacks, opts) {
|
|
|
81
84
|
const chainId = opts.clientManager.getConfig().chainId;
|
|
82
85
|
const activeChain = /mainnet|main/i.test(chainId) ? "mainnet" : "testnet";
|
|
83
86
|
const queryClient = await opts.clientManager.getQueryClient();
|
|
84
|
-
let readiness =
|
|
87
|
+
let readiness = evaluateReadinessFromFredResponse(await checkDeploymentReadiness(queryClient, tenantAddress, {
|
|
85
88
|
image: primaryImage(spec),
|
|
86
89
|
size: requestedSize(spec)
|
|
87
|
-
}), opts.clientManager.getConfig().gasPrice ?? "1umfx", denomMap);
|
|
90
|
+
}), opts.clientManager.getConfig().gasPrice ?? "1umfx", denomMap, tenantAddress);
|
|
88
91
|
callbacks.onProgress?.({
|
|
89
92
|
kind: "readiness_evaluated",
|
|
90
93
|
readiness
|
|
@@ -117,7 +120,7 @@ async function deployApp(spec, callbacks, opts) {
|
|
|
117
120
|
});
|
|
118
121
|
if (callbacks.onPlan) {
|
|
119
122
|
const verdict = await callbacks.onPlan(plan);
|
|
120
|
-
if (verdict === "cancel") throw new ManifestMCPError(ManifestMCPErrorCode.
|
|
123
|
+
if (verdict === "cancel") throw new ManifestMCPError(ManifestMCPErrorCode.OPERATION_CANCELLED, "User cancelled deployment at plan step.");
|
|
121
124
|
if (verdict !== "confirm") {
|
|
122
125
|
confirmedSpec = applyPlanEdit(confirmedSpec, verdict);
|
|
123
126
|
try {
|
|
@@ -125,10 +128,10 @@ async function deployApp(spec, callbacks, opts) {
|
|
|
125
128
|
} catch (err) {
|
|
126
129
|
throw new ManifestMCPError(ManifestMCPErrorCode.INVALID_CONFIG, err instanceof Error ? `Post-edit spec failed validation: ${err.message}` : `Post-edit spec failed validation: ${String(err)}`);
|
|
127
130
|
}
|
|
128
|
-
readiness =
|
|
131
|
+
readiness = evaluateReadinessFromFredResponse(await checkDeploymentReadiness(queryClient, tenantAddress, {
|
|
129
132
|
image: primaryImage(confirmedSpec),
|
|
130
133
|
size: requestedSize(confirmedSpec)
|
|
131
|
-
}), opts.clientManager.getConfig().gasPrice ?? "1umfx", denomMap);
|
|
134
|
+
}), opts.clientManager.getConfig().gasPrice ?? "1umfx", denomMap, tenantAddress);
|
|
132
135
|
callbacks.onProgress?.({
|
|
133
136
|
kind: "readiness_evaluated",
|
|
134
137
|
readiness
|
|
@@ -162,7 +165,7 @@ async function deployApp(spec, callbacks, opts) {
|
|
|
162
165
|
activeChain
|
|
163
166
|
}) };
|
|
164
167
|
if (callbacks.onConfirm) {
|
|
165
|
-
if (await callbacks.onConfirm(recapBlock) !== "yes") throw new ManifestMCPError(ManifestMCPErrorCode.
|
|
168
|
+
if (await callbacks.onConfirm(recapBlock) !== "yes") throw new ManifestMCPError(ManifestMCPErrorCode.OPERATION_CANCELLED, "User declined to proceed at intent-recap step.");
|
|
166
169
|
}
|
|
167
170
|
callbacks.onProgress?.({ kind: "user_confirmed" });
|
|
168
171
|
const signArbitrary = opts.walletProvider.signArbitrary.bind(opts.walletProvider);
|
|
@@ -183,16 +186,72 @@ async function deployApp(spec, callbacks, opts) {
|
|
|
183
186
|
try {
|
|
184
187
|
fredResult = await deployApp$1(opts.clientManager, getAuthToken, getLeaseDataAuthToken, fredInput, opts.fetchFn);
|
|
185
188
|
} catch (err) {
|
|
186
|
-
|
|
189
|
+
const recoveryCtx = {
|
|
190
|
+
manifestJson: preview.manifest_json,
|
|
191
|
+
metaHash: preview.meta_hash_hex,
|
|
192
|
+
getAuthToken,
|
|
193
|
+
getLeaseDataAuthToken,
|
|
194
|
+
tenantAddress,
|
|
195
|
+
chainId,
|
|
196
|
+
denomMap
|
|
197
|
+
};
|
|
198
|
+
return await handleBroadcastFailure(err, confirmedSpec, callbacks, opts, recoveryCtx);
|
|
187
199
|
}
|
|
188
|
-
|
|
200
|
+
let liveState = fredResult.state;
|
|
201
|
+
let liveConnection = fredResult.connection;
|
|
202
|
+
let classification = classifyDeployResponse(fredResult);
|
|
189
203
|
callbacks.onProgress?.({
|
|
190
204
|
kind: "deploy_response_classified",
|
|
191
205
|
outcome: classification.outcome
|
|
192
206
|
});
|
|
193
|
-
if (classification.outcome
|
|
194
|
-
const
|
|
195
|
-
throw new ManifestMCPError(ManifestMCPErrorCode.
|
|
207
|
+
if (classification.outcome === "failed") {
|
|
208
|
+
const reason = classification.errorSummary ?? `fred deployApp returned failed outcome for lease ${classification.leaseUuid ?? "<no-uuid>"}`;
|
|
209
|
+
throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);
|
|
210
|
+
}
|
|
211
|
+
if (classification.outcome === "needs_wait") {
|
|
212
|
+
if (!classification.leaseUuid) throw new ManifestMCPError(ManifestMCPErrorCode.INVALID_CONFIG, "Internal invariant: classifier returned needs_wait without leaseUuid.");
|
|
213
|
+
const leaseUuid = classification.leaseUuid;
|
|
214
|
+
const pollStartMs = Date.now();
|
|
215
|
+
let attempt = 0;
|
|
216
|
+
let pollResult;
|
|
217
|
+
try {
|
|
218
|
+
pollResult = await waitForAppReady(queryClient, tenantAddress, leaseUuid, getAuthToken, {
|
|
219
|
+
timeoutMs: opts.waitForReadyTimeoutMs ?? 48e4,
|
|
220
|
+
onProgress: (status) => {
|
|
221
|
+
attempt += 1;
|
|
222
|
+
const stateName = decode(status.state);
|
|
223
|
+
callbacks.onProgress?.({
|
|
224
|
+
kind: "polling_for_readiness",
|
|
225
|
+
leaseUuid,
|
|
226
|
+
attempt,
|
|
227
|
+
elapsedMs: Date.now() - pollStartMs,
|
|
228
|
+
...stateName !== void 0 ? { state: stateName } : {}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}, opts.fetchFn);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
const reason = err instanceof Error ? `wait_for_app_ready failed for lease ${leaseUuid}: ${err.message}` : `wait_for_app_ready failed for lease ${leaseUuid}: ${String(err)}`;
|
|
234
|
+
throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);
|
|
235
|
+
}
|
|
236
|
+
classification = classifyDeployResponse({
|
|
237
|
+
lease_uuid: pollResult.lease_uuid,
|
|
238
|
+
provider_uuid: pollResult.provider_uuid,
|
|
239
|
+
provider_url: pollResult.provider_url,
|
|
240
|
+
state: pollResult.state,
|
|
241
|
+
connection: pollResult.status
|
|
242
|
+
});
|
|
243
|
+
if (classification.outcome !== "active") {
|
|
244
|
+
const reason = classification.errorSummary ?? `wait_for_app_ready returned for lease ${leaseUuid} but post-poll classifier outcome is ${classification.outcome}`;
|
|
245
|
+
throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);
|
|
246
|
+
}
|
|
247
|
+
fredResult = {
|
|
248
|
+
...fredResult,
|
|
249
|
+
lease_uuid: pollResult.lease_uuid,
|
|
250
|
+
provider_uuid: pollResult.provider_uuid,
|
|
251
|
+
provider_url: pollResult.provider_url
|
|
252
|
+
};
|
|
253
|
+
liveState = pollResult.status.state;
|
|
254
|
+
liveConnection = pollResult.status;
|
|
196
255
|
}
|
|
197
256
|
callbacks.onProgress?.({
|
|
198
257
|
kind: "app_ready_confirmed",
|
|
@@ -211,13 +270,13 @@ async function deployApp(spec, callbacks, opts) {
|
|
|
211
270
|
callbacks
|
|
212
271
|
});
|
|
213
272
|
let leaseStateDecoded;
|
|
214
|
-
if (
|
|
273
|
+
if (liveState === void 0) leaseStateDecoded = "LEASE_STATE_ACTIVE";
|
|
215
274
|
else {
|
|
216
|
-
const decoded = decode(
|
|
217
|
-
if (decoded === void 0) throw new ManifestMCPError(ManifestMCPErrorCode.INVALID_CONFIG, `Unrecognized lease state from fred deployApp response: ${String(
|
|
275
|
+
const decoded = decode(liveState);
|
|
276
|
+
if (decoded === void 0) throw new ManifestMCPError(ManifestMCPErrorCode.INVALID_CONFIG, `Unrecognized lease state from fred deployApp response: ${String(liveState)}. Cannot safely classify; refusing to silently coerce to ACTIVE.`);
|
|
218
277
|
leaseStateDecoded = decoded;
|
|
219
278
|
}
|
|
220
|
-
const endpointUrls = extractRunningEndpoints(
|
|
279
|
+
const endpointUrls = extractRunningEndpoints(liveConnection).map(formatEndpointAsUrl);
|
|
221
280
|
const fallbackUrl = typeof fredResult.url === "string" ? normalizeFredUrl(fredResult.url) : "";
|
|
222
281
|
const result = {
|
|
223
282
|
leaseUuid: fredResult.lease_uuid,
|
|
@@ -251,40 +310,16 @@ function customDomainOf(spec) {
|
|
|
251
310
|
function customDomainServiceOf(spec) {
|
|
252
311
|
if (isStackSpec(spec)) return spec.serviceName;
|
|
253
312
|
}
|
|
254
|
-
function evaluateReadinessFromRaw(raw, gasPrice, denomMap) {
|
|
255
|
-
const rawAny = raw;
|
|
256
|
-
return {
|
|
257
|
-
status: "ok",
|
|
258
|
-
reasons: [],
|
|
259
|
-
suggestedActions: [],
|
|
260
|
-
walletBalances: rawAny.wallet_balances ?? [],
|
|
261
|
-
credits: rawAny.credits ?? null,
|
|
262
|
-
sku: rawAny.sku ?? null
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
function buildManifestPreviewInput(spec, size) {
|
|
266
|
-
if (isStackSpec(spec)) return {
|
|
267
|
-
size,
|
|
268
|
-
services: spec.services
|
|
269
|
-
};
|
|
270
|
-
const single = spec;
|
|
271
|
-
return {
|
|
272
|
-
size,
|
|
273
|
-
image: single.image,
|
|
274
|
-
port: typeof single.port === "number" ? single.port : Array.isArray(single.port) ? single.port[0] : void 0,
|
|
275
|
-
env: single.env
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
313
|
async function estimateFees(opts, spec, metaHashHex) {
|
|
279
314
|
const size = requestedSize(spec);
|
|
280
315
|
const { skuUuid } = await findSkuUuid(opts.clientManager, size);
|
|
281
|
-
const
|
|
316
|
+
const itemArgs = isStackSpec(spec) ? Object.keys(spec.services).map((name) => `${skuUuid}:1:${name}`) : [`${skuUuid}:1`];
|
|
282
317
|
let createLeaseEstimate;
|
|
283
318
|
try {
|
|
284
319
|
createLeaseEstimate = await cosmosEstimateFee(opts.clientManager, "billing", "create-lease", [
|
|
285
320
|
"--meta-hash",
|
|
286
321
|
metaHashHex,
|
|
287
|
-
|
|
322
|
+
...itemArgs
|
|
288
323
|
]);
|
|
289
324
|
} catch (err) {
|
|
290
325
|
const msg = `Failed to estimate create-lease fee: ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -301,27 +336,10 @@ async function estimateFees(opts, spec, metaHashHex) {
|
|
|
301
336
|
},
|
|
302
337
|
...typeof customDomainOf(spec) === "string" ? { setDomain: {
|
|
303
338
|
notEstimated: true,
|
|
304
|
-
reason: "
|
|
339
|
+
reason: "no representative lease for pre-broadcast simulation"
|
|
305
340
|
} } : {}
|
|
306
341
|
};
|
|
307
342
|
}
|
|
308
|
-
function buildFredDeployInput(spec, size) {
|
|
309
|
-
const base = { size };
|
|
310
|
-
if (isStackSpec(spec)) base.services = spec.services;
|
|
311
|
-
else {
|
|
312
|
-
const single = spec;
|
|
313
|
-
base.image = single.image;
|
|
314
|
-
base.port = typeof single.port === "number" ? single.port : Array.isArray(single.port) ? single.port[0] : void 0;
|
|
315
|
-
base.env = single.env;
|
|
316
|
-
}
|
|
317
|
-
const customDomain = spec.customDomain;
|
|
318
|
-
if (customDomain) {
|
|
319
|
-
base.customDomain = customDomain;
|
|
320
|
-
const svcName = customDomainServiceOf(spec);
|
|
321
|
-
if (svcName) base.serviceName = svcName;
|
|
322
|
-
}
|
|
323
|
-
return base;
|
|
324
|
-
}
|
|
325
343
|
function applyPlanEdit(spec, edit) {
|
|
326
344
|
if (edit.kind === "replace_spec") return edit.spec;
|
|
327
345
|
if (edit.kind === "edit_env") {
|
|
@@ -354,7 +372,7 @@ function applyPlanEdit(spec, edit) {
|
|
|
354
372
|
}
|
|
355
373
|
return spec;
|
|
356
374
|
}
|
|
357
|
-
async function handleBroadcastFailure(err, spec, callbacks, opts) {
|
|
375
|
+
async function handleBroadcastFailure(err, spec, callbacks, opts, ctx) {
|
|
358
376
|
const requestedCustomDomain = customDomainOf(spec);
|
|
359
377
|
const classified = classifyDeployError(err, { ...requestedCustomDomain ? { expectedCustomDomain: requestedCustomDomain } : {} });
|
|
360
378
|
if (classified.outcome === "partially_succeeded" && classified.leaseUuid) {
|
|
@@ -364,33 +382,34 @@ async function handleBroadcastFailure(err, spec, callbacks, opts) {
|
|
|
364
382
|
...requestedCustomDomain ? { requestedCustomDomain } : {},
|
|
365
383
|
reason: classified.reason
|
|
366
384
|
};
|
|
367
|
-
const
|
|
385
|
+
const promptPayload = renderPartialSuccessPrompt({
|
|
368
386
|
leaseUuid: classified.leaseUuid,
|
|
369
387
|
decodedState: "LEASE_STATE_PENDING",
|
|
370
388
|
reason: classified.reason,
|
|
371
389
|
...requestedCustomDomain ? { requestedCustomDomain } : {}
|
|
372
|
-
})
|
|
390
|
+
});
|
|
391
|
+
const options = promptPayload.options.map((id) => ({
|
|
373
392
|
id,
|
|
374
393
|
label: recoveryOptionLabel(id),
|
|
375
394
|
description: recoveryOptionDescription(id)
|
|
376
395
|
}));
|
|
377
|
-
if (options.length > 0 && callbacks.onFailure !== void 0)
|
|
396
|
+
if (options.length > 0 && callbacks.onFailure !== void 0) {
|
|
397
|
+
callbacks.onProgress?.({
|
|
398
|
+
kind: "partial_success_prompt_rendered",
|
|
399
|
+
prompt: promptPayload.prompt,
|
|
400
|
+
leaseUuid: envelope.leaseUuid
|
|
401
|
+
});
|
|
402
|
+
return await dispatchRecovery(await callbacks.onFailure(envelope, options), envelope, spec, opts, callbacks, ctx);
|
|
403
|
+
}
|
|
378
404
|
throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, classified.reason);
|
|
379
405
|
}
|
|
380
406
|
classified.reason;
|
|
381
407
|
throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, classified.reason);
|
|
382
408
|
}
|
|
383
|
-
async function dispatchRecovery(choice, envelope, spec, opts) {
|
|
409
|
+
async function dispatchRecovery(choice, envelope, spec, opts, callbacks, ctx) {
|
|
384
410
|
const leaseUuid = envelope.outcome === "partially_succeeded" ? envelope.leaseUuid : "";
|
|
385
411
|
switch (choice.id) {
|
|
386
|
-
case "retry_set_domain":
|
|
387
|
-
const domain = customDomainOf(spec);
|
|
388
|
-
if (!domain) throw new ManifestMCPError(ManifestMCPErrorCode.INVALID_CONFIG, "retry_set_domain requires a customDomain in spec.");
|
|
389
|
-
const serviceName = customDomainServiceOf(spec);
|
|
390
|
-
const setItemOpts = serviceName ? { serviceName } : void 0;
|
|
391
|
-
await setItemCustomDomain(opts.clientManager, leaseUuid, domain, setItemOpts);
|
|
392
|
-
throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, `retry_set_domain completed for ${leaseUuid}; caller should re-run troubleshootDeployment to confirm app readiness.`);
|
|
393
|
-
}
|
|
412
|
+
case "retry_set_domain": return await retrySetDomainAndComplete(leaseUuid, spec, opts, callbacks, ctx);
|
|
394
413
|
case "salvage_without_domain": throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, `salvage_without_domain: lease ${leaseUuid} retained without domain; caller should re-run troubleshootDeployment.`);
|
|
395
414
|
case "cancel_lease":
|
|
396
415
|
case "close_lease":
|
|
@@ -399,6 +418,155 @@ async function dispatchRecovery(choice, envelope, spec, opts) {
|
|
|
399
418
|
}
|
|
400
419
|
throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, `Unknown recovery option: ${choice.id}`);
|
|
401
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* `retry_set_domain` recovery: decompose the deploy after the partial-
|
|
423
|
+
* success failure. ENG-185 sub-PR E.
|
|
424
|
+
*
|
|
425
|
+
* Steps (mirrors fred's atomic `deployApp` minus the create-lease tx,
|
|
426
|
+
* which already succeeded):
|
|
427
|
+
* 1. `setItemCustomDomain` — broadcast the domain claim against the
|
|
428
|
+
* pre-existing lease. Stack specs thread `serviceName` so the
|
|
429
|
+
* tx targets the named lease item.
|
|
430
|
+
* 2. `fetchActiveLease` + `resolveProviderUrl` — look up the provider
|
|
431
|
+
* URL from the on-chain lease record (the partial-success error
|
|
432
|
+
* envelope only carries `leaseUuid`).
|
|
433
|
+
* 3. `uploadLeaseData` — push the manifest payload to the provider.
|
|
434
|
+
* Uses the ADR-036 lease-data auth token (signed against the
|
|
435
|
+
* manifest's meta-hash).
|
|
436
|
+
* 4. `pollLeaseUntilReady` — poll until the provider reports ACTIVE +
|
|
437
|
+
* running. Uses the LOWER-LEVEL primitive (not `waitForAppReady`)
|
|
438
|
+
* so the already-resolved `providerApiUrl` and auth-token closure
|
|
439
|
+
* pass through directly — no redundant on-chain queries (Copilot
|
|
440
|
+
* fix-1, PR #71). Reuses D's canonical polling-emission pattern:
|
|
441
|
+
* `onProgress` closure translates each `FredLeaseStatus` sample
|
|
442
|
+
* into a typed `polling_for_readiness` ProgressEvent, default
|
|
443
|
+
* 480_000ms timeout overridable via `opts.waitForReadyTimeoutMs`.
|
|
444
|
+
* 5. Defense #2 parity (post-poll re-classify) — guard the
|
|
445
|
+
* ACTIVE-with-no-instances race per D's pattern.
|
|
446
|
+
* 6. Persist manifest (best-effort) + build typed `DeployResult` +
|
|
447
|
+
* emit `app_ready_confirmed` + `success_rendered` + onComplete.
|
|
448
|
+
*
|
|
449
|
+
* Failure paths (sibling-parity wraps — every catch site surfaces
|
|
450
|
+
* `retry_set_domain <primitive-name> failed for lease ${leaseUuid}:
|
|
451
|
+
* ${err.message}` in the thrown message, matching D's L548-550 style).
|
|
452
|
+
* Error-code policy: typed `ManifestMCPError`s flow through with their
|
|
453
|
+
* original code preserved (precedent at `estimateFees` — see the
|
|
454
|
+
* `cosmosEstimateFee` catch block); untyped errors default to
|
|
455
|
+
* `TX_FAILED`. The post-poll re-classify path likewise prefixes BOTH
|
|
456
|
+
* the errorSummary-set and the no-errorSummary branches with
|
|
457
|
+
* `retry_set_domain` + leaseUuid (Copilot fix-4, PR #71):
|
|
458
|
+
* - `setItemCustomDomain` throws → wrap with prefix + leaseUuid +
|
|
459
|
+
* code preservation. Most likely cause: chain rejected the
|
|
460
|
+
* set-item-custom-domain tx (FQDN validation, reserved-suffix
|
|
461
|
+
* match, lease not active, etc.).
|
|
462
|
+
* - `fetchActiveLease` / `resolveProviderUrl` throw → wrap with
|
|
463
|
+
* prefix + leaseUuid.
|
|
464
|
+
* - `uploadLeaseData` throws → wrap with prefix + leaseUuid.
|
|
465
|
+
* - `pollLeaseUntilReady` throws → wrap with prefix + leaseUuid.
|
|
466
|
+
* - Post-poll re-classify outcome !== 'active' → wrap both
|
|
467
|
+
* branches: errorSummary-set (terminal-state response) AND
|
|
468
|
+
* no-errorSummary fallback (ACTIVE-with-no-instances Defense #2
|
|
469
|
+
* race) carry prefix + leaseUuid.
|
|
470
|
+
*/
|
|
471
|
+
async function retrySetDomainAndComplete(leaseUuid, spec, opts, callbacks, ctx) {
|
|
472
|
+
const domain = customDomainOf(spec);
|
|
473
|
+
if (!domain) throw new ManifestMCPError(ManifestMCPErrorCode.INVALID_CONFIG, "retry_set_domain requires a customDomain in spec.");
|
|
474
|
+
const serviceName = customDomainServiceOf(spec);
|
|
475
|
+
const setItemOpts = serviceName ? { serviceName } : void 0;
|
|
476
|
+
try {
|
|
477
|
+
await setItemCustomDomain(opts.clientManager, leaseUuid, domain, setItemOpts);
|
|
478
|
+
} catch (err) {
|
|
479
|
+
const reason = err instanceof Error ? `retry_set_domain set-item-custom-domain failed for lease ${leaseUuid}: ${err.message}` : `retry_set_domain set-item-custom-domain failed for lease ${leaseUuid}: ${String(err)}`;
|
|
480
|
+
throw new ManifestMCPError(err instanceof ManifestMCPError ? err.code : ManifestMCPErrorCode.TX_FAILED, reason);
|
|
481
|
+
}
|
|
482
|
+
const queryClient = await opts.clientManager.getQueryClient();
|
|
483
|
+
let lease;
|
|
484
|
+
let providerApiUrl;
|
|
485
|
+
try {
|
|
486
|
+
lease = await fetchActiveLease(queryClient, leaseUuid, "cannot complete retry_set_domain");
|
|
487
|
+
providerApiUrl = await resolveProviderUrl(queryClient, lease.providerUuid);
|
|
488
|
+
} catch (err) {
|
|
489
|
+
const reason = err instanceof Error ? `retry_set_domain failed to resolve provider for lease ${leaseUuid}: ${err.message}` : `retry_set_domain failed to resolve provider for lease ${leaseUuid}: ${String(err)}`;
|
|
490
|
+
throw new ManifestMCPError(err instanceof ManifestMCPError ? err.code : ManifestMCPErrorCode.TX_FAILED, reason);
|
|
491
|
+
}
|
|
492
|
+
const manifestBytes = new TextEncoder().encode(ctx.manifestJson);
|
|
493
|
+
try {
|
|
494
|
+
const leaseDataAuthToken = await ctx.getLeaseDataAuthToken(ctx.tenantAddress, leaseUuid, ctx.metaHash);
|
|
495
|
+
await uploadLeaseData(providerApiUrl, leaseUuid, manifestBytes, leaseDataAuthToken, opts.fetchFn);
|
|
496
|
+
} catch (err) {
|
|
497
|
+
const reason = err instanceof Error ? `retry_set_domain manifest upload failed for lease ${leaseUuid}: ${err.message}` : `retry_set_domain manifest upload failed for lease ${leaseUuid}: ${String(err)}`;
|
|
498
|
+
throw new ManifestMCPError(err instanceof ManifestMCPError ? err.code : ManifestMCPErrorCode.TX_FAILED, reason);
|
|
499
|
+
}
|
|
500
|
+
const pollStartMs = Date.now();
|
|
501
|
+
let attempt = 0;
|
|
502
|
+
let pollResult;
|
|
503
|
+
try {
|
|
504
|
+
pollResult = await pollLeaseUntilReady(providerApiUrl, leaseUuid, () => ctx.getAuthToken(ctx.tenantAddress, leaseUuid), {
|
|
505
|
+
timeoutMs: opts.waitForReadyTimeoutMs ?? 48e4,
|
|
506
|
+
onProgress: (status) => {
|
|
507
|
+
attempt += 1;
|
|
508
|
+
const stateName = decode(status.state);
|
|
509
|
+
callbacks.onProgress?.({
|
|
510
|
+
kind: "polling_for_readiness",
|
|
511
|
+
leaseUuid,
|
|
512
|
+
attempt,
|
|
513
|
+
elapsedMs: Date.now() - pollStartMs,
|
|
514
|
+
...stateName !== void 0 ? { state: stateName } : {}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}, opts.fetchFn);
|
|
518
|
+
} catch (err) {
|
|
519
|
+
const reason = err instanceof Error ? `retry_set_domain pollLeaseUntilReady failed for lease ${leaseUuid}: ${err.message}` : `retry_set_domain pollLeaseUntilReady failed for lease ${leaseUuid}: ${String(err)}`;
|
|
520
|
+
throw new ManifestMCPError(err instanceof ManifestMCPError ? err.code : ManifestMCPErrorCode.TX_FAILED, reason);
|
|
521
|
+
}
|
|
522
|
+
const classification = classifyDeployResponse({
|
|
523
|
+
lease_uuid: leaseUuid,
|
|
524
|
+
provider_uuid: lease.providerUuid,
|
|
525
|
+
provider_url: providerApiUrl,
|
|
526
|
+
state: pollResult.state,
|
|
527
|
+
connection: pollResult
|
|
528
|
+
});
|
|
529
|
+
if (classification.outcome !== "active") {
|
|
530
|
+
const reason = classification.errorSummary !== void 0 ? `retry_set_domain post-poll re-classification failed for lease ${leaseUuid}: ${classification.errorSummary}` : `retry_set_domain: pollLeaseUntilReady returned for lease ${leaseUuid} but post-poll classifier outcome is ${classification.outcome}`;
|
|
531
|
+
throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);
|
|
532
|
+
}
|
|
533
|
+
callbacks.onProgress?.({
|
|
534
|
+
kind: "app_ready_confirmed",
|
|
535
|
+
leaseUuid
|
|
536
|
+
});
|
|
537
|
+
const persistedPath = await tryPersistManifest({
|
|
538
|
+
leaseUuid,
|
|
539
|
+
image: primaryImage(spec),
|
|
540
|
+
size: requestedSize(spec),
|
|
541
|
+
metaHash: ctx.metaHash,
|
|
542
|
+
chainId: ctx.chainId,
|
|
543
|
+
manifestJson: ctx.manifestJson,
|
|
544
|
+
customDomain: domain,
|
|
545
|
+
customDomainService: serviceName,
|
|
546
|
+
dataDir: opts.dataDir,
|
|
547
|
+
callbacks
|
|
548
|
+
});
|
|
549
|
+
const liveState = pollResult.state;
|
|
550
|
+
let leaseStateDecoded;
|
|
551
|
+
const decoded = decode(liveState);
|
|
552
|
+
if (decoded === void 0) throw new ManifestMCPError(ManifestMCPErrorCode.INVALID_CONFIG, `Unrecognized lease state from pollLeaseUntilReady response: ${String(liveState)}. Cannot safely classify; refusing to silently coerce to ACTIVE.`);
|
|
553
|
+
leaseStateDecoded = decoded;
|
|
554
|
+
const endpointUrls = extractRunningEndpoints(pollResult).map(formatEndpointAsUrl);
|
|
555
|
+
const result = {
|
|
556
|
+
leaseUuid,
|
|
557
|
+
providerUuid: lease.providerUuid,
|
|
558
|
+
leaseState: leaseStateDecoded,
|
|
559
|
+
urls: endpointUrls,
|
|
560
|
+
customDomain: domain,
|
|
561
|
+
manifestPath: persistedPath ?? ""
|
|
562
|
+
};
|
|
563
|
+
callbacks.onProgress?.({
|
|
564
|
+
kind: "success_rendered",
|
|
565
|
+
result
|
|
566
|
+
});
|
|
567
|
+
callbacks.onComplete?.(result);
|
|
568
|
+
return result;
|
|
569
|
+
}
|
|
402
570
|
function recoveryOptionLabel(id) {
|
|
403
571
|
switch (id) {
|
|
404
572
|
case "retry_set_domain": return "Retry set-domain + upload";
|