shennian 0.3.86 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +3 -2
  2. package/dist/bin/shennian.js +1 -1
  3. package/dist/publish-build-manifest.json +32 -27
  4. package/dist/src/agents/adapter.js +1 -19
  5. package/dist/src/agents/claude-managed-permissions.js +1 -248
  6. package/dist/src/agents/claude.js +8 -393
  7. package/dist/src/agents/codex-managed-permissions.js +1 -294
  8. package/dist/src/agents/codex-managed-room-tools.js +1 -249
  9. package/dist/src/agents/codex-utils.js +7 -200
  10. package/dist/src/agents/codex.d.ts +7 -1
  11. package/dist/src/agents/codex.js +17 -1352
  12. package/dist/src/agents/config-status.js +1 -226
  13. package/dist/src/agents/cursor-managed-permissions.js +2 -299
  14. package/dist/src/agents/cursor.js +1 -314
  15. package/dist/src/agents/custom.js +4 -418
  16. package/dist/src/agents/external-channel-instructions.js +11 -94
  17. package/dist/src/agents/gemini.js +1 -180
  18. package/dist/src/agents/manager.js +18 -250
  19. package/dist/src/agents/model-registry/cache.js +1 -37
  20. package/dist/src/agents/model-registry/discovery.js +2 -188
  21. package/dist/src/agents/model-registry/parsers.js +4 -521
  22. package/dist/src/agents/model-registry/runner.js +1 -30
  23. package/dist/src/agents/model-registry/service.js +1 -115
  24. package/dist/src/agents/model-registry/types.js +1 -8
  25. package/dist/src/agents/model-registry.js +1 -19
  26. package/dist/src/agents/native-capabilities.js +1 -237
  27. package/dist/src/agents/openclaw.js +2 -293
  28. package/dist/src/agents/opencode-managed-permissions.js +2 -187
  29. package/dist/src/agents/opencode.js +1 -298
  30. package/dist/src/agents/platform-instructions.js +17 -46
  31. package/dist/src/agents/runtime-compatibility.js +1 -44
  32. package/dist/src/agents/workbuddy-managed-permissions.js +2 -329
  33. package/dist/src/agents/workbuddy.js +3 -444
  34. package/dist/src/commands/logout.d.ts +3 -0
  35. package/dist/src/commands/logout.js +1 -0
  36. package/dist/src/daemon-ipc/paths.js +1 -1
  37. package/dist/src/fs/boundary.js +1 -185
  38. package/dist/src/fs/handler.js +1 -130
  39. package/dist/src/fs/security.js +1 -32
  40. package/dist/src/fs/text-decoder.js +1 -110
  41. package/dist/src/index.js +3 -587
  42. package/dist/src/manager/prompt.js +29 -34
  43. package/dist/src/manager/registry.js +1 -317
  44. package/dist/src/manager/retired-external-channel-compat.js +1 -40
  45. package/dist/src/manager/runtime.js +23 -1514
  46. package/dist/src/native-fusion/codex-parser.d.ts +3 -1
  47. package/dist/src/native-fusion/codex-parser.js +4 -660
  48. package/dist/src/native-fusion/codex-user-input.js +5 -102
  49. package/dist/src/native-fusion/config.js +1 -5
  50. package/dist/src/native-fusion/file-watcher.js +1 -191
  51. package/dist/src/native-fusion/local-repository.js +2 -569
  52. package/dist/src/native-fusion/opencode-parser.js +3 -130
  53. package/dist/src/native-fusion/parser-common.d.ts +6 -1
  54. package/dist/src/native-fusion/parser-common.js +4 -4
  55. package/dist/src/native-fusion/parsers.d.ts +4 -2
  56. package/dist/src/native-fusion/parsers.js +3 -140
  57. package/dist/src/native-fusion/service.js +2 -739
  58. package/dist/src/native-fusion/session-header.js +2 -101
  59. package/dist/src/native-fusion/state.js +2 -104
  60. package/dist/src/native-fusion/types.js +1 -1
  61. package/dist/src/personal-sync/machine-authority.js +1 -34
  62. package/dist/src/room/activation-audit.js +2 -45
  63. package/dist/src/room/activation-policy.js +1 -30
  64. package/dist/src/room/activation-service.js +8 -408
  65. package/dist/src/room/invite-bootstrap.js +1 -156
  66. package/dist/src/room/managed-prompt-composer.d.ts +1 -1
  67. package/dist/src/room/managed-prompt-composer.js +17 -16
  68. package/dist/src/room/managed-room-binding.js +1 -167
  69. package/dist/src/room/managed-room-ipc.d.ts +4 -0
  70. package/dist/src/room/managed-room-ipc.js +2 -2
  71. package/dist/src/room/mirror-sync-service.d.ts +1 -0
  72. package/dist/src/room/mirror-sync-service.js +1 -183
  73. package/dist/src/room/room-binding-client.js +1 -151
  74. package/dist/src/session/archive-zip.js +1 -220
  75. package/dist/src/session/deletion-sync-state.js +2 -136
  76. package/dist/src/session/handlers/agent-capabilities.js +1 -14
  77. package/dist/src/session/handlers/agent-config.js +1 -150
  78. package/dist/src/session/handlers/agents.js +1 -55
  79. package/dist/src/session/handlers/chat.js +2 -1030
  80. package/dist/src/session/handlers/control.js +1 -61
  81. package/dist/src/session/handlers/fs.js +1 -594
  82. package/dist/src/session/handlers/message-status.js +1 -15
  83. package/dist/src/session/handlers/remote-access.js +1 -43
  84. package/dist/src/session/handlers/room-managed.js +1 -101
  85. package/dist/src/session/handlers/session-refresh.js +1 -35
  86. package/dist/src/session/handlers/title.js +1 -79
  87. package/dist/src/session/handlers/tool-detail.js +1 -149
  88. package/dist/src/session/handlers/voice-window.js +1 -62
  89. package/dist/src/session/history-subscriptions.js +1 -470
  90. package/dist/src/session/manager.d.ts +1 -0
  91. package/dist/src/session/manager.js +1 -658
  92. package/dist/src/session/native-cleanup-outbox.js +2 -287
  93. package/dist/src/session/native-session-lifecycle.js +6 -327
  94. package/dist/src/session/queue.d.ts +8 -0
  95. package/dist/src/session/queue.js +6 -6
  96. package/dist/src/upgrade/scheduler.js +1 -126
  97. package/node_modules/@shennian/wire/dist/agent-workspace.js +2 -2
  98. package/package.json +1 -1
@@ -1,185 +1 @@
1
- // @arch docs/architecture/cli/daemon.md#文件系统权限边界
2
- // @test src/__tests__/fs-boundary.test.ts
3
- import fs from 'node:fs';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
- const WINDOWS_ABSOLUTE_RE = /^[A-Za-z]:([\\/]|$)/;
7
- const WINDOWS_UNC_RE = /^\\\\[^\\]+\\[^\\]+/;
8
- const WINDOWS_DRIVE_RELATIVE_RE = /^[A-Za-z]:(?![\\/])/;
9
- function isWindowsAbsolutePath(input) {
10
- return WINDOWS_ABSOLUTE_RE.test(input) || WINDOWS_UNC_RE.test(input);
11
- }
12
- function hasWindowsDriveRelativePrefix(input) {
13
- return WINDOWS_DRIVE_RELATIVE_RE.test(input);
14
- }
15
- function normalizeWindowsAbsolutePath(input) {
16
- return input.replace(/^[/\\]([A-Za-z]:[\\/].*)$/, '$1');
17
- }
18
- function isHomeRelativePath(input) {
19
- return input === '~' || input.startsWith('~/') || input.startsWith('~\\');
20
- }
21
- function pathFlavor(input) {
22
- return isWindowsAbsolutePath(normalizeWindowsAbsolutePath(input)) ? 'win32' : 'posix';
23
- }
24
- function pathApi(flavor) {
25
- return flavor === 'win32' ? path.win32 : path.posix;
26
- }
27
- function normalizeForCompare(input, flavor) {
28
- const api = pathApi(flavor);
29
- const normalized = api.normalize(input);
30
- const trimmed = normalized.length > api.parse(normalized).root.length
31
- ? normalized.replace(/[\\/]+$/, '')
32
- : normalized;
33
- return flavor === 'win32' ? trimmed.toLowerCase() : trimmed;
34
- }
35
- function isWithinOrEqual(candidate, root, flavor) {
36
- const api = pathApi(flavor);
37
- const normalizedCandidate = normalizeForCompare(candidate, flavor);
38
- const normalizedRoot = normalizeForCompare(root, flavor);
39
- if (normalizedCandidate === normalizedRoot)
40
- return true;
41
- const relative = api.relative(normalizedRoot, normalizedCandidate);
42
- return Boolean(relative) && !relative.startsWith('..') && !api.isAbsolute(relative);
43
- }
44
- function safeRealpath(pathValue) {
45
- try {
46
- return fs.realpathSync.native(pathValue);
47
- }
48
- catch {
49
- return null;
50
- }
51
- }
52
- async function safeRealpathAsync(pathValue) {
53
- try {
54
- return await fs.promises.realpath(pathValue);
55
- }
56
- catch {
57
- return null;
58
- }
59
- }
60
- export function resolveSessionWorkDir(input) {
61
- if (!input)
62
- return os.homedir();
63
- const normalizedInput = normalizeWindowsAbsolutePath(input);
64
- const flavor = pathFlavor(normalizedInput);
65
- const api = pathApi(flavor);
66
- if (isHomeRelativePath(normalizedInput)) {
67
- return path.join(os.homedir(), normalizedInput.slice(1).replace(/^[/\\]+/, ''));
68
- }
69
- if (process.platform === 'win32') {
70
- const normalized = normalizedInput.replace(/\\/g, '/');
71
- if (normalized === '/tmp' || normalized.startsWith('/tmp/')) {
72
- const suffix = normalized.slice('/tmp'.length).replace(/^\/+/, '');
73
- return suffix ? path.win32.join(os.tmpdir(), ...suffix.split('/')) : os.tmpdir();
74
- }
75
- }
76
- if (isWindowsAbsolutePath(normalizedInput)) {
77
- return path.win32.resolve(normalizedInput);
78
- }
79
- return api.resolve(normalizedInput);
80
- }
81
- export function createAuthorizedFsRoot(rawRoot) {
82
- const normalized = resolveSessionWorkDir(rawRoot);
83
- const flavor = pathFlavor(normalized);
84
- const stat = safeStat(normalized);
85
- const file = typeof stat?.isFile === 'function' ? stat.isFile() : false;
86
- const boundary = file ? pathApi(flavor).dirname(normalized) : normalized;
87
- return {
88
- raw: rawRoot,
89
- normalized,
90
- boundary,
91
- real: safeRealpath(boundary),
92
- flavor,
93
- file,
94
- };
95
- }
96
- export async function createAuthorizedFsRootAsync(rawRoot) {
97
- const normalized = resolveSessionWorkDir(rawRoot);
98
- const flavor = pathFlavor(normalized);
99
- const stat = await safeStatAsync(normalized);
100
- const file = typeof stat?.isFile === 'function' ? stat.isFile() : false;
101
- const boundary = file ? pathApi(flavor).dirname(normalized) : normalized;
102
- return {
103
- raw: rawRoot,
104
- normalized,
105
- boundary,
106
- real: await safeRealpathAsync(boundary),
107
- flavor,
108
- file,
109
- };
110
- }
111
- export function resolveAuthorizedPath(rawPath, root) {
112
- const input = String(rawPath || '').trim() || root.normalized;
113
- const normalizedInput = isHomeRelativePath(input)
114
- ? resolveSessionWorkDir(input)
115
- : normalizeWindowsAbsolutePath(input);
116
- const api = pathApi(root.flavor);
117
- if (hasWindowsDriveRelativePrefix(normalizedInput)) {
118
- return { ok: false, error: `Access denied: ${rawPath}` };
119
- }
120
- const candidate = api.isAbsolute(normalizedInput)
121
- ? api.resolve(normalizedInput)
122
- : api.resolve(root.boundary, normalizedInput);
123
- if (!isWithinOrEqual(candidate, root.boundary, root.flavor)) {
124
- return { ok: false, error: `Access denied: ${rawPath}` };
125
- }
126
- if (root.file && normalizeForCompare(candidate, root.flavor) !== normalizeForCompare(root.normalized, root.flavor)) {
127
- return { ok: false, error: `Access denied: ${rawPath}` };
128
- }
129
- const realCandidate = safeRealpath(candidate);
130
- if (realCandidate) {
131
- const realRoot = root.real ?? safeRealpath(root.boundary);
132
- if (realRoot && !isWithinOrEqual(realCandidate, realRoot, pathFlavor(realRoot))) {
133
- return { ok: false, error: `Access denied: ${rawPath}` };
134
- }
135
- }
136
- return { ok: true, path: candidate, root };
137
- }
138
- export async function resolveAuthorizedPathAsync(rawPath, root) {
139
- const input = String(rawPath || '').trim() || root.normalized;
140
- const normalizedInput = isHomeRelativePath(input)
141
- ? resolveSessionWorkDir(input)
142
- : normalizeWindowsAbsolutePath(input);
143
- const api = pathApi(root.flavor);
144
- if (hasWindowsDriveRelativePrefix(normalizedInput)) {
145
- return { ok: false, error: `Access denied: ${rawPath}` };
146
- }
147
- const candidate = api.isAbsolute(normalizedInput)
148
- ? api.resolve(normalizedInput)
149
- : api.resolve(root.boundary, normalizedInput);
150
- if (!isWithinOrEqual(candidate, root.boundary, root.flavor)) {
151
- return { ok: false, error: `Access denied: ${rawPath}` };
152
- }
153
- if (root.file
154
- && normalizeForCompare(candidate, root.flavor) !== normalizeForCompare(root.normalized, root.flavor)) {
155
- return { ok: false, error: `Access denied: ${rawPath}` };
156
- }
157
- const realCandidate = await safeRealpathAsync(candidate);
158
- if (realCandidate) {
159
- const realRoot = root.real ?? await safeRealpathAsync(root.boundary);
160
- if (realRoot && !isWithinOrEqual(realCandidate, realRoot, pathFlavor(realRoot))) {
161
- return { ok: false, error: `Access denied: ${rawPath}` };
162
- }
163
- }
164
- return { ok: true, path: candidate, root };
165
- }
166
- export function isAuthorizedRootPath(pathValue, root) {
167
- return isWithinOrEqual(pathValue, root.boundary, root.flavor) &&
168
- normalizeForCompare(pathValue, root.flavor) === normalizeForCompare(root.boundary, root.flavor);
169
- }
170
- function safeStat(pathValue) {
171
- try {
172
- return fs.statSync(pathValue);
173
- }
174
- catch {
175
- return null;
176
- }
177
- }
178
- async function safeStatAsync(pathValue) {
179
- try {
180
- return await fs.promises.stat(pathValue);
181
- }
182
- catch {
183
- return null;
184
- }
185
- }
1
+ import p from"node:fs";import m from"node:os";import u from"node:path";const R=/^[A-Za-z]:([\\/]|$)/,$=/^\\\\[^\\]+\\[^\\]+/,k=/^[A-Za-z]:(?![\\/])/;function b(n){return R.test(n)||$.test(n)}function W(n){return k.test(n)}function A(n){return n.replace(/^[/\\]([A-Za-z]:[\\/].*)$/,"$1")}function h(n){return n==="~"||n.startsWith("~/")||n.startsWith("~\\")}function f(n){return b(A(n))?"win32":"posix"}function l(n){return n==="win32"?u.win32:u.posix}function o(n,e){const i=l(e),r=i.normalize(n),t=r.length>i.parse(r).root.length?r.replace(/[\\/]+$/,""):r;return e==="win32"?t.toLowerCase():t}function d(n,e,i){const r=l(i),t=o(n,i),s=o(e,i);if(t===s)return!0;const a=r.relative(s,t);return!!a&&!a.startsWith("..")&&!r.isAbsolute(a)}function y(n){try{return p.realpathSync.native(n)}catch{return null}}async function z(n){try{return await p.promises.realpath(n)}catch{return null}}function v(n){if(!n)return m.homedir();const e=A(n),i=f(e),r=l(i);if(h(e))return u.join(m.homedir(),e.slice(1).replace(/^[/\\]+/,""));if(process.platform==="win32"){const t=e.replace(/\\/g,"/");if(t==="/tmp"||t.startsWith("/tmp/")){const s=t.slice(4).replace(/^\/+/,"");return s?u.win32.join(m.tmpdir(),...s.split("/")):m.tmpdir()}}return b(e)?u.win32.resolve(e):r.resolve(e)}function I(n){const e=v(n),i=f(e),r=S(e),t=typeof r?.isFile=="function"?r.isFile():!1,s=t?l(i).dirname(e):e;return{raw:n,normalized:e,boundary:s,real:y(s),flavor:i,file:t}}async function _(n){const e=v(n),i=f(e),r=await x(e),t=typeof r?.isFile=="function"?r.isFile():!1,s=t?l(i).dirname(e):e;return{raw:n,normalized:e,boundary:s,real:await z(s),flavor:i,file:t}}function g(n,e){const i=String(n||"").trim()||e.normalized,r=h(i)?v(i):A(i),t=l(e.flavor);if(W(r))return{ok:!1,error:`Access denied: ${n}`};const s=t.isAbsolute(r)?t.resolve(r):t.resolve(e.boundary,r);if(!d(s,e.boundary,e.flavor))return{ok:!1,error:`Access denied: ${n}`};if(e.file&&o(s,e.flavor)!==o(e.normalized,e.flavor))return{ok:!1,error:`Access denied: ${n}`};const a=y(s);if(a){const c=e.real??y(e.boundary);if(c&&!d(a,c,f(c)))return{ok:!1,error:`Access denied: ${n}`}}return{ok:!0,path:s,root:e}}async function C(n,e){const i=String(n||"").trim()||e.normalized,r=h(i)?v(i):A(i),t=l(e.flavor);if(W(r))return{ok:!1,error:`Access denied: ${n}`};const s=t.isAbsolute(r)?t.resolve(r):t.resolve(e.boundary,r);if(!d(s,e.boundary,e.flavor))return{ok:!1,error:`Access denied: ${n}`};if(e.file&&o(s,e.flavor)!==o(e.normalized,e.flavor))return{ok:!1,error:`Access denied: ${n}`};const a=await z(s);if(a){const c=e.real??await z(e.boundary);if(c&&!d(a,c,f(c)))return{ok:!1,error:`Access denied: ${n}`}}return{ok:!0,path:s,root:e}}function D(n,e){return d(n,e.boundary,e.flavor)&&o(n,e.flavor)===o(e.boundary,e.flavor)}function S(n){try{return p.statSync(n)}catch{return null}}async function x(n){try{return await p.promises.stat(n)}catch{return null}}export{I as createAuthorizedFsRoot,_ as createAuthorizedFsRootAsync,D as isAuthorizedRootPath,g as resolveAuthorizedPath,C as resolveAuthorizedPathAsync,v as resolveSessionWorkDir};
@@ -1,130 +1 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { normalizePath, isPathSafe } from './security.js';
5
- const FILE_SYSTEM_ROOTS_PATH = '__roots__';
6
- const MAX_READ_SIZE = 100 * 1024;
7
- const MAX_TRANSFER_SIZE = 20 * 1024 * 1024;
8
- const CHUNK_SIZE = 64 * 1024;
9
- const RATE_LIMIT_BYTES_PER_SEC = 5 * 1024 * 1024;
10
- function sleep(ms) {
11
- return new Promise((r) => setTimeout(r, ms));
12
- }
13
- function listFileSystemRoots() {
14
- if (os.platform() === 'win32') {
15
- const entries = [];
16
- for (let code = 65; code <= 90; code += 1) {
17
- const drive = `${String.fromCharCode(code)}:\\`;
18
- if (!fs.existsSync(drive))
19
- continue;
20
- entries.push({ name: drive, path: drive, isDir: true });
21
- }
22
- return { entries };
23
- }
24
- return { entries: [{ name: '/', path: '/', isDir: true }] };
25
- }
26
- export function handleFsLs(reqPath) {
27
- if (reqPath === FILE_SYSTEM_ROOTS_PATH) {
28
- return listFileSystemRoots();
29
- }
30
- const normalized = normalizePath(reqPath);
31
- if (!isPathSafe(normalized)) {
32
- return { error: `Access denied: ${reqPath}` };
33
- }
34
- try {
35
- const dirents = fs.readdirSync(normalized, { withFileTypes: true });
36
- const entries = dirents.map((d) => {
37
- const fullPath = path.join(normalized, d.name);
38
- const isDir = d.isDirectory();
39
- const entry = { name: d.name, path: fullPath, isDir };
40
- try {
41
- const stat = fs.statSync(fullPath);
42
- entry.size = isDir ? undefined : stat.size;
43
- entry.modifiedAt = stat.mtimeMs;
44
- }
45
- catch {
46
- // stat failure is non-fatal
47
- }
48
- return entry;
49
- });
50
- return { entries };
51
- }
52
- catch (err) {
53
- return { error: `Failed to list ${reqPath}: ${err.message}` };
54
- }
55
- }
56
- export function handleFsRead(reqPath) {
57
- const normalized = normalizePath(reqPath);
58
- if (!isPathSafe(normalized)) {
59
- return { error: `Access denied: ${reqPath}` };
60
- }
61
- try {
62
- const stat = fs.statSync(normalized);
63
- if (stat.isDirectory()) {
64
- return { error: `Not a file: ${reqPath}` };
65
- }
66
- if (stat.size > MAX_READ_SIZE) {
67
- return { error: `File too large (${stat.size} bytes, max ${MAX_READ_SIZE})` };
68
- }
69
- const content = fs.readFileSync(normalized, 'utf-8');
70
- return { path: normalized, content };
71
- }
72
- catch (err) {
73
- return { error: `Failed to read ${reqPath}: ${err.message}` };
74
- }
75
- }
76
- export async function* handleFsDownload(req, onProgress) {
77
- const filePath = req.path;
78
- if (!filePath) {
79
- throw new Error('Missing path for download');
80
- }
81
- const normalized = normalizePath(filePath);
82
- if (!isPathSafe(normalized)) {
83
- throw new Error(`Access denied: ${filePath}`);
84
- }
85
- const stat = fs.statSync(normalized);
86
- if (stat.size > MAX_TRANSFER_SIZE) {
87
- throw new Error(`File too large (${stat.size} bytes, max ${MAX_TRANSFER_SIZE})`);
88
- }
89
- const buf = fs.readFileSync(normalized);
90
- const total = Math.ceil(buf.length / CHUNK_SIZE);
91
- const chunksPerSec = Math.floor(RATE_LIMIT_BYTES_PER_SEC / CHUNK_SIZE);
92
- const delayMs = chunksPerSec > 0 ? Math.ceil(1000 / chunksPerSec) : 0;
93
- for (let seq = 0; seq < total; seq++) {
94
- const start = seq * CHUNK_SIZE;
95
- const end = Math.min(start + CHUNK_SIZE, buf.length);
96
- const data = buf.subarray(start, end).toString('base64');
97
- yield { transferId: req.transferId, seq, total, data };
98
- onProgress?.({
99
- transferId: req.transferId,
100
- seq,
101
- total,
102
- bytesSent: end,
103
- bytesTotal: buf.length,
104
- });
105
- if (delayMs > 0 && seq < total - 1) {
106
- await sleep(delayMs);
107
- }
108
- }
109
- }
110
- export function handleFsUploadInit(req) {
111
- const targetPath = req.targetPath ?? req.path;
112
- if (!targetPath) {
113
- return { error: 'Missing targetPath for upload' };
114
- }
115
- const normalized = normalizePath(targetPath);
116
- if (!isPathSafe(normalized)) {
117
- return { error: `Access denied: ${targetPath}` };
118
- }
119
- if (req.size != null && req.size > MAX_TRANSFER_SIZE) {
120
- return { error: `File too large (${req.size} bytes, max ${MAX_TRANSFER_SIZE})` };
121
- }
122
- const dir = path.dirname(normalized);
123
- fs.mkdirSync(dir, { recursive: true });
124
- return { targetPath: normalized };
125
- }
126
- export function handleFsUploadChunk(targetPath, chunk) {
127
- const buf = Buffer.from(chunk.data, 'base64');
128
- const flag = chunk.seq === 0 ? 'w' : 'a';
129
- fs.writeFileSync(targetPath, buf, { flag });
130
- }
1
+ import o from"node:fs";import _ from"node:os";import S from"node:path";import{normalizePath as f,isPathSafe as u}from"./security.js";const $="__roots__",g=100*1024,m=20*1024*1024,h=64*1024,w=5*1024*1024;function z(t){return new Promise(e=>setTimeout(e,t))}function E(){if(_.platform()==="win32"){const t=[];for(let e=65;e<=90;e+=1){const r=`${String.fromCharCode(e)}:\\`;o.existsSync(r)&&t.push({name:r,path:r,isDir:!0})}return{entries:t}}return{entries:[{name:"/",path:"/",isDir:!0}]}}function T(t){if(t===$)return E();const e=f(t);if(!u(e))return{error:`Access denied: ${t}`};try{return{entries:o.readdirSync(e,{withFileTypes:!0}).map(a=>{const s=S.join(e,a.name),i=a.isDirectory(),c={name:a.name,path:s,isDir:i};try{const d=o.statSync(s);c.size=i?void 0:d.size,c.modifiedAt=d.mtimeMs}catch{}return c})}}catch(r){return{error:`Failed to list ${t}: ${r.message}`}}}function x(t){const e=f(t);if(!u(e))return{error:`Access denied: ${t}`};try{const r=o.statSync(e);if(r.isDirectory())return{error:`Not a file: ${t}`};if(r.size>g)return{error:`File too large (${r.size} bytes, max ${g})`};const n=o.readFileSync(e,"utf-8");return{path:e,content:n}}catch(r){return{error:`Failed to read ${t}: ${r.message}`}}}async function*R(t,e){const r=t.path;if(!r)throw new Error("Missing path for download");const n=f(r);if(!u(n))throw new Error(`Access denied: ${r}`);const a=o.statSync(n);if(a.size>m)throw new Error(`File too large (${a.size} bytes, max ${m})`);const s=o.readFileSync(n),i=Math.ceil(s.length/h),c=Math.floor(w/h),d=c>0?Math.ceil(1e3/c):0;for(let l=0;l<i;l++){const y=l*h,p=Math.min(y+h,s.length),F=s.subarray(y,p).toString("base64");yield{transferId:t.transferId,seq:l,total:i,data:F},e?.({transferId:t.transferId,seq:l,total:i,bytesSent:p,bytesTotal:s.length}),d>0&&l<i-1&&await z(d)}}function D(t){const e=t.targetPath??t.path;if(!e)return{error:"Missing targetPath for upload"};const r=f(e);if(!u(r))return{error:`Access denied: ${e}`};if(t.size!=null&&t.size>m)return{error:`File too large (${t.size} bytes, max ${m})`};const n=S.dirname(r);return o.mkdirSync(n,{recursive:!0}),{targetPath:r}}function P(t,e){const r=Buffer.from(e.data,"base64"),n=e.seq===0?"w":"a";o.writeFileSync(t,r,{flag:n})}export{R as handleFsDownload,T as handleFsLs,x as handleFsRead,P as handleFsUploadChunk,D as handleFsUploadInit};
@@ -1,32 +1 @@
1
- import path from 'node:path';
2
- import os from 'node:os';
3
- const FILE_SYSTEM_ROOTS_PATH = '__roots__';
4
- const ALLOWED_ROOTS = [os.homedir()];
5
- const BLOCKED_PATTERNS = [
6
- /\.\./, // directory traversal
7
- /\/\.ssh\//,
8
- /\/\.gnupg\//,
9
- /\/\.aws\/credentials/,
10
- /\/\.env$/,
11
- /\/\.env\./,
12
- ];
13
- export function normalizePath(inputPath) {
14
- if (inputPath === FILE_SYSTEM_ROOTS_PATH)
15
- return inputPath;
16
- const expanded = inputPath.startsWith('~')
17
- ? path.join(os.homedir(), inputPath.slice(1))
18
- : inputPath;
19
- return path.resolve(expanded);
20
- }
21
- export function isPathSafe(normalizedPath) {
22
- if (normalizedPath === FILE_SYSTEM_ROOTS_PATH)
23
- return true;
24
- const withinAllowed = ALLOWED_ROOTS.some((root) => normalizedPath === root || normalizedPath.startsWith(root + path.sep));
25
- if (!withinAllowed)
26
- return false;
27
- for (const pattern of BLOCKED_PATTERNS) {
28
- if (pattern.test(normalizedPath))
29
- return false;
30
- }
31
- return true;
32
- }
1
+ import t from"node:path";import o from"node:os";const n="__roots__",i=[o.homedir()],f=[/\.\./,/\/\.ssh\//,/\/\.gnupg\//,/\/\.aws\/credentials/,/\/\.env$/,/\/\.env\./];function l(e){if(e===n)return e;const s=e.startsWith("~")?t.join(o.homedir(),e.slice(1)):e;return t.resolve(s)}function u(e){if(e===n)return!0;if(!i.some(r=>e===r||e.startsWith(r+t.sep)))return!1;for(const r of f)if(r.test(e))return!1;return!0}export{u as isPathSafe,l as normalizePath};
@@ -1,110 +1 @@
1
- // @arch docs/architecture/cli/daemon.md#文件系统权限边界
2
- // @test src/__tests__/session-manager.test.ts
3
- import { TextDecoder } from 'node:util';
4
- const AUTO_TEXT_ENCODINGS = new Set(['auto', 'utf8-auto', 'utf-8-auto', 'text-auto']);
5
- const TEXT_SAMPLE_BYTES = 8192;
6
- const MAX_SUSPICIOUS_CONTROL_RATIO = 0.01;
7
- export class BinaryTextPreviewError extends Error {
8
- constructor() {
9
- super('File appears to be binary; use encoding=base64');
10
- this.name = 'BinaryTextPreviewError';
11
- }
12
- }
13
- export function isAutoTextEncoding(value) {
14
- return AUTO_TEXT_ENCODINGS.has(value.trim().toLowerCase());
15
- }
16
- function startsWithBytes(buffer, bytes) {
17
- if (buffer.length < bytes.length)
18
- return false;
19
- return bytes.every((byte, index) => buffer[index] === byte);
20
- }
21
- function isAllowedTextControlByte(byte) {
22
- return byte === 0x09 || byte === 0x0a || byte === 0x0c || byte === 0x0d || byte === 0x1b;
23
- }
24
- function hasBinaryMagic(buffer) {
25
- return (startsWithBytes(buffer, [0x25, 0x50, 0x44, 0x46]) || // PDF
26
- startsWithBytes(buffer, [0x50, 0x4b, 0x03, 0x04]) || // ZIP / Office Open XML
27
- startsWithBytes(buffer, [0x50, 0x4b, 0x05, 0x06]) ||
28
- startsWithBytes(buffer, [0x50, 0x4b, 0x07, 0x08]) ||
29
- startsWithBytes(buffer, [0x89, 0x50, 0x4e, 0x47]) || // PNG
30
- startsWithBytes(buffer, [0xff, 0xd8, 0xff]) || // JPEG
31
- startsWithBytes(buffer, [0x47, 0x49, 0x46, 0x38]) || // GIF
32
- startsWithBytes(buffer, [0x52, 0x49, 0x46, 0x46]) || // RIFF / WebP / WAV / AVI
33
- startsWithBytes(buffer, [0x1f, 0x8b]) || // gzip
34
- startsWithBytes(buffer, [0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c]) || // 7z
35
- startsWithBytes(buffer, [0x52, 0x61, 0x72, 0x21, 0x1a, 0x07]) || // RAR
36
- startsWithBytes(buffer, [0x7f, 0x45, 0x4c, 0x46]) || // ELF
37
- startsWithBytes(buffer, [0xcf, 0xfa, 0xed, 0xfe]) || // Mach-O
38
- startsWithBytes(buffer, [0xca, 0xfe, 0xba, 0xbe]) ||
39
- startsWithBytes(buffer, [0x4d, 0x5a]) || // Windows PE
40
- startsWithBytes(buffer, [0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66]));
41
- }
42
- function looksLikeBinaryBuffer(buffer) {
43
- if (hasBinaryMagic(buffer))
44
- return true;
45
- const sample = buffer.subarray(0, Math.min(buffer.length, TEXT_SAMPLE_BYTES));
46
- if (sample.length === 0)
47
- return false;
48
- let suspiciousControls = 0;
49
- for (const byte of sample) {
50
- if (byte === 0x00)
51
- return true;
52
- if (byte < 0x20 && !isAllowedTextControlByte(byte))
53
- suspiciousControls += 1;
54
- }
55
- return suspiciousControls / sample.length > MAX_SUSPICIOUS_CONTROL_RATIO;
56
- }
57
- function isAllowedTextControlCode(codePoint) {
58
- return codePoint === 0x09 || codePoint === 0x0a || codePoint === 0x0c || codePoint === 0x0d || codePoint === 0x1b;
59
- }
60
- function hasDecodedControlNoise(content) {
61
- if (!content)
62
- return false;
63
- let suspiciousControls = 0;
64
- let total = 0;
65
- for (const char of content.slice(0, TEXT_SAMPLE_BYTES)) {
66
- const codePoint = char.codePointAt(0) ?? 0;
67
- total += 1;
68
- if (((codePoint >= 0x00 && codePoint < 0x20) || (codePoint >= 0x7f && codePoint <= 0x9f)) &&
69
- !isAllowedTextControlCode(codePoint)) {
70
- suspiciousControls += 1;
71
- }
72
- }
73
- return total > 0 && suspiciousControls / total > MAX_SUSPICIOUS_CONTROL_RATIO;
74
- }
75
- function decodeStrict(buffer, decoderEncoding, encoding, fallback) {
76
- const content = new TextDecoder(decoderEncoding, { fatal: true }).decode(buffer);
77
- if (hasDecodedControlNoise(content))
78
- throw new BinaryTextPreviewError();
79
- return { content, encoding, fallback };
80
- }
81
- export function decodeTextBufferAuto(buffer) {
82
- if (buffer.length === 0)
83
- return { content: '', encoding: 'utf8', fallback: false };
84
- if (startsWithBytes(buffer, [0xef, 0xbb, 0xbf])) {
85
- return decodeStrict(buffer, 'utf-8', 'utf8', false);
86
- }
87
- if (startsWithBytes(buffer, [0xff, 0xfe])) {
88
- return decodeStrict(buffer, 'utf-16le', 'utf16le', true);
89
- }
90
- if (startsWithBytes(buffer, [0xfe, 0xff])) {
91
- return decodeStrict(buffer, 'utf-16be', 'utf16be', true);
92
- }
93
- if (looksLikeBinaryBuffer(buffer))
94
- throw new BinaryTextPreviewError();
95
- try {
96
- return decodeStrict(buffer, 'utf-8', 'utf8', false);
97
- }
98
- catch (error) {
99
- if (error instanceof BinaryTextPreviewError)
100
- throw error;
101
- }
102
- try {
103
- return decodeStrict(buffer, 'gb18030', 'gb18030', true);
104
- }
105
- catch (error) {
106
- if (error instanceof BinaryTextPreviewError)
107
- throw error;
108
- }
109
- return decodeStrict(buffer, 'windows-1252', 'windows-1252', true);
110
- }
1
+ import{TextDecoder as l}from"node:util";const u=new Set(["auto","utf8-auto","utf-8-auto","text-auto"]),s=8192,c=.01;class a extends Error{constructor(){super("File appears to be binary; use encoding=base64"),this.name="BinaryTextPreviewError"}}function p(t){return u.has(t.trim().toLowerCase())}function x(t,n){return t.length<n.length?!1:n.every((e,o)=>t[o]===e)}function d(t){return t===9||t===10||t===12||t===13||t===27}function f(t){return x(t,[37,80,68,70])||x(t,[80,75,3,4])||x(t,[80,75,5,6])||x(t,[80,75,7,8])||x(t,[137,80,78,71])||x(t,[255,216,255])||x(t,[71,73,70,56])||x(t,[82,73,70,70])||x(t,[31,139])||x(t,[55,122,188,175,39,28])||x(t,[82,97,114,33,26,7])||x(t,[127,69,76,70])||x(t,[207,250,237,254])||x(t,[202,254,186,190])||x(t,[77,90])||x(t,[83,81,76,105,116,101,32,102])}function h(t){if(f(t))return!0;const n=t.subarray(0,Math.min(t.length,s));if(n.length===0)return!1;let e=0;for(const o of n){if(o===0)return!0;o<32&&!d(o)&&(e+=1)}return e/n.length>c}function w(t){return t===9||t===10||t===12||t===13||t===27}function T(t){if(!t)return!1;let n=0,e=0;for(const o of t.slice(0,s)){const r=o.codePointAt(0)??0;e+=1,(r>=0&&r<32||r>=127&&r<=159)&&!w(r)&&(n+=1)}return e>0&&n/e>c}function i(t,n,e,o){const r=new l(n,{fatal:!0}).decode(t);if(T(r))throw new a;return{content:r,encoding:e,fallback:o}}function C(t){if(t.length===0)return{content:"",encoding:"utf8",fallback:!1};if(x(t,[239,187,191]))return i(t,"utf-8","utf8",!1);if(x(t,[255,254]))return i(t,"utf-16le","utf16le",!0);if(x(t,[254,255]))return i(t,"utf-16be","utf16be",!0);if(h(t))throw new a;try{return i(t,"utf-8","utf8",!1)}catch(n){if(n instanceof a)throw n}try{return i(t,"gb18030","gb18030",!0)}catch(n){if(n instanceof a)throw n}return i(t,"windows-1252","windows-1252",!0)}export{a as BinaryTextPreviewError,C as decodeTextBufferAuto,p as isAutoTextEncoding};