@dvmkit/dvmctl 0.3.4-rc.1 → 0.3.5-rc.1

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.
@@ -1,273 +0,0 @@
1
- ## Quickstart — scaffold with `dvmctl create`
2
-
3
- ```bash
4
- dvmctl create my-dvm # scaffolds ./my-dvm/
5
- cd my-dvm
6
- npm install
7
- npm test # the scaffold ships a passing test
8
- dvmctl dev handler.ts # http://localhost:3000, hot reload, /_dev test page
9
- ```
10
-
11
- > Zod ships with the SDK — `import { z } from "@dvmkit/sdk"` for typed `input:` schemas, no extra
12
- > install. Using the SDK's own zod guarantees a builder schema and the SDK validate against one
13
- > instance. Skip the schema and `ctx.input` is a raw string you parse yourself.
14
-
15
- `dvmctl create <name>` writes a standalone npm project:
16
-
17
- ```
18
- my-dvm/
19
- handler.ts # your DVM — a configureDVM(...) descriptor (the file you edit)
20
- serve.ts # production entry point — calls serve(handler, {...})
21
- handler.test.ts # vitest unit tests
22
- package.json # depends on @dvmkit/sdk; scripts: test / dev / deploy
23
- tsconfig.json
24
- Dockerfile # two-stage Node 22 build (add system deps here)
25
- .env.example # copy to .env for API keys / secrets
26
- README.md
27
- ```
28
-
29
- The generated `handler.ts` is a one-capability echo stub. Replace its `onJob` body with your
30
- logic; everything below is how to grow it.
31
-
32
- > **Self-hosting instead of deploying to the platform?** Run `dvmctl init <handle> --self-hosted`
33
- > once to generate a stable `canonical_dvm_id` and persist it to `~/.dvmkit/builder.json` (a
34
- > platform deploy assigns this ID for you at deploy time). You then `serve(dvm)` on your own
35
- > infrastructure.
36
-
37
- **Imports** (the SDK ships three entry points):
38
-
39
- ```typescript
40
- import { configureDVM } from "@dvmkit/sdk"; // descriptor + types
41
- import { serve, createDVMHost } from "@dvmkit/sdk/server"; // hosting
42
- import { secp256k1Auth } from "@dvmkit/sdk/server"; // descriptor-level signed-request auth
43
- import { createTestContext } from "@dvmkit/sdk/testing"; // unit tests
44
- ```
45
-
46
- ---
47
-
48
- ## Quick example — single-capability DVM
49
-
50
- `handler.ts`:
51
-
52
- ```typescript
53
- import { configureDVM, z } from "@dvmkit/sdk";
54
-
55
- const inputSchema = z.object({ text: z.string() });
56
-
57
- export function createUppercaseDVM() {
58
- return configureDVM({
59
- name: "uppercase",
60
- description: "Convert text to uppercase.",
61
- capability: "uppercase",
62
- tags: ["text", "uppercase"],
63
- input: inputSchema,
64
- price: "$0.01",
65
-
66
- async onJob(ctx) {
67
- ctx.complete(ctx.input.text.toUpperCase());
68
- },
69
- });
70
- }
71
-
72
- export default createUppercaseDVM();
73
- ```
74
-
75
- `serve.ts`:
76
-
77
- ```typescript
78
- import { serve } from "@dvmkit/sdk/server";
79
- import dvm from "./handler";
80
-
81
- await serve(dvm);
82
- ```
83
-
84
- `serve(dvm)` reads all wiring (port, `DATABASE_URL`, `DVMKIT_CASHU_MINTS`, Tempo, x402, FX,
85
- platform reporter) from env. Pass `opts` to override; reach for `createDVMHost` when you need
86
- custom HTTP routes or to mount several DVMs on one host.
87
-
88
- ---
89
-
90
- ## configureDVM() reference
91
-
92
- ```typescript
93
- configureDVM({
94
- // --- DVM-level (always at top level) ---
95
- name: string,
96
- description?: string,
97
- tag?: string,
98
- tags?: string[],
99
- idleTimeout?: number, // seconds before auto-cancel (default: 3600)
100
-
101
- auth?: DVMAuthScheme, // descriptor-level signed-request auth (secp256k1Auth({...}))
102
-
103
- onBoot?(env): void | Promise<void>, // run once at server start
104
- onShutdown?(): void | Promise<void>, // run once at server stop
105
- routes?(app: Hono): void | Promise<void>, // DVM-scoped non-protocol routes
106
-
107
- paymentMethods?: PaymentMethod[], // ["cashu" | "x402" | "tempo"]; auto-derived from configured rails
108
-
109
- // --- Pick one of the two shapes below ---
110
-
111
- // Flat single-capability shape (most DVMs):
112
- capability?: string, // e.g. "synthesise" — names this one capability
113
- // (auto-slugified from `name` if omitted)
114
- input?: ZodSchema, // ctx.input is typed
115
- state?: object, // ctx.state defaults, typed
116
- price?: PriceValue, // "$0.05" — USD literal, the only form
117
- onQuote?: QuoteConfig, // dynamic pricing
118
- onJob(ctx): void | Promise<void>, // required
119
- onResponse?(ctx, content): void | Promise<void>,
120
- onPayment?(ctx, content): void | Promise<void>,
121
- onApproval?(ctx, content): void | Promise<void>,
122
- onCancel?(ctx, content): void | Promise<void>,
123
- onMessage?(ctx, msg): void | Promise<void>,
124
-
125
- // OR multi-capability block (mutually exclusive with flat shape):
126
- capabilities?: Record<string, CapabilityConfig>,
127
- });
128
- ```
129
-
130
- `configureDVM` returns a frozen `DVMDescriptor`. The flat shape desugars internally to a
131
- one-entry `capabilities` map — `dvm.capabilities[<name>].onJob` works in either form (tests rely
132
- on this).
133
-
134
- ### Per-capability fields (multi-capability shape)
135
-
136
- Each entry in `capabilities` accepts: `description`, `input`, `state`, `price`, `onQuote`,
137
- `onJob` (required), and the same message hooks (`onResponse`, `onPayment`, `onApproval`,
138
- `onCancel`, `onMessage`). DVM-level concerns (`name`, `description`, `tags`, `auth`,
139
- `paymentMethods`, `routes`, lifecycle hooks) stay on the parent and are shared across every
140
- capability.
141
-
142
- ---
143
-
144
- ## SDKJobContext reference
145
-
146
- ### Identity (readonly)
147
-
148
- | Property | Type | Description |
149
- | ------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------- |
150
- | `jobId` | `string` | Server-assigned job ID |
151
- | `tags` | `string[]` | Tags from the DVM descriptor |
152
- | `input` | `Input` | Job input (raw string, or parsed object when `input` schema is set) |
153
- | `params` | `Record<string, string>` | Key-value parameters |
154
- | `requesterId` | `string` | Opaque requester ID (derived from auth or "anonymous") |
155
- | `paidMsats` | `number` | Total msats paid so far |
156
- | `auth` | `{ pubkey, envelope } \| undefined` | Verified caller identity — present only when the descriptor declares `auth: secp256k1Auth(...)` |
157
-
158
- ### Messaging
159
-
160
- ```typescript
161
- ctx.text(message: string): void
162
- ctx.artifact(content: ArtifactContent): void
163
- ctx.sendMessage(type: MessageType, content: object): void
164
- ctx.progress(percentComplete: number, phase?: string, hint?: string): void
165
- ```
166
-
167
- `ArtifactContent`: `{ data: string, mime_type: string, name?: string, encoding?: "base64" }`
168
-
169
- ### Flow control
170
-
171
- ```typescript
172
- ctx.prompt(id: string, text: string, opts?): Promise<ResponseContent>
173
- ctx.requestPayment(
174
- amount: number | { amount: number; currency: "usd" },
175
- reason: string,
176
- opts?: SDKPaymentRequestOpts,
177
- ): Promise<PaymentContent>
178
- ctx.working(estimateSeconds?: number, hint?: string): void
179
- ctx.complete(summary: string): void // one-liner; artifacts carry the payload
180
- ctx.fail(error: string, opts?: { refund?: boolean }): void
181
- ctx.cost({ amount, currency }): void // your serving cost, in major units
182
- ```
183
-
184
- `requestPayment` takes msats (`number`) or a fiat envelope (`{ amount, currency: "usd" }`) — the
185
- SDK converts to sats at send-time. The legacy `"$0.05"` string form was removed. On counter-based
186
- auto-credit, `requestPayment` resolves synchronously when the upfront pool already
187
- covers the amount and only suspends when the pool is exhausted. The caller sees whatever currency
188
- you name; the ledger records the charge in the DVM's own `currency`, converted at the moment the
189
- charge is made, so the anti-double-charge ceiling on the job's credit stays exact.
190
-
191
- Pass `{ refund: true }` to `ctx.fail` to return Cashu proofs to the caller — typical when the
192
- failure isn't operator fault (a network glitch, an upstream provider error). Don't refund on
193
- operator-fault failures (misconfiguration, your own upload failing). See [Refund-on-failure
194
- policy](patterns-auth.md#refund-on-failure-policy).
195
-
196
- ### Cancellation — `ctx.signal`
197
-
198
- ```typescript
199
- ctx.signal: AbortSignal // aborts on caller cancel, idle timeout, stale sweep, supersession
200
- ```
201
-
202
- Cancel is **terminal**: after it, `ctx.complete` / `ctx.fail` / `ctx.working` and the message
203
- emitters are no-ops that log a warning (a cancelled job can never come back as `completed`, and no
204
- revenue is booked on it), and `ctx.prompt` / `ctx.requestPayment` reject instead of hanging. This
205
- holds across machines — a terminal status is sticky in the job store and the runtime re-reads it
206
- before persisting, so a handler on a machine that never saw the cancel still can't overwrite it.
207
-
208
- ### Report serving costs
209
-
210
- Use `ctx.cost({ amount, currency })` immediately after a paid provider call or other builder-side
211
- expense. Amounts are major units, repeated calls add, and only the job total is rounded to
212
- millionths. Every declaration in one job must use the same lowercase ISO currency; a mixed-currency
213
- job reports no total rather than guessing an exchange rate.
214
-
215
- No call means “unknown”; `{ amount: 0, currency: "usd" }` explicitly means free. Cost declarations
216
- inside `ctx.step()` are cached with the result and replay exactly once when the body is skipped, so
217
- declare the cost where it is incurred. Paid completions attach it to revenue, while free completions,
218
- failures, and cancellations use the separate cost-only report and create no revenue row.
219
-
220
- `ctx.fetch` already carries the signal — every request through it aborts on cancel, merged with any
221
- per-request signal you pass. Thread `ctx.signal` into everything else that can be torn down
222
- (provider SDK `signal` options, spawned processes, your own sleeps): that is where provider spend
223
- actually stops.
224
-
225
- ```typescript
226
- async onJob(ctx) {
227
- const audio = await client.tts.generate(text, { signal: ctx.signal });
228
-
229
- for (const chunk of chunks) {
230
- if (ctx.signal.aborted) return; // already terminal — don't call ctx.fail()
231
- await process(chunk);
232
- }
233
-
234
- ctx.complete("Done");
235
- }
236
- ```
237
-
238
- Return early on abort; the runtime has written the terminal status already. `onCancel` still fires if
239
- you declare one — use it for cleanup (release a lock, delete a staged upload); the signal is raised
240
- before the hook runs, so a slow cleanup can't hold the abort back.
241
-
242
- **`onCancel` is cleanup only — it can't emit.** The job is terminal before the hook runs, so a
243
- `ctx.text()` / `ctx.artifact()` / `ctx.complete()` inside it is ignored with a
244
- `ctx.<op>() ignored — job already cancelled` warning. Emit partial results as you go, not on the way
245
- out.
246
-
247
- **Multi-machine DVMs are covered.** On a DVM with `max_machines > 1`, a cancel that lands on a machine
248
- other than the one running the handler is committed to the store there; the machine running the
249
- handler subscribes to its own jobs' notifications, so it picks the cancel up within a notification
250
- round-trip — `ctx.signal` fires, `onCancel` runs on the machine holding the resources, the cancel
251
- lands in the job's message log, and the handler's late writes are rejected. Same behaviour whichever
252
- machine the cancel hits.
253
-
254
- **Isolate tier.** An isolate-hosted handler runs in its own Machine, so it learns of a cancel by
255
- polling its job's durable status on the platform (default every 2 s, `DVMKIT_ISOLATE_CANCEL_POLL_MS`).
256
- The contract above is identical — `ctx.signal` aborts, `ctx.fetch` tears down, emitters no-op, yields
257
- reject — just one poll tick behind the cancel.
258
-
259
- ### Platform services
260
-
261
- ```typescript
262
- ctx.state: State // typed from defaults; auto-persisted on every yield
263
- ctx.store: KVStore // cross-job async KV storage
264
- ctx.fetch: typeof fetch // instrumented fetch — dvm.fetch spans + auto-aborts on cancel
265
- ctx.env: Record<string, string> // environment variables (read at job time, not boot)
266
- ctx.log: Logger // structured logging (.debug, .info, .warn, .error)
267
- ctx.step<T>(id: string, fn: () => Promise<T>): Promise<T> // cached on yield
268
- ```
269
-
270
- Prefer `ctx.fetch` over the global `fetch` for outbound HTTP: calls show up in traces, and they
271
- abort when the caller cancels the job.
272
-
273
- ---
@@ -1,153 +0,0 @@
1
- ## Operating a deployed DVM
2
-
3
- Once a DVM is live, `dvmctl` is the full operational surface — you never need `flyctl`.
4
- Every command emits JSON by default and takes `--human`; `<slug>` is the DVM's deployed name.
5
- Exceptions noted inline (`logs` and `revenue csv` emit raw output, not JSON).
6
-
7
- Every DVM-specific command resolves its bare `<slug>` only inside the active `dvmctl switch` context. Pass `--org <handle>` for a one-shot override without changing that context. The CLI resolves that scoped slug to an immutable DVM ID before it reads, changes, or opens a machine resource, so a matching slug in another org is never selected accidentally.
8
-
9
- ### Fleet introspection
10
-
11
- | Command | Purpose | Key flags |
12
- | ----------------------- | ------------------------------------------------------------ | --------------------------- |
13
- | `dvmctl list` | List your deployed DVMs | |
14
- | `dvmctl info <slug>` | Deployed-DVM details | `--org` |
15
- | `dvmctl status <slug>` | Live Fly runtime state (machines, region, image, health) | `--org` |
16
- | `dvmctl metrics <slug>` | Request metrics | `--hours` (default 24), `--org` |
17
- | `dvmctl events <slug>` | Recent events (deploys, restarts, scale, env, machine state) | `--since` (e.g. `2h`, `7d`), `--org` |
18
-
19
- ### Runtime ops
20
-
21
- | Command | Purpose | Key flags |
22
- | --------------------------- | --------------------------------------------------------------- | ---------------------------------- |
23
- | `dvmctl logs <slug>` | View or stream logs (**raw output**, not JSON) | `--tail`, `--since`, `--lines`, `--org` |
24
- | `dvmctl ssh <slug>` | Interactive shell, or one-shot command, on the Fly machine | `--command`, `--machine`, `--org` |
25
- | `dvmctl restart <slug>` | Restart machines without a redeploy | `--machine` (default: all), `--org` |
26
- | `dvmctl rollback <slug>` | Revert to a previous successful deploy's image | `--to <deploy-id>` (default: prev), `--org` |
27
- | `dvmctl scale <slug>` | Change machine count and/or VM size without a redeploy | `--count` (1–8), `--size`, `--org` |
28
- | `dvmctl destroy <slug>` | Destroy the DVM and its paired Postgres cluster | `--confirm`, `--keep-db`, `--org` |
29
- | `dvmctl deploy-status <id>` | Status of a deploy by ID — recovery handle for a stuck `deploy` | |
30
-
31
- ### Config, data & tracing
32
-
33
- | Command | Purpose | Key flags |
34
- | ----------------------------------- | ----------------------------------------------------------------------------- | ------------------------------ |
35
- | `dvmctl env <slug>` | View or update environment variables | `--set KEY=val`, `--unset KEY`, `--org` |
36
- | `dvmctl secrets list <slug>` | List secret names + last-applied times (never values) | `--org` |
37
- | `dvmctl db connect <slug>` | Interactive `psql` against the DVM's database (needs local `psql`) | `--machine`, `--org` |
38
- | `dvmctl db proxy <slug>` | Bind a local port to the DVM's database (`pg_dump`, drizzle-kit, GUI clients) | `--port`, `--machine`, `--org` |
39
- | `dvmctl trace get <traceId>` | Fetch a trace by ID (isolate-dataset spans) | `--no-color` |
40
-
41
- ### Custom domains
42
-
43
- | Command | Purpose |
44
- | --------------------------------------- | ----------------------------------------------- |
45
- | `dvmctl domains add <slug> <domain>` | Attach a custom domain (e.g. `api.example.com`); `--org` selects its org |
46
- | `dvmctl domains list <slug>` | List attached domains; `--org` selects its org |
47
- | `dvmctl domains verify <slug> <domain>` | Verify DNS/TLS provisioning; `--org` selects its org |
48
- | `dvmctl domains remove <slug> <domain>` | Remove a custom domain; `--org` selects its org |
49
-
50
- ### Revenue & payout
51
-
52
- Reporting, the path to collect your earnings off the Cashu accumulator, and the operator queues for refunds
53
- and settlements no automatic loop will ever clear.
54
-
55
- | Command | Purpose | Key flags |
56
- | -------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------ |
57
- | `dvmctl revenue summary` | Totals + per-rail breakdown | `--from`, `--to` (required), `--currency` |
58
- | `dvmctl revenue events` | Individual revenue events | `--from`, `--to`, `--rail`, `--limit` |
59
- | `dvmctl revenue csv` | Export revenue as CSV (**raw CSV output**) | `--from`, `--to` (required), `--currency`, `--rail`, `--out` |
60
- | `dvmctl set-payout <handle> <ln-addr>` | Persist the default Lightning payout address for `melt-pending` | `--skip-preflight`, `--self-hosted`, `--org` |
61
- | `dvmctl melt-pending <handle>` | Drain pending Cashu accumulator batches to the Lightning payout addr | `--payout`, `--endpoint`, `--watch`, `--interval`, `--org` |
62
- | `dvmctl rotate <handle>` | Rotate the per-DVM Cashu lock pubkey (publishes next BIP-32 child) | `--endpoint`, `--org` |
63
- | `dvmctl credit blocked <handle>` | Lightning top-ups the ledger could never credit — the operator queue | `--include-resolved`, `--limit`, `--after`, `--endpoint`, `--org` |
64
- | `dvmctl credit reconcile <handle> <payment-hash>` | Credit a blocked payment onto a credit the caller holds | `--credit-id` (required), `--endpoint`, `--org` |
65
- | `dvmctl credit write-off <handle> <payment-hash>` | Reviewed, not crediting — moves no money, reversible | `--note`, `--endpoint`, `--org` |
66
- | `dvmctl credit drains-owed <handle>` | One-payment stablecoin refunds you owe (channel-backed ones settle themselves) | `--endpoint`, `--org` |
67
- | `dvmctl credit drain-settle <handle> <credit-id> <drain-id>` | Record a refund you sent on chain — idempotent | `--tx` (required), `--note`, `--endpoint`, `--org` |
68
- | `dvmctl credit x402-wedged <handle>` | x402 refunds that settled on chain but never booked — the repair queue | `--include-resolved`, `--limit`, `--after`, `--min-age-ms`, `--no-evidence`, `--endpoint`, `--org` |
69
- | `dvmctl credit x402-reconcile <handle> <settlement-id>` | Finish one at the chain's own figure — never one you supply | `--endpoint`, `--org` |
70
- | `dvmctl credit x402-write-off <handle> <settlement-id>` | Reviewed, not booking — moves no money, reversible | `--note`, `--endpoint`, `--org` |
71
- | `dvmctl credit tempo-wedged <handle>` | Tempo closes that landed on chain but never booked the drain | `--include-resolved`, `--limit`, `--after`, `--min-age-ms`, `--no-evidence`, `--endpoint`, `--org` |
72
- | `dvmctl credit tempo-reconcile <handle> <credit-id> <drain-id>` | Book one against the close receipt's own refund figure | `--endpoint`, `--org` |
73
- | `dvmctl credit tempo-write-off <handle> <credit-id> <drain-id>` | Reviewed, not booking — moves no money, reversible | `--note`, `--endpoint`, `--org` |
74
-
75
- ### Account, org & auth
76
-
77
- | Command | Purpose | Key flags |
78
- | ------------------------------ | -------------------------------------------------------- | -------------------------------- |
79
- | `dvmctl auth` | Authenticate with the platform | `--status`, `--logout` |
80
- | `dvmctl switch [handle]` | Set the active org context for deploys (omit = show) | `--personal` (clear) |
81
- | `dvmctl transfer <slug> <to>` | Transfer a DVM to an org (by handle) or back to personal | `--create-org`, `--display-name`, `--org` |
82
- | `dvmctl api-keys list` | List builder API keys | |
83
- | `dvmctl api-keys create` | Create a builder API key | `--label` |
84
- | `dvmctl api-keys revoke <id>` | Revoke a builder API key | |
85
- | `dvmctl handle set <handle>` | Claim a builder handle for `*.dvmkit.ai` subdomains | |
86
- | `dvmctl handle check <handle>` | Check handle availability | |
87
- | `dvmctl identity create` | Generate a per-workstation secp256k1 builder identity | `--force` |
88
- | `dvmctl identity show` | Print the builder identity pubkey | |
89
-
90
- ## Caller feedback
91
-
92
- Callers can send signed-envelope feedback about a DVM via `dvm feedback --dvm <slug> "..."` (or
93
- `--job <jobId>` to attach a trace_id). The platform persists every submission per-slug; as the
94
- builder, you read and toggle collection through `dvmctl feedback`.
95
-
96
- ```bash
97
- dvmctl feedback list <slug> # newest-first, JSON
98
- dvmctl feedback list <slug> --since 24h # relative duration ("24h", "7d") or ISO 8601
99
- dvmctl feedback list <slug> --limit 100 --cursor <c> --org acme
100
- ```
101
-
102
- `list` is cursor-paginated, capped at 100 per page (default 50). Walk older pages by passing the
103
- response's `next_cursor` back via `--cursor`. JSON shape:
104
-
105
- ```json
106
- {
107
- "slug": "...",
108
- "feedback": [
109
- { "id": "...", "slug": "...", "created_at": "...", "caller_pubkey": "...",
110
- "body": "...", "trace_id": "..." | null, "job_id": "..." | null }
111
- ],
112
- "next_cursor": "..." | undefined
113
- }
114
- ```
115
-
116
- Toggle collection per-DVM:
117
-
118
- ```bash
119
- dvmctl feedback enable <slug> # flips dvms.feedback_enabled = true
120
- dvmctl feedback disable <slug> # flips dvms.feedback_enabled = false
121
- ```
122
-
123
- When disabled, subsequent caller submissions to that slug fail with `409 feedback_disabled` and a
124
- `display` field the calling agent can relay to its user. `list` keeps returning previously-stored
125
- rows either way — the toggle gates collection, not retrieval.
126
-
127
- All three commands require `dvmctl auth` (org-scoped); `list` only returns feedback for DVMs owned
128
- by the authenticated org.
129
-
130
- ---
131
-
132
- ## Verification
133
-
134
- After writing or modifying DVM code, run the generated project's test command. Add project-owned
135
- `lint` and `typecheck` scripts before relying on them, then run all three from the project directory:
136
-
137
- ```bash
138
- npm run lint
139
- npm run typecheck
140
- npm test
141
- ```
142
-
143
- **If the capability charges and calls a paid third-party API, there is a fourth rung** — the
144
- [live provider contract](testing.md#live-provider-contracts):
145
-
146
- ```bash
147
- <PROVIDER_KEY>=… npm run test:providers:live
148
- ```
149
-
150
- This is a project-owned opt-in script: add it as described in the
151
- [live provider contract](testing.md#live-provider-contracts). It is key-gated and spends real vendor
152
- money, so it skips cleanly without the key. If you don't have one, **say so** and hand the operator
153
- a green static pass — don't report the provider contract as verified when the suite skipped.