@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.
@@ -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
+ }
@@ -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
@@ -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('bypass'),
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
- this.inferenceHookConfig!.organization_id,
288
+ hookConfig.organization_id,
288
289
  )
289
- const verdict = await sendInferenceCheck(this.inferenceHookConfig!, request)
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
- this.inferenceHookConfig!.organization_id,
568
+ hookConfig.organization_id,
567
569
  )
568
- const verdict = await sendInferenceCheck(this.inferenceHookConfig!, request)
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
- /** Valid mode transition order for Shift+Tab cycling. */
22
- export const MODE_CYCLE: PermissionMode[] = [
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
- export function nextMode(current: PermissionMode): PermissionMode {
32
- const idx = MODE_CYCLE.indexOf(current)
33
- return MODE_CYCLE[(idx + 1) % MODE_CYCLE.length]!
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
  }
@@ -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: { mode?: string; allow?: string[]; deny?: string[] }): void {
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<{ mode: PermissionMode; allow: string[]; deny: string[] }>,
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
- if (level === 'auto') this.mode = 'auto'
250
- else if (level === 'bypass') this.mode = 'bypassPermissions'
251
- else this.mode = 'default'
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(messages: Message[], metadata?: { provider?: string; model?: string }): string {
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
- const context = new ContextManager({ maxTokens: 200_000, compactionThreshold: 0.9 })
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.autoSave(context.getMessages(), {
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
+ }
@@ -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
+ ]