@miphamai/cli 0.24.1 → 0.24.3
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/package.json +1 -1
- package/src/agent/sub-agent.ts +14 -0
- package/src/commands/project.ts +44 -2
- package/src/core/engine.ts +38 -1
- package/src/core/workspace-trust.ts +144 -0
- package/src/index.tsx +64 -2
- package/src/ui/commands.ts +4 -0
package/package.json
CHANGED
package/src/agent/sub-agent.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { HookEngine } from '../core/hooks'
|
|
|
8
8
|
import type { PermissionSystem } from '../core/permission'
|
|
9
9
|
import { AgentExperience } from './agent-experience'
|
|
10
10
|
import { PatternAnalyzer } from './pattern-analyzer.js'
|
|
11
|
+
import { getWorkspaceTrust } from '../core/workspace-trust'
|
|
11
12
|
import type { ExperienceRuleEngine } from '../core/rule-engine.js'
|
|
12
13
|
|
|
13
14
|
// Singleton instances (created lazily)
|
|
@@ -200,6 +201,19 @@ export class SubAgent {
|
|
|
200
201
|
// Resolve execution directory: worktree isolation or process cwd
|
|
201
202
|
const execCwd = options.worktreePath || process.cwd()
|
|
202
203
|
|
|
204
|
+
// Check workspace trust for the execution directory.
|
|
205
|
+
// For worktree-isolated agents, auto-trust the worktree if the parent is trusted.
|
|
206
|
+
if (options.worktreePath) {
|
|
207
|
+
const trust = getWorkspaceTrust()
|
|
208
|
+
if (!trust.isTrusted(execCwd)) {
|
|
209
|
+
// Auto-trust worktree directories when the parent workspace is trusted
|
|
210
|
+
if (trust.isTrusted(process.cwd())) {
|
|
211
|
+
trust.trust(execCwd)
|
|
212
|
+
}
|
|
213
|
+
// Otherwise, proceed with a warning — don't block agent execution
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
203
217
|
// Resolve system prompt: agentDef > options.systemPrompt > builtin type
|
|
204
218
|
const systemPrompt =
|
|
205
219
|
agentDef?.systemPrompt || options.systemPrompt || TYPE_SYSTEM_PROMPTS[agentType]
|
package/src/commands/project.ts
CHANGED
|
@@ -6,8 +6,18 @@
|
|
|
6
6
|
* /security, /audit, /prompt-audit
|
|
7
7
|
*/
|
|
8
8
|
import type { CommandHandler, CommandContext, CommandResult } from '../ui/commands.js'
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
import { getWorkspaceTrust } from '../core/workspace-trust'
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
initCmd,
|
|
13
|
+
permissionsCmd,
|
|
14
|
+
recommendCmd,
|
|
15
|
+
setupCmd,
|
|
16
|
+
addDirCmd,
|
|
17
|
+
promptAuditCmd,
|
|
18
|
+
securityCmd,
|
|
19
|
+
trustCmd,
|
|
20
|
+
}
|
|
11
21
|
|
|
12
22
|
const initCmd: CommandHandler = async (ctx) => {
|
|
13
23
|
const { existsSync, mkdirSync, writeFileSync } = await import('node:fs')
|
|
@@ -1004,3 +1014,35 @@ const securityCmd: CommandHandler = async () => {
|
|
|
1004
1014
|
|
|
1005
1015
|
return { content: lines.join('\n') }
|
|
1006
1016
|
}
|
|
1017
|
+
|
|
1018
|
+
const trustCmd: CommandHandler = (ctx) => {
|
|
1019
|
+
const trust = getWorkspaceTrust()
|
|
1020
|
+
const cwd = process.cwd()
|
|
1021
|
+
const trusted = trust.listTrusted()
|
|
1022
|
+
|
|
1023
|
+
const lines: string[] = [
|
|
1024
|
+
'── Workspace Trust ──',
|
|
1025
|
+
'',
|
|
1026
|
+
`Current directory: ${cwd}`,
|
|
1027
|
+
`Trusted: ${trust.isTrusted(cwd) ? '✅ Yes' : '⚠️ No (restart to trust)'}`,
|
|
1028
|
+
`Trust store: ${trust.getStorePath()}`,
|
|
1029
|
+
'',
|
|
1030
|
+
]
|
|
1031
|
+
|
|
1032
|
+
if (trusted.length === 0) {
|
|
1033
|
+
lines.push('No trusted workspaces configured.')
|
|
1034
|
+
lines.push('')
|
|
1035
|
+
lines.push('Trust a workspace: restart Mipham Code in the directory')
|
|
1036
|
+
lines.push('and accept the trust prompt.')
|
|
1037
|
+
} else {
|
|
1038
|
+
lines.push(`Trusted workspaces (${trusted.length}):`)
|
|
1039
|
+
for (const dir of trusted) {
|
|
1040
|
+
const marker = cwd.startsWith(dir) ? ' ← current' : ''
|
|
1041
|
+
lines.push(` • ${dir}${marker}`)
|
|
1042
|
+
}
|
|
1043
|
+
lines.push('')
|
|
1044
|
+
lines.push('Remove a workspace: delete the directory entry from the trust store.')
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
return { content: lines.join('\n') }
|
|
1048
|
+
}
|
package/src/core/engine.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ToolResult,
|
|
5
5
|
InferenceHookConfig,
|
|
6
6
|
CrsiConfig,
|
|
7
|
+
CrossSessionConfig,
|
|
7
8
|
} from '../shared/index.ts'
|
|
8
9
|
import { ProviderRegistry } from '../providers/registry'
|
|
9
10
|
import { ContextManager } from './context'
|
|
@@ -69,6 +70,12 @@ export class QueryEngine {
|
|
|
69
70
|
/** Session identifier for cross-session messaging. Set by the app startup via setSessionId(). */
|
|
70
71
|
private sessionId: string = 'session-1'
|
|
71
72
|
|
|
73
|
+
/** Cross-session inbound policy. Default: 'ask' (messages require user review). */
|
|
74
|
+
private crossSessionConfig: CrossSessionConfig = {
|
|
75
|
+
crossSessionInbound: 'ask',
|
|
76
|
+
dialogExpiry: 300,
|
|
77
|
+
}
|
|
78
|
+
|
|
72
79
|
/** Set the session identifier (called from index.tsx at startup with the actual session name). */
|
|
73
80
|
setSessionId(id: string): void {
|
|
74
81
|
this.sessionId = id
|
|
@@ -79,15 +86,45 @@ export class QueryEngine {
|
|
|
79
86
|
return this.sessionId
|
|
80
87
|
}
|
|
81
88
|
|
|
89
|
+
/** Set the cross-session messaging policy (called from index.tsx at startup). */
|
|
90
|
+
setCrossSessionConfig(config: CrossSessionConfig): void {
|
|
91
|
+
this.crossSessionConfig = config
|
|
92
|
+
}
|
|
93
|
+
|
|
82
94
|
/** Poll the cross-session file inbox for messages addressed to this session. */
|
|
83
95
|
async pollCrossSessionInbox(): Promise<void> {
|
|
96
|
+
const policy = this.crossSessionConfig.crossSessionInbound
|
|
97
|
+
|
|
98
|
+
// 'deny' — silently discard all inbound messages
|
|
99
|
+
if (policy === 'deny') {
|
|
100
|
+
try {
|
|
101
|
+
const transport = getFileInboxTransport()
|
|
102
|
+
await transport.poll(this.sessionId) // read + delete, don't forward
|
|
103
|
+
} catch {
|
|
104
|
+
// Best-effort discard
|
|
105
|
+
}
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
84
109
|
try {
|
|
85
110
|
const transport = getFileInboxTransport()
|
|
86
111
|
const messages = await transport.poll(this.sessionId)
|
|
87
112
|
if (messages.length > 0) {
|
|
88
113
|
const bus = getMessageBus()
|
|
89
114
|
for (const msg of messages) {
|
|
90
|
-
|
|
115
|
+
if (policy === 'ask') {
|
|
116
|
+
// Mark as awaiting approval — the model should verify with the user before acting
|
|
117
|
+
bus.post(
|
|
118
|
+
msg.from,
|
|
119
|
+
msg.to,
|
|
120
|
+
`[Awaiting Approval] ${msg.summary}`,
|
|
121
|
+
`[Cross-session message from ${msg.from} — verify with user before acting]\n\n${msg.message}`,
|
|
122
|
+
'warning',
|
|
123
|
+
)
|
|
124
|
+
} else {
|
|
125
|
+
// 'allow' — forward directly
|
|
126
|
+
bus.post(msg.from, msg.to, msg.summary, msg.message, msg.type)
|
|
127
|
+
}
|
|
91
128
|
}
|
|
92
129
|
}
|
|
93
130
|
} catch (err) {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
2
|
+
import { join, dirname, resolve } from 'node:path'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
|
|
5
|
+
const MIPHAM_HOME = join(homedir(), '.mipham')
|
|
6
|
+
const TRUST_STORE_PATH = join(MIPHAM_HOME, 'trusted-workspaces.json')
|
|
7
|
+
|
|
8
|
+
export interface TrustedWorkspaces {
|
|
9
|
+
version: 1
|
|
10
|
+
directories: string[]
|
|
11
|
+
updatedAt: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolves a directory to its real absolute path, following symlinks.
|
|
16
|
+
* Falls back to the resolved path if realpath fails.
|
|
17
|
+
*/
|
|
18
|
+
function realPath(dir: string): string {
|
|
19
|
+
try {
|
|
20
|
+
// Use resolve to normalize, but don't require the directory to exist yet
|
|
21
|
+
const resolved = resolve(dir)
|
|
22
|
+
return resolved
|
|
23
|
+
} catch {
|
|
24
|
+
return resolve(dir)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Manages the workspace trust store.
|
|
30
|
+
*
|
|
31
|
+
* Trust is hierarchical: a directory is trusted if it or any of its
|
|
32
|
+
* ancestor directories are in the trust store. This means trusting
|
|
33
|
+
* /Users/me/Projects implicitly trusts all subdirectories.
|
|
34
|
+
*/
|
|
35
|
+
export class WorkspaceTrust {
|
|
36
|
+
private store: TrustedWorkspaces
|
|
37
|
+
|
|
38
|
+
constructor() {
|
|
39
|
+
this.store = this.load()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Load the trust store from disk, or return a fresh default. */
|
|
43
|
+
private load(): TrustedWorkspaces {
|
|
44
|
+
try {
|
|
45
|
+
if (!existsSync(TRUST_STORE_PATH)) {
|
|
46
|
+
return { version: 1, directories: [], updatedAt: new Date().toISOString() }
|
|
47
|
+
}
|
|
48
|
+
const raw = readFileSync(TRUST_STORE_PATH, 'utf-8')
|
|
49
|
+
const parsed = JSON.parse(raw) as TrustedWorkspaces
|
|
50
|
+
if (parsed.version !== 1) {
|
|
51
|
+
// Unknown version — reset
|
|
52
|
+
return { version: 1, directories: [], updatedAt: new Date().toISOString() }
|
|
53
|
+
}
|
|
54
|
+
return parsed
|
|
55
|
+
} catch {
|
|
56
|
+
return { version: 1, directories: [], updatedAt: new Date().toISOString() }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Persist the trust store to disk. */
|
|
61
|
+
private save(): void {
|
|
62
|
+
try {
|
|
63
|
+
if (!existsSync(MIPHAM_HOME)) {
|
|
64
|
+
mkdirSync(MIPHAM_HOME, { recursive: true })
|
|
65
|
+
}
|
|
66
|
+
this.store.updatedAt = new Date().toISOString()
|
|
67
|
+
writeFileSync(TRUST_STORE_PATH, JSON.stringify(this.store, null, 2), 'utf-8')
|
|
68
|
+
} catch {
|
|
69
|
+
// Best-effort: don't crash if we can't save
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check whether a directory is trusted.
|
|
75
|
+
* A directory is trusted if it or any ancestor is in the trust list.
|
|
76
|
+
*/
|
|
77
|
+
isTrusted(dir: string): boolean {
|
|
78
|
+
const resolved = realPath(dir)
|
|
79
|
+
const resolvedLower = resolved.toLowerCase()
|
|
80
|
+
|
|
81
|
+
for (const trusted of this.store.directories) {
|
|
82
|
+
const trustedLower = trusted.toLowerCase()
|
|
83
|
+
// Exact match
|
|
84
|
+
if (resolvedLower === trustedLower) return true
|
|
85
|
+
// Ancestor match: trusted dir is a parent of the target
|
|
86
|
+
if (resolvedLower.startsWith(trustedLower + '/')) return true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Add a directory to the trust store. */
|
|
93
|
+
trust(dir: string): void {
|
|
94
|
+
const resolved = realPath(dir)
|
|
95
|
+
// Don't add duplicates
|
|
96
|
+
if (this.store.directories.includes(resolved)) return
|
|
97
|
+
// Don't add subdirectories of already-trusted paths
|
|
98
|
+
if (this.isTrusted(resolved)) return
|
|
99
|
+
|
|
100
|
+
this.store.directories.push(resolved)
|
|
101
|
+
// Sort for readability
|
|
102
|
+
this.store.directories.sort()
|
|
103
|
+
this.save()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Remove a directory from the trust store (and any subdirectories). */
|
|
107
|
+
untrust(dir: string): void {
|
|
108
|
+
const resolved = realPath(dir)
|
|
109
|
+
const resolvedLower = resolved.toLowerCase()
|
|
110
|
+
|
|
111
|
+
this.store.directories = this.store.directories.filter((d) => {
|
|
112
|
+
const lower = d.toLowerCase()
|
|
113
|
+
// Remove exact match and all subdirectories
|
|
114
|
+
return lower !== resolvedLower && !lower.startsWith(resolvedLower + '/')
|
|
115
|
+
})
|
|
116
|
+
this.save()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** List all trusted directories. */
|
|
120
|
+
listTrusted(): string[] {
|
|
121
|
+
return [...this.store.directories]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Get the path to the trust store. */
|
|
125
|
+
getStorePath(): string {
|
|
126
|
+
return TRUST_STORE_PATH
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Singleton ──
|
|
131
|
+
|
|
132
|
+
let _instance: WorkspaceTrust | null = null
|
|
133
|
+
|
|
134
|
+
export function getWorkspaceTrust(): WorkspaceTrust {
|
|
135
|
+
if (!_instance) {
|
|
136
|
+
_instance = new WorkspaceTrust()
|
|
137
|
+
}
|
|
138
|
+
return _instance
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Reset the singleton (for tests). */
|
|
142
|
+
export function resetWorkspaceTrust(): void {
|
|
143
|
+
_instance = null
|
|
144
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import React from 'react'
|
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { existsSync } from 'node:fs'
|
|
4
4
|
import { render } from 'ink'
|
|
5
|
+
import * as readline from 'node:readline'
|
|
5
6
|
import { App } from './ui/app'
|
|
6
7
|
import {
|
|
7
8
|
loadConfig,
|
|
@@ -33,6 +34,7 @@ import { AgentRegistry } from './agent/agent-registry'
|
|
|
33
34
|
import { HookEngine } from './core/hooks'
|
|
34
35
|
import { ArtifactServer } from './artifacts/server'
|
|
35
36
|
import { getMetrics } from './core/metrics'
|
|
37
|
+
import { getWorkspaceTrust } from './core/workspace-trust'
|
|
36
38
|
import { ARTIFACTS_DIR, ARTIFACT_PORT, MIPHAM_DIR } from './shared/constants'
|
|
37
39
|
import { AgentViewManager } from './agent-view/agent-view-manager'
|
|
38
40
|
import { AgentViewDashboard } from './agent-view/dashboard'
|
|
@@ -52,10 +54,69 @@ interface RunOptions {
|
|
|
52
54
|
version?: string
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Check workspace trust before starting the session.
|
|
59
|
+
* If the cwd is not trusted, prompt the user via stdin.
|
|
60
|
+
* Exits the process if the user declines.
|
|
61
|
+
*/
|
|
62
|
+
async function checkWorkspaceTrust(): Promise<void> {
|
|
63
|
+
const cwd = process.cwd()
|
|
64
|
+
const trust = getWorkspaceTrust()
|
|
65
|
+
|
|
66
|
+
if (trust.isTrusted(cwd)) return
|
|
67
|
+
|
|
68
|
+
// Non-interactive mode (piped stdin, headless) — skip prompt, proceed
|
|
69
|
+
if (!process.stdin.isTTY) return
|
|
70
|
+
|
|
71
|
+
const rl = readline.createInterface({
|
|
72
|
+
input: process.stdin,
|
|
73
|
+
output: process.stderr, // use stderr to avoid interfering with stdout rendering
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const question = (prompt: string): Promise<string> =>
|
|
77
|
+
new Promise((resolve) => {
|
|
78
|
+
rl.question(prompt, (answer) => {
|
|
79
|
+
resolve(answer.trim().toLowerCase())
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
process.stderr.write('\n')
|
|
84
|
+
process.stderr.write('╔══════════════════════════════════════════════╗\n')
|
|
85
|
+
process.stderr.write('║ ⚠️ Workspace Trust ║\n')
|
|
86
|
+
process.stderr.write('╠══════════════════════════════════════════════╣\n')
|
|
87
|
+
process.stderr.write('║ ║\n')
|
|
88
|
+
process.stderr.write(`║ Directory not trusted: ║\n`)
|
|
89
|
+
process.stderr.write(`║ ${cwd.slice(0, 42).padEnd(42)}║\n`)
|
|
90
|
+
process.stderr.write('║ ║\n')
|
|
91
|
+
process.stderr.write('║ Trust this workspace? ║\n')
|
|
92
|
+
process.stderr.write('║ [Y] Trust and continue ║\n')
|
|
93
|
+
process.stderr.write('║ [N] Exit ║\n')
|
|
94
|
+
process.stderr.write('║ ║\n')
|
|
95
|
+
process.stderr.write('╚══════════════════════════════════════════════╝\n')
|
|
96
|
+
process.stderr.write('\n')
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const answer = await question('Trust this workspace? [Y/N]: ')
|
|
100
|
+
if (answer === 'y' || answer === 'yes') {
|
|
101
|
+
trust.trust(cwd)
|
|
102
|
+
process.stderr.write(`✓ Workspace trusted: ${cwd}\n\n`)
|
|
103
|
+
} else {
|
|
104
|
+
process.stderr.write('✗ Workspace not trusted. Exiting.\n')
|
|
105
|
+
rl.close()
|
|
106
|
+
process.exit(1)
|
|
107
|
+
}
|
|
108
|
+
} finally {
|
|
109
|
+
rl.close()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
55
113
|
export async function runApp(options: RunOptions): Promise<void> {
|
|
56
114
|
// Metrics: count CLI invocation
|
|
57
115
|
getMetrics().cliInvocations.inc()
|
|
58
116
|
|
|
117
|
+
// ── Workspace Trust Check ──
|
|
118
|
+
await checkWorkspaceTrust()
|
|
119
|
+
|
|
59
120
|
// Handle `mipham agents` subcommand — launch standalone dashboard
|
|
60
121
|
const args = process.argv.slice(2)
|
|
61
122
|
if (args[0] === 'agents') {
|
|
@@ -318,8 +379,9 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
318
379
|
heartbeatInterval.unref()
|
|
319
380
|
}
|
|
320
381
|
|
|
321
|
-
// Load cross-session config
|
|
322
|
-
loadCrossSessionConfig()
|
|
382
|
+
// Load cross-session config and wire into the engine's inbound policy
|
|
383
|
+
const crossSessionConfig = loadCrossSessionConfig(process.cwd())
|
|
384
|
+
engine.setCrossSessionConfig(crossSessionConfig)
|
|
323
385
|
|
|
324
386
|
// Auto-save session on exit
|
|
325
387
|
let saved = false
|
package/src/ui/commands.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
addDirCmd,
|
|
36
36
|
securityCmd,
|
|
37
37
|
promptAuditCmd,
|
|
38
|
+
trustCmd,
|
|
38
39
|
} from '../commands/project.js'
|
|
39
40
|
import { themeCmd, releaseNotesCmd, ideCmd, terminalSetupCmd } from '../commands/environment.js'
|
|
40
41
|
import { commitCmd, pushCmd, prCmd, issueCmd } from '../commands/git.js'
|
|
@@ -3215,6 +3216,7 @@ const commandsListCmd: CommandHandler = () => {
|
|
|
3215
3216
|
'/recommend': 'Project',
|
|
3216
3217
|
'/security': 'Project',
|
|
3217
3218
|
'/audit': 'Project',
|
|
3219
|
+
'/trust': 'Project',
|
|
3218
3220
|
'/prompt-audit': 'Code Quality',
|
|
3219
3221
|
'/ide': 'Environment',
|
|
3220
3222
|
'/terminal-setup': 'Environment',
|
|
@@ -3371,6 +3373,7 @@ registry.set('/permissions', permissionsCmd)
|
|
|
3371
3373
|
registry.set('/add-dir', addDirCmd)
|
|
3372
3374
|
registry.set('/security', securityCmd)
|
|
3373
3375
|
registry.set('/audit', securityCmd)
|
|
3376
|
+
registry.set('/trust', trustCmd)
|
|
3374
3377
|
registry.set('/prompt-audit', promptAuditCmd)
|
|
3375
3378
|
|
|
3376
3379
|
// Environment
|
|
@@ -3513,6 +3516,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
3513
3516
|
'/recommend': 'Analyze project + recommend skills & setup',
|
|
3514
3517
|
'/security': 'Security review checklist',
|
|
3515
3518
|
'/audit': 'Same as /security',
|
|
3519
|
+
'/trust': 'Show and manage trusted workspaces',
|
|
3516
3520
|
'/prompt-audit': 'Audit prompts for modern model optimization',
|
|
3517
3521
|
'/ide': 'IDE integration guide',
|
|
3518
3522
|
'/terminal-setup': 'Shell & terminal config',
|