@miphamai/cli 0.15.0 → 0.16.1
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/dist/mipham +0 -0
- package/package.json +2 -4
- package/src/agent/message-bus.ts +19 -1
- package/src/agent/sub-agent.ts +25 -2
- package/src/commands/environment.ts +270 -0
- package/src/commands/git.ts +152 -0
- package/src/commands/project.ts +1011 -0
- package/src/config/defaults.ts +4 -0
- package/src/config/preferences.ts +45 -0
- package/src/core/context.ts +9 -0
- package/src/core/engine.ts +17 -5
- package/src/core/permission-config.ts +70 -6
- package/src/core/permission.ts +50 -9
- package/src/core/session-store.ts +7 -2
- package/src/index.tsx +51 -2
- package/src/providers/registry.ts +8 -0
- package/src/shared/sanitize.ts +38 -0
- package/src/shared/types.ts +18 -0
- package/src/skills/community-registry.json +72 -0
- package/src/skills/registry.ts +71 -77
- package/src/tools/exec/bash.ts +40 -8
- package/src/tools/exec/enter-worktree.ts +23 -1
- package/src/tools/exec/exit-worktree.ts +13 -0
- package/src/tools/index.ts +5 -2
- package/src/ui/agent-footer.tsx +89 -0
- package/src/ui/app.tsx +169 -35
- package/src/ui/chat.tsx +80 -30
- package/src/ui/commands.ts +176 -1477
- package/src/ui/strip-indent.ts +19 -0
- package/src/workflow/runtime.ts +29 -31
- package/src/workflow/sandbox.ts +22 -10
package/src/config/defaults.ts
CHANGED
|
@@ -13,6 +13,10 @@ export const DEFAULT_CONFIG: MiphamConfig = {
|
|
|
13
13
|
defaultModel: 'deepseek-v4-pro',
|
|
14
14
|
permission: 'auto',
|
|
15
15
|
providers: DEFAULT_PROVIDERS,
|
|
16
|
+
marketplace: {
|
|
17
|
+
strictKnownMarketplaces: [],
|
|
18
|
+
blockedMarketplaces: [],
|
|
19
|
+
},
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
export const DEFAULT_INFERENCE_HOOK_CONFIG: InferenceHookConfig = {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight preferences store backed by ~/.mipham/preferences.json.
|
|
3
|
+
* Used for persisting user-level UI state (e.g. last code review effort).
|
|
4
|
+
*
|
|
5
|
+
* NOT for config.yml settings — those belong in the YAML config system.
|
|
6
|
+
* NOT for secrets — this file is plain JSON, not encrypted.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import { homedir } from 'node:os'
|
|
11
|
+
|
|
12
|
+
const PREFS_PATH = join(homedir(), '.mipham', 'preferences.json')
|
|
13
|
+
|
|
14
|
+
function readPrefs(): Record<string, string> {
|
|
15
|
+
try {
|
|
16
|
+
if (!existsSync(PREFS_PATH)) return {}
|
|
17
|
+
const raw = readFileSync(PREFS_PATH, 'utf-8')
|
|
18
|
+
const parsed: unknown = JSON.parse(raw)
|
|
19
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return {}
|
|
20
|
+
return parsed as Record<string, string>
|
|
21
|
+
} catch {
|
|
22
|
+
return {}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function writePrefs(prefs: Record<string, string>): void {
|
|
27
|
+
try {
|
|
28
|
+
const dir = join(homedir(), '.mipham')
|
|
29
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
30
|
+
writeFileSync(PREFS_PATH, JSON.stringify(prefs, null, 2), { mode: 0o600, encoding: 'utf-8' })
|
|
31
|
+
} catch {
|
|
32
|
+
// best-effort; never crash because preferences failed to save
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getPreference(key: string, defaultValue: string): string {
|
|
37
|
+
const prefs = readPrefs()
|
|
38
|
+
return prefs[key] ?? defaultValue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function setPreference(key: string, value: string): void {
|
|
42
|
+
const prefs = readPrefs()
|
|
43
|
+
prefs[key] = value
|
|
44
|
+
writePrefs(prefs)
|
|
45
|
+
}
|
package/src/core/context.ts
CHANGED
|
@@ -55,6 +55,15 @@ export class ContextManager {
|
|
|
55
55
|
|
|
56
56
|
constructor(private config: ContextConfig) {}
|
|
57
57
|
|
|
58
|
+
/** Dynamically update the max token limit (e.g., when switching models). */
|
|
59
|
+
updateMaxTokens(maxTokens: number): void {
|
|
60
|
+
this.config.maxTokens = maxTokens
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getMaxTokens(): number {
|
|
64
|
+
return this.config.maxTokens
|
|
65
|
+
}
|
|
66
|
+
|
|
58
67
|
/** Set an optional LLM summarizer for intelligent compaction. */
|
|
59
68
|
setSummarizer(fn: Summarizer): void {
|
|
60
69
|
this.summarizer = fn
|
package/src/core/engine.ts
CHANGED
|
@@ -41,7 +41,7 @@ export class QueryEngine {
|
|
|
41
41
|
private registry: ProviderRegistry,
|
|
42
42
|
private context: ContextManager,
|
|
43
43
|
private tools: Map<string, ToolDefinition>,
|
|
44
|
-
private permission: PermissionSystem = new PermissionSystem('
|
|
44
|
+
private permission: PermissionSystem = new PermissionSystem('default'),
|
|
45
45
|
) {}
|
|
46
46
|
|
|
47
47
|
/** Register a hook engine for pre/post tool-use lifecycle events. */
|
|
@@ -277,6 +277,7 @@ export class QueryEngine {
|
|
|
277
277
|
|
|
278
278
|
// ── PreInference DLP checkpoint ──
|
|
279
279
|
if (isInferenceHookEnabled(this.inferenceHookConfig)) {
|
|
280
|
+
const hookConfig = this.inferenceHookConfig!
|
|
280
281
|
const provider = this.registry.getActive().config.id
|
|
281
282
|
const model = this.registry.getActiveModel()
|
|
282
283
|
const request = buildRequest(
|
|
@@ -284,9 +285,9 @@ export class QueryEngine {
|
|
|
284
285
|
'session-1',
|
|
285
286
|
provider,
|
|
286
287
|
model,
|
|
287
|
-
|
|
288
|
+
hookConfig.organization_id,
|
|
288
289
|
)
|
|
289
|
-
const verdict = await sendInferenceCheck(
|
|
290
|
+
const verdict = await sendInferenceCheck(hookConfig, request)
|
|
290
291
|
if (!verdict.allowed) {
|
|
291
292
|
yield {
|
|
292
293
|
type: 'error',
|
|
@@ -556,6 +557,7 @@ export class QueryEngine {
|
|
|
556
557
|
|
|
557
558
|
// ── PreInference DLP checkpoint (every tool-calling turn) ──
|
|
558
559
|
if (isInferenceHookEnabled(this.inferenceHookConfig)) {
|
|
560
|
+
const hookConfig = this.inferenceHookConfig!
|
|
559
561
|
const provider = this.registry.getActive().config.id
|
|
560
562
|
const model = this.registry.getActiveModel()
|
|
561
563
|
const request = buildRequest(
|
|
@@ -563,9 +565,9 @@ export class QueryEngine {
|
|
|
563
565
|
'session-1',
|
|
564
566
|
provider,
|
|
565
567
|
model,
|
|
566
|
-
|
|
568
|
+
hookConfig.organization_id,
|
|
567
569
|
)
|
|
568
|
-
const verdict = await sendInferenceCheck(
|
|
570
|
+
const verdict = await sendInferenceCheck(hookConfig, request)
|
|
569
571
|
if (!verdict.allowed) {
|
|
570
572
|
yield {
|
|
571
573
|
type: 'error',
|
|
@@ -821,6 +823,16 @@ export class QueryEngine {
|
|
|
821
823
|
|
|
822
824
|
switchProvider(providerId: string, modelId?: string): void {
|
|
823
825
|
this.registry.switchProvider(providerId, modelId)
|
|
826
|
+
// Update context manager's max tokens to match the new model's context window
|
|
827
|
+
if (modelId) {
|
|
828
|
+
const model = this.registry.findModel(modelId)
|
|
829
|
+
if (model) {
|
|
830
|
+
const DISABLE_1M = process.env.MIPHAM_DISABLE_1M_CONTEXT === '1'
|
|
831
|
+
const maxTokens =
|
|
832
|
+
DISABLE_1M && model.contextWindow > 200_000 ? 200_000 : model.contextWindow
|
|
833
|
+
this.context.updateMaxTokens(maxTokens)
|
|
834
|
+
}
|
|
835
|
+
}
|
|
824
836
|
}
|
|
825
837
|
|
|
826
838
|
/** Wrap context compaction with PreCompact/PostCompact hooks. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PermissionConfig, PermissionMode } from '../shared/index.ts'
|
|
1
|
+
import type { PermissionConfig, PermissionMode, PermissionRestrictions } from '../shared/index.ts'
|
|
2
2
|
|
|
3
3
|
const DEFAULT_CONFIG: PermissionConfig = {
|
|
4
4
|
mode: 'default',
|
|
@@ -15,11 +15,15 @@ export function loadPermissionConfig(raw: Partial<PermissionConfig> = {}): Permi
|
|
|
15
15
|
mode: (raw.mode as PermissionMode) || DEFAULT_CONFIG.mode,
|
|
16
16
|
allow: Array.isArray(raw.allow) ? raw.allow : [...DEFAULT_CONFIG.allow],
|
|
17
17
|
deny: Array.isArray(raw.deny) ? raw.deny : [...DEFAULT_CONFIG.deny],
|
|
18
|
+
restrictions: raw.restrictions ?? undefined,
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Permission modes ordered from least to most permissive.
|
|
24
|
+
* Used to enforce maxAllowedMode: any mode ranked higher than the cap is forbidden.
|
|
25
|
+
*/
|
|
26
|
+
export const PERMISSION_MODE_HIERARCHY: PermissionMode[] = [
|
|
23
27
|
'default',
|
|
24
28
|
'acceptEdits',
|
|
25
29
|
'plan',
|
|
@@ -28,7 +32,67 @@ export const MODE_CYCLE: PermissionMode[] = [
|
|
|
28
32
|
'bypassPermissions',
|
|
29
33
|
]
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
/** Valid mode transition order for Shift+Tab cycling. */
|
|
36
|
+
export const MODE_CYCLE: PermissionMode[] = [...PERMISSION_MODE_HIERARCHY]
|
|
37
|
+
|
|
38
|
+
/** Resolve which modes are actually permitted given the restrictions. */
|
|
39
|
+
export function getAllowedModes(restrictions?: PermissionRestrictions): PermissionMode[] {
|
|
40
|
+
let allowed = [...MODE_CYCLE]
|
|
41
|
+
|
|
42
|
+
if (restrictions?.forbiddenModes && restrictions.forbiddenModes.length > 0) {
|
|
43
|
+
const forbidden = new Set(restrictions.forbiddenModes)
|
|
44
|
+
allowed = allowed.filter((m) => !forbidden.has(m))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (restrictions?.maxAllowedMode) {
|
|
48
|
+
const capIdx = PERMISSION_MODE_HIERARCHY.indexOf(restrictions.maxAllowedMode)
|
|
49
|
+
if (capIdx >= 0) {
|
|
50
|
+
allowed = allowed.filter((m) => PERMISSION_MODE_HIERARCHY.indexOf(m) <= capIdx)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return allowed
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Check whether a given mode is permitted under the restrictions. */
|
|
58
|
+
export function isModeAllowed(
|
|
59
|
+
mode: PermissionMode,
|
|
60
|
+
restrictions?: PermissionRestrictions,
|
|
61
|
+
): boolean {
|
|
62
|
+
return getAllowedModes(restrictions).includes(mode)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Return the highest allowed mode at or below `desired` given the restrictions.
|
|
67
|
+
* Used to silently downgrade when a forbidden mode is requested.
|
|
68
|
+
*/
|
|
69
|
+
export function clampMode(
|
|
70
|
+
desired: PermissionMode,
|
|
71
|
+
restrictions?: PermissionRestrictions,
|
|
72
|
+
): PermissionMode {
|
|
73
|
+
const allowed = getAllowedModes(restrictions)
|
|
74
|
+
if (allowed.includes(desired)) return desired
|
|
75
|
+
|
|
76
|
+
// Walk downward through the hierarchy to find the closest allowed mode
|
|
77
|
+
const desiredIdx = PERMISSION_MODE_HIERARCHY.indexOf(desired)
|
|
78
|
+
for (let i = desiredIdx - 1; i >= 0; i--) {
|
|
79
|
+
const candidate = PERMISSION_MODE_HIERARCHY[i]!
|
|
80
|
+
if (allowed.includes(candidate)) return candidate
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Fallback: return the first allowed mode (should always be at least 'default')
|
|
84
|
+
return allowed[0] ?? 'default'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function nextMode(
|
|
88
|
+
current: PermissionMode,
|
|
89
|
+
restrictions?: PermissionRestrictions,
|
|
90
|
+
): PermissionMode {
|
|
91
|
+
const allowed = getAllowedModes(restrictions)
|
|
92
|
+
const idx = allowed.indexOf(current)
|
|
93
|
+
if (idx === -1) {
|
|
94
|
+
// Current mode is not in the allowed set — clamp then find next
|
|
95
|
+
return clampMode(current, restrictions)
|
|
96
|
+
}
|
|
97
|
+
return allowed[(idx + 1) % allowed.length]!
|
|
34
98
|
}
|
package/src/core/permission.ts
CHANGED
|
@@ -3,10 +3,11 @@ import type {
|
|
|
3
3
|
PermissionMode,
|
|
4
4
|
PermissionLevel,
|
|
5
5
|
PermissionRule,
|
|
6
|
+
PermissionRestrictions,
|
|
6
7
|
} from '../shared/index.ts'
|
|
7
8
|
import type { PermissionRuleEntry } from '../shared/index.ts'
|
|
8
9
|
import { matchBashRule, compileRule } from './permission-rules'
|
|
9
|
-
import { loadPermissionConfig, nextMode, MODE_CYCLE } from './permission-config'
|
|
10
|
+
import { loadPermissionConfig, nextMode, clampMode, MODE_CYCLE } from './permission-config'
|
|
10
11
|
|
|
11
12
|
const VALID_MODES: Set<string> = new Set<string>(MODE_CYCLE)
|
|
12
13
|
|
|
@@ -25,6 +26,9 @@ export class PermissionSystem {
|
|
|
25
26
|
private checkCache = new Map<string, PermissionLevel>()
|
|
26
27
|
private cacheMode: PermissionMode | null = null
|
|
27
28
|
|
|
29
|
+
// ── Org-level restrictions (P0 security) ──
|
|
30
|
+
private restrictions: PermissionRestrictions | undefined = undefined
|
|
31
|
+
|
|
28
32
|
/** Invalidate the permission cache (called on any rule/mode change). */
|
|
29
33
|
private invalidateCache(): void {
|
|
30
34
|
this.checkCache.clear()
|
|
@@ -44,7 +48,7 @@ export class PermissionSystem {
|
|
|
44
48
|
// ── Mode management ──
|
|
45
49
|
|
|
46
50
|
setMode(mode: PermissionMode): void {
|
|
47
|
-
this.mode = mode
|
|
51
|
+
this.mode = clampMode(mode, this.restrictions)
|
|
48
52
|
this.invalidateCache()
|
|
49
53
|
}
|
|
50
54
|
|
|
@@ -53,10 +57,26 @@ export class PermissionSystem {
|
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
cycleMode(): PermissionMode {
|
|
56
|
-
this.mode = nextMode(this.mode)
|
|
60
|
+
this.mode = nextMode(this.mode, this.restrictions)
|
|
57
61
|
return this.mode
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
// ── Restrictions (P0: org-level policy gap) ──
|
|
65
|
+
|
|
66
|
+
/** Apply org-level permission restrictions. Overwrites any previous restrictions. */
|
|
67
|
+
setRestrictions(restrictions: PermissionRestrictions | undefined): void {
|
|
68
|
+
this.restrictions = restrictions
|
|
69
|
+
// Re-clamp current mode against new restrictions
|
|
70
|
+
if (restrictions) {
|
|
71
|
+
this.mode = clampMode(this.mode, restrictions)
|
|
72
|
+
}
|
|
73
|
+
this.invalidateCache()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getRestrictions(): PermissionRestrictions | undefined {
|
|
77
|
+
return this.restrictions
|
|
78
|
+
}
|
|
79
|
+
|
|
60
80
|
// ── Rule management ──
|
|
61
81
|
|
|
62
82
|
allow(rule: string): void {
|
|
@@ -71,11 +91,21 @@ export class PermissionSystem {
|
|
|
71
91
|
this.askRules.push(compileRule(rule, 'ask'))
|
|
72
92
|
}
|
|
73
93
|
|
|
74
|
-
loadConfig(raw: {
|
|
94
|
+
loadConfig(raw: {
|
|
95
|
+
mode?: string
|
|
96
|
+
allow?: string[]
|
|
97
|
+
deny?: string[]
|
|
98
|
+
restrictions?: PermissionRestrictions
|
|
99
|
+
}): void {
|
|
75
100
|
const config = loadPermissionConfig(
|
|
76
|
-
raw as Partial<{
|
|
101
|
+
raw as Partial<{
|
|
102
|
+
mode: PermissionMode
|
|
103
|
+
allow: string[]
|
|
104
|
+
deny: string[]
|
|
105
|
+
restrictions: PermissionRestrictions
|
|
106
|
+
}>,
|
|
77
107
|
)
|
|
78
|
-
this.mode = config.mode
|
|
108
|
+
this.mode = clampMode(config.mode, config.restrictions ?? this.restrictions)
|
|
79
109
|
|
|
80
110
|
this.allowRules = []
|
|
81
111
|
this.denyRules = []
|
|
@@ -88,6 +118,10 @@ export class PermissionSystem {
|
|
|
88
118
|
this.denyRules.push(compileRule(rule, 'deny'))
|
|
89
119
|
}
|
|
90
120
|
|
|
121
|
+
if (config.restrictions) {
|
|
122
|
+
this.restrictions = config.restrictions
|
|
123
|
+
}
|
|
124
|
+
|
|
91
125
|
this.invalidateCache()
|
|
92
126
|
}
|
|
93
127
|
|
|
@@ -105,6 +139,11 @@ export class PermissionSystem {
|
|
|
105
139
|
* 8. System default → 'ask'
|
|
106
140
|
*/
|
|
107
141
|
check(tool: ToolDefinition, input: Record<string, unknown>): PermissionLevel {
|
|
142
|
+
// ── Guard: reject undefined/null tool (defense-in-depth) ──
|
|
143
|
+
if (!tool) {
|
|
144
|
+
return 'ask' // absent tool → safest default
|
|
145
|
+
}
|
|
146
|
+
|
|
108
147
|
// ── Cache lookup (P2): reuse decision for same tool+mode+input ──
|
|
109
148
|
const cacheKey = tool.name + '|' + JSON.stringify(input, Object.keys(input).sort())
|
|
110
149
|
if (this.cacheMode === this.mode) {
|
|
@@ -246,9 +285,11 @@ export class PermissionSystem {
|
|
|
246
285
|
|
|
247
286
|
setDefaultLevel(level: PermissionLevel): void {
|
|
248
287
|
// Map legacy 3-level to new mode
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
else
|
|
288
|
+
let newMode: PermissionMode
|
|
289
|
+
if (level === 'auto') newMode = 'auto'
|
|
290
|
+
else if (level === 'bypass') newMode = 'bypassPermissions'
|
|
291
|
+
else newMode = 'default'
|
|
292
|
+
this.mode = clampMode(newMode, this.restrictions)
|
|
252
293
|
this.invalidateCache()
|
|
253
294
|
}
|
|
254
295
|
|
|
@@ -17,6 +17,7 @@ export interface SessionMetadata {
|
|
|
17
17
|
provider: string
|
|
18
18
|
model: string
|
|
19
19
|
messageCount: number
|
|
20
|
+
cwd?: string
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export interface StoredSession {
|
|
@@ -44,7 +45,7 @@ export class SessionStore {
|
|
|
44
45
|
static save(
|
|
45
46
|
name: string,
|
|
46
47
|
messages: Message[],
|
|
47
|
-
metadata?: { provider?: string; model?: string },
|
|
48
|
+
metadata?: { provider?: string; model?: string; cwd?: string },
|
|
48
49
|
): void {
|
|
49
50
|
ensureDir()
|
|
50
51
|
const path = filePath(name)
|
|
@@ -57,6 +58,7 @@ export class SessionStore {
|
|
|
57
58
|
provider: metadata?.provider || 'unknown',
|
|
58
59
|
model: metadata?.model || 'unknown',
|
|
59
60
|
messageCount: messages.length,
|
|
61
|
+
cwd: metadata?.cwd,
|
|
60
62
|
},
|
|
61
63
|
messages,
|
|
62
64
|
}
|
|
@@ -132,7 +134,10 @@ export class SessionStore {
|
|
|
132
134
|
/**
|
|
133
135
|
* Auto-save with timestamp-based name.
|
|
134
136
|
*/
|
|
135
|
-
static autoSave(
|
|
137
|
+
static autoSave(
|
|
138
|
+
messages: Message[],
|
|
139
|
+
metadata?: { provider?: string; model?: string; cwd?: string },
|
|
140
|
+
): string {
|
|
136
141
|
const name = `session-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}`
|
|
137
142
|
SessionStore.save(name, messages, metadata)
|
|
138
143
|
return name
|
package/src/index.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
2
3
|
import { render } from 'ink'
|
|
3
4
|
import { App } from './ui/app'
|
|
4
5
|
import { loadConfig, loadInferenceHookConfig, loadCredentialMaskingConfig } from './config/loader'
|
|
@@ -85,12 +86,53 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
85
86
|
// Initialize plugin manager
|
|
86
87
|
const pluginManager = new PluginManager()
|
|
87
88
|
|
|
89
|
+
// Generate session name for tracking (used by /cd to persist cwd)
|
|
90
|
+
const sessionName =
|
|
91
|
+
options.resume || `session-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}`
|
|
92
|
+
|
|
88
93
|
// Initialize context — restore saved session if available
|
|
89
|
-
|
|
94
|
+
// Read the active model's context window for dynamic max-token sizing.
|
|
95
|
+
// MIPHAM_DISABLE_1M_CONTEXT=1 caps the effective window at 200K even for
|
|
96
|
+
// models that support larger contexts (e.g. 1M).
|
|
97
|
+
// When the model is unknown (not in any provider's model list), assume a
|
|
98
|
+
// conservative 128K context window. Set MIPHAM_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT=1
|
|
99
|
+
// to restore the old behavior (default 200K fallback for unknown models).
|
|
100
|
+
const activeModel = registry.findModel(defaultModel)
|
|
101
|
+
let modelContextWindow: number
|
|
102
|
+
if (activeModel) {
|
|
103
|
+
modelContextWindow = activeModel.contextWindow
|
|
104
|
+
} else if (process.env.MIPHAM_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT === '1') {
|
|
105
|
+
modelContextWindow = 200_000 // old behavior: default fallback
|
|
106
|
+
} else {
|
|
107
|
+
modelContextWindow = 128_000 // conservative assumption for unknown models
|
|
108
|
+
console.error(
|
|
109
|
+
`[mipham] ⚠ Unknown model "${defaultModel}": assuming 128K context window. ` +
|
|
110
|
+
`Set MIPHAM_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT=1 to disable.`,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const DISABLE_1M = process.env.MIPHAM_DISABLE_1M_CONTEXT === '1'
|
|
115
|
+
const contextMaxTokens = DISABLE_1M && modelContextWindow > 200_000 ? 200_000 : modelContextWindow
|
|
116
|
+
|
|
117
|
+
if (DISABLE_1M && modelContextWindow <= 200_000) {
|
|
118
|
+
console.error(
|
|
119
|
+
'[mipham] ⚠ MIPHAM_DISABLE_1M_CONTEXT is set but auto-compaction is not holding the session to 200K — model context window is already ≤ 200K',
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const context = new ContextManager({ maxTokens: contextMaxTokens, compactionThreshold: 0.9 })
|
|
90
124
|
|
|
91
125
|
if (options.resume) {
|
|
92
126
|
const saved = SessionStore.load(options.resume)
|
|
93
127
|
if (saved) {
|
|
128
|
+
// Restore working directory if saved
|
|
129
|
+
if (saved.metadata.cwd && existsSync(saved.metadata.cwd)) {
|
|
130
|
+
try {
|
|
131
|
+
process.chdir(saved.metadata.cwd)
|
|
132
|
+
} catch {
|
|
133
|
+
// cwd may no longer exist; silently continue
|
|
134
|
+
}
|
|
135
|
+
}
|
|
94
136
|
for (const msg of saved.messages) {
|
|
95
137
|
context.addMessage(msg)
|
|
96
138
|
}
|
|
@@ -169,6 +211,11 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
169
211
|
engine.getPermission().setDefaultLevel(config.permission as PermissionLevel)
|
|
170
212
|
}
|
|
171
213
|
|
|
214
|
+
// Apply org-level permission restrictions (P0: bypassPermissions policy gap)
|
|
215
|
+
if (config.permissionRestrictions) {
|
|
216
|
+
engine.getPermission().setRestrictions(config.permissionRestrictions)
|
|
217
|
+
}
|
|
218
|
+
|
|
172
219
|
// Initialize agent registry and load plugin agents/skills/MCP/hooks
|
|
173
220
|
const agentRegistry = new AgentRegistry()
|
|
174
221
|
agentRegistry.loadUserAgents()
|
|
@@ -190,9 +237,10 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
190
237
|
const saveAndExit = () => {
|
|
191
238
|
artifactServer.stop()
|
|
192
239
|
if (context.getMessageCount() > 0) {
|
|
193
|
-
SessionStore.
|
|
240
|
+
SessionStore.save(sessionName, context.getMessages(), {
|
|
194
241
|
provider: defaultProvider,
|
|
195
242
|
model: defaultModel,
|
|
243
|
+
cwd: process.cwd(),
|
|
196
244
|
})
|
|
197
245
|
}
|
|
198
246
|
process.exit(0)
|
|
@@ -211,6 +259,7 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
211
259
|
skillsLoader={skillsLoader}
|
|
212
260
|
pluginManager={pluginManager}
|
|
213
261
|
version={options.version}
|
|
262
|
+
sessionId={sessionName}
|
|
214
263
|
/>,
|
|
215
264
|
)
|
|
216
265
|
await waitUntilExit()
|
|
@@ -64,6 +64,14 @@ export class ProviderRegistry {
|
|
|
64
64
|
return provider.config.models.filter((m) => m.status === 'active')
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
findModel(modelId: string): ModelInfo | undefined {
|
|
68
|
+
for (const provider of this.providers.values()) {
|
|
69
|
+
const model = provider.config.models.find((m) => m.id === modelId)
|
|
70
|
+
if (model) return model
|
|
71
|
+
}
|
|
72
|
+
return undefined
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
async *chat(req: ChatRequest): AsyncGenerator<StreamChunk> {
|
|
68
76
|
const provider = this.getActive()
|
|
69
77
|
yield* provider.chat({ ...req, model: req.model || this.activeModelId })
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unicode sanitization for tool inputs.
|
|
3
|
+
* Strips invisible/control characters that could hide command content
|
|
4
|
+
* from visual inspection or enable homoglyph attacks.
|
|
5
|
+
*
|
|
6
|
+
* Claude Code v2.1.223 parity: hidden command text via tabs/invisible Unicode.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const DANGEROUS_UNICODE = /[]/g
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Strip dangerous invisible Unicode characters from a string.
|
|
13
|
+
* - Zero-width: U+200B (ZWSP), U+200C (ZWNJ), U+200D (ZWJ), U+200E/F (LTR/RTL marks)
|
|
14
|
+
* - Bidi controls: U+202A-E, U+2066-9
|
|
15
|
+
* - Word joiner: U+2060
|
|
16
|
+
* - BOM: U+FEFF
|
|
17
|
+
*/
|
|
18
|
+
export function stripDangerousUnicode(input: string): string {
|
|
19
|
+
if (!input) return input
|
|
20
|
+
return input.replace(DANGEROUS_UNICODE, '')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Recursively sanitize all string values in a params object.
|
|
25
|
+
*/
|
|
26
|
+
export function sanitizeParams(params: Record<string, unknown>): Record<string, unknown> {
|
|
27
|
+
const result: Record<string, unknown> = {}
|
|
28
|
+
for (const [key, value] of Object.entries(params)) {
|
|
29
|
+
if (typeof value === 'string') {
|
|
30
|
+
result[key] = stripDangerousUnicode(value)
|
|
31
|
+
} else if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
32
|
+
result[key] = sanitizeParams(value as Record<string, unknown>)
|
|
33
|
+
} else {
|
|
34
|
+
result[key] = value
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return result
|
|
38
|
+
}
|
package/src/shared/types.ts
CHANGED
|
@@ -160,8 +160,16 @@ export interface MiphamConfig {
|
|
|
160
160
|
defaultProvider: string
|
|
161
161
|
defaultModel: string
|
|
162
162
|
permission: ToolPermission
|
|
163
|
+
/** Org-level permission restrictions (forbiddenModes, maxAllowedMode). */
|
|
164
|
+
permissionRestrictions?: PermissionRestrictions
|
|
163
165
|
providers: ProviderConfig[]
|
|
164
166
|
skills?: { paths: string[]; mcpServers: McpServerConfig[] }
|
|
167
|
+
marketplace?: {
|
|
168
|
+
/** If set, only allow installs from matching repos (e.g. ["One-Mipham/*"]) */
|
|
169
|
+
strictKnownMarketplaces?: string[]
|
|
170
|
+
/** Block installs from matching repos (e.g. ["malicious-org/*"]) */
|
|
171
|
+
blockedMarketplaces?: string[]
|
|
172
|
+
}
|
|
165
173
|
}
|
|
166
174
|
|
|
167
175
|
export interface McpServerConfig {
|
|
@@ -287,10 +295,20 @@ export type PermissionMode =
|
|
|
287
295
|
/** Backward-compatible alias: PermissionMode plus legacy 'ask' and 'bypass' */
|
|
288
296
|
export type PermissionLevel = PermissionMode | 'ask' | 'bypass'
|
|
289
297
|
|
|
298
|
+
/** Org-level restrictions that cap or forbid specific permission modes. */
|
|
299
|
+
export interface PermissionRestrictions {
|
|
300
|
+
/** Modes that may not be entered (cycle skips them). */
|
|
301
|
+
forbiddenModes?: PermissionMode[]
|
|
302
|
+
/** Ceiling — modes ranked higher (more permissive) than this are treated as forbidden. */
|
|
303
|
+
maxAllowedMode?: PermissionMode
|
|
304
|
+
}
|
|
305
|
+
|
|
290
306
|
export interface PermissionConfig {
|
|
291
307
|
mode: PermissionMode
|
|
292
308
|
allow: string[]
|
|
293
309
|
deny: string[]
|
|
310
|
+
/** Optional org-level restrictions enforced on every mode transition. */
|
|
311
|
+
restrictions?: PermissionRestrictions
|
|
294
312
|
}
|
|
295
313
|
|
|
296
314
|
export interface PermissionRuleEntry {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "code-review",
|
|
4
|
+
"description": "Automated code review with multiple dimensions — complexity, security, performance, quality",
|
|
5
|
+
"url": "https://github.com/One-Mipham/skill-code-review",
|
|
6
|
+
"file": "code-review.SKILL.md",
|
|
7
|
+
"category": "Development",
|
|
8
|
+
"author": "MiphamAI",
|
|
9
|
+
"builtin": true
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"name": "systematic-debugging",
|
|
13
|
+
"description": "Systematic debugging workflow — find root cause before fixing",
|
|
14
|
+
"url": "https://github.com/anthropics/skills",
|
|
15
|
+
"file": "systematic-debugging.SKILL.md",
|
|
16
|
+
"category": "Development",
|
|
17
|
+
"author": "Anthropic"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "test-driven-development",
|
|
21
|
+
"description": "TDD workflow — write failing test first, then implement",
|
|
22
|
+
"url": "https://github.com/anthropics/skills",
|
|
23
|
+
"file": "test-driven-development.SKILL.md",
|
|
24
|
+
"category": "Development",
|
|
25
|
+
"author": "Anthropic",
|
|
26
|
+
"builtin": true
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "web-access",
|
|
30
|
+
"description": "Web search, scraping, and browser automation — search engines, page fetching, authenticated sites",
|
|
31
|
+
"url": "https://github.com/One-Mipham/skill-web-access",
|
|
32
|
+
"file": "web-access.SKILL.md",
|
|
33
|
+
"category": "Network",
|
|
34
|
+
"author": "MiphamAI",
|
|
35
|
+
"builtin": true
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "doc-generator",
|
|
39
|
+
"description": "Generate API docs, READMEs, and changelogs from code analysis",
|
|
40
|
+
"url": "https://github.com/One-Mipham/skill-doc-generator",
|
|
41
|
+
"file": "doc-generator.SKILL.md",
|
|
42
|
+
"category": "Documentation",
|
|
43
|
+
"author": "MiphamAI",
|
|
44
|
+
"builtin": true
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "github-ops",
|
|
48
|
+
"description": "GitHub PR/issue management and automation — labels, milestones, reviews",
|
|
49
|
+
"url": "https://github.com/One-Mipham/skill-github-ops",
|
|
50
|
+
"file": "github-ops.SKILL.md",
|
|
51
|
+
"category": "DevOps",
|
|
52
|
+
"author": "MiphamAI",
|
|
53
|
+
"builtin": true
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "security-review",
|
|
57
|
+
"description": "Security audit and vulnerability scanning for code changes — OWASP, secrets, supply chain",
|
|
58
|
+
"url": "https://github.com/One-Mipham/skill-security-review",
|
|
59
|
+
"file": "security-review.SKILL.md",
|
|
60
|
+
"category": "Security",
|
|
61
|
+
"author": "MiphamAI",
|
|
62
|
+
"builtin": true
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "frontend-design",
|
|
66
|
+
"description": "Distinctive visual design guidance for UI components — typography, color, layout",
|
|
67
|
+
"url": "https://github.com/anthropics/skills",
|
|
68
|
+
"file": "frontend-design.SKILL.md",
|
|
69
|
+
"category": "Design",
|
|
70
|
+
"author": "Anthropic"
|
|
71
|
+
}
|
|
72
|
+
]
|