@hmharness/agent 0.7.0 → 0.8.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.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @hmharness/agent - capability plane (V2 blueprint M4).
3
+ *
4
+ * Projects every registered tool - native, MCP, domain - onto a
5
+ * CapabilityManifest: an auditable declaration of id, risk, permissions,
6
+ * approval requirement and side effects. The PolicyEngine then answers
7
+ * "may this capability run in this mode?" for lockdown/standard modes.
8
+ * This is the DECLARATION layer on top of the existing enforcement layer
9
+ * (kernel needsApproval + shellgate + DENY_PATTERNS stay authoritative);
10
+ * manifests make the security surface inspectable (`hmh capability list`).
11
+ */
12
+ import type { Tool } from '@hmharness/kernel';
13
+ export type CapabilityRisk = 'low' | 'medium' | 'high' | 'critical';
14
+ export interface CapabilityManifest {
15
+ id: string;
16
+ version: string;
17
+ description: string;
18
+ risk: CapabilityRisk;
19
+ /** coarse permission buckets derived from the tool's nature */
20
+ permissions: string[];
21
+ requiresApproval: boolean;
22
+ network: boolean;
23
+ sideEffects: string[];
24
+ }
25
+ export declare function manifestFor(tool: Tool): CapabilityManifest;
26
+ export declare function capabilityReport(registry: {
27
+ list(): Tool[];
28
+ }): CapabilityManifest[];
29
+ export type PolicyMode = 'standard' | 'lockdown';
30
+ export interface PolicyDecision {
31
+ allow: boolean;
32
+ reason: string;
33
+ }
34
+ /** Lockdown mode: anything with host/device/process reach is denied outright;
35
+ * approval-gated capabilities still require their gate. Standard mode:
36
+ * mirrors the existing enforcement (declaration-only view). */
37
+ export declare function authorize(manifest: CapabilityManifest, mode: PolicyMode, deniedIds?: ReadonlySet<string>): PolicyDecision;
@@ -0,0 +1,43 @@
1
+ /** Tool-name heuristics -> permission buckets and risk. Declaration only -
2
+ * enforcement stays in kernel (needsApproval/shellgate/DENY_PATTERNS). */
3
+ const RISK_RULES = [
4
+ { match: /^(run_command|ssh_run)$/, risk: 'high', permissions: ['process.spawn', 'fs.read', 'fs.write', 'net.remote'], network: true, sideEffects: ['host-process', 'remote-exec'] },
5
+ { match: /^(write_file|edit_file)$/, risk: 'medium', permissions: ['fs.write'], network: false, sideEffects: ['file-mutation'] },
6
+ { match: /^desktop_(click|type)$/, risk: 'high', permissions: ['desktop.control'], network: false, sideEffects: ['host-ui-input'] },
7
+ { match: /^desktop_screenshot$/, risk: 'medium', permissions: ['desktop.read'], network: false, sideEffects: [] },
8
+ { match: /^(browser_open|web_search|web_fetch)$/, risk: 'low', permissions: ['net.egress'], network: true, sideEffects: [] },
9
+ { match: /^(harmony_install|harmony_launch|harmony_uninstall|harmony_sign)$/, risk: 'high', permissions: ['device.write'], network: false, sideEffects: ['device-mutation'] },
10
+ { match: /^(harmony_emulator_(create|start|stop|delete))$/, risk: 'high', permissions: ['device.write', 'process.spawn'], network: true, sideEffects: ['device-mutation'] },
11
+ { match: /^(harmony_build|harmony_cjpm_build|harmony_cjpm_test|harmony_lint)$/, risk: 'medium', permissions: ['process.spawn', 'fs.write'], network: true, sideEffects: ['build-artifacts'] },
12
+ { match: /^spawn_agent$/, risk: 'high', permissions: ['agent.spawn'], network: false, sideEffects: ['subagent-run'] },
13
+ ];
14
+ export function manifestFor(tool) {
15
+ const rule = RISK_RULES.find((r) => r.match.test(tool.name));
16
+ const requiresApproval = typeof tool.needsApproval === 'function';
17
+ return {
18
+ id: tool.name,
19
+ version: '1.0.0',
20
+ description: tool.description.split('\n')[0].slice(0, 160),
21
+ risk: rule?.risk ?? (requiresApproval ? 'medium' : 'low'),
22
+ permissions: rule?.permissions ?? (requiresApproval ? ['gated.unknown'] : ['fs.read']),
23
+ requiresApproval,
24
+ network: rule?.network ?? false,
25
+ sideEffects: rule?.sideEffects ?? [],
26
+ };
27
+ }
28
+ export function capabilityReport(registry) {
29
+ return registry.list().map(manifestFor).sort((a, b) => a.id.localeCompare(b.id));
30
+ }
31
+ /** Lockdown mode: anything with host/device/process reach is denied outright;
32
+ * approval-gated capabilities still require their gate. Standard mode:
33
+ * mirrors the existing enforcement (declaration-only view). */
34
+ export function authorize(manifest, mode, deniedIds = new Set()) {
35
+ if (deniedIds.has(manifest.id))
36
+ return { allow: false, reason: `capability revoked: ${manifest.id}` };
37
+ if (mode === 'lockdown') {
38
+ const dangerous = manifest.permissions.some((p) => p.startsWith('process.') || p.startsWith('device.write') || p.startsWith('desktop.control') || p.startsWith('net.remote') || p.startsWith('agent.'));
39
+ if (dangerous)
40
+ return { allow: false, reason: `lockdown: ${manifest.id} touches ${manifest.permissions.filter((p) => !p.startsWith('fs.')).join(', ')}` };
41
+ }
42
+ return { allow: true, reason: manifest.requiresApproval ? 'allowed, approval gate applies' : 'allowed' };
43
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { baseTools, readFileTool, writeFileTool, listDirTool, runCommandTool, rememberTool, seeImageTool } from './tools.ts';
2
+ export { manifestFor, capabilityReport, authorize, type CapabilityManifest, type CapabilityRisk, type PolicyMode } from './capability.ts';
2
3
  export { buildSystemPrompt } from './prompt.ts';
3
4
  export { strings, type Locale, type Strings } from './i18n.ts';
4
5
  export { makeSpawnTool, MAX_SPAWN_DEPTH, type SpawnBase } from './spawn.ts';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { baseTools, readFileTool, writeFileTool, listDirTool, runCommandTool, rememberTool, seeImageTool } from "./tools.js";
2
+ export { manifestFor, capabilityReport, authorize } from "./capability.js";
2
3
  export { buildSystemPrompt } from "./prompt.js";
3
4
  export { strings } from "./i18n.js";
4
5
  export { makeSpawnTool, MAX_SPAWN_DEPTH } from "./spawn.js";
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @hmharness/agent - team roles (V2 blueprint M7).
3
+ *
4
+ * spawn_agent already accepts a free-text role label with a success-rate
5
+ * leaderboard; this adds the canonical engineering-team CHARTERS. When a
6
+ * spawn names a canonical role, the child's system prompt carries that
7
+ * role's discipline text (what it always does / never does), turning
8
+ * "role: a word" into "role: a contract".
9
+ */
10
+ export interface TeamRole {
11
+ name: string;
12
+ charter: string;
13
+ /** typical delegation phrasing hints, for docs only */
14
+ goodFor: string;
15
+ }
16
+ export declare const TEAM_ROLES: Record<string, TeamRole>;
17
+ /** Resolve a role label to its charter (canonical names only); '' when free-form. */
18
+ export declare function roleCharter(role: string): string;
19
+ /** The known-role list for tool descriptions. */
20
+ export declare const TEAM_ROLE_NAMES: string[];
package/dist/roles.js ADDED
@@ -0,0 +1,39 @@
1
+ export const TEAM_ROLES = {
2
+ planner: {
3
+ name: 'planner',
4
+ charter: 'PLANNER duty: decompose the goal into ordered, verifiable steps. Output a numbered plan where every step names its verification (command/check). Never execute the steps yourself - plan only. Flag risks and unknowns explicitly.',
5
+ goodFor: 'task decomposition before implementation',
6
+ },
7
+ coder: {
8
+ name: 'coder',
9
+ charter: 'CODER duty: implement exactly the agreed step. Prefer surgical edits (edit_file) inside the workspace. Keep changes minimal and consistent with surrounding code. Run the cheapest verification that proves the change (build/lint/test) before answering.',
10
+ goodFor: 'focused implementation steps',
11
+ },
12
+ tester: {
13
+ name: 'tester',
14
+ charter: 'TESTER duty: try to BREAK the thing under test. Probe edge cases, empty inputs, wrong types, boundary values. Report each probe as input -> actual vs expected. A run with zero failures must say what WAS covered, never "all good".',
15
+ goodFor: 'adversarial verification',
16
+ },
17
+ reviewer: {
18
+ name: 'reviewer',
19
+ charter: 'REVIEWER duty: findings first, ordered by severity, each with file:line evidence. Prioritize bugs, regressions, and missing tests over style. No fixes - report only. If nothing significant, say so explicitly rather than inventing nits.',
20
+ goodFor: 'code review passes',
21
+ },
22
+ judge: {
23
+ name: 'judge',
24
+ charter: 'JUDGE duty: verdict from EVIDENCE, not from the executor\'s self-report. Cite what you actually observed (command outputs, file contents). Hard evidence (exit codes, tests) outranks your opinion; label opinion as opinion. End with VERDICT: PASS or VERDICT: FAIL plus one line why.',
25
+ goodFor: 'independent evaluation of a finished step',
26
+ },
27
+ repairer: {
28
+ name: 'repairer',
29
+ charter: 'REPAIRER duty: reproduce the failure first, state the root cause in one sentence, then apply the smallest fix that removes it, then re-run the reproduction to prove it is gone. Report before/after outputs.',
30
+ goodFor: 'fixing broken builds/tests',
31
+ },
32
+ };
33
+ /** Resolve a role label to its charter (canonical names only); '' when free-form. */
34
+ export function roleCharter(role) {
35
+ const r = TEAM_ROLES[String(role ?? '').trim().toLowerCase()];
36
+ return r ? r.charter : '';
37
+ }
38
+ /** The known-role list for tool descriptions. */
39
+ export const TEAM_ROLE_NAMES = Object.keys(TEAM_ROLES);
package/dist/spawn.js CHANGED
@@ -16,6 +16,7 @@
16
16
  import { appendFile, mkdir, readFile } from 'node:fs/promises';
17
17
  import { join } from 'node:path';
18
18
  import { runLoop } from '@hmharness/kernel';
19
+ import { roleCharter } from "./roles.js";
19
20
  export const MAX_SPAWN_DEPTH = 2;
20
21
  export async function recordRole(home, role, ok) {
21
22
  try {
@@ -62,7 +63,7 @@ export function makeSpawnTool(deps) {
62
63
  type: 'object',
63
64
  properties: {
64
65
  task: { type: 'string', description: 'complete, self-contained instructions for the sub-agent' },
65
- role: { type: 'string', description: 'optional label for this delegation, e.g. "explorer", "reviewer", "build-fixer" - roles accumulate success rates you will see next time' },
66
+ role: { type: 'string', description: 'optional role label; canonical roles carry a duty charter: planner / coder / tester / reviewer / judge / repairer (plus any custom label - roles accumulate success rates you will see next time)' },
66
67
  max_turns: { type: 'number', description: 'turn budget for the sub-agent (default 8, max 12)' },
67
68
  },
68
69
  required: ['task'],
@@ -92,7 +93,7 @@ export function makeSpawnTool(deps) {
92
93
  }
93
94
  const system = [
94
95
  `You are a hmh sub-agent (depth ${childDepth}${role ? `, role: ${role}` : ''}). You have no conversation history beyond this task.`,
95
- role ? `Perform the ${role} duty with that specialty's discipline.` : '',
96
+ role ? roleCharter(role) || `Perform the ${role} duty with that specialty's discipline.` : '',
96
97
  'Do exactly what the task asks, use tools as needed, verify before answering, and reply with a concise result (the caller only sees your final answer).',
97
98
  ].filter(Boolean).join('\n');
98
99
  base.onLine?.(`[${tag}] start: ${task.slice(0, 80)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmharness/agent",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "hmharness agent execution layer: base tools, system prompt, sub-agent spawn, and the shared task runner that frontends (cli, web) drive.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -15,11 +15,11 @@
15
15
  "build": "tsc -p tsconfig.build.json"
16
16
  },
17
17
  "dependencies": {
18
- "@hmharness/kernel": "0.7.0",
18
+ "@hmharness/kernel": "0.8.0",
19
19
  "@hmharness/observability": "0.7.0",
20
- "@hmharness/evolution": "0.7.0",
21
- "@hmharness/domain-harmony": "0.7.0",
22
- "@hmharness/domain-ops": "0.7.0"
20
+ "@hmharness/evolution": "0.8.1",
21
+ "@hmharness/domain-harmony": "0.8.0",
22
+ "@hmharness/domain-ops": "0.8.0"
23
23
  },
24
24
  "files": [
25
25
  "dist"