failproofai 1.0.8-beta.0 → 1.0.8
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +3 -3
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13t1zkw._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_0o4xkpl._.js → _0-vcssj._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_13kfn90._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0aiy-os._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
- package/.next/standalone/.next/static/chunks/{3o3f1ibfci0p7.js → 06dnzbolj00mc.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0ahfwmbkpgfw4.js → 0m-9d6yn9hx4j.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3bgot5v6f6s3c.js → 0wbjy0zo-m7is.js} +1 -1
- package/.next/standalone/.next/static/chunks/16f3fa-lx38hk.js +1 -0
- package/.next/standalone/.next/static/chunks/{1v_tp3hm8wyhe.js → 21uv-uusw329x.js} +1 -1
- package/.next/standalone/.next/static/chunks/2g2tki08kdhie.js +1 -0
- package/.next/standalone/.next/static/chunks/{2vo7qbdbvnc0o.js → 2qdpj67x6ifk_.js} +1 -1
- package/.next/standalone/.next/static/chunks/3-nbtkhg9y-1j.js +1 -0
- package/.next/standalone/.next/static/chunks/{36uhh9el_oz8e.js → 355km0ihuqo1p.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2z7i-yg59w4pn.js → 3c3qbmosdjl6w.js} +1 -1
- package/.next/standalone/package.json +5 -5
- package/.next/standalone/sdk/typescript/CHANGELOG.md +18 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +0 -274
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +114 -38
- package/dist/worker.mjs +109 -33
- package/package.json +5 -5
- package/src/hooks/semantic/decide.ts +134 -29
- package/src/hooks/semantic/facts.ts +91 -2
- package/src/hooks/semantic/types.ts +6 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__14o3ek1._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1-7sqrb._.js +0 -23
- package/.next/standalone/.next/static/chunks/078gnymqoh3r4.js +0 -1
- package/.next/standalone/.next/static/chunks/1bu2-nv59ed6i.js +0 -1
- package/.next/standalone/.next/static/chunks/2a405_e1o26ol.js +0 -1
- /package/.next/standalone/.next/static/{O_b5R1axf5cb4NxIXq2kS → 5yKM8NoTOpUI0MoQl8eo5}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{O_b5R1axf5cb4NxIXq2kS → 5yKM8NoTOpUI0MoQl8eo5}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{O_b5R1axf5cb4NxIXq2kS → 5yKM8NoTOpUI0MoQl8eo5}/_ssgManifest.js +0 -0
package/dist/worker.mjs
CHANGED
|
@@ -1162,7 +1162,7 @@ function instruct(reason) {
|
|
|
1162
1162
|
}
|
|
1163
1163
|
|
|
1164
1164
|
// package.json
|
|
1165
|
-
var version = "1.0.8
|
|
1165
|
+
var version = "1.0.8";
|
|
1166
1166
|
var init_package = () => {};
|
|
1167
1167
|
|
|
1168
1168
|
// src/hooks/policy-types.ts
|
|
@@ -11905,6 +11905,19 @@ function classifyTool(toolName) {
|
|
|
11905
11905
|
return { toolClass: "other", toolIsKnown: true };
|
|
11906
11906
|
return { toolClass: "other", toolIsKnown: false };
|
|
11907
11907
|
}
|
|
11908
|
+
function runsNestedShell(tokens) {
|
|
11909
|
+
let shellSeen = false;
|
|
11910
|
+
for (const tok of tokens) {
|
|
11911
|
+
if (shellSeen && /^-[a-z]*c[a-z]*$/i.test(tok))
|
|
11912
|
+
return true;
|
|
11913
|
+
const base = tok.slice(tok.lastIndexOf("/") + 1);
|
|
11914
|
+
if (base === "eval")
|
|
11915
|
+
return true;
|
|
11916
|
+
if (NESTED_SHELLS.has(base))
|
|
11917
|
+
shellSeen = true;
|
|
11918
|
+
}
|
|
11919
|
+
return false;
|
|
11920
|
+
}
|
|
11908
11921
|
function scanCommand(command) {
|
|
11909
11922
|
const text = command.length > MAX_SCAN_CHARS ? command.slice(0, MAX_SCAN_CHARS) : command;
|
|
11910
11923
|
const segments = [];
|
|
@@ -11915,22 +11928,36 @@ function scanCommand(command) {
|
|
|
11915
11928
|
let out = "";
|
|
11916
11929
|
let commentsRemoved = false;
|
|
11917
11930
|
const comments = [];
|
|
11931
|
+
let complete = command.length <= MAX_SCAN_CHARS;
|
|
11932
|
+
let braceDepth = 0;
|
|
11933
|
+
let braceSep = false;
|
|
11934
|
+
let bracketOpen = false;
|
|
11918
11935
|
const endWord = () => {
|
|
11919
11936
|
if (inWord)
|
|
11920
11937
|
tokens.push(word);
|
|
11921
11938
|
word = "";
|
|
11922
11939
|
inWord = false;
|
|
11940
|
+
braceDepth = 0;
|
|
11941
|
+
braceSep = false;
|
|
11942
|
+
bracketOpen = false;
|
|
11923
11943
|
};
|
|
11924
11944
|
const endSegment = () => {
|
|
11925
11945
|
endWord();
|
|
11926
|
-
if (tokens.length > 0)
|
|
11946
|
+
if (tokens.length > 0) {
|
|
11947
|
+
if (complete && runsNestedShell(tokens))
|
|
11948
|
+
complete = false;
|
|
11927
11949
|
segments.push(tokens);
|
|
11950
|
+
}
|
|
11928
11951
|
tokens = [];
|
|
11929
11952
|
};
|
|
11930
11953
|
for (let i = 0;i < text.length; i++) {
|
|
11931
11954
|
const c = text[i];
|
|
11932
11955
|
if (quote) {
|
|
11933
11956
|
out += c;
|
|
11957
|
+
if (quote === '"' && (c === "`" || c === "$" && EXPANSION_START.test(text[i + 1] ?? "") || c === "\\" && text[i + 1] === `
|
|
11958
|
+
`)) {
|
|
11959
|
+
complete = false;
|
|
11960
|
+
}
|
|
11934
11961
|
if (c === quote) {
|
|
11935
11962
|
quote = null;
|
|
11936
11963
|
} else if (c === "\\" && quote === '"' && i + 1 < text.length) {
|
|
@@ -11941,6 +11968,10 @@ function scanCommand(command) {
|
|
|
11941
11968
|
}
|
|
11942
11969
|
continue;
|
|
11943
11970
|
}
|
|
11971
|
+
if (c === "`" || c === "$" && (text[i + 1] === "'" || text[i + 1] === '"' || EXPANSION_START.test(text[i + 1] ?? "")) || (c === "<" || c === ">") && text[i + 1] === "(" || c === "<" && text[i + 1] === "<" || c === "\\" && text[i + 1] === `
|
|
11972
|
+
`) {
|
|
11973
|
+
complete = false;
|
|
11974
|
+
}
|
|
11944
11975
|
if (c === "'" || c === '"') {
|
|
11945
11976
|
quote = c;
|
|
11946
11977
|
inWord = true;
|
|
@@ -11981,12 +12012,29 @@ function scanCommand(command) {
|
|
|
11981
12012
|
out += c;
|
|
11982
12013
|
continue;
|
|
11983
12014
|
}
|
|
12015
|
+
if (c === "{") {
|
|
12016
|
+
braceDepth++;
|
|
12017
|
+
} else if (braceDepth > 0 && (c === "," || c === "." && text[i + 1] === ".")) {
|
|
12018
|
+
braceSep = true;
|
|
12019
|
+
} else if (c === "}" && braceDepth > 0) {
|
|
12020
|
+
braceDepth--;
|
|
12021
|
+
if (braceSep)
|
|
12022
|
+
complete = false;
|
|
12023
|
+
} else if (c === "*" || c === "?") {
|
|
12024
|
+
complete = false;
|
|
12025
|
+
} else if (c === "[") {
|
|
12026
|
+
bracketOpen = true;
|
|
12027
|
+
} else if (c === "]" && bracketOpen) {
|
|
12028
|
+
complete = false;
|
|
12029
|
+
}
|
|
11984
12030
|
word += c;
|
|
11985
12031
|
inWord = true;
|
|
11986
12032
|
out += c;
|
|
11987
12033
|
}
|
|
11988
12034
|
endSegment();
|
|
11989
|
-
|
|
12035
|
+
if (quote)
|
|
12036
|
+
complete = false;
|
|
12037
|
+
return { segments, withoutComments: out.trimEnd(), commentsRemoved, comments, complete };
|
|
11990
12038
|
}
|
|
11991
12039
|
function expandHome(token, home) {
|
|
11992
12040
|
if (token === "~")
|
|
@@ -12119,7 +12167,7 @@ function computeFacts(toolName, toolInput, cwd, permissionMode, scanned, pinnedP
|
|
|
12119
12167
|
permissionMode
|
|
12120
12168
|
};
|
|
12121
12169
|
}
|
|
12122
|
-
var SHELL_TOOLS, WRITE_TOOLS, READ_TOOLS, NETWORK_TOOLS, INERT_TOOLS, MAX_SCAN_CHARS = 8192, PATH_LIKE, GLOB_CHARS, MAX_PATHS = 12;
|
|
12170
|
+
var SHELL_TOOLS, WRITE_TOOLS, READ_TOOLS, NETWORK_TOOLS, INERT_TOOLS, MAX_SCAN_CHARS = 8192, NESTED_SHELLS, EXPANSION_START, PATH_LIKE, GLOB_CHARS, MAX_PATHS = 12;
|
|
12123
12171
|
var init_facts = __esm(() => {
|
|
12124
12172
|
SHELL_TOOLS = new Set(["Bash", "BashOutput"]);
|
|
12125
12173
|
WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
|
@@ -12138,6 +12186,8 @@ var init_facts = __esm(() => {
|
|
|
12138
12186
|
"SlashCommand",
|
|
12139
12187
|
"Skill"
|
|
12140
12188
|
]);
|
|
12189
|
+
NESTED_SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh", "ash", "fish"]);
|
|
12190
|
+
EXPANSION_START = /[A-Za-z_0-9@*#?$!\-({]/;
|
|
12141
12191
|
PATH_LIKE = /^(?:\/|~|\.\.?(?:\/|$)|[^:]*\/)/;
|
|
12142
12192
|
GLOB_CHARS = /[*?[]/;
|
|
12143
12193
|
});
|
|
@@ -12146,50 +12196,70 @@ var init_facts = __esm(() => {
|
|
|
12146
12196
|
function tokensOf(text) {
|
|
12147
12197
|
return text.toLowerCase().split(/[^a-z0-9._@-]+/).filter((t) => t.length >= 3 && !GENERIC_TOKENS.has(t) && !/^\d+$/.test(t));
|
|
12148
12198
|
}
|
|
12149
|
-
function
|
|
12150
|
-
const
|
|
12199
|
+
function scanTargets(toolInput) {
|
|
12200
|
+
const groups = [];
|
|
12201
|
+
const addGroup = (tok) => {
|
|
12202
|
+
const words = tokensOf(tok);
|
|
12203
|
+
if (words.length > 0)
|
|
12204
|
+
groups.push(new Set(words));
|
|
12205
|
+
};
|
|
12151
12206
|
const command = typeof toolInput.command === "string" ? toolInput.command : null;
|
|
12152
12207
|
if (command) {
|
|
12153
|
-
|
|
12208
|
+
const scanned = scanCommand(command);
|
|
12209
|
+
for (const seg of scanned.segments) {
|
|
12154
12210
|
for (const tok of seg.slice(1)) {
|
|
12155
|
-
if (tok.startsWith("-"))
|
|
12156
|
-
|
|
12157
|
-
for (const t of tokensOf(tok))
|
|
12158
|
-
out.add(t);
|
|
12211
|
+
if (!tok.startsWith("-"))
|
|
12212
|
+
addGroup(tok);
|
|
12159
12213
|
}
|
|
12160
12214
|
}
|
|
12161
|
-
if (
|
|
12215
|
+
if (groups.length === 0) {
|
|
12162
12216
|
for (const piece of command.split(/[;&|\n]+/)) {
|
|
12163
12217
|
for (const tok of piece.trim().split(/\s+/).slice(1)) {
|
|
12164
12218
|
if (!tok.startsWith("-"))
|
|
12165
|
-
|
|
12166
|
-
out.add(t);
|
|
12219
|
+
addGroup(tok);
|
|
12167
12220
|
}
|
|
12168
12221
|
}
|
|
12169
12222
|
}
|
|
12170
|
-
return
|
|
12223
|
+
return { targets: new Set(groups.flatMap((g) => [...g])), groups, complete: scanned.complete };
|
|
12171
12224
|
}
|
|
12225
|
+
const all = new Set;
|
|
12172
12226
|
for (const [key, value] of Object.entries(toolInput)) {
|
|
12173
12227
|
if (typeof value !== "string" || value.length > 300)
|
|
12174
12228
|
continue;
|
|
12175
12229
|
if (/content|old_string|new_string|body|text|prompt/i.test(key))
|
|
12176
12230
|
continue;
|
|
12177
12231
|
for (const t of tokensOf(value))
|
|
12178
|
-
|
|
12232
|
+
all.add(t);
|
|
12179
12233
|
}
|
|
12180
|
-
return
|
|
12234
|
+
return { targets: all, groups: all.size > 0 ? [all] : [], complete: true };
|
|
12181
12235
|
}
|
|
12182
|
-
function
|
|
12183
|
-
if (userSaid.length === 0)
|
|
12236
|
+
function everyTargetNamed(scan, userSaid) {
|
|
12237
|
+
if (userSaid.length === 0 || scan.groups.length === 0)
|
|
12184
12238
|
return false;
|
|
12185
|
-
|
|
12239
|
+
const said = userSaid.join(`
|
|
12240
|
+
`).toLowerCase();
|
|
12241
|
+
return scan.groups.every((g) => {
|
|
12242
|
+
for (const t of g)
|
|
12243
|
+
if (said.includes(t))
|
|
12244
|
+
return true;
|
|
12245
|
+
return false;
|
|
12246
|
+
});
|
|
12247
|
+
}
|
|
12248
|
+
function partlyNamed(scan, userSaid) {
|
|
12249
|
+
if (userSaid.length === 0 || scan.groups.length < 2)
|
|
12186
12250
|
return false;
|
|
12187
12251
|
const said = userSaid.join(`
|
|
12188
12252
|
`).toLowerCase();
|
|
12189
|
-
|
|
12190
|
-
|
|
12191
|
-
|
|
12192
|
-
|
|
12253
|
+
let named = 0;
|
|
12254
|
+
for (const g of scan.groups) {
|
|
12255
|
+
for (const t of g) {
|
|
12256
|
+
if (said.includes(t)) {
|
|
12257
|
+
named++;
|
|
12258
|
+
break;
|
|
12259
|
+
}
|
|
12260
|
+
}
|
|
12261
|
+
}
|
|
12262
|
+
return named > 0 && named < scan.groups.length;
|
|
12193
12263
|
}
|
|
12194
12264
|
function formatOutcome(p, o) {
|
|
12195
12265
|
return `${p.title} (semantic/${p.name}, p=${o.evidence.toFixed(2)}). ${p.guidance}`;
|
|
@@ -12199,7 +12269,7 @@ function decide(selected, answers, toolInput, userSaid, thresholds = DEFAULT_THR
|
|
|
12199
12269
|
const injected = injection !== null && injection >= thresholds.injection;
|
|
12200
12270
|
const scope = typeof answers.scope === "number" ? answers.scope : null;
|
|
12201
12271
|
const withinScope = scope !== null && scope >= thresholds.scope;
|
|
12202
|
-
let
|
|
12272
|
+
let scan = null;
|
|
12203
12273
|
const outcomes = selected.map((p) => {
|
|
12204
12274
|
const evidence = Math.min(...p.probes.map((probe) => answers[`${p.name}.${probe.id}`] ?? 0));
|
|
12205
12275
|
const exempt = p.exempt ? answers[`${p.name}.exempt`] ?? 0 : null;
|
|
@@ -12222,9 +12292,11 @@ function decide(selected, answers, toolInput, userSaid, thresholds = DEFAULT_THR
|
|
|
12222
12292
|
return { ...base, escalatedByInjection: true, verdict: "deny" };
|
|
12223
12293
|
const fired = p.mode === "deny" && evidence >= thresholds.deny ? "deny" : "instruct";
|
|
12224
12294
|
if (p.userCanOverride && userAsked !== null && userAsked >= thresholds.userAsked && withinScope) {
|
|
12225
|
-
|
|
12226
|
-
|
|
12227
|
-
|
|
12295
|
+
scan ??= scanTargets(toolInput);
|
|
12296
|
+
if (!scan.complete)
|
|
12297
|
+
return { ...base, verdict: fired, targetScanIncomplete: true };
|
|
12298
|
+
const named = scan.groups.length > 0 && everyTargetNamed(scan, userSaid);
|
|
12299
|
+
if (scan.groups.length === 0 || named || userSaidCut)
|
|
12228
12300
|
return { ...base, targetNamedByUser: named, verdict: "overridden" };
|
|
12229
12301
|
}
|
|
12230
12302
|
return { ...base, verdict: fired };
|
|
@@ -12274,12 +12346,14 @@ function decideV1(selected, answers, toolInput, userSaid, agentLastMessage, opts
|
|
|
12274
12346
|
const task = num("task_step");
|
|
12275
12347
|
const op = num("op_requested");
|
|
12276
12348
|
const beyond = num("beyond_task");
|
|
12277
|
-
let
|
|
12349
|
+
let scan = null;
|
|
12350
|
+
const targetScan = () => scan ??= scanTargets(toolInput);
|
|
12351
|
+
const evidenceSaid = agentLastMessage ? [...userSaid, agentLastMessage] : userSaid;
|
|
12278
12352
|
const targetOk = () => {
|
|
12279
|
-
|
|
12280
|
-
if (
|
|
12353
|
+
const s = targetScan();
|
|
12354
|
+
if (s.groups.length === 0)
|
|
12281
12355
|
return { ok: true, named: false };
|
|
12282
|
-
const named =
|
|
12356
|
+
const named = everyTargetNamed(s, evidenceSaid);
|
|
12283
12357
|
return { ok: named || userSaidCut, named };
|
|
12284
12358
|
};
|
|
12285
12359
|
const outcomes = selected.map((p) => {
|
|
@@ -12304,12 +12378,14 @@ function decideV1(selected, answers, toolInput, userSaid, agentLastMessage, opts
|
|
|
12304
12378
|
const fired = p.mode === "deny" && evidence >= t.deny ? "deny" : "instruct";
|
|
12305
12379
|
if (!p.userCanOverride)
|
|
12306
12380
|
return { ...base, verdict: fired };
|
|
12381
|
+
if (!targetScan().complete)
|
|
12382
|
+
return { ...base, verdict: fired, targetScanIncomplete: true };
|
|
12307
12383
|
if (op !== null && op >= t.opRequested && beyond !== null && beyond < t.opBeyondMax) {
|
|
12308
12384
|
const target = targetOk();
|
|
12309
12385
|
if (target.ok)
|
|
12310
12386
|
return { ...base, targetNamedByUser: target.named, verdict: "overridden", intent: "op-requested" };
|
|
12311
12387
|
}
|
|
12312
|
-
if (taskClears && task !== null && task >= t.taskStep && beyond !== null && beyond < t.taskBeyondMax) {
|
|
12388
|
+
if (taskClears && task !== null && task >= t.taskStep && beyond !== null && beyond < t.taskBeyondMax && !(typeof toolInput.command === "string" && !userSaidCut && partlyNamed(targetScan(), evidenceSaid))) {
|
|
12313
12389
|
if (fired === "instruct")
|
|
12314
12390
|
return { ...base, verdict: "overridden", intent: "task-step" };
|
|
12315
12391
|
return { ...base, verdict: "instruct", intent: "downgraded-task-step" };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "1.0.8
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "Observability and enforcement for AI agent harnesses. 40 built-in policies hooked into 12 of them — Claude Code, Codex, Cursor, Hermes, OpenClaw and more — blocking the tool call before it runs. Local dashboard included, no account needed.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"failproofai": "./dist/cli.mjs",
|
|
@@ -122,10 +122,10 @@
|
|
|
122
122
|
"browserslist": "4.28.8"
|
|
123
123
|
},
|
|
124
124
|
"optionalDependencies": {
|
|
125
|
-
"@failproofai/failproofaid-linux-x64": "1.0.8
|
|
126
|
-
"@failproofai/failproofaid-linux-arm64": "1.0.8
|
|
127
|
-
"@failproofai/failproofaid-darwin-x64": "1.0.8
|
|
128
|
-
"@failproofai/failproofaid-darwin-arm64": "1.0.8
|
|
125
|
+
"@failproofai/failproofaid-linux-x64": "1.0.8",
|
|
126
|
+
"@failproofai/failproofaid-linux-arm64": "1.0.8",
|
|
127
|
+
"@failproofai/failproofaid-darwin-x64": "1.0.8",
|
|
128
|
+
"@failproofai/failproofaid-darwin-arm64": "1.0.8"
|
|
129
129
|
},
|
|
130
130
|
"failproofaidBinaries": {
|
|
131
131
|
"linux-x64": "35b641f60e1724513fbb760db382123dad2b5fcdd67dda6373bda80651bd36bb",
|
|
@@ -13,9 +13,13 @@
|
|
|
13
13
|
* - A `deny` policy blocks only on strong evidence; moderate evidence warns.
|
|
14
14
|
* - The user may clear a policy only if (a) they explicitly asked for this
|
|
15
15
|
* action, (b) everything the call affects stays inside what they asked for
|
|
16
|
-
* (the `scope` probe), (c) when the call names identifiable targets,
|
|
17
|
-
* them appears in what they typed — checked here, in code, not by the
|
|
18
|
-
* — and (d) the request does not look like it is talking to the
|
|
16
|
+
* (the `scope` probe), (c) when the call names identifiable targets, EVERY
|
|
17
|
+
* one of them appears in what they typed — checked here, in code, not by the
|
|
18
|
+
* model — and (d) the request does not look like it is talking to the
|
|
19
|
+
* reviewer. When the local shell scan may have missed part of the command
|
|
20
|
+
* (`$'…'`, a heredoc, `$(…)`, …), (c) cannot be checked and nothing is
|
|
21
|
+
* cleared: the scan can see an innocent first target and stop before the
|
|
22
|
+
* destructive one.
|
|
19
23
|
* A call that names no target is never cleared by default: the scope answer
|
|
20
24
|
* has to carry it.
|
|
21
25
|
* - The injection probe withdraws any override, and turns a policy that has
|
|
@@ -68,44 +72,117 @@ function tokensOf(text: string): string[] {
|
|
|
68
72
|
.filter((t) => t.length >= 3 && !GENERIC_TOKENS.has(t) && !/^\d+$/.test(t));
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
/**
|
|
76
|
+
* What a tool call acts on, as the local target check sees it.
|
|
77
|
+
*
|
|
78
|
+
* `groups` holds one entry per identifiable target — a non-flag argument of a
|
|
79
|
+
* shell command (path components and all), or, for any other tool, the whole
|
|
80
|
+
* of its argument values taken together — and each entry is the set of words
|
|
81
|
+
* that name it. `targets` is every word of every group.
|
|
82
|
+
*
|
|
83
|
+
* `complete` is false when the shell scan may have missed a word bash would
|
|
84
|
+
* run (`ScannedCommand.complete`). The groups are then a lower bound, and no
|
|
85
|
+
* clear may rest on them: see {@link everyTargetNamed}'s callers.
|
|
86
|
+
*/
|
|
87
|
+
export interface TargetScan {
|
|
88
|
+
targets: Set<string>;
|
|
89
|
+
groups: Set<string>[];
|
|
90
|
+
complete: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
71
93
|
/**
|
|
72
94
|
* The words that identify WHAT a tool call acts on: its non-flag arguments
|
|
73
95
|
* (path components included), file paths, URL hosts, MCP argument values.
|
|
74
96
|
* The verb is left to Jev's `user_asked` question; this only checks the noun.
|
|
75
97
|
*/
|
|
76
|
-
export function
|
|
77
|
-
const
|
|
98
|
+
export function scanTargets(toolInput: Record<string, unknown>): TargetScan {
|
|
99
|
+
const groups: Set<string>[] = [];
|
|
100
|
+
const addGroup = (tok: string) => {
|
|
101
|
+
const words = tokensOf(tok);
|
|
102
|
+
if (words.length > 0) groups.push(new Set(words));
|
|
103
|
+
};
|
|
78
104
|
const command = typeof toolInput.command === "string" ? toolInput.command : null;
|
|
79
105
|
if (command) {
|
|
80
|
-
|
|
106
|
+
const scanned = scanCommand(command);
|
|
107
|
+
for (const seg of scanned.segments) {
|
|
81
108
|
for (const tok of seg.slice(1)) {
|
|
82
|
-
if (tok.startsWith("-"))
|
|
83
|
-
for (const t of tokensOf(tok)) out.add(t);
|
|
109
|
+
if (!tok.startsWith("-")) addGroup(tok);
|
|
84
110
|
}
|
|
85
111
|
}
|
|
86
112
|
// Empty reads as "names no target", and lets consent rest on Jev's answers
|
|
87
113
|
// alone. But the scanner is not bash — a `#` inside `$'…'`, `${x:- # }` or
|
|
88
114
|
// backticks ends its view early, and it reads only MAX_SCAN_CHARS — so an
|
|
89
115
|
// empty scan may just have stopped before the target. Then judge the words
|
|
90
|
-
// as written. Only ever stricter: an empty set already passed.
|
|
91
|
-
|
|
116
|
+
// as written. Only ever stricter: an empty set already passed. (Such a scan
|
|
117
|
+
// is also reported incomplete, which withholds the clear on its own; this
|
|
118
|
+
// keeps the recorded targets honest.)
|
|
119
|
+
if (groups.length === 0) {
|
|
92
120
|
for (const piece of command.split(/[;&|\n]+/)) {
|
|
93
121
|
for (const tok of piece.trim().split(/\s+/).slice(1)) {
|
|
94
|
-
if (!tok.startsWith("-"))
|
|
122
|
+
if (!tok.startsWith("-")) addGroup(tok);
|
|
95
123
|
}
|
|
96
124
|
}
|
|
97
125
|
}
|
|
98
|
-
return
|
|
126
|
+
return { targets: new Set(groups.flatMap((g) => [...g])), groups, complete: scanned.complete };
|
|
99
127
|
}
|
|
128
|
+
const all = new Set<string>();
|
|
100
129
|
for (const [key, value] of Object.entries(toolInput)) {
|
|
101
130
|
if (typeof value !== "string" || value.length > 300) continue;
|
|
102
131
|
if (/content|old_string|new_string|body|text|prompt/i.test(key)) continue;
|
|
103
|
-
for (const t of tokensOf(value))
|
|
132
|
+
for (const t of tokensOf(value)) all.add(t);
|
|
133
|
+
}
|
|
134
|
+
// A non-shell tool's fields qualify one another (`owner`, `repo`, `branch`)
|
|
135
|
+
// rather than naming separate targets, so they are ONE target: naming any of
|
|
136
|
+
// them names it.
|
|
137
|
+
return { targets: all, groups: all.size > 0 ? [all] : [], complete: true };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Every word of {@link scanTargets}, flattened. */
|
|
141
|
+
export function targetTokens(toolInput: Record<string, unknown>): Set<string> {
|
|
142
|
+
return scanTargets(toolInput).targets;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* True when the user's own words name EVERY target the call acts on — each
|
|
147
|
+
* group through at least one of its words.
|
|
148
|
+
*
|
|
149
|
+
* Not "any one": `rm -rf build/ ~/important` after "clean the build" names
|
|
150
|
+
* `build` and not `important`, and a clear on the first would carry the
|
|
151
|
+
* second, which nobody asked for.
|
|
152
|
+
*/
|
|
153
|
+
export function everyTargetNamed(scan: TargetScan, userSaid: ReadonlyArray<string>): boolean {
|
|
154
|
+
if (userSaid.length === 0 || scan.groups.length === 0) return false;
|
|
155
|
+
const said = userSaid.join("\n").toLowerCase();
|
|
156
|
+
return scan.groups.every((g) => {
|
|
157
|
+
for (const t of g) if (said.includes(t)) return true;
|
|
158
|
+
return false;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* True when the user's words name SOME of the call's targets but not all:
|
|
164
|
+
* they drew a line and the call reaches past it. Naming none is not this — a
|
|
165
|
+
* goal ("fix the failing tests") names no path at all.
|
|
166
|
+
*/
|
|
167
|
+
export function partlyNamed(scan: TargetScan, userSaid: ReadonlyArray<string>): boolean {
|
|
168
|
+
if (userSaid.length === 0 || scan.groups.length < 2) return false;
|
|
169
|
+
const said = userSaid.join("\n").toLowerCase();
|
|
170
|
+
let named = 0;
|
|
171
|
+
for (const g of scan.groups) {
|
|
172
|
+
for (const t of g) {
|
|
173
|
+
if (said.includes(t)) {
|
|
174
|
+
named++;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
104
178
|
}
|
|
105
|
-
return
|
|
179
|
+
return named > 0 && named < scan.groups.length;
|
|
106
180
|
}
|
|
107
181
|
|
|
108
|
-
/**
|
|
182
|
+
/**
|
|
183
|
+
* True when the user's own words name at least one of `targets`. The deciders
|
|
184
|
+
* do not use this any-one check — see {@link everyTargetNamed}.
|
|
185
|
+
*/
|
|
109
186
|
export function targetNamedByUser(targets: Set<string>, userSaid: ReadonlyArray<string>): boolean {
|
|
110
187
|
if (userSaid.length === 0) return false;
|
|
111
188
|
// Nothing identifiable to check is not a match. This used to return true,
|
|
@@ -136,7 +213,7 @@ export function decide(
|
|
|
136
213
|
const injected = injection !== null && injection >= thresholds.injection;
|
|
137
214
|
const scope = typeof answers.scope === "number" ? answers.scope : null;
|
|
138
215
|
const withinScope = scope !== null && scope >= thresholds.scope;
|
|
139
|
-
let
|
|
216
|
+
let scan: TargetScan | null = null;
|
|
140
217
|
|
|
141
218
|
const outcomes: PolicyOutcome[] = selected.map((p) => {
|
|
142
219
|
const evidence = Math.min(...p.probes.map((probe) => answers[`${p.name}.${probe.id}`] ?? 0));
|
|
@@ -161,13 +238,18 @@ export function decide(
|
|
|
161
238
|
|
|
162
239
|
const fired: PolicyOutcome["verdict"] = p.mode === "deny" && evidence >= thresholds.deny ? "deny" : "instruct";
|
|
163
240
|
if (p.userCanOverride && userAsked !== null && userAsked >= thresholds.userAsked && withinScope) {
|
|
164
|
-
|
|
241
|
+
scan ??= scanTargets(toolInput);
|
|
242
|
+
// A shell scan that may have missed a word cannot say what the call
|
|
243
|
+
// touches, so it cannot say the user named it: no clear. Not rescued by
|
|
244
|
+
// `userSaidCut` — that gap is in what the human typed, this one is in
|
|
245
|
+
// the call.
|
|
246
|
+
if (!scan.complete) return { ...base, verdict: fired, targetScanIncomplete: true };
|
|
165
247
|
// No identifiable target: the scope answer (already required) carries it.
|
|
166
|
-
// Otherwise
|
|
248
|
+
// Otherwise EVERY target must also appear in the user's own words —
|
|
167
249
|
// unless the words we hold were cut, when "absent" is not something this
|
|
168
250
|
// check knows (see {@link DecideV1Options.userSaidCut}).
|
|
169
|
-
const named =
|
|
170
|
-
if (
|
|
251
|
+
const named = scan.groups.length > 0 && everyTargetNamed(scan, userSaid);
|
|
252
|
+
if (scan.groups.length === 0 || named || userSaidCut) return { ...base, targetNamedByUser: named, verdict: "overridden" };
|
|
171
253
|
}
|
|
172
254
|
return { ...base, verdict: fired };
|
|
173
255
|
});
|
|
@@ -293,12 +375,16 @@ export interface DecideV1Options {
|
|
|
293
375
|
* - A policy fires exactly as in v0 (every probe holds, no exemption).
|
|
294
376
|
* - Injection withdraws every clear and turns a fired policy into a block.
|
|
295
377
|
* - The human asked for THIS operation on THIS target (`op_requested`), the
|
|
296
|
-
* call reaches no further (`beyond_task`), and — when the call names
|
|
297
|
-
*
|
|
298
|
-
* proposal they replied to: the policy is cleared.
|
|
378
|
+
* call reaches no further (`beyond_task`), and — when the call names
|
|
379
|
+
* targets — every one of them appears in what the human typed or in the
|
|
380
|
+
* agent proposal they replied to: the policy is cleared.
|
|
381
|
+
* - A shell command the local scan could not read whole is cleared and
|
|
382
|
+
* softened by neither route: see `ScannedCommand.complete` in `facts.ts`.
|
|
299
383
|
* - Otherwise, the call is a step toward the human's task (`task_step`) and
|
|
300
384
|
* reaches no further: a warn-level outcome is cleared and a block is
|
|
301
|
-
* softened to a warning. A goal never licenses a block on its own.
|
|
385
|
+
* softened to a warning. A goal never licenses a block on its own. On a
|
|
386
|
+
* shell command whose targets the human named only in part, this route
|
|
387
|
+
* does not apply either.
|
|
302
388
|
* - Policies with `userCanOverride: false` are never cleared or softened.
|
|
303
389
|
* - Nothing fired, but the call reaches beyond the task, is not a step toward
|
|
304
390
|
* it, and some "does it do X" probe is at least half-raised: warn.
|
|
@@ -321,11 +407,13 @@ export function decideV1(
|
|
|
321
407
|
const task = num("task_step");
|
|
322
408
|
const op = num("op_requested");
|
|
323
409
|
const beyond = num("beyond_task");
|
|
324
|
-
let
|
|
410
|
+
let scan: TargetScan | null = null;
|
|
411
|
+
const targetScan = (): TargetScan => (scan ??= scanTargets(toolInput));
|
|
412
|
+
const evidenceSaid = agentLastMessage ? [...userSaid, agentLastMessage] : userSaid;
|
|
325
413
|
const targetOk = (): { ok: boolean; named: boolean } => {
|
|
326
|
-
|
|
327
|
-
if (
|
|
328
|
-
const named =
|
|
414
|
+
const s = targetScan();
|
|
415
|
+
if (s.groups.length === 0) return { ok: true, named: false };
|
|
416
|
+
const named = everyTargetNamed(s, evidenceSaid);
|
|
329
417
|
return { ok: named || userSaidCut, named };
|
|
330
418
|
};
|
|
331
419
|
|
|
@@ -349,12 +437,29 @@ export function decideV1(
|
|
|
349
437
|
|
|
350
438
|
const fired: PolicyOutcome["verdict"] = p.mode === "deny" && evidence >= t.deny ? "deny" : "instruct";
|
|
351
439
|
if (!p.userCanOverride) return { ...base, verdict: fired };
|
|
440
|
+
// The shell scan may have missed a word bash would run (`$'…'`, a heredoc,
|
|
441
|
+
// `$(…)`, …): what the call touches is not known here, so neither intent
|
|
442
|
+
// route may clear or soften it. Jev's own deny or instruct stands.
|
|
443
|
+
if (!targetScan().complete) return { ...base, verdict: fired, targetScanIncomplete: true };
|
|
352
444
|
|
|
353
445
|
if (op !== null && op >= t.opRequested && beyond !== null && beyond < t.opBeyondMax) {
|
|
354
446
|
const target = targetOk();
|
|
355
447
|
if (target.ok) return { ...base, targetNamedByUser: target.named, verdict: "overridden", intent: "op-requested" };
|
|
356
448
|
}
|
|
357
|
-
|
|
449
|
+
// The task-step route reaches `combine.ts` as a clear too — a warning it
|
|
450
|
+
// leaves behind clears a reviewable regex deny. It is consent by GOAL, so
|
|
451
|
+
// a shell command whose targets the human never named still rides on it
|
|
452
|
+
// ("fix the failing tests" → `rm -rf node_modules`). But once the human
|
|
453
|
+
// HAS named targets, they drew the line: a call reaching past it is not
|
|
454
|
+
// softened. After "clean the build", `rm -rf build/ ~/important` is not.
|
|
455
|
+
if (
|
|
456
|
+
taskClears &&
|
|
457
|
+
task !== null &&
|
|
458
|
+
task >= t.taskStep &&
|
|
459
|
+
beyond !== null &&
|
|
460
|
+
beyond < t.taskBeyondMax &&
|
|
461
|
+
!(typeof toolInput.command === "string" && !userSaidCut && partlyNamed(targetScan(), evidenceSaid))
|
|
462
|
+
) {
|
|
358
463
|
if (fired === "instruct") return { ...base, verdict: "overridden", intent: "task-step" };
|
|
359
464
|
return { ...base, verdict: "instruct", intent: "downgraded-task-step" };
|
|
360
465
|
}
|