@miphamai/cli 0.37.0 → 0.39.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.
- package/bin/mipham.ts +25 -0
- package/package.json +1 -1
- package/src/agent/cross-session/discovery.ts +45 -1
- package/src/agent/cross-session/file-inbox.ts +15 -0
- package/src/agent/message-router.ts +22 -2
- package/src/agent/sub-agent.ts +4 -3
- package/src/config/defaults.ts +19 -1
- package/src/config/keys-manager.ts +11 -1
- package/src/config/loader.ts +20 -10
- package/src/core/context.ts +86 -56
- package/src/core/credential-masker/output-scrub.ts +5 -0
- package/src/core/crsi-sandbox.ts +19 -7
- package/src/core/engine.ts +69 -35
- package/src/core/instructions.ts +4 -42
- package/src/core/permission.ts +7 -1
- package/src/core/red-team.ts +20 -5
- package/src/core/self-critique.ts +3 -1
- package/src/core/session-log.ts +225 -0
- package/src/core/session-store.ts +119 -34
- package/src/core/workspace-trust.ts +25 -2
- package/src/daemon/auth.ts +9 -6
- package/src/daemon/cors.ts +24 -16
- package/src/daemon/rate-limiter.ts +4 -0
- package/src/daemon/server.ts +24 -3
- package/src/i18n-core/locales/en-US.json +4 -0
- package/src/i18n-core/locales/zh-CN.json +4 -0
- package/src/index.tsx +62 -30
- package/src/mcp/client.ts +56 -17
- package/src/mcp/http-transport.ts +2 -0
- package/src/mcp/oauth.ts +1 -1
- package/src/providers/llm-replay.ts +36 -0
- package/src/providers/llm.ts +16 -0
- package/src/providers/registry.ts +2 -1
- package/src/security/gate.ts +10 -0
- package/src/security/url.ts +37 -14
- package/src/shared/constants.ts +4 -486
- package/src/shared/types.ts +6 -437
- package/src/skills/fork-executor.ts +3 -1
- package/src/skills/loader.ts +2 -1
- package/src/skills/seam.ts +18 -0
- package/src/tools/agent/agent.ts +1 -0
- package/src/tools/agent/skill.ts +1 -0
- package/src/tools/exec/bash.ts +141 -129
- package/src/tools/exec/git.ts +17 -0
- package/src/tools/file/read.ts +73 -66
- package/src/tools/file/write.ts +1 -1
- package/src/tools/index.ts +54 -132
- package/src/tools/network/web-fetch.ts +41 -26
- package/src/tools/seam.ts +25 -0
- package/src/tools/validation.ts +91 -0
- package/src/ui/app.tsx +36 -0
- package/src/ui/commands.ts +27 -6
- package/src/ui/input.tsx +21 -0
- package/src/vajra/compose/assemble.ts +19 -0
- package/src/vajra/compose/bundle.ts +39 -0
- package/src/vajra/compose/dump.ts +5 -0
- package/src/vajra/compose/index.ts +4 -0
- package/src/vajra/compose/mount.ts +30 -0
- package/src/vajra/context.ts +183 -0
- package/src/vajra/events.ts +13 -0
- package/src/vajra/index.ts +4 -0
- package/src/vajra/leaf/plan-runner.ts +94 -0
- package/src/vajra/service.ts +15 -0
- package/src/workflow/primitives/agent.ts +3 -1
- package/src/workflow/runtime.ts +23 -15
- package/src/workflow/sandbox.ts +51 -9
- package/src/core/compaction-progress.ts +0 -205
- package/src/core/context-compact.ts +0 -124
- package/src/core/context-drain.ts +0 -113
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { join } from 'node:path'
|
|
12
12
|
import { createHash } from 'node:crypto'
|
|
13
13
|
import type { Message } from '../shared/types'
|
|
14
|
+
import { sanitizeSessionName, SessionLog, deriveMessages, messageToEvents } from './session-log'
|
|
14
15
|
|
|
15
16
|
export interface SessionMetadata {
|
|
16
17
|
name: string
|
|
@@ -46,21 +47,11 @@ export interface SessionIndexEntry {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
function ensureDir(): void {
|
|
49
|
-
mkdirSync(SESSIONS_DIR, { recursive: true })
|
|
50
|
+
mkdirSync(SESSIONS_DIR, { recursive: true, mode: 0o700 })
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
function filePath(name: string): string {
|
|
53
|
-
|
|
54
|
-
const safe = name.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
55
|
-
|
|
56
|
-
// For very long names (>100 chars), use SHA-256 hash suffix to prevent collision
|
|
57
|
-
if (safe.length > 100) {
|
|
58
|
-
const hash = createHash('sha256').update(safe).digest('hex').slice(0, 16)
|
|
59
|
-
const truncated = safe.slice(0, 80)
|
|
60
|
-
return join(SESSIONS_DIR, `${truncated}-${hash}.jsonl`)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return join(SESSIONS_DIR, `${safe}.jsonl`)
|
|
54
|
+
return join(SESSIONS_DIR, `${sanitizeSessionName(name)}.jsonl`)
|
|
64
55
|
}
|
|
65
56
|
|
|
66
57
|
export class SessionStore {
|
|
@@ -90,7 +81,7 @@ export class SessionStore {
|
|
|
90
81
|
|
|
91
82
|
// Atomic write: write to temp file, then rename (same-fs rename is atomic)
|
|
92
83
|
const tmp = path + '.tmp'
|
|
93
|
-
writeFileSync(tmp, JSON.stringify(session) + '\n', 'utf-8')
|
|
84
|
+
writeFileSync(tmp, JSON.stringify(session) + '\n', { encoding: 'utf-8', mode: 0o600 })
|
|
94
85
|
renameSync(tmp, path)
|
|
95
86
|
|
|
96
87
|
// Incremental index update — only touch this session's entry
|
|
@@ -101,6 +92,69 @@ export class SessionStore {
|
|
|
101
92
|
}
|
|
102
93
|
}
|
|
103
94
|
|
|
95
|
+
/** 追加持久化一个 SessionLog(幂等:只写新事件)。缺失 session/start 时补一个。 */
|
|
96
|
+
static saveLog(
|
|
97
|
+
name: string,
|
|
98
|
+
log: SessionLog,
|
|
99
|
+
meta?: { provider?: string; model?: string; cwd?: string },
|
|
100
|
+
): void {
|
|
101
|
+
ensureDir()
|
|
102
|
+
const events = log.events()
|
|
103
|
+
if (!events.some((e) => e.type === 'session/start')) {
|
|
104
|
+
log.append({ type: 'session/start', at: Date.now(), sessionId: name, ...meta })
|
|
105
|
+
}
|
|
106
|
+
log.save()
|
|
107
|
+
try {
|
|
108
|
+
const messages = deriveMessages(log.events())
|
|
109
|
+
SessionStore.updateIndexEntry(name, {
|
|
110
|
+
name,
|
|
111
|
+
createdAt: new Date().toISOString(),
|
|
112
|
+
updatedAt: new Date().toISOString(),
|
|
113
|
+
provider: meta?.provider || 'unknown',
|
|
114
|
+
model: meta?.model || 'unknown',
|
|
115
|
+
messageCount: messages.length,
|
|
116
|
+
cwd: meta?.cwd,
|
|
117
|
+
})
|
|
118
|
+
} catch {
|
|
119
|
+
// index 更新 best-effort
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 从磁盘重开一个 SessionLog(不存在返回空 log)。旧格式快照自动迁移为事件日志。 */
|
|
124
|
+
static loadLog(name: string): SessionLog {
|
|
125
|
+
const path = filePath(name)
|
|
126
|
+
if (!existsSync(path)) return SessionLog.open(name)
|
|
127
|
+
try {
|
|
128
|
+
const raw = readFileSync(path, 'utf-8')
|
|
129
|
+
const parsed = JSON.parse(raw) as unknown
|
|
130
|
+
if (
|
|
131
|
+
parsed &&
|
|
132
|
+
typeof parsed === 'object' &&
|
|
133
|
+
'metadata' in parsed &&
|
|
134
|
+
Array.isArray((parsed as { messages?: unknown }).messages)
|
|
135
|
+
) {
|
|
136
|
+
// 旧格式快照 → 迁移为事件日志(重写文件,避免后续 append 混格式)
|
|
137
|
+
const old = parsed as StoredSession
|
|
138
|
+
const log = new SessionLog(name)
|
|
139
|
+
log.append({
|
|
140
|
+
type: 'session/start',
|
|
141
|
+
at: Date.parse(old.metadata.createdAt) || Date.now(),
|
|
142
|
+
sessionId: name,
|
|
143
|
+
provider: old.metadata.provider,
|
|
144
|
+
model: old.metadata.model,
|
|
145
|
+
cwd: old.metadata.cwd,
|
|
146
|
+
})
|
|
147
|
+
for (const m of old.messages) for (const e of messageToEvents(m)) log.append(e)
|
|
148
|
+
unlinkSync(path)
|
|
149
|
+
log.save()
|
|
150
|
+
return log
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// 非旧格式(多行 JSONL 或单事件)→ 走 open
|
|
154
|
+
}
|
|
155
|
+
return SessionLog.open(name)
|
|
156
|
+
}
|
|
157
|
+
|
|
104
158
|
/**
|
|
105
159
|
* Load a saved session. Returns null if not found or unparseable.
|
|
106
160
|
*/
|
|
@@ -110,14 +164,48 @@ export class SessionStore {
|
|
|
110
164
|
|
|
111
165
|
try {
|
|
112
166
|
const raw = readFileSync(path, 'utf-8')
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
|
|
167
|
+
// 旧格式:单 JSON 对象 {metadata, messages}
|
|
168
|
+
const parsed = JSON.parse(raw) as unknown
|
|
169
|
+
if (
|
|
170
|
+
parsed &&
|
|
171
|
+
typeof parsed === 'object' &&
|
|
172
|
+
'metadata' in parsed &&
|
|
173
|
+
Array.isArray((parsed as { messages?: unknown }).messages)
|
|
174
|
+
) {
|
|
175
|
+
return parsed as StoredSession
|
|
117
176
|
}
|
|
118
|
-
return session
|
|
119
177
|
} catch {
|
|
120
|
-
|
|
178
|
+
// 多行 JSONL → 新格式,走事件解析
|
|
179
|
+
}
|
|
180
|
+
return SessionStore.logToStoredSession(name, SessionLog.open(name))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private static logToStoredSession(name: string, log: SessionLog): StoredSession | null {
|
|
184
|
+
const events = log.events()
|
|
185
|
+
if (events.length === 0) return null
|
|
186
|
+
const start = events.find((e) => e.type === 'session/start') as
|
|
187
|
+
| {
|
|
188
|
+
type: 'session/start'
|
|
189
|
+
at: number
|
|
190
|
+
sessionId: string
|
|
191
|
+
provider?: string
|
|
192
|
+
model?: string
|
|
193
|
+
cwd?: string
|
|
194
|
+
}
|
|
195
|
+
| undefined
|
|
196
|
+
const messages = deriveMessages(events)
|
|
197
|
+
const stat = statSync(filePath(name))
|
|
198
|
+
return {
|
|
199
|
+
metadata: {
|
|
200
|
+
name,
|
|
201
|
+
createdAt: start?.at ? new Date(start.at).toISOString() : stat.mtime.toISOString(),
|
|
202
|
+
updatedAt: stat.mtime.toISOString(),
|
|
203
|
+
provider: start?.provider || 'unknown',
|
|
204
|
+
model: start?.model || 'unknown',
|
|
205
|
+
messageCount: messages.length,
|
|
206
|
+
cwd: start?.cwd,
|
|
207
|
+
},
|
|
208
|
+
messages,
|
|
121
209
|
}
|
|
122
210
|
}
|
|
123
211
|
|
|
@@ -128,20 +216,14 @@ export class SessionStore {
|
|
|
128
216
|
ensureDir()
|
|
129
217
|
try {
|
|
130
218
|
const files = readdirSync(SESSIONS_DIR).filter((f) => f.endsWith('.jsonl'))
|
|
131
|
-
|
|
132
219
|
const sessions: SessionMetadata[] = []
|
|
133
220
|
for (const file of files) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
sessions.push(session.metadata)
|
|
139
|
-
}
|
|
140
|
-
} catch {
|
|
141
|
-
// Skip corrupt files
|
|
221
|
+
const name = file.replace('.jsonl', '')
|
|
222
|
+
const session = SessionStore.load(name)
|
|
223
|
+
if (session?.metadata) {
|
|
224
|
+
sessions.push(session.metadata)
|
|
142
225
|
}
|
|
143
226
|
}
|
|
144
|
-
|
|
145
227
|
sessions.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
|
|
146
228
|
return sessions
|
|
147
229
|
} catch {
|
|
@@ -203,7 +285,7 @@ export class SessionStore {
|
|
|
203
285
|
}
|
|
204
286
|
}
|
|
205
287
|
|
|
206
|
-
writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2), 'utf-8')
|
|
288
|
+
writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2), { encoding: 'utf-8', mode: 0o600 })
|
|
207
289
|
}
|
|
208
290
|
|
|
209
291
|
/**
|
|
@@ -231,7 +313,7 @@ export class SessionStore {
|
|
|
231
313
|
} else {
|
|
232
314
|
index.push(entry)
|
|
233
315
|
}
|
|
234
|
-
writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2), 'utf-8')
|
|
316
|
+
writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2), { encoding: 'utf-8', mode: 0o600 })
|
|
235
317
|
}
|
|
236
318
|
|
|
237
319
|
/**
|
|
@@ -240,7 +322,7 @@ export class SessionStore {
|
|
|
240
322
|
*/
|
|
241
323
|
static saveSummary(name: string, summary: string, tags: string[]): void {
|
|
242
324
|
ensureDir()
|
|
243
|
-
mkdirSync(SUMMARIES_DIR, { recursive: true })
|
|
325
|
+
mkdirSync(SUMMARIES_DIR, { recursive: true, mode: 0o700 })
|
|
244
326
|
|
|
245
327
|
const safeRaw = name.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
246
328
|
const safe =
|
|
@@ -248,7 +330,10 @@ export class SessionStore {
|
|
|
248
330
|
? `${safeRaw.slice(0, 80)}-${createHash('sha256').update(safeRaw).digest('hex').slice(0, 16)}`
|
|
249
331
|
: safeRaw
|
|
250
332
|
const summaryPath = join(SUMMARIES_DIR, `${safe}.md`)
|
|
251
|
-
writeFileSync(summaryPath, `# ${name}\n\n${summary}\n\nTags: ${tags.join(', ')}\n`,
|
|
333
|
+
writeFileSync(summaryPath, `# ${name}\n\n${summary}\n\nTags: ${tags.join(', ')}\n`, {
|
|
334
|
+
encoding: 'utf-8',
|
|
335
|
+
mode: 0o600,
|
|
336
|
+
})
|
|
252
337
|
|
|
253
338
|
// Update index entry — create minimal one if not present
|
|
254
339
|
try {
|
|
@@ -270,7 +355,7 @@ export class SessionStore {
|
|
|
270
355
|
tags,
|
|
271
356
|
})
|
|
272
357
|
}
|
|
273
|
-
writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2), 'utf-8')
|
|
358
|
+
writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2), { encoding: 'utf-8', mode: 0o600 })
|
|
274
359
|
} catch {
|
|
275
360
|
// Index write is best-effort
|
|
276
361
|
}
|
|
@@ -25,6 +25,26 @@ function realPath(dir: string): string {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* True if a `.git` file or directory exists anywhere strictly between `dir` and
|
|
30
|
+
* its trusted ancestor `ancestor`. A `.git` marks the root of a separate
|
|
31
|
+
* repository — trust does not cross that boundary (nested-repo isolation).
|
|
32
|
+
*/
|
|
33
|
+
function hasGitBoundary(dir: string, ancestor: string): boolean {
|
|
34
|
+
let cur = dir
|
|
35
|
+
// Case-insensitive termination matches isTrusted's `toLowerCase()` ancestor
|
|
36
|
+
// check — otherwise a path referenced with different casing than when it was
|
|
37
|
+
// trusted would walk past the ancestor and miss the boundary.
|
|
38
|
+
const targetLower = ancestor.toLowerCase()
|
|
39
|
+
while (cur.toLowerCase() !== targetLower && cur.length > ancestor.length) {
|
|
40
|
+
if (existsSync(join(cur, '.git'))) return true
|
|
41
|
+
const parent = dirname(cur)
|
|
42
|
+
if (parent === cur) break
|
|
43
|
+
cur = parent
|
|
44
|
+
}
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
/**
|
|
29
49
|
* Manages the workspace trust store.
|
|
30
50
|
*
|
|
@@ -82,8 +102,11 @@ export class WorkspaceTrust {
|
|
|
82
102
|
const trustedLower = trusted.toLowerCase()
|
|
83
103
|
// Exact match
|
|
84
104
|
if (resolvedLower === trustedLower) return true
|
|
85
|
-
// Ancestor match: trusted dir is a parent of the target
|
|
86
|
-
|
|
105
|
+
// Ancestor match: trusted dir is a parent of the target. Trust does not
|
|
106
|
+
// cross a nested git-repository boundary (vendored/submodule isolation).
|
|
107
|
+
if (resolvedLower.startsWith(trustedLower + '/') && !hasGitBoundary(resolved, trusted)) {
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
87
110
|
}
|
|
88
111
|
|
|
89
112
|
return false
|
package/src/daemon/auth.ts
CHANGED
|
@@ -73,16 +73,19 @@ export function listTokens(tokenPath: string): string[] {
|
|
|
73
73
|
* trusted and auth is bypassed. Auth enforcement activates when
|
|
74
74
|
* MIPHAM_BIND=0.0.0.0 for remote access.
|
|
75
75
|
*/
|
|
76
|
-
export function authMiddleware(
|
|
76
|
+
export function authMiddleware(
|
|
77
|
+
request: Request,
|
|
78
|
+
validToken: string,
|
|
79
|
+
peerAddress: string | undefined,
|
|
80
|
+
): Response | null {
|
|
77
81
|
// Allow health endpoint without auth
|
|
78
82
|
const url = new URL(request.url)
|
|
79
83
|
if (url.pathname === '/api/v1/health') return null
|
|
80
84
|
|
|
81
|
-
//
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
85
|
+
// Trust only actual loopback connections (socket IP), never the spoofable Host header.
|
|
86
|
+
const isLoopback =
|
|
87
|
+
peerAddress === '127.0.0.1' || peerAddress === '::1' || peerAddress === '::ffff:127.0.0.1'
|
|
88
|
+
if (isLoopback) return null
|
|
86
89
|
|
|
87
90
|
const auth = request.headers.get('authorization')
|
|
88
91
|
if (!auth || !auth.startsWith('Bearer ')) {
|
package/src/daemon/cors.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// apps/cli/src/daemon/cors.ts
|
|
2
2
|
//
|
|
3
3
|
// CORS middleware for the daemon HTTP API.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// Only origins explicitly listed in MIPHAM_CORS_ORIGINS (comma-separated) are
|
|
5
|
+
// allowed cross-origin access. By default (empty) no external origin is allowed,
|
|
6
|
+
// so a malicious web page cannot read daemon responses cross-origin.
|
|
6
7
|
|
|
7
|
-
const ALLOWED_HEADERS = '
|
|
8
|
+
const ALLOWED_HEADERS = 'Content-Type'
|
|
8
9
|
const ALLOWED_METHODS = 'GET, POST, PATCH, DELETE, OPTIONS'
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -14,24 +15,31 @@ export function isLocalhostOrigin(origin: string): boolean {
|
|
|
14
15
|
return origin.includes('localhost') || origin.includes('127.0.0.1') || origin.includes('[::1]')
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
function getAllowedOrigins(): string[] {
|
|
19
|
+
const raw = process.env.MIPHAM_CORS_ORIGINS || ''
|
|
20
|
+
return raw
|
|
21
|
+
.split(',')
|
|
22
|
+
.map((s) => s.trim())
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isAllowedOrigin(origin: string): boolean {
|
|
27
|
+
return getAllowedOrigins().includes(origin)
|
|
28
|
+
}
|
|
29
|
+
|
|
17
30
|
/**
|
|
18
31
|
* Handle CORS preflight (OPTIONS) requests.
|
|
19
32
|
*
|
|
20
|
-
* Returns a Response with
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* For non-preflight requests with an external Origin, the caller should
|
|
25
|
-
* use `addCorsHeaders` to attach CORS headers to the response.
|
|
33
|
+
* Returns a Response with CORS headers only for explicitly allow-listed external
|
|
34
|
+
* origins. Localhost origins need no CORS; unlisted external origins get no CORS
|
|
35
|
+
* headers (the browser blocks the cross-origin read).
|
|
26
36
|
*/
|
|
27
37
|
export function corsMiddleware(request: Request): Response | null {
|
|
28
38
|
const origin = request.headers.get('origin')
|
|
29
39
|
if (!origin) return null
|
|
30
40
|
|
|
31
|
-
|
|
32
|
-
if (isLocalhostOrigin(origin)) return null
|
|
41
|
+
if (isLocalhostOrigin(origin) || !isAllowedOrigin(origin)) return null
|
|
33
42
|
|
|
34
|
-
// Handle preflight
|
|
35
43
|
if (request.method === 'OPTIONS') {
|
|
36
44
|
return new Response(null, {
|
|
37
45
|
status: 204,
|
|
@@ -39,7 +47,7 @@ export function corsMiddleware(request: Request): Response | null {
|
|
|
39
47
|
'Access-Control-Allow-Origin': origin,
|
|
40
48
|
'Access-Control-Allow-Methods': ALLOWED_METHODS,
|
|
41
49
|
'Access-Control-Allow-Headers': ALLOWED_HEADERS,
|
|
42
|
-
'Access-Control-Max-Age': '
|
|
50
|
+
'Access-Control-Max-Age': '3600',
|
|
43
51
|
},
|
|
44
52
|
})
|
|
45
53
|
}
|
|
@@ -48,12 +56,12 @@ export function corsMiddleware(request: Request): Response | null {
|
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
/**
|
|
51
|
-
* Add CORS headers to a response
|
|
52
|
-
*
|
|
59
|
+
* Add CORS headers to a response only for explicitly allow-listed external origins.
|
|
60
|
+
* Localhost / absent / unlisted origins return the response unchanged.
|
|
53
61
|
*/
|
|
54
62
|
export function addCorsHeaders(response: Response, request: Request): Response {
|
|
55
63
|
const origin = request.headers.get('origin')
|
|
56
|
-
if (!origin || isLocalhostOrigin(origin)) return response
|
|
64
|
+
if (!origin || isLocalhostOrigin(origin) || !isAllowedOrigin(origin)) return response
|
|
57
65
|
|
|
58
66
|
const headers = new Headers(response.headers)
|
|
59
67
|
headers.set('Access-Control-Allow-Origin', origin)
|
|
@@ -30,6 +30,10 @@ export class RateLimiter {
|
|
|
30
30
|
*/
|
|
31
31
|
check(key: string): { allowed: boolean; remaining: number; resetAt: number } {
|
|
32
32
|
const now = Date.now()
|
|
33
|
+
// Bound the map size to prevent unbounded growth under a flood of distinct keys.
|
|
34
|
+
if (this.windows.size >= 100_000 && !this.windows.has(key)) {
|
|
35
|
+
this.cleanup()
|
|
36
|
+
}
|
|
33
37
|
let entry = this.windows.get(key)
|
|
34
38
|
|
|
35
39
|
if (!entry || now >= entry.resetAt) {
|
package/src/daemon/server.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { bootstrapProviders } from '../providers/bootstrap'
|
|
|
21
21
|
import { createToolRegistry } from '../tools'
|
|
22
22
|
import { PermissionSystem } from '../core/permission'
|
|
23
23
|
import type { ProviderRegistry } from '../providers/registry'
|
|
24
|
-
import type { ToolDefinition } from '../shared/types'
|
|
24
|
+
import type { ToolDefinition, PermissionMode } from '../shared/types'
|
|
25
25
|
|
|
26
26
|
interface ServerConfig {
|
|
27
27
|
db: DaemonDatabase
|
|
@@ -42,6 +42,27 @@ interface WsData {
|
|
|
42
42
|
sessionId: string
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
const DAEMON_PERMISSION_MODES: ReadonlySet<PermissionMode> = new Set<PermissionMode>([
|
|
46
|
+
'default',
|
|
47
|
+
'acceptEdits',
|
|
48
|
+
'plan',
|
|
49
|
+
'auto',
|
|
50
|
+
'dontAsk',
|
|
51
|
+
'bypassPermissions',
|
|
52
|
+
])
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the daemon's permission mode from env, defaulting to least-privilege
|
|
56
|
+
* 'default'. Headless daemon sessions can never prompt, so 'ask'-level tools
|
|
57
|
+
* (Bash/Write/Edit) are blocked rather than auto-approved.
|
|
58
|
+
*/
|
|
59
|
+
function resolveDaemonPermission(): PermissionMode {
|
|
60
|
+
const fromEnv = process.env.MIPHAM_DAEMON_PERMISSION
|
|
61
|
+
return fromEnv && DAEMON_PERMISSION_MODES.has(fromEnv as PermissionMode)
|
|
62
|
+
? (fromEnv as PermissionMode)
|
|
63
|
+
: 'default'
|
|
64
|
+
}
|
|
65
|
+
|
|
45
66
|
export function createServer(config: ServerConfig): Server<WsData> {
|
|
46
67
|
const {
|
|
47
68
|
db,
|
|
@@ -142,7 +163,7 @@ export function createServer(config: ServerConfig): Server<WsData> {
|
|
|
142
163
|
}
|
|
143
164
|
}
|
|
144
165
|
|
|
145
|
-
const permission = new PermissionSystem(
|
|
166
|
+
const permission = new PermissionSystem(resolveDaemonPermission())
|
|
146
167
|
const engine = new QueryEngine(sharedRegistry, context, sharedTools, permission)
|
|
147
168
|
engine.setSessionId(sessionId)
|
|
148
169
|
engineCache.set(sessionId, engine)
|
|
@@ -201,7 +222,7 @@ export function createServer(config: ServerConfig): Server<WsData> {
|
|
|
201
222
|
}
|
|
202
223
|
|
|
203
224
|
// ── Auth check ──────────────────────────────────
|
|
204
|
-
const authError = authMiddleware(req, token)
|
|
225
|
+
const authError = authMiddleware(req, token, server.requestIP(req)?.address)
|
|
205
226
|
if (authError) return addCorsHeaders(authError, req)
|
|
206
227
|
|
|
207
228
|
// Helper: create JSON response with CORS headers for external origins
|
|
@@ -682,6 +682,10 @@
|
|
|
682
682
|
"count": "{shown} of {total}",
|
|
683
683
|
"all": "all"
|
|
684
684
|
},
|
|
685
|
+
"mention_hints": {
|
|
686
|
+
"label": "Sessions:",
|
|
687
|
+
"empty": "No active sessions"
|
|
688
|
+
},
|
|
685
689
|
"picker": {
|
|
686
690
|
"title": "Select Model",
|
|
687
691
|
"nav_help": "←→ navigate · ↑↓ move · Enter select · Esc close",
|
package/src/index.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { join } from 'node:path'
|
|
2
|
+
import { join, basename } from 'node:path'
|
|
3
3
|
import { homedir } from 'node:os'
|
|
4
4
|
import { existsSync } from 'node:fs'
|
|
5
5
|
import { render } from 'ink'
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
heartbeatSession,
|
|
17
17
|
unregisterSession,
|
|
18
18
|
createSessionInfo,
|
|
19
|
+
ensureUniqueSessionName,
|
|
20
|
+
discoverSessions,
|
|
19
21
|
} from './agent/cross-session/discovery'
|
|
20
22
|
import { bootstrapProviders } from './providers/bootstrap'
|
|
21
23
|
import { InstructionsLoader } from './core/instructions'
|
|
@@ -24,12 +26,16 @@ import { ContextManager } from './core/context'
|
|
|
24
26
|
import { PrefixCacheTracker } from './core/context-token'
|
|
25
27
|
import { QueryEngine } from './core/engine'
|
|
26
28
|
import { ExperienceRuleEngine } from './core/rule-engine.js'
|
|
29
|
+
import { SessionLog } from './core/session-log'
|
|
27
30
|
import { SessionStore } from './core/session-store'
|
|
28
31
|
import type { PermissionLevel, MiphamConfig } from './shared/types'
|
|
29
32
|
import { SkillsLoader } from './skills/loader'
|
|
30
33
|
import { PluginManager } from './plugin/plugin-manager'
|
|
31
34
|
import { loadPlugins } from './plugin/plugin-loader'
|
|
32
35
|
import { createToolRegistry } from './tools'
|
|
36
|
+
import { Context } from './vajra'
|
|
37
|
+
import { mountSkills, SKILLS_KEY } from './skills/seam'
|
|
38
|
+
import { mountLlm, LLM_KEY } from './providers/llm'
|
|
33
39
|
import { McpClient } from './mcp/client'
|
|
34
40
|
import { registerMcpServerTools } from './mcp/registry'
|
|
35
41
|
import { AgentRegistry } from './agent/agent-registry'
|
|
@@ -326,6 +332,16 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
326
332
|
compactionThreshold: 0.9,
|
|
327
333
|
contextWindow: adaptiveThresholds ? modelContextWindow : undefined,
|
|
328
334
|
})
|
|
335
|
+
const sessionLog = new SessionLog(sessionName)
|
|
336
|
+
sessionLog.append({
|
|
337
|
+
type: 'session/start',
|
|
338
|
+
at: Date.now(),
|
|
339
|
+
sessionId: sessionName,
|
|
340
|
+
provider: defaultProvider,
|
|
341
|
+
model: defaultModel,
|
|
342
|
+
cwd: process.cwd(),
|
|
343
|
+
})
|
|
344
|
+
context.setLog(sessionLog)
|
|
329
345
|
// Cache-aware microcompaction: track the provider's prompt-cache prefix.
|
|
330
346
|
context.setCacheTracker(new PrefixCacheTracker())
|
|
331
347
|
|
|
@@ -333,19 +349,19 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
333
349
|
getMemoryManager().setContextWindow(modelContextWindow)
|
|
334
350
|
|
|
335
351
|
if (options.resume) {
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
352
|
+
const log = SessionStore.loadLog(options.resume)
|
|
353
|
+
const events = log.events()
|
|
354
|
+
if (events.length > 0) {
|
|
355
|
+
const start = events.find((e) => e.type === 'session/start') as
|
|
356
|
+
{ type: 'session/start'; cwd?: string } | undefined
|
|
357
|
+
if (start?.cwd && existsSync(start.cwd)) {
|
|
340
358
|
try {
|
|
341
|
-
process.chdir(
|
|
359
|
+
process.chdir(start.cwd)
|
|
342
360
|
} catch {
|
|
343
|
-
// cwd
|
|
361
|
+
// cwd 可能已不存在
|
|
344
362
|
}
|
|
345
363
|
}
|
|
346
|
-
|
|
347
|
-
context.addMessage(msg)
|
|
348
|
-
}
|
|
364
|
+
context.restoreLog(log)
|
|
349
365
|
context.setSystemPrompt(instructions.buildSystemPrompt(config.permission as string))
|
|
350
366
|
}
|
|
351
367
|
}
|
|
@@ -421,8 +437,21 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
421
437
|
context.setSystemPrompt(prompt)
|
|
422
438
|
}
|
|
423
439
|
|
|
424
|
-
//
|
|
425
|
-
const
|
|
440
|
+
// Initialize credential masking pipeline (strategies: Full, Extract, JWT)
|
|
441
|
+
const { initializePipeline } = await import('./core/credential-masker/index')
|
|
442
|
+
initializePipeline()
|
|
443
|
+
|
|
444
|
+
// Load credential masking config and inject it into the tool seam
|
|
445
|
+
const credentialMaskingConfig = loadCredentialMaskingConfig()
|
|
446
|
+
const vajraContext = new Context()
|
|
447
|
+
vajraContext.provide('credentials', credentialMaskingConfig)
|
|
448
|
+
|
|
449
|
+
// Create tool registry with all built-in tools (mounted as Vajra services)
|
|
450
|
+
const tools = createToolRegistry(vajraContext)
|
|
451
|
+
|
|
452
|
+
// 绞杀收官:skills + llm 也挂载到同一 Vajra Context(tools 已接)
|
|
453
|
+
mountSkills(vajraContext, skillsLoader)
|
|
454
|
+
mountLlm(vajraContext, registry)
|
|
426
455
|
|
|
427
456
|
// Connect MCP servers and register their tools into the tool registry.
|
|
428
457
|
// Uses Promise.allSettled for parallel connection — failures are non-fatal.
|
|
@@ -469,23 +498,13 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
469
498
|
engine.setHookEngine(hookEngine)
|
|
470
499
|
engine.setArtifactServer(artifactServer)
|
|
471
500
|
engine.setAgentViewManager(agentViewManager)
|
|
472
|
-
engine.
|
|
501
|
+
engine.setSkills(vajraContext.get(SKILLS_KEY)!)
|
|
502
|
+
engine.setLlm(vajraContext.get(LLM_KEY)!)
|
|
473
503
|
|
|
474
504
|
// Wire inference hooks (DLP) configuration
|
|
475
505
|
const inferenceHookConfig = loadInferenceHookConfig()
|
|
476
506
|
engine.setInferenceHookConfig(inferenceHookConfig)
|
|
477
507
|
|
|
478
|
-
// Initialize credential masking pipeline (strategies: Full, Extract, JWT)
|
|
479
|
-
const { initializePipeline } = await import('./core/credential-masker/index')
|
|
480
|
-
initializePipeline()
|
|
481
|
-
|
|
482
|
-
// Wire credential masking configuration into tools
|
|
483
|
-
const credentialMaskingConfig = loadCredentialMaskingConfig()
|
|
484
|
-
const { setCredentialMaskingConfigForRead } = await import('./tools/file/read')
|
|
485
|
-
const { setCredentialMaskingConfigForBash } = await import('./tools/exec/bash')
|
|
486
|
-
setCredentialMaskingConfigForRead(credentialMaskingConfig)
|
|
487
|
-
setCredentialMaskingConfigForBash(credentialMaskingConfig)
|
|
488
|
-
|
|
489
508
|
// Sync engine permission with config (fix: UI shows "auto" but engine defaulted to bypass-legacy)
|
|
490
509
|
if (config.permission) {
|
|
491
510
|
engine.getPermission().setDefaultLevel(config.permission as PermissionLevel)
|
|
@@ -515,9 +534,13 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
515
534
|
|
|
516
535
|
// ── Cross-session messaging: register session, heartbeat, shutdown ──
|
|
517
536
|
engine.setSessionId(sessionName)
|
|
537
|
+
// Human-readable, cross-session-addressable name. Defaults to the cwd basename
|
|
538
|
+
// so `@mipham-code` can address this session; uniqueness is enforced against
|
|
539
|
+
// other live sessions (suffix -2, -3, …). The id stays a stable `session-<ts>`.
|
|
540
|
+
const defaultName = basename(process.cwd()) || 'session'
|
|
518
541
|
const sessionInfo = createSessionInfo(
|
|
519
542
|
sessionName,
|
|
520
|
-
|
|
543
|
+
ensureUniqueSessionName(defaultName, discoverSessions()),
|
|
521
544
|
process.cwd(),
|
|
522
545
|
defaultProvider,
|
|
523
546
|
defaultModel,
|
|
@@ -553,11 +576,20 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
553
576
|
hookEngine.executeSessionEnd(sessionName).catch(() => {})
|
|
554
577
|
artifactServer.stop()
|
|
555
578
|
if (context.getMessageCount() > 0) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
579
|
+
const log = context.getLog()
|
|
580
|
+
if (log) {
|
|
581
|
+
SessionStore.saveLog(sessionName, log, {
|
|
582
|
+
provider: defaultProvider,
|
|
583
|
+
model: defaultModel,
|
|
584
|
+
cwd: process.cwd(),
|
|
585
|
+
})
|
|
586
|
+
} else {
|
|
587
|
+
SessionStore.save(sessionName, context.getMessages(), {
|
|
588
|
+
provider: defaultProvider,
|
|
589
|
+
model: defaultModel,
|
|
590
|
+
cwd: process.cwd(),
|
|
591
|
+
})
|
|
592
|
+
}
|
|
561
593
|
}
|
|
562
594
|
process.exit(0)
|
|
563
595
|
}
|