@landstrip/landstrip-api 0.19.12 → 0.19.13

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/README.md CHANGED
@@ -40,6 +40,15 @@ Descriptor lifetimes and the reply protocol are covered in the manual.
40
40
  Integration helpers are exported through [`/shared`](lib/shared.d.ts) and
41
41
  [`/proxy`](lib/proxy.d.ts).
42
42
 
43
+ `decodeLandstripTrap(value)` and `parseTrapLine(line)` from `/shared` return a
44
+ validated `LandstripTrap`, or `null` for malformed input. Extra fields are
45
+ preserved. Missing or unknown denial states are normalized to `info`, never
46
+ queries; query IDs must be decimal `u64` strings and nonzero for queries.
47
+ `isQueryTrap(trap)` narrows validated events to `LandstripQueryTrap` with
48
+ `state: 'query'`. The declarations include all five native mechanisms and the
49
+ current Windows error codes; the previously declared `POLICY_TCP_BIND_UNSUPPORTED`
50
+ code remains accepted alongside `POLICY_LOCAL_BINDING_UNSUPPORTED`.
51
+
43
52
  `writeLandstripPolicyFile(policy, prefix = 'landstrip-')` from `/shared` writes
44
53
  `policy.json` and returns `{ dir, path }`. The caller owns `dir` and must remove it
45
54
  with `rmSync(dir, { recursive: true, force: true })` after use. Serialization
package/lib/index.d.ts CHANGED
@@ -5,7 +5,8 @@ export type LandstripMechanism =
5
5
  | 'landlock'
6
6
  | 'seccomp'
7
7
  | 'seatbelt'
8
- | 'appcontainer';
8
+ | 'appcontainer'
9
+ | 'windowsuser';
9
10
 
10
11
  /**
11
12
  * `query` holds the syscall until the launcher answers with a
@@ -86,6 +87,7 @@ export type LandstripPolicyErrorCode =
86
87
  | 'POLICY_DENY_READ_ALWAYS_UNSUPPORTED'
87
88
  | 'POLICY_DENY_WRITE_ALWAYS_UNSUPPORTED'
88
89
  | 'POLICY_TCP_BIND_UNSUPPORTED'
90
+ | 'POLICY_LOCAL_BINDING_UNSUPPORTED'
89
91
  | 'POLICY_UNIX_SOCKET_UNSUPPORTED'
90
92
  | 'POLICY_UNIX_SOCKET_PATH'
91
93
  | 'POLICY_DENY_WRITE_SYMLINK_ANCESTOR'
@@ -99,6 +101,7 @@ export type LandstripInternalCode =
99
101
  | LandstripPolicyErrorCode
100
102
  | 'SANDBOX_SETUP_FAILED'
101
103
  | 'SUPERVISE_FAILED'
104
+ | 'HOST_JOB_INCOMPATIBLE'
102
105
  | 'PLATFORM_UNSUPPORTED'
103
106
  | 'INTEGER_TOO_LARGE'
104
107
  | 'INTERNAL_ERROR';
package/lib/shared.d.ts CHANGED
@@ -24,6 +24,7 @@ import type {
24
24
  } from './index.js';
25
25
 
26
26
  export type LandstripDenialTrap = LandstripFilesystemTrap | LandstripNetworkTrap;
27
+ export type LandstripQueryTrap = LandstripDenialTrap & { state: 'query' };
27
28
 
28
29
  export type {
29
30
  SandboxConfig,
@@ -78,7 +79,7 @@ export function isFilesystemTrap(trap: LandstripTrap): trap is LandstripFilesyst
78
79
 
79
80
  export function isDenialTrap(trap: LandstripTrap): trap is LandstripDenialTrap;
80
81
 
81
- export function isQueryTrap(trap: LandstripTrap): trap is LandstripDenialTrap;
82
+ export function isQueryTrap(trap: LandstripTrap): trap is LandstripQueryTrap;
82
83
 
83
84
  export function formatLandstripTrap(trap: LandstripTrap): string;
84
85
 
@@ -222,7 +223,11 @@ export function extractCandidatePaths(command: string): string[];
222
223
 
223
224
  export function extractNativeDeniedPath(output: string, cwd?: string): string | null;
224
225
 
225
- export function extractNativeWriteDeniedPath(output: string, cwd?: string): string | null;
226
+ export function extractNativeWriteDeniedPath(
227
+ output: string,
228
+ cwd?: string,
229
+ command?: string,
230
+ ): string | null;
226
231
 
227
232
  export interface ExtractDeniedPathOptions {
228
233
  cwd?: string;
package/lib/shared.js CHANGED
@@ -122,51 +122,115 @@ function allowsAllDomains(allowedDomains) {
122
122
  return allowedDomains.includes('*');
123
123
  }
124
124
 
125
- // landstrip emits each trap as a flat JSON record tagged by a `kind`
126
- // discriminant (`filesystem`, `network`, `launch`, `usage`, `internal`)
127
- // alongside a stable `code` and variant-specific fields. The declarations it
128
- // ships are erased at compile time, so validate the fields callers read
129
- // before trusting a decoded line. `state` is deliberately not validated: a
130
- // missing or unknown state degrades to "informational", the safe direction.
131
- // `query_id` must be a string — landstrip < 0.17 sent a number, and a
132
- // numeric id fails its own deserializer when echoed back.
133
- const LANDSTRIP_OPERATIONS = new Set(['read', 'write']);
125
+ const LANDSTRIP_MECHANISMS = new Set([
126
+ 'landlock',
127
+ 'seccomp',
128
+ 'seatbelt',
129
+ 'appcontainer',
130
+ 'windowsuser',
131
+ ]);
132
+ const LANDSTRIP_INTERNAL_CODES = new Set([
133
+ 'POLICY_PARSE_FAILED',
134
+ 'POLICY_IO_FAILED',
135
+ 'POLICY_UNRESTRICTED_READ',
136
+ 'POLICY_DENY_READ_ALWAYS_UNSUPPORTED',
137
+ 'POLICY_DENY_WRITE_ALWAYS_UNSUPPORTED',
138
+ 'POLICY_TCP_BIND_UNSUPPORTED',
139
+ 'POLICY_LOCAL_BINDING_UNSUPPORTED',
140
+ 'POLICY_UNIX_SOCKET_UNSUPPORTED',
141
+ 'POLICY_UNIX_SOCKET_PATH',
142
+ 'POLICY_DENY_WRITE_SYMLINK_ANCESTOR',
143
+ 'POLICY_INVALID_PORT',
144
+ 'POLICY_EMPTY_PATH',
145
+ 'POLICY_HOME_UNAVAILABLE',
146
+ 'POLICY_TRAVERSAL_DEPTH',
147
+ 'SUPERVISE_FAILED',
148
+ 'HOST_JOB_INCOMPATIBLE',
149
+ 'PLATFORM_UNSUPPORTED',
150
+ 'INTEGER_TOO_LARGE',
151
+ 'INTERNAL_ERROR',
152
+ ]);
153
+
154
+ function isDenialRecord(value) {
155
+ return (
156
+ typeof value.query_id === 'string' &&
157
+ /^(0|[1-9][0-9]{0,19})$/.test(value.query_id) &&
158
+ BigInt(value.query_id) <= 18446744073709551615n &&
159
+ (value.state !== 'query' || value.query_id !== '0') &&
160
+ typeof value.syscall === 'string' &&
161
+ typeof value.errno === 'string' &&
162
+ value.mechanism === 'seccomp' &&
163
+ isRecord(value.process) &&
164
+ Number.isInteger(value.process.pid) &&
165
+ value.process.pid >= 0 &&
166
+ value.process.pid <= 4294967295 &&
167
+ (value.process.exe === null || typeof value.process.exe === 'string') &&
168
+ (value.process.cwd === null || typeof value.process.cwd === 'string')
169
+ );
170
+ }
134
171
 
135
172
  function isLandstripTrap(value) {
136
173
  if (!isRecord(value)) return false;
137
174
 
138
175
  switch (value.kind) {
139
- case 'filesystem':
176
+ case 'filesystem': {
177
+ if (!Array.isArray(value.flags)) return false;
178
+ for (const flag of value.flags) {
179
+ if (typeof flag !== 'string') return false;
180
+ }
181
+ const grant = value.operation === 'read' ? 'allowRead' : 'allowWrite';
182
+ const otherGrant = value.operation === 'read' ? 'allowWrite' : 'allowRead';
140
183
  return (
141
- LANDSTRIP_OPERATIONS.has(value.operation) &&
184
+ value.code === 'FILESYSTEM_DENIED' &&
185
+ (value.operation === 'read' || value.operation === 'write') &&
142
186
  typeof value.path === 'string' &&
143
- typeof value.query_id === 'string'
187
+ typeof value.requested_path === 'string' &&
188
+ (value.reason === 'allow_miss' || value.reason === 'deny_match') &&
189
+ isRecord(value.suggested_grant) &&
190
+ typeof value.suggested_grant[grant] === 'string' &&
191
+ value.suggested_grant[otherGrant] === undefined &&
192
+ isDenialRecord(value)
144
193
  );
194
+ }
145
195
  case 'network':
146
196
  return (
147
- typeof value.operation === 'string' &&
197
+ value.code === 'NETWORK_DENIED' &&
198
+ (value.operation === 'connect' || value.operation === 'bind') &&
148
199
  typeof value.target === 'string' &&
149
- typeof value.query_id === 'string'
200
+ isDenialRecord(value)
150
201
  );
151
202
  case 'launch':
152
- return typeof value.program === 'string' && typeof value.message === 'string';
203
+ return (
204
+ value.code === 'LAUNCH_FAILED' &&
205
+ typeof value.program === 'string' &&
206
+ typeof value.message === 'string' &&
207
+ (value.errno === undefined || typeof value.errno === 'string')
208
+ );
153
209
  case 'usage':
154
- return typeof value.message === 'string';
210
+ return value.code === 'USAGE_ERROR' && typeof value.message === 'string';
155
211
  case 'internal':
156
- return typeof value.code === 'string' && typeof value.message === 'string';
212
+ return (
213
+ typeof value.message === 'string' &&
214
+ (value.code === 'SANDBOX_SETUP_FAILED'
215
+ ? LANDSTRIP_MECHANISMS.has(value.mechanism)
216
+ : LANDSTRIP_INTERNAL_CODES.has(value.code) && value.mechanism === undefined)
217
+ );
157
218
  default:
158
219
  return false;
159
220
  }
160
221
  }
161
222
 
162
223
  function decodeLandstripTrap(value) {
163
- return isLandstripTrap(value) ? value : null;
224
+ if (!isLandstripTrap(value)) return null;
225
+ if (isDenialTrap(value) && value.state !== 'query' && value.state !== 'info') {
226
+ return { ...value, state: 'info' };
227
+ }
228
+ return value;
164
229
  }
165
230
 
166
231
  function parseTrapLine(line) {
167
232
  try {
168
- const parsed = JSON.parse(line);
169
- return isLandstripTrap(parsed) ? parsed : null;
233
+ return decodeLandstripTrap(JSON.parse(line));
170
234
  } catch {
171
235
  return null;
172
236
  }
@@ -928,7 +992,37 @@ function extractNativeDeniedPath(output, cwd = process.cwd()) {
928
992
  return null;
929
993
  }
930
994
 
931
- function extractNativeWriteDeniedPath(output, cwd = process.cwd()) {
995
+ const nativeWriteTools = new Set(['mkdir', 'rm', 'rmdir', 'tee', 'touch', 'truncate', 'unlink']);
996
+ const nativeShellTools = new Set(['bash', 'dash', 'fish', 'ksh', 'sh', 'zsh']);
997
+ const nativeWriteSyscalls = new Set([
998
+ 'chmod',
999
+ 'chown',
1000
+ 'copyfile',
1001
+ 'link',
1002
+ 'mkdir',
1003
+ 'mkdtemp',
1004
+ 'rename',
1005
+ 'rmdir',
1006
+ 'symlink',
1007
+ 'truncate',
1008
+ 'unlink',
1009
+ 'utimes',
1010
+ 'write',
1011
+ ]);
1012
+
1013
+ function commandRedirectsToPath(command, path, cwd) {
1014
+ if (typeof command !== 'string') return false;
1015
+ const redirects = command.matchAll(
1016
+ /(?:[0-9]*>>?|&>>?|>\|)\s*(?:'([^'\r\n]+)'|"([^"\r\n]+)"|\x60([^\x60\r\n]+)\x60|([^\s'"\x60;&|<>]+))/g,
1017
+ );
1018
+ for (const redirect of redirects) {
1019
+ const target = redirect[1] ?? redirect[2] ?? redirect[3] ?? redirect[4];
1020
+ if (target !== undefined && normalizePathMatch(target, cwd) === path) return true;
1021
+ }
1022
+ return false;
1023
+ }
1024
+
1025
+ function extractNativeWriteDeniedPath(output, cwd = process.cwd(), command) {
932
1026
  if (typeof output !== 'string') return null;
933
1027
  const denial = String.raw`(?:Access is denied\.?|Operation not permitted|Permission denied)`;
934
1028
  let match = output.match(
@@ -956,6 +1050,29 @@ function extractNativeWriteDeniedPath(output, cwd = process.cwd()) {
956
1050
  );
957
1051
  if (match) return normalizePathMatch(match[1], cwd);
958
1052
 
1053
+ match = output.match(
1054
+ new RegExp(
1055
+ String.raw`^([^\s:]+):\s+((?:[a-zA-Z]:[\\/]|\\\\|\/|\.{1,2}[\\/])[^:\r\n]*?):\s+${denial}$`,
1056
+ 'im',
1057
+ ),
1058
+ );
1059
+ if (match) {
1060
+ const tool = basename(match[1]);
1061
+ const path = normalizePathMatch(match[2], cwd);
1062
+ if (path && nativeWriteTools.has(tool)) return path;
1063
+ if (path && nativeShellTools.has(tool) && commandRedirectsToPath(command, path, cwd)) return path;
1064
+ }
1065
+
1066
+ match = output.match(
1067
+ new RegExp(
1068
+ String.raw`(?:EPERM|EACCES):\s*(?:operation not permitted|permission denied),\s*([a-z]+)\s+['"\x60]([^'"\x60\r\n]+)['"\x60]`,
1069
+ 'i',
1070
+ ),
1071
+ );
1072
+ if (match && nativeWriteSyscalls.has(match[1].toLowerCase())) {
1073
+ return normalizePathMatch(match[2], cwd);
1074
+ }
1075
+
959
1076
  return null;
960
1077
  }
961
1078
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@landstrip/landstrip-api",
3
- "version": "0.19.12",
3
+ "version": "0.19.13",
4
4
  "description": "Sandbox runner using Landlock, Seatbelt, and AppContainer",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/landstrip/landstrip#readme",
@@ -40,12 +40,12 @@
40
40
  "ipaddr.js": "^2.5.0"
41
41
  },
42
42
  "optionalDependencies": {
43
- "@landstrip/landstrip-darwin-arm64": "0.19.12",
44
- "@landstrip/landstrip-darwin-x64": "0.19.12",
45
- "@landstrip/landstrip-linux-x64": "0.19.12",
46
- "@landstrip/landstrip-linux-arm64": "0.19.12",
47
- "@landstrip/landstrip-win32-x64": "0.19.12",
48
- "@landstrip/landstrip-win32-arm64": "0.19.12"
43
+ "@landstrip/landstrip-darwin-arm64": "0.19.13",
44
+ "@landstrip/landstrip-darwin-x64": "0.19.13",
45
+ "@landstrip/landstrip-linux-x64": "0.19.13",
46
+ "@landstrip/landstrip-linux-arm64": "0.19.13",
47
+ "@landstrip/landstrip-win32-x64": "0.19.13",
48
+ "@landstrip/landstrip-win32-arm64": "0.19.13"
49
49
  },
50
50
  "publishConfig": {
51
51
  "access": "public"