@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
package/bin/mipham.ts
CHANGED
|
@@ -1144,6 +1144,7 @@ Usage:
|
|
|
1144
1144
|
mipham --help Show this help
|
|
1145
1145
|
|
|
1146
1146
|
Flags:
|
|
1147
|
+
--dump-config Print the assembled profile tree
|
|
1147
1148
|
--safe-mode Skip custom agents, skills, hooks, plugins
|
|
1148
1149
|
--version, -v, -V Print version
|
|
1149
1150
|
|
|
@@ -1152,6 +1153,30 @@ npm: https://www.npmjs.com/package/@miphamai/cli`)
|
|
|
1152
1153
|
process.exit(0)
|
|
1153
1154
|
}
|
|
1154
1155
|
|
|
1156
|
+
// ── Dump config flag ───────────────────────────────────────────────────────
|
|
1157
|
+
if (process.argv.includes('--dump-config')) {
|
|
1158
|
+
const { homedir } = await import('node:os')
|
|
1159
|
+
const { join } = await import('node:path')
|
|
1160
|
+
const { existsSync } = await import('node:fs')
|
|
1161
|
+
const { loadProfile, loadBundle, assemble, dumpConfig } = await import('../src/vajra/compose')
|
|
1162
|
+
|
|
1163
|
+
const profileIdx = process.argv.indexOf('--profile')
|
|
1164
|
+
const profileName =
|
|
1165
|
+
profileIdx !== -1 && process.argv[profileIdx + 1] ? process.argv[profileIdx + 1]! : 'default'
|
|
1166
|
+
const dir = join(homedir(), '.mipham', 'profiles')
|
|
1167
|
+
const profilePath = join(dir, `${profileName}.yml`)
|
|
1168
|
+
|
|
1169
|
+
if (!existsSync(profilePath)) {
|
|
1170
|
+
console.error(`Profile "${profileName}" not found at ${profilePath}`)
|
|
1171
|
+
process.exit(1)
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const profile = loadProfile(profilePath)
|
|
1175
|
+
const resolveBundle = (name: string) => loadBundle(join(dir, `${name}.yml`))
|
|
1176
|
+
console.log(dumpConfig(assemble(profile, resolveBundle)))
|
|
1177
|
+
process.exit(0)
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1155
1180
|
// ── Update / upgrade ──────────────────────────────────────────────────────
|
|
1156
1181
|
const handledUpdate = await runUpdate()
|
|
1157
1182
|
if (handledUpdate) return
|
package/package.json
CHANGED
package/src/agent/sub-agent.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ProviderRegistry } from '../providers/registry'
|
|
2
|
+
import type { Llm } from '../providers/llm'
|
|
2
3
|
import type { ToolDefinition } from '../shared/index.ts'
|
|
3
4
|
import type { SubAgentType, SubAgentOptions, AgentDefinition } from './types'
|
|
4
5
|
import { createAgentContext } from './agent-context'
|
|
@@ -44,6 +45,7 @@ export class SubAgent {
|
|
|
44
45
|
private permission?: PermissionSystem,
|
|
45
46
|
private hookEngine?: HookEngine,
|
|
46
47
|
private ruleEngine?: ExperienceRuleEngine,
|
|
48
|
+
private llm?: Llm,
|
|
47
49
|
) {}
|
|
48
50
|
|
|
49
51
|
/**
|
|
@@ -189,8 +191,7 @@ export class SubAgent {
|
|
|
189
191
|
options: SubAgentOptions,
|
|
190
192
|
signal?: AbortSignal,
|
|
191
193
|
): Promise<string> {
|
|
192
|
-
|
|
193
|
-
if (!provider) {
|
|
194
|
+
if (!this.registry.getActive()) {
|
|
194
195
|
throw new Error('No active provider available for sub-agent execution')
|
|
195
196
|
}
|
|
196
197
|
|
|
@@ -332,7 +333,7 @@ export class SubAgent {
|
|
|
332
333
|
const toolUses: Array<{ id: string; name: string; input: Record<string, unknown> }> = []
|
|
333
334
|
let turnText = ''
|
|
334
335
|
|
|
335
|
-
for await (const chunk of
|
|
336
|
+
for await (const chunk of (this.llm ?? this.registry).chat({
|
|
336
337
|
model: finalModel,
|
|
337
338
|
messages: currentMessages,
|
|
338
339
|
systemPrompt: currentSystemPrompt,
|
package/src/config/defaults.ts
CHANGED
|
@@ -41,7 +41,17 @@ export const DEFAULT_INFERENCE_HOOK_CONFIG: InferenceHookConfig = {
|
|
|
41
41
|
|
|
42
42
|
export const DEFAULT_CREDENTIAL_MASKING_CONFIG: CredentialMaskingConfig = {
|
|
43
43
|
enabled: true,
|
|
44
|
-
|
|
44
|
+
// Default sensitive-file rules: full-mask any of these before the model sees them.
|
|
45
|
+
files: [
|
|
46
|
+
{ path: '**/.env*', mode: 'full' },
|
|
47
|
+
{ path: '**/.aws/credentials', mode: 'full' },
|
|
48
|
+
{ path: '**/.aws/config', mode: 'full' },
|
|
49
|
+
{ path: '**/.ssh/id_*', mode: 'full' },
|
|
50
|
+
{ path: '**/*.pem', mode: 'full' },
|
|
51
|
+
{ path: '**/*.key', mode: 'full' },
|
|
52
|
+
{ path: '**/.netrc', mode: 'full' },
|
|
53
|
+
{ path: '**/credentials.*', mode: 'full' },
|
|
54
|
+
],
|
|
45
55
|
output_scrubbing: {
|
|
46
56
|
enabled: true,
|
|
47
57
|
patterns: ['(?i)(api[_-]?key|secret|token|password|credential)\\s*[:=]\\s*\\S+'],
|
|
@@ -52,6 +62,14 @@ export const DEFAULT_CREDENTIAL_MASKING_CONFIG: CredentialMaskingConfig = {
|
|
|
52
62
|
},
|
|
53
63
|
}
|
|
54
64
|
|
|
65
|
+
/** 掩码中立配置:Read/Bash 照常挂载,但不启用任何凭据掩码(对齐 pre-seam 无参行为)。 */
|
|
66
|
+
export const DISABLED_CREDENTIAL_MASKING_CONFIG: CredentialMaskingConfig = {
|
|
67
|
+
enabled: false,
|
|
68
|
+
files: [],
|
|
69
|
+
output_scrubbing: { enabled: false, patterns: [] },
|
|
70
|
+
env_filter: { enabled: false, patterns: [] },
|
|
71
|
+
}
|
|
72
|
+
|
|
55
73
|
export const DEFAULT_BACKGROUND_AGENT_CONFIG: BackgroundAgentConfig = {
|
|
56
74
|
auto_commit: true,
|
|
57
75
|
auto_push: true,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs'
|
|
2
2
|
import { join, dirname } from 'node:path'
|
|
3
3
|
import { homedir } from 'node:os'
|
|
4
|
+
import { saveProviderApiKey } from './loader'
|
|
4
5
|
|
|
5
6
|
const MIPHAM_DIR = join(homedir(), '.mipham')
|
|
6
7
|
const KEYS_FILE = join(MIPHAM_DIR, 'keys.json')
|
|
@@ -67,7 +68,16 @@ export class KeyManager {
|
|
|
67
68
|
}))
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
rotate(provider: string,
|
|
71
|
+
rotate(provider: string, newKey: string): { success: boolean; message: string } {
|
|
72
|
+
// Persist the new key first — the actual rotation. Metadata below is only
|
|
73
|
+
// bookkeeping; without writing the key, "rotate" would be a silent no-op.
|
|
74
|
+
if (!saveProviderApiKey(provider, newKey)) {
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
message: `Rotation aborted: failed to persist new key for "${provider}".`,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
const data = loadKeys()
|
|
72
82
|
const now = new Date().toISOString()
|
|
73
83
|
const existing = data[provider]
|
package/src/config/loader.ts
CHANGED
|
@@ -58,6 +58,7 @@ function safeParseYaml(path: string, label: string): Partial<MiphamConfig> | nul
|
|
|
58
58
|
function mergeProviders(
|
|
59
59
|
baseProviders: ProviderConfig[],
|
|
60
60
|
overrideProviders: ProviderConfig[],
|
|
61
|
+
allowBaseUrlOverride: boolean,
|
|
61
62
|
): ProviderConfig[] {
|
|
62
63
|
const merged = [...baseProviders]
|
|
63
64
|
|
|
@@ -75,7 +76,10 @@ function mergeProviders(
|
|
|
75
76
|
id: base.id,
|
|
76
77
|
name: op.name || base.name,
|
|
77
78
|
protocol: op.protocol || base.protocol,
|
|
78
|
-
|
|
79
|
+
// baseUrl is a routing field — it decides where the user's API key is
|
|
80
|
+
// sent, so only trusted (user-level) config may override it. Untrusted
|
|
81
|
+
// (project-level) config cannot redirect a built-in provider's traffic.
|
|
82
|
+
baseUrl: allowBaseUrlOverride ? (op.baseUrl ?? base.baseUrl) : base.baseUrl,
|
|
79
83
|
apiKey: op.apiKey ?? base.apiKey,
|
|
80
84
|
models: op.models?.length ? op.models : base.models,
|
|
81
85
|
status: op.status ?? base.status,
|
|
@@ -85,10 +89,14 @@ function mergeProviders(
|
|
|
85
89
|
return merged
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
function mergeConfig(
|
|
92
|
+
function mergeConfig(
|
|
93
|
+
base: MiphamConfig,
|
|
94
|
+
override: Partial<MiphamConfig>,
|
|
95
|
+
allowBaseUrlOverride: boolean,
|
|
96
|
+
): MiphamConfig {
|
|
89
97
|
const merged: MiphamConfig = { ...base, ...override }
|
|
90
98
|
if (override.providers) {
|
|
91
|
-
merged.providers = mergeProviders(base.providers, override.providers)
|
|
99
|
+
merged.providers = mergeProviders(base.providers, override.providers, allowBaseUrlOverride)
|
|
92
100
|
} else {
|
|
93
101
|
merged.providers = base.providers
|
|
94
102
|
}
|
|
@@ -216,7 +224,7 @@ export function loadConfig(cwd: string = process.cwd()): MiphamConfig {
|
|
|
216
224
|
// ── Load project-level config ──
|
|
217
225
|
const projectConfig = safeParseYaml(configPath, 'project config')
|
|
218
226
|
if (projectConfig) {
|
|
219
|
-
config = mergeConfig(config, projectConfig)
|
|
227
|
+
config = mergeConfig(config, projectConfig, false)
|
|
220
228
|
} else if (existsSync(configPath)) {
|
|
221
229
|
// File exists but failed to parse — try to restore from backup
|
|
222
230
|
process.stderr.write(`⚠ Mipham Code: project config is corrupted, attempting recovery...\n`)
|
|
@@ -226,7 +234,7 @@ export function loadConfig(cwd: string = process.cwd()): MiphamConfig {
|
|
|
226
234
|
// Retry parsing after restore
|
|
227
235
|
const restored = safeParseYaml(configPath, 'restored project config')
|
|
228
236
|
if (restored) {
|
|
229
|
-
config = mergeConfig(config, restored)
|
|
237
|
+
config = mergeConfig(config, restored, false)
|
|
230
238
|
}
|
|
231
239
|
}
|
|
232
240
|
}
|
|
@@ -234,7 +242,7 @@ export function loadConfig(cwd: string = process.cwd()): MiphamConfig {
|
|
|
234
242
|
// ── Load user-level config ──
|
|
235
243
|
const userConfig = safeParseYaml(userConfigPath, 'user config')
|
|
236
244
|
if (userConfig) {
|
|
237
|
-
config = mergeConfig(config, userConfig)
|
|
245
|
+
config = mergeConfig(config, userConfig, true)
|
|
238
246
|
} else if (existsSync(userConfigPath)) {
|
|
239
247
|
// File exists but failed to parse — try to restore from backup
|
|
240
248
|
process.stderr.write(`⚠ Mipham Code: user config is corrupted, attempting recovery...\n`)
|
|
@@ -244,7 +252,7 @@ export function loadConfig(cwd: string = process.cwd()): MiphamConfig {
|
|
|
244
252
|
// Retry parsing after restore
|
|
245
253
|
const restored = safeParseYaml(userConfigPath, 'restored user config')
|
|
246
254
|
if (restored) {
|
|
247
|
-
config = mergeConfig(config, restored)
|
|
255
|
+
config = mergeConfig(config, restored, true)
|
|
248
256
|
}
|
|
249
257
|
}
|
|
250
258
|
} else {
|
|
@@ -286,13 +294,15 @@ export function loadConfig(cwd: string = process.cwd()): MiphamConfig {
|
|
|
286
294
|
*
|
|
287
295
|
* Returns default (disabled) config if no inference_hooks section is present.
|
|
288
296
|
*/
|
|
289
|
-
export function loadInferenceHookConfig(
|
|
290
|
-
|
|
297
|
+
export function loadInferenceHookConfig(): InferenceHookConfig {
|
|
298
|
+
// DLP is a user/org-level security setting — the endpoint controls where the
|
|
299
|
+
// entire conversation is sent, so a project config must NOT be able to
|
|
300
|
+
// redirect it (exfiltration). Read only from the user-level config.
|
|
291
301
|
const userConfigPath = join(MIPHAM_HOME, 'config.yml')
|
|
292
302
|
|
|
293
303
|
let merged = { ...DEFAULT_INFERENCE_HOOK_CONFIG }
|
|
294
304
|
|
|
295
|
-
const paths = [userConfigPath
|
|
305
|
+
const paths = [userConfigPath]
|
|
296
306
|
for (const path of paths) {
|
|
297
307
|
try {
|
|
298
308
|
if (!existsSync(path)) continue
|
package/src/core/context.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import type { Message } from '../shared/index.ts'
|
|
1
|
+
import type { Message, ToolResult } from '../shared/index.ts'
|
|
2
2
|
import { snipMessages } from './context-snip'
|
|
3
3
|
import { microcompact } from './context-microcompact'
|
|
4
|
-
import { reactiveCompact } from './context-compact'
|
|
5
|
-
import { emergencyDrain } from './context-drain'
|
|
6
4
|
import { NoopCacheTracker, type CacheTracker, type CacheStatus } from './context-token'
|
|
5
|
+
import {
|
|
6
|
+
SessionLog,
|
|
7
|
+
messageToEvents,
|
|
8
|
+
deriveMessages,
|
|
9
|
+
assertModelVisible,
|
|
10
|
+
isAssertModelVisibleDebug,
|
|
11
|
+
} from './session-log'
|
|
7
12
|
|
|
8
13
|
export type Summarizer = (messages: Message[], heading: string) => Promise<string>
|
|
9
14
|
|
|
@@ -18,9 +23,6 @@ export interface CompactionStats {
|
|
|
18
23
|
snipMessagesRemoved: number
|
|
19
24
|
microcompactCount: number
|
|
20
25
|
microcompactTokensSaved: number
|
|
21
|
-
compactCount: number
|
|
22
|
-
compactTokensSaved: number
|
|
23
|
-
drainCount: number
|
|
24
26
|
lastCompaction: Date | null
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -47,9 +49,6 @@ export class ContextManager {
|
|
|
47
49
|
snipMessagesRemoved: 0,
|
|
48
50
|
microcompactCount: 0,
|
|
49
51
|
microcompactTokensSaved: 0,
|
|
50
|
-
compactCount: 0,
|
|
51
|
-
compactTokensSaved: 0,
|
|
52
|
-
drainCount: 0,
|
|
53
52
|
lastCompaction: null,
|
|
54
53
|
}
|
|
55
54
|
private compressionPending = false
|
|
@@ -58,6 +57,24 @@ export class ContextManager {
|
|
|
58
57
|
this.calculateThresholds()
|
|
59
58
|
}
|
|
60
59
|
|
|
60
|
+
private log?: SessionLog
|
|
61
|
+
|
|
62
|
+
/** 附加一个 append-only 会话日志;addMessage/seedMessages 将写通镜像到日志。 */
|
|
63
|
+
setLog(log?: SessionLog): void {
|
|
64
|
+
this.log = log
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getLog(): SessionLog | undefined {
|
|
68
|
+
return this.log
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 从已持久化的日志恢复:设 log 为源,messages 为投影(不重复写通)。 */
|
|
72
|
+
restoreLog(log: SessionLog): void {
|
|
73
|
+
this.log = log
|
|
74
|
+
this.messages = deriveMessages(log.events())
|
|
75
|
+
this.reEstimateTokens()
|
|
76
|
+
}
|
|
77
|
+
|
|
61
78
|
/**
|
|
62
79
|
* Calculate adaptive compaction thresholds based on the model's context window.
|
|
63
80
|
*
|
|
@@ -112,10 +129,36 @@ export class ContextManager {
|
|
|
112
129
|
typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
|
|
113
130
|
)
|
|
114
131
|
|
|
132
|
+
if (this.log) {
|
|
133
|
+
for (const e of messageToEvents(msg, Date.now())) this.log.append(e)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (this.log && isAssertModelVisibleDebug()) {
|
|
137
|
+
assertModelVisible(this.log.events(), this.messages)
|
|
138
|
+
}
|
|
139
|
+
|
|
115
140
|
// Auto-trigger compression checks (fire-and-forget, don't await)
|
|
116
141
|
this.checkCompression()
|
|
117
142
|
}
|
|
118
143
|
|
|
144
|
+
/** 记录工具执行结果(全量 ToolResult 含 success/error)到日志,并写投影消息(不重复走 messageToEvents 拆分)。 */
|
|
145
|
+
addToolResult(toolUseId: string, result: ToolResult): void {
|
|
146
|
+
const content = result.success ? result.content : result.error || result.content
|
|
147
|
+
const msg: Message = {
|
|
148
|
+
role: 'user',
|
|
149
|
+
content: [{ type: 'tool_result', tool_use_id: toolUseId, content }],
|
|
150
|
+
}
|
|
151
|
+
this.messages.push(msg)
|
|
152
|
+
this.estimatedTokens += this.estimateTokens(JSON.stringify(msg.content))
|
|
153
|
+
if (this.log) this.log.append({ type: 'tool/result', at: Date.now(), id: toolUseId, result })
|
|
154
|
+
this.checkCompression()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 记录原始 assistant 流块(保 replay 保真);不写入投影 messages。 */
|
|
158
|
+
recordChunk(chunk: string): void {
|
|
159
|
+
if (this.log) this.log.append({ type: 'assistant/chunk', at: Date.now(), chunk })
|
|
160
|
+
}
|
|
161
|
+
|
|
119
162
|
/**
|
|
120
163
|
* Seed a batch of pre-existing messages (e.g., an inherited parent
|
|
121
164
|
* conversation). Unlike addMessage, this does not trigger compaction so the
|
|
@@ -124,7 +167,14 @@ export class ContextManager {
|
|
|
124
167
|
seedMessages(messages: Message[]): void {
|
|
125
168
|
if (messages.length === 0) return
|
|
126
169
|
this.messages.push(...messages)
|
|
170
|
+
if (this.log) {
|
|
171
|
+
for (const m of messages) for (const e of messageToEvents(m, Date.now())) this.log.append(e)
|
|
172
|
+
}
|
|
127
173
|
this.reEstimateTokens()
|
|
174
|
+
|
|
175
|
+
if (this.log && isAssertModelVisibleDebug()) {
|
|
176
|
+
assertModelVisible(this.log.events(), this.messages)
|
|
177
|
+
}
|
|
128
178
|
}
|
|
129
179
|
|
|
130
180
|
getMessages(): Message[] {
|
|
@@ -155,13 +205,30 @@ export class ContextManager {
|
|
|
155
205
|
content: `[Earlier conversation summary]: ${summary}`,
|
|
156
206
|
}
|
|
157
207
|
this.messages = [summaryMsg, ...this.messages.slice(-keep)]
|
|
208
|
+
if (this.log) {
|
|
209
|
+
const derivedCount = deriveMessages(this.log.events()).length
|
|
210
|
+
const replacedCount = Math.max(0, derivedCount - keep)
|
|
211
|
+
this.log.append({ type: 'compaction/summary', at: Date.now(), summary, replacedCount })
|
|
212
|
+
}
|
|
158
213
|
} catch {
|
|
159
214
|
// Fall back to truncation on summarizer failure
|
|
160
|
-
|
|
215
|
+
const kept = this.messages.slice(-keep)
|
|
216
|
+
if (this.log) {
|
|
217
|
+
this.log.append({ type: 'compaction/rewrite', at: Date.now(), messages: kept })
|
|
218
|
+
this.messages = deriveMessages(this.log.events())
|
|
219
|
+
} else {
|
|
220
|
+
this.messages = kept
|
|
221
|
+
}
|
|
161
222
|
}
|
|
162
223
|
} else {
|
|
163
224
|
// Simple truncation fallback
|
|
164
|
-
|
|
225
|
+
const kept = this.messages.slice(-keep)
|
|
226
|
+
if (this.log) {
|
|
227
|
+
this.log.append({ type: 'compaction/rewrite', at: Date.now(), messages: kept })
|
|
228
|
+
this.messages = deriveMessages(this.log.events())
|
|
229
|
+
} else {
|
|
230
|
+
this.messages = kept
|
|
231
|
+
}
|
|
165
232
|
}
|
|
166
233
|
|
|
167
234
|
// Re-estimate tokens
|
|
@@ -280,50 +347,6 @@ export class ContextManager {
|
|
|
280
347
|
return { ...this.compactionStats }
|
|
281
348
|
}
|
|
282
349
|
|
|
283
|
-
// ── 413 recovery ──
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Emergency context drain for 413 (context too large) errors.
|
|
287
|
-
* Progressively strips messages until the context fits.
|
|
288
|
-
* Returns true if recovery was possible, false if already minimal.
|
|
289
|
-
*/
|
|
290
|
-
async on413Error(): Promise<boolean> {
|
|
291
|
-
const result = await emergencyDrain(this)
|
|
292
|
-
if (result.recovered) {
|
|
293
|
-
this.compactionStats.drainCount++
|
|
294
|
-
this.compactionStats.lastCompaction = new Date()
|
|
295
|
-
}
|
|
296
|
-
return result.recovered
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// ── Forced compaction (e.g. /compact command) ──
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Force compaction of the conversation history.
|
|
303
|
-
*
|
|
304
|
-
* Uses the provided summarizer, the internal summarizer, or falls back
|
|
305
|
-
* to emergency drain if neither is available.
|
|
306
|
-
*/
|
|
307
|
-
async forceCompact(heading: string, summarizer?: Summarizer): Promise<void> {
|
|
308
|
-
const beforeTokens = this.estimatedTokens
|
|
309
|
-
|
|
310
|
-
if (summarizer) {
|
|
311
|
-
await reactiveCompact(this, summarizer, heading)
|
|
312
|
-
} else if (this.summarizer) {
|
|
313
|
-
await reactiveCompact(this, this.summarizer, heading)
|
|
314
|
-
} else {
|
|
315
|
-
// Fallback: simple truncation via drain
|
|
316
|
-
await emergencyDrain(this)
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const tokensSaved = beforeTokens - this.estimatedTokens
|
|
320
|
-
this.compactionStats.compactCount++
|
|
321
|
-
if (tokensSaved > 0) {
|
|
322
|
-
this.compactionStats.compactTokensSaved += tokensSaved
|
|
323
|
-
}
|
|
324
|
-
this.compactionStats.lastCompaction = new Date()
|
|
325
|
-
}
|
|
326
|
-
|
|
327
350
|
// ── Private compression helpers ──
|
|
328
351
|
|
|
329
352
|
/**
|
|
@@ -373,7 +396,14 @@ export class ContextManager {
|
|
|
373
396
|
this.compactionStats.lastCompaction = new Date()
|
|
374
397
|
}
|
|
375
398
|
|
|
376
|
-
|
|
399
|
+
// 仅当内容真变时写日志并重建投影(model-visible means logged 不变量)。
|
|
400
|
+
// 用 deriveMessages(log) 而非直接赋值 compacted,避免与日志事件共享 messages 引用被 addMessage.push 污染。
|
|
401
|
+
if (this.log && (removed > 0 || tokensSaved > 0)) {
|
|
402
|
+
this.log.append({ type: 'compaction/rewrite', at: Date.now(), messages: compacted })
|
|
403
|
+
this.messages = deriveMessages(this.log.events())
|
|
404
|
+
} else {
|
|
405
|
+
this.messages = compacted
|
|
406
|
+
}
|
|
377
407
|
this.reEstimateTokens()
|
|
378
408
|
}
|
|
379
409
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CREDENTIAL_SENTINEL } from './types'
|
|
2
2
|
import type { CredentialMaskingConfig } from '../../shared/index.ts'
|
|
3
|
+
import { SecurityGate } from '../../security/gate'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Well-known secret token prefixes. These are high-signal formats that must
|
|
@@ -23,6 +24,10 @@ export function maskOutput(output: string, config: CredentialMaskingConfig): str
|
|
|
23
24
|
// Redact bare secret tokens by prefix (always on, independent of config patterns).
|
|
24
25
|
masked = masked.replace(TOKEN_REDACTION_PATTERN, CREDENTIAL_SENTINEL)
|
|
25
26
|
|
|
27
|
+
// Redact bare sk-/sk-ant-/JWT tokens — wire the SecurityGate credential-leak
|
|
28
|
+
// detection into the output path (defense-in-depth beyond the config patterns).
|
|
29
|
+
masked = SecurityGate.redactCredentialLeak(masked)
|
|
30
|
+
|
|
26
31
|
for (const pattern of config.output_scrubbing.patterns) {
|
|
27
32
|
try {
|
|
28
33
|
// Strip (?i) inline flags — JS uses the 'i' flag instead
|
package/src/core/crsi-sandbox.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { execSync } from 'node:child_process'
|
|
18
18
|
import { mkdirSync, rmSync, existsSync, writeFileSync, readFileSync, readdirSync } from 'node:fs'
|
|
19
|
-
import { join, resolve } from 'node:path'
|
|
19
|
+
import { join, resolve, sep } from 'node:path'
|
|
20
20
|
import { tmpdir, homedir } from 'node:os'
|
|
21
21
|
import { randomUUID } from 'node:crypto'
|
|
22
22
|
|
|
@@ -172,7 +172,16 @@ export class CrsiSandbox {
|
|
|
172
172
|
phase: 'pending',
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
const
|
|
175
|
+
const worktreeRoot = resolve(this.worktreePath)
|
|
176
|
+
const targetPath = resolve(this.worktreePath, mod.filePath)
|
|
177
|
+
|
|
178
|
+
// Path traversal guard: reject any target that resolves outside the sandbox worktree
|
|
179
|
+
if (targetPath !== worktreeRoot && !targetPath.startsWith(worktreeRoot + sep)) {
|
|
180
|
+
result.error = `Path traversal blocked: "${mod.filePath}" resolves outside the worktree.`
|
|
181
|
+
result.phase = 'failed'
|
|
182
|
+
this.sessionReport.modifications.push(result)
|
|
183
|
+
return result
|
|
184
|
+
}
|
|
176
185
|
|
|
177
186
|
// Verify the file exists in the worktree
|
|
178
187
|
if (!existsSync(targetPath)) {
|
|
@@ -182,7 +191,7 @@ export class CrsiSandbox {
|
|
|
182
191
|
return result
|
|
183
192
|
}
|
|
184
193
|
|
|
185
|
-
// Verify original content matches (safety check)
|
|
194
|
+
// Verify original content matches (safety check — empty originalContent = lenient skip)
|
|
186
195
|
try {
|
|
187
196
|
const currentContent = readFileSync(targetPath, 'utf-8')
|
|
188
197
|
if (currentContent !== mod.originalContent && mod.originalContent) {
|
|
@@ -319,12 +328,15 @@ export class CrsiSandbox {
|
|
|
319
328
|
return { success: false, message: 'No worktree to merge from.' }
|
|
320
329
|
}
|
|
321
330
|
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
331
|
+
// Require every modification to have passed tests before merging. A mod left
|
|
332
|
+
// in 'pending'/'applied'/'testing' (tests never run or never passed) must not merge.
|
|
333
|
+
const mergeable = new Set(['passed', 'approved', 'merged'])
|
|
334
|
+
const blockers = this.sessionReport.modifications.filter((m) => !mergeable.has(m.phase))
|
|
335
|
+
if (blockers.length > 0) {
|
|
336
|
+
const phases = [...new Set(blockers.map((m) => m.phase))].join(', ')
|
|
325
337
|
return {
|
|
326
338
|
success: false,
|
|
327
|
-
message: `${
|
|
339
|
+
message: `${blockers.length} modification(s) not ready to merge (phase: ${phases}). Cannot merge.`,
|
|
328
340
|
}
|
|
329
341
|
}
|
|
330
342
|
|