@wrongstack/plugin-sdk 0.308.7
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 +21 -0
- package/README.md +94 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +7 -0
- package/dist/runtime/bounded-map.d.ts +86 -0
- package/dist/runtime/credential-patterns.d.ts +42 -0
- package/dist/runtime/h1-state.d.ts +62 -0
- package/dist/runtime/handles.d.ts +46 -0
- package/dist/runtime/index.d.ts +183 -0
- package/dist/runtime/llm.d.ts +44 -0
- package/dist/runtime/local-bin.d.ts +120 -0
- package/dist/runtime/redos-guard.d.ts +69 -0
- package/dist/runtime/safe-json.d.ts +25 -0
- package/dist/runtime/sandbox.d.ts +59 -0
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +1004 -0
- package/package.json +42 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wrongstack/plugins — project-local binary resolution.
|
|
3
|
+
*
|
|
4
|
+
* Why this module exists
|
|
5
|
+
* ----------------------
|
|
6
|
+
* Several plugins need to run a Node CLI that ships with the project
|
|
7
|
+
* (`biome`, `eslint`, `tsc`, `vitest`, …). The obvious spelling —
|
|
8
|
+
* `execFile('npx', ['biome', …])` — is wrong in three separate ways:
|
|
9
|
+
*
|
|
10
|
+
* 1. **It does not run on Windows at all.** `npx`/`pnpm`/`npm` are
|
|
11
|
+
* `.cmd` shims there, and `execFile`/`spawn` without a shell do not
|
|
12
|
+
* consult `PATHEXT`. The call fails `ENOENT`, plugins swallow the
|
|
13
|
+
* error as "tool not installed", and the feature silently no-ops on
|
|
14
|
+
* every Windows machine.
|
|
15
|
+
* 2. **It is slow.** `npx` re-resolves the package on each invocation,
|
|
16
|
+
* and for a missing package it may try to *download* one — on a hook
|
|
17
|
+
* that fires after every write.
|
|
18
|
+
* 3. **It is a weaker sandbox.** The shim is a shell script; arguments
|
|
19
|
+
* cross a `cmd.exe` boundary on Windows (the BatBadBut class).
|
|
20
|
+
*
|
|
21
|
+
* `lint-gate` and `test-flake-detector` already solved this locally by
|
|
22
|
+
* resolving the package's `bin` entry through `createRequire` and running
|
|
23
|
+
* it as `process.execPath <entry>`. That is the correct pattern: no shell,
|
|
24
|
+
* no shim, no network, identical on every platform. This module promotes
|
|
25
|
+
* that proven approach to a shared helper so every plugin gets it.
|
|
26
|
+
*
|
|
27
|
+
* Fallback: when the package is genuinely absent from the project,
|
|
28
|
+
* `resolveNodeBin` returns `null` and the caller decides whether to
|
|
29
|
+
* degrade gracefully or fall back to a PATH lookup via
|
|
30
|
+
* `resolveWin32Command` (exported here for that purpose).
|
|
31
|
+
*/
|
|
32
|
+
import { resolveWin32Command } from '@wrongstack/tools/win32';
|
|
33
|
+
export { resolveWin32Command };
|
|
34
|
+
/** A spawn-ready invocation, already adjusted for the host platform. */
|
|
35
|
+
export interface ExecInvocation {
|
|
36
|
+
cmd: string;
|
|
37
|
+
args: string[];
|
|
38
|
+
/** Only ever true on the Windows `.cmd`/`.bat` shim path. */
|
|
39
|
+
windowsVerbatimArguments: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Turn a `(command, args)` pair into something `execFile`/`spawn` can
|
|
43
|
+
* actually launch on this platform.
|
|
44
|
+
*
|
|
45
|
+
* On Windows, `npx`/`npm`/`pnpm`/`biome`/`tsc`/… ship as `.cmd` wrappers.
|
|
46
|
+
* `execFile` and `spawn` without a shell ignore `PATHEXT`, so a bare
|
|
47
|
+
* `execFile('npx', …)` fails `ENOENT` — and because every plugin treats a
|
|
48
|
+
* spawn failure as "tool not installed", the feature silently no-ops on
|
|
49
|
+
* every Windows machine. This resolves the real path first and, for a
|
|
50
|
+
* `.cmd`/`.bat` shim, routes through `cmd.exe` with per-argument quoting
|
|
51
|
+
* and a metacharacter guard (the BatBadBut argument-injection class), so
|
|
52
|
+
* a dynamic path argument still cannot chain a second command.
|
|
53
|
+
*
|
|
54
|
+
* On non-Windows, and for real `.exe` binaries, this is a passthrough.
|
|
55
|
+
*
|
|
56
|
+
* Throws when an argument carries a `cmd.exe` metacharacter on the shim
|
|
57
|
+
* path — callers should treat that as "skip", never as "run anyway".
|
|
58
|
+
*
|
|
59
|
+
* Prefer {@link resolveNodeBin} when the target is a Node CLI that the
|
|
60
|
+
* project depends on: `node <bin-entry>` needs no shim at all.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveExecInvocation(command: string, args?: readonly string[]): ExecInvocation;
|
|
63
|
+
/**
|
|
64
|
+
* Locate `cmd` on `PATH`, or return `null` if it is not there.
|
|
65
|
+
*
|
|
66
|
+
* This is the existence check `resolveWin32Command` deliberately does not
|
|
67
|
+
* provide: that function returns its input unchanged when nothing matches,
|
|
68
|
+
* so a caller cannot distinguish "found `biome`" from "gave up and handed
|
|
69
|
+
* back the string `biome`". Treating the passthrough as success makes a
|
|
70
|
+
* missing tool look installed — the caller then spawns it, gets `ENOENT`,
|
|
71
|
+
* and (because plugins read a spawn failure as "not installed") silently
|
|
72
|
+
* does nothing while reporting itself healthy.
|
|
73
|
+
*
|
|
74
|
+
* Returns the resolved path on success. Walks `PATH` directly, applying
|
|
75
|
+
* `PATHEXT` suffixes on Windows for a bare name and checking the execute
|
|
76
|
+
* bit on POSIX.
|
|
77
|
+
*/
|
|
78
|
+
export declare function findOnPath(cmd: string): string | null;
|
|
79
|
+
/** A resolved, directly-spawnable invocation. Never a shell or a shim. */
|
|
80
|
+
export interface ResolvedNodeBin {
|
|
81
|
+
/** Always `process.execPath` — the Node binary running this process. */
|
|
82
|
+
cmd: string;
|
|
83
|
+
/** `[binEntryPath, ...extraArgs]`. */
|
|
84
|
+
args: string[];
|
|
85
|
+
/** Absolute path of the package's bin entry, for diagnostics. */
|
|
86
|
+
entry: string;
|
|
87
|
+
}
|
|
88
|
+
/** Drop every cached resolution. Call from `teardown()`. */
|
|
89
|
+
export declare function clearLocalBinCache(): void;
|
|
90
|
+
/**
|
|
91
|
+
* Resolve a project-local Node CLI to a `process.execPath <entry>`
|
|
92
|
+
* invocation.
|
|
93
|
+
*
|
|
94
|
+
* @param packageName npm package to resolve, e.g. `@biomejs/biome`.
|
|
95
|
+
* @param binName which `bin` key to prefer when the package declares
|
|
96
|
+
* several. Falls back to the sole/first entry.
|
|
97
|
+
* @param cwd project root whose `package.json` anchors resolution.
|
|
98
|
+
* @param extraArgs arguments appended after the bin entry.
|
|
99
|
+
*
|
|
100
|
+
* Returns `null` when the package is not installed, declares no `bin`, or
|
|
101
|
+
* the declared entry escapes its own package directory (a tampered
|
|
102
|
+
* `package.json` must not become an arbitrary-file execution primitive).
|
|
103
|
+
*/
|
|
104
|
+
export declare function resolveNodeBin(packageName: string, binName: string, cwd: string, extraArgs?: readonly string[]): ResolvedNodeBin | null;
|
|
105
|
+
/**
|
|
106
|
+
* Resolve the first installed package from `candidates`.
|
|
107
|
+
*
|
|
108
|
+
* Used by plugins that accept several equivalent tools (biome *or*
|
|
109
|
+
* eslint, vitest *or* jest) and want the first one the project actually
|
|
110
|
+
* has, without probing each with a subprocess.
|
|
111
|
+
*/
|
|
112
|
+
export declare function resolveFirstNodeBin(candidates: readonly {
|
|
113
|
+
packageName: string;
|
|
114
|
+
binName: string;
|
|
115
|
+
args?: readonly string[];
|
|
116
|
+
}[], cwd: string): (ResolvedNodeBin & {
|
|
117
|
+
packageName: string;
|
|
118
|
+
binName: string;
|
|
119
|
+
}) | null;
|
|
120
|
+
//# sourceMappingURL=local-bin.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redos guard — run a regex inside a `node:worker_threads` worker so
|
|
3
|
+
* the host can actually terminate it on a wall-clock budget.
|
|
4
|
+
*
|
|
5
|
+
* Why a worker thread?
|
|
6
|
+
* A `setTimeout`-based watchdog cannot interrupt a synchronous
|
|
7
|
+
* CPU-bound regex in Node.js's single-threaded event loop. The
|
|
8
|
+
* `setImmediate`/`setTimeout` race fires whichever wins the next
|
|
9
|
+
* event-loop tick; if the regex blocks the loop synchronously for
|
|
10
|
+
* 7 seconds, the timer has long since fired and the regex still
|
|
11
|
+
* returns a result — only after the loop is unblocked does the
|
|
12
|
+
* `setImmediate` callback resume and resolve `{ timedOut: false }`.
|
|
13
|
+
*
|
|
14
|
+
* The only honest fix is to run the regex in a separate thread that
|
|
15
|
+
* the host can `worker.terminate()`. `node:worker_threads` gives us
|
|
16
|
+
* that, and the per-thread cost is amortized by the runtime helper
|
|
17
|
+
* itself (the host doesn't pay for the thread except when it
|
|
18
|
+
* invokes `withReDoSGuard`).
|
|
19
|
+
*
|
|
20
|
+
* Why is this here, not inside each plugin?
|
|
21
|
+
* Three plugins (`secret-scanner`, `prompt-firewall`, `path-guard`)
|
|
22
|
+
* need the same contract. Three copies would drift; one copy is
|
|
23
|
+
* auditable and testable.
|
|
24
|
+
*
|
|
25
|
+
* Contract:
|
|
26
|
+
* `withReDoSGuard(re, input, ms)` returns:
|
|
27
|
+
* { timedOut: false, match: RegExpExecArray | null } on normal completion
|
|
28
|
+
* { timedOut: true, match: null } on timeout
|
|
29
|
+
*/
|
|
30
|
+
export interface ReDoSResult {
|
|
31
|
+
/** True when the regex did not complete within the wall-clock budget. */
|
|
32
|
+
timedOut: boolean;
|
|
33
|
+
/** The match result (groups, indices) when `timedOut === false`; null otherwise. */
|
|
34
|
+
match: RegExpExecArray | null;
|
|
35
|
+
}
|
|
36
|
+
export interface ReDoSOptions {
|
|
37
|
+
/** Wall-clock budget in ms. Default 50. */
|
|
38
|
+
budgetMs?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Optional hook invoked exactly once when the budget is exceeded.
|
|
41
|
+
* Called synchronously after the regex is terminated. Default: no-op.
|
|
42
|
+
*/
|
|
43
|
+
onTimeout?: (info: {
|
|
44
|
+
regex: RegExp;
|
|
45
|
+
input: string;
|
|
46
|
+
budgetMs: number;
|
|
47
|
+
elapsedMs: number;
|
|
48
|
+
}) => void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Run `re.exec(input)` inside a worker thread with a wall-clock
|
|
52
|
+
* watchdog. The worker is terminated when the budget elapses; the
|
|
53
|
+
* regex cannot keep running.
|
|
54
|
+
*
|
|
55
|
+
* Returns a Promise; resolved with `{ timedOut, match }`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function withReDoSGuard(re: RegExp, input: string, budgetMs?: number, options?: ReDoSOptions): Promise<ReDoSResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Convenience: build a guarded matcher.
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* const matchCredential = guardedMatcher(/AKIA[0-9A-Z]{16}/g, 25);
|
|
63
|
+
* const r = await matchCredential(line);
|
|
64
|
+
* if (r.timedOut) counters.redosTimeouts++;
|
|
65
|
+
* else if (r.match) report(r.match);
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function guardedMatcher(re: RegExp, budgetMs?: number, onTimeout?: ReDoSOptions['onTimeout']): (input: string) => Promise<ReDoSResult>;
|
|
69
|
+
//# sourceMappingURL=redos-guard.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wrongstack/plugins — serialisation that cannot take the process down.
|
|
3
|
+
*
|
|
4
|
+
* Plugins routinely stringify values they did not construct: event-bus
|
|
5
|
+
* payloads, tool results, mailbox bodies. `JSON.stringify` throws on a
|
|
6
|
+
* circular reference and on `BigInt`, and several of these call sites sit
|
|
7
|
+
* inside detached (fire-and-forget) work where a throw becomes an
|
|
8
|
+
* unhandled rejection rather than a handled failure.
|
|
9
|
+
*
|
|
10
|
+
* `safeJsonStringify` never throws. A value it cannot represent is
|
|
11
|
+
* replaced with a marker, so the surrounding feature degrades to "this
|
|
12
|
+
* field is unreadable" instead of failing — or crashing — outright.
|
|
13
|
+
*/
|
|
14
|
+
/** Placeholder substituted for a value that cannot be serialised. */
|
|
15
|
+
export declare const UNSERIALIZABLE = "[unserializable]";
|
|
16
|
+
/**
|
|
17
|
+
* `JSON.stringify` with cycle handling and a total-failure fallback.
|
|
18
|
+
*
|
|
19
|
+
* @param value anything
|
|
20
|
+
* @param indent passed through to `JSON.stringify` (e.g. `2` to pretty-print)
|
|
21
|
+
* @returns a JSON string, or a marker string when the value resists
|
|
22
|
+
* serialisation entirely. Never throws.
|
|
23
|
+
*/
|
|
24
|
+
export declare function safeJsonStringify(value: unknown, indent?: number): string;
|
|
25
|
+
//# sourceMappingURL=safe-json.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox — canonical project-path validation with symlink resolution.
|
|
3
|
+
*
|
|
4
|
+
* Why a new helper when `runtime/index.ts` already exports
|
|
5
|
+
* `withinProject` and `sanitizeRunnerPath`?
|
|
6
|
+
*
|
|
7
|
+
* - `withinProject` does NOT resolve symlinks: a path like
|
|
8
|
+
* `project/link-to-/etc/passwd` reports as inside the project
|
|
9
|
+
* even though the resolved target is outside.
|
|
10
|
+
* - `sanitizeRunnerPath` is for runner argv (linter binaries),
|
|
11
|
+
* not user-supplied tool input. The two paths share the
|
|
12
|
+
* leading-dash + length checks but `sanitizeRunnerPath`
|
|
13
|
+
* rejects a `cwd` for which it cannot resolve; user-input
|
|
14
|
+
* sandbox needs canonicalization instead.
|
|
15
|
+
*
|
|
16
|
+
* The previous design (per SAGE memory T-03) relied on three
|
|
17
|
+
* duplicate `withinProject` copies in `runtime/index.ts`,
|
|
18
|
+
* `file-watcher/index.ts`, and `path-guard/glob.ts`. They drifted
|
|
19
|
+
* on edge cases. This helper is the single replacement.
|
|
20
|
+
*
|
|
21
|
+
* Contract:
|
|
22
|
+
* `safePath(input, projectRoot?)` returns the canonical absolute
|
|
23
|
+
* path inside the project on success, or `null` on rejection.
|
|
24
|
+
*
|
|
25
|
+
* Rejection cases:
|
|
26
|
+
* - empty string
|
|
27
|
+
* - length > 4096 bytes (matches `runtime.withinProject`)
|
|
28
|
+
* - leading-dash (option smuggling)
|
|
29
|
+
* - cannot be resolved (path doesn't exist or EPERM)
|
|
30
|
+
* - resolved path escapes the project root
|
|
31
|
+
* - symlink target is outside the project
|
|
32
|
+
*/
|
|
33
|
+
export interface SafePathOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Project root for the sandbox. Defaults to `process.cwd()`. Pass
|
|
36
|
+
* the session cwd from the plugin host when available so that
|
|
37
|
+
* tools running in a subdirectory are scoped to that directory.
|
|
38
|
+
*/
|
|
39
|
+
projectRoot?: string;
|
|
40
|
+
/**
|
|
41
|
+
* If true (default), follow symlinks via `realpathSync`. Set false
|
|
42
|
+
* for plugins that need to record the literal path the user wrote
|
|
43
|
+
* (e.g. checkpoint capture, git diff).
|
|
44
|
+
*/
|
|
45
|
+
followSymlinks?: boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve `input` to an absolute path inside the project root.
|
|
49
|
+
*
|
|
50
|
+
* Returns `null` for empty/oversized/leading-dash inputs and for
|
|
51
|
+
* paths whose real target escapes the project.
|
|
52
|
+
*/
|
|
53
|
+
export declare function safePath(input: string, options?: SafePathOptions): string | null;
|
|
54
|
+
/**
|
|
55
|
+
* Boolean convenience for callers that don't need the canonical path.
|
|
56
|
+
* Equivalent to `safePath(input, options) !== null`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function isInsideProject(input: string, options?: SafePathOptions): boolean;
|
|
59
|
+
//# sourceMappingURL=sandbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './runtime/index.js';
|