@sealant/api-contracts-next 0.39.0-next.708 → 0.39.0-next.710
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/core-api/control-plane.d.ts +762 -761
- package/dist/core-api/control-plane.js +3 -0
- package/dist/core-api/request-refusal.d.ts +39 -0
- package/dist/core-api/request-refusal.js +135 -0
- package/dist/core-api/sessions.d.ts +44 -4
- package/dist/core-api/sessions.js +68 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +2 -2
|
@@ -6,6 +6,7 @@ import { InferenceGroup } from "./inference.js";
|
|
|
6
6
|
import { PackagesGroup } from "./packages.js";
|
|
7
7
|
import { ProfilesGroup } from "./profiles.js";
|
|
8
8
|
import { RegistriesGroup } from "./registries.js";
|
|
9
|
+
import { RequestRefusal } from "./request-refusal.js";
|
|
9
10
|
import { RunsGroup } from "./runs.js";
|
|
10
11
|
import { SessionsGroup } from "./sessions.js";
|
|
11
12
|
import { SshKeysGroup } from "./ssh-keys.js";
|
|
@@ -26,6 +27,8 @@ export const ControlPlaneAPI = HttpApi.make("sealantControlPlaneApi")
|
|
|
26
27
|
.add(RegistriesGroup.prefix("/v1/registries"))
|
|
27
28
|
.add(GitHubGroup.prefix("/v1/github"))
|
|
28
29
|
.add(UsersGroup.prefix("/v1/users"))
|
|
30
|
+
// Last, so it reaches every endpoint above (`HttpApi.middleware` skips endpoints added after it).
|
|
31
|
+
.middleware(RequestRefusal)
|
|
29
32
|
.annotate(OpenApi.Title, "Sealant Control Plane API")
|
|
30
33
|
.annotate(OpenApi.Version, "0.0.0")
|
|
31
34
|
.annotate(OpenApi.Description, "Sealant control-plane HTTP API.");
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Requests the contract cannot decode, on every route of the control plane API.
|
|
3
|
+
*
|
|
4
|
+
* Effect answers such a request with an empty `400`, and the failure it reports (to the request
|
|
5
|
+
* log, every `ErrorReporter` and the request's tracing span) quotes the rejected input: `Expected
|
|
6
|
+
* array, got "<argv>"`, a whole body that arrived wrapped in an array, `JSON.parse`'s excerpt of a
|
|
7
|
+
* malformed body. A request body can carry a process's arguments, a script or a credential, so
|
|
8
|
+
* `RequestRefusal`, applied to the whole API, answers `400` `RequestRefusedError` instead, with a
|
|
9
|
+
* reason that names where the request is wrong and never what it held. `describeRequestIssue` words
|
|
10
|
+
* that reason from the decode issue alone: field names the contract declares, array positions, and
|
|
11
|
+
* what was expected; a key the caller chose (an `env` or `metadata` name) reads `[…]`.
|
|
12
|
+
*/
|
|
13
|
+
import { Schema, SchemaIssue } from "effect";
|
|
14
|
+
import { HttpApiMiddleware } from "effect/unstable/httpapi";
|
|
15
|
+
declare const RequestRefusedError_base: Schema.Class<RequestRefusedError, Schema.TaggedStruct<"RequestRefusedError", {
|
|
16
|
+
/** Where the request is wrong and what was expected; never a value it held. */
|
|
17
|
+
readonly message: Schema.String;
|
|
18
|
+
}>, import("effect/Cause").YieldableError>;
|
|
19
|
+
export declare class RequestRefusedError extends RequestRefusedError_base {
|
|
20
|
+
}
|
|
21
|
+
declare const RequestRefusal_base: HttpApiMiddleware.ServiceClass<RequestRefusal, "@sealant/api-contracts/RequestRefusal", {
|
|
22
|
+
requires: never;
|
|
23
|
+
provides: never;
|
|
24
|
+
error: typeof RequestRefusedError;
|
|
25
|
+
clientError: never;
|
|
26
|
+
requiredForClient: false;
|
|
27
|
+
security: never;
|
|
28
|
+
}, HttpApiMiddleware.HttpApiMiddleware<never, typeof RequestRefusedError, never>>;
|
|
29
|
+
/** Applied to every endpoint of `ControlPlaneAPI`: see the module comment. */
|
|
30
|
+
export declare class RequestRefusal extends RequestRefusal_base {
|
|
31
|
+
}
|
|
32
|
+
/** The reason for a body that is not JSON at all (`JSON.parse` failed). */
|
|
33
|
+
export declare const REQUEST_BODY_NOT_JSON = "the request body is not valid JSON";
|
|
34
|
+
/**
|
|
35
|
+
* Why a request part (`body`, `query`, `path parameters`, `headers`) cannot be decoded, from its
|
|
36
|
+
* decode issue: up to three places, each with what was expected. Never a value the request held.
|
|
37
|
+
*/
|
|
38
|
+
export declare const describeRequestIssue: (part: string, issue: SchemaIssue.Issue) => string;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Requests the contract cannot decode, on every route of the control plane API.
|
|
3
|
+
*
|
|
4
|
+
* Effect answers such a request with an empty `400`, and the failure it reports (to the request
|
|
5
|
+
* log, every `ErrorReporter` and the request's tracing span) quotes the rejected input: `Expected
|
|
6
|
+
* array, got "<argv>"`, a whole body that arrived wrapped in an array, `JSON.parse`'s excerpt of a
|
|
7
|
+
* malformed body. A request body can carry a process's arguments, a script or a credential, so
|
|
8
|
+
* `RequestRefusal`, applied to the whole API, answers `400` `RequestRefusedError` instead, with a
|
|
9
|
+
* reason that names where the request is wrong and never what it held. `describeRequestIssue` words
|
|
10
|
+
* that reason from the decode issue alone: field names the contract declares, array positions, and
|
|
11
|
+
* what was expected; a key the caller chose (an `env` or `metadata` name) reads `[…]`.
|
|
12
|
+
*/
|
|
13
|
+
import { Option, Schema, SchemaAST, SchemaIssue } from "effect";
|
|
14
|
+
import { HttpApiMiddleware } from "effect/unstable/httpapi";
|
|
15
|
+
export class RequestRefusedError extends Schema.TaggedErrorClass()("RequestRefusedError", {
|
|
16
|
+
/** Where the request is wrong and what was expected; never a value it held. */
|
|
17
|
+
message: Schema.String,
|
|
18
|
+
}, { httpApiStatus: 400 }) {
|
|
19
|
+
}
|
|
20
|
+
/** Applied to every endpoint of `ControlPlaneAPI`: see the module comment. */
|
|
21
|
+
export class RequestRefusal extends HttpApiMiddleware.Service()("@sealant/api-contracts/RequestRefusal", { error: RequestRefusedError }) {
|
|
22
|
+
}
|
|
23
|
+
/** The reason for a body that is not JSON at all (`JSON.parse` failed). */
|
|
24
|
+
export const REQUEST_BODY_NOT_JSON = "the request body is not valid JSON";
|
|
25
|
+
const MAX_REASONS = 3;
|
|
26
|
+
const expectedOf = (ast) => {
|
|
27
|
+
if (SchemaAST.isString(ast))
|
|
28
|
+
return "a string";
|
|
29
|
+
if (SchemaAST.isNumber(ast))
|
|
30
|
+
return "a number";
|
|
31
|
+
if (SchemaAST.isBoolean(ast))
|
|
32
|
+
return "a boolean";
|
|
33
|
+
if (SchemaAST.isArrays(ast))
|
|
34
|
+
return "an array";
|
|
35
|
+
if (SchemaAST.isObjects(ast))
|
|
36
|
+
return "an object";
|
|
37
|
+
if (SchemaAST.isNull(ast))
|
|
38
|
+
return "null";
|
|
39
|
+
if (SchemaAST.isUndefined(ast))
|
|
40
|
+
return undefined;
|
|
41
|
+
if (SchemaAST.isUnion(ast)) {
|
|
42
|
+
// The JSON codec widens some types into unions (a number also takes "NaN" and "Infinity"):
|
|
43
|
+
// what the members expect, the fixed values said only when nothing else is.
|
|
44
|
+
const kinds = [...new Set(ast.types.map(expectedOf))].filter((kind) => kind !== undefined);
|
|
45
|
+
const open = kinds.filter((kind) => kind !== "one of the allowed values");
|
|
46
|
+
return (open.length > 0 ? open : kinds).join(" or ") || undefined;
|
|
47
|
+
}
|
|
48
|
+
return "one of the allowed values";
|
|
49
|
+
};
|
|
50
|
+
const stringAnnotation = (annotations, key) => {
|
|
51
|
+
if (typeof annotations !== "object" || annotations === null || !(key in annotations)) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const value = Reflect.get(annotations, key);
|
|
55
|
+
return typeof value === "string" ? value : undefined;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* A schema author's own words for a failed check, unless they quote the rejected value (a guard
|
|
59
|
+
* against a future check that interpolates its input; no check in the contract does today).
|
|
60
|
+
*/
|
|
61
|
+
const authoredMessage = (message, actual) => {
|
|
62
|
+
if (message === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
if (typeof actual === "string" && actual.length >= 3 && message.includes(actual)) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
return message;
|
|
68
|
+
};
|
|
69
|
+
/** A path segment: a declared field by name, a position by index, anything else as `[…]`. */
|
|
70
|
+
const segment = (key, parent) => {
|
|
71
|
+
if (typeof key === "number")
|
|
72
|
+
return `[${key}]`;
|
|
73
|
+
const declared = parent !== undefined &&
|
|
74
|
+
SchemaAST.isObjects(parent) &&
|
|
75
|
+
parent.propertySignatures.some((signature) => signature.name === key);
|
|
76
|
+
return declared && typeof key === "string" ? `.${key}` : "[…]";
|
|
77
|
+
};
|
|
78
|
+
const leaf = (path, message) => [{ path, message }];
|
|
79
|
+
const reasonsOf = (issue, path, parent) => {
|
|
80
|
+
if (issue instanceof SchemaIssue.Composite) {
|
|
81
|
+
return issue.issues.flatMap((inner) => reasonsOf(inner, path, issue.ast));
|
|
82
|
+
}
|
|
83
|
+
if (issue instanceof SchemaIssue.Pointer) {
|
|
84
|
+
const at = issue.path.reduce((acc, key) => acc + segment(key, parent), path);
|
|
85
|
+
return reasonsOf(issue.issue, at, undefined);
|
|
86
|
+
}
|
|
87
|
+
if (issue instanceof SchemaIssue.Encoding) {
|
|
88
|
+
return reasonsOf(issue.issue, path, parent);
|
|
89
|
+
}
|
|
90
|
+
if (issue instanceof SchemaIssue.AnyOf) {
|
|
91
|
+
return issue.issues.length === 0
|
|
92
|
+
? leaf(path, `must be ${expectedOf(issue.ast) ?? "one of the allowed values"}`)
|
|
93
|
+
: issue.issues.flatMap((inner) => reasonsOf(inner, path, parent));
|
|
94
|
+
}
|
|
95
|
+
if (issue instanceof SchemaIssue.Filter) {
|
|
96
|
+
const inner = issue.issue instanceof SchemaIssue.InvalidValue
|
|
97
|
+
? stringAnnotation(issue.issue.annotations, "message")
|
|
98
|
+
: undefined;
|
|
99
|
+
const message = authoredMessage(inner ?? stringAnnotation(issue.filter.annotations, "message"), issue.actual);
|
|
100
|
+
if (message !== undefined)
|
|
101
|
+
return leaf(path, message);
|
|
102
|
+
const expected = stringAnnotation(issue.filter.annotations, "expected");
|
|
103
|
+
if (expected !== undefined)
|
|
104
|
+
return leaf(path, `must be ${expected}`);
|
|
105
|
+
return issue.issue instanceof SchemaIssue.InvalidValue
|
|
106
|
+
? leaf(path, "is invalid")
|
|
107
|
+
: reasonsOf(issue.issue, path, parent);
|
|
108
|
+
}
|
|
109
|
+
if (issue instanceof SchemaIssue.InvalidType) {
|
|
110
|
+
return leaf(path, `must be ${expectedOf(issue.ast) ?? "present"}`);
|
|
111
|
+
}
|
|
112
|
+
if (issue instanceof SchemaIssue.MissingKey)
|
|
113
|
+
return leaf(path, "is required");
|
|
114
|
+
if (issue instanceof SchemaIssue.InvalidValue) {
|
|
115
|
+
const actual = Option.getOrUndefined(issue.actual);
|
|
116
|
+
return leaf(path, authoredMessage(stringAnnotation(issue.annotations, "message"), actual) ?? "is invalid");
|
|
117
|
+
}
|
|
118
|
+
if (issue instanceof SchemaIssue.OneOf)
|
|
119
|
+
return leaf(path, "matches more than one allowed shape");
|
|
120
|
+
return leaf(path, "is not allowed");
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Why a request part (`body`, `query`, `path parameters`, `headers`) cannot be decoded, from its
|
|
124
|
+
* decode issue: up to three places, each with what was expected. Never a value the request held.
|
|
125
|
+
*/
|
|
126
|
+
export const describeRequestIssue = (part, issue) => {
|
|
127
|
+
const reasons = reasonsOf(issue, "", undefined);
|
|
128
|
+
const shown = reasons.slice(0, MAX_REASONS).map(({ path, message }) => {
|
|
129
|
+
const where = path === "" ? `the request ${part}` : path.replace(/^\./, "");
|
|
130
|
+
// A check that names its own position (`argv[1] contains a NUL byte`) needs no prefix.
|
|
131
|
+
return message.startsWith(where) ? message : `${where} ${message}`;
|
|
132
|
+
});
|
|
133
|
+
const more = reasons.length - shown.length;
|
|
134
|
+
return more > 0 ? `${shown.join("; ")}; and ${more} more` : shown.join("; ");
|
|
135
|
+
};
|
|
@@ -51,10 +51,41 @@ export type WorkspaceProcessUser = typeof workspaceProcessUserSchema.Type;
|
|
|
51
51
|
* group other than `mend`), or no such user exists yet. Nothing is started.
|
|
52
52
|
*/
|
|
53
53
|
export declare const PROCESS_USER_UNSUPPORTED_CODE = "user-unsupported";
|
|
54
|
+
/** The most words a session's argv may hold, the program included. */
|
|
55
|
+
export declare const SESSION_ARGV_MAX_WORDS = 64;
|
|
56
|
+
/**
|
|
57
|
+
* The most UTF-8 bytes one word of a session's argv may hold, its terminating NUL not counted:
|
|
58
|
+
* Linux's `MAX_ARG_STRLEN` (32 pages, 128 KiB on 4 KiB pages) counts the NUL, so 131,071 bytes of
|
|
59
|
+
* text is the longest word `execve` takes there; one byte more fails with `E2BIG` whatever the total.
|
|
60
|
+
*/
|
|
61
|
+
export declare const SESSION_ARGV_MAX_WORD_BYTES: number;
|
|
62
|
+
/**
|
|
63
|
+
* The most UTF-8 bytes a session's argv may hold in all: half of Linux's usual 2 MiB `ARG_MAX`,
|
|
64
|
+
* which argv shares with the environment, and far below sealantd's 8 MiB control frame.
|
|
65
|
+
*/
|
|
66
|
+
export declare const SESSION_ARGV_MAX_TOTAL_BYTES: number;
|
|
67
|
+
/**
|
|
68
|
+
* Why `argv` cannot start a session, or `undefined` when it can. `argv[0]`, the program, is
|
|
69
|
+
* non-empty with no leading or trailing whitespace. Every later word is opaque to Sealant and may be
|
|
70
|
+
* any string, empty, whitespace-led or multi-line (`bash -lc "\n echo hi"`, `git commit -m ""`),
|
|
71
|
+
* except one with a NUL byte, which no process argument can carry, or a lone UTF-16 surrogate, which
|
|
72
|
+
* has no UTF-8 form and would not reach the process as sent. The reason names a word by its position
|
|
73
|
+
* and size, never its text, which can carry a secret: it reaches the caller in the `400` and the
|
|
74
|
+
* server's request log.
|
|
75
|
+
*/
|
|
76
|
+
export declare const sessionArgvIssue: (argv: readonly string[]) => string | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* A session's argv: the program, then its arguments, as `sessionArgvIssue` allows. One check over
|
|
79
|
+
* the whole array, because Effect's own length and whitespace checks quote the offending value.
|
|
80
|
+
*/
|
|
81
|
+
export declare const sessionArgvSchema: Schema.$Array<Schema.String>;
|
|
54
82
|
export declare const createSessionRequestSchema: Schema.Struct<{
|
|
55
83
|
readonly workspaceId: Schema.String;
|
|
56
84
|
readonly ownerUserId: Schema.String;
|
|
57
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* argv[0] is the program the session runs; the rest its arguments, passed to it as an argv array,
|
|
87
|
+
* never through a shell. See `sessionArgvIssue` for what is refused.
|
|
88
|
+
*/
|
|
58
89
|
readonly argv: Schema.$Array<Schema.String>;
|
|
59
90
|
/** Working directory inside the workspace (defaults to the workspace working directory). */
|
|
60
91
|
readonly cwd: Schema.optional<Schema.String>;
|
|
@@ -83,7 +114,10 @@ export type CreateSessionRequest = typeof createSessionRequestSchema.Type;
|
|
|
83
114
|
export declare const createSessionAsUserRequestSchema: Schema.Struct<{
|
|
84
115
|
readonly workspaceId: Schema.String;
|
|
85
116
|
readonly ownerUserId: Schema.String;
|
|
86
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* argv[0] is the program the session runs; the rest its arguments, passed to it as an argv array,
|
|
119
|
+
* never through a shell. See `sessionArgvIssue` for what is refused.
|
|
120
|
+
*/
|
|
87
121
|
readonly argv: Schema.$Array<Schema.String>;
|
|
88
122
|
/** Working directory inside the workspace (defaults to the workspace working directory). */
|
|
89
123
|
readonly cwd: Schema.optional<Schema.String>;
|
|
@@ -309,7 +343,10 @@ export declare const SessionsGroup: HttpApiGroup.HttpApiGroup<"sessions", HttpAp
|
|
|
309
343
|
}>>, HttpApiEndpoint.Json<typeof SessionBadRequestError | typeof SessionUnauthorizedError | typeof SessionForbiddenError | typeof SessionNotFoundError | typeof SessionBadGatewayError | typeof SessionInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"createSession", "POST", "/", HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
|
|
310
344
|
readonly workspaceId: Schema.String;
|
|
311
345
|
readonly ownerUserId: Schema.String;
|
|
312
|
-
/**
|
|
346
|
+
/**
|
|
347
|
+
* argv[0] is the program the session runs; the rest its arguments, passed to it as an argv array,
|
|
348
|
+
* never through a shell. See `sessionArgvIssue` for what is refused.
|
|
349
|
+
*/
|
|
313
350
|
readonly argv: Schema.$Array<Schema.String>;
|
|
314
351
|
/** Working directory inside the workspace (defaults to the workspace working directory). */
|
|
315
352
|
readonly cwd: Schema.optional<Schema.String>;
|
|
@@ -366,7 +403,10 @@ export declare const SessionsGroup: HttpApiGroup.HttpApiGroup<"sessions", HttpAp
|
|
|
366
403
|
}>>, HttpApiEndpoint.Json<typeof SessionBadRequestError | typeof SessionUnauthorizedError | typeof SessionForbiddenError | typeof SessionNotFoundError | typeof SessionConflictError | typeof SessionBadGatewayError | typeof SessionInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"createSessionAsUser", "POST", "/as-user", HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
|
|
367
404
|
readonly workspaceId: Schema.String;
|
|
368
405
|
readonly ownerUserId: Schema.String;
|
|
369
|
-
/**
|
|
406
|
+
/**
|
|
407
|
+
* argv[0] is the program the session runs; the rest its arguments, passed to it as an argv array,
|
|
408
|
+
* never through a shell. See `sessionArgvIssue` for what is refused.
|
|
409
|
+
*/
|
|
370
410
|
readonly argv: Schema.$Array<Schema.String>;
|
|
371
411
|
/** Working directory inside the workspace (defaults to the workspace working directory). */
|
|
372
412
|
readonly cwd: Schema.optional<Schema.String>;
|
|
@@ -48,11 +48,77 @@ export const workspaceProcessUserSchema = Schema.String.check(Schema.isPattern(/
|
|
|
48
48
|
* group other than `mend`), or no such user exists yet. Nothing is started.
|
|
49
49
|
*/
|
|
50
50
|
export const PROCESS_USER_UNSUPPORTED_CODE = "user-unsupported";
|
|
51
|
+
/** The most words a session's argv may hold, the program included. */
|
|
52
|
+
export const SESSION_ARGV_MAX_WORDS = 64;
|
|
53
|
+
/**
|
|
54
|
+
* The most UTF-8 bytes one word of a session's argv may hold, its terminating NUL not counted:
|
|
55
|
+
* Linux's `MAX_ARG_STRLEN` (32 pages, 128 KiB on 4 KiB pages) counts the NUL, so 131,071 bytes of
|
|
56
|
+
* text is the longest word `execve` takes there; one byte more fails with `E2BIG` whatever the total.
|
|
57
|
+
*/
|
|
58
|
+
export const SESSION_ARGV_MAX_WORD_BYTES = 128 * 1024 - 1;
|
|
59
|
+
/**
|
|
60
|
+
* The most UTF-8 bytes a session's argv may hold in all: half of Linux's usual 2 MiB `ARG_MAX`,
|
|
61
|
+
* which argv shares with the environment, and far below sealantd's 8 MiB control frame.
|
|
62
|
+
*/
|
|
63
|
+
export const SESSION_ARGV_MAX_TOTAL_BYTES = 1024 * 1024;
|
|
64
|
+
const utf8Bytes = (value) => new TextEncoder().encode(value).length;
|
|
65
|
+
/**
|
|
66
|
+
* A UTF-16 surrogate with no partner (with the `u` flag a pair is one code point and never matches):
|
|
67
|
+
* it has no UTF-8 form, so it would reach the process as U+FFFD.
|
|
68
|
+
*/
|
|
69
|
+
const LONE_SURROGATE = /[\uD800-\uDFFF]/u;
|
|
70
|
+
/**
|
|
71
|
+
* Why `argv` cannot start a session, or `undefined` when it can. `argv[0]`, the program, is
|
|
72
|
+
* non-empty with no leading or trailing whitespace. Every later word is opaque to Sealant and may be
|
|
73
|
+
* any string, empty, whitespace-led or multi-line (`bash -lc "\n echo hi"`, `git commit -m ""`),
|
|
74
|
+
* except one with a NUL byte, which no process argument can carry, or a lone UTF-16 surrogate, which
|
|
75
|
+
* has no UTF-8 form and would not reach the process as sent. The reason names a word by its position
|
|
76
|
+
* and size, never its text, which can carry a secret: it reaches the caller in the `400` and the
|
|
77
|
+
* server's request log.
|
|
78
|
+
*/
|
|
79
|
+
export const sessionArgvIssue = (argv) => {
|
|
80
|
+
const program = argv[0];
|
|
81
|
+
if (program === undefined) {
|
|
82
|
+
return "argv is empty: argv[0] must name the program";
|
|
83
|
+
}
|
|
84
|
+
if (argv.length > SESSION_ARGV_MAX_WORDS) {
|
|
85
|
+
return `argv has ${argv.length} words; the maximum is ${SESSION_ARGV_MAX_WORDS}`;
|
|
86
|
+
}
|
|
87
|
+
if (program.length === 0 || program.trim() !== program) {
|
|
88
|
+
return "argv[0], the program, must be non-empty with no leading or trailing whitespace";
|
|
89
|
+
}
|
|
90
|
+
let totalBytes = 0;
|
|
91
|
+
for (const [index, word] of argv.entries()) {
|
|
92
|
+
if (word.includes("\u0000")) {
|
|
93
|
+
return `argv[${index}] contains a NUL byte, which no process argument can carry`;
|
|
94
|
+
}
|
|
95
|
+
if (LONE_SURROGATE.test(word)) {
|
|
96
|
+
return `argv[${index}] is not well-formed Unicode (a lone surrogate)`;
|
|
97
|
+
}
|
|
98
|
+
const bytes = utf8Bytes(word);
|
|
99
|
+
if (bytes > SESSION_ARGV_MAX_WORD_BYTES) {
|
|
100
|
+
return `argv[${index}] is ${bytes} bytes; the maximum per word is ${SESSION_ARGV_MAX_WORD_BYTES}`;
|
|
101
|
+
}
|
|
102
|
+
totalBytes += bytes;
|
|
103
|
+
}
|
|
104
|
+
if (totalBytes > SESSION_ARGV_MAX_TOTAL_BYTES) {
|
|
105
|
+
return `argv totals ${totalBytes} bytes; the maximum is ${SESSION_ARGV_MAX_TOTAL_BYTES}`;
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* A session's argv: the program, then its arguments, as `sessionArgvIssue` allows. One check over
|
|
111
|
+
* the whole array, because Effect's own length and whitespace checks quote the offending value.
|
|
112
|
+
*/
|
|
113
|
+
export const sessionArgvSchema = Schema.Array(Schema.String).check(Schema.makeFilter(sessionArgvIssue));
|
|
51
114
|
export const createSessionRequestSchema = Schema.Struct({
|
|
52
115
|
workspaceId: NonEmptyString,
|
|
53
116
|
ownerUserId: NonEmptyString,
|
|
54
|
-
/**
|
|
55
|
-
|
|
117
|
+
/**
|
|
118
|
+
* argv[0] is the program the session runs; the rest its arguments, passed to it as an argv array,
|
|
119
|
+
* never through a shell. See `sessionArgvIssue` for what is refused.
|
|
120
|
+
*/
|
|
121
|
+
argv: sessionArgvSchema,
|
|
56
122
|
/** Working directory inside the workspace (defaults to the workspace working directory). */
|
|
57
123
|
cwd: Schema.optional(NonEmptyString),
|
|
58
124
|
/** Extra environment for the PTY process (values are NOT secrets — use credentials for those). */
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./core-api/budgets.js";
|
|
|
9
9
|
export * from "./core-api/oci-names.js";
|
|
10
10
|
export * from "./core-api/registries.js";
|
|
11
11
|
export * from "./core-api/runs.js";
|
|
12
|
+
export * from "./core-api/request-refusal.js";
|
|
12
13
|
export * from "./core-api/sessions.js";
|
|
13
14
|
export * from "./core-api/access-tokens.js";
|
|
14
15
|
export * from "./core-api/workspaces.js";
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./core-api/budgets.js";
|
|
|
9
9
|
export * from "./core-api/oci-names.js";
|
|
10
10
|
export * from "./core-api/registries.js";
|
|
11
11
|
export * from "./core-api/runs.js";
|
|
12
|
+
export * from "./core-api/request-refusal.js";
|
|
12
13
|
export * from "./core-api/sessions.js";
|
|
13
14
|
export * from "./core-api/access-tokens.js";
|
|
14
15
|
export * from "./core-api/workspaces.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sealant/api-contracts-next",
|
|
3
|
-
"version": "0.39.0-next.
|
|
3
|
+
"version": "0.39.0-next.710",
|
|
4
4
|
"description": "Contract-first HTTP API definitions for Sealant services",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"effect": ">=4.0.0-beta.85 <5"
|
|
34
34
|
},
|
|
35
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "5e7c33bdaf78dd6aaf0b4b64332e803f3ce8d396",
|
|
36
36
|
"publishConfig": {
|
|
37
37
|
"access": "public"
|
|
38
38
|
}
|