Skip to content

feat(agent): consume MCP tool registry (Operations profile), RBAC-filtered (#626) - #1545

Merged
kriszyp merged 3 commits into
kris/agent-runnablefrom
kris/agent-registry-tools
Jul 4, 2026
Merged

feat(agent): consume MCP tool registry (Operations profile), RBAC-filtered (#626)#1545
kriszyp merged 3 commits into
kris/agent-runnablefrom
kris/agent-registry-tools

Conversation

@kriszyp

@kriszyp kriszyp commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

Wires the unified MCP tool registry's Operations profile (#617) into the built-in Harper agent (#626), fulfilling the tool-composition design: the agent consumes the same registry the MCP server exposes, RBAC-filtered for its configured user. This re-does the intent of the earlier #893 against current main (that branch was ~465 commits stale and used the pre-merge registry API).

Scope is deliberately Operations-profile only — the built-in agent runs on the main thread adjacent to the operations API server. The Application profile (#618, per-Resource tools, worker-thread-shaped) is out of scope by design.

What it does

  • agent/registryTools.ts (new)
    • ensureOperationsToolsRegistered() — populates the Operations profile on the main thread. Idempotent (addTool is Map.set-backed), so the agent gets these tools whether or not the operator enabled the mcp: HTTP surface.
    • composeRegistryTools(listingUser, resolveIdentity) — snapshots the Operations profile, filters by visibleTo(listingUser), and adapts each ToolDef → AgentTool: inputSchema → parameters, annotations.destructiveHint → the loop's approval gate, and a handler wrapping the registry handler (ToolResult.structuredContent/text as the observation; isError → throw, so the loop records a recoverable failure rather than aborting the run).
  • agent/agent.ts — resolves the agent identity and threads registry tools into composeToolset.
  • agent/toolset.ts — merges operator-only + registry tools; operator-only tools win on a name collision (a private tool is never shadowed).
  • unitTests/agent/registryTools.test.js — 12 tests: RBAC filtering, shape adaptation, destructive gating, result unwrap/throw, per-call re-resolution, fail-closed propagation, and the collision/allowDestructive merge rules. No server boot, no LLM credits.

The six agent operations (agent_prompt, get_agent_session, list_agent_sessions, cancel_agent_run, approve_agent_action, set_agent_config) already landed with the scaffold (#839); this PR adds the tool surface they drive.

Security model

visibleTo controls listing only (which tools the LLM is shown) — it is not a boundary. Real enforcement runs per-call in the operation handler via hdb_user set to the agent's identity, gated by Harper's existing verifyPerms in the operations runtime.

Cross-model review (Codex + Gemini + Harper-domain)

Two authorization defects were caught and fixed at root in the second commit:

  • Blocker — fail-open super_user. The identity fallback previously fabricated { super_user: true } whenever agent.user couldn't be resolved, silently escalating a misconfigured/transient restricted service account to admin. Now the super_user fallback is bounded to the default hdb_agent bootstrap user (whose provisioning Built-in Harper Agent Component #626 defers); an explicitly-configured user that won't resolve fails closed (agent runs with only operator-only tools).
  • Significant — stale cached identity. The agent user was resolved once at startup and closed over in every handler, so a role revocation wasn't honored until restart. Identity is now resolved per call (resolveIdentity thunk), mirroring how the MCP HTTP path re-auths per request. The startup snapshot is used only for visibleTo listing.
  • Adjudicated out: Gemini's "setConfig ignores agent.user changes" — set_agent_config doesn't accept user, so it can't change at runtime.

Known follow-ups (not in this PR)

Verification

  • npm run build clean.
  • Full agent unit suite (unitTests/agent/**): 51 passing.

🤖 Generated with Claude Code

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request integrates the unified MCP tool registry's Operations profile into the built-in agent, allowing RBAC-filtered tools to be composed and executed based on the agent's configured user identity. Feedback on these changes highlights a bug where updating the agent's user configuration does not re-compose the tool listing snapshot, potentially leaving the LLM with stale tools. Additionally, suggestions were made to add defensive checks when handling tool execution results and parsing content to prevent potential runtime errors.

Comment thread agent/agent.ts
composed = composeToolset({
allowDestructive: liveConfig.allowDestructive,
onFollowup: handleFollowup,
registryTools,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When liveConfig.user is updated via setConfig, the registryTools listing snapshot is not re-composed. This results in a stale tool listing for the LLM (which remains filtered by visibleTo for the old user), even though the enforcement identity is correctly resolved fresh per call.

Since setConfig is synchronous and composeRegistryToolsForListing is asynchronous, you can trigger the async re-composition and update the toolset once it resolves:

	function setConfig(patch: Partial<AgentConfig>): AgentConfig {
		const previousAllowDestructive = liveConfig.allowDestructive;
		const previousUser = liveConfig.user;
		liveConfig = { ...liveConfig, ...patch };

		const userChanged = liveConfig.user !== previousUser;
		const destructiveChanged = liveConfig.allowDestructive !== previousAllowDestructive;

		if (userChanged) {
			composeRegistryToolsForListing(agentIdentity)
				.then((tools) => {
					registryTools = tools;
					composed = composeToolset({
						allowDestructive: liveConfig.allowDestructive,
						onFollowup: handleFollowup,
						registryTools,
					});
				})
				.catch((err) => {
					log.error?.(`Failed to update registry tools for new user: ${err instanceof Error ? err.message : String(err)}`);
				});
		} else if (destructiveChanged) {
			composed = composeToolset({
				allowDestructive: liveConfig.allowDestructive,
				onFollowup: handleFollowup,
				registryTools,
			});
		}
		return liveConfig;
	}

Comment thread agent/registryTools.ts
Comment on lines +91 to +97
const result = await def.handler(args, context);
// The loop records a tool observation from the return value and turns a thrown error
// into a structured `{ ok: false, error }` note for the next model turn. Mapping
// `isError` → throw keeps a Harper operation failure a recoverable observation rather
// than aborting the whole run.
if (result.isError) throw new Error(errorText(result));
return result.structuredContent ?? textOf(result);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent potential TypeErrors if a tool handler returns an empty or invalid result, add a defensive check to ensure result is defined before accessing its properties.

			const result = await def.handler(args, context);
			if (!result) {
				throw new Error('Operation returned an empty or invalid result');
			}
			// The loop records a tool observation from the return value and turns a thrown error
			// into a structured '{ ok: false, error }' note for the next model turn. Mapping
			// 'isError' -> throw keeps a Harper operation failure a recoverable observation rather
			// than aborting the whole run.
			if (result.isError) throw new Error(errorText(result));
			return result.structuredContent ?? textOf(result);
References
  1. When accessing properties of a result from a method that might return nullish, guard against nullish values to prevent runtime errors.

Comment thread agent/registryTools.ts
Comment on lines +102 to +107
function textOf(result: ToolResult): string {
return (result.content ?? [])
.filter((c) => c.type === 'text' && typeof c.text === 'string')
.map((c) => c.text)
.join('\n');
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If result.content contains any null or undefined elements, accessing c.type will throw a TypeError. Add a defensive check to ensure c is defined before accessing its properties.

Suggested change
function textOf(result: ToolResult): string {
return (result.content ?? [])
.filter((c) => c.type === 'text' && typeof c.text === 'string')
.map((c) => c.text)
.join('\n');
}
function textOf(result: ToolResult): string {
return (result.content ?? [])
.filter((c) => c && c.type === 'text' && typeof c.text === 'string')
.map((c) => c.text)
.join('\n');
}

@claude

claude Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

Comment thread agent/agent.ts
server: {
registerOperation: (def: { name: string; execute: (op: any) => any | Promise<any> }) => void;
/** Resolve a Harper user (with role/permissions) by username. Password/request unused here. */
getUser?: (username: string, password: string | null, request: unknown) => Promise<AuthedUser> | AuthedUser;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be token aware too? And if so, having a third parameter with an unknown type seems rather dangerous.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the other PR #1546

Kris Zyp and others added 2 commits July 1, 2026 18:02
…tered (#626)

Fold the unified MCP tool registry's Operations profile (#617) into the
built-in agent's toolset, fulfilling #626's tool-composition design: the agent
consumes the same registry the MCP server exposes, RBAC-filtered for its
configured user. Re-does the intent of the abandoned #893 against current main
(that branch was ~465 commits stale and used the pre-merge registry API).

- agent/registryTools.ts (new): ensureOperationsToolsRegistered() populates the
  Operations profile on the main thread (idempotent); composeRegistryTools()
  snapshots the profile, filters by visibleTo(agentUser), and adapts each
  ToolDef -> AgentTool (inputSchema->parameters, destructiveHint->approval gate,
  ToolResult->return value with isError->throw so the loop records a recoverable
  failure rather than aborting).
- agent/agent.ts: resolve the configured agent user via server.getUser (falls
  back to a super_user identity with a warning when unresolved) and thread the
  registry tools through both composeToolset calls.
- agent/toolset.ts: merge operator-only + registry tools; operator-only tools
  win on a name collision.
- unitTests/agent/registryTools.test.js: 10 tests over RBAC filtering, shape
  adaptation, destructive gating, result unwrapping, and the collision rule.
  No server boot / no LLM credits.

Enforcement note: visibleTo controls listing only; real RBAC runs per-call in
the operation handler via hdb_user set to the agent's identity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cross-model review (Codex + Gemini) flagged two authorization defects in the
registry integration; both fixed at root:

- Fail-open super_user: resolveAgentUser previously fabricated a
  { super_user: true } identity whenever agent.user couldn't be resolved,
  silently escalating a misconfigured or transient restricted service account to
  admin. Now resolveAgentIdentity only falls back to super_user for the *default*
  hdb_agent bootstrap user (whose provisioning #626 defers); an explicitly
  configured user that won't resolve throws (fail closed), and the agent runs with
  only its operator-only tools until the operator fixes agent.user.

- Stale cached identity: the agent user was resolved once at startup and closed
  over in every tool handler, so a role revocation/change wasn't honored until
  restart. The enforcement identity is now resolved *per call* (composeRegistryTools
  takes a resolveIdentity thunk), mirroring how the MCP HTTP path re-auths per
  request. The startup snapshot is used only for visibleTo listing (not a boundary).

Adjudicated out: Gemini's "setConfig ignores agent.user changes" — set_agent_config
does not accept `user` (not in its patch keys), so it can't change at runtime.

Adds 2 unit tests: per-call re-resolution honors a live role change; a fail-closed
rejection propagates and the operation never dispatches. 51 agent unit tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kriszyp
kriszyp force-pushed the kris/agent-registry-tools branch from 525cdd3 to 84cf845 Compare July 2, 2026 00:04
kriszyp pushed a commit that referenced this pull request Jul 2, 2026
…#626)

Adds the operator-only inspector tool surface from the #626 design — the last
of the operator tools not yet built. The agent (main thread) attaches over the
Chrome DevTools Protocol to *worker* thread inspector ports to evaluate, set
breakpoints/logpoints, and CPU-profile a worker without stalling the thread it
runs on.

- agent/tools/inspectorTool.ts (new): buildInspectorTools(deps) → five tools:
  inspector_attach, inspector_evaluate, inspector_set_breakpoint,
  inspector_set_logpoint, inspector_profile_cpu. A minimal CDP client over `ws`
  (id-correlated request/response + event listeners), one reused connection per
  worker debug port. Deps (debug config + live worker count) are injected so the
  tool is unit-testable without a server boot.
- Safety envelope (resolvePort): requires threads_debug + threads_debug_startingPort,
  range-checks workerIndex against the live worker pool, and REJECTS workerIndex < 0
  (the main thread — a self-attach/breakpoint would deadlock the agent).
- Worker debug port = threads_debug_startingPort + workerIndex (mirrors
  server/threads/threadServer.js). Logpoints are non-pausing (breakpoint whose
  condition console.logs and returns false). CPU profiles are summarized to the
  hottest functions by self time so the observation stays small.
- evaluate + set_breakpoint are marked destructive (arbitrary in-worker code /
  can pause a live worker) → gated by the loop's approval flow.
- agent/toolset.ts + agent/agent.ts: compose inspector tools into the operator-only
  set; deps read from env (threads_debug*) and the live `workers` pool.
- unitTests/agent/inspectorTool.test.js: safety-envelope guards, summarizeProfile,
  and a LIVE CDP round-trip (opens a real inspector on an ephemeral port; drives
  attach + evaluate + profile). 60 agent unit tests green.

Stacked on #1545 (registry tools).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kriszyp
kriszyp changed the base branch from main to kris/agent-runnable July 2, 2026 00:04
* with stubs, so this stays a fast, credit-free unit test.
*/

const assert = require('node:assert/strict');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion (non-blocking): swap to plain assert — AGENTS.md explicitly says not to use node:assert/strict ("strict mode's deep-equality and coercion rules cause more friction and surprising failures than they prevent; plain assert is the house style").

Suggested change
const assert = require('node:assert/strict');
const assert = require('node:assert');

…#626) (#1547)

* feat(agent): V8 inspector (CDP) tools for debugging/profiling workers (#626)

Adds the operator-only inspector tool surface from the #626 design — the last
of the operator tools not yet built. The agent (main thread) attaches over the
Chrome DevTools Protocol to *worker* thread inspector ports to evaluate, set
breakpoints/logpoints, and CPU-profile a worker without stalling the thread it
runs on.

- agent/tools/inspectorTool.ts (new): buildInspectorTools(deps) → five tools:
  inspector_attach, inspector_evaluate, inspector_set_breakpoint,
  inspector_set_logpoint, inspector_profile_cpu. A minimal CDP client over `ws`
  (id-correlated request/response + event listeners), one reused connection per
  worker debug port. Deps (debug config + live worker count) are injected so the
  tool is unit-testable without a server boot.
- Safety envelope (resolvePort): requires threads_debug + threads_debug_startingPort,
  range-checks workerIndex against the live worker pool, and REJECTS workerIndex < 0
  (the main thread — a self-attach/breakpoint would deadlock the agent).
- Worker debug port = threads_debug_startingPort + workerIndex (mirrors
  server/threads/threadServer.js). Logpoints are non-pausing (breakpoint whose
  condition console.logs and returns false). CPU profiles are summarized to the
  hottest functions by self time so the observation stays small.
- evaluate + set_breakpoint are marked destructive (arbitrary in-worker code /
  can pause a live worker) → gated by the loop's approval flow.
- agent/toolset.ts + agent/agent.ts: compose inspector tools into the operator-only
  set; deps read from env (threads_debug*) and the live `workers` pool.
- unitTests/agent/inspectorTool.test.js: safety-envelope guards, summarizeProfile,
  and a LIVE CDP round-trip (opens a real inspector on an ephemeral port; drives
  attach + evaluate + profile). 60 agent unit tests green.

Stacked on #1545 (registry tools).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(agent): harden inspector CDP client after cross-model review (#626)

Cross-model review (Gemini) flagged real lifecycle/concurrency hazards in the
CDP tooling; all addressed:

- openCdp never settled if the socket closed/aborted before 'open' → sessionFor
  hung the agent. Now a pre-open close/error/abort rejects the connect promise.
- Session-cache races: cache the connection *promise* (dedupes concurrent opens);
  evict by identity so a stale close from a replaced connection can't drop a live
  session; drop a failed connect so the next call retries.
- Breakpoints could wedge a worker (paused, no resume path). Every connection now
  registers a Debugger.paused handler that logs a stack snapshot and auto-resumes,
  so a hit is observable via the Harper log but never leaves the worker paused.
- Logpoint injection: logExpression is now JSON-encoded and eval'd (not spliced as
  raw code), so it can't break out of the wrapper to force a pause. set_logpoint
  is also marked destructive (it runs an expression in the worker on every hit),
  alongside evaluate and set_breakpoint.
- CDP calls now carry a per-call abort signal + 30s timeout, so an unresponsive
  worker can't hang the agent loop. The connect signal guards only the handshake
  (aborting one caller no longer tears down the shared connection).
- Strict workerIndex parsing: reject ""/null/false/[] instead of Number()-coercing
  them to worker 0.
- Profiler.disable on cleanup; resolvePort rejects a port past 65535.
- _closeInspectorSessions handles in-flight opens.

Also fixes a test-teardown deadlock: inspector.close() blocks until CDP clients
drop, so the live-round-trip after() hook only closes our client and lets --exit
tear down the inspector. Full agent unit suite: 61 passing, build clean.

Codex leg returned no structured findings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(agent): tolerate an already-active inspector in CDP round-trip test (#626)

The dev-mode install CI uses for unit tests sets threads.debug: true, so
threadServer opens the V8 inspector on the main process during the suite.
The live-CDP test's before hook then called inspector.open() unconditionally
and threw ERR_INSPECTOR_ALREADY_ACTIVATED. Reuse the live inspector when one
is already open; only open our own ephemeral port otherwise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Kris Zyp <kris@harperdb.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* the test — exactly the reason main-thread attach is banned).
*/

const assert = require('node:assert/strict');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion (non-blocking): AGENTS.md prescribes plain assert over node:assert/strict — strict mode's deep-equality coercion is called out as causing friction, and all new test files in this repo use require('node:assert').

Suggested change
const assert = require('node:assert/strict');
const assert = require('node:assert');

@kriszyp
kriszyp merged commit 37a0438 into kris/agent-runnable Jul 4, 2026
48 of 51 checks passed
@kriszyp
kriszyp deleted the kris/agent-registry-tools branch July 4, 2026 11:57
kriszyp added a commit that referenced this pull request Jul 11, 2026
…nd (#626) (#1560)

* feat(agent): consume MCP tool registry (Operations profile), RBAC-filtered (#626)

Fold the unified MCP tool registry's Operations profile (#617) into the
built-in agent's toolset, fulfilling #626's tool-composition design: the agent
consumes the same registry the MCP server exposes, RBAC-filtered for its
configured user. Re-does the intent of the abandoned #893 against current main
(that branch was ~465 commits stale and used the pre-merge registry API).

- agent/registryTools.ts (new): ensureOperationsToolsRegistered() populates the
  Operations profile on the main thread (idempotent); composeRegistryTools()
  snapshots the profile, filters by visibleTo(agentUser), and adapts each
  ToolDef -> AgentTool (inputSchema->parameters, destructiveHint->approval gate,
  ToolResult->return value with isError->throw so the loop records a recoverable
  failure rather than aborting).
- agent/agent.ts: resolve the configured agent user via server.getUser (falls
  back to a super_user identity with a warning when unresolved) and thread the
  registry tools through both composeToolset calls.
- agent/toolset.ts: merge operator-only + registry tools; operator-only tools
  win on a name collision.
- unitTests/agent/registryTools.test.js: 10 tests over RBAC filtering, shape
  adaptation, destructive gating, result unwrapping, and the collision rule.
  No server boot / no LLM credits.

Enforcement note: visibleTo controls listing only; real RBAC runs per-call in
the operation handler via hdb_user set to the agent's identity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(agent): fail closed + per-call identity for registry tools (#626)

Cross-model review (Codex + Gemini) flagged two authorization defects in the
registry integration; both fixed at root:

- Fail-open super_user: resolveAgentUser previously fabricated a
  { super_user: true } identity whenever agent.user couldn't be resolved,
  silently escalating a misconfigured or transient restricted service account to
  admin. Now resolveAgentIdentity only falls back to super_user for the *default*
  hdb_agent bootstrap user (whose provisioning #626 defers); an explicitly
  configured user that won't resolve throws (fail closed), and the agent runs with
  only its operator-only tools until the operator fixes agent.user.

- Stale cached identity: the agent user was resolved once at startup and closed
  over in every tool handler, so a role revocation/change wasn't honored until
  restart. The enforcement identity is now resolved *per call* (composeRegistryTools
  takes a resolveIdentity thunk), mirroring how the MCP HTTP path re-auths per
  request. The startup snapshot is used only for visibleTo listing (not a boundary).

Adjudicated out: Gemini's "setConfig ignores agent.user changes" — set_agent_config
does not accept `user` (not in its patch keys), so it can't change at runtime.

Adds 2 unit tests: per-call re-resolution honors a live role change; a fail-closed
rejection propagates and the operation never dispatches. 51 agent unit tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(agent): V8 inspector (CDP) tools for debugging/profiling workers (#626)

Adds the operator-only inspector tool surface from the #626 design — the last
of the operator tools not yet built. The agent (main thread) attaches over the
Chrome DevTools Protocol to *worker* thread inspector ports to evaluate, set
breakpoints/logpoints, and CPU-profile a worker without stalling the thread it
runs on.

- agent/tools/inspectorTool.ts (new): buildInspectorTools(deps) → five tools:
  inspector_attach, inspector_evaluate, inspector_set_breakpoint,
  inspector_set_logpoint, inspector_profile_cpu. A minimal CDP client over `ws`
  (id-correlated request/response + event listeners), one reused connection per
  worker debug port. Deps (debug config + live worker count) are injected so the
  tool is unit-testable without a server boot.
- Safety envelope (resolvePort): requires threads_debug + threads_debug_startingPort,
  range-checks workerIndex against the live worker pool, and REJECTS workerIndex < 0
  (the main thread — a self-attach/breakpoint would deadlock the agent).
- Worker debug port = threads_debug_startingPort + workerIndex (mirrors
  server/threads/threadServer.js). Logpoints are non-pausing (breakpoint whose
  condition console.logs and returns false). CPU profiles are summarized to the
  hottest functions by self time so the observation stays small.
- evaluate + set_breakpoint are marked destructive (arbitrary in-worker code /
  can pause a live worker) → gated by the loop's approval flow.
- agent/toolset.ts + agent/agent.ts: compose inspector tools into the operator-only
  set; deps read from env (threads_debug*) and the live `workers` pool.
- unitTests/agent/inspectorTool.test.js: safety-envelope guards, summarizeProfile,
  and a LIVE CDP round-trip (opens a real inspector on an ephemeral port; drives
  attach + evaluate + profile). 60 agent unit tests green.

Stacked on #1545 (registry tools).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(agent): harden inspector CDP client after cross-model review (#626)

Cross-model review (Gemini) flagged real lifecycle/concurrency hazards in the
CDP tooling; all addressed:

- openCdp never settled if the socket closed/aborted before 'open' → sessionFor
  hung the agent. Now a pre-open close/error/abort rejects the connect promise.
- Session-cache races: cache the connection *promise* (dedupes concurrent opens);
  evict by identity so a stale close from a replaced connection can't drop a live
  session; drop a failed connect so the next call retries.
- Breakpoints could wedge a worker (paused, no resume path). Every connection now
  registers a Debugger.paused handler that logs a stack snapshot and auto-resumes,
  so a hit is observable via the Harper log but never leaves the worker paused.
- Logpoint injection: logExpression is now JSON-encoded and eval'd (not spliced as
  raw code), so it can't break out of the wrapper to force a pause. set_logpoint
  is also marked destructive (it runs an expression in the worker on every hit),
  alongside evaluate and set_breakpoint.
- CDP calls now carry a per-call abort signal + 30s timeout, so an unresponsive
  worker can't hang the agent loop. The connect signal guards only the handshake
  (aborting one caller no longer tears down the shared connection).
- Strict workerIndex parsing: reject ""/null/false/[] instead of Number()-coercing
  them to worker 0.
- Profiler.disable on cleanup; resolvePort rejects a port past 65535.
- _closeInspectorSessions handles in-flight opens.

Also fixes a test-teardown deadlock: inspector.close() blocks until CDP clients
drop, so the live-round-trip after() hook only closes our client and lets --exit
tear down the inspector. Full agent unit suite: 61 passing, build clean.

Codex leg returned no structured findings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(agent): Harper best-practices grounding + agent.systemPromptAppend (#626)

Gives the built-in agent Harper conventions so it builds idiomatic apps, and
lets operators tune its persona/policy without a rebuild.

- Depends on @harperfast/skills and sources the `harper-best-practices` skill
  from it (versioned, no drift). Progressive disclosure, mirroring the skill's
  own design:
    * agent/bestPractices.ts (new): loadBestPracticesOverview() injects the
      SKILL.md overview (rule index + when-to-use, ~1.2k tokens) into the system
      prompt; buildBestPracticeTool() exposes a `harper_best_practice` tool that
      lists rules (no arg) or returns rules/<name>.md on demand — so the agent
      only spends context on the guidance relevant to the task. Both degrade to
      nothing if the package isn't resolvable (agent still runs). Rule arg is
      guarded against path traversal.
- agent.systemPromptAppend: operator text appended after the built-in grounding
  and the best-practices overview. Read from liveConfig each run, and accepted by
  set_agent_config, so it can be tuned on a running instance. Added to the config
  schema and AgentConfig.
- System prompt is now assembled: built-in grounding → best-practices overview →
  operator append.

Stacked on the inspector PR (kris/agent-inspector). Agent unit suite green
(incl. new bestPractices tests exercising the real skill package).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(agent): tolerate a pre-existing inspector session in the live CDP test (#626)

node:inspector is a single process-wide agent. In CI, threads_debug defaults
to true (installer.ts DEV_MODE_CONFIG), and server/threads/threadServer.js
opens the main-thread inspector as a top-level side effect of merely being
imported (e.g. via DurableSubscriptionsSession.ts pulling in
whenComponentsLoaded) whenever that config is on. That leaves this process's
one inspector slot already occupied by the time the "live CDP round-trip"
suite's before() hook runs, so inspector.open() throws
ERR_INSPECTOR_ALREADY_ACTIVATED.

Check inspector.url() first and reuse whatever is already listening instead
of assuming the suite is the sole owner of the process's debug port.

Verified by reproducing the exact CI error locally (mocha --require a script
that opens the inspector before test files load), confirming the fix resolves
it, and running the suite in isolation, within unitTests/agent/**, and across
3 full unitTests/**/*test.*js runs (2985 passing, 0 failing each time).

* docs(#626): document @harperfast/skills dependency; drop node:assert/strict

* feat(agent): expose the built-in agent over MCP (curated tools) (#626)

Lets any MCP client drive the built-in agent (agent_prompt, get_agent_session,
list_agent_sessions, approve_agent_action, cancel_agent_run).

Why not just `mcp.operations.allow`: the generic MCP operations profile walks
OPERATION_FUNCTION_MAP once, BEFORE this component registers its operations, so
the agent ops are added to the map too late for the walk — allow-listing them has
no effect (confirmed live: profile builds at 14 tools, then the 6 agent ops
register; tools/list never shows them). So we register a curated agent tool set
directly into the registry AFTER the ops exist.

- agent/mcpTools.ts (new): registerAgentMcpTools(operations) adds curated tools
  (proper schemas/descriptions, destructive/read-only annotations) on the
  operations profile. visibleTo is super_user-only for listing; each handler
  dispatches to the operation's execute with the MCP caller as hdb_user, so the
  op's own super_user check + downstream RBAC still apply. set_agent_config is
  intentionally NOT exposed (operator/config action).
- agent/agent.ts: call it after registering the ops. Tools sit inertly in the
  registry unless the MCP HTTP surface is enabled.

Verified live over the MCP Streamable HTTP transport: an MCP client completed
initialize → tools/list (agent tools present) → tools/call agent_prompt →
get_agent_session, and the agent ran a tool and answered.

Stacked on the best-practices PR (kris/agent-bestpractices).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(agent): preserve non-standard error details in MCP tool error responses

Fall back to String(err) before the generic message so string/plain-object
thrown values aren't reduced to a useless generic message (#1561).

* fix(agent): read best-practices from @harperfast/skills exports, not the filesystem (#626)

Per review, @harperfast/skills exposes its skill content directly as module
exports — skillSummary (SKILL.md), ruleNames, and a rules name→markdown map —
so there's no need to resolve the package on disk and read files by hand.

- bestPractices.ts: import { ruleNames, rules, skillSummary } and serve from
  them. This removes the sync readdirSync/readFileSync in the tool handler (no
  event-loop blocking on the main thread), the require.resolve path walk (ESM
  require-undefined hazard), and the path-traversal regex — rule lookup is now a
  plain map access, so malformed/traversal names simply miss.
- agent.ts: tighten the systemPromptAppend guard to an explicit string check
  before trim().
- dependencies.md: rewrite the @harperfast/skills entry to reflect module-export
  consumption (no fs access, rule bodies resident in heap, hard runtime dep).
- test: traversal/malformed names now surface as "No such best-practice rule".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Kris Zyp <kris@harperdb.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants