@skrr-ai/cli 0.1.26 → 0.1.28

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.
Files changed (45) hide show
  1. package/dist/base-command.js +25 -0
  2. package/dist/commands/machines/dedicated/attach.js +4 -3
  3. package/dist/commands/machines/dedicated/current.d.ts +19 -0
  4. package/dist/commands/machines/dedicated/current.js +49 -0
  5. package/dist/commands/machines/dedicated/destroy.js +7 -6
  6. package/dist/commands/machines/dedicated/exec.d.ts +4 -0
  7. package/dist/commands/machines/dedicated/exec.js +19 -2
  8. package/dist/commands/machines/dedicated/grow.js +3 -2
  9. package/dist/commands/machines/dedicated/health-check.js +4 -3
  10. package/dist/commands/machines/dedicated/incident-close.js +3 -2
  11. package/dist/commands/machines/dedicated/index.js +2 -0
  12. package/dist/commands/machines/dedicated/restart.js +3 -2
  13. package/dist/commands/machines/dedicated/restore.d.ts +26 -0
  14. package/dist/commands/machines/dedicated/restore.js +64 -0
  15. package/dist/commands/machines/dedicated/show.js +3 -2
  16. package/dist/commands/machines/dedicated/snapshot.js +3 -2
  17. package/dist/commands/machines/dedicated/spending-limit.js +4 -3
  18. package/dist/commands/machines/dedicated/start.js +3 -2
  19. package/dist/commands/machines/dedicated/stop.js +3 -2
  20. package/dist/lib/dedicated-exec.d.ts +80 -0
  21. package/dist/lib/dedicated-exec.js +271 -0
  22. package/dist/lib/dedicated-lease-command.d.ts +9 -0
  23. package/dist/lib/dedicated-lease-command.js +24 -1
  24. package/dist/lib/dedicated-machines.d.ts +90 -1
  25. package/dist/lib/dedicated-machines.js +125 -6
  26. package/dist/lib/edge-error-page.d.ts +49 -0
  27. package/dist/lib/edge-error-page.js +120 -0
  28. package/dist/lib/harnesses.d.ts +9 -1
  29. package/dist/lib/harnesses.js +20 -2
  30. package/dist/lib/hosted-machines.js +7 -0
  31. package/dist/lib/sky-code.js +11 -1
  32. package/dist/node_modules/@skrr-ai/data-provider/index.js +12 -0
  33. package/dist/node_modules/@skrr-ai/inference-broker/dist/cjs/index.d.ts +1 -1
  34. package/dist/node_modules/@skrr-ai/inference-broker/dist/cjs/index.js +3 -1
  35. package/dist/node_modules/@skrr-ai/inference-broker/dist/cjs/loopback-server.js +64 -1
  36. package/dist/node_modules/@skrr-ai/inference-broker/dist/cjs/managed-inference-broker.d.ts +30 -0
  37. package/dist/node_modules/@skrr-ai/inference-broker/dist/cjs/managed-inference-broker.js +43 -1
  38. package/dist/node_modules/@skrr-ai/inference-broker/dist/esm/index.d.ts +1 -1
  39. package/dist/node_modules/@skrr-ai/inference-broker/dist/esm/index.js +1 -1
  40. package/dist/node_modules/@skrr-ai/inference-broker/dist/esm/loopback-server.js +64 -1
  41. package/dist/node_modules/@skrr-ai/inference-broker/dist/esm/managed-inference-broker.d.ts +30 -0
  42. package/dist/node_modules/@skrr-ai/inference-broker/dist/esm/managed-inference-broker.js +41 -0
  43. package/dist/node_modules/@skrr-ai/inference-broker/package.json +1 -1
  44. package/oclif.manifest.json +2928 -2734
  45. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEDICATED_FILE_MAX_BYTES = exports.DEDICATED_FILE_CHUNK_BYTES = exports.DEDICATED_EXEC_MAX_TIMEOUT_MS = exports.DEDICATED_MACHINE_PRODUCT = exports.DEDICATED_LEASE_ACTIONS = void 0;
3
+ exports.DEDICATED_FILE_MAX_BYTES = exports.DEDICATED_FILE_CHUNK_BYTES = exports.DEDICATED_EXEC_DEFAULT_TIMEOUT_MS = exports.DEDICATED_EXEC_MAX_TIMEOUT_MS = exports.DEDICATED_MACHINE_PRODUCT = exports.DEDICATED_LEASE_ACTIONS = void 0;
4
4
  exports.dedicatedRuntimeCreateInput = dedicatedRuntimeCreateInput;
5
5
  exports.isDedicatedRepositoryRef = isDedicatedRepositoryRef;
6
6
  exports.createDedicatedRuntime = createDedicatedRuntime;
@@ -8,10 +8,13 @@ exports.listDedicatedRuntimes = listDedicatedRuntimes;
8
8
  exports.getDedicatedRuntime = getDedicatedRuntime;
9
9
  exports.performDedicatedLeaseAction = performDedicatedLeaseAction;
10
10
  exports.growDedicatedRuntime = growDedicatedRuntime;
11
+ exports.restoreDeletedDedicatedRuntime = restoreDeletedDedicatedRuntime;
11
12
  exports.setDedicatedRuntimeSpendingLimit = setDedicatedRuntimeSpendingLimit;
12
13
  exports.closeDedicatedRuntimeIncidentHold = closeDedicatedRuntimeIncidentHold;
13
14
  exports.destroyDedicatedRuntime = destroyDedicatedRuntime;
14
15
  exports.execDedicatedRuntime = execDedicatedRuntime;
16
+ exports.startDedicatedRuntimeExecution = startDedicatedRuntimeExecution;
17
+ exports.observeDedicatedRuntimeExecution = observeDedicatedRuntimeExecution;
15
18
  exports.readDedicatedRuntimeFileChunk = readDedicatedRuntimeFileChunk;
16
19
  exports.writeDedicatedRuntimeFileChunk = writeDedicatedRuntimeFileChunk;
17
20
  exports.parseDedicatedCopyEndpoints = parseDedicatedCopyEndpoints;
@@ -20,14 +23,17 @@ exports.dedicatedLeaseState = dedicatedLeaseState;
20
23
  exports.dedicatedLeaseDisplayName = dedicatedLeaseDisplayName;
21
24
  exports.formatUsdCents = formatUsdCents;
22
25
  exports.dedicatedSpendSummary = dedicatedSpendSummary;
26
+ exports.describeDedicatedDeletion = describeDedicatedDeletion;
23
27
  exports.describeDedicatedLease = describeDedicatedLease;
24
28
  exports.dedicatedLeaseHarnesses = dedicatedLeaseHarnesses;
29
+ exports.dedicatedLeaseIdForDaemon = dedicatedLeaseIdForDaemon;
25
30
  exports.dedicatedTerminalHarness = dedicatedTerminalHarness;
26
31
  exports.selectDedicatedAttachHarness = selectDedicatedAttachHarness;
27
32
  exports.dedicatedCatalogFromProducts = dedicatedCatalogFromProducts;
28
33
  exports.dedicatedRuntimeApiErrorCode = dedicatedRuntimeApiErrorCode;
29
34
  exports.formatDedicatedRuntimeApiError = formatDedicatedRuntimeApiError;
30
35
  const api_fetch_1 = require("./api-fetch");
36
+ const edge_error_page_1 = require("./edge-error-page");
31
37
  exports.DEDICATED_LEASE_ACTIONS = [
32
38
  'start',
33
39
  'stop',
@@ -85,11 +91,20 @@ function isDedicatedRepositoryRef(value) {
85
91
  // unreachable by domain-api-parity while being perfectly reachable, which is the
86
92
  // reading that trains people to allowlist things that are fine. Same shape as
87
93
  // `withQuery` on the `request.*` side.
88
- function withDedicatedScope(path, scope) {
89
- if (!scope.workspaceId)
94
+ //
95
+ // Query parameters therefore go in `query`, never into the literal: the coverage
96
+ // reader matches the literal against the route table, and a `?name=` inside it
97
+ // makes the endpoint read as unreachable.
98
+ function withDedicatedScope(path, scope, query = {}) {
99
+ const params = Object.entries(query);
100
+ if (scope.workspaceId)
101
+ params.push(['workspaceId', scope.workspaceId]);
102
+ if (params.length === 0)
90
103
  return path;
91
104
  const separator = path.includes('?') ? '&' : '?';
92
- return `${path}${separator}workspaceId=${encodeURIComponent(scope.workspaceId)}`;
105
+ return `${path}${separator}${params
106
+ .map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`)
107
+ .join('&')}`;
93
108
  }
94
109
  async function createDedicatedRuntime(input) {
95
110
  const { requestId, workspaceId, ...body } = input;
@@ -117,6 +132,25 @@ async function growDedicatedRuntime(leaseId, storageGb, options = {}) {
117
132
  ...(options.requestId ? { headers: { 'Idempotency-Key': options.requestId } } : {}),
118
133
  });
119
134
  }
135
+ /**
136
+ * Restore a deleted Dedicated Runtime into a NEW machine, from the final recovery
137
+ * point its deletion kept. Only inside the deletion's recovery window, and once
138
+ * per deletion; the response's `lease` is the new machine. The cap defaults to
139
+ * the deleted machine's.
140
+ */
141
+ async function restoreDeletedDedicatedRuntime(leaseId, options = {}) {
142
+ const body = {};
143
+ if (options.displayName)
144
+ body.displayName = options.displayName;
145
+ if (options.monthlySpendingLimitCents !== undefined) {
146
+ body.monthlySpendingLimitCents = options.monthlySpendingLimitCents;
147
+ }
148
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/restore`, options), {
149
+ method: 'POST',
150
+ body,
151
+ ...(options.requestId ? { headers: { 'Idempotency-Key': options.requestId } } : {}),
152
+ });
153
+ }
120
154
  /**
121
155
  * Change the monthly spending cap of one lease.
122
156
  *
@@ -143,8 +177,16 @@ async function destroyDedicatedRuntime(leaseId, options = {}) {
143
177
  }
144
178
  /** The server's ceiling for one exec; documented beside the route. */
145
179
  exports.DEDICATED_EXEC_MAX_TIMEOUT_MS = 10 * 60 * 1000;
180
+ /** The server's default when a command names no timeout. */
181
+ exports.DEDICATED_EXEC_DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
146
182
  /**
147
- * Run one command on the machine as the workload user.
183
+ * Run one command on the machine as the workload user, in ONE request.
184
+ *
185
+ * The synchronous route, kept for servers that predate `executions`. It holds
186
+ * the request open while the command runs, so behind CloudFront a command that
187
+ * runs past ~60 s is cut off at the edge while it keeps running (OSK-8705).
188
+ * `runDedicatedExec` (dedicated-exec.ts) starts and observes instead, and falls
189
+ * back to this only when the server has no `executions` route.
148
190
  *
149
191
  * Not idempotent and not retried: `apiFetch` gives each call a fresh
150
192
  * idempotency key, which is the rule for an unfenced write — running the same
@@ -160,6 +202,27 @@ async function execDedicatedRuntime(leaseId, input, scope = {}) {
160
202
  },
161
203
  });
162
204
  }
205
+ /**
206
+ * Start one command. `requestId` is the idempotency key: the same id, for the
207
+ * same command on the same lease, returns the execution it already started —
208
+ * so a retry after a lost response never runs the command twice.
209
+ */
210
+ async function startDedicatedRuntimeExecution(leaseId, input, options) {
211
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/executions`, options), {
212
+ method: 'POST',
213
+ body: {
214
+ command: input.command,
215
+ ...(input.cwd ? { cwd: input.cwd } : {}),
216
+ ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
217
+ },
218
+ headers: { 'X-Idempotency-Key': options.requestId },
219
+ ...(options.signal ? { signal: options.signal } : {}),
220
+ });
221
+ }
222
+ /** Read a started command, letting the server hold the answer up to `waitMs` for it to end. */
223
+ async function observeDedicatedRuntimeExecution(leaseId, executionId, options) {
224
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/executions/${encodeURIComponent(executionId)}`, options, { waitMs: String(Math.max(0, Math.floor(options.waitMs))) }), options.signal ? { signal: options.signal } : {});
225
+ }
163
226
  /** Mirrors the daemon's bounds (daemon/src/tools/file-transfer.ts). */
164
227
  exports.DEDICATED_FILE_CHUNK_BYTES = 512 * 1024;
165
228
  exports.DEDICATED_FILE_MAX_BYTES = 64 * 1024 * 1024;
@@ -234,12 +297,30 @@ function dedicatedSpendSummary(lease) {
234
297
  * lease that is still `requested` has no resources, daemon or spend yet, and a
235
298
  * missing value is omitted rather than printed as a column of dashes.
236
299
  */
300
+ /** One line saying what a deletion still allows. */
301
+ function describeDedicatedDeletion(deletion, leaseId) {
302
+ if (deletion.restoredToLeaseId) {
303
+ return `restored into ${deletion.restoredToLeaseId}`;
304
+ }
305
+ if (deletion.status === 'completed') {
306
+ return `permanently deleted${deletion.completedAt ? ` ${deletion.completedAt}` : ''}; no copy remains`;
307
+ }
308
+ if (deletion.status === 'purging') {
309
+ return 'recovery window closed; every copy is being deleted';
310
+ }
311
+ if (deletion.restorable) {
312
+ return `restorable until ${deletion.recoverableUntil} — \`skrr machines dedicated restore ${leaseId || '<lease-id>'}\``;
313
+ }
314
+ return 'nothing to restore; every copy is deleted when the window closes';
315
+ }
237
316
  function describeDedicatedLease(lease, harnesses = []) {
238
317
  const lines = [dedicatedLeaseDisplayName(lease)];
239
318
  const row = (label, value) => {
240
319
  if (value === undefined || value === null || value === '')
241
320
  return;
242
- lines.push(` ${label.padEnd(13)}${value}`);
321
+ // At least one space: `restored from` is as wide as the column and printed
322
+ // as `restored fromrtl_…`.
323
+ lines.push(` ${`${label} `.padEnd(13)}${value}`);
243
324
  };
244
325
  row('id', dedicatedLeaseId(lease));
245
326
  row('state', lease.stateReason
@@ -291,6 +372,11 @@ function describeDedicatedLease(lease, harnesses = []) {
291
372
  .filter(Boolean)
292
373
  .join(' · '));
293
374
  }
375
+ const deletion = storage.deletion;
376
+ if (deletion?.status) {
377
+ row('deleted', describeDedicatedDeletion(deletion, dedicatedLeaseId(lease)));
378
+ }
379
+ row('restored from', lease.recovery?.restoredFromLeaseId);
294
380
  const hold = lease.recovery?.incidentHold;
295
381
  if (hold?.status === 'open') {
296
382
  row('incident', `hold open since ${hold.heldAt || '?'} — the failed source volume is billed until \`incident-close\``);
@@ -328,6 +414,29 @@ function dedicatedLeaseHarnesses(lease, harnesses) {
328
414
  })
329
415
  .sort((a, b) => seenAt(b) - seenAt(a));
330
416
  }
417
+ /**
418
+ * The lease of the machine this process is running on, from the harness rows.
419
+ *
420
+ * Nothing in a workload process's environment names its lease — the daemon's
421
+ * environment (lease, generation, credentials) sits on the other side of the uid
422
+ * split by design. The CLI hand-off descriptor the daemon publishes to the
423
+ * workload group does carry the daemon's id, and every harness row that daemon
424
+ * registered carries `metadata.machineLeaseId`. Joining the two answers "which
425
+ * machine am I" with no new daemon surface.
426
+ */
427
+ function dedicatedLeaseIdForDaemon(daemonId, harnesses) {
428
+ if (!daemonId)
429
+ return null;
430
+ const leaseIds = new Set(harnesses
431
+ .filter((harness) => harness.daemonId === daemonId &&
432
+ harness.metadata?.machineProduct === exports.DEDICATED_MACHINE_PRODUCT &&
433
+ typeof harness.metadata?.machineLeaseId === 'string')
434
+ .map((harness) => String(harness.metadata?.machineLeaseId)));
435
+ // One daemon serves one lease. Two would mean rows from a replaced
436
+ // generation disagree, and guessing between them is how `self` would act on
437
+ // the wrong machine.
438
+ return leaseIds.size === 1 ? [...leaseIds][0] : null;
439
+ }
331
440
  /**
332
441
  * The row an interactive terminal opens on: online, local, with a daemon.
333
442
  *
@@ -485,6 +594,10 @@ function dedicatedRuntimeApiErrorCode(err) {
485
594
  if (!(err instanceof api_fetch_1.ApiFetchError)) {
486
595
  return undefined;
487
596
  }
597
+ const edge = (0, edge_error_page_1.describeEdgeErrorPage)({ status: err.status, body: err.body, method: err.method });
598
+ if (edge) {
599
+ return edge.code;
600
+ }
488
601
  try {
489
602
  const parsed = JSON.parse(err.body || '');
490
603
  if (typeof parsed.code === 'string' && parsed.code) {
@@ -503,6 +616,12 @@ function formatDedicatedRuntimeApiError(err) {
503
616
  if (err.status === 401) {
504
617
  return null;
505
618
  }
619
+ // An HTML page is the edge answering in the API's place — a WAF block or a
620
+ // gateway timeout. Printing its markup told nobody anything (OSK-8700).
621
+ const edge = (0, edge_error_page_1.describeEdgeErrorPage)({ status: err.status, body: err.body, method: err.method });
622
+ if (edge) {
623
+ return edge.message;
624
+ }
506
625
  const notFound = describeLeaseNotFound(err);
507
626
  if (notFound) {
508
627
  return notFound;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * edge-error-page.ts — say what an HTML error page from the EDGE means, instead
3
+ * of printing it.
4
+ *
5
+ * Every API route answers in JSON. An HTML page in an error response therefore
6
+ * did not come from the API: it is CloudFront, its web application firewall, or
7
+ * a load balancer answering in the API's place. Printed as-is it is a screen of
8
+ * markup whose one useful line ("Request blocked", a request id) is buried, and
9
+ * treated as an API answer it is misleading in a specific way — a 403 reads as
10
+ * "you lack permission", and CloudFront's 504 page tells the reader to "try again
11
+ * later", which for a write that is still running repeats it.
12
+ *
13
+ * Both happened to `skrr machines dedicated exec` (OSK-8700, OSK-8705): a shell
14
+ * command containing `http://10.0.0.10/` came back as CloudFront's raw
15
+ * "403 ERROR / Request blocked" page, and a command running past 60 seconds as
16
+ * its raw 504 page while it kept running on the machine.
17
+ *
18
+ * General on purpose: nothing here knows which command asked.
19
+ */
20
+ export type EdgeErrorCode = 'EDGE_REQUEST_BLOCKED' | 'EDGE_GATEWAY_TIMEOUT' | 'EDGE_ORIGIN_UNAVAILABLE' | 'EDGE_ERROR_PAGE';
21
+ export interface EdgeErrorPage {
22
+ status: number;
23
+ code: EdgeErrorCode;
24
+ /** The edge's own id for the request (CloudFront prints one), when the page carries it. */
25
+ requestId?: string;
26
+ message: string;
27
+ /** Whether sending the identical request again can help. */
28
+ retryable: boolean;
29
+ }
30
+ /**
31
+ * True when an error response body is an HTML page rather than an API answer.
32
+ *
33
+ * A JSON body is never a page, whatever its text says: an API refusal that
34
+ * quotes "The request could not be satisfied" or an HTML fragment in its message
35
+ * must keep its own words (the base command's handler already gates on "no JSON
36
+ * body"; this makes every caller agree). The CloudFront wording counts only
37
+ * inside markup, never on its own.
38
+ */
39
+ export declare function isHtmlErrorPage(body: string | null | undefined): boolean;
40
+ /**
41
+ * Describe an HTML error page, or return null when the body is not one (a JSON
42
+ * refusal from the API must keep its own words).
43
+ */
44
+ export declare function describeEdgeErrorPage(input: {
45
+ status: number | undefined;
46
+ body: string | null | undefined;
47
+ method?: string | null;
48
+ host?: string | null;
49
+ }): EdgeErrorPage | null;
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ /**
3
+ * edge-error-page.ts — say what an HTML error page from the EDGE means, instead
4
+ * of printing it.
5
+ *
6
+ * Every API route answers in JSON. An HTML page in an error response therefore
7
+ * did not come from the API: it is CloudFront, its web application firewall, or
8
+ * a load balancer answering in the API's place. Printed as-is it is a screen of
9
+ * markup whose one useful line ("Request blocked", a request id) is buried, and
10
+ * treated as an API answer it is misleading in a specific way — a 403 reads as
11
+ * "you lack permission", and CloudFront's 504 page tells the reader to "try again
12
+ * later", which for a write that is still running repeats it.
13
+ *
14
+ * Both happened to `skrr machines dedicated exec` (OSK-8700, OSK-8705): a shell
15
+ * command containing `http://10.0.0.10/` came back as CloudFront's raw
16
+ * "403 ERROR / Request blocked" page, and a command running past 60 seconds as
17
+ * its raw 504 page while it kept running on the machine.
18
+ *
19
+ * General on purpose: nothing here knows which command asked.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.isHtmlErrorPage = isHtmlErrorPage;
23
+ exports.describeEdgeErrorPage = describeEdgeErrorPage;
24
+ const HTML_START = /^\s*(<!doctype html\b|<html\b)/i;
25
+ const EDGE_MARK = /Generated by cloudfront|The request could not be satisfied/i;
26
+ const REQUEST_ID = /Request ID:\s*([A-Za-z0-9_=-]{8,128})/i;
27
+ const MARKUP = /<(html|head|body|title|h1|pre)\b/i;
28
+ function isJson(body) {
29
+ const trimmed = body.trim();
30
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('['))
31
+ return false;
32
+ try {
33
+ JSON.parse(trimmed);
34
+ return true;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ /**
41
+ * True when an error response body is an HTML page rather than an API answer.
42
+ *
43
+ * A JSON body is never a page, whatever its text says: an API refusal that
44
+ * quotes "The request could not be satisfied" or an HTML fragment in its message
45
+ * must keep its own words (the base command's handler already gates on "no JSON
46
+ * body"; this makes every caller agree). The CloudFront wording counts only
47
+ * inside markup, never on its own.
48
+ */
49
+ function isHtmlErrorPage(body) {
50
+ if (typeof body !== 'string' || !body || isJson(body))
51
+ return false;
52
+ return HTML_START.test(body) || (MARKUP.test(body) && EDGE_MARK.test(body));
53
+ }
54
+ function readOnly(method) {
55
+ const upper = (method || 'GET').toUpperCase();
56
+ return upper === 'GET' || upper === 'HEAD' || upper === 'OPTIONS';
57
+ }
58
+ function where(host) {
59
+ return host ? ` (${host})` : '';
60
+ }
61
+ /**
62
+ * Describe an HTML error page, or return null when the body is not one (a JSON
63
+ * refusal from the API must keep its own words).
64
+ */
65
+ function describeEdgeErrorPage(input) {
66
+ const { body, method, host } = input;
67
+ const status = Number(input.status) || 0;
68
+ if (!isHtmlErrorPage(body))
69
+ return null;
70
+ const requestId = REQUEST_ID.exec(String(body))?.[1];
71
+ const quote = requestId ? ` Edge request id: ${requestId} — quote it when reporting this.` : '';
72
+ const writeOutcome = readOnly(method)
73
+ ? 'This request changed nothing; retry shortly.'
74
+ : 'For a write, the request may still have been carried out, or may still be running — check before sending it again.';
75
+ if (status === 403) {
76
+ return {
77
+ status,
78
+ code: 'EDGE_REQUEST_BLOCKED',
79
+ ...(requestId ? { requestId } : {}),
80
+ message: `The request was blocked at the edge before it reached the API${where(host)}: a web ` +
81
+ 'application firewall rule matched something in it. This is not a problem with your ' +
82
+ 'account or permissions. Content that resembles an attack can be enough — a URL with an ' +
83
+ 'IP address, a relative path (../), markup or SQL, even inside a command or a document — ' +
84
+ 'and sending the same request again will be blocked again.' +
85
+ quote,
86
+ retryable: false,
87
+ };
88
+ }
89
+ if (status === 504) {
90
+ return {
91
+ status,
92
+ code: 'EDGE_GATEWAY_TIMEOUT',
93
+ ...(requestId ? { requestId } : {}),
94
+ message: `The edge stopped waiting for the API${where(host)} and answered HTTP 504. ` +
95
+ writeOutcome +
96
+ quote,
97
+ retryable: readOnly(method),
98
+ };
99
+ }
100
+ if (status === 502 || status === 503) {
101
+ return {
102
+ status,
103
+ code: 'EDGE_ORIGIN_UNAVAILABLE',
104
+ ...(requestId ? { requestId } : {}),
105
+ message: `The edge could not get an answer from the API${where(host)} (HTTP ${status}). ` +
106
+ writeOutcome +
107
+ quote,
108
+ retryable: readOnly(method),
109
+ };
110
+ }
111
+ return {
112
+ status,
113
+ code: 'EDGE_ERROR_PAGE',
114
+ ...(requestId ? { requestId } : {}),
115
+ message: `The request did not get an API response: something in front of the API${where(host)} ` +
116
+ `answered HTTP ${status || 'error'} with an HTML page.` +
117
+ quote,
118
+ retryable: false,
119
+ };
120
+ }
@@ -111,7 +111,15 @@ export interface HarnessLease {
111
111
  createdAt?: string;
112
112
  updatedAt?: string;
113
113
  }
114
- /** `pause | resume | destroy | terminate | health-check` (MachineLeaseService). */
114
+ /**
115
+ * Every action `POST /api/machines/leases/:id/actions/:action` accepts —
116
+ * `MACHINE_LEASE_ACTIONS` in MachineLeaseService. Which of them a given lease
117
+ * supports is its product's `lifecycleCapabilities`, decided by the server.
118
+ *
119
+ * `start`, `stop`, `restart` and `snapshot` were missing, so this command
120
+ * refused every Dedicated Runtime lifecycle verb at argument parsing — before
121
+ * the server that accepts them was asked — while claiming to mirror its set.
122
+ */
115
123
  export declare const HARNESS_LEASE_ACTIONS: string[];
116
124
  /** `user` (the default) or `workspace`. */
117
125
  export type HarnessDefaultScope = 'user' | 'workspace';
@@ -23,8 +23,26 @@ exports.releaseHarnessLease = releaseHarnessLease;
23
23
  exports.performHarnessLeaseAction = performHarnessLeaseAction;
24
24
  exports.setHarnessLeaseBilling = setHarnessLeaseBilling;
25
25
  const api_fetch_1 = require("./api-fetch");
26
- /** `pause | resume | destroy | terminate | health-check` (MachineLeaseService). */
27
- exports.HARNESS_LEASE_ACTIONS = ['pause', 'resume', 'destroy', 'terminate', 'health-check'];
26
+ /**
27
+ * Every action `POST /api/machines/leases/:id/actions/:action` accepts —
28
+ * `MACHINE_LEASE_ACTIONS` in MachineLeaseService. Which of them a given lease
29
+ * supports is its product's `lifecycleCapabilities`, decided by the server.
30
+ *
31
+ * `start`, `stop`, `restart` and `snapshot` were missing, so this command
32
+ * refused every Dedicated Runtime lifecycle verb at argument parsing — before
33
+ * the server that accepts them was asked — while claiming to mirror its set.
34
+ */
35
+ exports.HARNESS_LEASE_ACTIONS = [
36
+ 'pause',
37
+ 'resume',
38
+ 'start',
39
+ 'stop',
40
+ 'restart',
41
+ 'snapshot',
42
+ 'destroy',
43
+ 'terminate',
44
+ 'health-check',
45
+ ];
28
46
  // ── Fleet reads ────────────────────────────────────────────────────────────
29
47
  async function listHarnesses(filters = {}) {
30
48
  const params = new URLSearchParams();
@@ -25,6 +25,7 @@ exports.execHostedMachine = execHostedMachine;
25
25
  exports.waitForHostedMachineReady = waitForHostedMachineReady;
26
26
  exports.offlineForLabel = offlineForLabel;
27
27
  const api_fetch_1 = require("./api-fetch");
28
+ const edge_error_page_1 = require("./edge-error-page");
28
29
  function curateHostedMachineList(response) {
29
30
  return {
30
31
  object: 'list',
@@ -148,6 +149,12 @@ function formatHostedMachineApiError(err) {
148
149
  if (err.status === 401) {
149
150
  return null;
150
151
  }
152
+ // An HTML page is the edge answering in the API's place; describe it rather
153
+ // than printing its markup (OSK-8700).
154
+ const edge = (0, edge_error_page_1.describeEdgeErrorPage)({ status: err.status, body: err.body, method: err.method });
155
+ if (edge) {
156
+ return edge.message;
157
+ }
151
158
  try {
152
159
  const parsed = JSON.parse(err.body);
153
160
  return parsed.message || parsed.error || parsed.code || null;
@@ -36,6 +36,7 @@ const node_fs_1 = require("node:fs");
36
36
  const node_os_1 = require("node:os");
37
37
  const node_path_1 = __importDefault(require("node:path"));
38
38
  const auth_core_1 = require("@skrr-ai/auth-core");
39
+ const inference_broker_1 = require("@skrr-ai/inference-broker");
39
40
  const config_1 = require("./config");
40
41
  // Deliberately does NOT import the managed marker any more. This module used to
41
42
  // read it to decide whether a run was managed; that decision now arrives as a
@@ -468,11 +469,20 @@ function engineSpawnEnv(env, extraEnv,
468
469
  // Required, not defaulted: the safe value differs per call and a parameter that
469
470
  // can be forgotten on a security boundary will be.
470
471
  mode) {
471
- return {
472
+ const spawnEnv = {
472
473
  ...(0, auth_core_1.sanitizeSpawnEnv)(env),
473
474
  ...(mode === 'managed' ? {} : inheritedProviderCredentials(env)),
474
475
  ...extraEnv,
475
476
  };
477
+ if (mode !== 'managed')
478
+ return spawnEnv;
479
+ // OSK-8758 — a managed engine reaches the broker at a synthetic host, and must
480
+ // never address that host to a proxy. Behind an egress proxy (a Dedicated
481
+ // Runtime guest) Bun wrote every broker request in proxy form, and a runtime
482
+ // that honoured the proxy over the socket would send the prompt to it. Layered
483
+ // over the env built above, so it extends whatever list the engine would
484
+ // otherwise have received rather than replacing it.
485
+ return { ...spawnEnv, ...(0, inference_broker_1.inferenceBrokerNoProxyEnv)(spawnEnv) };
476
486
  }
477
487
  async function execEngine(args, env = process.env, options = {}) {
478
488
  // Resolve the binary FIRST, before `prepare` runs.
@@ -41331,6 +41331,18 @@ var nI = { get: function(e2, t2) {
41331
41331
  }
41332
41332
  });
41333
41333
  });
41334
+ }, patchWithOptions: function(e2, t2, o2) {
41335
+ return v(this, void 0, void 0, function() {
41336
+ var r2;
41337
+ return h(this, function(a2) {
41338
+ switch (a2.label) {
41339
+ case 0:
41340
+ return r2 = zk("PATCH", e2, t2, null == o2 ? void 0 : o2.headers), Mk ? [2, Mk.patch(e2, t2, Ok(f(f({}, o2), { headers: r2 })))] : [4, n.patch(e2, JSON.stringify(t2), f(f({}, o2), { headers: f({ "Content-Type": "application/json" }, r2) }))];
41341
+ case 1:
41342
+ return [2, a2.sent().data];
41343
+ }
41344
+ });
41345
+ });
41334
41346
  }, refreshToken: Zk, dispatchTokenUpdatedEvent: eI, broadcastLogout: $k, classifyRefreshFailure: Xk, noteRefreshSuccess: function() {
41335
41347
  Yk();
41336
41348
  } };
@@ -51,7 +51,7 @@
51
51
  * ESM and CJS are both emitted because the daemon is ESM and the CLI is CJS, and
52
52
  * this package exists precisely so both can host the broker.
53
53
  */
54
- export { INFERENCE_BROKER_ORIGIN, INFERENCE_BROKER_SOCKET_ENV, MANAGED_SESSION_ENV, MANAGED_PROMPT_CACHING_ENV, MANAGED_STATUS_PATH, type ManagedSessionStatus, buildManagedInferenceRelayCredential, startManagedInferenceBroker, isManagedInferenceModelScope, type GatewayCredential, type ManagedCapabilityRefresh, type ManagedCapabilityRefreshResult, type ManagedInferenceBrokerOptions, type ManagedInferenceBrokerSession, type ManagedInferenceInputTokenAccounting, type ManagedInferenceModelScope, type ManagedInferenceRequestEnvelope, type ManagedReasoningPolicy, } from './managed-inference-broker.js';
54
+ export { INFERENCE_BROKER_ORIGIN, INFERENCE_BROKER_SOCKET_ENV, MANAGED_SESSION_ENV, MANAGED_PROMPT_CACHING_ENV, MANAGED_STATUS_PATH, INFERENCE_BROKER_HOST, inferenceBrokerNoProxyEnv, type ManagedSessionStatus, buildManagedInferenceRelayCredential, startManagedInferenceBroker, isManagedInferenceModelScope, type GatewayCredential, type ManagedCapabilityRefresh, type ManagedCapabilityRefreshResult, type ManagedInferenceBrokerOptions, type ManagedInferenceBrokerSession, type ManagedInferenceInputTokenAccounting, type ManagedInferenceModelScope, type ManagedInferenceRequestEnvelope, type ManagedReasoningPolicy, } from './managed-inference-broker.js';
55
55
  export { minimumCapabilityWindowMs, scheduleRenewalDelay, startRenewalLoop, type CapabilityRefreshOutcome, type RenewalLoop, type RenewalLoopOptions, type RenewalOutcome, type RenewalStopReason, } from './managed-inference-renewal.js';
56
56
  export { ModelProxyLoopbackServer, type ModelProxyForwarder } from './loopback-server.js';
57
57
  export * from './contract.js';
@@ -67,13 +67,15 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
67
67
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
68
68
  };
69
69
  Object.defineProperty(exports, "__esModule", { value: true });
70
- exports.ModelProxyLoopbackServer = exports.startRenewalLoop = exports.scheduleRenewalDelay = exports.minimumCapabilityWindowMs = exports.isManagedInferenceModelScope = exports.startManagedInferenceBroker = exports.buildManagedInferenceRelayCredential = exports.MANAGED_STATUS_PATH = exports.MANAGED_PROMPT_CACHING_ENV = exports.MANAGED_SESSION_ENV = exports.INFERENCE_BROKER_SOCKET_ENV = exports.INFERENCE_BROKER_ORIGIN = void 0;
70
+ exports.ModelProxyLoopbackServer = exports.startRenewalLoop = exports.scheduleRenewalDelay = exports.minimumCapabilityWindowMs = exports.isManagedInferenceModelScope = exports.startManagedInferenceBroker = exports.buildManagedInferenceRelayCredential = exports.inferenceBrokerNoProxyEnv = exports.INFERENCE_BROKER_HOST = exports.MANAGED_STATUS_PATH = exports.MANAGED_PROMPT_CACHING_ENV = exports.MANAGED_SESSION_ENV = exports.INFERENCE_BROKER_SOCKET_ENV = exports.INFERENCE_BROKER_ORIGIN = void 0;
71
71
  var managed_inference_broker_js_1 = require("./managed-inference-broker.js");
72
72
  Object.defineProperty(exports, "INFERENCE_BROKER_ORIGIN", { enumerable: true, get: function () { return managed_inference_broker_js_1.INFERENCE_BROKER_ORIGIN; } });
73
73
  Object.defineProperty(exports, "INFERENCE_BROKER_SOCKET_ENV", { enumerable: true, get: function () { return managed_inference_broker_js_1.INFERENCE_BROKER_SOCKET_ENV; } });
74
74
  Object.defineProperty(exports, "MANAGED_SESSION_ENV", { enumerable: true, get: function () { return managed_inference_broker_js_1.MANAGED_SESSION_ENV; } });
75
75
  Object.defineProperty(exports, "MANAGED_PROMPT_CACHING_ENV", { enumerable: true, get: function () { return managed_inference_broker_js_1.MANAGED_PROMPT_CACHING_ENV; } });
76
76
  Object.defineProperty(exports, "MANAGED_STATUS_PATH", { enumerable: true, get: function () { return managed_inference_broker_js_1.MANAGED_STATUS_PATH; } });
77
+ Object.defineProperty(exports, "INFERENCE_BROKER_HOST", { enumerable: true, get: function () { return managed_inference_broker_js_1.INFERENCE_BROKER_HOST; } });
78
+ Object.defineProperty(exports, "inferenceBrokerNoProxyEnv", { enumerable: true, get: function () { return managed_inference_broker_js_1.inferenceBrokerNoProxyEnv; } });
77
79
  Object.defineProperty(exports, "buildManagedInferenceRelayCredential", { enumerable: true, get: function () { return managed_inference_broker_js_1.buildManagedInferenceRelayCredential; } });
78
80
  Object.defineProperty(exports, "startManagedInferenceBroker", { enumerable: true, get: function () { return managed_inference_broker_js_1.startManagedInferenceBroker; } });
79
81
  Object.defineProperty(exports, "isManagedInferenceModelScope", { enumerable: true, get: function () { return managed_inference_broker_js_1.isManagedInferenceModelScope; } });
@@ -131,6 +131,14 @@ class ModelProxyLoopbackServer {
131
131
  writeError(res, 401, contract_js_1.MODEL_PROXY_ERROR.UNAUTHORIZED, 'Invalid relay token.');
132
132
  return;
133
133
  }
134
+ // Judged after authentication, so an unauthenticated caller learns nothing
135
+ // about which request forms this relay accepts; and before the body is read,
136
+ // so a refused request is never buffered.
137
+ const path = originFormPath(req.url ?? '/', new URL(this.baseUrl()).origin);
138
+ if (path === null) {
139
+ req.resume();
140
+ throw new contract_js_1.ModelProxyError(contract_js_1.MODEL_PROXY_ERROR.REQUEST_REJECTED, 'model proxy loopback accepts only requests addressed to its own origin');
141
+ }
134
142
  const body = await readBody(req, this.opts.maxBodyBytes);
135
143
  const controller = new AbortController();
136
144
  this.activeRequests.add(controller);
@@ -143,7 +151,7 @@ class ModelProxyLoopbackServer {
143
151
  });
144
152
  const headers = normalizeIncomingHeaders(req.headers);
145
153
  try {
146
- const upstream = await this.opts.forwarder.forward({ method: req.method ?? 'POST', path: req.url ?? '/', headers, body }, { signal: controller.signal });
154
+ const upstream = await this.opts.forwarder.forward({ method: req.method ?? 'POST', path, headers, body }, { signal: controller.signal });
147
155
  res.writeHead(upstream.status, upstream.headers);
148
156
  for await (const chunk of upstream.body) {
149
157
  if (!res.write(chunk)) {
@@ -179,6 +187,61 @@ function constantTimeEqual(a, b) {
179
187
  return false;
180
188
  return (0, node_crypto_1.timingSafeEqual)(bufA, bufB);
181
189
  }
190
+ /**
191
+ * The request-target as the ORIGIN-FORM path the forwarder contract carries, or
192
+ * null when the request was addressed somewhere else.
193
+ *
194
+ * HTTP/1.1 gives a POST two request-target forms. Origin-form (`/v1/messages`) is
195
+ * what a client sends an origin server; absolute-form
196
+ * (`http://host/v1/messages`) is what it sends a forward PROXY — and RFC 9112
197
+ * §3.2.2 still requires a server to accept it. Node passes either through as
198
+ * `req.url`, verbatim.
199
+ *
200
+ * OSK-8758 is why this matters here. A Dedicated Runtime guest exports
201
+ * HTTP(S)_PROXY for its egress proxy, and the broker's synthetic
202
+ * `oversky-inference-broker.invalid` is in no NO_PROXY. Bun keeps dialing the Unix
203
+ * socket named by `fetch({ unix })` but writes the request line in proxy form, so
204
+ * every managed turn reached the broker with an absolute URL for a "path", failed
205
+ * its exact route allowlist, and was refused as an unapproved upstream route —
206
+ * while a laptop, with no proxy configured, sent origin-form and worked.
207
+ *
208
+ * Accepted only when the target denotes EXACTLY the request origin-form would
209
+ * have, so this is a parsing fix and never a widening:
210
+ *
211
+ * - the origin must be this relay's own advertised origin. This loopback is not a
212
+ * forward proxy: a foreign origin is refused here, even one a downstream egress
213
+ * allowlist would accept — naming the upstream is not a way to reach it;
214
+ * - no userinfo, which a request-target may not carry;
215
+ * - the path and query must survive URL parsing byte-for-byte. The WHATWG parser
216
+ * resolves dot segments and rewrites `\` to `/`, and origin-form bytes reach the
217
+ * forwarder unnormalized, so a spelling the forwarder would refuse in one form
218
+ * cannot become an approved route in the other.
219
+ *
220
+ * Anything that is not absolute-form (origin-form and the odd `*`) is returned
221
+ * untouched, exactly as before, for the forwarder to judge.
222
+ */
223
+ function originFormPath(rawTarget, ownOrigin) {
224
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(rawTarget))
225
+ return rawTarget;
226
+ let target;
227
+ try {
228
+ target = new URL(rawTarget);
229
+ }
230
+ catch {
231
+ return null;
232
+ }
233
+ // `origin` is the string "null" for every non-special scheme, so two unrelated
234
+ // opaque origins would otherwise compare equal.
235
+ if (target.origin === 'null' || target.origin !== ownOrigin)
236
+ return null;
237
+ if (target.username || target.password)
238
+ return null;
239
+ const authorityEnd = rawTarget.indexOf('/', rawTarget.indexOf('://') + 3);
240
+ if (authorityEnd < 0)
241
+ return null;
242
+ const path = rawTarget.slice(authorityEnd);
243
+ return path === `${target.pathname}${target.search}` ? path : null;
244
+ }
182
245
  function normalizeIncomingHeaders(headers) {
183
246
  const out = {};
184
247
  for (const [key, value] of Object.entries(headers)) {