@miphamai/cli 0.38.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/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 +50 -33
- package/src/core/instructions.ts +4 -42
- package/src/core/permission.ts +7 -1
- package/src/core/red-team.ts +4 -4
- 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/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/index.tsx +54 -28
- 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 +140 -128
- 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/commands.ts +17 -5
- 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
|
}
|
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
|
package/src/index.tsx
CHANGED
|
@@ -26,12 +26,16 @@ import { ContextManager } from './core/context'
|
|
|
26
26
|
import { PrefixCacheTracker } from './core/context-token'
|
|
27
27
|
import { QueryEngine } from './core/engine'
|
|
28
28
|
import { ExperienceRuleEngine } from './core/rule-engine.js'
|
|
29
|
+
import { SessionLog } from './core/session-log'
|
|
29
30
|
import { SessionStore } from './core/session-store'
|
|
30
31
|
import type { PermissionLevel, MiphamConfig } from './shared/types'
|
|
31
32
|
import { SkillsLoader } from './skills/loader'
|
|
32
33
|
import { PluginManager } from './plugin/plugin-manager'
|
|
33
34
|
import { loadPlugins } from './plugin/plugin-loader'
|
|
34
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'
|
|
35
39
|
import { McpClient } from './mcp/client'
|
|
36
40
|
import { registerMcpServerTools } from './mcp/registry'
|
|
37
41
|
import { AgentRegistry } from './agent/agent-registry'
|
|
@@ -328,6 +332,16 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
328
332
|
compactionThreshold: 0.9,
|
|
329
333
|
contextWindow: adaptiveThresholds ? modelContextWindow : undefined,
|
|
330
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)
|
|
331
345
|
// Cache-aware microcompaction: track the provider's prompt-cache prefix.
|
|
332
346
|
context.setCacheTracker(new PrefixCacheTracker())
|
|
333
347
|
|
|
@@ -335,19 +349,19 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
335
349
|
getMemoryManager().setContextWindow(modelContextWindow)
|
|
336
350
|
|
|
337
351
|
if (options.resume) {
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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)) {
|
|
342
358
|
try {
|
|
343
|
-
process.chdir(
|
|
359
|
+
process.chdir(start.cwd)
|
|
344
360
|
} catch {
|
|
345
|
-
// cwd
|
|
361
|
+
// cwd 可能已不存在
|
|
346
362
|
}
|
|
347
363
|
}
|
|
348
|
-
|
|
349
|
-
context.addMessage(msg)
|
|
350
|
-
}
|
|
364
|
+
context.restoreLog(log)
|
|
351
365
|
context.setSystemPrompt(instructions.buildSystemPrompt(config.permission as string))
|
|
352
366
|
}
|
|
353
367
|
}
|
|
@@ -423,8 +437,21 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
423
437
|
context.setSystemPrompt(prompt)
|
|
424
438
|
}
|
|
425
439
|
|
|
426
|
-
//
|
|
427
|
-
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)
|
|
428
455
|
|
|
429
456
|
// Connect MCP servers and register their tools into the tool registry.
|
|
430
457
|
// Uses Promise.allSettled for parallel connection — failures are non-fatal.
|
|
@@ -471,23 +498,13 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
471
498
|
engine.setHookEngine(hookEngine)
|
|
472
499
|
engine.setArtifactServer(artifactServer)
|
|
473
500
|
engine.setAgentViewManager(agentViewManager)
|
|
474
|
-
engine.
|
|
501
|
+
engine.setSkills(vajraContext.get(SKILLS_KEY)!)
|
|
502
|
+
engine.setLlm(vajraContext.get(LLM_KEY)!)
|
|
475
503
|
|
|
476
504
|
// Wire inference hooks (DLP) configuration
|
|
477
505
|
const inferenceHookConfig = loadInferenceHookConfig()
|
|
478
506
|
engine.setInferenceHookConfig(inferenceHookConfig)
|
|
479
507
|
|
|
480
|
-
// Initialize credential masking pipeline (strategies: Full, Extract, JWT)
|
|
481
|
-
const { initializePipeline } = await import('./core/credential-masker/index')
|
|
482
|
-
initializePipeline()
|
|
483
|
-
|
|
484
|
-
// Wire credential masking configuration into tools
|
|
485
|
-
const credentialMaskingConfig = loadCredentialMaskingConfig()
|
|
486
|
-
const { setCredentialMaskingConfigForRead } = await import('./tools/file/read')
|
|
487
|
-
const { setCredentialMaskingConfigForBash } = await import('./tools/exec/bash')
|
|
488
|
-
setCredentialMaskingConfigForRead(credentialMaskingConfig)
|
|
489
|
-
setCredentialMaskingConfigForBash(credentialMaskingConfig)
|
|
490
|
-
|
|
491
508
|
// Sync engine permission with config (fix: UI shows "auto" but engine defaulted to bypass-legacy)
|
|
492
509
|
if (config.permission) {
|
|
493
510
|
engine.getPermission().setDefaultLevel(config.permission as PermissionLevel)
|
|
@@ -559,11 +576,20 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
559
576
|
hookEngine.executeSessionEnd(sessionName).catch(() => {})
|
|
560
577
|
artifactServer.stop()
|
|
561
578
|
if (context.getMessageCount() > 0) {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
+
}
|
|
567
593
|
}
|
|
568
594
|
process.exit(0)
|
|
569
595
|
}
|
|
@@ -119,6 +119,7 @@ export class HttpTransport implements Transport {
|
|
|
119
119
|
},
|
|
120
120
|
body: JSON.stringify(request),
|
|
121
121
|
signal: controller.signal,
|
|
122
|
+
redirect: 'manual',
|
|
122
123
|
})
|
|
123
124
|
|
|
124
125
|
if (!response.ok) {
|
|
@@ -164,6 +165,7 @@ export class HttpTransport implements Transport {
|
|
|
164
165
|
...this.headers,
|
|
165
166
|
},
|
|
166
167
|
body: JSON.stringify(notification),
|
|
168
|
+
redirect: 'manual',
|
|
167
169
|
}).catch(() => {
|
|
168
170
|
/* notifications are best-effort */
|
|
169
171
|
})
|
package/src/mcp/oauth.ts
CHANGED
|
@@ -68,7 +68,7 @@ export class OAuthClient {
|
|
|
68
68
|
resolve(receivedCode)
|
|
69
69
|
}
|
|
70
70
|
})
|
|
71
|
-
server.listen(port, () => {
|
|
71
|
+
server.listen(port, '127.0.0.1', () => {
|
|
72
72
|
const authUrl = new URL(auth.authorizationUrl)
|
|
73
73
|
authUrl.searchParams.set('response_type', 'code')
|
|
74
74
|
authUrl.searchParams.set('client_id', auth.clientId)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Llm } from './llm'
|
|
2
|
+
import type { ChatRequest } from './registry'
|
|
3
|
+
import type { StreamChunk } from '../shared'
|
|
4
|
+
|
|
5
|
+
export interface RecordedTurn {
|
|
6
|
+
req: ChatRequest
|
|
7
|
+
chunks: StreamChunk[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** 包装一个 Llm:委托的同时记录每轮 (req, chunks)。 */
|
|
11
|
+
export function recordLlm(inner: Llm): { llm: Llm; turns: RecordedTurn[] } {
|
|
12
|
+
const turns: RecordedTurn[] = []
|
|
13
|
+
const llm: Llm = {
|
|
14
|
+
async *chat(req) {
|
|
15
|
+
const chunks: StreamChunk[] = []
|
|
16
|
+
for await (const chunk of inner.chat(req)) {
|
|
17
|
+
chunks.push(chunk)
|
|
18
|
+
yield chunk
|
|
19
|
+
}
|
|
20
|
+
turns.push({ req, chunks })
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
return { llm, turns }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 回放已记录的 turns(确定性、无网络)。按顺序消费,超出则静默结束。 */
|
|
27
|
+
export function replayLlm(turns: RecordedTurn[]): Llm {
|
|
28
|
+
let cursor = 0
|
|
29
|
+
return {
|
|
30
|
+
async *chat() {
|
|
31
|
+
const turn = turns[cursor++]
|
|
32
|
+
if (!turn) return
|
|
33
|
+
for (const chunk of turn.chunks) yield chunk
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Context, Disposer } from '../vajra'
|
|
2
|
+
import type { ChatRequest } from './registry'
|
|
3
|
+
import type { StreamChunk } from '../shared'
|
|
4
|
+
|
|
5
|
+
/** LLM 适配缝的 capability —— chat 能力。换 provider = 换 chat 实现,engine 零 fork 跟随。 */
|
|
6
|
+
export interface Llm {
|
|
7
|
+
chat(req: ChatRequest): AsyncGenerator<StreamChunk>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** 缝键:ctx.llm。 */
|
|
11
|
+
export const LLM_KEY = 'llm'
|
|
12
|
+
|
|
13
|
+
/** 把一个 Llm(如 ProviderRegistry 或 llm-replay)挂载为 ctx.llm。 */
|
|
14
|
+
export function mountLlm(ctx: Context, llm: Llm): Disposer {
|
|
15
|
+
return ctx.provide(LLM_KEY, llm)
|
|
16
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ProviderConfig, ModelInfo, Message, StreamChunk } from '../shared/index.ts'
|
|
2
|
+
import type { Llm } from './llm'
|
|
2
3
|
|
|
3
4
|
export interface ProviderInstance {
|
|
4
5
|
config: ProviderConfig
|
|
@@ -17,7 +18,7 @@ export interface ChatRequest {
|
|
|
17
18
|
signal?: AbortSignal
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
export class ProviderRegistry {
|
|
21
|
+
export class ProviderRegistry implements Llm {
|
|
21
22
|
private providers = new Map<string, ProviderInstance>()
|
|
22
23
|
private activeProviderId: string
|
|
23
24
|
private activeModelId: string
|
package/src/security/gate.ts
CHANGED
|
@@ -90,4 +90,14 @@ export class SecurityGate {
|
|
|
90
90
|
}
|
|
91
91
|
return { blocked: false }
|
|
92
92
|
}
|
|
93
|
+
|
|
94
|
+
/** Redact known API-key-shaped tokens from output (defense-in-depth beyond config patterns). */
|
|
95
|
+
static redactCredentialLeak(output: string): string {
|
|
96
|
+
if (!this.checkCredentialLeak(output).blocked) return output
|
|
97
|
+
let out = output
|
|
98
|
+
for (const { regex } of API_KEY_PATTERNS) {
|
|
99
|
+
out = out.replace(new RegExp(regex.source, 'gi'), '[REDACTED]')
|
|
100
|
+
}
|
|
101
|
+
return out
|
|
102
|
+
}
|
|
93
103
|
}
|