@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ECOSTACK TECHNOLOGY OÜ
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# @wrongstack/plugin-sdk
|
|
2
|
+
|
|
3
|
+
Authoring SDK for [WrongStack](../../README.md) plugins. Third-party plugin
|
|
4
|
+
authors should depend on **this package only** — it exposes the plugin
|
|
5
|
+
contract, the `definePlugin` helper, and the same audit-hardened runtime
|
|
6
|
+
helpers the built-in plugins use.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @wrongstack/plugin-sdk
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Minimal plugin
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { definePlugin } from '@wrongstack/plugin-sdk';
|
|
16
|
+
|
|
17
|
+
export default definePlugin(
|
|
18
|
+
{
|
|
19
|
+
name: 'wstack-plugin-hello',
|
|
20
|
+
version: '1.0.0',
|
|
21
|
+
description: 'Greets the team',
|
|
22
|
+
capabilities: { tools: true },
|
|
23
|
+
defaultConfig: { greeting: 'hello' },
|
|
24
|
+
},
|
|
25
|
+
async (api, options) => {
|
|
26
|
+
api.tools.register({
|
|
27
|
+
name: 'hello_greet',
|
|
28
|
+
description: `Say ${options.greeting} to someone`,
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: { who: { type: 'string' } },
|
|
32
|
+
required: ['who'],
|
|
33
|
+
},
|
|
34
|
+
permission: 'auto',
|
|
35
|
+
riskTier: 'safe',
|
|
36
|
+
async execute(input) {
|
|
37
|
+
return { greeting: `${options.greeting}, ${input.who}!` };
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`definePlugin` injects the current `KERNEL_API_VERSION` for you. If you
|
|
45
|
+
write the plugin object by hand instead, declare the contract version
|
|
46
|
+
your plugin was built against:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { KERNEL_API_VERSION } from '@wrongstack/plugin-sdk';
|
|
50
|
+
|
|
51
|
+
export const plugin = {
|
|
52
|
+
name: 'wstack-plugin-hello',
|
|
53
|
+
apiVersion: '^0.1', // keep loading across additive host releases
|
|
54
|
+
// ...
|
|
55
|
+
async setup(api) {},
|
|
56
|
+
};
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## What's in the box
|
|
60
|
+
|
|
61
|
+
| Import | Purpose |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `definePlugin` | Typed authoring helper (infers options, injects `apiVersion`) |
|
|
64
|
+
| `KERNEL_API_VERSION` | The host's plugin contract version |
|
|
65
|
+
| `Plugin`, `PluginAPI`, `PluginCapabilities`, … | The full plugin contract types |
|
|
66
|
+
| `HookEvent`, `HookOutcome`, `InProcessHook`, … | Lifecycle hook types (`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `SessionStart`, `Stop`) |
|
|
67
|
+
| `AgentExtension`, `ProviderRunnerWrapper`, … | Agent-loop extension point types |
|
|
68
|
+
| `Tool`, `EventName`, `Config`, `ConfigStore` | Registry, event, and config types |
|
|
69
|
+
| `@wrongstack/plugin-sdk/runtime` | Bounded collections, `releaseHandles`, sandboxed paths (`safePath`, `isInsideProject`), ReDoS guards (`withReDoSGuard`), safe runner spawning (`resolveRunnerCommand`, `runRunnerCommand`), optional-LLM helpers (`runOptionalPluginLlm`, `runOptionalPluginCouncil`) |
|
|
70
|
+
|
|
71
|
+
## Versioning
|
|
72
|
+
|
|
73
|
+
The SDK tracks the host's **plugin contract** (`KERNEL_API_VERSION`), not the
|
|
74
|
+
host package version. Pin `apiVersion: '^0.1'` for additive compatibility;
|
|
75
|
+
the host refuses plugins whose declared range no longer satisfies the kernel
|
|
76
|
+
(see `docs/plugin-third-party.md` in the repository for the compatibility
|
|
77
|
+
policy and release checklist).
|
|
78
|
+
|
|
79
|
+
## Loading your plugin in WrongStack
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# register an npm-installed package (installs into ~/.wrongstack/plugins,
|
|
83
|
+
# scripts disabled by default for supply-chain safety)
|
|
84
|
+
wstack plugin add wstack-plugin-hello --install
|
|
85
|
+
|
|
86
|
+
# or point at local code during development
|
|
87
|
+
wstack plugin add ./my-plugin # writes { name, path } into config.plugins
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
External plugins run in-process with full host privileges. On first load the
|
|
91
|
+
host pins a SHA-256 of your plugin's entry file
|
|
92
|
+
(`~/.wrongstack/plugin-trust.json`); after an update, users re-confirm with
|
|
93
|
+
`wstack plugin trust <name>`. See `docs/plugin-author-guide.md` for the full
|
|
94
|
+
isolation and capability-enforcement model.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wrongstack/plugin-sdk — the authoring surface for WrongStack plugins.
|
|
3
|
+
*
|
|
4
|
+
* Third-party plugins should depend on THIS package (not on
|
|
5
|
+
* `@wrongstack/core` or `@wrongstack/plugins`) to compile against the
|
|
6
|
+
* plugin contract. It re-exports:
|
|
7
|
+
*
|
|
8
|
+
* - `definePlugin` — ergonomic authoring helper that infers option types
|
|
9
|
+
* and injects the current `apiVersion`,
|
|
10
|
+
* - `KERNEL_API_VERSION` — the host contract version to pin against,
|
|
11
|
+
* - every type a plugin's manifest, hooks, tools, and config schema need.
|
|
12
|
+
*
|
|
13
|
+
* Runtime helpers (bounded collections, safe process spawning, sandboxed
|
|
14
|
+
* paths, optional-LLM plumbing) live in `@wrongstack/plugin-sdk/runtime`.
|
|
15
|
+
*
|
|
16
|
+
* Versioning: the SDK follows the host's plugin contract
|
|
17
|
+
* (`KERNEL_API_VERSION`). Declare `apiVersion: '^0.1'` (or use
|
|
18
|
+
* `definePlugin`, which does it for you) so your plugin keeps loading
|
|
19
|
+
* across additive host releases.
|
|
20
|
+
*/
|
|
21
|
+
export { definePlugin, KERNEL_API_VERSION } from '@wrongstack/core/plugin';
|
|
22
|
+
export type { JSONSchema, Plugin, PluginAPI, PluginCapabilities, PluginConfigFieldMetadata, PluginConfigFieldLifecycle, PluginDependency, PluginPipelines, PluginRuntime, } from '@wrongstack/core/types';
|
|
23
|
+
export type { AnyHookOutcome, HookEvent, HookInput, HookInvocationContext, HookMatcher, HookOutcome, HookRegistrationOptions, InProcessHook, PreToolUseOutcome, } from '@wrongstack/core/types';
|
|
24
|
+
export type { AfterIterationHook, AfterRunHook, AfterToolExecutionHook, AgentExtension, BeforeIterationHook, BeforeRunHook, BeforeToolExecutionHook, OnErrorHook, ProviderRunnerFn, ProviderRunnerWrapper, } from '@wrongstack/core/extension';
|
|
25
|
+
export type { SystemPromptContributor } from '@wrongstack/core/types';
|
|
26
|
+
export type { Tool, ToolCallContext } from '@wrongstack/core/types';
|
|
27
|
+
export type { EventName, Listener } from '@wrongstack/core/kernel';
|
|
28
|
+
export type { Config, ConfigStore, PluginConfig } from '@wrongstack/core/types';
|
|
29
|
+
export type { Logger } from '@wrongstack/core/types';
|
|
30
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wrongstack/plugins — bounded key/value cache.
|
|
3
|
+
*
|
|
4
|
+
* Several plugins memoise per-path or per-model work in a module-scope
|
|
5
|
+
* `Map` that is only ever cleared at teardown. In a short test that is
|
|
6
|
+
* invisible; in a long session over a large repository it is a slow leak —
|
|
7
|
+
* one entry per file touched, per branch resolved, per model seen — held
|
|
8
|
+
* for the lifetime of the process.
|
|
9
|
+
*
|
|
10
|
+
* `BoundedMap` is a drop-in replacement for those maps: same `get`/`set`/
|
|
11
|
+
* `delete`/`clear`/`size` surface, plus a hard entry cap with
|
|
12
|
+
* least-recently-used eviction and an optional per-entry TTL. Nothing
|
|
13
|
+
* about the call sites has to change beyond the constructor.
|
|
14
|
+
*
|
|
15
|
+
* Why LRU rather than insertion-order eviction: the access pattern for
|
|
16
|
+
* these caches is "a few hot paths, a long tail of cold ones". Evicting by
|
|
17
|
+
* insertion order throws away the hot entries first, which is exactly
|
|
18
|
+
* backwards; re-reading a hot entry should keep it.
|
|
19
|
+
*/
|
|
20
|
+
export interface BoundedMapOptions {
|
|
21
|
+
/** Hard cap on retained entries. Must be a safe integer >= 1. */
|
|
22
|
+
max: number;
|
|
23
|
+
/**
|
|
24
|
+
* Optional per-entry lifetime in ms. An entry older than this is treated
|
|
25
|
+
* as absent by `get`/`has` and dropped on access. Omit for no expiry.
|
|
26
|
+
*/
|
|
27
|
+
ttlMs?: number | undefined;
|
|
28
|
+
/** Clock injection point — tests override this instead of faking timers. */
|
|
29
|
+
now?: (() => number) | undefined;
|
|
30
|
+
}
|
|
31
|
+
export declare class BoundedMap<K, V> {
|
|
32
|
+
private readonly map;
|
|
33
|
+
private readonly max;
|
|
34
|
+
private readonly ttlMs;
|
|
35
|
+
private readonly now;
|
|
36
|
+
/** Entries dropped to stay under `max`. Surfaced by plugin health(). */
|
|
37
|
+
private evictions;
|
|
38
|
+
constructor(options: BoundedMapOptions);
|
|
39
|
+
private expired;
|
|
40
|
+
get(key: K): V | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Read without promoting the key to most-recently-used. Use for
|
|
43
|
+
* diagnostics that must not perturb the eviction order.
|
|
44
|
+
*/
|
|
45
|
+
peek(key: K): V | undefined;
|
|
46
|
+
has(key: K): boolean;
|
|
47
|
+
set(key: K, value: V): this;
|
|
48
|
+
delete(key: K): boolean;
|
|
49
|
+
clear(): void;
|
|
50
|
+
get size(): number;
|
|
51
|
+
/** How many entries have been dropped to respect `max`, since the last clear. */
|
|
52
|
+
get evictionCount(): number;
|
|
53
|
+
/** Drop every expired entry. Cheap enough to call from a status tool. */
|
|
54
|
+
prune(): number;
|
|
55
|
+
/** Live (non-expired) entries, coldest first. */
|
|
56
|
+
entries(): IterableIterator<[K, V]>;
|
|
57
|
+
[Symbol.iterator](): IterableIterator<[K, V]>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A `Set` with the same hard cap and LRU eviction as {@link BoundedMap}.
|
|
61
|
+
*
|
|
62
|
+
* Plugins use module-scope sets to remember "already reported" keys so a
|
|
63
|
+
* finding is surfaced once rather than on every scan. That set has to
|
|
64
|
+
* outlive a single scan, which is why it lives at module scope — but
|
|
65
|
+
* without a bound it grows one entry per distinct finding for the life of
|
|
66
|
+
* the process.
|
|
67
|
+
*
|
|
68
|
+
* Eviction means a very old key may eventually be reported a second time.
|
|
69
|
+
* That is the right trade: re-reporting a finding the user last saw
|
|
70
|
+
* thousands of findings ago is reasonable behaviour, whereas unbounded
|
|
71
|
+
* growth is not.
|
|
72
|
+
*/
|
|
73
|
+
export declare class BoundedSet<T> {
|
|
74
|
+
private readonly inner;
|
|
75
|
+
constructor(options: BoundedMapOptions);
|
|
76
|
+
has(value: T): boolean;
|
|
77
|
+
add(value: T): this;
|
|
78
|
+
delete(value: T): boolean;
|
|
79
|
+
clear(): void;
|
|
80
|
+
get size(): number;
|
|
81
|
+
/** How many entries have been dropped to respect `max`, since the last clear. */
|
|
82
|
+
get evictionCount(): number;
|
|
83
|
+
values(): IterableIterator<T>;
|
|
84
|
+
[Symbol.iterator](): IterableIterator<T>;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=bounded-map.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wrongstack/plugins — canonical credential-pattern table.
|
|
3
|
+
*
|
|
4
|
+
* Two plugins detect credentials, at different points in the pipeline:
|
|
5
|
+
*
|
|
6
|
+
* - `secret-scanner` gates tool *input* and inspects tool *output*.
|
|
7
|
+
* - `prompt-firewall` inspects the outgoing *provider request* (and the
|
|
8
|
+
* response) so a credential sitting in context is not shipped to a
|
|
9
|
+
* third party.
|
|
10
|
+
*
|
|
11
|
+
* They previously each carried their own pattern list. The lists drifted:
|
|
12
|
+
* one grew GitLab, npm, SendGrid and DigitalOcean coverage while the other
|
|
13
|
+
* did not, so which credentials were caught depended on which side of the
|
|
14
|
+
* pipeline they crossed. A credential is a credential — the two surfaces
|
|
15
|
+
* should never disagree about what one looks like.
|
|
16
|
+
*
|
|
17
|
+
* This module is the single source of truth. Adding a pattern here
|
|
18
|
+
* strengthens both surfaces at once.
|
|
19
|
+
*
|
|
20
|
+
* Style notes for new entries:
|
|
21
|
+
* - Prefer explicit lookaround boundaries — `(?<![A-Za-z0-9])` /
|
|
22
|
+
* `(?![A-Za-z0-9])` — over ``. `` is defined against word
|
|
23
|
+
* characters, so it silently fails next to the `+`, `/` and `=` that
|
|
24
|
+
* base64-shaped secrets routinely end with.
|
|
25
|
+
* - Keep every group non-capturing (`(?:…)`). The combined regex maps a
|
|
26
|
+
* capture-group index back to the pattern that fired; an inner group
|
|
27
|
+
* shifts that mapping. `secret-scanner` compensates for user-supplied
|
|
28
|
+
* patterns, but built-ins should not need it.
|
|
29
|
+
* - Avoid unbounded `.*` lookarounds: these run on every provider
|
|
30
|
+
* request, against the full context.
|
|
31
|
+
*/
|
|
32
|
+
/** One credential shape, with a stable machine-readable id. */
|
|
33
|
+
export interface CredentialPattern {
|
|
34
|
+
/** Stable id, e.g. `github_pat`. Reported to users and in metrics. */
|
|
35
|
+
type: string;
|
|
36
|
+
/** Global-flagged matcher. */
|
|
37
|
+
regex: RegExp;
|
|
38
|
+
}
|
|
39
|
+
export declare const CREDENTIAL_PATTERNS: readonly CredentialPattern[];
|
|
40
|
+
/** Fresh, independently-stateful copies (RegExp `lastIndex` is mutable). */
|
|
41
|
+
export declare function cloneCredentialPatterns(): CredentialPattern[];
|
|
42
|
+
//# sourceMappingURL=credential-patterns.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* H1 idempotent state — single-slot plugin state with a registry of
|
|
3
|
+
* releasable handles that survives `setup()` reload cycles.
|
|
4
|
+
*
|
|
5
|
+
* The "H1 audit pattern" (per SAGE memory T-03) is documented across
|
|
6
|
+
* the plugin suite: a plugin's module-scope `state` object holds
|
|
7
|
+
* counters plus a `hookUnregister` (or `extensionUnregister`) slot;
|
|
8
|
+
* on reload, the slot MUST be released before a new one is stored.
|
|
9
|
+
* Every plugin implements this inline with subtle variations:
|
|
10
|
+
*
|
|
11
|
+
* - some use `releaseHandle(state.hookUnregister)` (`accessibility-auditor`)
|
|
12
|
+
* - some use `try { state.hookUnregister(); } catch {}` (`config-validator`)
|
|
13
|
+
* - some use a single inline `if (state.hookUnregister) { … }` block
|
|
14
|
+
*
|
|
15
|
+
* The drift cost: in 4 plugins the prior handle was leaked on reload
|
|
16
|
+
* because the inline `if` check raced with the new registration.
|
|
17
|
+
* This helper centralises the contract.
|
|
18
|
+
*
|
|
19
|
+
* Contract:
|
|
20
|
+
* `createH1State<T>(initial)` returns
|
|
21
|
+
* {
|
|
22
|
+
* state: T, // the user's mutable state
|
|
23
|
+
* register: (key, unregister) => void,
|
|
24
|
+
* release: (key) => void,
|
|
25
|
+
* releaseAll: () => void,
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* - `register(key, unregister)` releases any prior handle at `key`
|
|
29
|
+
* before storing the new one.
|
|
30
|
+
* - `release(key)` is a no-op if no handle is registered.
|
|
31
|
+
* - `releaseAll()` releases every registered handle and clears the map.
|
|
32
|
+
* - A throwing unregister function is swallowed (best-effort), matching
|
|
33
|
+
* the existing `releaseHandle` semantics at `runtime/handles.ts`.
|
|
34
|
+
*
|
|
35
|
+
* The state object itself is NOT reset by `releaseAll` — counter
|
|
36
|
+
* reset is the plugin's responsibility (it knows the semantics of its
|
|
37
|
+
* counters). This helper owns the handle lifecycle only.
|
|
38
|
+
*/
|
|
39
|
+
export type Unregister = () => void;
|
|
40
|
+
export interface H1State<T> {
|
|
41
|
+
/** The plugin's mutable state. Owned by the caller; never reset by this helper. */
|
|
42
|
+
state: T;
|
|
43
|
+
/**
|
|
44
|
+
* Register an unregister function under `key`. Any prior handle at
|
|
45
|
+
* `key` is released first. Throwing unregister functions are
|
|
46
|
+
* swallowed.
|
|
47
|
+
*/
|
|
48
|
+
register: (key: string, unregister: Unregister | null | undefined) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Release the handle at `key` (if any). Idempotent. Throwing
|
|
51
|
+
* unregister functions are swallowed.
|
|
52
|
+
*/
|
|
53
|
+
release: (key: string) => void;
|
|
54
|
+
/** Release every registered handle. Idempotent. */
|
|
55
|
+
releaseAll: () => void;
|
|
56
|
+
/** Number of currently registered handles. Observability for health()/status tools. */
|
|
57
|
+
size: () => number;
|
|
58
|
+
/** List the registered keys. Order is insertion order; useful for diagnostics. */
|
|
59
|
+
keys: () => string[];
|
|
60
|
+
}
|
|
61
|
+
export declare function createH1State<T>(initial: T): H1State<T>;
|
|
62
|
+
//# sourceMappingURL=h1-state.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wrongstack/plugins — unregister-handle discipline.
|
|
3
|
+
*
|
|
4
|
+
* Plugins keep their hook/listener unregister functions in module-scope
|
|
5
|
+
* state, and every plugin's `setup()` is expected to be idempotent: calling
|
|
6
|
+
* it twice must not leave two live registrations. The house style calls
|
|
7
|
+
* this the "H1 pattern".
|
|
8
|
+
*
|
|
9
|
+
* The failure mode this helper exists to prevent is subtle and was present
|
|
10
|
+
* in ~15 plugins: `setup()` reset the handle with
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* state.hookUnregister = null; // WRONG
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* which drops the only reference to the *previous* registration without
|
|
17
|
+
* calling it. The old hook stays live in the registry, unreachable, and
|
|
18
|
+
* fires alongside the new one — so counters double, warnings appear twice,
|
|
19
|
+
* and a gate can block on a stale closure holding the previous config.
|
|
20
|
+
*
|
|
21
|
+
* `releaseHandle` makes the correct spelling a one-liner with the same
|
|
22
|
+
* shape as the wrong one, so the two are hard to confuse:
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* state.hookUnregister = releaseHandle(state.hookUnregister);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
/** An unregister function returned by `registerHook`/`onEvent`/etc. */
|
|
29
|
+
export type Unregister = (() => void) | null | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Invoke `off` if present, swallowing any error, and return `null` so the
|
|
32
|
+
* caller can assign the result straight back to the handle.
|
|
33
|
+
*
|
|
34
|
+
* Unregistering is best-effort by design: a handle whose owner has already
|
|
35
|
+
* been torn down may throw, and that must never prevent the rest of
|
|
36
|
+
* `setup()`/`teardown()` from running.
|
|
37
|
+
*/
|
|
38
|
+
export declare function releaseHandle(off: Unregister): null;
|
|
39
|
+
/**
|
|
40
|
+
* Release several handles held on one state object, clearing each in place.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* releaseHandles(state, ['hookUnregister', 'postHookUnregister']);
|
|
44
|
+
*/
|
|
45
|
+
export declare function releaseHandles<S extends object, K extends keyof S>(state: S, keys: readonly K[]): void;
|
|
46
|
+
//# sourceMappingURL=handles.d.ts.map
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wrongstack/plugins — Language-agnostic runtime helper.
|
|
3
|
+
*
|
|
4
|
+
* Plugins that need to invoke a build-time or test-time tool
|
|
5
|
+
* (TypeScript's `tsc`, Node's `vitest`, Python's `pytest`, Rust's
|
|
6
|
+
* `cargo`, Go's `go test`, etc.) share this module instead of
|
|
7
|
+
* re-implementing argv/sandbox/allowlist logic.
|
|
8
|
+
*
|
|
9
|
+
* Why one module instead of three plugin-local copies?
|
|
10
|
+
* - One audit surface for security-sensitive code: arg-splitting,
|
|
11
|
+
* sandboxing, execFile-with-shell-false, maxBuffer, timeout.
|
|
12
|
+
* - Plugins stay focused on their domain (linters, type-checkers,
|
|
13
|
+
* test runners); the runtime helper owns the cross-cutting concern.
|
|
14
|
+
* - New languages opt in by adding a `LanguageRuntime` entry; the
|
|
15
|
+
* plugin layer keeps working unchanged.
|
|
16
|
+
*
|
|
17
|
+
* The public API is intentionally small:
|
|
18
|
+
* - `LanguageRuntime`: declares which language a plugin targets.
|
|
19
|
+
* - `resolveRunnerCommand(runtime, command, options)`: validate +
|
|
20
|
+
* split a user-supplied command into a safe argv array.
|
|
21
|
+
* - `sanitizeRunnerPath(value, options)`: reject paths outside
|
|
22
|
+
* the project or that start with `-` (option smuggling).
|
|
23
|
+
* - `runRunnerCommand(argv, options)`: spawn the resolved argv
|
|
24
|
+
* with `shell:false`, capture stdout/stderr/code, and apply
|
|
25
|
+
* timeout/abort.
|
|
26
|
+
* - `probeRunner(runtime, argv, options)`: cheap availability check.
|
|
27
|
+
*
|
|
28
|
+
* Anything language-specific (flag tables, default commands,
|
|
29
|
+
* output parsing) stays in the plugin that owns that language.
|
|
30
|
+
*/
|
|
31
|
+
export { parseLlmJsonObject, runOptionalPluginCouncil, runOptionalPluginLlm, stripOuterMarkdownFence, type OptionalCouncilRequest, type OptionalLlmRequest, type OptionalLlmResult, } from './llm.js';
|
|
32
|
+
export { BoundedMap, BoundedSet, type BoundedMapOptions } from './bounded-map.js';
|
|
33
|
+
export { cloneCredentialPatterns, CREDENTIAL_PATTERNS, type CredentialPattern, } from './credential-patterns.js';
|
|
34
|
+
export { UNSERIALIZABLE, safeJsonStringify } from './safe-json.js';
|
|
35
|
+
export { releaseHandle, releaseHandles, type Unregister } from './handles.js';
|
|
36
|
+
export { withReDoSGuard, guardedMatcher, type ReDoSResult, type ReDoSOptions, } from './redos-guard.js';
|
|
37
|
+
export { safePath, isInsideProject, type SafePathOptions, } from './sandbox.js';
|
|
38
|
+
export { createH1State, type H1State, } from './h1-state.js';
|
|
39
|
+
export { clearLocalBinCache, findOnPath, resolveExecInvocation, resolveFirstNodeBin, resolveNodeBin, resolveWin32Command, type ExecInvocation, type ResolvedNodeBin, } from './local-bin.js';
|
|
40
|
+
export type LanguageId = 'typescript' | 'javascript' | 'python' | 'go' | 'rust' | 'shell' | 'ruby' | 'java' | 'kotlin' | 'dotnet' | 'generic';
|
|
41
|
+
export type PackageManagerId = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'pip' | 'poetry' | 'go' | 'cargo' | 'gem' | 'maven' | 'gradle' | 'dotnet' | 'none';
|
|
42
|
+
export interface LanguageRuntime {
|
|
43
|
+
/** Stable identifier for diagnostics and logging. */
|
|
44
|
+
id: LanguageId;
|
|
45
|
+
/**
|
|
46
|
+
* Default package manager launcher if the plugin must spawn a tool
|
|
47
|
+
* (e.g. `npx vitest` or `cargo test`). `none` means the executable
|
|
48
|
+
* itself is invoked directly (no launcher).
|
|
49
|
+
*/
|
|
50
|
+
packageManager: PackageManagerId;
|
|
51
|
+
/**
|
|
52
|
+
* Executable token that must appear as the second argv element when
|
|
53
|
+
* a launcher is used (e.g. `vitest` after `pnpm exec`, `test` after
|
|
54
|
+
* `cargo`). When `subcommands.length === 0` and the package manager
|
|
55
|
+
* has no subcommand step, this is also accepted as the head token.
|
|
56
|
+
*/
|
|
57
|
+
executable: string;
|
|
58
|
+
/**
|
|
59
|
+
* Allowlisted flag values, in addition to positional arguments and
|
|
60
|
+
* file paths. `null` means "no flag allowlist enforced" — every
|
|
61
|
+
* leading-dash token is still rejected, but no flag whitelist applies.
|
|
62
|
+
*/
|
|
63
|
+
allowedFlags: ReadonlySet<string> | null;
|
|
64
|
+
/**
|
|
65
|
+
* Optional list of subcommand tokens that may follow the launcher,
|
|
66
|
+
* e.g. `['exec']` for `pnpm exec tsc`. Empty array means the runner
|
|
67
|
+
* executable must appear immediately as the second token (e.g.
|
|
68
|
+
* `cargo test`, `go test`).
|
|
69
|
+
*/
|
|
70
|
+
subcommands: readonly string[];
|
|
71
|
+
/**
|
|
72
|
+
* Default command spelling for `resolveRunnerCommand` when no
|
|
73
|
+
* custom command is supplied. Plugins can override via config.
|
|
74
|
+
*/
|
|
75
|
+
defaultCommand: string;
|
|
76
|
+
}
|
|
77
|
+
export interface ResolvedCommand {
|
|
78
|
+
cmd: string;
|
|
79
|
+
args: readonly string[];
|
|
80
|
+
display: string;
|
|
81
|
+
}
|
|
82
|
+
export interface ResolveOptions {
|
|
83
|
+
/**
|
|
84
|
+
* Project root used to sandbox absolute executable paths. Defaults
|
|
85
|
+
* to `process.cwd()`. Absolute executable paths must resolve inside
|
|
86
|
+
* this directory; relative basenames must match `LanguageRuntime.executable`.
|
|
87
|
+
*/
|
|
88
|
+
projectRoot?: string;
|
|
89
|
+
}
|
|
90
|
+
export interface RunOptions extends ResolveOptions {
|
|
91
|
+
cwd: string;
|
|
92
|
+
timeoutMs: number;
|
|
93
|
+
signal?: AbortSignal;
|
|
94
|
+
}
|
|
95
|
+
export interface RunResult {
|
|
96
|
+
code: number | null;
|
|
97
|
+
stdout: string;
|
|
98
|
+
stderr: string;
|
|
99
|
+
timedOut: boolean;
|
|
100
|
+
/** True when the executable could not be spawned (ENOENT, EPERM, …). */
|
|
101
|
+
spawnError: boolean;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Validate `value` as a sandboxed path inside the project. Returns the
|
|
105
|
+
* canonical absolute path on success, `null` on rejection (empty,
|
|
106
|
+
* outside the project, leading-dash, or longer than 4096 bytes).
|
|
107
|
+
*/
|
|
108
|
+
export declare function sanitizeRunnerPath(value: string, options?: ResolveOptions): string | null;
|
|
109
|
+
/**
|
|
110
|
+
* Resolve a user-supplied command string into an argv-style invocation
|
|
111
|
+
* using the language runtime's allowlist. Returns `null` if the command
|
|
112
|
+
* fails closed (unknown launcher, unknown subcommand, disallowed flag,
|
|
113
|
+
* metacharacters, leading-dash injection, absolute-path escape).
|
|
114
|
+
*/
|
|
115
|
+
export declare function resolveRunnerCommand(runtime: LanguageRuntime, command: string, options?: ResolveOptions): ResolvedCommand | null;
|
|
116
|
+
/**
|
|
117
|
+
* Spawn the resolved argv with `shell:false`. Always uses
|
|
118
|
+
* `execFile` (no shell, argv), captures stdout/stderr with a hard
|
|
119
|
+
* `maxBuffer`, and respects `signal` + `timeoutMs`. Plugins can layer
|
|
120
|
+
* language-specific output parsing on top of `RunResult`.
|
|
121
|
+
*/
|
|
122
|
+
export declare function runRunnerCommand(argv: readonly string[], options: RunOptions): Promise<RunResult>;
|
|
123
|
+
/**
|
|
124
|
+
* Cheap availability check. Calls the runner's `--version`-like command
|
|
125
|
+
* with a short timeout; returns true only when exit code is zero.
|
|
126
|
+
*/
|
|
127
|
+
export declare function probeRunner(runtime: LanguageRuntime, probeArg: string | undefined, options: RunOptions): Promise<boolean>;
|
|
128
|
+
/**
|
|
129
|
+
* Check whether a file path is inside the project root. Uses
|
|
130
|
+
* `process.cwd()` as the project boundary. Returns `true` for valid
|
|
131
|
+
* paths inside the project, `false` for empty, too-long, outside,
|
|
132
|
+
* or absolute paths that escape.
|
|
133
|
+
*
|
|
134
|
+
* This is the canonical sandbox check that every file-mutating or
|
|
135
|
+
* file-reading plugin should call before touching a path supplied
|
|
136
|
+
* by tool input. It replaces 27 identical copies across plugins.
|
|
137
|
+
*
|
|
138
|
+
* Performance: caches `process.cwd()` per call to avoid redundant
|
|
139
|
+
* syscalls when checking multiple paths in the same tick.
|
|
140
|
+
*/
|
|
141
|
+
export declare function withinProject(p: string): boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Convenience: locate the runner binary on disk inside the project.
|
|
144
|
+
* Returns the absolute path or `null`.
|
|
145
|
+
*/
|
|
146
|
+
export declare function locateRunnerEntry(runtime: LanguageRuntime, projectRoot: string): string | null;
|
|
147
|
+
export interface CollectOptions {
|
|
148
|
+
/** File extensions to include (e.g. ['.ts', '.tsx', '.js']). */
|
|
149
|
+
extensions: string[];
|
|
150
|
+
/** Directory names to skip entirely. Default skips node_modules, dist, .git, coverage. */
|
|
151
|
+
excludeDirs?: string[] | undefined;
|
|
152
|
+
/** Maximum recursion depth. Unlimited when omitted. */
|
|
153
|
+
maxDepth?: number | undefined;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Recursively collect files under `root` whose extension is in
|
|
157
|
+
* `opts.extensions`. Skips directories named in `opts.excludeDirs`
|
|
158
|
+
* (defaulting to node_modules, dist, .git, coverage) and limits depth
|
|
159
|
+
* when `opts.maxDepth` is set.
|
|
160
|
+
*
|
|
161
|
+
* Shared by 6+ plugin source-scan tools that previously duplicated
|
|
162
|
+
* this implementation identically.
|
|
163
|
+
*
|
|
164
|
+
* Determinism: returns files in sorted order (locale-aware) so
|
|
165
|
+
* scan results are reproducible across platforms and file systems.
|
|
166
|
+
*/
|
|
167
|
+
export declare function collectSourceFiles(root: string, opts: CollectOptions): string[];
|
|
168
|
+
/**
|
|
169
|
+
* Async version of `collectSourceFiles` for non-blocking file collection.
|
|
170
|
+
* Uses `fs.promises` to avoid blocking the event loop on large directory trees.
|
|
171
|
+
*
|
|
172
|
+
* Performance: prefer this in hooks and tools that run on every write/edit
|
|
173
|
+
* (e.g., duplicate-code-detector) to keep the agent loop responsive during
|
|
174
|
+
* large scans.
|
|
175
|
+
*/
|
|
176
|
+
export declare function collectSourceFilesAsync(root: string, opts: CollectOptions): Promise<string[]>;
|
|
177
|
+
/**
|
|
178
|
+
* Check whether `p` has one of the given extensions (case-insensitive).
|
|
179
|
+
* Replaces a 6-copy helper that was duplicated identically across
|
|
180
|
+
* source-scan plugins.
|
|
181
|
+
*/
|
|
182
|
+
export declare function matchesExtension(p: string, exts: string[]): boolean;
|
|
183
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small, shared helpers for optional plugin LLM enrichment.
|
|
3
|
+
*
|
|
4
|
+
* The host-owned `api.llm` facade keeps provider credentials and routing out
|
|
5
|
+
* of plugins. These helpers standardise the other half of that contract:
|
|
6
|
+
* bounded prompts, cancellation, defensive response parsing, and an explicit
|
|
7
|
+
* deterministic fallback when no provider is wired or generation fails.
|
|
8
|
+
*/
|
|
9
|
+
import type { CouncilOption, PluginAPI, PluginLLMOptions } from '@wrongstack/core/types';
|
|
10
|
+
export interface OptionalLlmResult<T> {
|
|
11
|
+
used: boolean;
|
|
12
|
+
value: T | null;
|
|
13
|
+
fallbackReason: 'not-requested' | 'unavailable' | 'cancelled' | 'provider-error' | 'invalid-response' | null;
|
|
14
|
+
}
|
|
15
|
+
export interface OptionalLlmRequest<T> {
|
|
16
|
+
requested: boolean;
|
|
17
|
+
prompt: string;
|
|
18
|
+
options?: PluginLLMOptions | undefined;
|
|
19
|
+
parse(text: string): T | null;
|
|
20
|
+
api: Pick<PluginAPI, 'llm' | 'log'>;
|
|
21
|
+
label: string;
|
|
22
|
+
}
|
|
23
|
+
export interface OptionalCouncilRequest<T> extends OptionalLlmRequest<T> {
|
|
24
|
+
context?: string | undefined;
|
|
25
|
+
profile?: string | undefined;
|
|
26
|
+
councilOptions?: readonly CouncilOption[] | undefined;
|
|
27
|
+
}
|
|
28
|
+
/** Remove one outer Markdown fence without modifying inner code fences. */
|
|
29
|
+
export declare function stripOuterMarkdownFence(text: string): string;
|
|
30
|
+
/** Parse a JSON object from a plain or fenced provider response. */
|
|
31
|
+
export declare function parseLlmJsonObject(text: string): Record<string, unknown> | null;
|
|
32
|
+
/**
|
|
33
|
+
* Run optional enrichment without turning a provider outage into a tool
|
|
34
|
+
* failure. Abort remains observable as a fallback reason and the caller's
|
|
35
|
+
* deterministic result remains authoritative.
|
|
36
|
+
*/
|
|
37
|
+
export declare function runOptionalPluginLlm<T>(request: OptionalLlmRequest<T>): Promise<OptionalLlmResult<T>>;
|
|
38
|
+
/**
|
|
39
|
+
* Prefer the host Council for consequential analysis, then degrade through the
|
|
40
|
+
* same One Shot helper and finally the caller's deterministic result. Council
|
|
41
|
+
* outages therefore never turn an optional enrichment into a tool failure.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runOptionalPluginCouncil<T>(request: OptionalCouncilRequest<T>): Promise<OptionalLlmResult<T>>;
|
|
44
|
+
//# sourceMappingURL=llm.d.ts.map
|