@miphamai/cli 0.22.0 → 0.23.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/README.md +1 -1
- package/package.json +1 -1
- package/skills/workflows/audit.js +66 -18
- package/src/agent/cross-session/discovery.ts +99 -0
- package/src/agent/cross-session/file-inbox.ts +118 -0
- package/src/agent/cross-session/transport.ts +17 -0
- package/src/agent/message-router.ts +83 -0
- package/src/config/defaults.ts +6 -0
- package/src/config/loader.ts +33 -0
- package/src/core/credential-masker/env-filter.ts +39 -0
- package/src/core/credential-masker/index.ts +35 -0
- package/src/core/credential-masker/matcher.ts +106 -0
- package/src/core/credential-masker/output-scrub.ts +27 -0
- package/src/core/credential-masker/pipeline.ts +67 -0
- package/src/core/credential-masker/strategies/aws.ts +50 -0
- package/src/core/credential-masker/strategies/extract.ts +106 -0
- package/src/core/credential-masker/strategies/full.ts +15 -0
- package/src/core/credential-masker/strategies/jwt.ts +75 -0
- package/src/core/credential-masker/types.ts +16 -0
- package/src/core/credential-masker.ts +12 -187
- package/src/core/engine.ts +44 -9
- package/src/core/session-store.ts +15 -2
- package/src/index.tsx +44 -1
- package/src/shared/types.ts +55 -5
- package/src/tools/agent/list-agents.ts +67 -0
- package/src/tools/agent/send-message.ts +21 -24
- package/src/tools/exec/bash.ts +67 -3
- package/src/tools/index.ts +2 -0
package/src/core/engine.ts
CHANGED
|
@@ -18,6 +18,8 @@ import { getBackgroundAgentRegistry } from '../agent/background-registry'
|
|
|
18
18
|
import { RulesLoader } from './rules-loader'
|
|
19
19
|
import { UsageTracker } from './usage-tracker'
|
|
20
20
|
import { buildRequest, sendInferenceCheck, isInferenceHookEnabled } from './inference-hook'
|
|
21
|
+
import { getFileInboxTransport } from '../agent/cross-session/file-inbox'
|
|
22
|
+
import { getMessageBus } from '../agent/message-bus'
|
|
21
23
|
import { createT } from '../i18n-core/t'
|
|
22
24
|
import enUS from '../i18n-core/locales/en-US.json'
|
|
23
25
|
import zhCN from '../i18n-core/locales/zh-CN.json'
|
|
@@ -54,6 +56,36 @@ export class QueryEngine {
|
|
|
54
56
|
private permission: PermissionSystem = new PermissionSystem('default'),
|
|
55
57
|
) {}
|
|
56
58
|
|
|
59
|
+
/** Session identifier for cross-session messaging. Set by the app startup via setSessionId(). */
|
|
60
|
+
private sessionId: string = 'session-1'
|
|
61
|
+
|
|
62
|
+
/** Set the session identifier (called from index.tsx at startup with the actual session name). */
|
|
63
|
+
setSessionId(id: string): void {
|
|
64
|
+
this.sessionId = id
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Get the current session identifier. */
|
|
68
|
+
getSessionId(): string {
|
|
69
|
+
return this.sessionId
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Poll the cross-session file inbox for messages addressed to this session. */
|
|
73
|
+
async pollCrossSessionInbox(): Promise<void> {
|
|
74
|
+
try {
|
|
75
|
+
const transport = getFileInboxTransport()
|
|
76
|
+
const messages = await transport.poll(this.sessionId)
|
|
77
|
+
if (messages.length > 0) {
|
|
78
|
+
const bus = getMessageBus()
|
|
79
|
+
for (const msg of messages) {
|
|
80
|
+
bus.post(msg.from, msg.to, msg.summary, msg.message, msg.type)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
// Best-effort: don't let a failed inbox poll break the turn
|
|
85
|
+
console.warn('Cross-session inbox poll failed:', err)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
57
89
|
/** Register a hook engine for pre/post tool-use lifecycle events. */
|
|
58
90
|
setHookEngine(hooks: HookEngine): void {
|
|
59
91
|
this.hookEngine = hooks
|
|
@@ -255,9 +287,12 @@ export class QueryEngine {
|
|
|
255
287
|
}
|
|
256
288
|
|
|
257
289
|
async *process(userInput: string, signal?: AbortSignal): AsyncGenerator<StreamChunk> {
|
|
290
|
+
// Poll for cross-session messages before each turn
|
|
291
|
+
await this.pollCrossSessionInbox()
|
|
292
|
+
|
|
258
293
|
// Fire UserPromptSubmit hooks before processing
|
|
259
294
|
if (this.hookEngine) {
|
|
260
|
-
const submitResult = await this.hookEngine.executeUserPromptSubmit(userInput,
|
|
295
|
+
const submitResult = await this.hookEngine.executeUserPromptSubmit(userInput, this.sessionId)
|
|
261
296
|
if (submitResult.additionalContext) {
|
|
262
297
|
this.context.addMessage({
|
|
263
298
|
role: 'user',
|
|
@@ -292,7 +327,7 @@ export class QueryEngine {
|
|
|
292
327
|
const model = this.registry.getActiveModel()
|
|
293
328
|
const request = buildRequest(
|
|
294
329
|
messages,
|
|
295
|
-
|
|
330
|
+
this.sessionId,
|
|
296
331
|
provider,
|
|
297
332
|
model,
|
|
298
333
|
hookConfig.organization_id,
|
|
@@ -587,7 +622,7 @@ export class QueryEngine {
|
|
|
587
622
|
const model = this.registry.getActiveModel()
|
|
588
623
|
const request = buildRequest(
|
|
589
624
|
messages,
|
|
590
|
-
|
|
625
|
+
this.sessionId,
|
|
591
626
|
provider,
|
|
592
627
|
model,
|
|
593
628
|
hookConfig.organization_id,
|
|
@@ -762,7 +797,7 @@ export class QueryEngine {
|
|
|
762
797
|
// Run PreToolUse hooks
|
|
763
798
|
let effectiveParams = params
|
|
764
799
|
if (this.hookEngine) {
|
|
765
|
-
const preResult = await this.hookEngine.executePreToolUse(name, params,
|
|
800
|
+
const preResult = await this.hookEngine.executePreToolUse(name, params, this.sessionId)
|
|
766
801
|
if (!preResult.allowed) {
|
|
767
802
|
return {
|
|
768
803
|
success: false,
|
|
@@ -778,7 +813,7 @@ export class QueryEngine {
|
|
|
778
813
|
try {
|
|
779
814
|
const result = await tool.execute(effectiveParams, {
|
|
780
815
|
cwd: process.cwd(),
|
|
781
|
-
sessionId:
|
|
816
|
+
sessionId: this.sessionId,
|
|
782
817
|
provider: this.registry.getActive().config.id,
|
|
783
818
|
model: this.registry.getActiveModel(),
|
|
784
819
|
skillsLoader: this.skillsLoader,
|
|
@@ -795,7 +830,7 @@ export class QueryEngine {
|
|
|
795
830
|
|
|
796
831
|
// Run PostToolUse hooks
|
|
797
832
|
if (this.hookEngine) {
|
|
798
|
-
await this.hookEngine.executePostToolUse(name, effectiveParams, result,
|
|
833
|
+
await this.hookEngine.executePostToolUse(name, effectiveParams, result, this.sessionId)
|
|
799
834
|
}
|
|
800
835
|
|
|
801
836
|
return result
|
|
@@ -867,7 +902,7 @@ export class QueryEngine {
|
|
|
867
902
|
/** Wrap context compaction with PreCompact/PostCompact hooks. */
|
|
868
903
|
private async compactWithHooks(heading: string): Promise<void> {
|
|
869
904
|
if (this.hookEngine) {
|
|
870
|
-
const preResult = await this.hookEngine.executePreCompact(
|
|
905
|
+
const preResult = await this.hookEngine.executePreCompact(this.sessionId)
|
|
871
906
|
if (preResult.additionalContext) {
|
|
872
907
|
this.context.addMessage({
|
|
873
908
|
role: 'user',
|
|
@@ -879,7 +914,7 @@ export class QueryEngine {
|
|
|
879
914
|
await this.context.compact(heading)
|
|
880
915
|
|
|
881
916
|
if (this.hookEngine) {
|
|
882
|
-
const postResult = await this.hookEngine.executePostCompact(
|
|
917
|
+
const postResult = await this.hookEngine.executePostCompact(this.sessionId)
|
|
883
918
|
if (postResult.additionalContext) {
|
|
884
919
|
this.context.addMessage({
|
|
885
920
|
role: 'user',
|
|
@@ -893,7 +928,7 @@ export class QueryEngine {
|
|
|
893
928
|
private async *checkStopHook(signal?: AbortSignal): AsyncGenerator<StreamChunk> {
|
|
894
929
|
if (!this.hookEngine) return
|
|
895
930
|
|
|
896
|
-
const stopResult = await this.hookEngine.executeStop(
|
|
931
|
+
const stopResult = await this.hookEngine.executeStop(this.sessionId)
|
|
897
932
|
if (stopResult.decision === 'block') {
|
|
898
933
|
// Feed the block reason back to the AI and continue
|
|
899
934
|
this.context.addMessage({
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
statSync,
|
|
10
10
|
} from 'node:fs'
|
|
11
11
|
import { join } from 'node:path'
|
|
12
|
+
import { createHash } from 'node:crypto'
|
|
12
13
|
import type { Message } from '../shared/types'
|
|
13
14
|
|
|
14
15
|
export interface SessionMetadata {
|
|
@@ -50,7 +51,15 @@ function ensureDir(): void {
|
|
|
50
51
|
|
|
51
52
|
function filePath(name: string): string {
|
|
52
53
|
// Sanitize name for filesystem
|
|
53
|
-
const safe = name.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
54
|
+
const safe = name.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
55
|
+
|
|
56
|
+
// For very long names (>100 chars), use SHA-256 hash suffix to prevent collision
|
|
57
|
+
if (safe.length > 100) {
|
|
58
|
+
const hash = createHash('sha256').update(safe).digest('hex').slice(0, 16)
|
|
59
|
+
const truncated = safe.slice(0, 80)
|
|
60
|
+
return join(SESSIONS_DIR, `${truncated}-${hash}.jsonl`)
|
|
61
|
+
}
|
|
62
|
+
|
|
54
63
|
return join(SESSIONS_DIR, `${safe}.jsonl`)
|
|
55
64
|
}
|
|
56
65
|
|
|
@@ -233,7 +242,11 @@ export class SessionStore {
|
|
|
233
242
|
ensureDir()
|
|
234
243
|
mkdirSync(SUMMARIES_DIR, { recursive: true })
|
|
235
244
|
|
|
236
|
-
const
|
|
245
|
+
const safeRaw = name.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
246
|
+
const safe =
|
|
247
|
+
safeRaw.length > 100
|
|
248
|
+
? `${safeRaw.slice(0, 80)}-${createHash('sha256').update(safeRaw).digest('hex').slice(0, 16)}`
|
|
249
|
+
: safeRaw
|
|
237
250
|
const summaryPath = join(SUMMARIES_DIR, `${safe}.md`)
|
|
238
251
|
writeFileSync(summaryPath, `# ${name}\n\n${summary}\n\nTags: ${tags.join(', ')}\n`, 'utf-8')
|
|
239
252
|
|
package/src/index.tsx
CHANGED
|
@@ -3,7 +3,18 @@ import { join } from 'node:path'
|
|
|
3
3
|
import { existsSync } from 'node:fs'
|
|
4
4
|
import { render } from 'ink'
|
|
5
5
|
import { App } from './ui/app'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
loadConfig,
|
|
8
|
+
loadInferenceHookConfig,
|
|
9
|
+
loadCredentialMaskingConfig,
|
|
10
|
+
loadCrossSessionConfig,
|
|
11
|
+
} from './config/loader'
|
|
12
|
+
import {
|
|
13
|
+
registerActiveSession,
|
|
14
|
+
heartbeatSession,
|
|
15
|
+
unregisterSession,
|
|
16
|
+
createSessionInfo,
|
|
17
|
+
} from './agent/cross-session/discovery'
|
|
7
18
|
import { bootstrapProviders } from './providers/bootstrap'
|
|
8
19
|
import { InstructionsLoader } from './core/instructions'
|
|
9
20
|
import { loadSessionMemories, getMemoryManager } from './core/memory/memory-loader'
|
|
@@ -246,6 +257,10 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
246
257
|
const inferenceHookConfig = loadInferenceHookConfig()
|
|
247
258
|
engine.setInferenceHookConfig(inferenceHookConfig)
|
|
248
259
|
|
|
260
|
+
// Initialize credential masking pipeline (strategies: Full, Extract, JWT)
|
|
261
|
+
const { initializePipeline } = await import('./core/credential-masker/index')
|
|
262
|
+
initializePipeline()
|
|
263
|
+
|
|
249
264
|
// Wire credential masking configuration into tools
|
|
250
265
|
const credentialMaskingConfig = loadCredentialMaskingConfig()
|
|
251
266
|
const { setCredentialMaskingConfigForRead } = await import('./tools/file/read')
|
|
@@ -280,10 +295,36 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
280
295
|
|
|
281
296
|
engine.setupContextSummarizer()
|
|
282
297
|
|
|
298
|
+
// ── Cross-session messaging: register session, heartbeat, shutdown ──
|
|
299
|
+
engine.setSessionId(sessionName)
|
|
300
|
+
const sessionInfo = createSessionInfo(
|
|
301
|
+
sessionName,
|
|
302
|
+
sessionName,
|
|
303
|
+
process.cwd(),
|
|
304
|
+
defaultProvider,
|
|
305
|
+
defaultModel,
|
|
306
|
+
)
|
|
307
|
+
registerActiveSession(sessionInfo)
|
|
308
|
+
|
|
309
|
+
// 30-second heartbeat to keep the session file mtime fresh
|
|
310
|
+
const heartbeatInterval = setInterval(() => {
|
|
311
|
+
heartbeatSession(sessionName)
|
|
312
|
+
}, 30_000)
|
|
313
|
+
|
|
314
|
+
// Allow heartbeat to not keep the process alive
|
|
315
|
+
if (heartbeatInterval.unref) {
|
|
316
|
+
heartbeatInterval.unref()
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Load cross-session config (available for MessageRouter and future consumers)
|
|
320
|
+
loadCrossSessionConfig()
|
|
321
|
+
|
|
283
322
|
// Auto-save session on exit
|
|
284
323
|
let saved = false
|
|
285
324
|
const saveAndExit = () => {
|
|
286
325
|
saved = true
|
|
326
|
+
clearInterval(heartbeatInterval)
|
|
327
|
+
unregisterSession(sessionName)
|
|
287
328
|
artifactServer.stop()
|
|
288
329
|
if (context.getMessageCount() > 0) {
|
|
289
330
|
SessionStore.save(sessionName, context.getMessages(), {
|
|
@@ -300,6 +341,8 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
300
341
|
|
|
301
342
|
// Safety net: auto-save on exit for paths that bypass saveAndExit
|
|
302
343
|
process.on('exit', () => {
|
|
344
|
+
clearInterval(heartbeatInterval)
|
|
345
|
+
unregisterSession(sessionName)
|
|
303
346
|
if (!saved && context.getMessageCount() > 0) {
|
|
304
347
|
SessionStore.autoSave(context.getMessages(), {
|
|
305
348
|
provider: defaultProvider,
|
package/src/shared/types.ts
CHANGED
|
@@ -375,17 +375,44 @@ export interface CredentialFullMaskRule {
|
|
|
375
375
|
mode: 'full'
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
/** Per-extract-pattern configuration with optional field-based extraction. */
|
|
379
|
+
export interface CredentialExtractPattern {
|
|
380
|
+
/** Regex pattern to match. When `field` is set, applied to field value only. */
|
|
381
|
+
pattern: string
|
|
382
|
+
/** Optional replacement string. Defaults to CREDENTIAL_SENTINEL. */
|
|
383
|
+
replacement?: string
|
|
384
|
+
/** 🆕 JSON key to extract before applying pattern. If unset, pattern runs against full content. */
|
|
385
|
+
field?: string
|
|
386
|
+
}
|
|
387
|
+
|
|
378
388
|
/** Extract-based masking rule: only regex-matched tokens are replaced. */
|
|
379
389
|
export interface CredentialExtractRule {
|
|
380
390
|
path: string
|
|
381
391
|
mode: 'extract'
|
|
382
|
-
extract:
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
392
|
+
extract: CredentialExtractPattern[]
|
|
393
|
+
/** 🆕 Behavior when no extract pattern matches: 'mask' (replace all) or 'passthrough' (keep). Default: 'mask'. */
|
|
394
|
+
onExtractNoMatch?: 'mask' | 'passthrough'
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** 🆕 JWT-aware masking rule: decode payload and mask specified claims. */
|
|
398
|
+
export interface JwtMaskingRule {
|
|
399
|
+
path: string
|
|
400
|
+
type: 'jwt'
|
|
401
|
+
decode: 'jwt'
|
|
402
|
+
/** Claim names to mask in the JWT payload (e.g. ["sub", "email"]). */
|
|
403
|
+
maskClaims: string[]
|
|
386
404
|
}
|
|
387
405
|
|
|
388
|
-
|
|
406
|
+
/** 🆕 AWS credential pair masking rule: detect key pairs and optionally re-sign. */
|
|
407
|
+
export interface AwsMaskingRule {
|
|
408
|
+
path: string
|
|
409
|
+
type: 'aws'
|
|
410
|
+
awsPairs: boolean
|
|
411
|
+
sigv4: boolean
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export type CredentialFileRule =
|
|
415
|
+
CredentialFullMaskRule | CredentialExtractRule | JwtMaskingRule | AwsMaskingRule
|
|
389
416
|
|
|
390
417
|
/** Configuration for credential masking, loaded from config.yml. */
|
|
391
418
|
export interface CredentialMaskingConfig {
|
|
@@ -401,6 +428,29 @@ export interface CredentialMaskingConfig {
|
|
|
401
428
|
}
|
|
402
429
|
}
|
|
403
430
|
|
|
431
|
+
// ── Cross-Session Messaging Types ──
|
|
432
|
+
|
|
433
|
+
/** Controls how inbound cross-session messages are handled. */
|
|
434
|
+
export type CrossSessionInbound = 'allow' | 'ask' | 'deny'
|
|
435
|
+
|
|
436
|
+
/** 🆕 Configuration for cross-session messaging. */
|
|
437
|
+
export interface CrossSessionConfig {
|
|
438
|
+
crossSessionInbound: CrossSessionInbound
|
|
439
|
+
dialogExpiry: number // seconds
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Session information exposed via ListAgents. */
|
|
443
|
+
export interface SessionInfo {
|
|
444
|
+
id: string
|
|
445
|
+
name: string
|
|
446
|
+
machine: string
|
|
447
|
+
pid: number
|
|
448
|
+
startedAt: string
|
|
449
|
+
cwd?: string
|
|
450
|
+
provider?: string
|
|
451
|
+
model?: string
|
|
452
|
+
}
|
|
453
|
+
|
|
404
454
|
// ── Background Agent Types ──
|
|
405
455
|
|
|
406
456
|
export interface BackgroundAgentConfig {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
2
|
+
import { discoverSessions } from '../../agent/cross-session/discovery'
|
|
3
|
+
|
|
4
|
+
export const listAgentsTool: ToolDefinition = {
|
|
5
|
+
name: 'ListAgents',
|
|
6
|
+
description:
|
|
7
|
+
'Discover active Mipham Code sessions running on this machine. ' +
|
|
8
|
+
'Returns session ID, name, machine, working directory, provider, and model for each session. ' +
|
|
9
|
+
'Use with SendMessage to communicate across sessions.',
|
|
10
|
+
category: 'agent',
|
|
11
|
+
permission: 'auto',
|
|
12
|
+
parameters: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
scope: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
enum: ['local', 'all'],
|
|
18
|
+
default: 'local',
|
|
19
|
+
description:
|
|
20
|
+
'Discovery scope. "local" scans this machine only. "all" is reserved for future network discovery.',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
async execute(params) {
|
|
25
|
+
const scope = (params.scope as string) || 'local'
|
|
26
|
+
|
|
27
|
+
if (scope === 'all') {
|
|
28
|
+
return {
|
|
29
|
+
success: true,
|
|
30
|
+
content:
|
|
31
|
+
'Network discovery is not yet available. Showing local sessions only.\n\n' +
|
|
32
|
+
formatSessionList(discoverSessions()),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const sessions = discoverSessions()
|
|
37
|
+
|
|
38
|
+
if (sessions.length === 0) {
|
|
39
|
+
return {
|
|
40
|
+
success: true,
|
|
41
|
+
content: 'No active Mipham Code sessions found on this machine.',
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
success: true,
|
|
47
|
+
content: formatSessionList(sessions),
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function formatSessionList(sessions: import('../../shared/types').SessionInfo[]): string {
|
|
53
|
+
const lines: string[] = [`${sessions.length} active session(s):\n`]
|
|
54
|
+
|
|
55
|
+
for (const s of sessions) {
|
|
56
|
+
const modelInfo = s.model ? ` · ${s.model}` : ''
|
|
57
|
+
const providerInfo = s.provider ? ` (${s.provider})` : ''
|
|
58
|
+
lines.push(` ${s.id}`)
|
|
59
|
+
lines.push(` Name: ${s.name}`)
|
|
60
|
+
lines.push(` Machine: ${s.machine}${modelInfo}${providerInfo}`)
|
|
61
|
+
lines.push(` PID: ${s.pid} · Started: ${s.startedAt}`)
|
|
62
|
+
if (s.cwd) lines.push(` CWD: ${s.cwd}`)
|
|
63
|
+
lines.push('')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return lines.join('\n').trim()
|
|
67
|
+
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import type { ToolDefinition } from '../../shared/index.ts'
|
|
2
|
-
import {
|
|
2
|
+
import { getMessageRouter } from '../../agent/message-router'
|
|
3
3
|
|
|
4
4
|
export const sendMessageTool: ToolDefinition = {
|
|
5
5
|
name: 'SendMessage',
|
|
6
6
|
description:
|
|
7
|
-
'Send a message to another agent or
|
|
8
|
-
'Use "main"
|
|
9
|
-
'or a
|
|
10
|
-
'Messages are stored in the AgentMessageBus and can be polled by the recipient.',
|
|
7
|
+
'Send a message to another agent or session. ' +
|
|
8
|
+
'Use "main" for the parent conversation, a background task ID for same-process agents, ' +
|
|
9
|
+
'or a session ID for cross-session messaging (use ListAgents to discover sessions).',
|
|
11
10
|
category: 'agent',
|
|
12
11
|
permission: 'auto',
|
|
13
12
|
parameters: {
|
|
@@ -16,7 +15,7 @@ export const sendMessageTool: ToolDefinition = {
|
|
|
16
15
|
to: {
|
|
17
16
|
type: 'string',
|
|
18
17
|
description:
|
|
19
|
-
'Recipient: "main" for the parent conversation, a background task ID, or
|
|
18
|
+
'Recipient: "main" for the parent conversation, a background task ID, or a session ID for cross-session messaging.',
|
|
20
19
|
},
|
|
21
20
|
summary: {
|
|
22
21
|
type: 'string',
|
|
@@ -34,34 +33,32 @@ export const sendMessageTool: ToolDefinition = {
|
|
|
34
33
|
const summary = (params.summary as string) || '(no subject)'
|
|
35
34
|
const message = params.message as string
|
|
36
35
|
|
|
37
|
-
// Determine sender: use sessionId or 'main'
|
|
38
36
|
const from =
|
|
39
37
|
ctx.sessionId === 'sub-agent'
|
|
40
38
|
? `sub-agent-${Date.now().toString(36)}`
|
|
41
39
|
: ctx.sessionId || 'main'
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const msgId = bus.post(from, to, summary, message)
|
|
41
|
+
const router = getMessageRouter()
|
|
42
|
+
const result = await router.route(from, to, summary, message)
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
success: true,
|
|
51
|
-
content:
|
|
52
|
-
`── Message Sent ──\n\n` +
|
|
53
|
-
`ID: ${msgId}\n` +
|
|
54
|
-
`From: ${from}\n` +
|
|
55
|
-
`To: ${to}\n` +
|
|
56
|
-
`Summary: ${summary.slice(0, 100)}\n\n` +
|
|
57
|
-
`The recipient has ${unreadForRecipient} unread message(s).`,
|
|
58
|
-
}
|
|
59
|
-
} catch (err) {
|
|
44
|
+
if (!result.success) {
|
|
60
45
|
return {
|
|
61
46
|
success: false,
|
|
62
47
|
content: '',
|
|
63
|
-
error: `Failed to send message: ${
|
|
48
|
+
error: `Failed to send message: ${result.error}`,
|
|
64
49
|
}
|
|
65
50
|
}
|
|
51
|
+
|
|
52
|
+
const routedLabel = result.routedTo === 'bus' ? 'in-process' : 'cross-session'
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
success: true,
|
|
56
|
+
content:
|
|
57
|
+
`── Message Sent (${routedLabel}) ──\n\n` +
|
|
58
|
+
`ID: ${result.messageId}\n` +
|
|
59
|
+
`From: ${from}\n` +
|
|
60
|
+
`To: ${to}\n` +
|
|
61
|
+
`Summary: ${summary.slice(0, 100)}`,
|
|
62
|
+
}
|
|
66
63
|
},
|
|
67
64
|
}
|
package/src/tools/exec/bash.ts
CHANGED
|
@@ -122,6 +122,57 @@ function isBlocked(command: string): string | null {
|
|
|
122
122
|
return null // safe
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Detect sandbox violations from stderr output.
|
|
127
|
+
* Parses common OS-level error patterns indicating denied access.
|
|
128
|
+
*/
|
|
129
|
+
export function detectViolations(stderr: string): string[] {
|
|
130
|
+
const violations: string[] = []
|
|
131
|
+
|
|
132
|
+
// File access violations
|
|
133
|
+
const accessPatterns = /(?:Permission denied|EACCES|EPERM|Operation not permitted)/gi
|
|
134
|
+
const accessMatches = stderr.match(accessPatterns)
|
|
135
|
+
if (accessMatches && accessMatches.length > 0) {
|
|
136
|
+
// Extract file paths from error messages
|
|
137
|
+
const pathPattern =
|
|
138
|
+
/(?:Permission denied|EACCES|EPERM|Operation not permitted).*?['"]?(\/[^\s'"]+)['"]?/gi
|
|
139
|
+
const paths: string[] = []
|
|
140
|
+
let match: RegExpExecArray | null
|
|
141
|
+
while ((match = pathPattern.exec(stderr)) !== null) {
|
|
142
|
+
paths.push(match[1]!)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (paths.length > 0) {
|
|
146
|
+
violations.push(` File access denied: ${paths.join(', ')}`)
|
|
147
|
+
} else {
|
|
148
|
+
violations.push(` File access denied (${accessMatches.length} occurrence(s))`)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Network access violations
|
|
153
|
+
const netPatterns =
|
|
154
|
+
/(?:Network is unreachable|Connection refused|ECONNREFUSED|ENETUNREACH|Could not resolve host|Name or service not known|ETIMEDOUT|Connection timed out)/gi
|
|
155
|
+
const netMatches = stderr.match(netPatterns)
|
|
156
|
+
if (netMatches && netMatches.length > 0) {
|
|
157
|
+
// Extract host:port from error messages
|
|
158
|
+
const hostPattern =
|
|
159
|
+
/(?:connect to|Could not resolve host|Failed to connect to)\s+([^\s:]+(?::\d+)?)/gi
|
|
160
|
+
const hosts: string[] = []
|
|
161
|
+
let match: RegExpExecArray | null
|
|
162
|
+
while ((match = hostPattern.exec(stderr)) !== null) {
|
|
163
|
+
hosts.push(match[1]!)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (hosts.length > 0) {
|
|
167
|
+
violations.push(` Network access denied: ${hosts.join(', ')}`)
|
|
168
|
+
} else {
|
|
169
|
+
violations.push(` Network access denied (${netMatches.length} occurrence(s))`)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return violations
|
|
174
|
+
}
|
|
175
|
+
|
|
125
176
|
export const bashTool: ToolDefinition = {
|
|
126
177
|
name: 'Bash',
|
|
127
178
|
description:
|
|
@@ -173,6 +224,9 @@ export const bashTool: ToolDefinition = {
|
|
|
173
224
|
const exitCode = await proc.exited
|
|
174
225
|
clearTimeout(timer)
|
|
175
226
|
|
|
227
|
+
// Read stderr for violation detection and error reporting
|
|
228
|
+
const rawStderr = await new Response(proc.stderr).text()
|
|
229
|
+
|
|
176
230
|
// ── Credential masking: scrub output ──
|
|
177
231
|
let output = rawOutput
|
|
178
232
|
if (credentialConfig?.enabled && credentialConfig.output_scrubbing.enabled) {
|
|
@@ -180,20 +234,30 @@ export const bashTool: ToolDefinition = {
|
|
|
180
234
|
output = maskOutput(rawOutput, credentialConfig)
|
|
181
235
|
}
|
|
182
236
|
|
|
237
|
+
// ── Sandbox violation detection ──
|
|
238
|
+
const violations = detectViolations(rawStderr)
|
|
239
|
+
|
|
183
240
|
if (exitCode !== 0) {
|
|
184
|
-
const rawStderr = await new Response(proc.stderr).text()
|
|
185
241
|
const stderr =
|
|
186
242
|
credentialConfig?.enabled && credentialConfig.output_scrubbing.enabled
|
|
187
243
|
? (await import('../../core/credential-masker')).maskOutput(rawStderr, credentialConfig)
|
|
188
244
|
: rawStderr
|
|
245
|
+
let errorContent = output.slice(0, 5_000)
|
|
246
|
+
if (violations.length > 0) {
|
|
247
|
+
errorContent += '\n\n── Sandbox Violations ──\n' + violations.join('\n')
|
|
248
|
+
}
|
|
189
249
|
return {
|
|
190
250
|
success: false,
|
|
191
|
-
content:
|
|
251
|
+
content: errorContent,
|
|
192
252
|
error: `Exit code ${exitCode}: ${stderr.slice(0, 1_000)}`,
|
|
193
253
|
}
|
|
194
254
|
}
|
|
195
255
|
|
|
196
|
-
|
|
256
|
+
let successContent = output.slice(0, 100_000) || '(no output)'
|
|
257
|
+
if (violations.length > 0) {
|
|
258
|
+
successContent += '\n\n── Sandbox Violations ──\n' + violations.join('\n')
|
|
259
|
+
}
|
|
260
|
+
return { success: true, content: successContent }
|
|
197
261
|
} catch (err) {
|
|
198
262
|
return {
|
|
199
263
|
success: false,
|
package/src/tools/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { toolSearchTool } from './system/tool-search'
|
|
|
29
29
|
import { artifactTool } from './artifact/artifact'
|
|
30
30
|
import { reportFindingsTool } from './agent/report-findings'
|
|
31
31
|
import { sendMessageTool } from './agent/send-message'
|
|
32
|
+
import { listAgentsTool } from './agent/list-agents'
|
|
32
33
|
import { computerUseTool } from './computer/computer-use'
|
|
33
34
|
import { scheduleWakeupTool } from './scheduling/schedule-wakeup.js'
|
|
34
35
|
import { cronCreateTool, cronDeleteTool, cronListTool } from './scheduling/cron.js'
|
|
@@ -145,6 +146,7 @@ export function createToolRegistry(): Map<string, ToolDefinition> {
|
|
|
145
146
|
withValidation(workflowTool),
|
|
146
147
|
withValidation(reportFindingsTool),
|
|
147
148
|
withValidation(sendMessageTool),
|
|
149
|
+
withValidation(listAgentsTool),
|
|
148
150
|
// Network tools
|
|
149
151
|
withValidation(webFetchTool),
|
|
150
152
|
withValidation(webSearchTool),
|