@wrongstack/plugin-sdk 0.319.1 → 1.0.0
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/runtime/bounded-map.js +136 -0
- package/dist/runtime/credential-patterns.js +154 -0
- package/dist/runtime/h1-state.js +45 -0
- package/dist/runtime/handles.js +20 -0
- package/dist/runtime/llm.js +91 -0
- package/dist/runtime/local-bin.js +102 -0
- package/dist/runtime/redos-guard.js +96 -0
- package/dist/runtime/safe-json.js +27 -0
- package/dist/runtime/sandbox.js +50 -0
- package/dist/runtime.js +73 -49
- package/package.json +39 -3
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// src/runtime/bounded-map.ts
|
|
2
|
+
var BoundedMap = class {
|
|
3
|
+
map = /* @__PURE__ */ new Map();
|
|
4
|
+
max;
|
|
5
|
+
ttlMs;
|
|
6
|
+
now;
|
|
7
|
+
/** Entries dropped to stay under `max`. Surfaced by plugin health(). */
|
|
8
|
+
evictions = 0;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
const normalizedMax = Math.floor(options.max);
|
|
11
|
+
this.max = Number.isSafeInteger(normalizedMax) ? Math.max(1, normalizedMax) : 1;
|
|
12
|
+
this.ttlMs = options.ttlMs;
|
|
13
|
+
this.now = options.now ?? Date.now;
|
|
14
|
+
}
|
|
15
|
+
expired(entry) {
|
|
16
|
+
return this.ttlMs !== void 0 && this.now() - entry.storedAt > this.ttlMs;
|
|
17
|
+
}
|
|
18
|
+
get(key) {
|
|
19
|
+
const entry = this.map.get(key);
|
|
20
|
+
if (entry === void 0) return void 0;
|
|
21
|
+
if (this.expired(entry)) {
|
|
22
|
+
this.map.delete(key);
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
this.map.delete(key);
|
|
26
|
+
this.map.set(key, entry);
|
|
27
|
+
return entry.value;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Read without promoting the key to most-recently-used. Use for
|
|
31
|
+
* diagnostics that must not perturb the eviction order.
|
|
32
|
+
*/
|
|
33
|
+
peek(key) {
|
|
34
|
+
const entry = this.map.get(key);
|
|
35
|
+
if (entry === void 0 || this.expired(entry)) return void 0;
|
|
36
|
+
return entry.value;
|
|
37
|
+
}
|
|
38
|
+
has(key) {
|
|
39
|
+
const entry = this.map.get(key);
|
|
40
|
+
if (entry === void 0) return false;
|
|
41
|
+
if (this.expired(entry)) {
|
|
42
|
+
this.map.delete(key);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
set(key, value) {
|
|
48
|
+
this.map.delete(key);
|
|
49
|
+
this.map.set(key, { value, storedAt: this.now() });
|
|
50
|
+
if (this.map.size > this.max && this.ttlMs !== void 0) {
|
|
51
|
+
this.prune();
|
|
52
|
+
}
|
|
53
|
+
while (this.map.size > this.max) {
|
|
54
|
+
const coldest = this.map.keys().next().value;
|
|
55
|
+
if (coldest === void 0) break;
|
|
56
|
+
this.map.delete(coldest);
|
|
57
|
+
this.evictions += 1;
|
|
58
|
+
}
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
delete(key) {
|
|
62
|
+
return this.map.delete(key);
|
|
63
|
+
}
|
|
64
|
+
clear() {
|
|
65
|
+
this.map.clear();
|
|
66
|
+
this.evictions = 0;
|
|
67
|
+
}
|
|
68
|
+
get size() {
|
|
69
|
+
if (this.ttlMs !== void 0) {
|
|
70
|
+
this.prune();
|
|
71
|
+
}
|
|
72
|
+
return this.map.size;
|
|
73
|
+
}
|
|
74
|
+
/** How many entries have been dropped to respect `max`, since the last clear. */
|
|
75
|
+
get evictionCount() {
|
|
76
|
+
return this.evictions;
|
|
77
|
+
}
|
|
78
|
+
/** Drop every expired entry. Cheap enough to call from a status tool. */
|
|
79
|
+
prune() {
|
|
80
|
+
if (this.ttlMs === void 0) return 0;
|
|
81
|
+
let removed = 0;
|
|
82
|
+
for (const [key, entry] of this.map) {
|
|
83
|
+
if (this.expired(entry)) {
|
|
84
|
+
this.map.delete(key);
|
|
85
|
+
removed += 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return removed;
|
|
89
|
+
}
|
|
90
|
+
/** Live (non-expired) entries, coldest first. */
|
|
91
|
+
*entries() {
|
|
92
|
+
for (const [key, entry] of this.map) {
|
|
93
|
+
if (!this.expired(entry)) yield [key, entry.value];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
[Symbol.iterator]() {
|
|
97
|
+
return this.entries();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
var BoundedSet = class {
|
|
101
|
+
inner;
|
|
102
|
+
constructor(options) {
|
|
103
|
+
this.inner = new BoundedMap(options);
|
|
104
|
+
}
|
|
105
|
+
has(value) {
|
|
106
|
+
return this.inner.has(value);
|
|
107
|
+
}
|
|
108
|
+
add(value) {
|
|
109
|
+
this.inner.set(value, true);
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
delete(value) {
|
|
113
|
+
return this.inner.delete(value);
|
|
114
|
+
}
|
|
115
|
+
clear() {
|
|
116
|
+
this.inner.clear();
|
|
117
|
+
}
|
|
118
|
+
get size() {
|
|
119
|
+
return this.inner.size;
|
|
120
|
+
}
|
|
121
|
+
/** How many entries have been dropped to respect `max`, since the last clear. */
|
|
122
|
+
get evictionCount() {
|
|
123
|
+
return this.inner.evictionCount;
|
|
124
|
+
}
|
|
125
|
+
*values() {
|
|
126
|
+
for (const [key] of this.inner) yield key;
|
|
127
|
+
}
|
|
128
|
+
[Symbol.iterator]() {
|
|
129
|
+
return this.values();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
export {
|
|
133
|
+
BoundedMap,
|
|
134
|
+
BoundedSet
|
|
135
|
+
};
|
|
136
|
+
//# sourceMappingURL=bounded-map.js.map
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// src/runtime/credential-patterns.ts
|
|
2
|
+
var CREDENTIAL_PATTERNS = [
|
|
3
|
+
// LLM provider keys
|
|
4
|
+
{
|
|
5
|
+
type: "anthropic_key",
|
|
6
|
+
regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
type: "openai_key",
|
|
10
|
+
regex: /(?<![A-Za-z0-9])sk-(?!ant)(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g
|
|
11
|
+
},
|
|
12
|
+
// GitHub. `ghp_` is only the personal-access-token prefix — the OAuth
|
|
13
|
+
// (`gho_`), user-to-server (`ghu_`), server-to-server (`ghs_`) and
|
|
14
|
+
// refresh (`ghr_`) tokens grant the same or broader access and were
|
|
15
|
+
// previously not detected at all.
|
|
16
|
+
{ type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
|
|
17
|
+
{
|
|
18
|
+
type: "github_oauth_token",
|
|
19
|
+
regex: /(?<![A-Za-z0-9])gh[ousr]_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g
|
|
20
|
+
},
|
|
21
|
+
{ type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g },
|
|
22
|
+
// GitLab
|
|
23
|
+
{ type: "gitlab_pat", regex: /(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
|
|
24
|
+
{
|
|
25
|
+
type: "gitlab_runner_token",
|
|
26
|
+
regex: /(?<![A-Za-z0-9])glrt-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
|
|
27
|
+
},
|
|
28
|
+
// npm — a leaked publish token is a supply-chain compromise.
|
|
29
|
+
{ type: "npm_token", regex: /(?<![A-Za-z0-9])npm_[A-Za-z0-9]{36}(?![A-Za-z0-9])/g },
|
|
30
|
+
// AWS
|
|
31
|
+
{ type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g },
|
|
32
|
+
// GCP
|
|
33
|
+
{ type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g },
|
|
34
|
+
// Slack. `xoxe` (token-rotation) and `xapp` (app-level) were missing;
|
|
35
|
+
// both are as sensitive as the bot/user tokens already covered.
|
|
36
|
+
{
|
|
37
|
+
type: "slack_token",
|
|
38
|
+
regex: /(?<![A-Za-z0-9-])xox[abposer]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g
|
|
39
|
+
},
|
|
40
|
+
{ type: "slack_app_token", regex: /(?<![A-Za-z0-9-])xapp-\d-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
|
|
41
|
+
{
|
|
42
|
+
type: "slack_webhook",
|
|
43
|
+
regex: /https:\/\/hooks\.slack\.com\/services\/T[A-Za-z0-9_-]+\/B[A-Za-z0-9_-]+\/[A-Za-z0-9]{16,}/g
|
|
44
|
+
},
|
|
45
|
+
// Stripe
|
|
46
|
+
{
|
|
47
|
+
type: "stripe_key",
|
|
48
|
+
regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g
|
|
49
|
+
},
|
|
50
|
+
// Twilio
|
|
51
|
+
{ type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
|
|
52
|
+
// Telegram
|
|
53
|
+
{
|
|
54
|
+
type: "telegram_bot_token",
|
|
55
|
+
regex: /(?:(?<![A-Za-z0-9_])|(?<=(?:^|[^A-Za-z0-9_])bot))\d+:[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
|
|
56
|
+
},
|
|
57
|
+
// JWT
|
|
58
|
+
{
|
|
59
|
+
type: "jwt",
|
|
60
|
+
regex: /(?<![A-Za-z0-9/+=])eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9/+=])/g
|
|
61
|
+
},
|
|
62
|
+
// Private keys
|
|
63
|
+
{
|
|
64
|
+
type: "private_key",
|
|
65
|
+
regex: /(?:^|\n)(?:-----BEGIN (?:RSA|EC|OPENSSH|DSA)? ?PRIVATE KEY-----[\s\S]*?-----END (?:RSA|EC|OPENSSH|DSA)? ?PRIVATE KEY-----|-----BEGIN PGP PRIVATE KEY BLOCK-----[\s\S]*?-----END PGP PRIVATE KEY BLOCK-----)(?!\S)/g
|
|
66
|
+
},
|
|
67
|
+
// AI/ML provider tokens
|
|
68
|
+
{ type: "huggingface_token", regex: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{34}(?![A-Za-z0-9])/g },
|
|
69
|
+
{ type: "replicate_token", regex: /(?<![A-Za-z0-9])r8_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
|
|
70
|
+
{ type: "perplexity_key", regex: /(?<![A-Za-z0-9])pplx-[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
|
|
71
|
+
{ type: "groq_key", regex: /(?<![A-Za-z0-9])gsk_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
|
|
72
|
+
// SaaS / infrastructure tokens — each grants API access on the user's
|
|
73
|
+
// account, and each was previously invisible to this gate.
|
|
74
|
+
{
|
|
75
|
+
type: "sendgrid_key",
|
|
76
|
+
regex: /(?<![A-Za-z0-9])SG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
|
|
77
|
+
},
|
|
78
|
+
{ type: "digitalocean_token", regex: /(?<![A-Za-z0-9])dop_v1_[a-f0-9]{64}(?![A-Za-z0-9])/g },
|
|
79
|
+
{
|
|
80
|
+
type: "doppler_token",
|
|
81
|
+
regex: /(?<![A-Za-z0-9])dp\.(?:pt|st|sa|scim|audit)\.[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
type: "shopify_token",
|
|
85
|
+
regex: /(?<![A-Za-z0-9])shp(?:at|ca|pa|ss)_[a-fA-F0-9]{32}(?![A-Za-z0-9])/g
|
|
86
|
+
},
|
|
87
|
+
{ type: "docker_pat", regex: /(?<![A-Za-z0-9])dckr_pat_[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
|
|
88
|
+
{ type: "linear_key", regex: /(?<![A-Za-z0-9])lin_api_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
|
|
89
|
+
{
|
|
90
|
+
type: "atlassian_token",
|
|
91
|
+
regex: /(?<![A-Za-z0-9])ATATT3[A-Za-z0-9_\-=]{40,}(?![A-Za-z0-9_\-=])/g
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
type: "square_token",
|
|
95
|
+
regex: /(?<![A-Za-z0-9])(?:sq0(?:atp|csp)-|EAAA)[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
type: "azure_storage_key",
|
|
99
|
+
regex: /AccountKey=[A-Za-z0-9+/]{80,}={0,2}/g
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
type: "google_oauth_client_secret",
|
|
103
|
+
regex: /(?<![A-Za-z0-9_-])GOCSPX-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
|
|
104
|
+
},
|
|
105
|
+
// Bearer tokens
|
|
106
|
+
{
|
|
107
|
+
type: "bearer_token",
|
|
108
|
+
regex: /(?<![A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{12,512}=*(?![A-Za-z0-9._~+/-])/g
|
|
109
|
+
},
|
|
110
|
+
// Database URIs. Require password-bearing user-info; credential-free values stay scannable.
|
|
111
|
+
{ type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g },
|
|
112
|
+
{
|
|
113
|
+
type: "postgres_uri",
|
|
114
|
+
// Query parsers decode percent-encoded parameter names. Recognize each
|
|
115
|
+
// encoded character in `password` so mixed forms such as `pass%77ord`
|
|
116
|
+
// cannot bypass detection while keeping the scan strictly bounded.
|
|
117
|
+
regex: /postgres(?:ql)?:\/\/(?:[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+|[^&\s?"'`#]{1,2048}\?(?:(?!(?:p|%70)(?:a|%61)(?:s|%73)(?:s|%73)(?:w|%77)(?:o|%6[fF])(?:r|%72)(?:d|%64)=)[^&\s#"'`]{1,256}&){0,32}(?:p|%70)(?:a|%61)(?:s|%73)(?:s|%73)(?:w|%77)(?:o|%6[fF])(?:r|%72)(?:d|%64)=[^&\s#"'`]{1,4096})/g
|
|
118
|
+
},
|
|
119
|
+
{ type: "mysql_uri", regex: /mysql:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g },
|
|
120
|
+
{ type: "redis_uri", regex: /redis:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g },
|
|
121
|
+
{
|
|
122
|
+
// Credentials serialised as JSON, keyed rather than prefixed. Every other
|
|
123
|
+
// entry in this table recognises a credential by its SHAPE (`ghp_`, `sk-`,
|
|
124
|
+
// `eyJ`), which means a key with no distinctive prefix — Azure, a
|
|
125
|
+
// self-hosted gateway, an Anthropic/Codex OAuth token — was invisible to
|
|
126
|
+
// both surfaces. `prompt-firewall` guards the outgoing provider request, so
|
|
127
|
+
// this is what stops a JSON-shaped tool result carrying such a value to a
|
|
128
|
+
// third party.
|
|
129
|
+
//
|
|
130
|
+
// The key is matched in a LOOKBEHIND, so the reported match is the secret
|
|
131
|
+
// itself and the pattern keeps zero capturing groups — `secret-scanner`
|
|
132
|
+
// maps a combined-regex group index back to the pattern that fired, and an
|
|
133
|
+
// inner group would shift that mapping (see the style note above, enforced
|
|
134
|
+
// by credential-pattern-parity.test.ts).
|
|
135
|
+
//
|
|
136
|
+
// Mirrors `json_credential_key` in
|
|
137
|
+
// `@wrongstack/core` → `src/security/secret-scrubber.ts`. Keep the two key
|
|
138
|
+
// lists in step; the core side additionally preserves the key name when it
|
|
139
|
+
// rewrites, which is why it is written with capture groups instead.
|
|
140
|
+
type: "json_credential_key",
|
|
141
|
+
regex: /(?<="[A-Za-z0-9_]{0,64}(?:apiKey|api_key|token|secret|password|authorization|bearer|private_key|access_token|refresh_token|client_secret)"\s{0,8}:\s{0,8}")[^"\\]{8,512}(?=")/gi
|
|
142
|
+
}
|
|
143
|
+
];
|
|
144
|
+
function cloneCredentialPatterns() {
|
|
145
|
+
return CREDENTIAL_PATTERNS.map((p) => ({
|
|
146
|
+
type: p.type,
|
|
147
|
+
regex: new RegExp(p.regex.source, p.regex.flags)
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
export {
|
|
151
|
+
CREDENTIAL_PATTERNS,
|
|
152
|
+
cloneCredentialPatterns
|
|
153
|
+
};
|
|
154
|
+
//# sourceMappingURL=credential-patterns.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// src/runtime/h1-state.ts
|
|
2
|
+
function createH1State(initial) {
|
|
3
|
+
const handles = /* @__PURE__ */ new Map();
|
|
4
|
+
const safeRelease = (unregister) => {
|
|
5
|
+
try {
|
|
6
|
+
unregister();
|
|
7
|
+
} catch {
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
return {
|
|
11
|
+
state: initial,
|
|
12
|
+
register(key, unregister) {
|
|
13
|
+
const prior = handles.get(key);
|
|
14
|
+
if (prior) {
|
|
15
|
+
safeRelease(prior);
|
|
16
|
+
handles.delete(key);
|
|
17
|
+
}
|
|
18
|
+
if (unregister) {
|
|
19
|
+
handles.set(key, unregister);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
release(key) {
|
|
23
|
+
const prior = handles.get(key);
|
|
24
|
+
if (!prior) return;
|
|
25
|
+
handles.delete(key);
|
|
26
|
+
safeRelease(prior);
|
|
27
|
+
},
|
|
28
|
+
releaseAll() {
|
|
29
|
+
for (const unregister of handles.values()) {
|
|
30
|
+
safeRelease(unregister);
|
|
31
|
+
}
|
|
32
|
+
handles.clear();
|
|
33
|
+
},
|
|
34
|
+
size() {
|
|
35
|
+
return handles.size;
|
|
36
|
+
},
|
|
37
|
+
keys() {
|
|
38
|
+
return [...handles.keys()];
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
createH1State
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=h1-state.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/runtime/handles.ts
|
|
2
|
+
function releaseHandle(off) {
|
|
3
|
+
if (off) {
|
|
4
|
+
try {
|
|
5
|
+
off();
|
|
6
|
+
} catch {
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
function releaseHandles(state, keys) {
|
|
12
|
+
for (const key of keys) {
|
|
13
|
+
state[key] = releaseHandle(state[key]);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
releaseHandle,
|
|
18
|
+
releaseHandles
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=handles.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// src/runtime/llm.ts
|
|
2
|
+
function stripOuterMarkdownFence(text) {
|
|
3
|
+
const trimmed = text.trim();
|
|
4
|
+
const match = trimmed.match(/^```(?:[a-z0-9_-]+)?\s*\r?\n([\s\S]*?)\r?\n```$/i);
|
|
5
|
+
return (match?.[1] ?? trimmed).trim();
|
|
6
|
+
}
|
|
7
|
+
function parseLlmJsonObject(text) {
|
|
8
|
+
const candidate = stripOuterMarkdownFence(text);
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(candidate);
|
|
11
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function runOptionalPluginLlm(request) {
|
|
17
|
+
if (!request.requested) {
|
|
18
|
+
return { used: false, value: null, fallbackReason: "not-requested" };
|
|
19
|
+
}
|
|
20
|
+
if (!request.api.llm) {
|
|
21
|
+
return { used: false, value: null, fallbackReason: "unavailable" };
|
|
22
|
+
}
|
|
23
|
+
if (request.options?.signal?.aborted) {
|
|
24
|
+
return { used: false, value: null, fallbackReason: "cancelled" };
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const response = await request.api.llm.complete(request.prompt, request.options);
|
|
28
|
+
const parsed = request.parse(response.text);
|
|
29
|
+
if (parsed === null) {
|
|
30
|
+
request.api.log.warn(`${request.label}: ignored invalid LLM response`);
|
|
31
|
+
return { used: false, value: null, fallbackReason: "invalid-response" };
|
|
32
|
+
}
|
|
33
|
+
return { used: true, value: parsed, fallbackReason: null };
|
|
34
|
+
} catch (error) {
|
|
35
|
+
const cancelled = request.options?.signal?.aborted === true;
|
|
36
|
+
request.api.log.warn(`${request.label}: LLM enrichment failed; using deterministic fallback`, {
|
|
37
|
+
error: error instanceof Error ? error.message : String(error)
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
used: false,
|
|
41
|
+
value: null,
|
|
42
|
+
fallbackReason: cancelled ? "cancelled" : "provider-error"
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function runOptionalPluginCouncil(request) {
|
|
47
|
+
if (!request.requested) {
|
|
48
|
+
return { used: false, value: null, fallbackReason: "not-requested" };
|
|
49
|
+
}
|
|
50
|
+
if (request.options?.signal?.aborted) {
|
|
51
|
+
return { used: false, value: null, fallbackReason: "cancelled" };
|
|
52
|
+
}
|
|
53
|
+
const council = request.api.llm?.council;
|
|
54
|
+
if (council) {
|
|
55
|
+
try {
|
|
56
|
+
const result = await council(request.prompt, {
|
|
57
|
+
...request.context ? { context: request.context } : {},
|
|
58
|
+
...request.profile ? { profile: request.profile } : {},
|
|
59
|
+
...request.councilOptions ? { options: request.councilOptions } : {},
|
|
60
|
+
...request.options?.signal ? { signal: request.options.signal } : {}
|
|
61
|
+
});
|
|
62
|
+
if (result.status === "cancelled") {
|
|
63
|
+
return { used: false, value: null, fallbackReason: "cancelled" };
|
|
64
|
+
}
|
|
65
|
+
const parsed = result.status === "decided" ? request.parse(result.answer ?? "") : null;
|
|
66
|
+
if (parsed !== null) return { used: true, value: parsed, fallbackReason: null };
|
|
67
|
+
request.api.log.warn(
|
|
68
|
+
`${request.label}: Council did not return a valid answer; trying One Shot`,
|
|
69
|
+
{
|
|
70
|
+
status: result.status,
|
|
71
|
+
resolution: result.resolution
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (request.options?.signal?.aborted) {
|
|
76
|
+
return { used: false, value: null, fallbackReason: "cancelled" };
|
|
77
|
+
}
|
|
78
|
+
request.api.log.warn(`${request.label}: Council failed; trying One Shot`, {
|
|
79
|
+
error: error instanceof Error ? error.message : String(error)
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return runOptionalPluginLlm(request);
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
parseLlmJsonObject,
|
|
87
|
+
runOptionalPluginCouncil,
|
|
88
|
+
runOptionalPluginLlm,
|
|
89
|
+
stripOuterMarkdownFence
|
|
90
|
+
};
|
|
91
|
+
//# sourceMappingURL=llm.js.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// src/runtime/local-bin.ts
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { accessSync, constants, readFileSync } from "node:fs";
|
|
5
|
+
import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
|
|
6
|
+
function resolveExecInvocation(command, args = []) {
|
|
7
|
+
const resolved = resolveWin32Command(command);
|
|
8
|
+
const normalizedResolved = resolved.toLowerCase();
|
|
9
|
+
const needsShell = process.platform === "win32" && (normalizedResolved.endsWith(".cmd") || normalizedResolved.endsWith(".bat"));
|
|
10
|
+
if (needsShell) {
|
|
11
|
+
const shim = buildWin32CmdShimInvocation(resolved, args);
|
|
12
|
+
return { cmd: shim.command, args: shim.args, windowsVerbatimArguments: true };
|
|
13
|
+
}
|
|
14
|
+
return { cmd: resolved, args: [...args], windowsVerbatimArguments: false };
|
|
15
|
+
}
|
|
16
|
+
function findOnPath(cmd) {
|
|
17
|
+
if (!cmd) return null;
|
|
18
|
+
const exists = (p) => {
|
|
19
|
+
try {
|
|
20
|
+
accessSync(p, constants.X_OK);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
if (cmd.includes("/") || cmd.includes("\\")) {
|
|
27
|
+
return exists(cmd) ? resolve(cmd) : null;
|
|
28
|
+
}
|
|
29
|
+
const suffixes = process.platform === "win32" && extname(cmd) === "" ? (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
30
|
+
for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
|
|
31
|
+
if (!dir) continue;
|
|
32
|
+
const base = join(dir, cmd);
|
|
33
|
+
for (const suffix of suffixes) {
|
|
34
|
+
const candidate = `${base}${suffix}`;
|
|
35
|
+
if (exists(candidate)) return candidate;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function isInside(parent, candidate) {
|
|
41
|
+
const rel = relative(parent, candidate);
|
|
42
|
+
return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
|
|
43
|
+
}
|
|
44
|
+
var binCache = /* @__PURE__ */ new Map();
|
|
45
|
+
var BIN_CACHE_MAX = 64;
|
|
46
|
+
var NEGATIVE_BIN_CACHE_TTL_MS = 5e3;
|
|
47
|
+
function cachePut(key, value) {
|
|
48
|
+
while (binCache.size >= BIN_CACHE_MAX) {
|
|
49
|
+
const oldest = binCache.keys().next().value;
|
|
50
|
+
if (oldest === void 0) break;
|
|
51
|
+
binCache.delete(oldest);
|
|
52
|
+
}
|
|
53
|
+
binCache.set(key, { value, cachedAt: Date.now() });
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
function clearLocalBinCache() {
|
|
57
|
+
binCache.clear();
|
|
58
|
+
}
|
|
59
|
+
function resolveNodeBin(packageName, binName, cwd, extraArgs = []) {
|
|
60
|
+
const key = `${packageName}|${binName}|${cwd}`;
|
|
61
|
+
const cached = binCache.get(key);
|
|
62
|
+
if (cached !== void 0) {
|
|
63
|
+
if (cached.value !== null || Date.now() - cached.cachedAt < NEGATIVE_BIN_CACHE_TTL_MS) {
|
|
64
|
+
return cached.value === null ? null : { ...cached.value, args: [cached.value.entry, ...extraArgs] };
|
|
65
|
+
}
|
|
66
|
+
binCache.delete(key);
|
|
67
|
+
}
|
|
68
|
+
let resolved = null;
|
|
69
|
+
try {
|
|
70
|
+
const requireFromProject = createRequire(resolve(cwd, "package.json"));
|
|
71
|
+
const packagePath = requireFromProject.resolve(`${packageName}/package.json`);
|
|
72
|
+
const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
|
|
73
|
+
const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[binName] ?? Object.values(packageJson.bin ?? {})[0];
|
|
74
|
+
if (relativeBin && !isAbsolute(relativeBin)) {
|
|
75
|
+
const packageDir = dirname(packagePath);
|
|
76
|
+
const entry = resolve(packageDir, relativeBin);
|
|
77
|
+
if (isInside(packageDir, entry)) {
|
|
78
|
+
resolved = { cmd: process.execPath, args: [entry], entry };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
resolved = null;
|
|
83
|
+
}
|
|
84
|
+
cachePut(key, resolved);
|
|
85
|
+
return resolved === null ? null : { ...resolved, args: [resolved.entry, ...extraArgs] };
|
|
86
|
+
}
|
|
87
|
+
function resolveFirstNodeBin(candidates, cwd) {
|
|
88
|
+
for (const c of candidates) {
|
|
89
|
+
const hit = resolveNodeBin(c.packageName, c.binName, cwd, c.args ?? []);
|
|
90
|
+
if (hit) return { ...hit, packageName: c.packageName, binName: c.binName };
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
clearLocalBinCache,
|
|
96
|
+
findOnPath,
|
|
97
|
+
resolveExecInvocation,
|
|
98
|
+
resolveFirstNodeBin,
|
|
99
|
+
resolveNodeBin,
|
|
100
|
+
resolveWin32Command
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=local-bin.js.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// src/runtime/redos-guard.ts
|
|
2
|
+
import { Worker } from "node:worker_threads";
|
|
3
|
+
var warm = null;
|
|
4
|
+
var callSeq = 0;
|
|
5
|
+
function withReDoSGuard(re, input, budgetMs = 50, options = {}) {
|
|
6
|
+
const opts = { budgetMs, ...options };
|
|
7
|
+
const start = Date.now();
|
|
8
|
+
const id = ++callSeq;
|
|
9
|
+
let worker;
|
|
10
|
+
if (warm !== null) {
|
|
11
|
+
worker = warm;
|
|
12
|
+
} else {
|
|
13
|
+
worker = spawnPoolWorker();
|
|
14
|
+
}
|
|
15
|
+
warm = null;
|
|
16
|
+
worker.ref();
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
let settled = false;
|
|
19
|
+
const settle = (result) => {
|
|
20
|
+
if (settled) return;
|
|
21
|
+
settled = true;
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
worker.off("message", onMessage);
|
|
24
|
+
worker.off("error", onError);
|
|
25
|
+
resolve(result);
|
|
26
|
+
};
|
|
27
|
+
const onMessage = (msg) => {
|
|
28
|
+
if (settled || msg.id !== id) return;
|
|
29
|
+
worker.unref();
|
|
30
|
+
if (warm === null) {
|
|
31
|
+
warm = worker;
|
|
32
|
+
} else {
|
|
33
|
+
worker.terminate().catch(() => {
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (!msg.ok) {
|
|
37
|
+
settle({ timedOut: true, match: null });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
settle({ timedOut: false, match: msg.match });
|
|
41
|
+
};
|
|
42
|
+
const onError = () => {
|
|
43
|
+
if (settled) return;
|
|
44
|
+
settle({ timedOut: true, match: null });
|
|
45
|
+
};
|
|
46
|
+
const timer = setTimeout(() => {
|
|
47
|
+
if (settled) return;
|
|
48
|
+
const elapsedMs = Date.now() - start;
|
|
49
|
+
worker.terminate().catch(() => {
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
opts.onTimeout?.({
|
|
53
|
+
regex: re,
|
|
54
|
+
input,
|
|
55
|
+
budgetMs: opts.budgetMs,
|
|
56
|
+
elapsedMs
|
|
57
|
+
});
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
settle({ timedOut: true, match: null });
|
|
61
|
+
}, opts.budgetMs);
|
|
62
|
+
timer.unref?.();
|
|
63
|
+
worker.once("message", onMessage);
|
|
64
|
+
worker.once("error", onError);
|
|
65
|
+
worker.postMessage({ id, source: re.source, flags: re.flags, input });
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
var POOLED_WORKER_SOURCE = `
|
|
69
|
+
const { parentPort } = require('node:worker_threads');
|
|
70
|
+
parentPort.on('message', (msg) => {
|
|
71
|
+
const { id, source, flags, input } = msg;
|
|
72
|
+
try {
|
|
73
|
+
const re = new RegExp(source, flags);
|
|
74
|
+
const match = re.exec(input);
|
|
75
|
+
parentPort.postMessage({ id, ok: true, match });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
parentPort.postMessage({ id, ok: false, error: err && err.message ? err.message : String(err) });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
`;
|
|
81
|
+
function spawnPoolWorker() {
|
|
82
|
+
const worker = new Worker(POOLED_WORKER_SOURCE, {
|
|
83
|
+
eval: true,
|
|
84
|
+
name: "redos-guard:pool"
|
|
85
|
+
});
|
|
86
|
+
worker.unref();
|
|
87
|
+
return worker;
|
|
88
|
+
}
|
|
89
|
+
function guardedMatcher(re, budgetMs = 50, onTimeout) {
|
|
90
|
+
return (input) => withReDoSGuard(re, input, budgetMs, onTimeout ? { onTimeout } : {});
|
|
91
|
+
}
|
|
92
|
+
export {
|
|
93
|
+
guardedMatcher,
|
|
94
|
+
withReDoSGuard
|
|
95
|
+
};
|
|
96
|
+
//# sourceMappingURL=redos-guard.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/runtime/safe-json.ts
|
|
2
|
+
var UNSERIALIZABLE = "[unserializable]";
|
|
3
|
+
function safeJsonStringify(value, indent) {
|
|
4
|
+
try {
|
|
5
|
+
const stack = [];
|
|
6
|
+
const out = JSON.stringify(
|
|
7
|
+
value,
|
|
8
|
+
function replacer(_key, val) {
|
|
9
|
+
if (typeof val === "bigint") return `${val.toString()}n`;
|
|
10
|
+
if (val === null || typeof val !== "object") return val;
|
|
11
|
+
while (stack.length > 0 && stack[stack.length - 1] !== this) stack.pop();
|
|
12
|
+
if (stack.includes(val)) return "[circular]";
|
|
13
|
+
stack.push(val);
|
|
14
|
+
return val;
|
|
15
|
+
},
|
|
16
|
+
indent
|
|
17
|
+
);
|
|
18
|
+
return out ?? String(value);
|
|
19
|
+
} catch {
|
|
20
|
+
return UNSERIALIZABLE;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
UNSERIALIZABLE,
|
|
25
|
+
safeJsonStringify
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=safe-json.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/runtime/sandbox.ts
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
var MAX_PATH_BYTES = 4096;
|
|
5
|
+
function safePath(input, options = {}) {
|
|
6
|
+
if (typeof input !== "string") return null;
|
|
7
|
+
if (input.length === 0 || input.length > MAX_PATH_BYTES) return null;
|
|
8
|
+
if (input.startsWith("-")) return null;
|
|
9
|
+
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
10
|
+
const lexical = isAbsolute(input) ? resolve(input) : resolve(projectRoot, input);
|
|
11
|
+
if (!withinLexical(projectRoot, lexical)) return null;
|
|
12
|
+
if (options.followSymlinks !== false) {
|
|
13
|
+
const real = realpathWithMissingLeaf(lexical);
|
|
14
|
+
if (real === null || !withinLexical(projectRoot, real)) return null;
|
|
15
|
+
return real;
|
|
16
|
+
}
|
|
17
|
+
return lexical;
|
|
18
|
+
}
|
|
19
|
+
function realpathWithMissingLeaf(candidate) {
|
|
20
|
+
let current = candidate;
|
|
21
|
+
const missing = [];
|
|
22
|
+
while (true) {
|
|
23
|
+
try {
|
|
24
|
+
const resolved = realpathSync(current);
|
|
25
|
+
return missing.reduceRight((parent, part) => join(parent, part), resolved);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
const code = error.code;
|
|
28
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") return null;
|
|
29
|
+
const parent = dirname(current);
|
|
30
|
+
if (parent === current) return null;
|
|
31
|
+
missing.push(basename(current));
|
|
32
|
+
current = parent;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function withinLexical(projectRoot, candidate) {
|
|
37
|
+
const rel = relative(projectRoot, candidate);
|
|
38
|
+
if (rel === "" || rel === ".") return true;
|
|
39
|
+
if (rel.startsWith("..")) return false;
|
|
40
|
+
if (isAbsolute(rel)) return false;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
function isInsideProject(input, options = {}) {
|
|
44
|
+
return safePath(input, options) !== null;
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
isInsideProject,
|
|
48
|
+
safePath
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=sandbox.js.map
|
package/dist/runtime.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/runtime/index.ts
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
4
|
-
import { basename, extname as extname2, isAbsolute as isAbsolute3, relative as relative3, resolve as resolve3 } from "node:path";
|
|
5
|
-
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
4
|
+
import { basename as basename2, extname as extname2, isAbsolute as isAbsolute3, relative as relative3, resolve as resolve3 } from "node:path";
|
|
5
|
+
import { buildChildEnv } from "@wrongstack/core/utils/child-env";
|
|
6
6
|
|
|
7
7
|
// src/runtime/llm.ts
|
|
8
8
|
function stripOuterMarkdownFence(text) {
|
|
@@ -506,39 +506,51 @@ function releaseHandles(state, keys) {
|
|
|
506
506
|
|
|
507
507
|
// src/runtime/redos-guard.ts
|
|
508
508
|
import { Worker } from "node:worker_threads";
|
|
509
|
+
var warm = null;
|
|
510
|
+
var callSeq = 0;
|
|
509
511
|
function withReDoSGuard(re, input, budgetMs = 50, options = {}) {
|
|
510
512
|
const opts = { budgetMs, ...options };
|
|
511
513
|
const start = Date.now();
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
514
|
+
const id = ++callSeq;
|
|
515
|
+
let worker;
|
|
516
|
+
if (warm !== null) {
|
|
517
|
+
worker = warm;
|
|
518
|
+
} else {
|
|
519
|
+
worker = spawnPoolWorker();
|
|
520
|
+
}
|
|
521
|
+
warm = null;
|
|
522
|
+
worker.ref();
|
|
517
523
|
return new Promise((resolve4) => {
|
|
518
524
|
let settled = false;
|
|
519
|
-
const
|
|
525
|
+
const settle = (result) => {
|
|
520
526
|
if (settled) return;
|
|
521
527
|
settled = true;
|
|
522
528
|
clearTimeout(timer);
|
|
523
|
-
worker.
|
|
524
|
-
|
|
529
|
+
worker.off("message", onMessage);
|
|
530
|
+
worker.off("error", onError);
|
|
531
|
+
resolve4(result);
|
|
532
|
+
};
|
|
533
|
+
const onMessage = (msg) => {
|
|
534
|
+
if (settled || msg.id !== id) return;
|
|
535
|
+
worker.unref();
|
|
536
|
+
if (warm === null) {
|
|
537
|
+
warm = worker;
|
|
538
|
+
} else {
|
|
539
|
+
worker.terminate().catch(() => {
|
|
540
|
+
});
|
|
541
|
+
}
|
|
525
542
|
if (!msg.ok) {
|
|
526
|
-
|
|
543
|
+
settle({ timedOut: true, match: null });
|
|
527
544
|
return;
|
|
528
545
|
}
|
|
529
|
-
|
|
546
|
+
settle({ timedOut: false, match: msg.match });
|
|
530
547
|
};
|
|
531
548
|
const onError = () => {
|
|
532
549
|
if (settled) return;
|
|
533
|
-
|
|
534
|
-
clearTimeout(timer);
|
|
535
|
-
worker.terminate().catch(() => {
|
|
536
|
-
});
|
|
537
|
-
resolve4({ timedOut: true, match: null });
|
|
550
|
+
settle({ timedOut: true, match: null });
|
|
538
551
|
};
|
|
539
552
|
const timer = setTimeout(() => {
|
|
540
553
|
if (settled) return;
|
|
541
|
-
settled = true;
|
|
542
554
|
const elapsedMs = Date.now() - start;
|
|
543
555
|
worker.terminate().catch(() => {
|
|
544
556
|
});
|
|
@@ -551,34 +563,34 @@ function withReDoSGuard(re, input, budgetMs = 50, options = {}) {
|
|
|
551
563
|
});
|
|
552
564
|
} catch {
|
|
553
565
|
}
|
|
554
|
-
|
|
566
|
+
settle({ timedOut: true, match: null });
|
|
555
567
|
}, opts.budgetMs);
|
|
556
568
|
timer.unref?.();
|
|
557
|
-
worker.
|
|
558
|
-
worker.
|
|
569
|
+
worker.once("message", onMessage);
|
|
570
|
+
worker.once("error", onError);
|
|
571
|
+
worker.postMessage({ id, source: re.source, flags: re.flags, input });
|
|
559
572
|
});
|
|
560
573
|
}
|
|
561
|
-
|
|
562
|
-
const S = JSON.stringify(source);
|
|
563
|
-
const I = JSON.stringify(input);
|
|
564
|
-
const F = JSON.stringify(flags);
|
|
565
|
-
return `
|
|
574
|
+
var POOLED_WORKER_SOURCE = `
|
|
566
575
|
const { parentPort } = require('node:worker_threads');
|
|
567
|
-
|
|
568
|
-
const input =
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
parentPort.postMessage({ ok: true, match });
|
|
578
|
-
} catch (err) {
|
|
579
|
-
parentPort.postMessage({ ok: false, error: err && err.message ? err.message : String(err) });
|
|
580
|
-
}
|
|
576
|
+
parentPort.on('message', (msg) => {
|
|
577
|
+
const { id, source, flags, input } = msg;
|
|
578
|
+
try {
|
|
579
|
+
const re = new RegExp(source, flags);
|
|
580
|
+
const match = re.exec(input);
|
|
581
|
+
parentPort.postMessage({ id, ok: true, match });
|
|
582
|
+
} catch (err) {
|
|
583
|
+
parentPort.postMessage({ id, ok: false, error: err && err.message ? err.message : String(err) });
|
|
584
|
+
}
|
|
585
|
+
});
|
|
581
586
|
`;
|
|
587
|
+
function spawnPoolWorker() {
|
|
588
|
+
const worker = new Worker(POOLED_WORKER_SOURCE, {
|
|
589
|
+
eval: true,
|
|
590
|
+
name: "redos-guard:pool"
|
|
591
|
+
});
|
|
592
|
+
worker.unref();
|
|
593
|
+
return worker;
|
|
582
594
|
}
|
|
583
595
|
function guardedMatcher(re, budgetMs = 50, onTimeout) {
|
|
584
596
|
return (input) => withReDoSGuard(re, input, budgetMs, onTimeout ? { onTimeout } : {});
|
|
@@ -586,7 +598,7 @@ function guardedMatcher(re, budgetMs = 50, onTimeout) {
|
|
|
586
598
|
|
|
587
599
|
// src/runtime/sandbox.ts
|
|
588
600
|
import { realpathSync } from "node:fs";
|
|
589
|
-
import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2 } from "node:path";
|
|
601
|
+
import { basename, dirname as dirname2, isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve2 } from "node:path";
|
|
590
602
|
var MAX_PATH_BYTES = 4096;
|
|
591
603
|
function safePath(input, options = {}) {
|
|
592
604
|
if (typeof input !== "string") return null;
|
|
@@ -596,17 +608,29 @@ function safePath(input, options = {}) {
|
|
|
596
608
|
const lexical = isAbsolute2(input) ? resolve2(input) : resolve2(projectRoot, input);
|
|
597
609
|
if (!withinLexical(projectRoot, lexical)) return null;
|
|
598
610
|
if (options.followSymlinks !== false) {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
real = realpathSync(lexical);
|
|
602
|
-
} catch {
|
|
603
|
-
return null;
|
|
604
|
-
}
|
|
605
|
-
if (!withinLexical(projectRoot, real)) return null;
|
|
611
|
+
const real = realpathWithMissingLeaf(lexical);
|
|
612
|
+
if (real === null || !withinLexical(projectRoot, real)) return null;
|
|
606
613
|
return real;
|
|
607
614
|
}
|
|
608
615
|
return lexical;
|
|
609
616
|
}
|
|
617
|
+
function realpathWithMissingLeaf(candidate) {
|
|
618
|
+
let current = candidate;
|
|
619
|
+
const missing = [];
|
|
620
|
+
while (true) {
|
|
621
|
+
try {
|
|
622
|
+
const resolved = realpathSync(current);
|
|
623
|
+
return missing.reduceRight((parent, part) => join2(parent, part), resolved);
|
|
624
|
+
} catch (error) {
|
|
625
|
+
const code = error.code;
|
|
626
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") return null;
|
|
627
|
+
const parent = dirname2(current);
|
|
628
|
+
if (parent === current) return null;
|
|
629
|
+
missing.push(basename(current));
|
|
630
|
+
current = parent;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
610
634
|
function withinLexical(projectRoot, candidate) {
|
|
611
635
|
const rel = relative2(projectRoot, candidate);
|
|
612
636
|
if (rel === "" || rel === ".") return true;
|
|
@@ -731,7 +755,7 @@ function resolveRunnerCommand(runtime, command, options = {}) {
|
|
|
731
755
|
}
|
|
732
756
|
if (isAbsolute3(head)) {
|
|
733
757
|
if (!withinProjectPath(projectRoot, head)) return null;
|
|
734
|
-
const base =
|
|
758
|
+
const base = basename2(head);
|
|
735
759
|
if (base !== runtime.executable && base !== launcher) return null;
|
|
736
760
|
if (second !== runtime.executable) return null;
|
|
737
761
|
if (!everyFlagAllowed(runtime.allowedFlags, rest)) return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/plugin-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Authoring SDK for WrongStack plugins — the plugin contract types, definePlugin, and shared runtime helpers. Third-party plugins should depend on this package only.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ECOSTACK TECHNOLOGY OÜ",
|
|
@@ -16,6 +16,42 @@
|
|
|
16
16
|
"types": "./dist/runtime.d.ts",
|
|
17
17
|
"import": "./dist/runtime.js"
|
|
18
18
|
},
|
|
19
|
+
"./runtime/bounded-map": {
|
|
20
|
+
"types": "./dist/runtime/bounded-map.d.ts",
|
|
21
|
+
"import": "./dist/runtime/bounded-map.js"
|
|
22
|
+
},
|
|
23
|
+
"./runtime/credential-patterns": {
|
|
24
|
+
"types": "./dist/runtime/credential-patterns.d.ts",
|
|
25
|
+
"import": "./dist/runtime/credential-patterns.js"
|
|
26
|
+
},
|
|
27
|
+
"./runtime/h1-state": {
|
|
28
|
+
"types": "./dist/runtime/h1-state.d.ts",
|
|
29
|
+
"import": "./dist/runtime/h1-state.js"
|
|
30
|
+
},
|
|
31
|
+
"./runtime/handles": {
|
|
32
|
+
"types": "./dist/runtime/handles.d.ts",
|
|
33
|
+
"import": "./dist/runtime/handles.js"
|
|
34
|
+
},
|
|
35
|
+
"./runtime/llm": {
|
|
36
|
+
"types": "./dist/runtime/llm.d.ts",
|
|
37
|
+
"import": "./dist/runtime/llm.js"
|
|
38
|
+
},
|
|
39
|
+
"./runtime/local-bin": {
|
|
40
|
+
"types": "./dist/runtime/local-bin.d.ts",
|
|
41
|
+
"import": "./dist/runtime/local-bin.js"
|
|
42
|
+
},
|
|
43
|
+
"./runtime/redos-guard": {
|
|
44
|
+
"types": "./dist/runtime/redos-guard.d.ts",
|
|
45
|
+
"import": "./dist/runtime/redos-guard.js"
|
|
46
|
+
},
|
|
47
|
+
"./runtime/safe-json": {
|
|
48
|
+
"types": "./dist/runtime/safe-json.d.ts",
|
|
49
|
+
"import": "./dist/runtime/safe-json.js"
|
|
50
|
+
},
|
|
51
|
+
"./runtime/sandbox": {
|
|
52
|
+
"types": "./dist/runtime/sandbox.d.ts",
|
|
53
|
+
"import": "./dist/runtime/sandbox.js"
|
|
54
|
+
},
|
|
19
55
|
"./package.json": "./package.json"
|
|
20
56
|
},
|
|
21
57
|
"files": [
|
|
@@ -29,8 +65,8 @@
|
|
|
29
65
|
"vitest": "^4.1.11"
|
|
30
66
|
},
|
|
31
67
|
"dependencies": {
|
|
32
|
-
"@wrongstack/core": "0.
|
|
33
|
-
"@wrongstack/tools": "0.
|
|
68
|
+
"@wrongstack/core": "1.0.0",
|
|
69
|
+
"@wrongstack/tools": "1.0.0"
|
|
34
70
|
},
|
|
35
71
|
"scripts": {
|
|
36
72
|
"build": "node ../../scripts/build-package.mjs",
|