@sealant/api-contracts-next 0.0.0-next.0 → 0.39.0-next.682
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/LICENSE +202 -0
- package/dist/core-api/access-tokens.d.ts +126 -0
- package/dist/core-api/access-tokens.js +72 -0
- package/dist/core-api/budgets.d.ts +15 -0
- package/dist/core-api/budgets.js +13 -0
- package/dist/core-api/connected-accounts.d.ts +226 -0
- package/dist/core-api/connected-accounts.js +128 -0
- package/dist/core-api/control-plane.d.ts +1318 -0
- package/dist/core-api/control-plane.js +31 -0
- package/dist/core-api/github.d.ts +261 -0
- package/dist/core-api/github.js +153 -0
- package/dist/core-api/inference.d.ts +223 -0
- package/dist/core-api/inference.js +139 -0
- package/dist/core-api/oci-names.d.ts +21 -0
- package/dist/core-api/oci-names.js +44 -0
- package/dist/core-api/packages.d.ts +179 -0
- package/dist/core-api/packages.js +60 -0
- package/dist/core-api/profiles.d.ts +188 -0
- package/dist/core-api/profiles.js +88 -0
- package/dist/core-api/record-events.d.ts +209 -0
- package/dist/core-api/record-events.js +193 -0
- package/dist/core-api/registries.d.ts +106 -0
- package/dist/core-api/registries.js +85 -0
- package/dist/core-api/runs.d.ts +516 -0
- package/dist/core-api/runs.js +284 -0
- package/dist/core-api/sessions.d.ts +390 -0
- package/dist/core-api/sessions.js +264 -0
- package/dist/core-api/ssh-keys.d.ts +120 -0
- package/dist/core-api/ssh-keys.js +108 -0
- package/dist/core-api/system.d.ts +51 -0
- package/dist/core-api/system.js +41 -0
- package/dist/core-api/users.d.ts +71 -0
- package/dist/core-api/users.js +48 -0
- package/dist/core-api/workspaces.d.ts +1880 -0
- package/dist/core-api/workspaces.js +980 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +18 -0
- package/dist/workspace-environment.d.ts +124 -0
- package/dist/workspace-environment.js +259 -0
- package/package.json +31 -9
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from "./core-api/connected-accounts.js";
|
|
2
|
+
export * from "./core-api/control-plane.js";
|
|
3
|
+
export * from "./core-api/github.js";
|
|
4
|
+
export * from "./core-api/inference.js";
|
|
5
|
+
export * from "./core-api/packages.js";
|
|
6
|
+
export * from "./core-api/profiles.js";
|
|
7
|
+
export * from "./core-api/record-events.js";
|
|
8
|
+
export * from "./core-api/budgets.js";
|
|
9
|
+
export * from "./core-api/oci-names.js";
|
|
10
|
+
export * from "./core-api/registries.js";
|
|
11
|
+
export * from "./core-api/runs.js";
|
|
12
|
+
export * from "./core-api/sessions.js";
|
|
13
|
+
export * from "./core-api/access-tokens.js";
|
|
14
|
+
export * from "./core-api/workspaces.js";
|
|
15
|
+
export * from "./core-api/ssh-keys.js";
|
|
16
|
+
export * from "./core-api/system.js";
|
|
17
|
+
export * from "./core-api/users.js";
|
|
18
|
+
export * from "./workspace-environment.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from "./core-api/connected-accounts.js";
|
|
2
|
+
export * from "./core-api/control-plane.js";
|
|
3
|
+
export * from "./core-api/github.js";
|
|
4
|
+
export * from "./core-api/inference.js";
|
|
5
|
+
export * from "./core-api/packages.js";
|
|
6
|
+
export * from "./core-api/profiles.js";
|
|
7
|
+
export * from "./core-api/record-events.js";
|
|
8
|
+
export * from "./core-api/budgets.js";
|
|
9
|
+
export * from "./core-api/oci-names.js";
|
|
10
|
+
export * from "./core-api/registries.js";
|
|
11
|
+
export * from "./core-api/runs.js";
|
|
12
|
+
export * from "./core-api/sessions.js";
|
|
13
|
+
export * from "./core-api/access-tokens.js";
|
|
14
|
+
export * from "./core-api/workspaces.js";
|
|
15
|
+
export * from "./core-api/ssh-keys.js";
|
|
16
|
+
export * from "./core-api/system.js";
|
|
17
|
+
export * from "./core-api/users.js";
|
|
18
|
+
export * from "./workspace-environment.js";
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace environment policy: the ONE definition of which caller-supplied environment variables
|
|
3
|
+
* a workspace may receive at creation, shared verbatim by the SDK (client-side rejection with the
|
|
4
|
+
* same messages the server would produce), the control-plane validators (`runtime.userEnv`), and
|
|
5
|
+
* any downstream product composing its own additions on top.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately dependency-free (no schema library): consumers span an Effect-based contract
|
|
8
|
+
* package, zod-based validators, and browser bundles. Everything here is data plus pure functions.
|
|
9
|
+
*
|
|
10
|
+
* Two invariants shape the rules:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Nothing that validates may silently vanish.** sealantd rebuilds the child environment for
|
|
13
|
+
* every managed process from a boot-time snapshot of the container environment, dropping names
|
|
14
|
+
* that match its secret markers (substring `TOKEN`/`SECRET`/`PASSWORD`/`PASSWD`/`CREDENTIAL`/
|
|
15
|
+
* `APIKEY`, suffix `_KEY`, exact `KEY`) and its consumed `SEALANT_*` keys. The reserved rules
|
|
16
|
+
* here are a strict superset of that filter, so an accepted name is guaranteed to reach the
|
|
17
|
+
* workspace's processes rather than being filtered on the way in.
|
|
18
|
+
* 2. **Diagnostics never carry values.** Accepted values are ordinary (non-secret) configuration
|
|
19
|
+
* by contract, but issue records still identify entries by name or index only — a name may
|
|
20
|
+
* appear in an issue only after it has passed the grammar check; otherwise a bounded escaped
|
|
21
|
+
* rendering is used.
|
|
22
|
+
*/
|
|
23
|
+
export declare const WORKSPACE_ENV_NAME_PATTERN: RegExp;
|
|
24
|
+
/** Names are ASCII by grammar, so length in characters equals length in bytes. */
|
|
25
|
+
export declare const WORKSPACE_ENV_MAX_NAME_LENGTH = 128;
|
|
26
|
+
/** Maximum UTF-8 bytes per value. Values may be empty and may contain newlines, never NUL. */
|
|
27
|
+
export declare const WORKSPACE_ENV_MAX_VALUE_BYTES = 4096;
|
|
28
|
+
export declare const WORKSPACE_ENV_MAX_ENTRIES = 128;
|
|
29
|
+
/** Maximum total UTF-8 bytes across all names and values of one map. */
|
|
30
|
+
export declare const WORKSPACE_ENV_MAX_TOTAL_BYTES = 32768;
|
|
31
|
+
/**
|
|
32
|
+
* Why a name is reserved. `secret-marker` mirrors sealantd's filter exactly (see module doc);
|
|
33
|
+
* the rest prevent project-level settings from silently reconfiguring platform control,
|
|
34
|
+
* connected-account lookup, loaders, shells, or global tool runtimes. Legitimate uses of a
|
|
35
|
+
* blocked process-control variable belong in explicit command or workspace-image configuration
|
|
36
|
+
* where the effect is visible.
|
|
37
|
+
*/
|
|
38
|
+
export type WorkspaceEnvReservedRule = "platform-prefix" | "process-identity" | "runtime-network" | "account-lookup" | "dynamic-loader" | "shell-startup" | "runtime-injection" | "git-ssh" | "secret-marker";
|
|
39
|
+
/** sealantd's secret markers, matched as substrings of the ASCII-uppercased name. */
|
|
40
|
+
export declare const WORKSPACE_ENV_SECRET_MARKERS: readonly ["TOKEN", "SECRET", "PASSWORD", "PASSWD", "CREDENTIAL", "APIKEY"];
|
|
41
|
+
/**
|
|
42
|
+
* The reserved rule a (grammar-valid) name violates, or undefined when the name is allowed.
|
|
43
|
+
* Matching is ASCII-uppercase so `http_proxy` cannot bypass `HTTP_PROXY`.
|
|
44
|
+
*/
|
|
45
|
+
export declare const findWorkspaceEnvReservedRule: (name: string) => WorkspaceEnvReservedRule | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* One rejected aspect of a workspace environment map. Name-bearing variants carry a name only when
|
|
48
|
+
* it passed the grammar check; grammar/length failures identify the entry by enumeration index and
|
|
49
|
+
* a bounded escaped rendering. No variant ever carries a value.
|
|
50
|
+
*/
|
|
51
|
+
export type WorkspaceEnvIssue = {
|
|
52
|
+
readonly rule: "name-grammar";
|
|
53
|
+
readonly index: number;
|
|
54
|
+
readonly nameDisplay: string;
|
|
55
|
+
} | {
|
|
56
|
+
readonly rule: "name-length";
|
|
57
|
+
readonly index: number;
|
|
58
|
+
readonly nameDisplay: string;
|
|
59
|
+
} | {
|
|
60
|
+
readonly rule: "name-reserved";
|
|
61
|
+
readonly name: string;
|
|
62
|
+
readonly reservedRule: WorkspaceEnvReservedRule;
|
|
63
|
+
} | {
|
|
64
|
+
readonly rule: "value-nul";
|
|
65
|
+
readonly name: string;
|
|
66
|
+
} | {
|
|
67
|
+
readonly rule: "value-size";
|
|
68
|
+
readonly name: string;
|
|
69
|
+
readonly valueBytes: number;
|
|
70
|
+
} | {
|
|
71
|
+
readonly rule: "entry-count";
|
|
72
|
+
readonly entryCount: number;
|
|
73
|
+
} | {
|
|
74
|
+
readonly rule: "total-size";
|
|
75
|
+
readonly totalBytes: number;
|
|
76
|
+
};
|
|
77
|
+
export type WorkspaceEnvParseResult = {
|
|
78
|
+
readonly ok: true;
|
|
79
|
+
readonly env: Readonly<Record<string, string>>;
|
|
80
|
+
} | {
|
|
81
|
+
readonly ok: false;
|
|
82
|
+
readonly issues: ReadonlyArray<WorkspaceEnvIssue>;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Validate a caller-supplied environment map against the full policy. On success the returned map
|
|
86
|
+
* is re-serialized in name order (deterministic for tests and stored specs). Collects every issue
|
|
87
|
+
* rather than stopping at the first, so a settings UI can annotate all rows in one round trip.
|
|
88
|
+
* Duplicate names cannot be detected here — a parsed JS object has already collapsed them; owners
|
|
89
|
+
* of the pre-parse representation (databases, forms) must enforce uniqueness themselves.
|
|
90
|
+
*/
|
|
91
|
+
export declare const parseWorkspaceEnv: (input: Readonly<Record<string, string>>) => WorkspaceEnvParseResult;
|
|
92
|
+
/**
|
|
93
|
+
* Validate a caller-supplied SECRET environment map — the transient secret channel
|
|
94
|
+
* (`CreateOptions.secretEnv`): same grammar and size bounds as `env`, and the same platform-owned
|
|
95
|
+
* name classes are reserved, but secret-shaped names are exactly what belongs here, so the
|
|
96
|
+
* `secret-marker` rule does not apply. Account-lookup names (`GITHUB_TOKEN`,
|
|
97
|
+
* `CLAUDE_CODE_OAUTH_TOKEN`, …) stay reserved: connected accounts own those.
|
|
98
|
+
*/
|
|
99
|
+
export declare const parseWorkspaceSecretEnv: (input: Readonly<Record<string, string>>) => WorkspaceEnvParseResult;
|
|
100
|
+
/** Reserved-rule lookup for the secret lane: everything but `secret-marker`. */
|
|
101
|
+
export declare const findWorkspaceSecretEnvReservedRule: (name: string) => WorkspaceEnvReservedRule | undefined;
|
|
102
|
+
/** Human-readable, value-free rendering of one issue; identical client- and server-side. */
|
|
103
|
+
export declare const formatWorkspaceEnvIssue: (issue: WorkspaceEnvIssue) => string;
|
|
104
|
+
/**
|
|
105
|
+
* Secret names the CONTROL PLANE itself adds to the sealed secret channel beside the caller's map.
|
|
106
|
+
* They carry the `SEALANT_` prefix — platform-owned, so no caller lane can supply them — and are
|
|
107
|
+
* consumed by `sealantd boot` rather than handed to workspace processes. Today: the capture
|
|
108
|
+
* source's session credential (sealantd ADR-0015), which the create request carries as
|
|
109
|
+
* `captureToken` and the daemon reads from its boot secret file as `SEALANT_CAPTURE_TOKEN`.
|
|
110
|
+
*/
|
|
111
|
+
export declare const CAPTURE_TOKEN_SECRET_ENV_NAME = "SEALANT_CAPTURE_TOKEN";
|
|
112
|
+
export declare const PLATFORM_SECRET_ENV_NAMES: ReadonlySet<string>;
|
|
113
|
+
export interface SplitSecretEnv {
|
|
114
|
+
/** The caller's entries: everything `parseWorkspaceSecretEnv` must re-validate. */
|
|
115
|
+
readonly callerEnv: Readonly<Record<string, string>>;
|
|
116
|
+
/** The platform-owned entries, matched by exact name; never subject to the caller policy. */
|
|
117
|
+
readonly platformEnv: Readonly<Record<string, string>>;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Separate the platform-owned entries of a sealed secret map from the caller's so the last-hop
|
|
121
|
+
* re-validation applies the caller policy to the caller's lane only. Any other `SEALANT_`-prefixed
|
|
122
|
+
* name stays in the caller lane and is rejected there, as before.
|
|
123
|
+
*/
|
|
124
|
+
export declare const splitPlatformSecretEnv: (input: Readonly<Record<string, string>>) => SplitSecretEnv;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace environment policy: the ONE definition of which caller-supplied environment variables
|
|
3
|
+
* a workspace may receive at creation, shared verbatim by the SDK (client-side rejection with the
|
|
4
|
+
* same messages the server would produce), the control-plane validators (`runtime.userEnv`), and
|
|
5
|
+
* any downstream product composing its own additions on top.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately dependency-free (no schema library): consumers span an Effect-based contract
|
|
8
|
+
* package, zod-based validators, and browser bundles. Everything here is data plus pure functions.
|
|
9
|
+
*
|
|
10
|
+
* Two invariants shape the rules:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Nothing that validates may silently vanish.** sealantd rebuilds the child environment for
|
|
13
|
+
* every managed process from a boot-time snapshot of the container environment, dropping names
|
|
14
|
+
* that match its secret markers (substring `TOKEN`/`SECRET`/`PASSWORD`/`PASSWD`/`CREDENTIAL`/
|
|
15
|
+
* `APIKEY`, suffix `_KEY`, exact `KEY`) and its consumed `SEALANT_*` keys. The reserved rules
|
|
16
|
+
* here are a strict superset of that filter, so an accepted name is guaranteed to reach the
|
|
17
|
+
* workspace's processes rather than being filtered on the way in.
|
|
18
|
+
* 2. **Diagnostics never carry values.** Accepted values are ordinary (non-secret) configuration
|
|
19
|
+
* by contract, but issue records still identify entries by name or index only — a name may
|
|
20
|
+
* appear in an issue only after it has passed the grammar check; otherwise a bounded escaped
|
|
21
|
+
* rendering is used.
|
|
22
|
+
*/
|
|
23
|
+
export const WORKSPACE_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
24
|
+
/** Names are ASCII by grammar, so length in characters equals length in bytes. */
|
|
25
|
+
export const WORKSPACE_ENV_MAX_NAME_LENGTH = 128;
|
|
26
|
+
/** Maximum UTF-8 bytes per value. Values may be empty and may contain newlines, never NUL. */
|
|
27
|
+
export const WORKSPACE_ENV_MAX_VALUE_BYTES = 4096;
|
|
28
|
+
export const WORKSPACE_ENV_MAX_ENTRIES = 128;
|
|
29
|
+
/** Maximum total UTF-8 bytes across all names and values of one map. */
|
|
30
|
+
export const WORKSPACE_ENV_MAX_TOTAL_BYTES = 32768;
|
|
31
|
+
const RESERVED_PREFIXES = [
|
|
32
|
+
["SEALANT_", "platform-prefix"],
|
|
33
|
+
["LD_", "dynamic-loader"],
|
|
34
|
+
["DYLD_", "dynamic-loader"],
|
|
35
|
+
["GIT_CONFIG_KEY_", "git-ssh"],
|
|
36
|
+
["GIT_CONFIG_VALUE_", "git-ssh"],
|
|
37
|
+
];
|
|
38
|
+
const RESERVED_NAMES = new Map([
|
|
39
|
+
// Identity/process roots the daemon and shells own.
|
|
40
|
+
...[
|
|
41
|
+
"HOME",
|
|
42
|
+
"PATH",
|
|
43
|
+
"USER",
|
|
44
|
+
"LOGNAME",
|
|
45
|
+
"SHELL",
|
|
46
|
+
"PWD",
|
|
47
|
+
"OLDPWD",
|
|
48
|
+
"SHLVL",
|
|
49
|
+
"TERM",
|
|
50
|
+
"COLORTERM",
|
|
51
|
+
"_",
|
|
52
|
+
].map((name) => [name, "process-identity"]),
|
|
53
|
+
// Runtime/network ownership (lowercase variants are caught by the uppercase comparison).
|
|
54
|
+
...[
|
|
55
|
+
"DOCKER_HOST",
|
|
56
|
+
"DOCKER_TLS_CERTDIR",
|
|
57
|
+
"HTTP_PROXY",
|
|
58
|
+
"HTTPS_PROXY",
|
|
59
|
+
"ALL_PROXY",
|
|
60
|
+
"NO_PROXY",
|
|
61
|
+
].map((name) => [name, "runtime-network"]),
|
|
62
|
+
// Connected-account identity and config-lookup roots.
|
|
63
|
+
...[
|
|
64
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
65
|
+
"GITHUB_TOKEN",
|
|
66
|
+
"GH_TOKEN",
|
|
67
|
+
"XDG_CONFIG_HOME",
|
|
68
|
+
"CLAUDE_CONFIG_DIR",
|
|
69
|
+
"CODEX_HOME",
|
|
70
|
+
].map((name) => [name, "account-lookup"]),
|
|
71
|
+
["GLIBC_TUNABLES", "dynamic-loader"],
|
|
72
|
+
// Shell startup/control.
|
|
73
|
+
...["BASH_ENV", "ENV", "ZDOTDIR", "PROMPT_COMMAND", "PS4", "SHELLOPTS"].map((name) => [name, "shell-startup"]),
|
|
74
|
+
// Runtime-wide code injection.
|
|
75
|
+
...[
|
|
76
|
+
"NODE_OPTIONS",
|
|
77
|
+
"PYTHONPATH",
|
|
78
|
+
"PYTHONSTARTUP",
|
|
79
|
+
"RUBYOPT",
|
|
80
|
+
"PERL5OPT",
|
|
81
|
+
"JAVA_TOOL_OPTIONS",
|
|
82
|
+
].map((name) => [name, "runtime-injection"]),
|
|
83
|
+
// Git/SSH identity and config controls.
|
|
84
|
+
...[
|
|
85
|
+
"SSH_AUTH_SOCK",
|
|
86
|
+
"GIT_SSH",
|
|
87
|
+
"GIT_SSH_COMMAND",
|
|
88
|
+
"GIT_CONFIG_GLOBAL",
|
|
89
|
+
"GIT_CONFIG_SYSTEM",
|
|
90
|
+
"GIT_CONFIG_NOSYSTEM",
|
|
91
|
+
"GIT_CONFIG_COUNT",
|
|
92
|
+
].map((name) => [name, "git-ssh"]),
|
|
93
|
+
]);
|
|
94
|
+
/** sealantd's secret markers, matched as substrings of the ASCII-uppercased name. */
|
|
95
|
+
export const WORKSPACE_ENV_SECRET_MARKERS = [
|
|
96
|
+
"TOKEN",
|
|
97
|
+
"SECRET",
|
|
98
|
+
"PASSWORD",
|
|
99
|
+
"PASSWD",
|
|
100
|
+
"CREDENTIAL",
|
|
101
|
+
"APIKEY",
|
|
102
|
+
];
|
|
103
|
+
const toAsciiUpper = (name) => name.replace(/[a-z]/g, (char) => String.fromCharCode(char.charCodeAt(0) - 32));
|
|
104
|
+
/**
|
|
105
|
+
* The reserved rule a (grammar-valid) name violates, or undefined when the name is allowed.
|
|
106
|
+
* Matching is ASCII-uppercase so `http_proxy` cannot bypass `HTTP_PROXY`.
|
|
107
|
+
*/
|
|
108
|
+
export const findWorkspaceEnvReservedRule = (name) => {
|
|
109
|
+
const upper = toAsciiUpper(name);
|
|
110
|
+
for (const [prefix, rule] of RESERVED_PREFIXES) {
|
|
111
|
+
if (upper.startsWith(prefix)) {
|
|
112
|
+
return rule;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const exact = RESERVED_NAMES.get(upper);
|
|
116
|
+
if (exact !== undefined) {
|
|
117
|
+
return exact;
|
|
118
|
+
}
|
|
119
|
+
// Mirror of sealantd's is_secret_key: substring markers, `_KEY` suffix, bare `KEY`. Broader than
|
|
120
|
+
// it looks on purpose — `TOKENIZER_PATH` matches `TOKEN` — because the daemon's filter would
|
|
121
|
+
// silently drop those names, and a loud rejection here beats a variable that never appears.
|
|
122
|
+
if (WORKSPACE_ENV_SECRET_MARKERS.some((marker) => upper.includes(marker))) {
|
|
123
|
+
return "secret-marker";
|
|
124
|
+
}
|
|
125
|
+
if (upper === "KEY" || upper.endsWith("_KEY")) {
|
|
126
|
+
return "secret-marker";
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
};
|
|
130
|
+
const boundedNameDisplay = (name) => {
|
|
131
|
+
const head = [...name].slice(0, 32).join("");
|
|
132
|
+
return JSON.stringify(head.length < name.length ? `${head}…` : head);
|
|
133
|
+
};
|
|
134
|
+
const utf8Bytes = (value) => new TextEncoder().encode(value).length;
|
|
135
|
+
/**
|
|
136
|
+
* Validate a caller-supplied environment map against the full policy. On success the returned map
|
|
137
|
+
* is re-serialized in name order (deterministic for tests and stored specs). Collects every issue
|
|
138
|
+
* rather than stopping at the first, so a settings UI can annotate all rows in one round trip.
|
|
139
|
+
* Duplicate names cannot be detected here — a parsed JS object has already collapsed them; owners
|
|
140
|
+
* of the pre-parse representation (databases, forms) must enforce uniqueness themselves.
|
|
141
|
+
*/
|
|
142
|
+
export const parseWorkspaceEnv = (input) => parseEnvMap(input, "env");
|
|
143
|
+
/**
|
|
144
|
+
* Validate a caller-supplied SECRET environment map — the transient secret channel
|
|
145
|
+
* (`CreateOptions.secretEnv`): same grammar and size bounds as `env`, and the same platform-owned
|
|
146
|
+
* name classes are reserved, but secret-shaped names are exactly what belongs here, so the
|
|
147
|
+
* `secret-marker` rule does not apply. Account-lookup names (`GITHUB_TOKEN`,
|
|
148
|
+
* `CLAUDE_CODE_OAUTH_TOKEN`, …) stay reserved: connected accounts own those.
|
|
149
|
+
*/
|
|
150
|
+
export const parseWorkspaceSecretEnv = (input) => parseEnvMap(input, "secretEnv");
|
|
151
|
+
/** Reserved-rule lookup for the secret lane: everything but `secret-marker`. */
|
|
152
|
+
export const findWorkspaceSecretEnvReservedRule = (name) => {
|
|
153
|
+
const rule = findWorkspaceEnvReservedRule(name);
|
|
154
|
+
return rule === "secret-marker" ? undefined : rule;
|
|
155
|
+
};
|
|
156
|
+
const parseEnvMap = (input, lane) => {
|
|
157
|
+
const findReserved = lane === "env" ? findWorkspaceEnvReservedRule : findWorkspaceSecretEnvReservedRule;
|
|
158
|
+
const issues = [];
|
|
159
|
+
const entries = Object.entries(input);
|
|
160
|
+
if (entries.length > WORKSPACE_ENV_MAX_ENTRIES) {
|
|
161
|
+
issues.push({ rule: "entry-count", entryCount: entries.length });
|
|
162
|
+
}
|
|
163
|
+
let totalBytes = 0;
|
|
164
|
+
for (const [index, [name, value]] of entries.entries()) {
|
|
165
|
+
if (!WORKSPACE_ENV_NAME_PATTERN.test(name)) {
|
|
166
|
+
issues.push({ rule: "name-grammar", index, nameDisplay: boundedNameDisplay(name) });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (name.length > WORKSPACE_ENV_MAX_NAME_LENGTH) {
|
|
170
|
+
issues.push({ rule: "name-length", index, nameDisplay: boundedNameDisplay(name) });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const reservedRule = findReserved(name);
|
|
174
|
+
if (reservedRule !== undefined) {
|
|
175
|
+
issues.push({ rule: "name-reserved", name, reservedRule });
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (value.includes("\u0000")) {
|
|
179
|
+
issues.push({ rule: "value-nul", name });
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const valueBytes = utf8Bytes(value);
|
|
183
|
+
if (valueBytes > WORKSPACE_ENV_MAX_VALUE_BYTES) {
|
|
184
|
+
issues.push({ rule: "value-size", name, valueBytes });
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
totalBytes += name.length + valueBytes;
|
|
188
|
+
}
|
|
189
|
+
if (totalBytes > WORKSPACE_ENV_MAX_TOTAL_BYTES) {
|
|
190
|
+
issues.push({ rule: "total-size", totalBytes });
|
|
191
|
+
}
|
|
192
|
+
if (issues.length > 0) {
|
|
193
|
+
return { ok: false, issues };
|
|
194
|
+
}
|
|
195
|
+
const env = {};
|
|
196
|
+
for (const name of Object.keys(input).toSorted()) {
|
|
197
|
+
env[name] = input[name] ?? "";
|
|
198
|
+
}
|
|
199
|
+
return { ok: true, env };
|
|
200
|
+
};
|
|
201
|
+
const RESERVED_RULE_MESSAGES = {
|
|
202
|
+
"platform-prefix": "the SEALANT_ prefix is platform-owned",
|
|
203
|
+
"process-identity": "it is a process-identity variable owned by the workspace runtime",
|
|
204
|
+
"runtime-network": "it controls runtime or network routing",
|
|
205
|
+
"account-lookup": "it controls connected-account identity or configuration lookup",
|
|
206
|
+
"dynamic-loader": "it controls the dynamic loader",
|
|
207
|
+
"shell-startup": "it controls shell startup",
|
|
208
|
+
"runtime-injection": "it injects code into a language runtime",
|
|
209
|
+
"git-ssh": "it controls Git or SSH identity and configuration",
|
|
210
|
+
"secret-marker": "secret-looking names (containing TOKEN, SECRET, PASSWORD, PASSWD, CREDENTIAL, or APIKEY, ending in _KEY, or exactly KEY) are filtered by the workspace runtime and would never reach any process; environment variables are for ordinary non-secret configuration",
|
|
211
|
+
};
|
|
212
|
+
/** Human-readable, value-free rendering of one issue; identical client- and server-side. */
|
|
213
|
+
export const formatWorkspaceEnvIssue = (issue) => {
|
|
214
|
+
switch (issue.rule) {
|
|
215
|
+
case "name-grammar":
|
|
216
|
+
return `env entry ${issue.index} (${issue.nameDisplay}): names must match [A-Za-z_][A-Za-z0-9_]*`;
|
|
217
|
+
case "name-length":
|
|
218
|
+
return `env entry ${issue.index} (${issue.nameDisplay}): names must be at most ${WORKSPACE_ENV_MAX_NAME_LENGTH} characters`;
|
|
219
|
+
case "name-reserved":
|
|
220
|
+
return `env name ${issue.name} is reserved: ${RESERVED_RULE_MESSAGES[issue.reservedRule]}`;
|
|
221
|
+
case "value-nul":
|
|
222
|
+
return `env value for ${issue.name} must not contain NUL`;
|
|
223
|
+
case "value-size":
|
|
224
|
+
return `env value for ${issue.name} is ${issue.valueBytes} bytes; the maximum is ${WORKSPACE_ENV_MAX_VALUE_BYTES}`;
|
|
225
|
+
case "entry-count":
|
|
226
|
+
return `env has ${issue.entryCount} entries; the maximum is ${WORKSPACE_ENV_MAX_ENTRIES}`;
|
|
227
|
+
case "total-size":
|
|
228
|
+
return `env totals ${issue.totalBytes} bytes across names and values; the maximum is ${WORKSPACE_ENV_MAX_TOTAL_BYTES}`;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* Secret names the CONTROL PLANE itself adds to the sealed secret channel beside the caller's map.
|
|
233
|
+
* They carry the `SEALANT_` prefix — platform-owned, so no caller lane can supply them — and are
|
|
234
|
+
* consumed by `sealantd boot` rather than handed to workspace processes. Today: the capture
|
|
235
|
+
* source's session credential (sealantd ADR-0015), which the create request carries as
|
|
236
|
+
* `captureToken` and the daemon reads from its boot secret file as `SEALANT_CAPTURE_TOKEN`.
|
|
237
|
+
*/
|
|
238
|
+
export const CAPTURE_TOKEN_SECRET_ENV_NAME = "SEALANT_CAPTURE_TOKEN";
|
|
239
|
+
export const PLATFORM_SECRET_ENV_NAMES = new Set([
|
|
240
|
+
CAPTURE_TOKEN_SECRET_ENV_NAME,
|
|
241
|
+
]);
|
|
242
|
+
/**
|
|
243
|
+
* Separate the platform-owned entries of a sealed secret map from the caller's so the last-hop
|
|
244
|
+
* re-validation applies the caller policy to the caller's lane only. Any other `SEALANT_`-prefixed
|
|
245
|
+
* name stays in the caller lane and is rejected there, as before.
|
|
246
|
+
*/
|
|
247
|
+
export const splitPlatformSecretEnv = (input) => {
|
|
248
|
+
const callerEnv = {};
|
|
249
|
+
const platformEnv = {};
|
|
250
|
+
for (const [name, value] of Object.entries(input)) {
|
|
251
|
+
if (PLATFORM_SECRET_ENV_NAMES.has(name)) {
|
|
252
|
+
platformEnv[name] = value;
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
callerEnv[name] = value;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return { callerEnv, platformEnv };
|
|
259
|
+
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sealant/api-contracts-next",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
-
},
|
|
9
|
-
"keywords": [],
|
|
10
|
-
"author": "",
|
|
3
|
+
"version": "0.39.0-next.682",
|
|
4
|
+
"description": "Contract-first HTTP API definitions for Sealant services",
|
|
11
5
|
"license": "Apache-2.0",
|
|
12
|
-
"
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/sealant-sh/sealant.git",
|
|
9
|
+
"directory": "packages/api-contracts"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./workspace-environment": {
|
|
18
|
+
"types": "./dist/workspace-environment.d.ts",
|
|
19
|
+
"import": "./dist/workspace-environment.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"effect": ">=4.0.0-beta.85 <5"
|
|
30
|
+
},
|
|
31
|
+
"gitHead": "ef9e5377ecec37e1257a7a4d5048e79c5322a500",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
}
|
|
13
35
|
}
|