@hmharness/agent 0.6.8 → 0.8.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/dist/capability.d.ts +37 -0
- package/dist/capability.js +43 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/runner.js +26 -0
- package/package.json +6 -5
|
@@ -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";
|
package/dist/runner.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* approval gate construction.
|
|
7
7
|
*/
|
|
8
8
|
import { homeDir, loadConfig, resolveProvider, mcpServerTools, Registry, runLoop, Session, } from '@hmharness/kernel';
|
|
9
|
+
import { brief, createTrajectoryRecorder } from '@hmharness/observability';
|
|
9
10
|
import { readFile } from 'node:fs/promises';
|
|
10
11
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
11
12
|
import { join, dirname } from 'node:path';
|
|
@@ -229,6 +230,11 @@ export async function runAgentTask(opts) {
|
|
|
229
230
|
const ctx = opts.ctx ?? { cwd: process.cwd(), home: homeDir() };
|
|
230
231
|
const events = opts.events ?? {};
|
|
231
232
|
const session = new Session(ctx.home, ctx.cwd, cfg.provider.model);
|
|
233
|
+
// V2 M1 flight recorder: every run leaves a typed, replayable trajectory
|
|
234
|
+
// under <home>/runs/<run-id>/. Best-effort by contract - storage failures
|
|
235
|
+
// are swallowed inside the recorder and can never fail the task itself.
|
|
236
|
+
const traj = createTrajectoryRecorder(ctx.home, { task: opts.task, model: cfg.provider.model, cwd: ctx.cwd });
|
|
237
|
+
traj.emit('run.started', 'user', { task: brief(opts.task, 400) });
|
|
232
238
|
// workspace scoping + optional embedding hybrid for memory retrieval.
|
|
233
239
|
// Embeddings only when routing.embedding is EXPLICITLY set - an inherited
|
|
234
240
|
// chat route would 404 on /embeddings once per task for nothing.
|
|
@@ -256,6 +262,14 @@ export async function runAgentTask(opts) {
|
|
|
256
262
|
// watches IT - this makes the size visible every run (review finding #5)
|
|
257
263
|
const systemTokens = Math.ceil(system.length / 4);
|
|
258
264
|
events.onLine?.(` [prompt] system: ${system.length} chars (~${systemTokens} tokens) · ${agentsMd ? 'AGENTS.md: yes' : 'no AGENTS.md'}`);
|
|
265
|
+
traj.emit('context.assembled', 'system', {
|
|
266
|
+
systemChars: system.length,
|
|
267
|
+
systemTokens,
|
|
268
|
+
agentsMd: Boolean(agentsMd),
|
|
269
|
+
memoryChars: pack.memory.length,
|
|
270
|
+
skills: pack.skills.length,
|
|
271
|
+
insights: pack.insights.length,
|
|
272
|
+
});
|
|
259
273
|
await session.user(opts.task);
|
|
260
274
|
// YOLO fix: when yes=true the caller's approvalAsk (TUI dialog, web remote
|
|
261
275
|
// gate) must NOT override the auto-approve gate - it used to take
|
|
@@ -306,6 +320,7 @@ export async function runAgentTask(opts) {
|
|
|
306
320
|
onDelta: (kind, chunk) => events.onDelta?.(kind, chunk),
|
|
307
321
|
onToolCall: (name, args) => {
|
|
308
322
|
toolsUsed.push(name);
|
|
323
|
+
traj.emit('tool.requested', 'tool', { name, args: brief(args, 120) });
|
|
309
324
|
events.onToolCall?.(name, args);
|
|
310
325
|
},
|
|
311
326
|
onToolResult: (name, output, isError) => {
|
|
@@ -313,19 +328,30 @@ export async function runAgentTask(opts) {
|
|
|
313
328
|
const list = toolErrors.get(name) ?? [];
|
|
314
329
|
list.push(output.split('\n')[0].slice(0, 120));
|
|
315
330
|
toolErrors.set(name, list);
|
|
331
|
+
traj.emit('error.observed', 'tool', { name, preview: brief(output, 160) });
|
|
316
332
|
}
|
|
317
333
|
void session.tool(name, output, isError);
|
|
334
|
+
traj.emit('tool.completed', 'tool', { name, isError, preview: brief(output, 120) });
|
|
318
335
|
events.onToolResult?.(name, output, isError);
|
|
319
336
|
},
|
|
320
337
|
onApproval: (name, args, granted) => {
|
|
321
338
|
void session.approval(name, granted);
|
|
339
|
+
traj.emit(granted ? 'tool.approved' : 'tool.denied', 'system', { name });
|
|
322
340
|
events.onApproval?.(name, args, granted);
|
|
323
341
|
},
|
|
324
342
|
onAssistant: async (m) => {
|
|
343
|
+
// stamp the event when the message lands, not after the session
|
|
344
|
+
// write drains - awaiting first made model.responded land AFTER
|
|
345
|
+
// run.completed in the timeline
|
|
346
|
+
traj.emit('model.responded', 'agent', { contentChars: m.content?.length ?? 0, toolCalls: m.tool_calls?.length ?? 0 });
|
|
325
347
|
await session.assistant(m.content ?? null, m.tool_calls);
|
|
326
348
|
},
|
|
327
349
|
},
|
|
350
|
+
}).catch((err) => {
|
|
351
|
+
traj.finish({ success: false, reason: 'error', error: brief(String(err), 200) }, { toolUses: toolsUsed.length });
|
|
352
|
+
throw err;
|
|
328
353
|
});
|
|
354
|
+
traj.finish({ success: result.reason === 'final', reason: result.reason }, { turns: result.turns, toolUses: result.toolUses, promptTokens: result.usage.promptTokens, completionTokens: result.usage.completionTokens });
|
|
329
355
|
// ---- instant feedback: learn from THIS task's mistakes, not 8 tasks later ----
|
|
330
356
|
// Tier 1 (always, zero cost): raw error pattern → memory self-note. Lowered
|
|
331
357
|
// to 1 failure for system-level patterns (shell incompat, auth, missing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hmharness/agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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,10 +15,11 @@
|
|
|
15
15
|
"build": "tsc -p tsconfig.build.json"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@hmharness/kernel": "0.
|
|
19
|
-
"@hmharness/
|
|
20
|
-
"@hmharness/
|
|
21
|
-
"@hmharness/domain-
|
|
18
|
+
"@hmharness/kernel": "0.8.0",
|
|
19
|
+
"@hmharness/observability": "0.7.0",
|
|
20
|
+
"@hmharness/evolution": "0.8.0",
|
|
21
|
+
"@hmharness/domain-harmony": "0.8.0",
|
|
22
|
+
"@hmharness/domain-ops": "0.8.0"
|
|
22
23
|
},
|
|
23
24
|
"files": [
|
|
24
25
|
"dist"
|