mu-harness 0.16.11 → 0.16.12

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.
@@ -1,4 +1,4 @@
1
- export type { Agent, ToolDecision, ToolGrants } from './types.js';
2
- export { type AgentRegistry, createAgentRegistry, toolDecision, toolNames } from './registry.js';
1
+ export type { Agent, GrantValue, ToolDecision, ToolGrants } from './types.js';
2
+ export { type AgentRegistry, createAgentRegistry, grantArg, toolDecision, toolNames } from './registry.js';
3
3
  export { parseAgent } from './parser.js';
4
4
  export { loadAgents } from './loader.js';
@@ -1,3 +1,3 @@
1
- export { createAgentRegistry, toolDecision, toolNames } from './registry.js';
1
+ export { createAgentRegistry, grantArg, toolDecision, toolNames } from './registry.js';
2
2
  export { parseAgent } from './parser.js';
3
3
  export { loadAgents } from './loader.js';
@@ -1,9 +1,17 @@
1
1
  import { parseFrontmatter, str } from '../common/index.js';
2
2
  const DECISIONS = new Set(['allow', 'ask', 'deny']);
3
+ const isDecision = (value) => typeof value === 'string' && DECISIONS.has(value);
3
4
  const parseStringList = (raw) => (Array.isArray(raw) ? raw : raw.split(','))
4
5
  .filter((entry) => typeof entry === 'string')
5
6
  .map((entry) => entry.trim())
6
7
  .filter(Boolean);
8
+ const parseDecisionMap = (raw) => {
9
+ const out = {};
10
+ for (const [key, value] of Object.entries(raw))
11
+ if (isDecision(value))
12
+ out[key] = value;
13
+ return Object.keys(out).length > 0 ? out : undefined;
14
+ };
7
15
  const parseTools = (raw) => {
8
16
  if (Array.isArray(raw) || typeof raw === 'string') {
9
17
  const list = parseStringList(raw);
@@ -11,9 +19,14 @@ const parseTools = (raw) => {
11
19
  }
12
20
  if (raw && typeof raw === 'object') {
13
21
  const out = {};
14
- for (const [tool, decision] of Object.entries(raw)) {
15
- if (typeof decision === 'string' && DECISIONS.has(decision))
16
- out[tool] = decision;
22
+ for (const [tool, value] of Object.entries(raw)) {
23
+ if (isDecision(value))
24
+ out[tool] = value;
25
+ else if (value && typeof value === 'object' && !Array.isArray(value)) {
26
+ const nested = parseDecisionMap(value);
27
+ if (nested)
28
+ out[tool] = nested;
29
+ }
17
30
  }
18
31
  return Object.keys(out).length > 0 ? out : undefined;
19
32
  }
@@ -3,6 +3,7 @@ export interface AgentRegistry {
3
3
  list(): Agent[];
4
4
  get(name: string): Agent | undefined;
5
5
  }
6
- export declare const toolDecision: (agent: Agent, tool: string) => ToolDecision;
6
+ export declare const grantArg: (tool: string, input: unknown) => string | undefined;
7
+ export declare const toolDecision: (agent: Agent, tool: string, arg?: string) => ToolDecision;
7
8
  export declare const toolNames: (agent: Agent) => string[] | undefined;
8
9
  export declare const createAgentRegistry: (agents?: Agent[]) => AgentRegistry;
@@ -1,24 +1,59 @@
1
- const asMap = (tools) => {
2
- if (!tools)
1
+ import { matchesGlob } from 'node:path';
2
+ const asMap = (grants) => {
3
+ if (!grants)
3
4
  return undefined;
4
- if (Array.isArray(tools))
5
- return Object.fromEntries(tools.map((tool) => [tool, 'allow']));
6
- return tools;
5
+ if (Array.isArray(grants))
6
+ return Object.fromEntries(grants.map((name) => [name, 'allow']));
7
+ return grants;
7
8
  };
8
- export const toolDecision = (agent, tool) => {
9
- const map = asMap(agent.tools);
9
+ const matchKey = (keys, name) => {
10
+ if (keys.includes(name))
11
+ return name;
12
+ let glob;
13
+ for (const key of keys) {
14
+ if (key === '*' || key === name)
15
+ continue;
16
+ if (matchesGlob(name, key) && (glob === undefined || key.length > glob.length))
17
+ glob = key;
18
+ }
19
+ if (glob !== undefined)
20
+ return glob;
21
+ return keys.includes('*') ? '*' : undefined;
22
+ };
23
+ const resolveGrant = (grants, tool, arg) => {
24
+ const map = asMap(grants);
10
25
  if (!map)
11
26
  return 'allow';
12
- return map[tool] ?? map['*'] ?? 'deny';
27
+ const key = matchKey(Object.keys(map), tool);
28
+ if (key === undefined)
29
+ return 'deny';
30
+ const value = map[key];
31
+ if (typeof value === 'string')
32
+ return value;
33
+ if (arg === undefined)
34
+ return 'allow';
35
+ const inner = matchKey(Object.keys(value), arg);
36
+ return inner === undefined ? 'deny' : value[inner];
13
37
  };
14
- export const toolNames = (agent) => {
15
- const map = asMap(agent.tools);
38
+ const grantNames = (grants) => {
39
+ const map = asMap(grants);
16
40
  if (!map)
17
41
  return undefined;
18
- if (map['*'] && map['*'] !== 'deny')
42
+ const wildcard = map['*'];
43
+ if (wildcard !== undefined && wildcard !== 'deny')
19
44
  return ['*'];
20
- return Object.entries(map).filter(([, decision]) => decision !== 'deny').map(([tool]) => tool);
45
+ return Object.entries(map).filter(([, value]) => value !== 'deny').map(([name]) => name);
46
+ };
47
+ const GRANT_ARG = { skill: 'name', bash: 'command' };
48
+ export const grantArg = (tool, input) => {
49
+ const field = GRANT_ARG[tool];
50
+ if (!field || typeof input !== 'object' || input === null)
51
+ return undefined;
52
+ const value = input[field];
53
+ return typeof value === 'string' ? value : undefined;
21
54
  };
55
+ export const toolDecision = (agent, tool, arg) => resolveGrant(agent.tools, tool, arg);
56
+ export const toolNames = (agent) => grantNames(agent.tools);
22
57
  const merge = (base, child) => ({
23
58
  name: child.name,
24
59
  description: child.description || base.description,
@@ -1,5 +1,6 @@
1
1
  export type ToolDecision = 'allow' | 'ask' | 'deny';
2
- export type ToolGrants = string[] | Record<string, ToolDecision>;
2
+ export type GrantValue = ToolDecision | Record<string, ToolDecision>;
3
+ export type ToolGrants = string[] | Record<string, GrantValue>;
3
4
  export interface Agent {
4
5
  name: string;
5
6
  description: string;
@@ -1,6 +1,6 @@
1
1
  import { join } from 'node:path';
2
2
  import process from 'node:process';
3
- import { createAgentRegistry, loadAgents, toolDecision, toolNames } from '../agents/index.js';
3
+ import { createAgentRegistry, grantArg, loadAgents, toolDecision, toolNames } from '../agents/index.js';
4
4
  import { createAgentsCommand, createCommandRegistry, createHelpCommand, createSessionsCommand, createSkillsCommand, } from '../commands/index.js';
5
5
  import { createHarnessConfig } from '../config/index.js';
6
6
  import { mergeHooks } from '../hooks/index.js';
@@ -33,22 +33,35 @@ export const createHarness = async (options) => {
33
33
  const cwdSkills = await loadSkills(cwdSkillsDir);
34
34
  const diskSkills = await loadSkills(skillsDir);
35
35
  const skills = createSkillRegistry([...hostSkills, ...pluginSkills, ...cwdSkills, ...diskSkills]);
36
- const skillTools = [
37
- createSkillTool(skills),
38
- createSkillWriterTool({ dirs: { local: cwdSkillsDir, config: skillsDir }, registry: skills }),
39
- ];
36
+ const skillWriterTool = createSkillWriterTool({
37
+ dirs: { local: cwdSkillsDir, config: skillsDir },
38
+ registry: skills,
39
+ });
40
+ const scopeSkills = (agent) => {
41
+ if (!agent)
42
+ return skills;
43
+ return skills.select(skills.list().map((s) => s.name).filter((name) => toolDecision(agent, 'skill', name) !== 'deny'));
44
+ };
40
45
  const tasks = enableScheduler ? createTaskStore({ dir: join(config.configDir, 'tasks') }) : undefined;
41
46
  let schedulerTools = [];
42
47
  const runs = createSubAgentRegistry();
43
48
  const store = createSessionStore({ dir: join(config.dataDir, 'sessions') });
44
- const sessionTools = (extra = []) => [...(sessionDefaults.tools ?? []), ...extra, ...skillTools, ...schedulerTools];
49
+ const sessionTools = (agent, extra = []) => [
50
+ ...(sessionDefaults.tools ?? []),
51
+ ...extra,
52
+ createSkillTool(scopeSkills(agent)),
53
+ skillWriterTool,
54
+ ...schedulerTools,
55
+ ];
45
56
  const approvalHook = (getAgent) => approvals
46
57
  ? approvals.manager.hooksFor({
47
58
  decide: (call) => {
48
59
  const agent = getAgent();
49
60
  if (!agent)
50
61
  return 'allow';
51
- return approvals.decide ? approvals.decide(agent, call) : toolDecision(agent, call.name);
62
+ if (approvals.decide)
63
+ return approvals.decide(agent, call);
64
+ return toolDecision(agent, call.name, grantArg(call.name, call.input));
52
65
  },
53
66
  agent: () => getAgent()?.name,
54
67
  })
@@ -62,7 +75,7 @@ export const createHarness = async (options) => {
62
75
  id: newId(),
63
76
  });
64
77
  const spawn = (agent) => persistTo(store, persona(agent, {
65
- tools: sessionTools(),
78
+ tools: sessionTools(agent),
66
79
  hooks: mergeHooks([sessionDefaults.hooks, allowList(toolNames(agent)), approvalHook(() => agent)]),
67
80
  }));
68
81
  const scheduler = enableScheduler && tasks
@@ -112,7 +125,7 @@ export const createHarness = async (options) => {
112
125
  revive: ({ id, model: ref, messages }) => createAgentSession({
113
126
  ...sessionDefaults,
114
127
  hooks: mergeHooks([sessionDefaults.hooks, approvalHook(() => approvals?.activeAgent())]),
115
- tools: sessionTools([createSubAgentTool({ registry: agents, spawn, runs, parentId: id })]),
128
+ tools: sessionTools(undefined, [createSubAgentTool({ registry: agents, spawn, runs, parentId: id })]),
116
129
  ...models.resolve(ref),
117
130
  id,
118
131
  messages,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-harness",
3
- "version": "0.16.11",
3
+ "version": "0.16.12",
4
4
  "description": "Agent harness: createHarness wires mu-core into a host — XDG paths, model registry, plugins, disk-loaded agents & skills, sub-agents, sessions (JSONL + SQLite catalog), slash commands, permission/approval hooks, an optional scheduler, and a composable TUI chat app",
5
5
  "license": "MIT",
6
6
  "main": "./script/index.js",
@@ -16,8 +16,8 @@
16
16
  "dependencies": {
17
17
  "@swc/wasm-typescript": "^1.15.0",
18
18
  "croner": "^9.0.0",
19
- "mu-core": "^0.16.11",
20
- "mu-tui": "^0.16.11"
19
+ "mu-core": "^0.16.12",
20
+ "mu-tui": "^0.16.12"
21
21
  },
22
22
  "_generatedBy": "dnt@dev"
23
23
  }
@@ -1,4 +1,4 @@
1
- export type { Agent, ToolDecision, ToolGrants } from './types.js';
2
- export { type AgentRegistry, createAgentRegistry, toolDecision, toolNames } from './registry.js';
1
+ export type { Agent, GrantValue, ToolDecision, ToolGrants } from './types.js';
2
+ export { type AgentRegistry, createAgentRegistry, grantArg, toolDecision, toolNames } from './registry.js';
3
3
  export { parseAgent } from './parser.js';
4
4
  export { loadAgents } from './loader.js';
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.loadAgents = exports.parseAgent = exports.toolNames = exports.toolDecision = exports.createAgentRegistry = void 0;
3
+ exports.loadAgents = exports.parseAgent = exports.toolNames = exports.toolDecision = exports.grantArg = exports.createAgentRegistry = void 0;
4
4
  var registry_js_1 = require("./registry.js");
5
5
  Object.defineProperty(exports, "createAgentRegistry", { enumerable: true, get: function () { return registry_js_1.createAgentRegistry; } });
6
+ Object.defineProperty(exports, "grantArg", { enumerable: true, get: function () { return registry_js_1.grantArg; } });
6
7
  Object.defineProperty(exports, "toolDecision", { enumerable: true, get: function () { return registry_js_1.toolDecision; } });
7
8
  Object.defineProperty(exports, "toolNames", { enumerable: true, get: function () { return registry_js_1.toolNames; } });
8
9
  var parser_js_1 = require("./parser.js");
@@ -3,10 +3,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseAgent = void 0;
4
4
  const index_js_1 = require("../common/index.js");
5
5
  const DECISIONS = new Set(['allow', 'ask', 'deny']);
6
+ const isDecision = (value) => typeof value === 'string' && DECISIONS.has(value);
6
7
  const parseStringList = (raw) => (Array.isArray(raw) ? raw : raw.split(','))
7
8
  .filter((entry) => typeof entry === 'string')
8
9
  .map((entry) => entry.trim())
9
10
  .filter(Boolean);
11
+ const parseDecisionMap = (raw) => {
12
+ const out = {};
13
+ for (const [key, value] of Object.entries(raw))
14
+ if (isDecision(value))
15
+ out[key] = value;
16
+ return Object.keys(out).length > 0 ? out : undefined;
17
+ };
10
18
  const parseTools = (raw) => {
11
19
  if (Array.isArray(raw) || typeof raw === 'string') {
12
20
  const list = parseStringList(raw);
@@ -14,9 +22,14 @@ const parseTools = (raw) => {
14
22
  }
15
23
  if (raw && typeof raw === 'object') {
16
24
  const out = {};
17
- for (const [tool, decision] of Object.entries(raw)) {
18
- if (typeof decision === 'string' && DECISIONS.has(decision))
19
- out[tool] = decision;
25
+ for (const [tool, value] of Object.entries(raw)) {
26
+ if (isDecision(value))
27
+ out[tool] = value;
28
+ else if (value && typeof value === 'object' && !Array.isArray(value)) {
29
+ const nested = parseDecisionMap(value);
30
+ if (nested)
31
+ out[tool] = nested;
32
+ }
20
33
  }
21
34
  return Object.keys(out).length > 0 ? out : undefined;
22
35
  }
@@ -3,6 +3,7 @@ export interface AgentRegistry {
3
3
  list(): Agent[];
4
4
  get(name: string): Agent | undefined;
5
5
  }
6
- export declare const toolDecision: (agent: Agent, tool: string) => ToolDecision;
6
+ export declare const grantArg: (tool: string, input: unknown) => string | undefined;
7
+ export declare const toolDecision: (agent: Agent, tool: string, arg?: string) => ToolDecision;
7
8
  export declare const toolNames: (agent: Agent) => string[] | undefined;
8
9
  export declare const createAgentRegistry: (agents?: Agent[]) => AgentRegistry;
@@ -1,28 +1,64 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createAgentRegistry = exports.toolNames = exports.toolDecision = void 0;
4
- const asMap = (tools) => {
5
- if (!tools)
3
+ exports.createAgentRegistry = exports.toolNames = exports.toolDecision = exports.grantArg = void 0;
4
+ const node_path_1 = require("node:path");
5
+ const asMap = (grants) => {
6
+ if (!grants)
6
7
  return undefined;
7
- if (Array.isArray(tools))
8
- return Object.fromEntries(tools.map((tool) => [tool, 'allow']));
9
- return tools;
8
+ if (Array.isArray(grants))
9
+ return Object.fromEntries(grants.map((name) => [name, 'allow']));
10
+ return grants;
10
11
  };
11
- const toolDecision = (agent, tool) => {
12
- const map = asMap(agent.tools);
12
+ const matchKey = (keys, name) => {
13
+ if (keys.includes(name))
14
+ return name;
15
+ let glob;
16
+ for (const key of keys) {
17
+ if (key === '*' || key === name)
18
+ continue;
19
+ if ((0, node_path_1.matchesGlob)(name, key) && (glob === undefined || key.length > glob.length))
20
+ glob = key;
21
+ }
22
+ if (glob !== undefined)
23
+ return glob;
24
+ return keys.includes('*') ? '*' : undefined;
25
+ };
26
+ const resolveGrant = (grants, tool, arg) => {
27
+ const map = asMap(grants);
13
28
  if (!map)
14
29
  return 'allow';
15
- return map[tool] ?? map['*'] ?? 'deny';
30
+ const key = matchKey(Object.keys(map), tool);
31
+ if (key === undefined)
32
+ return 'deny';
33
+ const value = map[key];
34
+ if (typeof value === 'string')
35
+ return value;
36
+ if (arg === undefined)
37
+ return 'allow';
38
+ const inner = matchKey(Object.keys(value), arg);
39
+ return inner === undefined ? 'deny' : value[inner];
16
40
  };
17
- exports.toolDecision = toolDecision;
18
- const toolNames = (agent) => {
19
- const map = asMap(agent.tools);
41
+ const grantNames = (grants) => {
42
+ const map = asMap(grants);
20
43
  if (!map)
21
44
  return undefined;
22
- if (map['*'] && map['*'] !== 'deny')
45
+ const wildcard = map['*'];
46
+ if (wildcard !== undefined && wildcard !== 'deny')
23
47
  return ['*'];
24
- return Object.entries(map).filter(([, decision]) => decision !== 'deny').map(([tool]) => tool);
48
+ return Object.entries(map).filter(([, value]) => value !== 'deny').map(([name]) => name);
25
49
  };
50
+ const GRANT_ARG = { skill: 'name', bash: 'command' };
51
+ const grantArg = (tool, input) => {
52
+ const field = GRANT_ARG[tool];
53
+ if (!field || typeof input !== 'object' || input === null)
54
+ return undefined;
55
+ const value = input[field];
56
+ return typeof value === 'string' ? value : undefined;
57
+ };
58
+ exports.grantArg = grantArg;
59
+ const toolDecision = (agent, tool, arg) => resolveGrant(agent.tools, tool, arg);
60
+ exports.toolDecision = toolDecision;
61
+ const toolNames = (agent) => grantNames(agent.tools);
26
62
  exports.toolNames = toolNames;
27
63
  const merge = (base, child) => ({
28
64
  name: child.name,
@@ -1,5 +1,6 @@
1
1
  export type ToolDecision = 'allow' | 'ask' | 'deny';
2
- export type ToolGrants = string[] | Record<string, ToolDecision>;
2
+ export type GrantValue = ToolDecision | Record<string, ToolDecision>;
3
+ export type ToolGrants = string[] | Record<string, GrantValue>;
3
4
  export interface Agent {
4
5
  name: string;
5
6
  description: string;
@@ -39,22 +39,35 @@ const createHarness = async (options) => {
39
39
  const cwdSkills = await (0, index_js_9.loadSkills)(cwdSkillsDir);
40
40
  const diskSkills = await (0, index_js_9.loadSkills)(skillsDir);
41
41
  const skills = (0, index_js_9.createSkillRegistry)([...hostSkills, ...pluginSkills, ...cwdSkills, ...diskSkills]);
42
- const skillTools = [
43
- (0, index_js_9.createSkillTool)(skills),
44
- (0, index_js_9.createSkillWriterTool)({ dirs: { local: cwdSkillsDir, config: skillsDir }, registry: skills }),
45
- ];
42
+ const skillWriterTool = (0, index_js_9.createSkillWriterTool)({
43
+ dirs: { local: cwdSkillsDir, config: skillsDir },
44
+ registry: skills,
45
+ });
46
+ const scopeSkills = (agent) => {
47
+ if (!agent)
48
+ return skills;
49
+ return skills.select(skills.list().map((s) => s.name).filter((name) => (0, index_js_1.toolDecision)(agent, 'skill', name) !== 'deny'));
50
+ };
46
51
  const tasks = enableScheduler ? (0, index_js_7.createTaskStore)({ dir: (0, node_path_1.join)(config.configDir, 'tasks') }) : undefined;
47
52
  let schedulerTools = [];
48
53
  const runs = (0, index_js_10.createSubAgentRegistry)();
49
54
  const store = (0, index_js_8.createSessionStore)({ dir: (0, node_path_1.join)(config.dataDir, 'sessions') });
50
- const sessionTools = (extra = []) => [...(sessionDefaults.tools ?? []), ...extra, ...skillTools, ...schedulerTools];
55
+ const sessionTools = (agent, extra = []) => [
56
+ ...(sessionDefaults.tools ?? []),
57
+ ...extra,
58
+ (0, index_js_9.createSkillTool)(scopeSkills(agent)),
59
+ skillWriterTool,
60
+ ...schedulerTools,
61
+ ];
51
62
  const approvalHook = (getAgent) => approvals
52
63
  ? approvals.manager.hooksFor({
53
64
  decide: (call) => {
54
65
  const agent = getAgent();
55
66
  if (!agent)
56
67
  return 'allow';
57
- return approvals.decide ? approvals.decide(agent, call) : (0, index_js_1.toolDecision)(agent, call.name);
68
+ if (approvals.decide)
69
+ return approvals.decide(agent, call);
70
+ return (0, index_js_1.toolDecision)(agent, call.name, (0, index_js_1.grantArg)(call.name, call.input));
58
71
  },
59
72
  agent: () => getAgent()?.name,
60
73
  })
@@ -68,7 +81,7 @@ const createHarness = async (options) => {
68
81
  id: newId(),
69
82
  });
70
83
  const spawn = (agent) => (0, index_js_8.persistTo)(store, persona(agent, {
71
- tools: sessionTools(),
84
+ tools: sessionTools(agent),
72
85
  hooks: (0, index_js_4.mergeHooks)([sessionDefaults.hooks, (0, index_js_5.allowList)((0, index_js_1.toolNames)(agent)), approvalHook(() => agent)]),
73
86
  }));
74
87
  const scheduler = enableScheduler && tasks
@@ -118,7 +131,7 @@ const createHarness = async (options) => {
118
131
  revive: ({ id, model: ref, messages }) => (0, index_js_8.createAgentSession)({
119
132
  ...sessionDefaults,
120
133
  hooks: (0, index_js_4.mergeHooks)([sessionDefaults.hooks, approvalHook(() => approvals?.activeAgent())]),
121
- tools: sessionTools([(0, index_js_10.createSubAgentTool)({ registry: agents, spawn, runs, parentId: id })]),
134
+ tools: sessionTools(undefined, [(0, index_js_10.createSubAgentTool)({ registry: agents, spawn, runs, parentId: id })]),
122
135
  ...models.resolve(ref),
123
136
  id,
124
137
  messages,