farai 0.3.4 → 0.3.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/dist/cli/index.js +2096 -61
- package/dist/cli/index.js.map +21 -13
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -8784,8 +8784,8 @@ var init_process_output = __esm(() => {
|
|
|
8784
8784
|
|
|
8785
8785
|
// src/version.ts
|
|
8786
8786
|
function resolveFaraiVersion() {
|
|
8787
|
-
if ("0.3.
|
|
8788
|
-
return "0.3.
|
|
8787
|
+
if ("0.3.6")
|
|
8788
|
+
return "0.3.6";
|
|
8789
8789
|
try {
|
|
8790
8790
|
const parsed = JSON.parse(readBoundedFileTextSync(new URL("../package.json", import.meta.url), 1024 * 1024, "package metadata"));
|
|
8791
8791
|
if (typeof parsed.version === "string" && parsed.version)
|
|
@@ -28681,6 +28681,2019 @@ var init_email = __esm(() => {
|
|
|
28681
28681
|
emailTools = [emailListTool, emailCreateTool, emailInboxTool, emailReadTool, emailWaitTool];
|
|
28682
28682
|
});
|
|
28683
28683
|
|
|
28684
|
+
// src/agent-tools/android/shared.ts
|
|
28685
|
+
function shellQuote7(value) {
|
|
28686
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
28687
|
+
}
|
|
28688
|
+
function adbUnavailable(result) {
|
|
28689
|
+
return result.exitCode === 127 || /adb: not found|command not found/i.test(result.stderr);
|
|
28690
|
+
}
|
|
28691
|
+
function compactError2(value) {
|
|
28692
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
28693
|
+
return compact.slice(0, 400) || "adb command produced no error output";
|
|
28694
|
+
}
|
|
28695
|
+
async function runAdb(context, argline, timeoutMs, maxBytes = 2000000) {
|
|
28696
|
+
return backend(context).exec(argline, timeoutMs, context.signal, maxBytes);
|
|
28697
|
+
}
|
|
28698
|
+
function adbEnvPrefix() {
|
|
28699
|
+
const parts = ADB_SERVER_ENV.filter((name) => (process.env[name] ?? "").trim()).map((name) => `${name}=${shellQuote7(process.env[name].trim())}`);
|
|
28700
|
+
return parts.length ? `${parts.join(" ")} ` : "";
|
|
28701
|
+
}
|
|
28702
|
+
function adbBase() {
|
|
28703
|
+
return `${adbEnvPrefix()}adb`;
|
|
28704
|
+
}
|
|
28705
|
+
function adbPrefix(serial) {
|
|
28706
|
+
const trimmed = serial?.trim();
|
|
28707
|
+
return trimmed ? `${adbBase()} -s ${shellQuote7(trimmed)}` : adbBase();
|
|
28708
|
+
}
|
|
28709
|
+
function parseDevices(stdout) {
|
|
28710
|
+
const devices = [];
|
|
28711
|
+
for (const line of stdout.split(`
|
|
28712
|
+
`)) {
|
|
28713
|
+
const trimmed = line.trim();
|
|
28714
|
+
if (!trimmed || /^List of devices/i.test(trimmed))
|
|
28715
|
+
continue;
|
|
28716
|
+
const [serial, state, ...rest] = trimmed.split(/\s+/);
|
|
28717
|
+
if (!serial || !state)
|
|
28718
|
+
continue;
|
|
28719
|
+
const meta = rest.join(" ");
|
|
28720
|
+
const model = /\bmodel:(\S+)/.exec(meta)?.[1];
|
|
28721
|
+
const product = /\bproduct:(\S+)/.exec(meta)?.[1];
|
|
28722
|
+
devices.push({
|
|
28723
|
+
serial,
|
|
28724
|
+
state,
|
|
28725
|
+
...model ? {
|
|
28726
|
+
model
|
|
28727
|
+
} : {},
|
|
28728
|
+
...product ? {
|
|
28729
|
+
product
|
|
28730
|
+
} : {}
|
|
28731
|
+
});
|
|
28732
|
+
}
|
|
28733
|
+
return devices;
|
|
28734
|
+
}
|
|
28735
|
+
async function resolveDevice(context, serial, timeoutMs = 15000) {
|
|
28736
|
+
const provided = serial?.trim();
|
|
28737
|
+
if (provided)
|
|
28738
|
+
return provided;
|
|
28739
|
+
const result = await runAdb(context, `${adbBase()} devices -l`, timeoutMs);
|
|
28740
|
+
if (adbUnavailable(result))
|
|
28741
|
+
throw new Error("adb is not available in the container");
|
|
28742
|
+
const online = parseDevices(result.stdout).filter((device) => device.state === "device");
|
|
28743
|
+
if (online.length === 0)
|
|
28744
|
+
throw new Error("no android device is connected; use android_connect to attach one over tcp/ip");
|
|
28745
|
+
if (online.length > 1)
|
|
28746
|
+
throw new Error(`multiple devices connected (${online.map((device) => device.serial).join(", ")}); pass the serial argument`);
|
|
28747
|
+
return online[0].serial;
|
|
28748
|
+
}
|
|
28749
|
+
var ADB_SERVER_ENV;
|
|
28750
|
+
var init_shared4 = __esm(() => {
|
|
28751
|
+
init_backend();
|
|
28752
|
+
ADB_SERVER_ENV = ["ADB_SERVER_SOCKET", "ANDROID_ADB_SERVER_ADDRESS", "ANDROID_ADB_SERVER_PORT"];
|
|
28753
|
+
});
|
|
28754
|
+
|
|
28755
|
+
// src/agent-tools/android/device.ts
|
|
28756
|
+
var SERIAL_PROP, androidConnectTool, androidDevicesTool, androidShellTool, androidPackagesTool, androidDeviceInfoTool, androidLogcatTool;
|
|
28757
|
+
var init_device = __esm(() => {
|
|
28758
|
+
init_renderers();
|
|
28759
|
+
init_shared4();
|
|
28760
|
+
SERIAL_PROP = {
|
|
28761
|
+
type: "string",
|
|
28762
|
+
description: "device serial from android_devices; omit when exactly one device is connected"
|
|
28763
|
+
};
|
|
28764
|
+
androidConnectTool = {
|
|
28765
|
+
name: "android_connect",
|
|
28766
|
+
description: "Attach an android device over adb tcp/ip. Use this first when the device is reachable by wireless debugging, an emulator, or a remote host, since usb passthrough is unavailable inside the container. Provide host:port (default port 5555).",
|
|
28767
|
+
inputSchema: {
|
|
28768
|
+
type: "object",
|
|
28769
|
+
required: ["address"],
|
|
28770
|
+
properties: {
|
|
28771
|
+
address: {
|
|
28772
|
+
type: "string",
|
|
28773
|
+
description: "device address as host or host:port; port defaults to 5555 when omitted"
|
|
28774
|
+
}
|
|
28775
|
+
},
|
|
28776
|
+
additionalProperties: false
|
|
28777
|
+
},
|
|
28778
|
+
mutates: true,
|
|
28779
|
+
timeoutMs: 30000,
|
|
28780
|
+
parallel: false,
|
|
28781
|
+
renderHuman: defaultHumanRenderer,
|
|
28782
|
+
renderModel: defaultModelRenderer,
|
|
28783
|
+
run: async (args, context) => {
|
|
28784
|
+
assertObject(args, "args");
|
|
28785
|
+
const raw = asString(args.address, "address").trim();
|
|
28786
|
+
const address = /:\d+$/.test(raw) ? raw : `${raw}:5555`;
|
|
28787
|
+
const result = await runAdb(context, `${adbBase()} connect ${shellQuote7(address)}`, 30000);
|
|
28788
|
+
if (adbUnavailable(result))
|
|
28789
|
+
throw new Error("adb is not available in the container");
|
|
28790
|
+
const text2 = `${result.stdout}${result.stderr}`.trim();
|
|
28791
|
+
const ok = /connected to/i.test(text2) && !/cannot|failed|unable|refused/i.test(text2);
|
|
28792
|
+
return {
|
|
28793
|
+
ok,
|
|
28794
|
+
summary: ok ? `connected to ${address}` : `could not connect to ${address}`,
|
|
28795
|
+
output: text2 || `no output; exit ${result.exitCode}`,
|
|
28796
|
+
metadata: {
|
|
28797
|
+
address,
|
|
28798
|
+
connected: ok
|
|
28799
|
+
}
|
|
28800
|
+
};
|
|
28801
|
+
}
|
|
28802
|
+
};
|
|
28803
|
+
androidDevicesTool = {
|
|
28804
|
+
name: "android_devices",
|
|
28805
|
+
description: "List android devices adb can currently see, with serial, connection state, and model. Use this to pick a serial before other android tools, or to confirm android_connect worked.",
|
|
28806
|
+
inputSchema: {
|
|
28807
|
+
type: "object",
|
|
28808
|
+
properties: {},
|
|
28809
|
+
additionalProperties: false
|
|
28810
|
+
},
|
|
28811
|
+
mutates: false,
|
|
28812
|
+
timeoutMs: 15000,
|
|
28813
|
+
parallel: true,
|
|
28814
|
+
renderHuman: defaultHumanRenderer,
|
|
28815
|
+
renderModel: defaultModelRenderer,
|
|
28816
|
+
run: async (args, context) => {
|
|
28817
|
+
assertObject(args, "args");
|
|
28818
|
+
const result = await runAdb(context, `${adbBase()} devices -l`, 15000);
|
|
28819
|
+
if (adbUnavailable(result))
|
|
28820
|
+
throw new Error("adb is not available in the container");
|
|
28821
|
+
const devices = parseDevices(result.stdout);
|
|
28822
|
+
const online = devices.filter((device) => device.state === "device");
|
|
28823
|
+
const output = devices.length ? devices.map((device) => `${device.serial} ${device.state}${device.model ? ` model:${device.model}` : ""}`).join(`
|
|
28824
|
+
`) : "no devices";
|
|
28825
|
+
return {
|
|
28826
|
+
ok: true,
|
|
28827
|
+
summary: `${devices.length} device(s), ${online.length} online`,
|
|
28828
|
+
output,
|
|
28829
|
+
metadata: {
|
|
28830
|
+
devices
|
|
28831
|
+
}
|
|
28832
|
+
};
|
|
28833
|
+
}
|
|
28834
|
+
};
|
|
28835
|
+
androidShellTool = {
|
|
28836
|
+
name: "android_shell",
|
|
28837
|
+
description: "Run one shell command on the android device via adb shell. Use purpose-built android tools when they model the task; use this for arbitrary on-device commands, dumpsys, pm, or content queries.",
|
|
28838
|
+
inputSchema: {
|
|
28839
|
+
type: "object",
|
|
28840
|
+
required: ["command"],
|
|
28841
|
+
properties: {
|
|
28842
|
+
command: {
|
|
28843
|
+
type: "string",
|
|
28844
|
+
description: "complete shell command to run inside adb shell on the device"
|
|
28845
|
+
},
|
|
28846
|
+
serial: SERIAL_PROP
|
|
28847
|
+
},
|
|
28848
|
+
additionalProperties: false
|
|
28849
|
+
},
|
|
28850
|
+
mutates: true,
|
|
28851
|
+
timeoutMs: 60000,
|
|
28852
|
+
parallel: false,
|
|
28853
|
+
renderHuman: defaultHumanRenderer,
|
|
28854
|
+
renderModel: defaultModelRenderer,
|
|
28855
|
+
run: async (args, context) => {
|
|
28856
|
+
assertObject(args, "args");
|
|
28857
|
+
const command = asString(args.command, "command");
|
|
28858
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
28859
|
+
const result = await runAdb(context, `${adbPrefix(serial)} shell ${shellQuote7(command)}`, 60000);
|
|
28860
|
+
if (adbUnavailable(result))
|
|
28861
|
+
throw new Error("adb is not available in the container");
|
|
28862
|
+
const output = `${result.stdout}${result.stderr ? `
|
|
28863
|
+
${result.stderr}` : ""}`.trim();
|
|
28864
|
+
return {
|
|
28865
|
+
ok: result.exitCode === 0,
|
|
28866
|
+
summary: result.exitCode === 0 ? `ran on ${serial}` : `command exited ${result.exitCode} on ${serial}`,
|
|
28867
|
+
output: output || "(no output)",
|
|
28868
|
+
metadata: {
|
|
28869
|
+
serial,
|
|
28870
|
+
exitCode: result.exitCode
|
|
28871
|
+
}
|
|
28872
|
+
};
|
|
28873
|
+
}
|
|
28874
|
+
};
|
|
28875
|
+
androidPackagesTool = {
|
|
28876
|
+
name: "android_packages",
|
|
28877
|
+
description: "List installed packages on the device. Use thirdPartyOnly to focus on user-installed apps, and filter to narrow by substring.",
|
|
28878
|
+
inputSchema: {
|
|
28879
|
+
type: "object",
|
|
28880
|
+
properties: {
|
|
28881
|
+
filter: {
|
|
28882
|
+
type: "string",
|
|
28883
|
+
description: "case-insensitive substring to match against package names"
|
|
28884
|
+
},
|
|
28885
|
+
thirdPartyOnly: {
|
|
28886
|
+
type: "boolean",
|
|
28887
|
+
description: "list only user-installed apps (pm list packages -3) when true"
|
|
28888
|
+
},
|
|
28889
|
+
serial: SERIAL_PROP
|
|
28890
|
+
},
|
|
28891
|
+
additionalProperties: false
|
|
28892
|
+
},
|
|
28893
|
+
mutates: false,
|
|
28894
|
+
timeoutMs: 30000,
|
|
28895
|
+
parallel: true,
|
|
28896
|
+
renderHuman: defaultHumanRenderer,
|
|
28897
|
+
renderModel: defaultModelRenderer,
|
|
28898
|
+
run: async (args, context) => {
|
|
28899
|
+
assertObject(args, "args");
|
|
28900
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
28901
|
+
const thirdParty = args.thirdPartyOnly === true;
|
|
28902
|
+
const result = await runAdb(context, `${adbPrefix(serial)} shell pm list packages${thirdParty ? " -3" : ""}`, 30000);
|
|
28903
|
+
if (adbUnavailable(result))
|
|
28904
|
+
throw new Error("adb is not available in the container");
|
|
28905
|
+
const filter = typeof args.filter === "string" ? args.filter.trim().toLowerCase() : "";
|
|
28906
|
+
let packages = result.stdout.split(`
|
|
28907
|
+
`).map((line) => line.replace(/^package:/, "").trim()).filter(Boolean);
|
|
28908
|
+
if (filter)
|
|
28909
|
+
packages = packages.filter((name) => name.toLowerCase().includes(filter));
|
|
28910
|
+
packages.sort();
|
|
28911
|
+
return {
|
|
28912
|
+
ok: result.exitCode === 0,
|
|
28913
|
+
summary: `${packages.length} package(s)${thirdParty ? " (third-party)" : ""}${filter ? ` matching "${filter}"` : ""}`,
|
|
28914
|
+
output: packages.length ? packages.join(`
|
|
28915
|
+
`) : "no packages matched",
|
|
28916
|
+
metadata: {
|
|
28917
|
+
serial,
|
|
28918
|
+
count: packages.length,
|
|
28919
|
+
packages: packages.slice(0, 1000)
|
|
28920
|
+
}
|
|
28921
|
+
};
|
|
28922
|
+
}
|
|
28923
|
+
};
|
|
28924
|
+
androidDeviceInfoTool = {
|
|
28925
|
+
name: "android_device_info",
|
|
28926
|
+
description: "Summarize the target device in one call: android version, sdk, model, cpu abi, and root/su availability. Use this early to shape the methodology for the device.",
|
|
28927
|
+
inputSchema: {
|
|
28928
|
+
type: "object",
|
|
28929
|
+
properties: {
|
|
28930
|
+
serial: SERIAL_PROP
|
|
28931
|
+
},
|
|
28932
|
+
additionalProperties: false
|
|
28933
|
+
},
|
|
28934
|
+
mutates: false,
|
|
28935
|
+
timeoutMs: 30000,
|
|
28936
|
+
parallel: true,
|
|
28937
|
+
renderHuman: defaultHumanRenderer,
|
|
28938
|
+
renderModel: defaultModelRenderer,
|
|
28939
|
+
run: async (args, context) => {
|
|
28940
|
+
assertObject(args, "args");
|
|
28941
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
28942
|
+
const props = ["ro.build.version.release", "ro.build.version.sdk", "ro.product.model", "ro.product.manufacturer", "ro.product.cpu.abi", "ro.build.type"];
|
|
28943
|
+
const command = `${adbPrefix(serial)} shell ${shellQuote7(`for p in ${props.join(" ")}; do echo "$p=$(getprop $p)"; done; echo su=$(command -v su || echo none)`)}`;
|
|
28944
|
+
const result = await runAdb(context, command, 30000);
|
|
28945
|
+
if (adbUnavailable(result))
|
|
28946
|
+
throw new Error("adb is not available in the container");
|
|
28947
|
+
const info = {};
|
|
28948
|
+
for (const line of result.stdout.split(`
|
|
28949
|
+
`)) {
|
|
28950
|
+
const eq = line.indexOf("=");
|
|
28951
|
+
if (eq > 0)
|
|
28952
|
+
info[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
|
|
28953
|
+
}
|
|
28954
|
+
return {
|
|
28955
|
+
ok: result.exitCode === 0,
|
|
28956
|
+
summary: `${info["ro.product.manufacturer"] ?? "?"} ${info["ro.product.model"] ?? serial}, android ${info["ro.build.version.release"] ?? "?"} (sdk ${info["ro.build.version.sdk"] ?? "?"})`,
|
|
28957
|
+
output: Object.entries(info).map(([key, value]) => `${key}: ${value}`).join(`
|
|
28958
|
+
`) || result.stdout,
|
|
28959
|
+
metadata: {
|
|
28960
|
+
serial,
|
|
28961
|
+
info
|
|
28962
|
+
}
|
|
28963
|
+
};
|
|
28964
|
+
}
|
|
28965
|
+
};
|
|
28966
|
+
androidLogcatTool = {
|
|
28967
|
+
name: "android_logcat",
|
|
28968
|
+
description: "Capture recent logcat output from the device, optionally filtered by tag. Use this to spot leaked tokens, stack traces, and app behavior after an action; it dumps the current buffer and returns.",
|
|
28969
|
+
inputSchema: {
|
|
28970
|
+
type: "object",
|
|
28971
|
+
properties: {
|
|
28972
|
+
tag: {
|
|
28973
|
+
type: "string",
|
|
28974
|
+
description: "logcat tag filter; only lines from this tag are returned"
|
|
28975
|
+
},
|
|
28976
|
+
lines: {
|
|
28977
|
+
type: "integer",
|
|
28978
|
+
minimum: 1,
|
|
28979
|
+
maximum: 5000,
|
|
28980
|
+
description: "maximum trailing lines to return (default 200)"
|
|
28981
|
+
},
|
|
28982
|
+
serial: SERIAL_PROP
|
|
28983
|
+
},
|
|
28984
|
+
additionalProperties: false
|
|
28985
|
+
},
|
|
28986
|
+
mutates: false,
|
|
28987
|
+
timeoutMs: 30000,
|
|
28988
|
+
parallel: true,
|
|
28989
|
+
renderHuman: defaultHumanRenderer,
|
|
28990
|
+
renderModel: defaultModelRenderer,
|
|
28991
|
+
run: async (args, context) => {
|
|
28992
|
+
assertObject(args, "args");
|
|
28993
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
28994
|
+
const lines = typeof args.lines === "number" && Number.isInteger(args.lines) ? Math.max(1, Math.min(5000, args.lines)) : 200;
|
|
28995
|
+
const tag = typeof args.tag === "string" && args.tag.trim() ? args.tag.trim() : "";
|
|
28996
|
+
const filter = tag ? ` -s ${shellQuote7(tag)}` : "";
|
|
28997
|
+
const result = await runAdb(context, `${adbPrefix(serial)} logcat -d -t ${lines}${filter}`, 30000);
|
|
28998
|
+
if (adbUnavailable(result))
|
|
28999
|
+
throw new Error("adb is not available in the container");
|
|
29000
|
+
const output = result.stdout.trim();
|
|
29001
|
+
return {
|
|
29002
|
+
ok: result.exitCode === 0,
|
|
29003
|
+
summary: `${output.split(`
|
|
29004
|
+
`).filter(Boolean).length} logcat line(s)${tag ? ` for tag ${tag}` : ""}`,
|
|
29005
|
+
output: output || "(empty logcat buffer)",
|
|
29006
|
+
metadata: {
|
|
29007
|
+
serial,
|
|
29008
|
+
tag: tag || null
|
|
29009
|
+
}
|
|
29010
|
+
};
|
|
29011
|
+
}
|
|
29012
|
+
};
|
|
29013
|
+
});
|
|
29014
|
+
|
|
29015
|
+
// src/agent-tools/android/app.ts
|
|
29016
|
+
function sanitizePackage(value) {
|
|
29017
|
+
const clean = value.trim();
|
|
29018
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_.]*$/.test(clean))
|
|
29019
|
+
throw new Error("package must be a valid android package name");
|
|
29020
|
+
return clean;
|
|
29021
|
+
}
|
|
29022
|
+
function appLifecycleTool(name, verb) {
|
|
29023
|
+
return {
|
|
29024
|
+
name,
|
|
29025
|
+
description: verb === "start" ? "Launch an app by package name using monkey so the default launcher activity starts. Use before ui or dynamic tools that need the app running." : "Force-stop an app by package name. Use to reset app state between tests.",
|
|
29026
|
+
inputSchema: {
|
|
29027
|
+
type: "object",
|
|
29028
|
+
required: ["package"],
|
|
29029
|
+
properties: {
|
|
29030
|
+
package: {
|
|
29031
|
+
type: "string",
|
|
29032
|
+
description: "installed package name to control"
|
|
29033
|
+
},
|
|
29034
|
+
serial: SERIAL_PROP2
|
|
29035
|
+
},
|
|
29036
|
+
additionalProperties: false
|
|
29037
|
+
},
|
|
29038
|
+
mutates: true,
|
|
29039
|
+
timeoutMs: 30000,
|
|
29040
|
+
parallel: false,
|
|
29041
|
+
renderHuman: defaultHumanRenderer,
|
|
29042
|
+
renderModel: defaultModelRenderer,
|
|
29043
|
+
run: async (args, context) => {
|
|
29044
|
+
assertObject(args, "args");
|
|
29045
|
+
const pkg = sanitizePackage(asString(args.package, "package"));
|
|
29046
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
29047
|
+
const shell = verb === "start" ? `monkey -p ${shellQuote7(pkg)} -c android.intent.category.LAUNCHER 1` : `am force-stop ${shellQuote7(pkg)}`;
|
|
29048
|
+
const result = await runAdb(context, `${adbPrefix(serial)} shell ${shellQuote7(shell)}`, 30000);
|
|
29049
|
+
if (adbUnavailable(result))
|
|
29050
|
+
throw new Error("adb is not available in the container");
|
|
29051
|
+
const output = `${result.stdout}${result.stderr ? `
|
|
29052
|
+
${result.stderr}` : ""}`.trim();
|
|
29053
|
+
const ok = result.exitCode === 0 && !/error|no activities found/i.test(output);
|
|
29054
|
+
return {
|
|
29055
|
+
ok,
|
|
29056
|
+
summary: ok ? `${verb === "start" ? "started" : "stopped"} ${pkg}` : `could not ${verb} ${pkg}`,
|
|
29057
|
+
output: output || "(no output)",
|
|
29058
|
+
metadata: {
|
|
29059
|
+
serial,
|
|
29060
|
+
package: pkg
|
|
29061
|
+
}
|
|
29062
|
+
};
|
|
29063
|
+
}
|
|
29064
|
+
};
|
|
29065
|
+
}
|
|
29066
|
+
var SERIAL_PROP2, androidApkPullTool, androidInstallTool, androidAppStartTool, androidAppStopTool, androidDeeplinkTool, androidPullFileTool;
|
|
29067
|
+
var init_app = __esm(() => {
|
|
29068
|
+
init_renderers();
|
|
29069
|
+
init_shared4();
|
|
29070
|
+
SERIAL_PROP2 = {
|
|
29071
|
+
type: "string",
|
|
29072
|
+
description: "device serial from android_devices; omit when exactly one device is connected"
|
|
29073
|
+
};
|
|
29074
|
+
androidApkPullTool = {
|
|
29075
|
+
name: "android_apk_pull",
|
|
29076
|
+
description: "Pull every apk for an installed package (including split apks) from the device into the workspace for static analysis. Returns the local directory and file list.",
|
|
29077
|
+
inputSchema: {
|
|
29078
|
+
type: "object",
|
|
29079
|
+
required: ["package"],
|
|
29080
|
+
properties: {
|
|
29081
|
+
package: {
|
|
29082
|
+
type: "string",
|
|
29083
|
+
description: "installed package name, e.g. com.example.app"
|
|
29084
|
+
},
|
|
29085
|
+
serial: SERIAL_PROP2
|
|
29086
|
+
},
|
|
29087
|
+
additionalProperties: false
|
|
29088
|
+
},
|
|
29089
|
+
mutates: true,
|
|
29090
|
+
timeoutMs: 120000,
|
|
29091
|
+
parallel: false,
|
|
29092
|
+
renderHuman: defaultHumanRenderer,
|
|
29093
|
+
renderModel: defaultModelRenderer,
|
|
29094
|
+
run: async (args, context) => {
|
|
29095
|
+
assertObject(args, "args");
|
|
29096
|
+
const pkg = sanitizePackage(asString(args.package, "package"));
|
|
29097
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
29098
|
+
const pathResult = await runAdb(context, `${adbPrefix(serial)} shell pm path ${shellQuote7(pkg)}`, 30000);
|
|
29099
|
+
if (adbUnavailable(pathResult))
|
|
29100
|
+
throw new Error("adb is not available in the container");
|
|
29101
|
+
const remotes = pathResult.stdout.split(`
|
|
29102
|
+
`).map((line) => line.replace(/^package:/, "").trim()).filter(Boolean);
|
|
29103
|
+
if (remotes.length === 0)
|
|
29104
|
+
throw new Error(`package not found on device: ${pkg}`);
|
|
29105
|
+
const destDir = `android/${pkg}`;
|
|
29106
|
+
await runAdb(context, `mkdir -p ${shellQuote7(destDir)}`, 1e4);
|
|
29107
|
+
const pulled = [];
|
|
29108
|
+
const errors = [];
|
|
29109
|
+
for (const remote of remotes) {
|
|
29110
|
+
const local = `${destDir}/${remote.split("/").pop() || "base.apk"}`;
|
|
29111
|
+
const result = await runAdb(context, `${adbPrefix(serial)} pull ${shellQuote7(remote)} ${shellQuote7(local)}`, 90000);
|
|
29112
|
+
if (result.exitCode === 0)
|
|
29113
|
+
pulled.push(local);
|
|
29114
|
+
else
|
|
29115
|
+
errors.push(`${remote}: ${compactError2(result.stderr || result.stdout)}`);
|
|
29116
|
+
}
|
|
29117
|
+
return {
|
|
29118
|
+
ok: pulled.length > 0,
|
|
29119
|
+
summary: pulled.length ? `pulled ${pulled.length} apk(s) for ${pkg} to ${destDir}` : `failed to pull apks for ${pkg}`,
|
|
29120
|
+
output: [...pulled.map((path) => `pulled: ${path}`), ...errors.map((err) => `error: ${err}`)].join(`
|
|
29121
|
+
`),
|
|
29122
|
+
metadata: {
|
|
29123
|
+
serial,
|
|
29124
|
+
package: pkg,
|
|
29125
|
+
directory: destDir,
|
|
29126
|
+
files: pulled
|
|
29127
|
+
}
|
|
29128
|
+
};
|
|
29129
|
+
}
|
|
29130
|
+
};
|
|
29131
|
+
androidInstallTool = {
|
|
29132
|
+
name: "android_install",
|
|
29133
|
+
description: "Install an apk on the device with adb install -r. Use for patched or instrumentation builds; the path is a workspace-relative apk file.",
|
|
29134
|
+
inputSchema: {
|
|
29135
|
+
type: "object",
|
|
29136
|
+
required: ["apkPath"],
|
|
29137
|
+
properties: {
|
|
29138
|
+
apkPath: {
|
|
29139
|
+
type: "string",
|
|
29140
|
+
description: "workspace-relative path to the apk file to install"
|
|
29141
|
+
},
|
|
29142
|
+
serial: SERIAL_PROP2
|
|
29143
|
+
},
|
|
29144
|
+
additionalProperties: false
|
|
29145
|
+
},
|
|
29146
|
+
mutates: true,
|
|
29147
|
+
timeoutMs: 120000,
|
|
29148
|
+
parallel: false,
|
|
29149
|
+
renderHuman: defaultHumanRenderer,
|
|
29150
|
+
renderModel: defaultModelRenderer,
|
|
29151
|
+
run: async (args, context) => {
|
|
29152
|
+
assertObject(args, "args");
|
|
29153
|
+
const apkPath = asString(args.apkPath, "apkPath").trim();
|
|
29154
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
29155
|
+
const result = await runAdb(context, `${adbPrefix(serial)} install -r ${shellQuote7(apkPath)}`, 120000);
|
|
29156
|
+
if (adbUnavailable(result))
|
|
29157
|
+
throw new Error("adb is not available in the container");
|
|
29158
|
+
const text2 = `${result.stdout}${result.stderr}`.trim();
|
|
29159
|
+
const ok = /success/i.test(text2);
|
|
29160
|
+
return {
|
|
29161
|
+
ok,
|
|
29162
|
+
summary: ok ? `installed ${apkPath} on ${serial}` : `install failed for ${apkPath}`,
|
|
29163
|
+
output: text2 || `exit ${result.exitCode}`,
|
|
29164
|
+
metadata: {
|
|
29165
|
+
serial,
|
|
29166
|
+
apkPath,
|
|
29167
|
+
installed: ok
|
|
29168
|
+
}
|
|
29169
|
+
};
|
|
29170
|
+
}
|
|
29171
|
+
};
|
|
29172
|
+
androidAppStartTool = appLifecycleTool("android_app_start", "start");
|
|
29173
|
+
androidAppStopTool = appLifecycleTool("android_app_stop", "stop");
|
|
29174
|
+
androidDeeplinkTool = {
|
|
29175
|
+
name: "android_deeplink",
|
|
29176
|
+
description: "Fire a deep-link intent (VIEW) on the device to test deep-link and exported-component handling. Optionally scope it to a package to target one app.",
|
|
29177
|
+
inputSchema: {
|
|
29178
|
+
type: "object",
|
|
29179
|
+
required: ["uri"],
|
|
29180
|
+
properties: {
|
|
29181
|
+
uri: {
|
|
29182
|
+
type: "string",
|
|
29183
|
+
description: "deep link uri to open, e.g. myapp://path?arg=1"
|
|
29184
|
+
},
|
|
29185
|
+
package: {
|
|
29186
|
+
type: "string",
|
|
29187
|
+
description: "optional package to constrain the intent to one app"
|
|
29188
|
+
},
|
|
29189
|
+
serial: SERIAL_PROP2
|
|
29190
|
+
},
|
|
29191
|
+
additionalProperties: false
|
|
29192
|
+
},
|
|
29193
|
+
mutates: true,
|
|
29194
|
+
timeoutMs: 30000,
|
|
29195
|
+
parallel: false,
|
|
29196
|
+
renderHuman: defaultHumanRenderer,
|
|
29197
|
+
renderModel: defaultModelRenderer,
|
|
29198
|
+
run: async (args, context) => {
|
|
29199
|
+
assertObject(args, "args");
|
|
29200
|
+
const uri = asString(args.uri, "uri").trim();
|
|
29201
|
+
const pkg = typeof args.package === "string" && args.package.trim() ? sanitizePackage(args.package) : "";
|
|
29202
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
29203
|
+
const shell = `am start -a android.intent.action.VIEW -d ${shellQuote7(uri)}${pkg ? ` ${shellQuote7(pkg)}` : ""}`;
|
|
29204
|
+
const result = await runAdb(context, `${adbPrefix(serial)} shell ${shellQuote7(shell)}`, 30000);
|
|
29205
|
+
if (adbUnavailable(result))
|
|
29206
|
+
throw new Error("adb is not available in the container");
|
|
29207
|
+
const output = `${result.stdout}${result.stderr ? `
|
|
29208
|
+
${result.stderr}` : ""}`.trim();
|
|
29209
|
+
const ok = result.exitCode === 0 && !/error|exception/i.test(output);
|
|
29210
|
+
return {
|
|
29211
|
+
ok,
|
|
29212
|
+
summary: ok ? `fired deep link ${uri}` : `deep link may have failed: ${uri}`,
|
|
29213
|
+
output: output || "(no output)",
|
|
29214
|
+
metadata: {
|
|
29215
|
+
serial,
|
|
29216
|
+
uri,
|
|
29217
|
+
package: pkg || null
|
|
29218
|
+
}
|
|
29219
|
+
};
|
|
29220
|
+
}
|
|
29221
|
+
};
|
|
29222
|
+
androidPullFileTool = {
|
|
29223
|
+
name: "android_pull_file",
|
|
29224
|
+
description: "Read a file from the device. When package is given, uses run-as <package> to reach app-private files (requires a debuggable app). Returns bounded file contents.",
|
|
29225
|
+
inputSchema: {
|
|
29226
|
+
type: "object",
|
|
29227
|
+
required: ["path"],
|
|
29228
|
+
properties: {
|
|
29229
|
+
path: {
|
|
29230
|
+
type: "string",
|
|
29231
|
+
description: "absolute device path to read, e.g. /data/data/pkg/shared_prefs/x.xml"
|
|
29232
|
+
},
|
|
29233
|
+
package: {
|
|
29234
|
+
type: "string",
|
|
29235
|
+
description: "package to read app-private files via run-as; omit for world-readable paths"
|
|
29236
|
+
},
|
|
29237
|
+
serial: SERIAL_PROP2
|
|
29238
|
+
},
|
|
29239
|
+
additionalProperties: false
|
|
29240
|
+
},
|
|
29241
|
+
mutates: false,
|
|
29242
|
+
timeoutMs: 30000,
|
|
29243
|
+
parallel: true,
|
|
29244
|
+
renderHuman: defaultHumanRenderer,
|
|
29245
|
+
renderModel: defaultModelRenderer,
|
|
29246
|
+
run: async (args, context) => {
|
|
29247
|
+
assertObject(args, "args");
|
|
29248
|
+
const path = asString(args.path, "path").trim();
|
|
29249
|
+
const pkg = typeof args.package === "string" && args.package.trim() ? sanitizePackage(args.package) : "";
|
|
29250
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
29251
|
+
const shell = pkg ? `run-as ${shellQuote7(pkg)} cat ${shellQuote7(path)}` : `cat ${shellQuote7(path)}`;
|
|
29252
|
+
const result = await runAdb(context, `${adbPrefix(serial)} shell ${shellQuote7(shell)}`, 30000, 4000000);
|
|
29253
|
+
if (adbUnavailable(result))
|
|
29254
|
+
throw new Error("adb is not available in the container");
|
|
29255
|
+
const ok = result.exitCode === 0 && !/no such file|permission denied|not debuggable|run-as:/i.test(result.stderr);
|
|
29256
|
+
return {
|
|
29257
|
+
ok,
|
|
29258
|
+
summary: ok ? `read ${path}` : `could not read ${path}`,
|
|
29259
|
+
output: ok ? result.stdout : `${result.stdout}${result.stderr}`.trim() || "(no output)",
|
|
29260
|
+
metadata: {
|
|
29261
|
+
serial,
|
|
29262
|
+
path,
|
|
29263
|
+
package: pkg || null
|
|
29264
|
+
}
|
|
29265
|
+
};
|
|
29266
|
+
}
|
|
29267
|
+
};
|
|
29268
|
+
});
|
|
29269
|
+
|
|
29270
|
+
// src/agent-tools/android/static.ts
|
|
29271
|
+
function quoteDir(value) {
|
|
29272
|
+
const clean = value.trim();
|
|
29273
|
+
if (!clean || clean.includes(".."))
|
|
29274
|
+
throw new Error("directory must be a workspace-relative path without ..");
|
|
29275
|
+
return clean;
|
|
29276
|
+
}
|
|
29277
|
+
async function readManifest(context, dir) {
|
|
29278
|
+
const result = await backend(context).exec(`cat ${shellQuote7(`${dir}/AndroidManifest.xml`)}`, 20000, context.signal, 4000000);
|
|
29279
|
+
if (result.exitCode !== 0)
|
|
29280
|
+
throw new Error(`could not read AndroidManifest.xml in ${dir}; decompile with android_decompile first`);
|
|
29281
|
+
return result.stdout;
|
|
29282
|
+
}
|
|
29283
|
+
var DANGEROUS_PERMISSIONS, SECRET_PATTERN, androidDecompileTool, androidManifestTool, androidPermissionsTool, androidExportedComponentsTool, androidScanSecretsTool, androidGrepApkTool;
|
|
29284
|
+
var init_static = __esm(() => {
|
|
29285
|
+
init_backend();
|
|
29286
|
+
init_renderers();
|
|
29287
|
+
init_shared4();
|
|
29288
|
+
DANGEROUS_PERMISSIONS = new Set(["android.permission.READ_SMS", "android.permission.SEND_SMS", "android.permission.RECEIVE_SMS", "android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS", "android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_BACKGROUND_LOCATION", "android.permission.RECORD_AUDIO", "android.permission.CAMERA", "android.permission.READ_EXTERNAL_STORAGE", "android.permission.WRITE_EXTERNAL_STORAGE", "android.permission.MANAGE_EXTERNAL_STORAGE", "android.permission.READ_PHONE_STATE", "android.permission.READ_CALL_LOG", "android.permission.WRITE_CALL_LOG", "android.permission.REQUEST_INSTALL_PACKAGES", "android.permission.SYSTEM_ALERT_WINDOW", "android.permission.QUERY_ALL_PACKAGES", "android.permission.WRITE_SETTINGS"]);
|
|
29289
|
+
SECRET_PATTERN = ["AKIA[0-9A-Z]{16}", "AIza[0-9A-Za-z_-]{35}", "-----BEGIN [A-Z ]*PRIVATE KEY-----", "eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}", `(api[_-]?key|secret|passwd|password|token|bearer)["'\\s:=]{1,4}[A-Za-z0-9_.-]{8,}`, "https://[a-z0-9-]+\\.firebaseio\\.com"].join("|");
|
|
29290
|
+
androidDecompileTool = {
|
|
29291
|
+
name: "android_decompile",
|
|
29292
|
+
description: "Decompile an apk with apktool into smali, decoded resources, and a readable AndroidManifest.xml. Returns the output directory to pass to the other android static tools.",
|
|
29293
|
+
inputSchema: {
|
|
29294
|
+
type: "object",
|
|
29295
|
+
required: ["apkPath"],
|
|
29296
|
+
properties: {
|
|
29297
|
+
apkPath: {
|
|
29298
|
+
type: "string",
|
|
29299
|
+
description: "workspace-relative path to the apk to decompile"
|
|
29300
|
+
}
|
|
29301
|
+
},
|
|
29302
|
+
additionalProperties: false
|
|
29303
|
+
},
|
|
29304
|
+
mutates: true,
|
|
29305
|
+
timeoutMs: 240000,
|
|
29306
|
+
parallel: false,
|
|
29307
|
+
renderHuman: defaultHumanRenderer,
|
|
29308
|
+
renderModel: defaultModelRenderer,
|
|
29309
|
+
run: async (args, context) => {
|
|
29310
|
+
assertObject(args, "args");
|
|
29311
|
+
const apkPath = asString(args.apkPath, "apkPath").trim();
|
|
29312
|
+
const base = apkPath.split("/").pop()?.replace(/\.apk$/i, "") || "app";
|
|
29313
|
+
const outDir = `android/decompiled/${base}`;
|
|
29314
|
+
const result = await backend(context).exec(`apktool d -f -o ${shellQuote7(outDir)} ${shellQuote7(apkPath)}`, 240000, context.signal, 2000000);
|
|
29315
|
+
if (result.exitCode === 127)
|
|
29316
|
+
throw new Error("apktool is not available in the container");
|
|
29317
|
+
const ok = result.exitCode === 0;
|
|
29318
|
+
return {
|
|
29319
|
+
ok,
|
|
29320
|
+
summary: ok ? `decompiled to ${outDir}` : `apktool failed on ${apkPath}`,
|
|
29321
|
+
output: ok ? `output directory: ${outDir}
|
|
29322
|
+
${result.stdout}`.trim() : compactError2(result.stderr || result.stdout),
|
|
29323
|
+
metadata: {
|
|
29324
|
+
apkPath,
|
|
29325
|
+
directory: outDir
|
|
29326
|
+
}
|
|
29327
|
+
};
|
|
29328
|
+
}
|
|
29329
|
+
};
|
|
29330
|
+
androidManifestTool = {
|
|
29331
|
+
name: "android_manifest",
|
|
29332
|
+
description: "Read and summarize AndroidManifest.xml from a decompiled apk directory: package, sdk versions, debuggable/allowBackup flags, and permission and component counts.",
|
|
29333
|
+
inputSchema: {
|
|
29334
|
+
type: "object",
|
|
29335
|
+
required: ["directory"],
|
|
29336
|
+
properties: {
|
|
29337
|
+
directory: {
|
|
29338
|
+
type: "string",
|
|
29339
|
+
description: "decompiled apk directory from android_decompile"
|
|
29340
|
+
}
|
|
29341
|
+
},
|
|
29342
|
+
additionalProperties: false
|
|
29343
|
+
},
|
|
29344
|
+
mutates: false,
|
|
29345
|
+
timeoutMs: 30000,
|
|
29346
|
+
parallel: true,
|
|
29347
|
+
renderHuman: defaultHumanRenderer,
|
|
29348
|
+
renderModel: defaultModelRenderer,
|
|
29349
|
+
run: async (args, context) => {
|
|
29350
|
+
assertObject(args, "args");
|
|
29351
|
+
const dir = quoteDir(asString(args.directory, "directory"));
|
|
29352
|
+
const xml = await readManifest(context, dir);
|
|
29353
|
+
const pkg = /package="([^"]+)"/.exec(xml)?.[1] ?? "unknown";
|
|
29354
|
+
const debuggable = /android:debuggable="true"/.test(xml);
|
|
29355
|
+
const allowBackup = !/android:allowBackup="false"/.test(xml);
|
|
29356
|
+
const permissions = [...xml.matchAll(/<uses-permission[^>]*android:name="([^"]+)"/g)].map((match) => match[1]);
|
|
29357
|
+
const counts = {
|
|
29358
|
+
activities: (xml.match(/<activity[\s>]/g) ?? []).length,
|
|
29359
|
+
services: (xml.match(/<service[\s>]/g) ?? []).length,
|
|
29360
|
+
receivers: (xml.match(/<receiver[\s>]/g) ?? []).length,
|
|
29361
|
+
providers: (xml.match(/<provider[\s>]/g) ?? []).length
|
|
29362
|
+
};
|
|
29363
|
+
const output = [`package: ${pkg}`, `debuggable: ${debuggable}`, `allowBackup: ${allowBackup}`, `permissions: ${permissions.length}`, `components: activity=${counts.activities} service=${counts.services} receiver=${counts.receivers} provider=${counts.providers}`].join(`
|
|
29364
|
+
`);
|
|
29365
|
+
return {
|
|
29366
|
+
ok: true,
|
|
29367
|
+
summary: `${pkg}${debuggable ? " [debuggable]" : ""}${allowBackup ? " [allowBackup]" : ""}`,
|
|
29368
|
+
output,
|
|
29369
|
+
metadata: {
|
|
29370
|
+
package: pkg,
|
|
29371
|
+
debuggable,
|
|
29372
|
+
allowBackup,
|
|
29373
|
+
permissions,
|
|
29374
|
+
counts
|
|
29375
|
+
}
|
|
29376
|
+
};
|
|
29377
|
+
}
|
|
29378
|
+
};
|
|
29379
|
+
androidPermissionsTool = {
|
|
29380
|
+
name: "android_permissions",
|
|
29381
|
+
description: "List declared permissions from a decompiled apk and flag dangerous ones (sms, contacts, location, storage, install-packages, overlay, etc).",
|
|
29382
|
+
inputSchema: {
|
|
29383
|
+
type: "object",
|
|
29384
|
+
required: ["directory"],
|
|
29385
|
+
properties: {
|
|
29386
|
+
directory: {
|
|
29387
|
+
type: "string",
|
|
29388
|
+
description: "decompiled apk directory from android_decompile"
|
|
29389
|
+
}
|
|
29390
|
+
},
|
|
29391
|
+
additionalProperties: false
|
|
29392
|
+
},
|
|
29393
|
+
mutates: false,
|
|
29394
|
+
timeoutMs: 30000,
|
|
29395
|
+
parallel: true,
|
|
29396
|
+
renderHuman: defaultHumanRenderer,
|
|
29397
|
+
renderModel: defaultModelRenderer,
|
|
29398
|
+
run: async (args, context) => {
|
|
29399
|
+
assertObject(args, "args");
|
|
29400
|
+
const dir = quoteDir(asString(args.directory, "directory"));
|
|
29401
|
+
const xml = await readManifest(context, dir);
|
|
29402
|
+
const permissions = [...xml.matchAll(/<uses-permission[^>]*android:name="([^"]+)"/g)].map((match) => match[1]).sort();
|
|
29403
|
+
const dangerous = permissions.filter((name) => DANGEROUS_PERMISSIONS.has(name));
|
|
29404
|
+
const output = permissions.length ? permissions.map((name) => `${dangerous.includes(name) ? "[!] " : " "}${name}`).join(`
|
|
29405
|
+
`) : "no permissions declared";
|
|
29406
|
+
return {
|
|
29407
|
+
ok: true,
|
|
29408
|
+
summary: `${permissions.length} permission(s), ${dangerous.length} dangerous`,
|
|
29409
|
+
output,
|
|
29410
|
+
metadata: {
|
|
29411
|
+
permissions,
|
|
29412
|
+
dangerous
|
|
29413
|
+
}
|
|
29414
|
+
};
|
|
29415
|
+
}
|
|
29416
|
+
};
|
|
29417
|
+
androidExportedComponentsTool = {
|
|
29418
|
+
name: "android_exported_components",
|
|
29419
|
+
description: "List exported components (activity, service, receiver, provider) from a decompiled apk. Exported components are reachable by other apps and are a primary attack surface.",
|
|
29420
|
+
inputSchema: {
|
|
29421
|
+
type: "object",
|
|
29422
|
+
required: ["directory"],
|
|
29423
|
+
properties: {
|
|
29424
|
+
directory: {
|
|
29425
|
+
type: "string",
|
|
29426
|
+
description: "decompiled apk directory from android_decompile"
|
|
29427
|
+
}
|
|
29428
|
+
},
|
|
29429
|
+
additionalProperties: false
|
|
29430
|
+
},
|
|
29431
|
+
mutates: false,
|
|
29432
|
+
timeoutMs: 30000,
|
|
29433
|
+
parallel: true,
|
|
29434
|
+
renderHuman: defaultHumanRenderer,
|
|
29435
|
+
renderModel: defaultModelRenderer,
|
|
29436
|
+
run: async (args, context) => {
|
|
29437
|
+
assertObject(args, "args");
|
|
29438
|
+
const dir = quoteDir(asString(args.directory, "directory"));
|
|
29439
|
+
const xml = await readManifest(context, dir);
|
|
29440
|
+
const exported = [];
|
|
29441
|
+
for (const kind of ["activity", "activity-alias", "service", "receiver", "provider"]) {
|
|
29442
|
+
const regex = new RegExp(`<${kind}\\b[^>]*?(?:/>|>[\\s\\S]*?</${kind}>)`, "g");
|
|
29443
|
+
for (const match of xml.matchAll(regex)) {
|
|
29444
|
+
const block = match[0];
|
|
29445
|
+
const name = /android:name="([^"]+)"/.exec(block)?.[1] ?? "(unknown)";
|
|
29446
|
+
const explicitExport = /android:exported="true"/.test(block);
|
|
29447
|
+
const implicitExport = !/android:exported="false"/.test(block) && /<intent-filter/.test(block);
|
|
29448
|
+
if (explicitExport || implicitExport)
|
|
29449
|
+
exported.push({
|
|
29450
|
+
kind,
|
|
29451
|
+
name,
|
|
29452
|
+
explicit: explicitExport
|
|
29453
|
+
});
|
|
29454
|
+
}
|
|
29455
|
+
}
|
|
29456
|
+
const output = exported.length ? exported.map((item) => `${item.kind} ${item.explicit ? "exported=true" : "intent-filter"} ${item.name}`).join(`
|
|
29457
|
+
`) : "no exported components found";
|
|
29458
|
+
return {
|
|
29459
|
+
ok: true,
|
|
29460
|
+
summary: `${exported.length} exported component(s)`,
|
|
29461
|
+
output,
|
|
29462
|
+
metadata: {
|
|
29463
|
+
exported
|
|
29464
|
+
}
|
|
29465
|
+
};
|
|
29466
|
+
}
|
|
29467
|
+
};
|
|
29468
|
+
androidScanSecretsTool = {
|
|
29469
|
+
name: "android_scan_secrets",
|
|
29470
|
+
description: "Scan a decompiled apk directory for hardcoded secrets: aws/google api keys, private keys, jwts, and password/token assignments. Matches are candidate leads, not confirmed findings.",
|
|
29471
|
+
inputSchema: {
|
|
29472
|
+
type: "object",
|
|
29473
|
+
required: ["directory"],
|
|
29474
|
+
properties: {
|
|
29475
|
+
directory: {
|
|
29476
|
+
type: "string",
|
|
29477
|
+
description: "decompiled apk directory from android_decompile"
|
|
29478
|
+
},
|
|
29479
|
+
limit: {
|
|
29480
|
+
type: "integer",
|
|
29481
|
+
minimum: 1,
|
|
29482
|
+
maximum: 1000,
|
|
29483
|
+
description: "maximum matching lines to return (default 200)"
|
|
29484
|
+
}
|
|
29485
|
+
},
|
|
29486
|
+
additionalProperties: false
|
|
29487
|
+
},
|
|
29488
|
+
mutates: false,
|
|
29489
|
+
timeoutMs: 90000,
|
|
29490
|
+
parallel: true,
|
|
29491
|
+
renderHuman: defaultHumanRenderer,
|
|
29492
|
+
renderModel: defaultModelRenderer,
|
|
29493
|
+
run: async (args, context) => {
|
|
29494
|
+
assertObject(args, "args");
|
|
29495
|
+
const dir = quoteDir(asString(args.directory, "directory"));
|
|
29496
|
+
const limit = typeof args.limit === "number" && Number.isInteger(args.limit) ? Math.max(1, Math.min(1000, args.limit)) : 200;
|
|
29497
|
+
const command = `grep -rEIn ${shellQuote7(SECRET_PATTERN)} ${shellQuote7(dir)} 2>/dev/null | head -n ${limit}`;
|
|
29498
|
+
const result = await backend(context).exec(command, 90000, context.signal, 2000000);
|
|
29499
|
+
const lines = result.stdout.split(`
|
|
29500
|
+
`).map((line) => line.trim()).filter(Boolean);
|
|
29501
|
+
return {
|
|
29502
|
+
ok: true,
|
|
29503
|
+
summary: lines.length ? `${lines.length} secret candidate line(s)` : "no secret candidates matched",
|
|
29504
|
+
output: lines.length ? lines.join(`
|
|
29505
|
+
`) : "no matches",
|
|
29506
|
+
metadata: {
|
|
29507
|
+
directory: dir,
|
|
29508
|
+
matches: lines.length,
|
|
29509
|
+
truncated: lines.length >= limit
|
|
29510
|
+
}
|
|
29511
|
+
};
|
|
29512
|
+
}
|
|
29513
|
+
};
|
|
29514
|
+
androidGrepApkTool = {
|
|
29515
|
+
name: "android_grep_apk",
|
|
29516
|
+
description: "Regex-search a decompiled apk directory (smali, resources, assets). Use for urls, class names, string constants, and crypto usage after android_decompile.",
|
|
29517
|
+
inputSchema: {
|
|
29518
|
+
type: "object",
|
|
29519
|
+
required: ["directory", "pattern"],
|
|
29520
|
+
properties: {
|
|
29521
|
+
directory: {
|
|
29522
|
+
type: "string",
|
|
29523
|
+
description: "decompiled apk directory from android_decompile"
|
|
29524
|
+
},
|
|
29525
|
+
pattern: {
|
|
29526
|
+
type: "string",
|
|
29527
|
+
description: "extended regular expression to search for"
|
|
29528
|
+
},
|
|
29529
|
+
limit: {
|
|
29530
|
+
type: "integer",
|
|
29531
|
+
minimum: 1,
|
|
29532
|
+
maximum: 1000,
|
|
29533
|
+
description: "maximum matching lines to return (default 200)"
|
|
29534
|
+
}
|
|
29535
|
+
},
|
|
29536
|
+
additionalProperties: false
|
|
29537
|
+
},
|
|
29538
|
+
mutates: false,
|
|
29539
|
+
timeoutMs: 90000,
|
|
29540
|
+
parallel: true,
|
|
29541
|
+
renderHuman: defaultHumanRenderer,
|
|
29542
|
+
renderModel: defaultModelRenderer,
|
|
29543
|
+
run: async (args, context) => {
|
|
29544
|
+
assertObject(args, "args");
|
|
29545
|
+
const dir = quoteDir(asString(args.directory, "directory"));
|
|
29546
|
+
const pattern = asString(args.pattern, "pattern");
|
|
29547
|
+
const limit = typeof args.limit === "number" && Number.isInteger(args.limit) ? Math.max(1, Math.min(1000, args.limit)) : 200;
|
|
29548
|
+
const command = `grep -rEIn ${shellQuote7(pattern)} ${shellQuote7(dir)} 2>/dev/null | head -n ${limit}`;
|
|
29549
|
+
const result = await backend(context).exec(command, 90000, context.signal, 2000000);
|
|
29550
|
+
const lines = result.stdout.split(`
|
|
29551
|
+
`).map((line) => line.trim()).filter(Boolean);
|
|
29552
|
+
return {
|
|
29553
|
+
ok: true,
|
|
29554
|
+
summary: lines.length ? `${lines.length} match(es) for /${pattern}/` : `no matches for /${pattern}/`,
|
|
29555
|
+
output: lines.length ? lines.join(`
|
|
29556
|
+
`) : "no matches",
|
|
29557
|
+
metadata: {
|
|
29558
|
+
directory: dir,
|
|
29559
|
+
pattern,
|
|
29560
|
+
matches: lines.length,
|
|
29561
|
+
truncated: lines.length >= limit
|
|
29562
|
+
}
|
|
29563
|
+
};
|
|
29564
|
+
}
|
|
29565
|
+
};
|
|
29566
|
+
});
|
|
29567
|
+
|
|
29568
|
+
// src/agent-tools/android/ui.ts
|
|
29569
|
+
function parseUiNodes(xml) {
|
|
29570
|
+
const nodes = [];
|
|
29571
|
+
for (const match of xml.matchAll(/<node\b([^>]*)>/g)) {
|
|
29572
|
+
const attrs = match[1] ?? "";
|
|
29573
|
+
const bounds = /bounds="\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]"/.exec(attrs);
|
|
29574
|
+
if (!bounds)
|
|
29575
|
+
continue;
|
|
29576
|
+
const x1 = Number(bounds[1]);
|
|
29577
|
+
const y1 = Number(bounds[2]);
|
|
29578
|
+
const x2 = Number(bounds[3]);
|
|
29579
|
+
const y2 = Number(bounds[4]);
|
|
29580
|
+
const attr = (name) => new RegExp(`\\b${name}="([^"]*)"`).exec(attrs)?.[1] ?? "";
|
|
29581
|
+
nodes.push({
|
|
29582
|
+
text: attr("text"),
|
|
29583
|
+
resourceId: attr("resource-id"),
|
|
29584
|
+
contentDesc: attr("content-desc"),
|
|
29585
|
+
className: attr("class"),
|
|
29586
|
+
clickable: attr("clickable") === "true",
|
|
29587
|
+
bounds: [x1, y1, x2, y2],
|
|
29588
|
+
center: [Math.round((x1 + x2) / 2), Math.round((y1 + y2) / 2)]
|
|
29589
|
+
});
|
|
29590
|
+
}
|
|
29591
|
+
return nodes;
|
|
29592
|
+
}
|
|
29593
|
+
function findUiNode(nodes, selector) {
|
|
29594
|
+
const wantId = selector.resourceId?.trim();
|
|
29595
|
+
const wantText = selector.text?.trim();
|
|
29596
|
+
const wantDesc = selector.contentDesc?.trim();
|
|
29597
|
+
return nodes.find((node) => {
|
|
29598
|
+
if (wantId && !(node.resourceId === wantId || node.resourceId.endsWith(`/${wantId}`)))
|
|
29599
|
+
return false;
|
|
29600
|
+
if (wantText && !(node.text === wantText || node.text.toLowerCase().includes(wantText.toLowerCase())))
|
|
29601
|
+
return false;
|
|
29602
|
+
if (wantDesc && !(node.contentDesc === wantDesc || node.contentDesc.toLowerCase().includes(wantDesc.toLowerCase())))
|
|
29603
|
+
return false;
|
|
29604
|
+
return Boolean(wantId || wantText || wantDesc);
|
|
29605
|
+
});
|
|
29606
|
+
}
|
|
29607
|
+
async function dumpHierarchy(context, serial) {
|
|
29608
|
+
const remote = "/sdcard/farai_uidump.xml";
|
|
29609
|
+
const dump = await runAdb(context, `${adbPrefix(serial)} shell uiautomator dump ${remote}`, 30000);
|
|
29610
|
+
if (adbUnavailable(dump))
|
|
29611
|
+
throw new Error("adb is not available in the container");
|
|
29612
|
+
const read = await runAdb(context, `${adbPrefix(serial)} shell cat ${remote}`, 20000, 8000000);
|
|
29613
|
+
const xml = read.stdout.trim();
|
|
29614
|
+
if (!xml.includes("<hierarchy") && !xml.includes("<node")) {
|
|
29615
|
+
throw new Error(`could not capture ui hierarchy: ${(dump.stderr || dump.stdout || "no output").trim().slice(0, 200)}`);
|
|
29616
|
+
}
|
|
29617
|
+
return xml;
|
|
29618
|
+
}
|
|
29619
|
+
function describeNode(node) {
|
|
29620
|
+
const label = node.text || node.contentDesc || node.resourceId || node.className;
|
|
29621
|
+
const parts = [node.resourceId ? `id=${node.resourceId}` : "", node.text ? `text=${JSON.stringify(node.text)}` : "", node.contentDesc ? `desc=${JSON.stringify(node.contentDesc)}` : "", node.clickable ? "clickable" : "", `@${node.center[0]},${node.center[1]}`].filter(Boolean);
|
|
29622
|
+
return `${label} \u2014 ${parts.join(" ")}`;
|
|
29623
|
+
}
|
|
29624
|
+
var SERIAL_PROP3, androidUiDumpTool, androidUiHierarchyTool, androidScreenshotTool, androidUiTapTool, androidUiTapElementTool, androidUiTypeTool, androidUiSwipeTool, KEYEVENTS, androidUiKeyTool, androidUiWindowSizeTool, androidUiWaitForTool;
|
|
29625
|
+
var init_ui = __esm(() => {
|
|
29626
|
+
init_renderers();
|
|
29627
|
+
init_shared4();
|
|
29628
|
+
SERIAL_PROP3 = {
|
|
29629
|
+
type: "string",
|
|
29630
|
+
description: "device serial from android_devices; omit when exactly one device is connected"
|
|
29631
|
+
};
|
|
29632
|
+
androidUiDumpTool = {
|
|
29633
|
+
name: "android_ui_dump",
|
|
29634
|
+
description: "Capture the current screen's ui hierarchy via uiautomator and return interactive elements (text, resource-id, content-desc, clickable, tap center). Use this before android_ui_tap_element to see what is on screen.",
|
|
29635
|
+
inputSchema: {
|
|
29636
|
+
type: "object",
|
|
29637
|
+
properties: {
|
|
29638
|
+
clickableOnly: {
|
|
29639
|
+
type: "boolean",
|
|
29640
|
+
description: "return only clickable elements when true (default true)"
|
|
29641
|
+
},
|
|
29642
|
+
serial: SERIAL_PROP3
|
|
29643
|
+
},
|
|
29644
|
+
additionalProperties: false
|
|
29645
|
+
},
|
|
29646
|
+
mutates: false,
|
|
29647
|
+
timeoutMs: 40000,
|
|
29648
|
+
parallel: false,
|
|
29649
|
+
renderHuman: defaultHumanRenderer,
|
|
29650
|
+
renderModel: defaultModelRenderer,
|
|
29651
|
+
run: async (args, context) => {
|
|
29652
|
+
assertObject(args, "args");
|
|
29653
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
29654
|
+
const xml = await dumpHierarchy(context, serial);
|
|
29655
|
+
const all = parseUiNodes(xml);
|
|
29656
|
+
const clickableOnly = args.clickableOnly !== false;
|
|
29657
|
+
const shown = (clickableOnly ? all.filter((node) => node.clickable) : all).filter((node) => node.text || node.contentDesc || node.resourceId);
|
|
29658
|
+
return {
|
|
29659
|
+
ok: true,
|
|
29660
|
+
summary: `${shown.length} element(s) of ${all.length} on screen`,
|
|
29661
|
+
output: shown.length ? shown.map(describeNode).join(`
|
|
29662
|
+
`) : "no labeled elements found",
|
|
29663
|
+
metadata: {
|
|
29664
|
+
serial,
|
|
29665
|
+
total: all.length,
|
|
29666
|
+
elements: shown.slice(0, 200)
|
|
29667
|
+
}
|
|
29668
|
+
};
|
|
29669
|
+
}
|
|
29670
|
+
};
|
|
29671
|
+
androidUiHierarchyTool = {
|
|
29672
|
+
name: "android_ui_hierarchy",
|
|
29673
|
+
description: "Return the full raw uiautomator xml hierarchy of the current screen. Use when android_ui_dump omits an element you need to inspect precisely.",
|
|
29674
|
+
inputSchema: {
|
|
29675
|
+
type: "object",
|
|
29676
|
+
properties: {
|
|
29677
|
+
serial: SERIAL_PROP3
|
|
29678
|
+
},
|
|
29679
|
+
additionalProperties: false
|
|
29680
|
+
},
|
|
29681
|
+
mutates: false,
|
|
29682
|
+
timeoutMs: 40000,
|
|
29683
|
+
parallel: false,
|
|
29684
|
+
renderHuman: defaultHumanRenderer,
|
|
29685
|
+
renderModel: defaultModelRenderer,
|
|
29686
|
+
run: async (args, context) => {
|
|
29687
|
+
assertObject(args, "args");
|
|
29688
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
29689
|
+
const xml = await dumpHierarchy(context, serial);
|
|
29690
|
+
return {
|
|
29691
|
+
ok: true,
|
|
29692
|
+
summary: `captured ui hierarchy (${xml.length} bytes)`,
|
|
29693
|
+
output: xml,
|
|
29694
|
+
metadata: {
|
|
29695
|
+
serial
|
|
29696
|
+
}
|
|
29697
|
+
};
|
|
29698
|
+
}
|
|
29699
|
+
};
|
|
29700
|
+
androidScreenshotTool = {
|
|
29701
|
+
name: "android_screenshot",
|
|
29702
|
+
description: "Take a screenshot of the current screen and save it as a png in the workspace. Use android's image_view tool afterwards to inspect the saved file.",
|
|
29703
|
+
inputSchema: {
|
|
29704
|
+
type: "object",
|
|
29705
|
+
properties: {
|
|
29706
|
+
serial: SERIAL_PROP3
|
|
29707
|
+
},
|
|
29708
|
+
additionalProperties: false
|
|
29709
|
+
},
|
|
29710
|
+
mutates: false,
|
|
29711
|
+
timeoutMs: 40000,
|
|
29712
|
+
parallel: false,
|
|
29713
|
+
renderHuman: defaultHumanRenderer,
|
|
29714
|
+
renderModel: defaultModelRenderer,
|
|
29715
|
+
run: async (args, context) => {
|
|
29716
|
+
assertObject(args, "args");
|
|
29717
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
29718
|
+
const remote = "/sdcard/farai_screen.png";
|
|
29719
|
+
const local = `android/screenshots/${Date.now()}.png`;
|
|
29720
|
+
await runAdb(context, `mkdir -p android/screenshots`, 1e4);
|
|
29721
|
+
const cap = await runAdb(context, `${adbPrefix(serial)} shell screencap -p ${remote}`, 30000);
|
|
29722
|
+
if (adbUnavailable(cap))
|
|
29723
|
+
throw new Error("adb is not available in the container");
|
|
29724
|
+
const pull = await runAdb(context, `${adbPrefix(serial)} pull ${remote} ${shellQuote7(local)}`, 40000);
|
|
29725
|
+
const ok = pull.exitCode === 0;
|
|
29726
|
+
return {
|
|
29727
|
+
ok,
|
|
29728
|
+
summary: ok ? `saved screenshot to ${local}` : "screenshot capture failed",
|
|
29729
|
+
output: ok ? local : `${pull.stderr || pull.stdout}`.trim() || "screencap failed",
|
|
29730
|
+
metadata: {
|
|
29731
|
+
serial,
|
|
29732
|
+
path: ok ? local : null
|
|
29733
|
+
}
|
|
29734
|
+
};
|
|
29735
|
+
}
|
|
29736
|
+
};
|
|
29737
|
+
androidUiTapTool = {
|
|
29738
|
+
name: "android_ui_tap",
|
|
29739
|
+
description: "Tap the screen at absolute pixel coordinates. Use android_ui_tap_element when you can identify the target by id or text instead.",
|
|
29740
|
+
inputSchema: {
|
|
29741
|
+
type: "object",
|
|
29742
|
+
required: ["x", "y"],
|
|
29743
|
+
properties: {
|
|
29744
|
+
x: {
|
|
29745
|
+
type: "integer",
|
|
29746
|
+
minimum: 0,
|
|
29747
|
+
description: "x pixel coordinate"
|
|
29748
|
+
},
|
|
29749
|
+
y: {
|
|
29750
|
+
type: "integer",
|
|
29751
|
+
minimum: 0,
|
|
29752
|
+
description: "y pixel coordinate"
|
|
29753
|
+
},
|
|
29754
|
+
serial: SERIAL_PROP3
|
|
29755
|
+
},
|
|
29756
|
+
additionalProperties: false
|
|
29757
|
+
},
|
|
29758
|
+
mutates: true,
|
|
29759
|
+
timeoutMs: 20000,
|
|
29760
|
+
parallel: false,
|
|
29761
|
+
renderHuman: defaultHumanRenderer,
|
|
29762
|
+
renderModel: defaultModelRenderer,
|
|
29763
|
+
run: async (args, context) => {
|
|
29764
|
+
assertObject(args, "args");
|
|
29765
|
+
const x = Number(args.x);
|
|
29766
|
+
const y = Number(args.y);
|
|
29767
|
+
if (!Number.isInteger(x) || !Number.isInteger(y))
|
|
29768
|
+
throw new Error("x and y must be integers");
|
|
29769
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
29770
|
+
const result = await runAdb(context, `${adbPrefix(serial)} shell input tap ${x} ${y}`, 20000);
|
|
29771
|
+
if (adbUnavailable(result))
|
|
29772
|
+
throw new Error("adb is not available in the container");
|
|
29773
|
+
return {
|
|
29774
|
+
ok: result.exitCode === 0,
|
|
29775
|
+
summary: `tapped ${x},${y}`,
|
|
29776
|
+
output: result.stdout.trim() || "tapped",
|
|
29777
|
+
metadata: {
|
|
29778
|
+
serial,
|
|
29779
|
+
x,
|
|
29780
|
+
y
|
|
29781
|
+
}
|
|
29782
|
+
};
|
|
29783
|
+
}
|
|
29784
|
+
};
|
|
29785
|
+
androidUiTapElementTool = {
|
|
29786
|
+
name: "android_ui_tap_element",
|
|
29787
|
+
description: "Tap a ui element identified by resource-id, visible text, or content-desc. Dumps the hierarchy, resolves the element center, and taps it. Provide at least one selector.",
|
|
29788
|
+
inputSchema: {
|
|
29789
|
+
type: "object",
|
|
29790
|
+
properties: {
|
|
29791
|
+
resourceId: {
|
|
29792
|
+
type: "string",
|
|
29793
|
+
description: "full or trailing resource-id, e.g. com.app:id/login or login"
|
|
29794
|
+
},
|
|
29795
|
+
text: {
|
|
29796
|
+
type: "string",
|
|
29797
|
+
description: "exact or substring visible text of the element"
|
|
29798
|
+
},
|
|
29799
|
+
contentDesc: {
|
|
29800
|
+
type: "string",
|
|
29801
|
+
description: "exact or substring content-desc of the element"
|
|
29802
|
+
},
|
|
29803
|
+
serial: SERIAL_PROP3
|
|
29804
|
+
},
|
|
29805
|
+
additionalProperties: false
|
|
29806
|
+
},
|
|
29807
|
+
mutates: true,
|
|
29808
|
+
timeoutMs: 40000,
|
|
29809
|
+
parallel: false,
|
|
29810
|
+
renderHuman: defaultHumanRenderer,
|
|
29811
|
+
renderModel: defaultModelRenderer,
|
|
29812
|
+
run: async (args, context) => {
|
|
29813
|
+
assertObject(args, "args");
|
|
29814
|
+
const selector = {
|
|
29815
|
+
...typeof args.resourceId === "string" ? {
|
|
29816
|
+
resourceId: args.resourceId
|
|
29817
|
+
} : {},
|
|
29818
|
+
...typeof args.text === "string" ? {
|
|
29819
|
+
text: args.text
|
|
29820
|
+
} : {},
|
|
29821
|
+
...typeof args.contentDesc === "string" ? {
|
|
29822
|
+
contentDesc: args.contentDesc
|
|
29823
|
+
} : {}
|
|
29824
|
+
};
|
|
29825
|
+
if (!selector.resourceId && !selector.text && !selector.contentDesc)
|
|
29826
|
+
throw new Error("provide at least one of resourceId, text, or contentDesc");
|
|
29827
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
29828
|
+
const xml = await dumpHierarchy(context, serial);
|
|
29829
|
+
const node = findUiNode(parseUiNodes(xml), selector);
|
|
29830
|
+
if (!node)
|
|
29831
|
+
return {
|
|
29832
|
+
ok: false,
|
|
29833
|
+
summary: "no element matched the selector",
|
|
29834
|
+
output: "element not found on the current screen",
|
|
29835
|
+
metadata: {
|
|
29836
|
+
serial,
|
|
29837
|
+
selector
|
|
29838
|
+
}
|
|
29839
|
+
};
|
|
29840
|
+
const [x, y] = node.center;
|
|
29841
|
+
const result = await runAdb(context, `${adbPrefix(serial)} shell input tap ${x} ${y}`, 20000);
|
|
29842
|
+
return {
|
|
29843
|
+
ok: result.exitCode === 0,
|
|
29844
|
+
summary: `tapped ${node.resourceId || node.text || node.contentDesc} @${x},${y}`,
|
|
29845
|
+
output: describeNode(node),
|
|
29846
|
+
metadata: {
|
|
29847
|
+
serial,
|
|
29848
|
+
x,
|
|
29849
|
+
y,
|
|
29850
|
+
node
|
|
29851
|
+
}
|
|
29852
|
+
};
|
|
29853
|
+
}
|
|
29854
|
+
};
|
|
29855
|
+
androidUiTypeTool = {
|
|
29856
|
+
name: "android_ui_type",
|
|
29857
|
+
description: "Type text into the currently focused input field via adb input. Tap the field first with android_ui_tap_element to focus it.",
|
|
29858
|
+
inputSchema: {
|
|
29859
|
+
type: "object",
|
|
29860
|
+
required: ["text"],
|
|
29861
|
+
properties: {
|
|
29862
|
+
text: {
|
|
29863
|
+
type: "string",
|
|
29864
|
+
description: "text to type into the focused field"
|
|
29865
|
+
},
|
|
29866
|
+
serial: SERIAL_PROP3
|
|
29867
|
+
},
|
|
29868
|
+
additionalProperties: false
|
|
29869
|
+
},
|
|
29870
|
+
mutates: true,
|
|
29871
|
+
timeoutMs: 20000,
|
|
29872
|
+
parallel: false,
|
|
29873
|
+
renderHuman: defaultHumanRenderer,
|
|
29874
|
+
renderModel: defaultModelRenderer,
|
|
29875
|
+
run: async (args, context) => {
|
|
29876
|
+
assertObject(args, "args");
|
|
29877
|
+
const text2 = asString(args.text, "text");
|
|
29878
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
29879
|
+
const escaped = text2.replace(/(["\\$`])/g, "\\$1").replace(/ /g, "%s");
|
|
29880
|
+
const result = await runAdb(context, `${adbPrefix(serial)} shell input text ${shellQuote7(escaped)}`, 20000);
|
|
29881
|
+
if (adbUnavailable(result))
|
|
29882
|
+
throw new Error("adb is not available in the container");
|
|
29883
|
+
return {
|
|
29884
|
+
ok: result.exitCode === 0,
|
|
29885
|
+
summary: `typed ${text2.length} char(s)`,
|
|
29886
|
+
output: result.stdout.trim() || "typed",
|
|
29887
|
+
metadata: {
|
|
29888
|
+
serial
|
|
29889
|
+
}
|
|
29890
|
+
};
|
|
29891
|
+
}
|
|
29892
|
+
};
|
|
29893
|
+
androidUiSwipeTool = {
|
|
29894
|
+
name: "android_ui_swipe",
|
|
29895
|
+
description: "Swipe from one point to another over a duration. Use for scrolling, dismissing, and gesture navigation.",
|
|
29896
|
+
inputSchema: {
|
|
29897
|
+
type: "object",
|
|
29898
|
+
required: ["x1", "y1", "x2", "y2"],
|
|
29899
|
+
properties: {
|
|
29900
|
+
x1: {
|
|
29901
|
+
type: "integer",
|
|
29902
|
+
minimum: 0,
|
|
29903
|
+
description: "start x"
|
|
29904
|
+
},
|
|
29905
|
+
y1: {
|
|
29906
|
+
type: "integer",
|
|
29907
|
+
minimum: 0,
|
|
29908
|
+
description: "start y"
|
|
29909
|
+
},
|
|
29910
|
+
x2: {
|
|
29911
|
+
type: "integer",
|
|
29912
|
+
minimum: 0,
|
|
29913
|
+
description: "end x"
|
|
29914
|
+
},
|
|
29915
|
+
y2: {
|
|
29916
|
+
type: "integer",
|
|
29917
|
+
minimum: 0,
|
|
29918
|
+
description: "end y"
|
|
29919
|
+
},
|
|
29920
|
+
durationMs: {
|
|
29921
|
+
type: "integer",
|
|
29922
|
+
minimum: 50,
|
|
29923
|
+
maximum: 1e4,
|
|
29924
|
+
description: "swipe duration in ms (default 300)"
|
|
29925
|
+
},
|
|
29926
|
+
serial: SERIAL_PROP3
|
|
29927
|
+
},
|
|
29928
|
+
additionalProperties: false
|
|
29929
|
+
},
|
|
29930
|
+
mutates: true,
|
|
29931
|
+
timeoutMs: 20000,
|
|
29932
|
+
parallel: false,
|
|
29933
|
+
renderHuman: defaultHumanRenderer,
|
|
29934
|
+
renderModel: defaultModelRenderer,
|
|
29935
|
+
run: async (args, context) => {
|
|
29936
|
+
assertObject(args, "args");
|
|
29937
|
+
const coords = ["x1", "y1", "x2", "y2"].map((key) => Number(args[key]));
|
|
29938
|
+
if (coords.some((value) => !Number.isInteger(value)))
|
|
29939
|
+
throw new Error("x1, y1, x2, y2 must be integers");
|
|
29940
|
+
const duration = typeof args.durationMs === "number" && Number.isInteger(args.durationMs) ? Math.max(50, Math.min(1e4, args.durationMs)) : 300;
|
|
29941
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
29942
|
+
const [x1, y1, x2, y2] = coords;
|
|
29943
|
+
const result = await runAdb(context, `${adbPrefix(serial)} shell input swipe ${x1} ${y1} ${x2} ${y2} ${duration}`, 20000);
|
|
29944
|
+
if (adbUnavailable(result))
|
|
29945
|
+
throw new Error("adb is not available in the container");
|
|
29946
|
+
return {
|
|
29947
|
+
ok: result.exitCode === 0,
|
|
29948
|
+
summary: `swiped ${x1},${y1} -> ${x2},${y2}`,
|
|
29949
|
+
output: result.stdout.trim() || "swiped",
|
|
29950
|
+
metadata: {
|
|
29951
|
+
serial
|
|
29952
|
+
}
|
|
29953
|
+
};
|
|
29954
|
+
}
|
|
29955
|
+
};
|
|
29956
|
+
KEYEVENTS = {
|
|
29957
|
+
back: 4,
|
|
29958
|
+
home: 3,
|
|
29959
|
+
menu: 82,
|
|
29960
|
+
enter: 66,
|
|
29961
|
+
tab: 61,
|
|
29962
|
+
escape: 111,
|
|
29963
|
+
up: 19,
|
|
29964
|
+
down: 20,
|
|
29965
|
+
left: 21,
|
|
29966
|
+
right: 22,
|
|
29967
|
+
delete: 67,
|
|
29968
|
+
search: 84,
|
|
29969
|
+
power: 26,
|
|
29970
|
+
appswitch: 187
|
|
29971
|
+
};
|
|
29972
|
+
androidUiKeyTool = {
|
|
29973
|
+
name: "android_ui_key",
|
|
29974
|
+
description: "Send a keyevent to the device. Accepts a named key (back, home, enter, tab, up, down, delete, ...) or a raw android keycode number.",
|
|
29975
|
+
inputSchema: {
|
|
29976
|
+
type: "object",
|
|
29977
|
+
required: ["key"],
|
|
29978
|
+
properties: {
|
|
29979
|
+
key: {
|
|
29980
|
+
type: "string",
|
|
29981
|
+
description: "named key (back, home, menu, enter, tab, up, down, left, right, delete, search, power, appswitch) or a numeric keycode"
|
|
29982
|
+
},
|
|
29983
|
+
serial: SERIAL_PROP3
|
|
29984
|
+
},
|
|
29985
|
+
additionalProperties: false
|
|
29986
|
+
},
|
|
29987
|
+
mutates: true,
|
|
29988
|
+
timeoutMs: 20000,
|
|
29989
|
+
parallel: false,
|
|
29990
|
+
renderHuman: defaultHumanRenderer,
|
|
29991
|
+
renderModel: defaultModelRenderer,
|
|
29992
|
+
run: async (args, context) => {
|
|
29993
|
+
assertObject(args, "args");
|
|
29994
|
+
const key = asString(args.key, "key").trim().toLowerCase();
|
|
29995
|
+
const code = KEYEVENTS[key] ?? (/^\d+$/.test(key) ? Number(key) : undefined);
|
|
29996
|
+
if (code === undefined)
|
|
29997
|
+
throw new Error(`unknown key "${key}"; use a named key or a numeric keycode`);
|
|
29998
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
29999
|
+
const result = await runAdb(context, `${adbPrefix(serial)} shell input keyevent ${code}`, 20000);
|
|
30000
|
+
if (adbUnavailable(result))
|
|
30001
|
+
throw new Error("adb is not available in the container");
|
|
30002
|
+
return {
|
|
30003
|
+
ok: result.exitCode === 0,
|
|
30004
|
+
summary: `sent key ${key} (${code})`,
|
|
30005
|
+
output: result.stdout.trim() || "sent",
|
|
30006
|
+
metadata: {
|
|
30007
|
+
serial,
|
|
30008
|
+
key,
|
|
30009
|
+
code
|
|
30010
|
+
}
|
|
30011
|
+
};
|
|
30012
|
+
}
|
|
30013
|
+
};
|
|
30014
|
+
androidUiWindowSizeTool = {
|
|
30015
|
+
name: "android_ui_window_size",
|
|
30016
|
+
description: "Return the device screen resolution. Use to compute tap and swipe coordinates.",
|
|
30017
|
+
inputSchema: {
|
|
30018
|
+
type: "object",
|
|
30019
|
+
properties: {
|
|
30020
|
+
serial: SERIAL_PROP3
|
|
30021
|
+
},
|
|
30022
|
+
additionalProperties: false
|
|
30023
|
+
},
|
|
30024
|
+
mutates: false,
|
|
30025
|
+
timeoutMs: 20000,
|
|
30026
|
+
parallel: true,
|
|
30027
|
+
renderHuman: defaultHumanRenderer,
|
|
30028
|
+
renderModel: defaultModelRenderer,
|
|
30029
|
+
run: async (args, context) => {
|
|
30030
|
+
assertObject(args, "args");
|
|
30031
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
30032
|
+
const result = await runAdb(context, `${adbPrefix(serial)} shell wm size`, 20000);
|
|
30033
|
+
if (adbUnavailable(result))
|
|
30034
|
+
throw new Error("adb is not available in the container");
|
|
30035
|
+
const size = /(\d+)x(\d+)/.exec(result.stdout);
|
|
30036
|
+
return {
|
|
30037
|
+
ok: result.exitCode === 0,
|
|
30038
|
+
summary: size ? `${size[1]}x${size[2]}` : result.stdout.trim(),
|
|
30039
|
+
output: result.stdout.trim() || "(no output)",
|
|
30040
|
+
metadata: {
|
|
30041
|
+
serial,
|
|
30042
|
+
...size ? {
|
|
30043
|
+
width: Number(size[1]),
|
|
30044
|
+
height: Number(size[2])
|
|
30045
|
+
} : {}
|
|
30046
|
+
}
|
|
30047
|
+
};
|
|
30048
|
+
}
|
|
30049
|
+
};
|
|
30050
|
+
androidUiWaitForTool = {
|
|
30051
|
+
name: "android_ui_wait_for",
|
|
30052
|
+
description: "Poll the ui hierarchy until an element matching a selector appears or the timeout elapses. Use after an action that triggers a screen transition.",
|
|
30053
|
+
inputSchema: {
|
|
30054
|
+
type: "object",
|
|
30055
|
+
properties: {
|
|
30056
|
+
resourceId: {
|
|
30057
|
+
type: "string",
|
|
30058
|
+
description: "resource-id to wait for"
|
|
30059
|
+
},
|
|
30060
|
+
text: {
|
|
30061
|
+
type: "string",
|
|
30062
|
+
description: "visible text to wait for (substring match)"
|
|
30063
|
+
},
|
|
30064
|
+
contentDesc: {
|
|
30065
|
+
type: "string",
|
|
30066
|
+
description: "content-desc to wait for (substring match)"
|
|
30067
|
+
},
|
|
30068
|
+
timeoutSeconds: {
|
|
30069
|
+
type: "integer",
|
|
30070
|
+
minimum: 1,
|
|
30071
|
+
maximum: 120,
|
|
30072
|
+
description: "maximum seconds to wait (default 15)"
|
|
30073
|
+
},
|
|
30074
|
+
serial: SERIAL_PROP3
|
|
30075
|
+
},
|
|
30076
|
+
additionalProperties: false
|
|
30077
|
+
},
|
|
30078
|
+
mutates: false,
|
|
30079
|
+
timeoutMs: 130000,
|
|
30080
|
+
parallel: false,
|
|
30081
|
+
renderHuman: defaultHumanRenderer,
|
|
30082
|
+
renderModel: defaultModelRenderer,
|
|
30083
|
+
run: async (args, context) => {
|
|
30084
|
+
assertObject(args, "args");
|
|
30085
|
+
const selector = {
|
|
30086
|
+
...typeof args.resourceId === "string" ? {
|
|
30087
|
+
resourceId: args.resourceId
|
|
30088
|
+
} : {},
|
|
30089
|
+
...typeof args.text === "string" ? {
|
|
30090
|
+
text: args.text
|
|
30091
|
+
} : {},
|
|
30092
|
+
...typeof args.contentDesc === "string" ? {
|
|
30093
|
+
contentDesc: args.contentDesc
|
|
30094
|
+
} : {}
|
|
30095
|
+
};
|
|
30096
|
+
if (!selector.resourceId && !selector.text && !selector.contentDesc)
|
|
30097
|
+
throw new Error("provide at least one of resourceId, text, or contentDesc");
|
|
30098
|
+
const timeoutSeconds = typeof args.timeoutSeconds === "number" && Number.isInteger(args.timeoutSeconds) ? Math.max(1, Math.min(120, args.timeoutSeconds)) : 15;
|
|
30099
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
30100
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
30101
|
+
let attempts = 0;
|
|
30102
|
+
while (Date.now() < deadline) {
|
|
30103
|
+
if (context.signal?.aborted)
|
|
30104
|
+
throw new Error("wait cancelled");
|
|
30105
|
+
attempts += 1;
|
|
30106
|
+
const node = findUiNode(parseUiNodes(await dumpHierarchy(context, serial)), selector);
|
|
30107
|
+
if (node)
|
|
30108
|
+
return {
|
|
30109
|
+
ok: true,
|
|
30110
|
+
summary: `element appeared after ${attempts} check(s)`,
|
|
30111
|
+
output: describeNode(node),
|
|
30112
|
+
metadata: {
|
|
30113
|
+
serial,
|
|
30114
|
+
node,
|
|
30115
|
+
attempts
|
|
30116
|
+
}
|
|
30117
|
+
};
|
|
30118
|
+
await new Promise((resolve9) => setTimeout(resolve9, 1000));
|
|
30119
|
+
}
|
|
30120
|
+
return {
|
|
30121
|
+
ok: false,
|
|
30122
|
+
summary: `element did not appear within ${timeoutSeconds}s`,
|
|
30123
|
+
output: "timed out waiting for the element",
|
|
30124
|
+
metadata: {
|
|
30125
|
+
serial,
|
|
30126
|
+
selector,
|
|
30127
|
+
attempts
|
|
30128
|
+
}
|
|
30129
|
+
};
|
|
30130
|
+
}
|
|
30131
|
+
};
|
|
30132
|
+
});
|
|
30133
|
+
|
|
30134
|
+
// src/agent-tools/android/frida.ts
|
|
30135
|
+
async function writeAsset(context, relPath, content) {
|
|
30136
|
+
const dir = relPath.includes("/") ? relPath.slice(0, relPath.lastIndexOf("/")) : ".";
|
|
30137
|
+
const b64 = Buffer.from(content, "utf8").toString("base64");
|
|
30138
|
+
const command = `mkdir -p ${shellQuote7(dir)} && printf %s ${shellQuote7(b64)} | base64 -d > ${shellQuote7(relPath)}`;
|
|
30139
|
+
const result = await backend(context).exec(command, 20000, context.signal);
|
|
30140
|
+
if (result.exitCode !== 0)
|
|
30141
|
+
throw new Error(`failed to write ${relPath}: ${compactError2(result.stderr || result.stdout)}`);
|
|
30142
|
+
}
|
|
30143
|
+
function fridaRunCommand(serial, mode, target, scriptPath, durationSeconds) {
|
|
30144
|
+
return [`${adbEnvPrefix()}python3`, RUNNER_PATH, "--serial", shellQuote7(serial), "--mode", shellQuote7(mode), "--target", shellQuote7(target), "--script", shellQuote7(scriptPath), "--duration", String(durationSeconds)].join(" ");
|
|
30145
|
+
}
|
|
30146
|
+
function parseFridaMessages(stdout) {
|
|
30147
|
+
const messages = [];
|
|
30148
|
+
let summary;
|
|
30149
|
+
for (const line of stdout.split(`
|
|
30150
|
+
`)) {
|
|
30151
|
+
const trimmed = line.trim();
|
|
30152
|
+
if (trimmed.startsWith("FRIDA_DONE ")) {
|
|
30153
|
+
try {
|
|
30154
|
+
summary = JSON.parse(trimmed.slice("FRIDA_DONE ".length));
|
|
30155
|
+
} catch {}
|
|
30156
|
+
} else if (trimmed.startsWith("FRIDA ")) {
|
|
30157
|
+
try {
|
|
30158
|
+
messages.push(JSON.parse(trimmed.slice("FRIDA ".length)));
|
|
30159
|
+
} catch {}
|
|
30160
|
+
}
|
|
30161
|
+
}
|
|
30162
|
+
return {
|
|
30163
|
+
messages,
|
|
30164
|
+
summary
|
|
30165
|
+
};
|
|
30166
|
+
}
|
|
30167
|
+
async function pipInstallFrida(context, version) {
|
|
30168
|
+
const spec = version ? `frida==${version} frida-tools` : "frida-tools";
|
|
30169
|
+
const install = await backend(context).exec(`pip install --upgrade ${spec} 2>&1`, 360000, context.signal, 2000000);
|
|
30170
|
+
const check = await backend(context).exec("frida --version 2>/dev/null", 15000, context.signal);
|
|
30171
|
+
return {
|
|
30172
|
+
output: install.stdout.trim().split(`
|
|
30173
|
+
`).slice(-15).join(`
|
|
30174
|
+
`),
|
|
30175
|
+
resolved: check.stdout.trim()
|
|
30176
|
+
};
|
|
30177
|
+
}
|
|
30178
|
+
var SERIAL_PROP4, DEVICE_SERVER_PATH = "/data/local/tmp/frida-server", ASSET_DIR = ".farai/android", RUNNER_PATH, ABI_MAP, FRIDA_RUNNER = `import sys, json, time, argparse
|
|
30179
|
+
try:
|
|
30180
|
+
import frida
|
|
30181
|
+
except Exception as exc:
|
|
30182
|
+
print("FRIDA_DONE " + json.dumps({"error": "frida python module missing: %s" % exc}))
|
|
30183
|
+
sys.exit(0)
|
|
30184
|
+
|
|
30185
|
+
def emit(obj):
|
|
30186
|
+
print("FRIDA " + json.dumps(obj), flush=True)
|
|
30187
|
+
|
|
30188
|
+
def main():
|
|
30189
|
+
ap = argparse.ArgumentParser()
|
|
30190
|
+
ap.add_argument("--serial", default="")
|
|
30191
|
+
ap.add_argument("--mode", default="attach")
|
|
30192
|
+
ap.add_argument("--target", required=True)
|
|
30193
|
+
ap.add_argument("--script", required=True)
|
|
30194
|
+
ap.add_argument("--duration", type=float, default=10.0)
|
|
30195
|
+
a = ap.parse_args()
|
|
30196
|
+
collected = []
|
|
30197
|
+
def on_message(message, data):
|
|
30198
|
+
if message.get("type") == "send":
|
|
30199
|
+
payload = message.get("payload")
|
|
30200
|
+
collected.append(payload)
|
|
30201
|
+
emit({"send": payload})
|
|
30202
|
+
elif message.get("type") == "error":
|
|
30203
|
+
desc = message.get("description")
|
|
30204
|
+
collected.append({"__error__": desc})
|
|
30205
|
+
emit({"error": desc})
|
|
30206
|
+
try:
|
|
30207
|
+
device = frida.get_device(a.serial, timeout=5) if a.serial else frida.get_usb_device(timeout=5)
|
|
30208
|
+
except Exception as exc:
|
|
30209
|
+
print("FRIDA_DONE " + json.dumps({"error": "device not reachable by frida: %s" % exc}))
|
|
30210
|
+
return
|
|
30211
|
+
try:
|
|
30212
|
+
with open(a.script) as handle:
|
|
30213
|
+
source = handle.read()
|
|
30214
|
+
except Exception as exc:
|
|
30215
|
+
print("FRIDA_DONE " + json.dumps({"error": "cannot read script: %s" % exc}))
|
|
30216
|
+
return
|
|
30217
|
+
spawned = False
|
|
30218
|
+
pid = None
|
|
30219
|
+
try:
|
|
30220
|
+
if a.mode == "spawn":
|
|
30221
|
+
pid = device.spawn([a.target])
|
|
30222
|
+
spawned = True
|
|
30223
|
+
session = device.attach(pid)
|
|
30224
|
+
else:
|
|
30225
|
+
session = device.attach(a.target)
|
|
30226
|
+
except Exception as exc:
|
|
30227
|
+
print("FRIDA_DONE " + json.dumps({"error": "attach/spawn failed: %s" % exc}))
|
|
30228
|
+
return
|
|
30229
|
+
try:
|
|
30230
|
+
script = session.create_script(source)
|
|
30231
|
+
script.on("message", on_message)
|
|
30232
|
+
script.load()
|
|
30233
|
+
if spawned and pid is not None:
|
|
30234
|
+
device.resume(pid)
|
|
30235
|
+
time.sleep(a.duration)
|
|
30236
|
+
except Exception as exc:
|
|
30237
|
+
collected.append({"__error__": str(exc)})
|
|
30238
|
+
finally:
|
|
30239
|
+
try:
|
|
30240
|
+
session.detach()
|
|
30241
|
+
except Exception:
|
|
30242
|
+
pass
|
|
30243
|
+
print("FRIDA_DONE " + json.dumps({"messages": collected, "spawned": spawned, "target": a.target}))
|
|
30244
|
+
|
|
30245
|
+
main()
|
|
30246
|
+
`, SSL_BYPASS_JS = `Java.perform(function () {
|
|
30247
|
+
function log(m) { send({ tag: "ssl-bypass", msg: m }); }
|
|
30248
|
+
try {
|
|
30249
|
+
var TMImpl = Java.use("com.android.org.conscrypt.TrustManagerImpl");
|
|
30250
|
+
TMImpl.checkTrustedRecursive.implementation = function () { log("conscrypt checkTrustedRecursive bypassed"); return Java.use("java.util.ArrayList").$new(); };
|
|
30251
|
+
} catch (e) {}
|
|
30252
|
+
try {
|
|
30253
|
+
var TrustManager = Java.registerClass({
|
|
30254
|
+
name: "com.farai.TrustAll",
|
|
30255
|
+
implements: [Java.use("javax.net.ssl.X509TrustManager")],
|
|
30256
|
+
methods: {
|
|
30257
|
+
checkClientTrusted: function () {},
|
|
30258
|
+
checkServerTrusted: function () {},
|
|
30259
|
+
getAcceptedIssuers: function () { return []; }
|
|
30260
|
+
}
|
|
30261
|
+
});
|
|
30262
|
+
var SSLContext = Java.use("javax.net.ssl.SSLContext");
|
|
30263
|
+
SSLContext.init.overload("[Ljavax.net.ssl.KeyManager;", "[Ljavax.net.ssl.TrustManager;", "java.security.SecureRandom").implementation = function (km, tm, sr) {
|
|
30264
|
+
log("SSLContext.init overridden with trust-all manager");
|
|
30265
|
+
this.init(km, [TrustManager.$new()], sr);
|
|
30266
|
+
};
|
|
30267
|
+
} catch (e) {}
|
|
30268
|
+
try {
|
|
30269
|
+
var OkHostnameVerifier = Java.use("okhttp3.internal.tls.OkHostnameVerifier");
|
|
30270
|
+
OkHostnameVerifier.verify.overload("java.lang.String", "javax.net.ssl.SSLSession").implementation = function () { log("okhttp OkHostnameVerifier bypassed"); return true; };
|
|
30271
|
+
} catch (e) {}
|
|
30272
|
+
try {
|
|
30273
|
+
var CertPinner = Java.use("okhttp3.CertificatePinner");
|
|
30274
|
+
CertPinner.check.overload("java.lang.String", "java.util.List").implementation = function () { log("okhttp CertificatePinner.check bypassed"); return; };
|
|
30275
|
+
} catch (e) {}
|
|
30276
|
+
log("ssl pinning bypass hooks installed");
|
|
30277
|
+
});
|
|
30278
|
+
`, ROOT_BYPASS_JS = `Java.perform(function () {
|
|
30279
|
+
function log(m) { send({ tag: "root-bypass", msg: m }); }
|
|
30280
|
+
try {
|
|
30281
|
+
var RootBeer = Java.use("com.scottyab.rootbeer.RootBeer");
|
|
30282
|
+
["isRooted", "isRootedWithoutBusyBoxCheck", "detectRootManagementApps", "detectPotentiallyDangerousApps", "checkForBinary", "checkForSuBinary", "checkForDangerousProps", "detectTestKeys", "checkSuExists"].forEach(function (m) {
|
|
30283
|
+
try { RootBeer[m].implementation = function () { log("RootBeer." + m + " -> false"); return false; }; } catch (e) {}
|
|
30284
|
+
});
|
|
30285
|
+
} catch (e) {}
|
|
30286
|
+
try {
|
|
30287
|
+
var Runtime = Java.use("java.lang.Runtime");
|
|
30288
|
+
Runtime.exec.overload("java.lang.String").implementation = function (cmd) {
|
|
30289
|
+
if (cmd && (cmd.indexOf("su") !== -1 || cmd.indexOf("which") !== -1 || cmd.indexOf("busybox") !== -1)) { log("blocked Runtime.exec(" + cmd + ")"); throw Java.use("java.io.IOException").$new("blocked"); }
|
|
30290
|
+
return this.exec(cmd);
|
|
30291
|
+
};
|
|
30292
|
+
} catch (e) {}
|
|
30293
|
+
try {
|
|
30294
|
+
var File = Java.use("java.io.File");
|
|
30295
|
+
File.exists.implementation = function () {
|
|
30296
|
+
var path = this.getAbsolutePath();
|
|
30297
|
+
if (path && (path.indexOf("su") !== -1 || path.indexOf("magisk") !== -1 || path.indexOf("supersu") !== -1)) { log("hid file " + path); return false; }
|
|
30298
|
+
return this.exists();
|
|
30299
|
+
};
|
|
30300
|
+
} catch (e) {}
|
|
30301
|
+
log("root detection bypass hooks installed");
|
|
30302
|
+
});
|
|
30303
|
+
`, BYPASS_SCRIPTS, androidFridaInstallTool, androidFridaStatusTool, androidFridaSetupTool, androidFridaPsTool, androidFridaRunTool, androidFridaBypassTool;
|
|
30304
|
+
var init_frida = __esm(() => {
|
|
30305
|
+
init_backend();
|
|
30306
|
+
init_background_result();
|
|
30307
|
+
init_session_manager();
|
|
30308
|
+
init_renderers();
|
|
30309
|
+
init_shared4();
|
|
30310
|
+
SERIAL_PROP4 = {
|
|
30311
|
+
type: "string",
|
|
30312
|
+
description: "device serial from android_devices; omit when exactly one device is connected"
|
|
30313
|
+
};
|
|
30314
|
+
RUNNER_PATH = `${ASSET_DIR}/frida_runner.py`;
|
|
30315
|
+
ABI_MAP = {
|
|
30316
|
+
"arm64-v8a": "arm64",
|
|
30317
|
+
"armeabi-v7a": "arm",
|
|
30318
|
+
x86_64: "x86_64",
|
|
30319
|
+
x86: "x86"
|
|
30320
|
+
};
|
|
30321
|
+
BYPASS_SCRIPTS = {
|
|
30322
|
+
ssl: SSL_BYPASS_JS,
|
|
30323
|
+
root: ROOT_BYPASS_JS
|
|
30324
|
+
};
|
|
30325
|
+
androidFridaInstallTool = {
|
|
30326
|
+
name: "android_frida_install",
|
|
30327
|
+
description: "Install frida-tools in the container, optionally pinned to a version. Frida is intentionally not baked into the image because the python frida module must match the frida-server version, which varies per device and app. Install here first, then run android_frida_setup to provision a matching frida-server.",
|
|
30328
|
+
inputSchema: {
|
|
30329
|
+
type: "object",
|
|
30330
|
+
properties: {
|
|
30331
|
+
version: {
|
|
30332
|
+
type: "string",
|
|
30333
|
+
description: "exact frida version to pin, e.g. 16.5.9; omit to install the latest frida-tools"
|
|
30334
|
+
}
|
|
30335
|
+
},
|
|
30336
|
+
additionalProperties: false
|
|
30337
|
+
},
|
|
30338
|
+
mutates: true,
|
|
30339
|
+
timeoutMs: 360000,
|
|
30340
|
+
parallel: false,
|
|
30341
|
+
renderHuman: defaultHumanRenderer,
|
|
30342
|
+
renderModel: defaultModelRenderer,
|
|
30343
|
+
run: async (args, context) => {
|
|
30344
|
+
assertObject(args, "args");
|
|
30345
|
+
const version = typeof args.version === "string" && args.version.trim() ? args.version.trim() : "";
|
|
30346
|
+
if (version && !/^\d+(\.\d+){1,3}$/.test(version))
|
|
30347
|
+
throw new Error("version must look like 16.5.9");
|
|
30348
|
+
const spec = version ? `frida==${version} frida-tools` : "frida-tools";
|
|
30349
|
+
const command = `pip install --upgrade ${spec} 2>&1`;
|
|
30350
|
+
const result = await backend(context).exec(command, 360000, context.signal, 2000000);
|
|
30351
|
+
const installed = await backend(context).exec("frida --version 2>/dev/null", 15000, context.signal);
|
|
30352
|
+
const resolved = installed.stdout.trim();
|
|
30353
|
+
const ok = Boolean(resolved) && (!version || resolved === version);
|
|
30354
|
+
return {
|
|
30355
|
+
ok,
|
|
30356
|
+
summary: ok ? `frida-tools ${resolved} installed` : `frida install did not settle at the requested version${version ? ` (${version})` : ""}`,
|
|
30357
|
+
output: `${result.stdout}`.trim().split(`
|
|
30358
|
+
`).slice(-20).join(`
|
|
30359
|
+
`) || `exit ${result.exitCode}`,
|
|
30360
|
+
metadata: {
|
|
30361
|
+
requested: version || "latest",
|
|
30362
|
+
installedVersion: resolved || null
|
|
30363
|
+
}
|
|
30364
|
+
};
|
|
30365
|
+
}
|
|
30366
|
+
};
|
|
30367
|
+
androidFridaStatusTool = {
|
|
30368
|
+
name: "android_frida_status",
|
|
30369
|
+
description: "Check whether frida is ready: frida-tools in the container, and frida-server binary, process, and listening port on the device. Run this before other frida tools; if frida-tools is missing run android_frida_install, then android_frida_setup.",
|
|
30370
|
+
inputSchema: {
|
|
30371
|
+
type: "object",
|
|
30372
|
+
properties: {
|
|
30373
|
+
serial: SERIAL_PROP4
|
|
30374
|
+
},
|
|
30375
|
+
additionalProperties: false
|
|
30376
|
+
},
|
|
30377
|
+
mutates: false,
|
|
30378
|
+
timeoutMs: 40000,
|
|
30379
|
+
parallel: false,
|
|
30380
|
+
renderHuman: defaultHumanRenderer,
|
|
30381
|
+
renderModel: defaultModelRenderer,
|
|
30382
|
+
run: async (args, context) => {
|
|
30383
|
+
assertObject(args, "args");
|
|
30384
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
30385
|
+
const version = await backend(context).exec("frida --version 2>/dev/null || true", 15000, context.signal);
|
|
30386
|
+
const fridaTools = version.stdout.trim();
|
|
30387
|
+
const binary = await runAdb(context, `${adbPrefix(serial)} shell ls ${DEVICE_SERVER_PATH} 2>/dev/null`, 15000);
|
|
30388
|
+
const proc = await runAdb(context, `${adbPrefix(serial)} shell 'ps -A 2>/dev/null | grep frida-server || ps | grep frida-server'`, 15000);
|
|
30389
|
+
const checks = {
|
|
30390
|
+
fridaToolsVersion: fridaTools || "missing",
|
|
30391
|
+
serverBinary: binary.stdout.includes("frida-server") ? "present" : "missing",
|
|
30392
|
+
serverProcess: /frida-server/.test(proc.stdout) ? "running" : "stopped"
|
|
30393
|
+
};
|
|
30394
|
+
const ready = Boolean(fridaTools) && checks.serverBinary === "present" && checks.serverProcess === "running";
|
|
30395
|
+
return {
|
|
30396
|
+
ok: true,
|
|
30397
|
+
summary: ready ? "frida is ready" : `frida is not ready; run ${fridaTools ? "android_frida_setup" : "android_frida_install then android_frida_setup"}`,
|
|
30398
|
+
output: Object.entries(checks).map(([key, value]) => `${key}: ${value}`).join(`
|
|
30399
|
+
`),
|
|
30400
|
+
metadata: {
|
|
30401
|
+
serial,
|
|
30402
|
+
ready,
|
|
30403
|
+
checks
|
|
30404
|
+
}
|
|
30405
|
+
};
|
|
30406
|
+
}
|
|
30407
|
+
};
|
|
30408
|
+
androidFridaSetupTool = {
|
|
30409
|
+
name: "android_frida_setup",
|
|
30410
|
+
description: "One-command frida provisioning: install frida-tools in the container (pinned to version when given), detect the device cpu abi, download the matching frida-server build, push it to /data/local/tmp/frida-server, and start it (needs root via su or adb root). The python frida module and frida-server versions are kept identical. Run android_frida_status afterwards to confirm.",
|
|
30411
|
+
inputSchema: {
|
|
30412
|
+
type: "object",
|
|
30413
|
+
properties: {
|
|
30414
|
+
version: {
|
|
30415
|
+
type: "string",
|
|
30416
|
+
description: "exact frida version to pin for both the python module and frida-server, e.g. 16.5.9; omit to use the latest installed frida-tools"
|
|
30417
|
+
},
|
|
30418
|
+
serial: SERIAL_PROP4
|
|
30419
|
+
},
|
|
30420
|
+
additionalProperties: false
|
|
30421
|
+
},
|
|
30422
|
+
mutates: true,
|
|
30423
|
+
timeoutMs: 500000,
|
|
30424
|
+
parallel: false,
|
|
30425
|
+
renderHuman: defaultHumanRenderer,
|
|
30426
|
+
renderModel: defaultModelRenderer,
|
|
30427
|
+
run: async (args, context) => {
|
|
30428
|
+
assertObject(args, "args");
|
|
30429
|
+
const requested = typeof args.version === "string" && args.version.trim() ? args.version.trim() : "";
|
|
30430
|
+
if (requested && !/^\d+(\.\d+){1,3}$/.test(requested))
|
|
30431
|
+
throw new Error("version must look like 16.5.9");
|
|
30432
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
30433
|
+
const steps = [];
|
|
30434
|
+
let version = (await backend(context).exec("frida --version 2>/dev/null", 15000, context.signal)).stdout.trim();
|
|
30435
|
+
if (requested || !version) {
|
|
30436
|
+
const install = await pipInstallFrida(context, requested);
|
|
30437
|
+
version = install.resolved;
|
|
30438
|
+
steps.push(`pip install ${requested || "latest"}: ${version ? `frida-tools ${version}` : "failed"}`);
|
|
30439
|
+
} else {
|
|
30440
|
+
steps.push(`frida-tools already present: ${version}`);
|
|
30441
|
+
}
|
|
30442
|
+
if (!version)
|
|
30443
|
+
return {
|
|
30444
|
+
ok: false,
|
|
30445
|
+
summary: "frida-tools install failed",
|
|
30446
|
+
output: steps.join(`
|
|
30447
|
+
`),
|
|
30448
|
+
metadata: {
|
|
30449
|
+
serial
|
|
30450
|
+
}
|
|
30451
|
+
};
|
|
30452
|
+
const abiResult = await runAdb(context, `${adbPrefix(serial)} shell getprop ro.product.cpu.abi`, 15000);
|
|
30453
|
+
if (adbUnavailable(abiResult))
|
|
30454
|
+
throw new Error("adb is not available in the container");
|
|
30455
|
+
const abi = abiResult.stdout.trim().replace(/\r/g, "");
|
|
30456
|
+
const arch = ABI_MAP[abi];
|
|
30457
|
+
if (!arch)
|
|
30458
|
+
throw new Error(`unsupported device abi: ${abi || "unknown"} (supported: ${Object.keys(ABI_MAP).join(", ")})`);
|
|
30459
|
+
const url = `https://github.com/frida/frida/releases/download/${version}/frida-server-${version}-android-${arch}.xz`;
|
|
30460
|
+
const download = await backend(context).exec(`curl -fsSL -o /tmp/frida-server.xz ${shellQuote7(url)} && xz -d -f /tmp/frida-server.xz`, 180000, context.signal);
|
|
30461
|
+
steps.push(`download ${arch} ${version}: ${download.exitCode === 0 ? "ok" : `failed (${compactError2(download.stderr || download.stdout)})`}`);
|
|
30462
|
+
if (download.exitCode !== 0) {
|
|
30463
|
+
return {
|
|
30464
|
+
ok: false,
|
|
30465
|
+
summary: `could not download frida-server ${version} for ${arch}`,
|
|
30466
|
+
output: steps.join(`
|
|
30467
|
+
`),
|
|
30468
|
+
metadata: {
|
|
30469
|
+
serial,
|
|
30470
|
+
version,
|
|
30471
|
+
arch,
|
|
30472
|
+
url
|
|
30473
|
+
}
|
|
30474
|
+
};
|
|
30475
|
+
}
|
|
30476
|
+
const push = await runAdb(context, `${adbPrefix(serial)} push /tmp/frida-server ${DEVICE_SERVER_PATH} && ${adbPrefix(serial)} shell chmod 755 ${DEVICE_SERVER_PATH}`, 90000);
|
|
30477
|
+
steps.push(`push + chmod: ${push.exitCode === 0 ? "ok" : `failed (${compactError2(push.stderr || push.stdout)})`}`);
|
|
30478
|
+
const start = await runAdb(context, `${adbPrefix(serial)} shell 'su -c "${DEVICE_SERVER_PATH} -D" >/dev/null 2>&1 & echo started' || ${adbPrefix(serial)} shell '${DEVICE_SERVER_PATH} -D >/dev/null 2>&1 & echo started'`, 20000);
|
|
30479
|
+
steps.push(`start: ${/started/.test(start.stdout) ? "attempted (verify with android_frida_status)" : "could not start; device may need root"}`);
|
|
30480
|
+
return {
|
|
30481
|
+
ok: push.exitCode === 0,
|
|
30482
|
+
summary: `frida-server ${version} (${arch}) provisioned; verify with android_frida_status`,
|
|
30483
|
+
output: steps.join(`
|
|
30484
|
+
`),
|
|
30485
|
+
metadata: {
|
|
30486
|
+
serial,
|
|
30487
|
+
version,
|
|
30488
|
+
arch
|
|
30489
|
+
}
|
|
30490
|
+
};
|
|
30491
|
+
}
|
|
30492
|
+
};
|
|
30493
|
+
androidFridaPsTool = {
|
|
30494
|
+
name: "android_frida_ps",
|
|
30495
|
+
description: "List processes and applications visible to frida on the device. Use to find the exact process name or pid to attach to.",
|
|
30496
|
+
inputSchema: {
|
|
30497
|
+
type: "object",
|
|
30498
|
+
properties: {
|
|
30499
|
+
applicationsOnly: {
|
|
30500
|
+
type: "boolean",
|
|
30501
|
+
description: "list installed applications (frida-ps -Uai) instead of running processes when true"
|
|
30502
|
+
},
|
|
30503
|
+
serial: SERIAL_PROP4
|
|
30504
|
+
},
|
|
30505
|
+
additionalProperties: false
|
|
30506
|
+
},
|
|
30507
|
+
mutates: false,
|
|
30508
|
+
timeoutMs: 40000,
|
|
30509
|
+
parallel: false,
|
|
30510
|
+
renderHuman: defaultHumanRenderer,
|
|
30511
|
+
renderModel: defaultModelRenderer,
|
|
30512
|
+
run: async (args, context) => {
|
|
30513
|
+
assertObject(args, "args");
|
|
30514
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
30515
|
+
const listFlag = args.applicationsOnly === true ? "-ai" : "-a";
|
|
30516
|
+
const deviceFlag = `-D ${shellQuote7(serial)}`;
|
|
30517
|
+
const result = await backend(context).exec(`${adbEnvPrefix()}frida-ps ${listFlag} ${deviceFlag} 2>&1`, 40000, context.signal);
|
|
30518
|
+
if (/not found|no such/i.test(result.stdout) && result.exitCode !== 0)
|
|
30519
|
+
throw new Error("frida-tools is not installed in the container");
|
|
30520
|
+
return {
|
|
30521
|
+
ok: result.exitCode === 0,
|
|
30522
|
+
summary: result.exitCode === 0 ? "listed frida targets" : "frida-ps failed (is frida-server running?)",
|
|
30523
|
+
output: result.stdout.trim() || "(no output)",
|
|
30524
|
+
metadata: {
|
|
30525
|
+
serial,
|
|
30526
|
+
listFlag
|
|
30527
|
+
}
|
|
30528
|
+
};
|
|
30529
|
+
}
|
|
30530
|
+
};
|
|
30531
|
+
androidFridaRunTool = {
|
|
30532
|
+
name: "android_frida_run",
|
|
30533
|
+
description: "Run a frida javascript script against an app and collect its send() messages. Write the script first with fs_write, then attach to a running process or spawn a package. Use background=true to keep hooks live while you interact with the app, then read output with session_poll.",
|
|
30534
|
+
inputSchema: {
|
|
30535
|
+
type: "object",
|
|
30536
|
+
required: ["scriptPath", "target"],
|
|
30537
|
+
properties: {
|
|
30538
|
+
scriptPath: {
|
|
30539
|
+
type: "string",
|
|
30540
|
+
description: "workspace-relative path to the frida javascript to load"
|
|
30541
|
+
},
|
|
30542
|
+
target: {
|
|
30543
|
+
type: "string",
|
|
30544
|
+
description: "package name to spawn, or process name/pid to attach to"
|
|
30545
|
+
},
|
|
30546
|
+
mode: {
|
|
30547
|
+
type: "string",
|
|
30548
|
+
enum: ["spawn", "attach"],
|
|
30549
|
+
description: "spawn launches the package fresh; attach hooks an already-running process (default attach)"
|
|
30550
|
+
},
|
|
30551
|
+
durationSeconds: {
|
|
30552
|
+
type: "integer",
|
|
30553
|
+
minimum: 1,
|
|
30554
|
+
maximum: 600,
|
|
30555
|
+
description: "how long to keep the session open collecting messages (default 15)"
|
|
30556
|
+
},
|
|
30557
|
+
background: {
|
|
30558
|
+
type: "boolean",
|
|
30559
|
+
description: "run as a persistent background session and return a job id; poll it with session_poll"
|
|
30560
|
+
},
|
|
30561
|
+
serial: SERIAL_PROP4
|
|
30562
|
+
},
|
|
30563
|
+
additionalProperties: false
|
|
30564
|
+
},
|
|
30565
|
+
mutates: true,
|
|
30566
|
+
timeoutMs: 620000,
|
|
30567
|
+
parallel: false,
|
|
30568
|
+
renderHuman: defaultHumanRenderer,
|
|
30569
|
+
renderModel: defaultModelRenderer,
|
|
30570
|
+
run: async (args, context) => {
|
|
30571
|
+
assertObject(args, "args");
|
|
30572
|
+
const scriptPath = asString(args.scriptPath, "scriptPath").trim();
|
|
30573
|
+
const target = asString(args.target, "target").trim();
|
|
30574
|
+
const mode = args.mode === "spawn" ? "spawn" : "attach";
|
|
30575
|
+
const durationSeconds = typeof args.durationSeconds === "number" && Number.isInteger(args.durationSeconds) ? Math.max(1, Math.min(600, args.durationSeconds)) : 15;
|
|
30576
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
30577
|
+
await writeAsset(context, RUNNER_PATH, FRIDA_RUNNER);
|
|
30578
|
+
const command = fridaRunCommand(serial, mode, target, scriptPath, durationSeconds);
|
|
30579
|
+
if (args.background === true) {
|
|
30580
|
+
const started = await sessionManager.start(backend(context), "android_frida_run", command, clampYieldMs(args.background === true ? undefined : 1000), context.signal, {
|
|
30581
|
+
kind: "generic"
|
|
30582
|
+
});
|
|
30583
|
+
return backgroundToolResult("android_frida_run", started, "generic");
|
|
30584
|
+
}
|
|
30585
|
+
const result = await backend(context).exec(command, (durationSeconds + 30) * 1000, context.signal, 4000000);
|
|
30586
|
+
const {
|
|
30587
|
+
messages,
|
|
30588
|
+
summary
|
|
30589
|
+
} = parseFridaMessages(result.stdout);
|
|
30590
|
+
const error = summary && typeof summary.error === "string" ? summary.error : undefined;
|
|
30591
|
+
const output = error ? `frida error: ${error}` : `${messages.length} message(s):
|
|
30592
|
+
${messages.map((m) => JSON.stringify(m)).join(`
|
|
30593
|
+
`)}`;
|
|
30594
|
+
return {
|
|
30595
|
+
ok: !error,
|
|
30596
|
+
summary: error ? `frida run failed: ${error}` : `collected ${messages.length} message(s) from ${target}`,
|
|
30597
|
+
output: output || "(no messages)",
|
|
30598
|
+
metadata: {
|
|
30599
|
+
serial,
|
|
30600
|
+
target,
|
|
30601
|
+
mode,
|
|
30602
|
+
messages: messages.slice(0, 500),
|
|
30603
|
+
...summary ? {
|
|
30604
|
+
summary
|
|
30605
|
+
} : {}
|
|
30606
|
+
}
|
|
30607
|
+
};
|
|
30608
|
+
}
|
|
30609
|
+
};
|
|
30610
|
+
androidFridaBypassTool = {
|
|
30611
|
+
name: "android_frida_bypass",
|
|
30612
|
+
description: "Spawn an app with a bundled bypass script attached: ssl for certificate-pinning bypass (to see traffic through the proxy), root for root-detection bypass. Use background=true to keep the bypass active while you drive the app.",
|
|
30613
|
+
inputSchema: {
|
|
30614
|
+
type: "object",
|
|
30615
|
+
required: ["type", "package"],
|
|
30616
|
+
properties: {
|
|
30617
|
+
type: {
|
|
30618
|
+
type: "string",
|
|
30619
|
+
enum: ["ssl", "root"],
|
|
30620
|
+
description: "which bundled bypass to inject"
|
|
30621
|
+
},
|
|
30622
|
+
package: {
|
|
30623
|
+
type: "string",
|
|
30624
|
+
description: "package name to spawn with the bypass attached"
|
|
30625
|
+
},
|
|
30626
|
+
durationSeconds: {
|
|
30627
|
+
type: "integer",
|
|
30628
|
+
minimum: 1,
|
|
30629
|
+
maximum: 600,
|
|
30630
|
+
description: "how long to keep the bypass session open (default 30)"
|
|
30631
|
+
},
|
|
30632
|
+
background: {
|
|
30633
|
+
type: "boolean",
|
|
30634
|
+
description: "run as a persistent background session so hooks stay active; poll with session_poll"
|
|
30635
|
+
},
|
|
30636
|
+
serial: SERIAL_PROP4
|
|
30637
|
+
},
|
|
30638
|
+
additionalProperties: false
|
|
30639
|
+
},
|
|
30640
|
+
mutates: true,
|
|
30641
|
+
timeoutMs: 620000,
|
|
30642
|
+
parallel: false,
|
|
30643
|
+
renderHuman: defaultHumanRenderer,
|
|
30644
|
+
renderModel: defaultModelRenderer,
|
|
30645
|
+
run: async (args, context) => {
|
|
30646
|
+
assertObject(args, "args");
|
|
30647
|
+
const type = asString(args.type, "type");
|
|
30648
|
+
const script = BYPASS_SCRIPTS[type];
|
|
30649
|
+
if (!script)
|
|
30650
|
+
throw new Error(`unknown bypass type: ${type}; use ssl or root`);
|
|
30651
|
+
const pkg = asString(args.package, "package").trim();
|
|
30652
|
+
const durationSeconds = typeof args.durationSeconds === "number" && Number.isInteger(args.durationSeconds) ? Math.max(1, Math.min(600, args.durationSeconds)) : 30;
|
|
30653
|
+
const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
|
|
30654
|
+
const scriptPath = `${ASSET_DIR}/scripts/bypass_${type}.js`;
|
|
30655
|
+
await writeAsset(context, RUNNER_PATH, FRIDA_RUNNER);
|
|
30656
|
+
await writeAsset(context, scriptPath, script);
|
|
30657
|
+
const command = fridaRunCommand(serial, "spawn", pkg, scriptPath, durationSeconds);
|
|
30658
|
+
if (args.background === true) {
|
|
30659
|
+
const started = await sessionManager.start(backend(context), "android_frida_bypass", command, clampYieldMs(1000), context.signal, {
|
|
30660
|
+
kind: "generic"
|
|
30661
|
+
});
|
|
30662
|
+
return backgroundToolResult("android_frida_bypass", started, "generic");
|
|
30663
|
+
}
|
|
30664
|
+
const result = await backend(context).exec(command, (durationSeconds + 30) * 1000, context.signal, 4000000);
|
|
30665
|
+
const {
|
|
30666
|
+
messages,
|
|
30667
|
+
summary
|
|
30668
|
+
} = parseFridaMessages(result.stdout);
|
|
30669
|
+
const error = summary && typeof summary.error === "string" ? summary.error : undefined;
|
|
30670
|
+
return {
|
|
30671
|
+
ok: !error,
|
|
30672
|
+
summary: error ? `${type} bypass failed: ${error}` : `${type} bypass injected into ${pkg} (${messages.length} hook message(s))`,
|
|
30673
|
+
output: error ? `frida error: ${error}` : messages.map((m) => JSON.stringify(m)).join(`
|
|
30674
|
+
`) || "hooks installed (no messages emitted)",
|
|
30675
|
+
metadata: {
|
|
30676
|
+
serial,
|
|
30677
|
+
type,
|
|
30678
|
+
package: pkg,
|
|
30679
|
+
messages: messages.slice(0, 500)
|
|
30680
|
+
}
|
|
30681
|
+
};
|
|
30682
|
+
}
|
|
30683
|
+
};
|
|
30684
|
+
});
|
|
30685
|
+
|
|
30686
|
+
// src/agent-tools/android/index.ts
|
|
30687
|
+
var androidTools;
|
|
30688
|
+
var init_android = __esm(() => {
|
|
30689
|
+
init_device();
|
|
30690
|
+
init_app();
|
|
30691
|
+
init_static();
|
|
30692
|
+
init_ui();
|
|
30693
|
+
init_frida();
|
|
30694
|
+
androidTools = [androidConnectTool, androidDevicesTool, androidShellTool, androidPackagesTool, androidDeviceInfoTool, androidLogcatTool, androidApkPullTool, androidInstallTool, androidAppStartTool, androidAppStopTool, androidDeeplinkTool, androidPullFileTool, androidDecompileTool, androidManifestTool, androidPermissionsTool, androidExportedComponentsTool, androidScanSecretsTool, androidGrepApkTool, androidUiDumpTool, androidUiHierarchyTool, androidScreenshotTool, androidUiTapTool, androidUiTapElementTool, androidUiTypeTool, androidUiSwipeTool, androidUiKeyTool, androidUiWindowSizeTool, androidUiWaitForTool, androidFridaInstallTool, androidFridaStatusTool, androidFridaSetupTool, androidFridaPsTool, androidFridaRunTool, androidFridaBypassTool];
|
|
30695
|
+
});
|
|
30696
|
+
|
|
28684
30697
|
// src/agent-tools/registry.ts
|
|
28685
30698
|
function listToolsForSession(session) {
|
|
28686
30699
|
const tools = [...baseTools, ...listMcpTools(session)];
|
|
@@ -28726,10 +30739,11 @@ var init_registry4 = __esm(() => {
|
|
|
28726
30739
|
init_worktree();
|
|
28727
30740
|
init_proxy();
|
|
28728
30741
|
init_email();
|
|
30742
|
+
init_android();
|
|
28729
30743
|
init_mcp_manager();
|
|
28730
30744
|
init_process_output();
|
|
28731
30745
|
init_mcp_manager();
|
|
28732
|
-
baseTools = [...shellTools, ...reconTools, ...filesystemTools, ...gitTools, ...knowledgeTools, ...todoTools, ...reportTools, ...codegenTools, ...callbackTools, ...campaignTools, ...outputTools, ...lspTools, ...browserTools, ...kaliTools, ...agentTools, ...webTools, ...mediaTools, ...interactionTools, ...mcpResourceTools, ...worktreeTools, ...proxyTools, ...emailTools];
|
|
30746
|
+
baseTools = [...shellTools, ...reconTools, ...filesystemTools, ...gitTools, ...knowledgeTools, ...todoTools, ...reportTools, ...codegenTools, ...callbackTools, ...campaignTools, ...outputTools, ...lspTools, ...browserTools, ...kaliTools, ...agentTools, ...webTools, ...mediaTools, ...interactionTools, ...mcpResourceTools, ...worktreeTools, ...proxyTools, ...emailTools, ...androidTools];
|
|
28733
30747
|
});
|
|
28734
30748
|
|
|
28735
30749
|
// src/agent-core/mcp-server-management.ts
|
|
@@ -29928,12 +31942,25 @@ var init_tool_guidance = __esm(() => {
|
|
|
29928
31942
|
});
|
|
29929
31943
|
|
|
29930
31944
|
// src/agent-core/default-model.ts
|
|
29931
|
-
var DEFAULT_MODEL_PROVIDER_ID = "
|
|
31945
|
+
var DEFAULT_MODEL_PROVIDER_ID = "openrouter", DEFAULT_MODEL_BASE_URL = "https://openrouter.ai/api/v1", DEFAULT_MODEL_ID = "openrouter/free", DEFAULT_MODEL_PUBLIC_API_KEY = "", DEFAULT_CONTEXT_WINDOW = 200000, DEFAULT_MAX_OUTPUT_TOKENS = 4096, DEFAULT_MAX_STEPS, DEFAULT_MAX_TURN_SECONDS;
|
|
29932
31946
|
var init_default_model = __esm(() => {
|
|
29933
31947
|
DEFAULT_MAX_STEPS = Number.POSITIVE_INFINITY;
|
|
29934
31948
|
DEFAULT_MAX_TURN_SECONDS = Number.POSITIVE_INFINITY;
|
|
29935
31949
|
});
|
|
29936
31950
|
|
|
31951
|
+
// src/agent-core/default-source.ts
|
|
31952
|
+
function defaultSourceKey() {
|
|
31953
|
+
return Buffer.from(DEFAULT_SOURCE_KEY_B64, "base64").toString("utf8");
|
|
31954
|
+
}
|
|
31955
|
+
function isDefaultMode() {
|
|
31956
|
+
const config = loadGlobalConfig();
|
|
31957
|
+
return !config.baseUrl && !config.apiKeyEnv;
|
|
31958
|
+
}
|
|
31959
|
+
var DEFAULT_SOURCE_BASE_URL = "https://openrouter.ai/api/v1", DEFAULT_SOURCE_MODEL = "openrouter/free", DEFAULT_SOURCE_CONTEXT_WINDOW = 200000, DEFAULT_SOURCE_KEY_B64 = "c2stb3ItdjEtODM0NDYyZDI2YWMzODFkMDc5ZWI3N2Q1NTM2YWU0MTc2NWIyMGNhNWFiMzQ1YmEwNTJjNjU4ZTRhOWQ3ZGYzYQ==";
|
|
31960
|
+
var init_default_source = __esm(() => {
|
|
31961
|
+
init_global_config();
|
|
31962
|
+
});
|
|
31963
|
+
|
|
29937
31964
|
// src/agent-core/model-registry.ts
|
|
29938
31965
|
function resolveDefaultModel() {
|
|
29939
31966
|
return resolveModel({});
|
|
@@ -29941,8 +31968,9 @@ function resolveDefaultModel() {
|
|
|
29941
31968
|
function resolveModel(input = {}) {
|
|
29942
31969
|
const config = loadGlobalConfig();
|
|
29943
31970
|
const baseUrl = input.baseUrl ?? config.baseUrl ?? DEFAULT_MODEL_BASE_URL;
|
|
29944
|
-
const
|
|
29945
|
-
const
|
|
31971
|
+
const isDefault = baseUrl === DEFAULT_MODEL_BASE_URL && !config.baseUrl;
|
|
31972
|
+
const model = input.model ?? config.model ?? (DEFAULT_MODEL_ID || undefined);
|
|
31973
|
+
const apiKey = input.apiKey ?? (config.apiKeyEnv ? process.env[config.apiKeyEnv] : undefined) ?? (isDefault ? defaultSourceKey() : DEFAULT_MODEL_PUBLIC_API_KEY || undefined);
|
|
29946
31974
|
return {
|
|
29947
31975
|
baseUrl,
|
|
29948
31976
|
...model ? {
|
|
@@ -30045,6 +32073,7 @@ var HEURISTIC_MODEL_ID = "heuristic", MODEL_DISCOVERY_TIMEOUT_MS = 4000, MODEL_D
|
|
|
30045
32073
|
var init_model_registry = __esm(() => {
|
|
30046
32074
|
init_global_config();
|
|
30047
32075
|
init_default_model();
|
|
32076
|
+
init_default_source();
|
|
30048
32077
|
init_http_response();
|
|
30049
32078
|
MODEL_DISCOVERY_MAX_BYTES = 8 * 1024 * 1024;
|
|
30050
32079
|
});
|
|
@@ -30394,6 +32423,15 @@ async function resolveModelSelection(workspace, selection) {
|
|
|
30394
32423
|
}
|
|
30395
32424
|
async function resolveDefaultCatalogModel(workspace) {
|
|
30396
32425
|
const config = loadGlobalConfig();
|
|
32426
|
+
if (isDefaultMode()) {
|
|
32427
|
+
return {
|
|
32428
|
+
baseUrl: DEFAULT_SOURCE_BASE_URL,
|
|
32429
|
+
model: DEFAULT_SOURCE_MODEL,
|
|
32430
|
+
apiKey: defaultSourceKey(),
|
|
32431
|
+
contextWindow: DEFAULT_SOURCE_CONTEXT_WINDOW,
|
|
32432
|
+
name: "default"
|
|
32433
|
+
};
|
|
32434
|
+
}
|
|
30397
32435
|
const catalog = await buildModelCatalog(workspace);
|
|
30398
32436
|
const recent = readRecentModelSelections();
|
|
30399
32437
|
for (const selection of recent) {
|
|
@@ -30403,9 +32441,6 @@ async function resolveDefaultCatalogModel(workspace) {
|
|
|
30403
32441
|
}
|
|
30404
32442
|
if (config.model)
|
|
30405
32443
|
return resolveModelSelection(workspace, config.model);
|
|
30406
|
-
const openCodeDefault = catalog.models.find((model) => model.providerID === DEFAULT_PROVIDER_ID && model.modelID === OPENCODE_DEFAULT_MODEL_ID);
|
|
30407
|
-
if (openCodeDefault)
|
|
30408
|
-
return withSavedModelLimits(modelChoiceToResolved(openCodeDefault), openCodeDefault.id, workspace);
|
|
30409
32444
|
const first = sortModelChoices(catalog.models).find((model) => model.verified) ?? sortModelChoices(catalog.models)[0];
|
|
30410
32445
|
if (first)
|
|
30411
32446
|
return withSavedModelLimits(modelChoiceToResolved(first), first.id, workspace);
|
|
@@ -30449,7 +32484,7 @@ function normalizeModelsDevProviderHint(providerID, profile) {
|
|
|
30449
32484
|
if (providerID !== DEFAULT_PROVIDER_ID)
|
|
30450
32485
|
return providerID;
|
|
30451
32486
|
if (!profile && resolveModel().baseUrl === DEFAULT_MODEL_BASE_URL)
|
|
30452
|
-
return
|
|
32487
|
+
return DEFAULT_SOURCE_PROVIDER_ID;
|
|
30453
32488
|
return;
|
|
30454
32489
|
}
|
|
30455
32490
|
function readRecentModelSelections() {
|
|
@@ -30461,8 +32496,14 @@ function defaultModelSelection() {
|
|
|
30461
32496
|
function displayModelSelection(workspace, selection) {
|
|
30462
32497
|
if (selection) {
|
|
30463
32498
|
const profile = loadModelProfiles(workspace).find((candidate) => candidate.name === selection);
|
|
30464
|
-
|
|
30465
|
-
|
|
32499
|
+
if (profile)
|
|
32500
|
+
return profile.model ?? selection;
|
|
32501
|
+
if (isDefaultMode() && selection === DEFAULT_SOURCE_MODEL)
|
|
32502
|
+
return "default";
|
|
32503
|
+
return selection;
|
|
32504
|
+
}
|
|
32505
|
+
if (isDefaultMode())
|
|
32506
|
+
return "default";
|
|
30466
32507
|
return defaultModelSelection() ?? "auto";
|
|
30467
32508
|
}
|
|
30468
32509
|
async function providerDefinitions(workspace, profiles) {
|
|
@@ -30490,39 +32531,21 @@ async function providerDefinitions(workspace, profiles) {
|
|
|
30490
32531
|
for (const definition of profileDefinitions)
|
|
30491
32532
|
if (definition)
|
|
30492
32533
|
definitions2.push(definition);
|
|
30493
|
-
const
|
|
30494
|
-
|
|
30495
|
-
|
|
30496
|
-
|
|
30497
|
-
|
|
30498
|
-
|
|
30499
|
-
|
|
30500
|
-
|
|
30501
|
-
|
|
30502
|
-
|
|
30503
|
-
|
|
30504
|
-
|
|
30505
|
-
|
|
30506
|
-
|
|
30507
|
-
|
|
30508
|
-
catalogModels: freeModels.map((model) => ({
|
|
30509
|
-
id: model.id,
|
|
30510
|
-
...model.name ? {
|
|
30511
|
-
name: model.name
|
|
30512
|
-
} : {},
|
|
30513
|
-
free: isFreeModel(model),
|
|
30514
|
-
...model.limit?.context ? {
|
|
30515
|
-
contextWindow: model.limit.context
|
|
30516
|
-
} : {},
|
|
30517
|
-
...model.limit?.output ? {
|
|
30518
|
-
maxOutputTokens: model.limit.output
|
|
30519
|
-
} : {},
|
|
30520
|
-
...model.release_date ? {
|
|
30521
|
-
releaseDate: model.release_date
|
|
30522
|
-
} : {}
|
|
30523
|
-
}))
|
|
30524
|
-
});
|
|
30525
|
-
}
|
|
32534
|
+
const configuredDefaultSource = definitions2.some((provider) => provider.id === DEFAULT_PROVIDER_ID);
|
|
32535
|
+
if (isDefaultMode() && !configuredDefaultSource) {
|
|
32536
|
+
definitions2.push({
|
|
32537
|
+
id: DEFAULT_PROVIDER_ID,
|
|
32538
|
+
name: "default",
|
|
32539
|
+
baseUrl: DEFAULT_SOURCE_BASE_URL,
|
|
32540
|
+
apiKey: defaultSourceKey(),
|
|
32541
|
+
source: "models.dev",
|
|
32542
|
+
catalogModels: [{
|
|
32543
|
+
id: DEFAULT_SOURCE_MODEL,
|
|
32544
|
+
name: "default",
|
|
32545
|
+
free: true,
|
|
32546
|
+
contextWindow: DEFAULT_SOURCE_CONTEXT_WINDOW
|
|
32547
|
+
}]
|
|
32548
|
+
});
|
|
30526
32549
|
}
|
|
30527
32550
|
return definitions2;
|
|
30528
32551
|
}
|
|
@@ -30938,20 +32961,19 @@ function ensureConcrete(resolved) {
|
|
|
30938
32961
|
function modelsDevCachePath() {
|
|
30939
32962
|
return join18(globalDataDir(), "cache", "models-dev.json");
|
|
30940
32963
|
}
|
|
30941
|
-
var DEFAULT_PROVIDER_ID = "default",
|
|
32964
|
+
var DEFAULT_PROVIDER_ID = "default", DEFAULT_SOURCE_PROVIDER_ID, MODELS_DEV_URL = "https://models.dev/api.json", MODELS_DEV_CACHE_TTL_MS, MODELS_DEV_STALE_TTL_MS, MODELS_DEV_FETCH_TIMEOUT_MS = 4000, MODELS_DEV_MAX_BYTES, RECENT_MODEL_LIMIT = 12, modelsDevRefreshes;
|
|
30942
32965
|
var init_model_catalog = __esm(() => {
|
|
30943
32966
|
init_default_model();
|
|
30944
32967
|
init_global_config();
|
|
30945
32968
|
init_config();
|
|
30946
32969
|
init_model_registry();
|
|
30947
32970
|
init_model_profiles();
|
|
32971
|
+
init_default_source();
|
|
30948
32972
|
init_http_response();
|
|
30949
32973
|
init_file_read();
|
|
30950
32974
|
init_atomic_file();
|
|
30951
32975
|
init_private_path();
|
|
30952
|
-
|
|
30953
|
-
OPENCODE_DEFAULT_MODEL_ID = DEFAULT_MODEL_ID;
|
|
30954
|
-
OPENCODE_PUBLIC_API_KEY = DEFAULT_MODEL_PUBLIC_API_KEY;
|
|
32976
|
+
DEFAULT_SOURCE_PROVIDER_ID = DEFAULT_MODEL_PROVIDER_ID;
|
|
30955
32977
|
MODELS_DEV_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
30956
32978
|
MODELS_DEV_STALE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
30957
32979
|
MODELS_DEV_MAX_BYTES = 16 * 1024 * 1024;
|
|
@@ -42958,6 +44980,17 @@ var init_runtime = __esm(() => {
|
|
|
42958
44980
|
|
|
42959
44981
|
// src/branding.ts
|
|
42960
44982
|
import figlet from "figlet";
|
|
44983
|
+
function printBannerOnce() {
|
|
44984
|
+
if (bannerPrinted)
|
|
44985
|
+
return;
|
|
44986
|
+
bannerPrinted = true;
|
|
44987
|
+
console.log(FARAI_BANNER);
|
|
44988
|
+
}
|
|
44989
|
+
function clearBannerIfShown() {
|
|
44990
|
+
if (!bannerPrinted || !process.stdout.isTTY)
|
|
44991
|
+
return;
|
|
44992
|
+
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
44993
|
+
}
|
|
42961
44994
|
function renderFaraiBanner() {
|
|
42962
44995
|
try {
|
|
42963
44996
|
return figlet.textSync("farai", {
|
|
@@ -42967,7 +45000,7 @@ function renderFaraiBanner() {
|
|
|
42967
45000
|
return "farai";
|
|
42968
45001
|
}
|
|
42969
45002
|
}
|
|
42970
|
-
var FARAI_BANNER, FARAI_BANNER_LINES;
|
|
45003
|
+
var FARAI_BANNER, FARAI_BANNER_LINES, bannerPrinted = false;
|
|
42971
45004
|
var init_branding = __esm(() => {
|
|
42972
45005
|
FARAI_BANNER = renderFaraiBanner();
|
|
42973
45006
|
FARAI_BANNER_LINES = FARAI_BANNER.split(`
|
|
@@ -43812,6 +45845,7 @@ async function runStartupContentPreflight(workspace) {
|
|
|
43812
45845
|
}
|
|
43813
45846
|
async function promptForUpdate(version, knowledge, skills) {
|
|
43814
45847
|
const contents = [knowledge ? "knowledge" : undefined, skills ? "skills" : undefined].filter(Boolean).join(" + ");
|
|
45848
|
+
printBannerOnce();
|
|
43815
45849
|
console.log("");
|
|
43816
45850
|
console.log(`farai content ${version} is available${contents ? ` (${contents})` : ""}`);
|
|
43817
45851
|
const interfaceHandle = createInterface({
|
|
@@ -43866,6 +45900,7 @@ function errorMessage7(error) {
|
|
|
43866
45900
|
return error instanceof Error ? error.message : String(error);
|
|
43867
45901
|
}
|
|
43868
45902
|
var init_preflight = __esm(() => {
|
|
45903
|
+
init_branding();
|
|
43869
45904
|
init_config();
|
|
43870
45905
|
init_updater();
|
|
43871
45906
|
});
|
|
@@ -43903,8 +45938,7 @@ async function runStartupContainerPreflight(workspace) {
|
|
|
43903
45938
|
return "continue";
|
|
43904
45939
|
}
|
|
43905
45940
|
async function promptForImagePull(exists) {
|
|
43906
|
-
|
|
43907
|
-
console.log(FARAI_BANNER);
|
|
45941
|
+
printBannerOnce();
|
|
43908
45942
|
console.log("");
|
|
43909
45943
|
console.log(exists ? "a newer kali container image is available" : "kali container image is not installed");
|
|
43910
45944
|
const interfaceHandle = createInterface2({
|
|
@@ -56064,7 +58098,7 @@ function modelProviderOptions(choices, sessionModel) {
|
|
|
56064
58098
|
return {
|
|
56065
58099
|
id: `model-provider-${providerID}`,
|
|
56066
58100
|
title: providerID,
|
|
56067
|
-
description: [`${providerChoices.length} models`, freeCount ? `${freeCount} free` : undefined, readyCount ? `${readyCount} ready` : undefined, first?.baseUrl].filter(Boolean).join(" \xB7 "),
|
|
58101
|
+
description: [`${providerChoices.length} models`, freeCount ? `${freeCount} free` : undefined, readyCount ? `${readyCount} ready` : undefined, providerID === "default" ? undefined : first?.baseUrl].filter(Boolean).join(" \xB7 "),
|
|
56068
58102
|
footer: current ? "current" : "",
|
|
56069
58103
|
value: {
|
|
56070
58104
|
kind: "model_provider",
|
|
@@ -58179,7 +60213,7 @@ function createComposerController(input) {
|
|
|
58179
60213
|
composer.blur();
|
|
58180
60214
|
try {
|
|
58181
60215
|
renderer.suspend();
|
|
58182
|
-
const proc = Bun.spawn(["sh", "-lc", `${editor} ${
|
|
60216
|
+
const proc = Bun.spawn(["sh", "-lc", `${editor} ${shellQuote8(file)}`], {
|
|
58183
60217
|
stdin: "inherit",
|
|
58184
60218
|
stdout: "inherit",
|
|
58185
60219
|
stderr: "inherit"
|
|
@@ -58244,7 +60278,7 @@ function createComposerController(input) {
|
|
|
58244
60278
|
function normalizeComposerText(text2) {
|
|
58245
60279
|
return text2.replace(/^\s*\n+/, "").replace(/\n+\s*$/, "").trim();
|
|
58246
60280
|
}
|
|
58247
|
-
function
|
|
60281
|
+
function shellQuote8(value) {
|
|
58248
60282
|
return /^[A-Za-z0-9_./:@-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
58249
60283
|
}
|
|
58250
60284
|
function slashPromptText(rawText, completedTitle, slashName) {
|
|
@@ -65963,7 +67997,7 @@ function footerRightItems(backgroundActivities, subagents, browserContexts, queu
|
|
|
65963
67997
|
items.push({
|
|
65964
67998
|
id: "update",
|
|
65965
67999
|
kind: "update",
|
|
65966
|
-
text: `update ${updateNotice.latestVersion}`
|
|
68000
|
+
text: `update available ${updateNotice.latestVersion}`
|
|
65967
68001
|
});
|
|
65968
68002
|
}
|
|
65969
68003
|
if (contextUsage && contextUsage.tokens >= 0) {
|
|
@@ -70452,7 +72486,7 @@ var init_app_shell = __esm(() => {
|
|
|
70452
72486
|
function App() {
|
|
70453
72487
|
return createComponent2(AppShell, {});
|
|
70454
72488
|
}
|
|
70455
|
-
var
|
|
72489
|
+
var init_app2 = __esm(() => {
|
|
70456
72490
|
init_solid2();
|
|
70457
72491
|
init_app_shell();
|
|
70458
72492
|
});
|
|
@@ -70815,7 +72849,7 @@ function formatResumeHint(sessionId, _title, options = {}) {
|
|
|
70815
72849
|
const brand = options.styled ? `\x1B[2m${banner}\x1B[22m` : banner;
|
|
70816
72850
|
const saved = options.styled ? "\x1B[2msession saved\x1B[22m" : "session saved";
|
|
70817
72851
|
const usage = options.usage ? formatTokenUsage(options.usage) : undefined;
|
|
70818
|
-
return ["", brand, "", saved, ` farai resume ${
|
|
72852
|
+
return ["", brand, "", saved, ` farai resume ${shellQuote9(sessionId)}`, "", usage, ""].filter((line) => line !== undefined).join(`
|
|
70819
72853
|
`);
|
|
70820
72854
|
}
|
|
70821
72855
|
function formatTokenUsage(usage) {
|
|
@@ -70840,7 +72874,7 @@ function exitBanner(width) {
|
|
|
70840
72874
|
function terminalStylingEnabled() {
|
|
70841
72875
|
return Boolean(process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM !== "dumb");
|
|
70842
72876
|
}
|
|
70843
|
-
function
|
|
72877
|
+
function shellQuote9(value) {
|
|
70844
72878
|
return /^[A-Za-z0-9_./:@-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
70845
72879
|
}
|
|
70846
72880
|
async function launchOpenTui(workspace, sessionId) {
|
|
@@ -70875,7 +72909,7 @@ var init_agent_tui = __esm(() => {
|
|
|
70875
72909
|
init_solid2();
|
|
70876
72910
|
init_solid2();
|
|
70877
72911
|
init_runtime();
|
|
70878
|
-
|
|
72912
|
+
init_app2();
|
|
70879
72913
|
init_runtime2();
|
|
70880
72914
|
init_store4();
|
|
70881
72915
|
init_exit();
|
|
@@ -74189,6 +76223,7 @@ async function launchTui(workspace, sessionId) {
|
|
|
74189
76223
|
process.exitCode = 130;
|
|
74190
76224
|
return;
|
|
74191
76225
|
}
|
|
76226
|
+
clearBannerIfShown();
|
|
74192
76227
|
if (import.meta.path.endsWith(".ts")) {
|
|
74193
76228
|
const sourceTuiPreload = "@opentui/solid/preload";
|
|
74194
76229
|
await import(sourceTuiPreload);
|
|
@@ -74402,5 +76437,5 @@ Examples:
|
|
|
74402
76437
|
`);
|
|
74403
76438
|
}
|
|
74404
76439
|
|
|
74405
|
-
//# debugId=
|
|
76440
|
+
//# debugId=80A953E297D3B0EB64756E2164756E21
|
|
74406
76441
|
//# sourceMappingURL=index.js.map
|