@yufengtadian/freedom-cli 1.13.1 → 1.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +40 -0
- package/README.md +119 -15
- package/lib/agents.js +448 -0
- package/lib/build.js +189 -45
- package/lib/cli.js +258 -9
- package/lib/config.js +20 -1
- package/lib/desktop.js +133 -0
- package/lib/dev.js +149 -0
- package/lib/mcp.js +217 -0
- package/lib/release.js +90 -0
- package/lib/security.js +113 -54
- package/lib/shell.js +170 -63
- package/lib/toolchain.js +339 -0
- package/lib/tui.js +37 -1
- package/lib/utils.js +51 -0
- package/lib/verify.js +48 -12
- package/lib/webkit.js +45 -0
- package/package.json +3 -2
- package/shell/darwin-arm64/freedom-shell +0 -0
- package/shell/linux-x64/freedom-shell +0 -0
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/skill/freedom/SKILL.md +159 -0
- package/templates/desktop/app.html +559 -0
- package/templates/desktop/backend/desktop.mjs +327 -0
- package/templates/desktop/freedom.config.js +32 -0
- package/templates/desktop/icon.ico +0 -0
- package/templates/go/go.mod +3 -0
- package/templates/go/pkg/freedom/anti_debug_linux.go +69 -0
- package/templates/go/pkg/freedom/anti_debug_other.go +5 -3
- package/templates/go/pkg/freedom/anti_debug_windows.go +238 -11
- package/templates/go/pkg/freedom/assets/freedom.js +3 -0
- package/templates/go/pkg/freedom/assets_embed.go +1 -0
- package/templates/go/pkg/freedom/backend_proc.go +5 -5
- package/templates/go/pkg/freedom/events_windows.go +5 -5
- package/templates/go/pkg/freedom/freedom.go +64 -10
- package/templates/go/pkg/freedom/msgwindow_windows.go +8 -8
- package/templates/go/pkg/freedom/resources.go +119 -14
- package/templates/go/pkg/freedom/securetemp.go +61 -0
- package/templates/go/pkg/freedom/securetemp_other.go +20 -0
- package/templates/go/pkg/freedom/securetemp_windows.go +25 -0
- package/templates/go/pkg/freedom/security.go +293 -64
- package/templates/go/pkg/freedom/shutdown.go +55 -0
- package/templates/go/pkg/freedom/shutdown_windows.go +47 -0
- package/templates/go/pkg/freedom/store.go +11 -3
- package/templates/go/pkg/freedom/syscap_windows.go +33 -33
- package/templates/go/pkg/freedom/sysint_windows.go +2 -2
- package/templates/go/pkg/freedom/tray_windows.go +18 -18
- package/templates/go/pkg/freedom/updater.go +3 -3
- package/templates/go/pkg/freedom/window_other.go +7 -1
- package/templates/go/pkg/freedom/window_windows.go +185 -17
- package/templates/go/third_party/webview_go/CHANGELOG.md +15 -0
- package/templates/go/third_party/webview_go/FREEDOM-PATCH.md +32 -0
- package/templates/go/third_party/webview_go/LICENSE +22 -0
- package/templates/go/third_party/webview_go/README.md +50 -0
- package/templates/go/third_party/webview_go/glue.c +36 -0
- package/templates/go/third_party/webview_go/go.mod +3 -0
- package/templates/go/third_party/webview_go/go.sum +0 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/LICENSE +27 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/include/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/version.txt +1 -0
- package/templates/go/third_party/webview_go/libs/webview/LICENSE +22 -0
- package/templates/go/third_party/webview_go/libs/webview/include/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/webview/include/webview.h +3598 -0
- package/templates/go/third_party/webview_go/libs/webview/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/webview/version.txt +1 -0
- package/templates/go/third_party/webview_go/webkit2_40.go +21 -0
- package/templates/go/third_party/webview_go/webkit2_41.go +8 -0
- package/templates/go/third_party/webview_go/webview.cc +1 -0
- package/templates/go/third_party/webview_go/webview.go +332 -0
- package/templates/installer/app.nsi +43 -0
- package/templates/project/freedom.config.js +14 -0
- package/templates/project/freedom.d.ts +77 -0
- package/templates/project-minimal/freedom.config.js +14 -0
- package/templates/project-minimal/freedom.d.ts +77 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// Freedom Desktop 后端(零依赖 Node,NDJSON over stdio 进程后端)。
|
|
2
|
+
//
|
|
3
|
+
// 职责:把 freedom CLI 的能力包成界面可调用的方法。所有实际操作都经"子进程调用 CLI"完成,
|
|
4
|
+
// 而不是 require 内部模块——这样界面走的正是用户手册里那条命令路径,CLI 升级即界面升级,
|
|
5
|
+
// 且子进程输出不会污染本进程与壳之间的 NDJSON 协议通道(日志改经 event 帧转发)。
|
|
6
|
+
//
|
|
7
|
+
// 协议:入 {id, method, params:[...]};出 {id, result, error};推送 {event, data}。
|
|
8
|
+
// 日志:每次调用先回 {event:'call', data:{id, label}},再逐行 {event:'log', data:{id, line}}。
|
|
9
|
+
|
|
10
|
+
import readline from 'node:readline';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
function write(msg) {
|
|
20
|
+
process.stdout.write(JSON.stringify(msg) + '\n');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---- CLI 定位:优先 cli-entry.json(freedom desktop 打包时写入),再退到环境变量 ----
|
|
24
|
+
// cliMeta.root 一并带出:high 模式下本文件被解到系统临时目录,
|
|
25
|
+
// 不能再靠 `__dirname/../package.json` 反推 CLI 包版本,只能读容器里的 root。
|
|
26
|
+
const cliMeta = { entry: null, root: null };
|
|
27
|
+
function cliEntry() {
|
|
28
|
+
const candidates = [];
|
|
29
|
+
if (!cliMeta.entry) {
|
|
30
|
+
try {
|
|
31
|
+
const meta = JSON.parse(fs.readFileSync(path.join(__dirname, 'cli-entry.json'), 'utf8'));
|
|
32
|
+
if (meta && meta.entry) { cliMeta.entry = meta.entry; cliMeta.root = meta.root || null; }
|
|
33
|
+
} catch { /* 缺文件走后续回退 */ }
|
|
34
|
+
}
|
|
35
|
+
if (cliMeta.entry && fs.existsSync(cliMeta.entry)) return cliMeta.entry;
|
|
36
|
+
if (process.env.FREEDOM_CLI_ENTRY) candidates.push(process.env.FREEDOM_CLI_ENTRY);
|
|
37
|
+
for (const c of candidates) {
|
|
38
|
+
if (c && fs.existsSync(c)) return c;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 子进程跑 CLI:逐行转发输出,返回 {code, lines}。
|
|
44
|
+
function runCli(args, cwd) {
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
const entry = cliEntry();
|
|
47
|
+
const id = args.join(' ');
|
|
48
|
+
if (!entry) {
|
|
49
|
+
write({ event: 'log', data: { id, line: '未找到 freedom CLI 入口(cli-entry.json 缺失),请重新运行 freedom desktop。' } });
|
|
50
|
+
return resolve({ code: 1, lines: [] });
|
|
51
|
+
}
|
|
52
|
+
const child = spawn(process.execPath, [entry, ...args], {
|
|
53
|
+
cwd: cwd || os.homedir(),
|
|
54
|
+
env: { ...process.env, FREEDOM_AUTO_UPDATE: '0', NO_COLOR: '1', FORCE_COLOR: '0' },
|
|
55
|
+
shell: false,
|
|
56
|
+
});
|
|
57
|
+
const lines = [];
|
|
58
|
+
let buf = '';
|
|
59
|
+
const onData = (chunk) => {
|
|
60
|
+
buf += String(chunk);
|
|
61
|
+
let i;
|
|
62
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
63
|
+
const line = buf.slice(0, i).replace(/\r$/, '');
|
|
64
|
+
buf = buf.slice(i + 1);
|
|
65
|
+
if (!line) continue;
|
|
66
|
+
lines.push(line);
|
|
67
|
+
write({ event: 'log', data: { id, line } });
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
child.stdout.on('data', onData);
|
|
71
|
+
child.stderr.on('data', onData);
|
|
72
|
+
child.on('exit', (code) => {
|
|
73
|
+
if (buf) { lines.push(buf); write({ event: 'log', data: { id, line: buf } }); }
|
|
74
|
+
resolve({ code: code === null ? -1 : code, lines });
|
|
75
|
+
});
|
|
76
|
+
child.on('error', (err) => {
|
|
77
|
+
write({ event: 'log', data: { id, line: `启动 CLI 失败:${err.message}` } });
|
|
78
|
+
resolve({ code: 1, lines: [] });
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---- 最近项目状态(存 ~/.freedom/desktop-state.json) ----
|
|
84
|
+
function statePath() {
|
|
85
|
+
return path.join(os.homedir(), '.freedom', 'desktop-state.json');
|
|
86
|
+
}
|
|
87
|
+
function readState() {
|
|
88
|
+
try {
|
|
89
|
+
const s = JSON.parse(fs.readFileSync(statePath(), 'utf8'));
|
|
90
|
+
if (Array.isArray(s.recent)) return s;
|
|
91
|
+
} catch { /* 首次运行无状态文件 */ }
|
|
92
|
+
return { recent: [], current: null };
|
|
93
|
+
}
|
|
94
|
+
function writeState(s) {
|
|
95
|
+
fs.mkdirSync(path.dirname(statePath()), { recursive: true });
|
|
96
|
+
fs.writeFileSync(statePath(), JSON.stringify(s, null, 2), 'utf8');
|
|
97
|
+
}
|
|
98
|
+
function isProject(dir) {
|
|
99
|
+
return !!(dir && fs.existsSync(path.join(dir, 'freedom.config.js')));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let devChild = null;
|
|
103
|
+
function stopDev() {
|
|
104
|
+
if (!devChild) return false;
|
|
105
|
+
const c = devChild;
|
|
106
|
+
devChild = null;
|
|
107
|
+
try {
|
|
108
|
+
if (process.platform === 'win32') spawn('taskkill', ['/pid', String(c.pid), '/T', '/F'], { stdio: 'ignore' });
|
|
109
|
+
else c.kill('SIGTERM');
|
|
110
|
+
} catch { /* 已退出 */ }
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const api = {
|
|
115
|
+
async 'app.info'() {
|
|
116
|
+
const entry = cliEntry();
|
|
117
|
+
const readVersion = (base) => {
|
|
118
|
+
try { return JSON.parse(fs.readFileSync(path.join(base, 'package.json'), 'utf8')).version || null; } catch { return null; }
|
|
119
|
+
};
|
|
120
|
+
const cliVersion = (entry ? readVersion(path.join(path.dirname(entry), '..')) : null)
|
|
121
|
+
|| (cliMeta.root ? readVersion(cliMeta.root) : null);
|
|
122
|
+
return {
|
|
123
|
+
cliEntry: entry,
|
|
124
|
+
cliVersion,
|
|
125
|
+
platform: `${process.platform}-${process.arch}`,
|
|
126
|
+
nodeVersion: process.version,
|
|
127
|
+
home: os.homedir(),
|
|
128
|
+
desktopDir: process.env.FREEDOM_DESKTOP_DIR || null,
|
|
129
|
+
hasConfig: !!isProject(process.cwd()),
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
async 'project.list'() {
|
|
134
|
+
const s = readState();
|
|
135
|
+
return {
|
|
136
|
+
recent: s.recent.filter(isProject),
|
|
137
|
+
current: isProject(s.current) ? s.current : (s.recent.find(isProject) || null),
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
async 'project.open'([dir]) {
|
|
142
|
+
const abs = path.resolve(String(dir || ''));
|
|
143
|
+
if (!isProject(abs)) throw new Error(`不是 freedom 项目(缺 freedom.config.js):${abs}`);
|
|
144
|
+
const s = readState();
|
|
145
|
+
s.recent = [abs, ...s.recent.filter((p) => p !== abs)].slice(0, 12);
|
|
146
|
+
s.current = abs;
|
|
147
|
+
writeState(s);
|
|
148
|
+
return { current: abs, recent: s.recent };
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
async 'project.forget'([dir]) {
|
|
152
|
+
const abs = path.resolve(String(dir || ''));
|
|
153
|
+
const s = readState();
|
|
154
|
+
s.recent = s.recent.filter((p) => p !== abs);
|
|
155
|
+
if (s.current === abs) s.current = s.recent[0] || null;
|
|
156
|
+
writeState(s);
|
|
157
|
+
return { recent: s.recent, current: s.current };
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
async 'project.init'([dir, template]) {
|
|
161
|
+
const target = path.resolve(String(dir || ''));
|
|
162
|
+
const args = ['init', target];
|
|
163
|
+
if (template === 'minimal') args.push('--template', 'minimal');
|
|
164
|
+
const r = await runCli(args, path.dirname(target));
|
|
165
|
+
return { code: r.code };
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
async 'project.build'([opts]) {
|
|
169
|
+
const o = opts || {};
|
|
170
|
+
const dir = path.resolve(String(o.dir || ''));
|
|
171
|
+
if (!isProject(dir)) throw new Error(`不是 freedom 项目:${dir}`);
|
|
172
|
+
const args = ['build'];
|
|
173
|
+
if (o.platform) args.push('--platform', String(o.platform));
|
|
174
|
+
if (o.security) args.push('--security', String(o.security));
|
|
175
|
+
if (o.installer) args.push('--installer');
|
|
176
|
+
if (o.noCache) args.push('--no-cache');
|
|
177
|
+
const r = await runCli(args, dir);
|
|
178
|
+
return { code: r.code };
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
async 'project.verify'([dir]) {
|
|
182
|
+
const target = path.resolve(String(dir || ''));
|
|
183
|
+
const r = await runCli(['verify'], target);
|
|
184
|
+
return { code: r.code };
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
async 'project.dev'([opts]) {
|
|
188
|
+
const o = opts || {};
|
|
189
|
+
const dir = path.resolve(String(o.dir || ''));
|
|
190
|
+
if (!isProject(dir)) throw new Error(`不是 freedom 项目:${dir}`);
|
|
191
|
+
if (devChild) stopDev();
|
|
192
|
+
const entry = cliEntry();
|
|
193
|
+
if (!entry) throw new Error('未找到 freedom CLI 入口');
|
|
194
|
+
const args = ['dev', ...(o.port ? ['--port', String(o.port)] : [])];
|
|
195
|
+
devChild = spawn(process.execPath, [entry, ...args], {
|
|
196
|
+
cwd: dir,
|
|
197
|
+
env: { ...process.env, FREEDOM_AUTO_UPDATE: '0', NO_COLOR: '1' },
|
|
198
|
+
shell: false,
|
|
199
|
+
});
|
|
200
|
+
const id = 'dev';
|
|
201
|
+
let buf = '';
|
|
202
|
+
const onData = (chunk) => {
|
|
203
|
+
buf += String(chunk);
|
|
204
|
+
let i;
|
|
205
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
206
|
+
const line = buf.slice(0, i).replace(/\r$/, '');
|
|
207
|
+
buf = buf.slice(i + 1);
|
|
208
|
+
if (line) write({ event: 'log', data: { id, line } });
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
devChild.stdout.on('data', onData);
|
|
212
|
+
devChild.stderr.on('data', onData);
|
|
213
|
+
devChild.on('exit', (code) => {
|
|
214
|
+
devChild = null;
|
|
215
|
+
write({ event: 'log', data: { id, line: `[dev] 结束(code=${code})` } });
|
|
216
|
+
write({ event: 'dev.exit', data: { code } });
|
|
217
|
+
});
|
|
218
|
+
return { started: true };
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
async 'project.devStop'() {
|
|
222
|
+
return { stopped: stopDev() };
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
async 'project.config'([dir]) {
|
|
226
|
+
const target = path.resolve(String(dir || ''));
|
|
227
|
+
const r = await runCli(['config'], target);
|
|
228
|
+
return { text: r.lines.join('\n'), code: r.code };
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
async 'project.configSet'([dir, key, value]) {
|
|
232
|
+
const target = path.resolve(String(dir || ''));
|
|
233
|
+
const r = await runCli(['config', 'set', String(key), String(value)], target);
|
|
234
|
+
return { code: r.code, text: r.lines.join('\n') };
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
async 'shell.list'() {
|
|
238
|
+
const r = await runCli(['shell', 'list'], os.homedir());
|
|
239
|
+
return { text: r.lines.join('\n') };
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
async 'shell.download'([plat]) {
|
|
243
|
+
const r = await runCli(['shell', 'download', String(plat)], os.homedir());
|
|
244
|
+
return { code: r.code };
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
async 'release.keygen'([dir]) {
|
|
248
|
+
const r = await runCli(['keygen'], path.resolve(String(dir || '')));
|
|
249
|
+
return { code: r.code, text: r.lines.join('\n') };
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
async 'release.manifest'([opts]) {
|
|
253
|
+
const o = opts || {};
|
|
254
|
+
const args = ['manifest', '--artifact', String(o.artifact || ''), '--url', String(o.url || '')];
|
|
255
|
+
if (o.version) args.push('--version', String(o.version));
|
|
256
|
+
if (o.notes) args.push('--notes', String(o.notes));
|
|
257
|
+
const r = await runCli(args, path.resolve(String(o.dir || '')));
|
|
258
|
+
return { code: r.code, text: r.lines.join('\n') };
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
async 'agents.list'() {
|
|
262
|
+
const r = await runCli(['agents'], os.homedir());
|
|
263
|
+
return { text: r.lines.join('\n') };
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
async 'agents.install'([opts]) {
|
|
267
|
+
const o = opts || {};
|
|
268
|
+
const what = o.what === 'mcp' ? 'mcp' : 'skill';
|
|
269
|
+
const args = [what, 'install'];
|
|
270
|
+
if (Array.isArray(o.agents) && o.agents.length) args.push('--agent', o.agents.join(','));
|
|
271
|
+
else args.push('--agent', 'all');
|
|
272
|
+
if (o.dryRun) args.push('--dry-run');
|
|
273
|
+
const r = await runCli(args, os.homedir());
|
|
274
|
+
return { code: r.code };
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
async 'desktop.rebuild'() {
|
|
278
|
+
const r = await runCli(['desktop', '--rebuild'], os.homedir());
|
|
279
|
+
return { code: r.code };
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
async 'docs.open'() {
|
|
283
|
+
const r = await runCli(['tutorial'], os.homedir());
|
|
284
|
+
return { code: r.code };
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
289
|
+
|
|
290
|
+
// stdin EOF 不等于可以立刻退出:在途请求(如经子进程跑 CLI 的调用)还没回帧,
|
|
291
|
+
// 直接 exit 会让调用方永远等不到响应。等在途清零后再收尾。
|
|
292
|
+
let inflight = 0;
|
|
293
|
+
let stdinClosed = false;
|
|
294
|
+
function maybeExit() {
|
|
295
|
+
if (stdinClosed && inflight === 0) {
|
|
296
|
+
stopDev();
|
|
297
|
+
process.exit(0);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
rl.on('close', () => {
|
|
301
|
+
stdinClosed = true;
|
|
302
|
+
maybeExit();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
rl.on('line', async (line) => {
|
|
306
|
+
let req;
|
|
307
|
+
try {
|
|
308
|
+
req = JSON.parse(line);
|
|
309
|
+
} catch {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
inflight++;
|
|
313
|
+
try {
|
|
314
|
+
const fn = api[req.method];
|
|
315
|
+
if (!fn) {
|
|
316
|
+
write({ id: req.id, result: null, error: `unknown method ${req.method}` });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const result = await fn(req.params || []);
|
|
320
|
+
write({ id: req.id, result, error: '' });
|
|
321
|
+
} catch (e) {
|
|
322
|
+
write({ id: req.id, result: null, error: String((e && e.message) || e) });
|
|
323
|
+
} finally {
|
|
324
|
+
inflight--;
|
|
325
|
+
maybeExit();
|
|
326
|
+
}
|
|
327
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Freedom Desktop 应用配置(自举:本项目的界面由 freedom 自己打包成桌面应用)。
|
|
2
|
+
// 由 `freedom desktop` 自动同步到 ~/.freedom/desktop/ 并自动打包,一般无需手改。
|
|
3
|
+
export default {
|
|
4
|
+
name: 'freedom-desktop',
|
|
5
|
+
title: 'Freedom Desktop',
|
|
6
|
+
|
|
7
|
+
width: 1180,
|
|
8
|
+
height: 780,
|
|
9
|
+
minWidth: 920,
|
|
10
|
+
minHeight: 560,
|
|
11
|
+
center: true,
|
|
12
|
+
debug: false,
|
|
13
|
+
|
|
14
|
+
// 静态单文件前端直通:不跑 npm install / vite(自举工具不应依赖网络与构建链)。
|
|
15
|
+
staticHtml: 'app.html',
|
|
16
|
+
|
|
17
|
+
titlebar: 'native',
|
|
18
|
+
// 闭源自举:Desktop 自身也要经得起逆向——前端 app.html、config.json 与后端
|
|
19
|
+
// backend/*(含 cli-entry.json)整体进 FRDM2 容器,磁盘只留 app.bin + .integrity,
|
|
20
|
+
// 运行期由壳解密到私有临时目录(退出即删,崩溃残留由下次启动回收)。
|
|
21
|
+
security: 'high',
|
|
22
|
+
outDir: 'dist',
|
|
23
|
+
|
|
24
|
+
// 应用图标:构建时注入 exe(Windows 用 rcedit 写 PE 资源),运行时壳层经
|
|
25
|
+
// WM_SETICON 同步到标题栏与任务栏。由 tools/freedomres -ico-out 从
|
|
26
|
+
// assets/freedom-desktop.png 生成(多尺寸阶梯 16/24/32/48/64/128/256)。
|
|
27
|
+
icon: 'icon.ico',
|
|
28
|
+
|
|
29
|
+
// 后端 = Node 进程(零依赖),经 NDJSON/stdio 桥将 freedom CLI 能力暴露给界面。
|
|
30
|
+
// 相对路径以 resources/ 为工作目录解析;CLI 入口由 freedom desktop 写入 cli-entry.json。
|
|
31
|
+
backend: { command: 'node', args: ['backend/desktop.mjs'] },
|
|
32
|
+
};
|
|
Binary file
|
package/templates/go/go.mod
CHANGED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//go:build linux
|
|
2
|
+
|
|
3
|
+
package freedom
|
|
4
|
+
|
|
5
|
+
// 反调试(Linux 实现):high 安全模式下由 Run() 在资源解密前后各调用一次,
|
|
6
|
+
// 与 Windows 侧同一语义——**探测失败一律记未命中**,只在确证被附加调试器时退出,
|
|
7
|
+
// 误杀正常用户的代价高于多一路信号的收益。
|
|
8
|
+
//
|
|
9
|
+
// 两道信号:
|
|
10
|
+
// 1. ptrace(PTRACE_TRACEME):本进程已被别的进程 attach 时返回 EPERM —— 这是
|
|
11
|
+
// "GDB 已挂上来"的确证。TRACEME 是一次性的(成功后无法再 DETACH 复位),
|
|
12
|
+
// 故整个进程生命周期只探测一次并缓存结论;Run 里的"解密前 / 解密后"两次调用
|
|
13
|
+
// 因此共享同一结论——本就只能挡住「启动时已挂调试器」,运行中途 attach
|
|
14
|
+
// (gdb -p)要等下次启动才被抓到,这是诚实的能力边界。
|
|
15
|
+
// **已知边界**:本进程此前处于停止/跟踪-停止状态(被 SIGSTOP 过)时 TRACEME
|
|
16
|
+
// 也会 EPERM,理论上存在误判窗口;桌面壳正常生命周期内不走这条路,且
|
|
17
|
+
// 非 high 模式完全不检测,可用 FREEDOM_DISABLE_ANTIDEBUG=1 关闭。
|
|
18
|
+
// 2. PR_SET_DUMPABLE=0:不是检测而是加固——内核不再产出 core dump,且非属主、
|
|
19
|
+
// 非 ptrace 授权的进程读不到 /proc/<pid>/mem 与 /proc/<pid>/maps。
|
|
20
|
+
// 副作用是本进程写的文件属主不再是 dumpable 语义下的自己(/tmp 下按目录名
|
|
21
|
+
// 隔离的私有临时目录不受影响)。
|
|
22
|
+
//
|
|
23
|
+
// 未纳入:/proc/<pid>/status 的 TracerPid 字段——读它本身没问题,但攻击者 attach
|
|
24
|
+
// 前后改一行 status 即可绕过(同一函数里 ptrace 系统调用才是硬证据),徒增误报源。
|
|
25
|
+
|
|
26
|
+
import (
|
|
27
|
+
"os"
|
|
28
|
+
"sync"
|
|
29
|
+
"syscall"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// antiDebugCheck 命中即退出(退出码 77,与 Windows 侧一致,不显示窗口、不回显原因)。
|
|
33
|
+
func antiDebugCheck() {
|
|
34
|
+
if debuggerPresent() {
|
|
35
|
+
os.Exit(77)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// debuggerPresent 汇总 Linux 侧信号。
|
|
40
|
+
func debuggerPresent() bool {
|
|
41
|
+
if ptracemeRejectedOnce() {
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
makeNonDumpable() // 未命中也要落地下一步加固(core dump / /proc 读内存收紧)
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var (
|
|
49
|
+
tracemeOnce = new(sync.Once) // 指针:单测需要可复位的实例
|
|
50
|
+
tracemeDetected bool
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
// ptracemeRejectedOnce 缓存一次性 TRACEME 结论(见文件头:TRACEME 不可复位)。
|
|
54
|
+
func ptracemeRejectedOnce() bool {
|
|
55
|
+
tracemeOnce.Do(func() { tracemeDetected = ptracemeRejected() })
|
|
56
|
+
return tracemeDetected
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ptracemeRejected 报告 PTRACE_TRACEME 是否被拒(被拒 = 已有调试器附加);
|
|
60
|
+
// 变量形式便于单测替换,生产路径只会被调用一次。
|
|
61
|
+
var ptracemeRejected = func() bool {
|
|
62
|
+
r, _, errno := syscall.RawSyscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_TRACEME), 0, 0)
|
|
63
|
+
return r != 0 || errno != 0
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// makeNonDumpable 关闭 core dump 并收紧 /proc/<pid>/* 的读取授权;失败按未命中处理。
|
|
67
|
+
var makeNonDumpable = func() {
|
|
68
|
+
syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0)
|
|
69
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
//go:build !windows
|
|
1
|
+
//go:build !windows && !linux
|
|
2
2
|
|
|
3
3
|
package freedom
|
|
4
4
|
|
|
5
|
-
//
|
|
6
|
-
//
|
|
5
|
+
// 反调试(macOS 占位实现)。Linux 侧已实装在 anti_debug_linux.go
|
|
6
|
+
// (PTRACE_TRACEME 一次性探测 + PR_SET_DUMPABLE=0 加固);macOS 需要
|
|
7
|
+
// ptrace(PT_DENY_ATTACH) / task_for_pid 等平台 API,本机无 Apple 设备无法验证,
|
|
8
|
+
// 故留空不装——诚实的"未实现"优于未验证的代码。
|
|
7
9
|
func antiDebugCheck() {}
|