failproofai 1.0.6-beta.1 → 1.0.6
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 +4 -4
- 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/_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]__0-_ki57._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0576iuy._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1b2dbet._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1x3j9mj._.js → [root-of-the-server]__1svlq8e._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1v185mo._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1_ddpm3._.js → _0do6d26._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0qlygw6._.js → _17o744m._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- 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 +4 -4
- 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_settings-client_tsx_20lq-mq._.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 +19 -19
- package/.next/standalone/.next/static/chunks/0bepqeawy72n6.js +1 -0
- package/.next/standalone/.next/static/chunks/{0cbvytf8epn34.js → 0h-gz8e00478q.js} +1 -1
- package/.next/standalone/.next/static/chunks/{06fjx8-0_qufs.js → 25m8mg7bosgip.js} +1 -1
- package/.next/standalone/.next/static/chunks/{27_bwbg8sv6vj.js → 2y4ycwcwfjv4k.js} +1 -1
- package/.next/standalone/.next/static/chunks/3-6zhytp6adkk.js +1 -0
- package/.next/standalone/.next/static/chunks/{1p4xauf-zu6zk.js → 319c_91le7-h_.js} +1 -1
- package/.next/standalone/.next/static/chunks/35m53kix7uaii.js +6 -0
- package/.next/standalone/.next/static/chunks/{1qf2uiipatr80.js → 35pw7zxhh76n7.js} +1 -1
- package/.next/standalone/.next/static/chunks/3xacd7qmuqlvs.js +1 -0
- package/.next/standalone/.next/static/chunks/{02vwr5js1x28j.js → 40-wncjnguy4e.js} +1 -1
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +123 -62
- package/dist/worker.mjs +1 -1
- package/package.json +9 -9
- package/src/hooks/daemon-client.ts +128 -50
- package/src/hooks/daemon-service.ts +30 -0
- package/src/hooks/integrations.ts +3 -2
- package/src/hooks/manager.ts +4 -4
- package/.next/standalone/.next/static/chunks/0y227_ohdqwvn.js +0 -1
- package/.next/standalone/.next/static/chunks/1uc_s_dftgxiq.js +0 -6
- package/.next/standalone/.next/static/chunks/2x776s0x3n_tu.js +0 -1
- package/.next/standalone/.next/static/chunks/3ht0xsco46pul.js +0 -1
- /package/.next/standalone/.next/static/{QCNYz2yCxxdeggg1k0iEq → LzR_etPlaZV9JZH09lEp3}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{QCNYz2yCxxdeggg1k0iEq → LzR_etPlaZV9JZH09lEp3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{QCNYz2yCxxdeggg1k0iEq → LzR_etPlaZV9JZH09lEp3}/_ssgManifest.js +0 -0
package/dist/cli.mjs
CHANGED
|
@@ -29,7 +29,7 @@ var __esm = (fn, res, err) => () => {
|
|
|
29
29
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
30
30
|
|
|
31
31
|
// package.json
|
|
32
|
-
var version2 = "1.0.6
|
|
32
|
+
var version2 = "1.0.6", package_default;
|
|
33
33
|
var init_package = __esm(() => {
|
|
34
34
|
package_default = {
|
|
35
35
|
name: "failproofai",
|
|
@@ -155,16 +155,16 @@ var init_package = __esm(() => {
|
|
|
155
155
|
browserslist: "4.28.8"
|
|
156
156
|
},
|
|
157
157
|
optionalDependencies: {
|
|
158
|
-
"@failproofai/failproofaid-linux-x64": "1.0.6
|
|
159
|
-
"@failproofai/failproofaid-linux-arm64": "1.0.6
|
|
160
|
-
"@failproofai/failproofaid-darwin-x64": "1.0.6
|
|
161
|
-
"@failproofai/failproofaid-darwin-arm64": "1.0.6
|
|
158
|
+
"@failproofai/failproofaid-linux-x64": "1.0.6",
|
|
159
|
+
"@failproofai/failproofaid-linux-arm64": "1.0.6",
|
|
160
|
+
"@failproofai/failproofaid-darwin-x64": "1.0.6",
|
|
161
|
+
"@failproofai/failproofaid-darwin-arm64": "1.0.6"
|
|
162
162
|
},
|
|
163
163
|
failproofaidBinaries: {
|
|
164
|
-
"linux-x64": "
|
|
165
|
-
"linux-arm64": "
|
|
166
|
-
"darwin-x64": "
|
|
167
|
-
"darwin-arm64": "
|
|
164
|
+
"linux-x64": "e075f24a3a0950b631979d9370b665da2c3e2920decfb917ff0831d1f17c9740",
|
|
165
|
+
"linux-arm64": "b90ce8c642f2685719f4915695615219860daf3155a9c07344cf1e10395028c3",
|
|
166
|
+
"darwin-x64": "51dd95e9b85226e4f1888c7edaa56ec4464e91eb645d39c5cb7cede16d2da2f1",
|
|
167
|
+
"darwin-arm64": "f6a1b9d63363e8805e0d72b4fa168fde661bdbbd4125b54047f7de8948ea0f39"
|
|
168
168
|
}
|
|
169
169
|
};
|
|
170
170
|
});
|
|
@@ -809,7 +809,7 @@ function encodeFrame(value) {
|
|
|
809
809
|
header.writeUInt32BE(body.length, 0);
|
|
810
810
|
return Buffer.concat([header, body]);
|
|
811
811
|
}
|
|
812
|
-
async function
|
|
812
|
+
async function attemptDaemonRequest(request, opts) {
|
|
813
813
|
if (process.platform === "win32")
|
|
814
814
|
return { ok: false, failure: "unreachable" };
|
|
815
815
|
const responseBudget = opts?.responseTimeoutMs ?? DAEMON_RESPONSE_TIMEOUT_MS;
|
|
@@ -837,14 +837,7 @@ async function attemptDaemonHook(req, opts) {
|
|
|
837
837
|
let declaredLen = null;
|
|
838
838
|
socket.on("connect", () => {
|
|
839
839
|
arm(responseBudget);
|
|
840
|
-
socket.write(encodeFrame(
|
|
841
|
-
type: "hook",
|
|
842
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
843
|
-
hookEvent: req.hookEvent,
|
|
844
|
-
cli: req.cli,
|
|
845
|
-
stdin: req.stdin,
|
|
846
|
-
cwd: req.cwd
|
|
847
|
-
}));
|
|
840
|
+
socket.write(encodeFrame(request));
|
|
848
841
|
});
|
|
849
842
|
socket.on("data", (chunk) => {
|
|
850
843
|
recvBuf = Buffer.concat([recvBuf, chunk]);
|
|
@@ -861,34 +854,52 @@ async function attemptDaemonHook(req, opts) {
|
|
|
861
854
|
if (recvBuf.length < declaredLen)
|
|
862
855
|
return;
|
|
863
856
|
const body = recvBuf.subarray(0, declaredLen);
|
|
864
|
-
let
|
|
857
|
+
let parsed;
|
|
865
858
|
try {
|
|
866
|
-
|
|
859
|
+
parsed = JSON.parse(body.toString("utf8"));
|
|
867
860
|
} catch {
|
|
868
861
|
fail("unreachable");
|
|
869
862
|
return;
|
|
870
863
|
}
|
|
871
|
-
if (
|
|
872
|
-
fail("
|
|
864
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
865
|
+
fail("unreachable");
|
|
873
866
|
return;
|
|
874
867
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
response: {
|
|
879
|
-
exitCode: message.exitCode,
|
|
880
|
-
stdout: message.stdout,
|
|
881
|
-
stderr: message.stderr
|
|
882
|
-
}
|
|
883
|
-
});
|
|
868
|
+
const message = parsed;
|
|
869
|
+
if (message.protocolVersion !== PROTOCOL_VERSION) {
|
|
870
|
+
fail("protocol-mismatch");
|
|
884
871
|
return;
|
|
885
872
|
}
|
|
886
|
-
|
|
873
|
+
finish({ ok: true, message });
|
|
887
874
|
});
|
|
888
875
|
socket.on("error", () => fail("unreachable"));
|
|
889
876
|
socket.on("close", () => fail("unreachable"));
|
|
890
877
|
});
|
|
891
878
|
}
|
|
879
|
+
async function attemptDaemonHook(req, opts) {
|
|
880
|
+
const attempt = await attemptDaemonRequest({
|
|
881
|
+
type: "hook",
|
|
882
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
883
|
+
hookEvent: req.hookEvent,
|
|
884
|
+
cli: req.cli,
|
|
885
|
+
stdin: req.stdin,
|
|
886
|
+
cwd: req.cwd
|
|
887
|
+
}, opts);
|
|
888
|
+
if (!attempt.ok)
|
|
889
|
+
return attempt;
|
|
890
|
+
const message = attempt.message;
|
|
891
|
+
if (message.type === "hookResult" && typeof message.exitCode === "number" && typeof message.stdout === "string" && typeof message.stderr === "string") {
|
|
892
|
+
return {
|
|
893
|
+
ok: true,
|
|
894
|
+
response: {
|
|
895
|
+
exitCode: message.exitCode,
|
|
896
|
+
stdout: message.stdout,
|
|
897
|
+
stderr: message.stderr
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
return { ok: false, failure: "unreachable" };
|
|
902
|
+
}
|
|
892
903
|
var PROTOCOL_VERSION = 1, DAEMON_CONNECT_TIMEOUT_MS = 150, DAEMON_RESPONSE_TIMEOUT_MS = 30000, MAX_FRAME_LEN;
|
|
893
904
|
var init_daemon_client = __esm(() => {
|
|
894
905
|
init_fp_home();
|
|
@@ -21996,7 +22007,7 @@ function encodeFrame2(value) {
|
|
|
21996
22007
|
header.writeUInt32BE(body.length, 0);
|
|
21997
22008
|
return Buffer.concat([header, body]);
|
|
21998
22009
|
}
|
|
21999
|
-
async function
|
|
22010
|
+
async function attemptDaemonRequest2(request, opts) {
|
|
22000
22011
|
if (process.platform === "win32")
|
|
22001
22012
|
return { ok: false, failure: "unreachable" };
|
|
22002
22013
|
const responseBudget = opts?.responseTimeoutMs ?? DAEMON_RESPONSE_TIMEOUT_MS2;
|
|
@@ -22024,14 +22035,7 @@ async function attemptDaemonHook2(req, opts) {
|
|
|
22024
22035
|
let declaredLen = null;
|
|
22025
22036
|
socket.on("connect", () => {
|
|
22026
22037
|
arm(responseBudget);
|
|
22027
|
-
socket.write(encodeFrame2(
|
|
22028
|
-
type: "hook",
|
|
22029
|
-
protocolVersion: PROTOCOL_VERSION2,
|
|
22030
|
-
hookEvent: req.hookEvent,
|
|
22031
|
-
cli: req.cli,
|
|
22032
|
-
stdin: req.stdin,
|
|
22033
|
-
cwd: req.cwd
|
|
22034
|
-
}));
|
|
22038
|
+
socket.write(encodeFrame2(request));
|
|
22035
22039
|
});
|
|
22036
22040
|
socket.on("data", (chunk) => {
|
|
22037
22041
|
recvBuf = Buffer.concat([recvBuf, chunk]);
|
|
@@ -22048,34 +22052,73 @@ async function attemptDaemonHook2(req, opts) {
|
|
|
22048
22052
|
if (recvBuf.length < declaredLen)
|
|
22049
22053
|
return;
|
|
22050
22054
|
const body = recvBuf.subarray(0, declaredLen);
|
|
22051
|
-
let
|
|
22055
|
+
let parsed;
|
|
22052
22056
|
try {
|
|
22053
|
-
|
|
22057
|
+
parsed = JSON.parse(body.toString("utf8"));
|
|
22054
22058
|
} catch {
|
|
22055
22059
|
fail("unreachable");
|
|
22056
22060
|
return;
|
|
22057
22061
|
}
|
|
22058
|
-
if (
|
|
22059
|
-
fail("
|
|
22062
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
22063
|
+
fail("unreachable");
|
|
22060
22064
|
return;
|
|
22061
22065
|
}
|
|
22062
|
-
|
|
22063
|
-
|
|
22064
|
-
|
|
22065
|
-
response: {
|
|
22066
|
-
exitCode: message.exitCode,
|
|
22067
|
-
stdout: message.stdout,
|
|
22068
|
-
stderr: message.stderr
|
|
22069
|
-
}
|
|
22070
|
-
});
|
|
22066
|
+
const message = parsed;
|
|
22067
|
+
if (message.protocolVersion !== PROTOCOL_VERSION2) {
|
|
22068
|
+
fail("protocol-mismatch");
|
|
22071
22069
|
return;
|
|
22072
22070
|
}
|
|
22073
|
-
|
|
22071
|
+
finish({ ok: true, message });
|
|
22074
22072
|
});
|
|
22075
22073
|
socket.on("error", () => fail("unreachable"));
|
|
22076
22074
|
socket.on("close", () => fail("unreachable"));
|
|
22077
22075
|
});
|
|
22078
22076
|
}
|
|
22077
|
+
async function attemptDaemonHook2(req, opts) {
|
|
22078
|
+
const attempt = await attemptDaemonRequest2({
|
|
22079
|
+
type: "hook",
|
|
22080
|
+
protocolVersion: PROTOCOL_VERSION2,
|
|
22081
|
+
hookEvent: req.hookEvent,
|
|
22082
|
+
cli: req.cli,
|
|
22083
|
+
stdin: req.stdin,
|
|
22084
|
+
cwd: req.cwd
|
|
22085
|
+
}, opts);
|
|
22086
|
+
if (!attempt.ok)
|
|
22087
|
+
return attempt;
|
|
22088
|
+
const message = attempt.message;
|
|
22089
|
+
if (message.type === "hookResult" && typeof message.exitCode === "number" && typeof message.stdout === "string" && typeof message.stderr === "string") {
|
|
22090
|
+
return {
|
|
22091
|
+
ok: true,
|
|
22092
|
+
response: {
|
|
22093
|
+
exitCode: message.exitCode,
|
|
22094
|
+
stdout: message.stdout,
|
|
22095
|
+
stderr: message.stderr
|
|
22096
|
+
}
|
|
22097
|
+
};
|
|
22098
|
+
}
|
|
22099
|
+
return { ok: false, failure: "unreachable" };
|
|
22100
|
+
}
|
|
22101
|
+
function isStringArray(value) {
|
|
22102
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
22103
|
+
}
|
|
22104
|
+
async function attemptDaemonPolicyEvaluation(req, opts) {
|
|
22105
|
+
const attempt = await attemptDaemonRequest2({
|
|
22106
|
+
type: "policyEvaluation",
|
|
22107
|
+
protocolVersion: PROTOCOL_VERSION2,
|
|
22108
|
+
integration: req.integration,
|
|
22109
|
+
event: req.event,
|
|
22110
|
+
payload: req.payload,
|
|
22111
|
+
cwd: req.cwd
|
|
22112
|
+
}, opts);
|
|
22113
|
+
if (!attempt.ok)
|
|
22114
|
+
return attempt;
|
|
22115
|
+
const message = attempt.message;
|
|
22116
|
+
const validOptionalString = (value) => value === null || value === undefined || typeof value === "string";
|
|
22117
|
+
if (message.type === "policyResult" && (message.decision === "allow" || message.decision === "deny" || message.decision === "instruct") && isStringArray(message.policyNames) && validOptionalString(message.reason) && isStringArray(message.matchedPolicies) && typeof message.durationMs === "number" && Number.isInteger(message.durationMs) && message.durationMs >= 0 && validOptionalString(message.toolName)) {
|
|
22118
|
+
return { ok: true };
|
|
22119
|
+
}
|
|
22120
|
+
return { ok: false, failure: "unreachable" };
|
|
22121
|
+
}
|
|
22079
22122
|
var PROTOCOL_VERSION2 = 1, DAEMON_CONNECT_TIMEOUT_MS2 = 150, DAEMON_RESPONSE_TIMEOUT_MS2 = 30000, MAX_FRAME_LEN2;
|
|
22080
22123
|
var init_daemon_client2 = __esm(() => {
|
|
22081
22124
|
init_fp_home();
|
|
@@ -22488,6 +22531,24 @@ async function daemonAcceptsConnectionsQuietly() {
|
|
|
22488
22531
|
async function probeDaemonEndToEnd() {
|
|
22489
22532
|
return (await probeDaemon()).ok;
|
|
22490
22533
|
}
|
|
22534
|
+
async function probeDaemonPolicyEvaluation() {
|
|
22535
|
+
if (!await probeDaemonEndToEnd())
|
|
22536
|
+
return false;
|
|
22537
|
+
try {
|
|
22538
|
+
await Promise.resolve().then(() => init_daemon_client2());
|
|
22539
|
+
const attempt = await attemptDaemonPolicyEvaluation({
|
|
22540
|
+
integration: "hermes",
|
|
22541
|
+
event: "on_session_start",
|
|
22542
|
+
payload: {
|
|
22543
|
+
hook_event_name: "on_session_start",
|
|
22544
|
+
source: "failproofai-native-policy-health-probe"
|
|
22545
|
+
}
|
|
22546
|
+
}, { responseTimeoutMs: DAEMON_PROBE_TIMEOUT_MS });
|
|
22547
|
+
return attempt.ok;
|
|
22548
|
+
} catch {
|
|
22549
|
+
return false;
|
|
22550
|
+
}
|
|
22551
|
+
}
|
|
22491
22552
|
async function waitForDaemonRunning(deps = {}) {
|
|
22492
22553
|
const status = deps.status ?? daemonServiceStatus;
|
|
22493
22554
|
const accepts = deps.accepts ?? daemonAcceptsConnectionsQuietly;
|
|
@@ -25156,11 +25217,11 @@ async function installHooksImpl(policyNames, scope = "user", cwd, includeBeta =
|
|
|
25156
25217
|
}
|
|
25157
25218
|
}
|
|
25158
25219
|
const daemonRequiredBy = selectedIntegrations.map(({ integration }) => integration).filter((integration) => integration.requiresHealthyDaemon);
|
|
25159
|
-
if (daemonRequiredBy.length > 0 && !await
|
|
25220
|
+
if (daemonRequiredBy.length > 0 && !await probeDaemonPolicyEvaluation()) {
|
|
25160
25221
|
const names = daemonRequiredBy.map((integration) => integration.displayName).join(", ");
|
|
25161
25222
|
const verb = daemonRequiredBy.length === 1 ? "requires" : "require";
|
|
25162
|
-
throw new CliError(`${names} ${verb} a
|
|
25163
|
-
` + "Run `failproofai config` to install
|
|
25223
|
+
throw new CliError(`${names} ${verb} a compatible failproofaid daemon with native policy evaluation before FailproofAI enforcement can be enabled.
|
|
25224
|
+
` + "Run `failproofai config` to install or update the daemon, then retry.");
|
|
25164
25225
|
}
|
|
25165
25226
|
const binaryPath = resolveFailproofaiBinary();
|
|
25166
25227
|
const previousConfig = readScopedHooksConfig(scope, cwd);
|
|
@@ -33148,11 +33209,11 @@ async function installHooksImpl2(policyNames, scope = "user", cwd, includeBeta =
|
|
|
33148
33209
|
}
|
|
33149
33210
|
}
|
|
33150
33211
|
const daemonRequiredBy = selectedIntegrations.map(({ integration }) => integration).filter((integration) => integration.requiresHealthyDaemon);
|
|
33151
|
-
if (daemonRequiredBy.length > 0 && !await
|
|
33212
|
+
if (daemonRequiredBy.length > 0 && !await probeDaemonPolicyEvaluation()) {
|
|
33152
33213
|
const names = daemonRequiredBy.map((integration) => integration.displayName).join(", ");
|
|
33153
33214
|
const verb = daemonRequiredBy.length === 1 ? "requires" : "require";
|
|
33154
|
-
throw new CliError(`${names} ${verb} a
|
|
33155
|
-
` + "Run `failproofai config` to install
|
|
33215
|
+
throw new CliError(`${names} ${verb} a compatible failproofaid daemon with native policy evaluation before FailproofAI enforcement can be enabled.
|
|
33216
|
+
` + "Run `failproofai config` to install or update the daemon, then retry.");
|
|
33156
33217
|
}
|
|
33157
33218
|
const binaryPath = resolveFailproofaiBinary2();
|
|
33158
33219
|
const previousConfig = readScopedHooksConfig(scope, cwd);
|
|
@@ -35290,7 +35351,7 @@ import { realpathSync as realpathSync7 } from "fs";
|
|
|
35290
35351
|
import { dirname as dirname26, resolve as resolve36 } from "path";
|
|
35291
35352
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
35292
35353
|
// package.json
|
|
35293
|
-
var version = "1.0.6
|
|
35354
|
+
var version = "1.0.6";
|
|
35294
35355
|
|
|
35295
35356
|
// bin/failproofai.mjs
|
|
35296
35357
|
if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
|
package/dist/worker.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "1.0.6
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Observability and enforcement for AI agent harnesses. 39 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,15 +122,15 @@
|
|
|
122
122
|
"browserslist": "4.28.8"
|
|
123
123
|
},
|
|
124
124
|
"optionalDependencies": {
|
|
125
|
-
"@failproofai/failproofaid-linux-x64": "1.0.6
|
|
126
|
-
"@failproofai/failproofaid-linux-arm64": "1.0.6
|
|
127
|
-
"@failproofai/failproofaid-darwin-x64": "1.0.6
|
|
128
|
-
"@failproofai/failproofaid-darwin-arm64": "1.0.6
|
|
125
|
+
"@failproofai/failproofaid-linux-x64": "1.0.6",
|
|
126
|
+
"@failproofai/failproofaid-linux-arm64": "1.0.6",
|
|
127
|
+
"@failproofai/failproofaid-darwin-x64": "1.0.6",
|
|
128
|
+
"@failproofai/failproofaid-darwin-arm64": "1.0.6"
|
|
129
129
|
},
|
|
130
130
|
"failproofaidBinaries": {
|
|
131
|
-
"linux-x64": "
|
|
132
|
-
"linux-arm64": "
|
|
133
|
-
"darwin-x64": "
|
|
134
|
-
"darwin-arm64": "
|
|
131
|
+
"linux-x64": "e075f24a3a0950b631979d9370b665da2c3e2920decfb917ff0831d1f17c9740",
|
|
132
|
+
"linux-arm64": "b90ce8c642f2685719f4915695615219860daf3155a9c07344cf1e10395028c3",
|
|
133
|
+
"darwin-x64": "51dd95e9b85226e4f1888c7edaa56ec4464e91eb645d39c5cb7cede16d2da2f1",
|
|
134
|
+
"darwin-arm64": "f6a1b9d63363e8805e0d72b4fa168fde661bdbbd4125b54047f7de8948ea0f39"
|
|
135
135
|
}
|
|
136
136
|
}
|
|
@@ -93,6 +93,26 @@ export type DaemonAttempt =
|
|
|
93
93
|
| { ok: true; response: DaemonHookResponse }
|
|
94
94
|
| { ok: false; failure: DaemonFailure };
|
|
95
95
|
|
|
96
|
+
export interface DaemonPolicyEvaluationRequest {
|
|
97
|
+
integration: IntegrationType;
|
|
98
|
+
event: string;
|
|
99
|
+
payload: Record<string, unknown>;
|
|
100
|
+
cwd?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type DaemonPolicyEvaluationAttempt =
|
|
104
|
+
| { ok: true }
|
|
105
|
+
| { ok: false; failure: DaemonFailure };
|
|
106
|
+
|
|
107
|
+
interface DaemonRequestOptions {
|
|
108
|
+
/** Override the response budget; the connect budget is never relaxed. */
|
|
109
|
+
responseTimeoutMs?: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type DaemonWireAttempt =
|
|
113
|
+
| { ok: true; message: Record<string, unknown> }
|
|
114
|
+
| { ok: false; failure: DaemonFailure };
|
|
115
|
+
|
|
96
116
|
/**
|
|
97
117
|
* Whether a daemon is listening, regardless of how it was started.
|
|
98
118
|
*
|
|
@@ -176,29 +196,22 @@ function encodeFrame(value: unknown): Buffer {
|
|
|
176
196
|
return Buffer.concat([header, body]);
|
|
177
197
|
}
|
|
178
198
|
|
|
179
|
-
/**
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
* broken. Never set this on the hook path: 30s is matched to the
|
|
189
|
-
* daemon's own read timeout so this side never gives up on a request the
|
|
190
|
-
* daemon is still honestly working on.
|
|
191
|
-
*/
|
|
192
|
-
responseTimeoutMs?: number;
|
|
193
|
-
},
|
|
194
|
-
): Promise<DaemonAttempt> {
|
|
199
|
+
/**
|
|
200
|
+
* Sends one framed daemon request while preserving the failure category.
|
|
201
|
+
* Shape validation belongs to the typed caller: both shell hooks and native
|
|
202
|
+
* integrations share the transport, but require different response variants.
|
|
203
|
+
*/
|
|
204
|
+
async function attemptDaemonRequest(
|
|
205
|
+
request: Record<string, unknown>,
|
|
206
|
+
opts?: DaemonRequestOptions,
|
|
207
|
+
): Promise<DaemonWireAttempt> {
|
|
195
208
|
// Windows never has a daemon in this phase (see the plan's platform
|
|
196
209
|
// scope) — skip the attempt outright rather than depending on however
|
|
197
210
|
// Node happens to behave when handed a POSIX socket path on Windows.
|
|
198
211
|
if (process.platform === "win32") return { ok: false, failure: "unreachable" };
|
|
199
212
|
const responseBudget = opts?.responseTimeoutMs ?? DAEMON_RESPONSE_TIMEOUT_MS;
|
|
200
213
|
|
|
201
|
-
return new Promise<
|
|
214
|
+
return new Promise<DaemonWireAttempt>((resolvePromise) => {
|
|
202
215
|
let settled = false;
|
|
203
216
|
let timer: ReturnType<typeof setTimeout>;
|
|
204
217
|
const arm = (ms: number) => {
|
|
@@ -210,7 +223,7 @@ export async function attemptDaemonHook(
|
|
|
210
223
|
// process lingers if something upstream forgets to await us.
|
|
211
224
|
timer.unref?.();
|
|
212
225
|
};
|
|
213
|
-
const finish = (result:
|
|
226
|
+
const finish = (result: DaemonWireAttempt) => {
|
|
214
227
|
if (settled) return;
|
|
215
228
|
settled = true;
|
|
216
229
|
clearTimeout(timer);
|
|
@@ -230,16 +243,7 @@ export async function attemptDaemonHook(
|
|
|
230
243
|
// Connected: the daemon is demonstrably reachable, so the question is
|
|
231
244
|
// no longer "is it there" but "how long does this evaluation take".
|
|
232
245
|
arm(responseBudget);
|
|
233
|
-
socket.write(
|
|
234
|
-
encodeFrame({
|
|
235
|
-
type: "hook",
|
|
236
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
237
|
-
hookEvent: req.hookEvent,
|
|
238
|
-
cli: req.cli,
|
|
239
|
-
stdin: req.stdin,
|
|
240
|
-
cwd: req.cwd,
|
|
241
|
-
}),
|
|
242
|
-
);
|
|
246
|
+
socket.write(encodeFrame(request));
|
|
243
247
|
});
|
|
244
248
|
|
|
245
249
|
socket.on("data", (chunk: Buffer) => {
|
|
@@ -258,13 +262,18 @@ export async function attemptDaemonHook(
|
|
|
258
262
|
if (recvBuf.length < declaredLen) return;
|
|
259
263
|
|
|
260
264
|
const body = recvBuf.subarray(0, declaredLen);
|
|
261
|
-
let
|
|
265
|
+
let parsed: unknown;
|
|
262
266
|
try {
|
|
263
|
-
|
|
267
|
+
parsed = JSON.parse(body.toString("utf8"));
|
|
264
268
|
} catch {
|
|
265
269
|
fail("unreachable");
|
|
266
270
|
return;
|
|
267
271
|
}
|
|
272
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
273
|
+
fail("unreachable");
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const message = parsed as Record<string, unknown>;
|
|
268
277
|
|
|
269
278
|
if (message.protocolVersion !== PROTOCOL_VERSION) {
|
|
270
279
|
// Catches BOTH directions. A newer CLI sends v2 and the daemon replies
|
|
@@ -274,29 +283,98 @@ export async function attemptDaemonHook(
|
|
|
274
283
|
fail("protocol-mismatch");
|
|
275
284
|
return;
|
|
276
285
|
}
|
|
277
|
-
|
|
278
|
-
message.type === "hookResult" &&
|
|
279
|
-
typeof message.exitCode === "number" &&
|
|
280
|
-
typeof message.stdout === "string" &&
|
|
281
|
-
typeof message.stderr === "string"
|
|
282
|
-
) {
|
|
283
|
-
finish({
|
|
284
|
-
ok: true,
|
|
285
|
-
response: {
|
|
286
|
-
exitCode: message.exitCode,
|
|
287
|
-
stdout: message.stdout,
|
|
288
|
-
stderr: message.stderr,
|
|
289
|
-
},
|
|
290
|
-
});
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
// Anything else — an explicit `error` message at a MATCHING protocol
|
|
294
|
-
// version, a `pong` (protocol confusion), or a well-formed-but-wrong-shape
|
|
295
|
-
// body — is treated identically to a connection failure: no partial trust.
|
|
296
|
-
fail("unreachable");
|
|
286
|
+
finish({ ok: true, message });
|
|
297
287
|
});
|
|
298
288
|
|
|
299
289
|
socket.on("error", () => fail("unreachable"));
|
|
300
290
|
socket.on("close", () => fail("unreachable"));
|
|
301
291
|
});
|
|
302
292
|
}
|
|
293
|
+
|
|
294
|
+
/** Attempts a shell-hook daemon evaluation while preserving the failure category. */
|
|
295
|
+
export async function attemptDaemonHook(
|
|
296
|
+
req: DaemonHookRequest,
|
|
297
|
+
opts?: DaemonRequestOptions,
|
|
298
|
+
): Promise<DaemonAttempt> {
|
|
299
|
+
const attempt = await attemptDaemonRequest(
|
|
300
|
+
{
|
|
301
|
+
type: "hook",
|
|
302
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
303
|
+
hookEvent: req.hookEvent,
|
|
304
|
+
cli: req.cli,
|
|
305
|
+
stdin: req.stdin,
|
|
306
|
+
cwd: req.cwd,
|
|
307
|
+
},
|
|
308
|
+
opts,
|
|
309
|
+
);
|
|
310
|
+
if (!attempt.ok) return attempt;
|
|
311
|
+
|
|
312
|
+
const message = attempt.message;
|
|
313
|
+
if (
|
|
314
|
+
message.type === "hookResult" &&
|
|
315
|
+
typeof message.exitCode === "number" &&
|
|
316
|
+
typeof message.stdout === "string" &&
|
|
317
|
+
typeof message.stderr === "string"
|
|
318
|
+
) {
|
|
319
|
+
return {
|
|
320
|
+
ok: true,
|
|
321
|
+
response: {
|
|
322
|
+
exitCode: message.exitCode,
|
|
323
|
+
stdout: message.stdout,
|
|
324
|
+
stderr: message.stderr,
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// An explicit matching-version error, pong, or malformed hook result is not
|
|
330
|
+
// an evaluation. Never grant partial trust to a daemon that answered the
|
|
331
|
+
// connection but not the request we sent.
|
|
332
|
+
return { ok: false, failure: "unreachable" };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function isStringArray(value: unknown): value is string[] {
|
|
336
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Attempts the structured request used by native in-process integrations.
|
|
341
|
+
* A matching protocol version is not sufficient: pre-native v1 daemons speak
|
|
342
|
+
* `hook` but cannot deserialize `policyEvaluation`, so only a complete,
|
|
343
|
+
* well-shaped `policyResult` proves this capability exists.
|
|
344
|
+
*/
|
|
345
|
+
export async function attemptDaemonPolicyEvaluation(
|
|
346
|
+
req: DaemonPolicyEvaluationRequest,
|
|
347
|
+
opts?: DaemonRequestOptions,
|
|
348
|
+
): Promise<DaemonPolicyEvaluationAttempt> {
|
|
349
|
+
const attempt = await attemptDaemonRequest(
|
|
350
|
+
{
|
|
351
|
+
type: "policyEvaluation",
|
|
352
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
353
|
+
integration: req.integration,
|
|
354
|
+
event: req.event,
|
|
355
|
+
payload: req.payload,
|
|
356
|
+
cwd: req.cwd,
|
|
357
|
+
},
|
|
358
|
+
opts,
|
|
359
|
+
);
|
|
360
|
+
if (!attempt.ok) return attempt;
|
|
361
|
+
|
|
362
|
+
const message = attempt.message;
|
|
363
|
+
const validOptionalString = (value: unknown) =>
|
|
364
|
+
value === null || value === undefined || typeof value === "string";
|
|
365
|
+
if (
|
|
366
|
+
message.type === "policyResult" &&
|
|
367
|
+
(message.decision === "allow" || message.decision === "deny" || message.decision === "instruct") &&
|
|
368
|
+
isStringArray(message.policyNames) &&
|
|
369
|
+
validOptionalString(message.reason) &&
|
|
370
|
+
isStringArray(message.matchedPolicies) &&
|
|
371
|
+
typeof message.durationMs === "number" &&
|
|
372
|
+
Number.isInteger(message.durationMs) &&
|
|
373
|
+
message.durationMs >= 0 &&
|
|
374
|
+
validOptionalString(message.toolName)
|
|
375
|
+
) {
|
|
376
|
+
return { ok: true };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return { ok: false, failure: "unreachable" };
|
|
380
|
+
}
|
|
@@ -1070,6 +1070,36 @@ export async function probeDaemonEndToEnd(): Promise<boolean> {
|
|
|
1070
1070
|
return (await probeDaemon()).ok;
|
|
1071
1071
|
}
|
|
1072
1072
|
|
|
1073
|
+
/**
|
|
1074
|
+
* Proves that the daemon supports the structured request native plugins use.
|
|
1075
|
+
*
|
|
1076
|
+
* This deliberately follows the ordinary end-to-end hook probe. Besides
|
|
1077
|
+
* retaining its startup-race retry, that makes the failure meaningful: an old
|
|
1078
|
+
* daemon can be fully healthy for shell hooks while still lacking the
|
|
1079
|
+
* `policyEvaluation` variant added for Hermes. Protocol v1 predates that
|
|
1080
|
+
* variant, so version equality alone cannot establish the capability.
|
|
1081
|
+
*/
|
|
1082
|
+
export async function probeDaemonPolicyEvaluation(): Promise<boolean> {
|
|
1083
|
+
if (!(await probeDaemonEndToEnd())) return false;
|
|
1084
|
+
try {
|
|
1085
|
+
const { attemptDaemonPolicyEvaluation } = await import("./daemon-client");
|
|
1086
|
+
const attempt = await attemptDaemonPolicyEvaluation(
|
|
1087
|
+
{
|
|
1088
|
+
integration: "hermes",
|
|
1089
|
+
event: "on_session_start",
|
|
1090
|
+
payload: {
|
|
1091
|
+
hook_event_name: "on_session_start",
|
|
1092
|
+
source: "failproofai-native-policy-health-probe",
|
|
1093
|
+
},
|
|
1094
|
+
},
|
|
1095
|
+
{ responseTimeoutMs: DAEMON_PROBE_TIMEOUT_MS },
|
|
1096
|
+
);
|
|
1097
|
+
return attempt.ok;
|
|
1098
|
+
} catch {
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1073
1103
|
/**
|
|
1074
1104
|
* Waits for the service to report running and to HOLD it — a `Type=simple` unit
|
|
1075
1105
|
* is active the moment it forks, so one optimistic reading passes a daemon that
|
|
@@ -124,8 +124,9 @@ export interface Integration {
|
|
|
124
124
|
displayName: string;
|
|
125
125
|
/**
|
|
126
126
|
* This integration has no in-process/CLI fallback and cannot evaluate policy
|
|
127
|
-
* unless failproofaid answers
|
|
128
|
-
* before writing any hook or plugin when the daemon is unavailable
|
|
127
|
+
* unless failproofaid answers the structured native-policy probe. Installation
|
|
128
|
+
* must stop before writing any hook or plugin when the daemon is unavailable
|
|
129
|
+
* or predates the `policyEvaluation` capability.
|
|
129
130
|
*/
|
|
130
131
|
requiresHealthyDaemon?: boolean;
|
|
131
132
|
/** Settings scopes this integration supports (e.g. claude: user/project/local; codex: user/project). */
|
package/src/hooks/manager.ts
CHANGED
|
@@ -29,7 +29,7 @@ import { CORE_SOURCE, addPack, setPackPolicyEnabled } from "./pack-store";
|
|
|
29
29
|
import type { ResolvedPack } from "./pack-manifest";
|
|
30
30
|
import { hasInstalledPacks, readInstalledPacks } from "./pack-manifest";
|
|
31
31
|
import { packPolicyParamKey } from "./policy-evaluator";
|
|
32
|
-
import {
|
|
32
|
+
import { probeDaemonPolicyEvaluation } from "./daemon-service";
|
|
33
33
|
import {
|
|
34
34
|
chip,
|
|
35
35
|
note,
|
|
@@ -477,12 +477,12 @@ async function installHooksImpl(
|
|
|
477
477
|
const daemonRequiredBy = selectedIntegrations
|
|
478
478
|
.map(({ integration }) => integration)
|
|
479
479
|
.filter((integration) => integration.requiresHealthyDaemon);
|
|
480
|
-
if (daemonRequiredBy.length > 0 && !(await
|
|
480
|
+
if (daemonRequiredBy.length > 0 && !(await probeDaemonPolicyEvaluation())) {
|
|
481
481
|
const names = daemonRequiredBy.map((integration) => integration.displayName).join(", ");
|
|
482
482
|
const verb = daemonRequiredBy.length === 1 ? "requires" : "require";
|
|
483
483
|
throw new CliError(
|
|
484
|
-
`${names} ${verb} a
|
|
485
|
-
"Run `failproofai config` to install
|
|
484
|
+
`${names} ${verb} a compatible failproofaid daemon with native policy evaluation before FailproofAI enforcement can be enabled.\n` +
|
|
485
|
+
"Run `failproofai config` to install or update the daemon, then retry.",
|
|
486
486
|
);
|
|
487
487
|
}
|
|
488
488
|
|