@north-light/api 0.3.558 → 0.3.560
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/client.js +34 -0
- package/dist/api/contract.d.ts +1 -1
- package/dist/api/contract.js +1 -1
- package/dist/api/dto/exec.d.ts +10 -0
- package/dist/api/dto/exec.js +1 -0
- package/dist/api/error-codes.d.ts +15 -0
- package/dist/api/error-codes.js +6 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/core/io.js +7 -0
- package/package.json +1 -1
package/dist/api/client.js
CHANGED
|
@@ -908,6 +908,8 @@ export class CrtrClient {
|
|
|
908
908
|
const opts = this.resolveBudget(callOpts);
|
|
909
909
|
const idempotent = method === 'GET' || method === 'HEAD' || method === 'DELETE';
|
|
910
910
|
const maxRetries = opts.maxRetries ?? this.maxRetries;
|
|
911
|
+
// A hosted runtime being created or replaced answers reads with a refusal that never reached it; wait for it inside this allowance.
|
|
912
|
+
const replacementWaitEndsAt = Math.min(opts.deadlineAt ?? Infinity, Date.now() + HOSTED_REPLACEMENT_WAIT_MS);
|
|
911
913
|
for (let attempt = 0;; attempt++) {
|
|
912
914
|
let response;
|
|
913
915
|
try {
|
|
@@ -942,6 +944,15 @@ export class CrtrClient {
|
|
|
942
944
|
throw toTransportApiError(err, opts.signal);
|
|
943
945
|
}
|
|
944
946
|
}
|
|
947
|
+
if (isRead(method) && maxRetries > 0 && response.status === 503 && await isRefusedBeforeForwarding(response, opts.deadlineAt)) {
|
|
948
|
+
const delayMs = Math.max(250, (retryAfterHeaderS(response) ?? 0) * 1_000) + Math.random() * 250;
|
|
949
|
+
// Out of allowance: the refusal itself is the answer, so the caller sees its cause.
|
|
950
|
+
if (Date.now() + delayMs >= replacementWaitEndsAt)
|
|
951
|
+
return response;
|
|
952
|
+
await drainResponseBody(response, opts.deadlineAt);
|
|
953
|
+
await sleepWithin(delayMs, { signal: opts.signal });
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
945
956
|
if (idempotent && attempt < maxRetries && isRetryableStatus(response.status)) {
|
|
946
957
|
await drainResponseBody(response, opts.deadlineAt);
|
|
947
958
|
await sleepWithin(retryDelayMs(attempt + 1), opts, retryAfterHeaderS(response));
|
|
@@ -1266,6 +1277,29 @@ async function parse(res, deadlineAt) {
|
|
|
1266
1277
|
}
|
|
1267
1278
|
throw new ApiError(res.status, 'internal', `request failed with status ${res.status}`, undefined, res.headers);
|
|
1268
1279
|
}
|
|
1280
|
+
/** How long a read waits for a hosted runtime that is being created or replaced. */
|
|
1281
|
+
const HOSTED_REPLACEMENT_WAIT_MS = 60_000;
|
|
1282
|
+
/** Only reads are replayed after a refusal; a write is never retried on this state. */
|
|
1283
|
+
function isRead(method) {
|
|
1284
|
+
return method === 'GET' || method === 'HEAD';
|
|
1285
|
+
}
|
|
1286
|
+
/** True when the response is the hosting router's 503 for a runtime that provisioning is creating or replacing
|
|
1287
|
+
* (`details.forwarded === false`): the request reached no runtime, so replaying it cannot run anything twice.
|
|
1288
|
+
* Reads a clone, so the caller still gets the body of the response it is handed. */
|
|
1289
|
+
async function isRefusedBeforeForwarding(response, deadlineAt) {
|
|
1290
|
+
try {
|
|
1291
|
+
// A router refusal is a few hundred bytes; anything declared larger, or a body that stalls, is not one.
|
|
1292
|
+
if (Number(response.headers.get('content-length') ?? 0) > 8_192)
|
|
1293
|
+
return false;
|
|
1294
|
+
const text = await readTextWithin(response.clone(), Math.min(deadlineAt ?? Infinity, Date.now() + 5_000));
|
|
1295
|
+
const error = JSON.parse(text).error;
|
|
1296
|
+
return error?.origin === 'router' && error.details?.forwarded === false
|
|
1297
|
+
&& (error.details.runtime_state === 'creating' || error.details.runtime_state === 'recovering');
|
|
1298
|
+
}
|
|
1299
|
+
catch {
|
|
1300
|
+
return false;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1269
1303
|
/** `429`/`5xx` are the only statuses the §7 retry policy replays. */
|
|
1270
1304
|
function isRetryableStatus(status) {
|
|
1271
1305
|
return status === 429 || status >= 500;
|
package/dist/api/contract.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The contract level this build serves and requires. Written by `scripts/contract.mjs release`; never edit by hand. */
|
|
2
|
-
export declare const CONTRACT_LEVEL = "1.
|
|
2
|
+
export declare const CONTRACT_LEVEL = "1.34";
|
|
3
3
|
/** Request header naming the one contract level the caller needs, e.g. `1.3`. Optional: absent means no check. */
|
|
4
4
|
export declare const REQUIRED_CONTRACT_HEADER = "Crouter-Required-Contract";
|
|
5
5
|
/** Response header listing the contract levels the server serves, comma-separated, one per major, e.g. `1.3`. */
|
package/dist/api/contract.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
//
|
|
13
13
|
// PURITY (spec §3.1): Node built-ins + `src/api/*` only.
|
|
14
14
|
/** The contract level this build serves and requires. Written by `scripts/contract.mjs release`; never edit by hand. */
|
|
15
|
-
export const CONTRACT_LEVEL = '1.
|
|
15
|
+
export const CONTRACT_LEVEL = '1.34';
|
|
16
16
|
/** Request header naming the one contract level the caller needs, e.g. `1.3`. Optional: absent means no check. */
|
|
17
17
|
export const REQUIRED_CONTRACT_HEADER = 'Crouter-Required-Contract';
|
|
18
18
|
/** Response header listing the contract levels the server serves, comma-separated, one per major, e.g. `1.3`. */
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BashRunDTO } from './bash.js';
|
|
2
|
+
/** `POST /v1/exec`: run the `crtr` binary with `argv` (no shell, no stdin) in the calling app's sandbox. */
|
|
3
|
+
export interface ExecRunParams {
|
|
4
|
+
/** Arguments after `crtr`; at least one, under 32 KB as JSON text. */
|
|
5
|
+
argv: string[];
|
|
6
|
+
/** Seconds before the process is killed; default 60, at most 100. */
|
|
7
|
+
timeout_s?: number;
|
|
8
|
+
}
|
|
9
|
+
/** A non-zero exit or a timeout is still a 200 with this body. */
|
|
10
|
+
export type ExecRunDTO = BashRunDTO;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -134,6 +134,11 @@ export declare const errorCodes: {
|
|
|
134
134
|
readonly status: 409;
|
|
135
135
|
readonly retryable: false;
|
|
136
136
|
};
|
|
137
|
+
readonly model_sign_in_required: {
|
|
138
|
+
readonly type: "invalid_request_error";
|
|
139
|
+
readonly status: 409;
|
|
140
|
+
readonly retryable: false;
|
|
141
|
+
};
|
|
137
142
|
readonly invalid_argument: {
|
|
138
143
|
readonly type: "invalid_request_error";
|
|
139
144
|
readonly status: 400;
|
|
@@ -214,6 +219,11 @@ export declare const errorCodes: {
|
|
|
214
219
|
readonly status: 429;
|
|
215
220
|
readonly retryable: true;
|
|
216
221
|
};
|
|
222
|
+
readonly busy: {
|
|
223
|
+
readonly type: "rate_limit_error";
|
|
224
|
+
readonly status: 429;
|
|
225
|
+
readonly retryable: true;
|
|
226
|
+
};
|
|
217
227
|
readonly runtime_unreachable: {
|
|
218
228
|
readonly type: "server_error";
|
|
219
229
|
readonly status: 503;
|
|
@@ -224,6 +234,11 @@ export declare const errorCodes: {
|
|
|
224
234
|
readonly status: 503;
|
|
225
235
|
readonly retryable: true;
|
|
226
236
|
};
|
|
237
|
+
readonly sandbox_unavailable: {
|
|
238
|
+
readonly type: "server_error";
|
|
239
|
+
readonly status: 503;
|
|
240
|
+
readonly retryable: false;
|
|
241
|
+
};
|
|
227
242
|
readonly runtime_starting: {
|
|
228
243
|
readonly type: "server_error";
|
|
229
244
|
readonly status: 503;
|
package/dist/api/error-codes.js
CHANGED
|
@@ -37,6 +37,8 @@ export const errorCodes = {
|
|
|
37
37
|
// A peer machine runs a crtr release below the peer-protocol floor; details `{ peer_version, floor, receiver_version }`. Update it with `crtr sys update`.
|
|
38
38
|
peer_version_too_old: { type: 'invalid_request_error', status: 426, retryable: false },
|
|
39
39
|
conflict: { type: 'invalid_request_error', status: 409, retryable: false },
|
|
40
|
+
// A node start by an exec (or a cron armed from one) whose model provider is not signed in; details `{ url }` = `<directory issuer>/account/ai-subscription`.
|
|
41
|
+
model_sign_in_required: { type: 'invalid_request_error', status: 409, retryable: false },
|
|
40
42
|
// `provider.call` input that cannot apply: `machine` on a provider that is not served by a machine.
|
|
41
43
|
invalid_argument: { type: 'invalid_request_error', status: 400, retryable: false },
|
|
42
44
|
// A device call names no machine and the account has several (or the named one is not a device); details `{ candidates: [{ machine_id, name, last_seen }] }`.
|
|
@@ -58,8 +60,12 @@ export const errorCodes = {
|
|
|
58
60
|
all_routes_exhausted: { type: 'rate_limit_error', status: 429, retryable: true },
|
|
59
61
|
provider_rate_limited: { type: 'rate_limit_error', status: 429, retryable: true },
|
|
60
62
|
rate_limited: { type: 'rate_limit_error', status: 429, retryable: true },
|
|
63
|
+
// `exec`: the app already has 4 live exec processes on this runtime.
|
|
64
|
+
busy: { type: 'rate_limit_error', status: 429, retryable: true },
|
|
61
65
|
runtime_unreachable: { type: 'server_error', status: 503, retryable: true },
|
|
62
66
|
isolated_unavailable: { type: 'server_error', status: 503, retryable: true },
|
|
67
|
+
// `exec` on a runtime without a root launcher: it cannot sandbox the app's process, so it refuses to run it.
|
|
68
|
+
sandbox_unavailable: { type: 'server_error', status: 503, retryable: false },
|
|
63
69
|
runtime_starting: { type: 'server_error', status: 503, retryable: true },
|
|
64
70
|
runtime_provisioning: { type: 'server_error', status: 503, retryable: true },
|
|
65
71
|
provider_unavailable: { type: 'server_error', status: 503, retryable: true },
|
package/dist/api/index.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ export * from './dto/worktree.js';
|
|
|
42
42
|
export * from './dto/human.js';
|
|
43
43
|
export * from './dto/files.js';
|
|
44
44
|
export * from './dto/bash.js';
|
|
45
|
+
export * from './dto/exec.js';
|
|
45
46
|
export * from './dto/inbox.js';
|
|
46
47
|
export * from './dto/human-requests.js';
|
|
47
48
|
export * from './dto/reviews.js';
|
package/dist/api/index.js
CHANGED
|
@@ -39,6 +39,7 @@ export * from './dto/worktree.js';
|
|
|
39
39
|
export * from './dto/human.js';
|
|
40
40
|
export * from './dto/files.js';
|
|
41
41
|
export * from './dto/bash.js';
|
|
42
|
+
export * from './dto/exec.js';
|
|
42
43
|
export * from './dto/inbox.js';
|
|
43
44
|
export * from './dto/human-requests.js';
|
|
44
45
|
export * from './dto/reviews.js';
|
package/dist/api/routes.d.ts
CHANGED
|
@@ -151,6 +151,7 @@ export declare const routes: {
|
|
|
151
151
|
readonly fileWrite: () => string;
|
|
152
152
|
readonly fileList: () => string;
|
|
153
153
|
readonly bash: () => string;
|
|
154
|
+
readonly exec: () => string;
|
|
154
155
|
readonly objects: () => string;
|
|
155
156
|
readonly object: (ref: string) => string;
|
|
156
157
|
readonly objectsSearch: () => string;
|
package/dist/api/routes.js
CHANGED
|
@@ -182,6 +182,7 @@ export const routes = {
|
|
|
182
182
|
fileWrite: () => `${V}/files/write`,
|
|
183
183
|
fileList: () => `${V}/files/list`,
|
|
184
184
|
bash: () => `${V}/bash`,
|
|
185
|
+
exec: () => `${V}/exec`,
|
|
185
186
|
// Canvas objects and documents. A ref (name or id) may contain `/`, so it is
|
|
186
187
|
// always one percent-encoded path segment.
|
|
187
188
|
objects: () => `${V}/objects`,
|
package/dist/core/io.js
CHANGED
|
@@ -310,6 +310,13 @@ export function apiErrorToCliError(err, next, options = {}) {
|
|
|
310
310
|
next: 'Start it with `crtr sys daemon start`, then retry.',
|
|
311
311
|
});
|
|
312
312
|
}
|
|
313
|
+
// A node start through an app's exec without a signed-in AI subscription: the person signs in at the URL.
|
|
314
|
+
if (err.code === 'model_sign_in_required') {
|
|
315
|
+
const url = err.details?.url;
|
|
316
|
+
return new InputError({ error: err.code, message: err.message,
|
|
317
|
+
next: typeof url === 'string' ? `Ask the person to sign in at ${url}, then retry.` : 'Ask the person to sign in to an AI subscription on their Northlight account, then retry.',
|
|
318
|
+
...(typeof url === 'string' ? { details: { url } } : {}) }, ExitCode.USAGE);
|
|
319
|
+
}
|
|
313
320
|
// A responding daemon can relay an unavailable provider or storage service.
|
|
314
321
|
// HTTP 503 alone does not mean that crtrd is down.
|
|
315
322
|
const d = err.details;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/api",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.560",
|
|
4
4
|
"description": "Typed crtrd /v1 API contract — DTOs, route builders, the error contract, the CrtrClient, and the command-plugin manifest format. Zero runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/api/index.js",
|