@harperfast/harper 5.2.0-alpha.4 → 5.2.0-alpha.6
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/agent/agent.ts +152 -10
- package/agent/bestPractices.ts +58 -0
- package/agent/mcpTools.ts +122 -0
- package/agent/operations.ts +10 -1
- package/agent/registryTools.ts +117 -0
- package/agent/session.ts +16 -7
- package/agent/tools/fsTools.ts +74 -27
- package/agent/tools/inspectorTool.ts +459 -0
- package/agent/toolset.ts +27 -7
- package/agent/types.ts +2 -0
- package/components/Application.ts +566 -85
- package/components/OptionsWatcher.ts +62 -2
- package/components/Scope.ts +31 -8
- package/components/componentLoader.ts +5 -1
- package/components/componentSecrets.ts +514 -55
- package/components/deploymentRecorder.ts +7 -6
- package/components/gitCredentialHelper.js +115 -0
- package/components/gitCredentialServer.ts +251 -0
- package/components/mcp/tools/application.ts +197 -23
- package/components/mcp/tools/operations.ts +1 -1
- package/components/mcp/tools/schemas/derive.ts +6 -2
- package/components/operations.js +21 -19
- package/components/operationsValidation.js +86 -21
- package/components/secretOperations.ts +110 -30
- package/config/harperConfigEnvVars.ts +90 -5
- package/config-root.schema.json +4 -0
- package/dist/agent/agent.d.ts +24 -3
- package/dist/agent/agent.js +172 -10
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/bestPractices.d.ts +24 -0
- package/dist/agent/bestPractices.js +60 -0
- package/dist/agent/bestPractices.js.map +1 -0
- package/dist/agent/mcpTools.d.ts +24 -0
- package/dist/agent/mcpTools.js +115 -0
- package/dist/agent/mcpTools.js.map +1 -0
- package/dist/agent/operations.js +10 -1
- package/dist/agent/operations.js.map +1 -1
- package/dist/agent/registryTools.d.ts +45 -0
- package/dist/agent/registryTools.js +113 -0
- package/dist/agent/registryTools.js.map +1 -0
- package/dist/agent/session.js +16 -7
- package/dist/agent/session.js.map +1 -1
- package/dist/agent/tools/fsTools.js +70 -28
- package/dist/agent/tools/fsTools.js.map +1 -1
- package/dist/agent/tools/inspectorTool.d.ts +40 -0
- package/dist/agent/tools/inspectorTool.js +428 -0
- package/dist/agent/tools/inspectorTool.js.map +1 -0
- package/dist/agent/toolset.d.ts +16 -6
- package/dist/agent/toolset.js +17 -7
- package/dist/agent/toolset.js.map +1 -1
- package/dist/agent/types.d.ts +2 -0
- package/dist/components/Application.d.ts +73 -18
- package/dist/components/Application.js +475 -64
- package/dist/components/Application.js.map +1 -1
- package/dist/components/OptionsWatcher.d.ts +1 -1
- package/dist/components/OptionsWatcher.js +65 -2
- package/dist/components/OptionsWatcher.js.map +1 -1
- package/dist/components/Scope.d.ts +8 -6
- package/dist/components/Scope.js +22 -6
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.js +5 -1
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/componentSecrets.d.ts +33 -7
- package/dist/components/componentSecrets.js +473 -59
- package/dist/components/componentSecrets.js.map +1 -1
- package/dist/components/deploymentRecorder.d.ts +2 -2
- package/dist/components/deploymentRecorder.js +1 -1
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/gitCredentialHelper.d.ts +1 -0
- package/dist/components/gitCredentialHelper.js +113 -0
- package/dist/components/gitCredentialHelper.js.map +1 -0
- package/dist/components/gitCredentialServer.d.ts +33 -0
- package/dist/components/gitCredentialServer.js +236 -0
- package/dist/components/gitCredentialServer.js.map +1 -0
- package/dist/components/mcp/tools/application.d.ts +19 -0
- package/dist/components/mcp/tools/application.js +180 -22
- package/dist/components/mcp/tools/application.js.map +1 -1
- package/dist/components/mcp/tools/operations.d.ts +13 -0
- package/dist/components/mcp/tools/operations.js +1 -0
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/mcp/tools/schemas/derive.js +6 -2
- package/dist/components/mcp/tools/schemas/derive.js.map +1 -1
- package/dist/components/operations.js +23 -21
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +84 -21
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/secretOperations.d.ts +33 -11
- package/dist/components/secretOperations.js +90 -26
- package/dist/components/secretOperations.js.map +1 -1
- package/dist/config/harperConfigEnvVars.d.ts +21 -0
- package/dist/config/harperConfigEnvVars.js +95 -5
- package/dist/config/harperConfigEnvVars.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/resources/Resource.d.ts +19 -0
- package/dist/resources/Resource.js +93 -2
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/Table.d.ts +9 -0
- package/dist/resources/Table.js +256 -50
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/read.js +28 -25
- package/dist/resources/analytics/read.js.map +1 -1
- package/dist/resources/defineResource.d.ts +180 -0
- package/dist/resources/defineResource.js +505 -0
- package/dist/resources/defineResource.js.map +1 -0
- package/dist/resources/defineTable.d.ts +221 -0
- package/dist/resources/defineTable.js +227 -0
- package/dist/resources/defineTable.js.map +1 -0
- package/dist/resources/jsonSchemaTypes.d.ts +2 -0
- package/dist/resources/jsonSchemaTypes.js +12 -4
- package/dist/resources/jsonSchemaTypes.js.map +1 -1
- package/dist/resources/openApi.js +69 -13
- package/dist/resources/openApi.js.map +1 -1
- package/dist/resources/search.js +6 -8
- package/dist/resources/search.js.map +1 -1
- package/dist/server/graphqlQuerying.js +4 -2
- package/dist/server/graphqlQuerying.js.map +1 -1
- package/dist/server/http.d.ts +12 -0
- package/dist/server/http.js +35 -15
- package/dist/server/http.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.js +8 -5
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/serverHelpers/uwsServer.js +23 -0
- package/dist/server/serverHelpers/uwsServer.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +21 -0
- package/dist/utility/errors/hdbError.js +23 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/validation/configValidator.js +22 -6
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +33 -0
- package/npm-shrinkwrap.json +9197 -15236
- package/package.json +5 -3
- package/resources/DESIGN.md +42 -15
- package/resources/Resource.ts +99 -2
- package/resources/Table.ts +275 -71
- package/resources/analytics/read.ts +30 -25
- package/resources/defineResource.ts +651 -0
- package/resources/defineTable.ts +407 -0
- package/resources/jsonSchemaTypes.ts +12 -4
- package/resources/openApi.ts +68 -16
- package/resources/search.ts +5 -8
- package/server/graphqlQuerying.ts +4 -2
- package/server/http.ts +40 -16
- package/server/serverHelpers/serverUtilities.ts +19 -6
- package/server/serverHelpers/uwsServer.ts +24 -0
- package/studio/web/assets/{Chat-D4FIaBu7.js → Chat-CTjtL8Z4.js} +2 -2
- package/studio/web/assets/{Chat-D4FIaBu7.js.map → Chat-CTjtL8Z4.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-DGUC3L4r.js → FloatingChat-CafHR4Ur.js} +4 -4
- package/studio/web/assets/{FloatingChat-DGUC3L4r.js.map → FloatingChat-CafHR4Ur.js.map} +1 -1
- package/studio/web/assets/{applications-D8Am9ikM.js → applications-Buh_q0Vj.js} +2 -2
- package/studio/web/assets/{applications-D8Am9ikM.js.map → applications-Buh_q0Vj.js.map} +1 -1
- package/studio/web/assets/{index-HGL4WHdb.js → index-0hXeECkS.js} +6 -6
- package/studio/web/assets/{index-HGL4WHdb.js.map → index-0hXeECkS.js.map} +1 -1
- package/studio/web/assets/{index.lazy-Dk5j1AoQ.js → index.lazy-B00B7VBT.js} +4 -4
- package/studio/web/assets/{index.lazy-Dk5j1AoQ.js.map → index.lazy-B00B7VBT.js.map} +1 -1
- package/studio/web/assets/{profile-7bu_CF1f.js → profile-Cg2wwYPn.js} +2 -2
- package/studio/web/assets/{profile-7bu_CF1f.js.map → profile-Cg2wwYPn.js.map} +1 -1
- package/studio/web/assets/{setComponentFile-MfYm9F94.js → setComponentFile-DCaDIvyB.js} +2 -2
- package/studio/web/assets/{setComponentFile-MfYm9F94.js.map → setComponentFile-DCaDIvyB.js.map} +1 -1
- package/studio/web/assets/{setup-SKA4UhdE.js → setup-CAVcAQjK.js} +2 -2
- package/studio/web/assets/{setup-SKA4UhdE.js.map → setup-CAVcAQjK.js.map} +1 -1
- package/studio/web/assets/{status-DetVLUxR.js → status-BRXorNdD.js} +2 -2
- package/studio/web/assets/{status-DetVLUxR.js.map → status-BRXorNdD.js.map} +1 -1
- package/studio/web/assets/{swagger-ui-react-DLeYARY2.js → swagger-ui-react-Dy1D62vO.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-DLeYARY2.js.map → swagger-ui-react-Dy1D62vO.js.map} +1 -1
- package/studio/web/assets/{tsMode-DGO-jIF7.js → tsMode-A8gbL74v.js} +2 -2
- package/studio/web/assets/{tsMode-DGO-jIF7.js.map → tsMode-A8gbL74v.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-4M2i6bo4.js → useEntityRestURL-cDodrVcQ.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-4M2i6bo4.js.map → useEntityRestURL-cDodrVcQ.js.map} +1 -1
- package/studio/web/index.html +1 -1
- package/utility/errors/hdbError.ts +32 -0
- package/validation/configValidator.ts +23 -6
package/agent/agent.ts
CHANGED
|
@@ -8,17 +8,25 @@
|
|
|
8
8
|
* is realized lazily on first use, and the loop runs in-process.
|
|
9
9
|
*
|
|
10
10
|
* The component intentionally avoids `handleApplication`: it has nothing
|
|
11
|
-
* worker-thread-shaped to do.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* worker-thread-shaped to do. Two tool sources compose: operator-only tools
|
|
12
|
+
* (FS, schedule, fetch, and the V8 inspector) that are inline, and RBAC-filtered
|
|
13
|
+
* registry tools (#615/#617) drained for the agent's configured user via
|
|
14
|
+
* `registryTools.ts`.
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
import { dirname, isAbsolute, resolve as resolvePath } from 'node:path';
|
|
17
18
|
import { CONFIG_PARAMS } from '../utility/hdbTerms.ts';
|
|
19
|
+
import * as env from '../utility/environment/environmentManager.ts';
|
|
18
20
|
import harperLogger from '../utility/logging/harper_logger.ts';
|
|
19
21
|
import { Models } from '../resources/models/Models.ts';
|
|
22
|
+
import type { AuthedUser } from '../components/mcp/toolRegistry.ts';
|
|
23
|
+
import { workers } from '../server/threads/manageThreads.js';
|
|
20
24
|
import { composeToolset } from './toolset.ts';
|
|
25
|
+
import { buildInspectorTools } from './tools/inspectorTool.ts';
|
|
26
|
+
import { buildBestPracticeTool, loadBestPracticesOverview } from './bestPractices.ts';
|
|
27
|
+
import { composeRegistryTools, ensureOperationsToolsRegistered } from './registryTools.ts';
|
|
21
28
|
import { buildOperations } from './operations.ts';
|
|
29
|
+
import { registerAgentMcpTools } from './mcpTools.ts';
|
|
22
30
|
import { runAgent, _resetInFlightForTests } from './loop.ts';
|
|
23
31
|
import { appendMessage, getSession } from './session.ts';
|
|
24
32
|
import type { AgentConfig, AgentScopes, AgentTool } from './types.ts';
|
|
@@ -37,6 +45,8 @@ const DEFAULT_CONFIG: AgentConfig = {
|
|
|
37
45
|
interface StartOpts {
|
|
38
46
|
server: {
|
|
39
47
|
registerOperation: (def: { name: string; execute: (op: any) => any | Promise<any> }) => void;
|
|
48
|
+
/** Resolve a Harper user (with role/permissions) by username. Password/request unused here. */
|
|
49
|
+
getUser?: (username: string, password: string | null, request: unknown) => Promise<AuthedUser> | AuthedUser;
|
|
40
50
|
};
|
|
41
51
|
// Component-level config plumbed by componentLoader (`...componentConfig`).
|
|
42
52
|
enabled?: boolean;
|
|
@@ -48,6 +58,7 @@ interface StartOpts {
|
|
|
48
58
|
allowDestructive?: boolean;
|
|
49
59
|
user?: string;
|
|
50
60
|
componentsScope?: string;
|
|
61
|
+
systemPromptAppend?: string;
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
export async function startOnMainThread(opts: StartOpts): Promise<void> {
|
|
@@ -64,11 +75,63 @@ export async function startOnMainThread(opts: StartOpts): Promise<void> {
|
|
|
64
75
|
const models = new Models();
|
|
65
76
|
const abortControllers = new Map<string, AbortController>();
|
|
66
77
|
let liveConfig: AgentConfig = config;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
|
|
79
|
+
// Populate the Operations MCP profile on the main thread. `agentIdentity` resolves the enforcement
|
|
80
|
+
// identity fresh on each call (so a live role change is honored, and an unresolvable non-default
|
|
81
|
+
// user fails closed); the startup snapshot below is only for `visibleTo` listing.
|
|
82
|
+
ensureOperationsToolsRegistered();
|
|
83
|
+
const agentIdentity = () => resolveAgentIdentity(opts.server, liveConfig.user);
|
|
84
|
+
let registryTools = await composeRegistryToolsForListing(agentIdentity);
|
|
85
|
+
|
|
86
|
+
// Operator-only V8 inspector tools. Debug config is read once here — enabling threads_debug opens the
|
|
87
|
+
// worker inspector ports at thread boot, so it can't be toggled without a restart anyway. Worker
|
|
88
|
+
// count is a live closure over the pool, so attaches range-check against the current worker set.
|
|
89
|
+
const inspectorTools = buildInspectorTools({
|
|
90
|
+
debugEnabled: env.get(CONFIG_PARAMS.THREADS_DEBUG) !== false,
|
|
91
|
+
startingPort: (env.get(CONFIG_PARAMS.THREADS_DEBUG_STARTINGPORT) as number | undefined) ?? undefined,
|
|
92
|
+
host: (env.get(CONFIG_PARAMS.THREADS_DEBUG_HOST) as string | undefined) ?? '127.0.0.1',
|
|
93
|
+
getWorkerCount: () => workers.length,
|
|
70
94
|
});
|
|
71
95
|
|
|
96
|
+
// Harper best-practices skill (@harperfast/skills): the SKILL.md overview goes in the system prompt;
|
|
97
|
+
// the `harper_best_practice` tool serves the detailed rules on demand. Both degrade to nothing if the
|
|
98
|
+
// package isn't resolvable, so the agent still runs without it.
|
|
99
|
+
const bestPracticesOverview = loadBestPracticesOverview();
|
|
100
|
+
const bestPracticeTool = buildBestPracticeTool();
|
|
101
|
+
const extraTools: AgentTool[] = bestPracticeTool ? [bestPracticeTool] : [];
|
|
102
|
+
|
|
103
|
+
// The grounding + best-practices portion of the system prompt is static for the component's
|
|
104
|
+
// lifetime (scopes don't change), so build it once here; only the operator's per-run
|
|
105
|
+
// `systemPromptAppend` is folded on at run time in `startRun`.
|
|
106
|
+
const staticSystemPrompt = buildStaticSystemPrompt(scopes, bestPracticesOverview);
|
|
107
|
+
|
|
108
|
+
function compose(): ReturnType<typeof composeToolset> {
|
|
109
|
+
return composeToolset({
|
|
110
|
+
allowDestructive: liveConfig.allowDestructive,
|
|
111
|
+
onFollowup: handleFollowup,
|
|
112
|
+
inspectorTools,
|
|
113
|
+
registryTools,
|
|
114
|
+
extraTools,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let composed = compose();
|
|
119
|
+
|
|
120
|
+
// Build the listing snapshot best-effort: if the configured user can't be resolved (and isn't the
|
|
121
|
+
// default bootstrap user), `agentIdentity` fails closed — the agent then runs with only its
|
|
122
|
+
// operator-only tools until the operator fixes `agent.user`. Never falls back to an escalated list.
|
|
123
|
+
async function composeRegistryToolsForListing(resolveIdentity: () => Promise<AuthedUser>): Promise<AgentTool[]> {
|
|
124
|
+
try {
|
|
125
|
+
const listingUser = await resolveIdentity();
|
|
126
|
+
return composeRegistryTools(listingUser, resolveIdentity);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
log.error?.(
|
|
129
|
+
`Registry tools disabled — agent.user='${liveConfig.user}' could not be resolved: ${err instanceof Error ? err.message : String(err)}`
|
|
130
|
+
);
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
72
135
|
// Only warn when the operator explicitly configured `maxCostUsd`. Logging on the default
|
|
73
136
|
// every boot would flood the log without telling anyone anything actionable.
|
|
74
137
|
if (opts.maxCostUsd !== undefined) {
|
|
@@ -109,6 +172,7 @@ export async function startOnMainThread(opts: StartOpts): Promise<void> {
|
|
|
109
172
|
autoApprove: liveConfig.autoApprove,
|
|
110
173
|
signal: controller.signal,
|
|
111
174
|
generateOpts: { model: liveConfig.model },
|
|
175
|
+
systemPrompt: composeSystemPrompt(staticSystemPrompt, liveConfig.systemPromptAppend),
|
|
112
176
|
})
|
|
113
177
|
.catch((err) => log.error?.(`Agent run failed for ${sessionId}: ${(err as Error)?.message ?? err}`))
|
|
114
178
|
.finally(() => {
|
|
@@ -137,10 +201,7 @@ export async function startOnMainThread(opts: StartOpts): Promise<void> {
|
|
|
137
201
|
const previousAllowDestructive = liveConfig.allowDestructive;
|
|
138
202
|
liveConfig = { ...liveConfig, ...patch };
|
|
139
203
|
if (liveConfig.allowDestructive !== previousAllowDestructive) {
|
|
140
|
-
composed =
|
|
141
|
-
allowDestructive: liveConfig.allowDestructive,
|
|
142
|
-
onFollowup: handleFollowup,
|
|
143
|
-
});
|
|
204
|
+
composed = compose();
|
|
144
205
|
}
|
|
145
206
|
// NOTE: an already in-flight run captured its toolset (and autoApprove) at start, so flipping
|
|
146
207
|
// allowDestructive here only affects subsequent runs — the live loop finishes on its existing
|
|
@@ -158,9 +219,89 @@ export async function startOnMainThread(opts: StartOpts): Promise<void> {
|
|
|
158
219
|
});
|
|
159
220
|
for (const op of operations) opts.server.registerOperation(op);
|
|
160
221
|
|
|
222
|
+
// Expose the agent over MCP: register curated agent tools into the registry AFTER the ops exist.
|
|
223
|
+
// (The generic operations-profile walk ran before this, so allow-listing the agent ops wouldn't
|
|
224
|
+
// surface them — see mcpTools.ts.) They only reach clients when the MCP surface is enabled.
|
|
225
|
+
registerAgentMcpTools(operations);
|
|
226
|
+
|
|
161
227
|
log.info?.(`Agent component initialized with ${composed.tools.length} tools`);
|
|
162
228
|
}
|
|
163
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Grounding prompt so the agent knows what it is, where its files live, and how a Harper app is
|
|
232
|
+
* shaped. The filesystem tools take a `root` scope enum, so we describe the scopes rather than
|
|
233
|
+
* spelling out path-prefixing rules.
|
|
234
|
+
*
|
|
235
|
+
* We deliberately do NOT enumerate individual tool names here — the model already receives the full
|
|
236
|
+
* tool schema from the SDK, and duplicating names in the prompt drifts as tools are added/removed and
|
|
237
|
+
* invites hallucinated calls. This describes the tool *surface* at a category level only.
|
|
238
|
+
*
|
|
239
|
+
* The built-in grounding and best-practices overview are static for the component's lifetime, so the
|
|
240
|
+
* caller precomputes them once (`buildStaticSystemPrompt`) and only the operator
|
|
241
|
+
* `agent.systemPromptAppend` — which can change via `set_agent_config` without a restart — is folded
|
|
242
|
+
* in per run by `composeSystemPrompt`.
|
|
243
|
+
*/
|
|
244
|
+
function buildStaticSystemPrompt(scopes: AgentScopes, bestPracticesOverview?: string): string {
|
|
245
|
+
const parts = [
|
|
246
|
+
'You are the built-in Harper agent, running on the main thread inside a live Harper server.',
|
|
247
|
+
'You operate this instance for an operator through the tools provided to you: Harper database/cluster operations, scoped filesystem tools, HTTP fetch against this server, followup scheduling, and (when available) V8 inspector tools for debugging worker threads plus a Harper best-practices lookup. Consult the provided tool schemas for the exact set and their parameters.',
|
|
248
|
+
'',
|
|
249
|
+
'Filesystem scopes (the fs tools take a `root` naming one of these; paths are relative to it):',
|
|
250
|
+
`- components — the app source directory, your only WRITE scope: ${scopes.componentsRoot}`,
|
|
251
|
+
`- logs — read-only: ${scopes.logDir}`,
|
|
252
|
+
`- config — read-only: ${scopes.configDir}`,
|
|
253
|
+
'',
|
|
254
|
+
'A Harper app is a component directory under the components dir. Define tables/resources in a schema (GraphQL `.graphql` with `@table`/`@export`, or `config.yaml` + resource files). After writing or changing component files, deploy/restart as needed for them to load, then verify by querying the REST endpoint via an HTTP fetch against this server.',
|
|
255
|
+
'Prefer the operations tools for database/cluster actions; use the filesystem tools for app source. When designing schemas or building app logic, consult the Harper best practices below and read the relevant rule via the best-practice tool. Be concise and verify your work.',
|
|
256
|
+
];
|
|
257
|
+
if (bestPracticesOverview) {
|
|
258
|
+
parts.push('', '=== Harper best practices (overview) ===', bestPracticesOverview);
|
|
259
|
+
}
|
|
260
|
+
return parts.join('\n');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Fold the operator's per-run `systemPromptAppend` onto the precomputed static base. */
|
|
264
|
+
function composeSystemPrompt(staticPrompt: string, append?: string): string {
|
|
265
|
+
if (typeof append === 'string' && append.trim()) {
|
|
266
|
+
return `${staticPrompt}\n\n=== Operator instructions ===\n${append.trim()}`;
|
|
267
|
+
}
|
|
268
|
+
return staticPrompt;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Resolve the identity the agent's tool calls run as. Registry tools are enforced
|
|
273
|
+
* (`hdb_user` at call time) against this user, so an operator who sets `agent.user`
|
|
274
|
+
* to a restricted role gets a narrowed surface — and a later role change is honored,
|
|
275
|
+
* because this runs per call rather than once at startup.
|
|
276
|
+
*
|
|
277
|
+
* Failure policy — fail closed, with one bounded exception:
|
|
278
|
+
* - If `agent.user` resolves to a permissioned user, use it.
|
|
279
|
+
* - If it can't be resolved AND it's the *default* `hdb_agent` bootstrap user,
|
|
280
|
+
* fall back to a super_user identity (the system user isn't provisioned yet —
|
|
281
|
+
* #626 defers creating it). This is the documented default-agent behavior.
|
|
282
|
+
* - If it can't be resolved and the operator configured a *non-default* user,
|
|
283
|
+
* throw. Silently escalating a misconfigured/transient restricted account to
|
|
284
|
+
* super_user is the vulnerability we refuse to introduce.
|
|
285
|
+
*/
|
|
286
|
+
export async function resolveAgentIdentity(server: StartOpts['server'], username: string): Promise<AuthedUser> {
|
|
287
|
+
let resolved: AuthedUser | undefined;
|
|
288
|
+
if (typeof server.getUser === 'function') {
|
|
289
|
+
try {
|
|
290
|
+
const user = await server.getUser(username, null, null);
|
|
291
|
+
if (user?.role?.permission) resolved = user;
|
|
292
|
+
} catch (err) {
|
|
293
|
+
log.warn?.(`Failed to resolve agent.user='${username}': ${err instanceof Error ? err.message : String(err)}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (resolved) return resolved;
|
|
297
|
+
|
|
298
|
+
if (username === DEFAULT_CONFIG.user) {
|
|
299
|
+
log.warn?.(`Default agent user '${username}' is not provisioned yet; using super_user bootstrap identity`);
|
|
300
|
+
return { username, role: { permission: { super_user: true } } };
|
|
301
|
+
}
|
|
302
|
+
throw new Error(`agent.user '${username}' could not be resolved to a permissioned user; failing closed`);
|
|
303
|
+
}
|
|
304
|
+
|
|
164
305
|
function resolveScopes(
|
|
165
306
|
config: AgentConfig,
|
|
166
307
|
getConfigPath: (param: string) => string | undefined,
|
|
@@ -194,6 +335,7 @@ function mergeConfig(opts: StartOpts): AgentConfig {
|
|
|
194
335
|
...(opts.allowDestructive !== undefined && { allowDestructive: !!opts.allowDestructive }),
|
|
195
336
|
...(opts.user !== undefined && { user: String(opts.user) }),
|
|
196
337
|
...(opts.componentsScope !== undefined && { componentsScope: String(opts.componentsScope) }),
|
|
338
|
+
...(opts.systemPromptAppend !== undefined && { systemPromptAppend: String(opts.systemPromptAppend) }),
|
|
197
339
|
};
|
|
198
340
|
}
|
|
199
341
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harper best-practices knowledge for the built-in agent (#626).
|
|
3
|
+
*
|
|
4
|
+
* Sources the `harper-best-practices` skill from the `@harperfast/skills`
|
|
5
|
+
* package (versioned with the release, no drift). The package exposes the skill
|
|
6
|
+
* content directly as module exports — `skillSummary` (the SKILL.md overview),
|
|
7
|
+
* `ruleNames` (the rule index), and `rules` (name → markdown body) — so we read
|
|
8
|
+
* from those rather than groveling around the installed package on disk.
|
|
9
|
+
*
|
|
10
|
+
* Progressive disclosure, mirroring how the skill is meant to be used:
|
|
11
|
+
* - the SKILL.md overview (rule index + when-to-use) is injected into the
|
|
12
|
+
* agent's system prompt so it always knows *which* practices exist;
|
|
13
|
+
* - the detailed rule bodies are pulled on demand via the
|
|
14
|
+
* `harper_best_practice` tool, so the agent only spends context on the
|
|
15
|
+
* guidance relevant to the task instead of carrying all ~20 rules.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { ruleNames, rules, skillSummary } from '@harperfast/skills';
|
|
19
|
+
import type { AgentTool } from './types.ts';
|
|
20
|
+
|
|
21
|
+
/** The SKILL.md overview (rule index + guidance), for injection into the system prompt. Undefined if unavailable. */
|
|
22
|
+
export function loadBestPracticesOverview(): string | undefined {
|
|
23
|
+
return skillSummary || undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A tool that lists the best-practice rules (no arg) or returns one rule's full body (`rule` arg).
|
|
28
|
+
* Returns undefined when the skill package ships no rules, so the caller simply omits it.
|
|
29
|
+
*/
|
|
30
|
+
export function buildBestPracticeTool(): AgentTool | undefined {
|
|
31
|
+
if (!ruleNames?.length) return undefined;
|
|
32
|
+
return {
|
|
33
|
+
def: {
|
|
34
|
+
name: 'harper_best_practice',
|
|
35
|
+
description:
|
|
36
|
+
'Read a Harper best-practices rule for detailed guidance and code examples — schema design, relationships, automatic/REST APIs, authentication, custom resources, caching, vector indexing, TypeScript type-stripping, deployment, logging, and more. Call with no `rule` to list the available rule names; then call again with a `rule` to read it. Consult these before designing schemas or building app logic.',
|
|
37
|
+
parameters: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
rule: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
description: 'Rule name without extension, e.g. "adding-tables-with-schemas". Omit to list all rules.',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
handler: async (args: any) => {
|
|
48
|
+
const rule = args?.rule ? String(args.rule).trim() : '';
|
|
49
|
+
if (!rule) return { rules: [...ruleNames] };
|
|
50
|
+
// A plain map lookup — an unknown or malformed name simply misses, so there's no
|
|
51
|
+
// path-traversal surface to guard against.
|
|
52
|
+
const content = (rules as Record<string, string>)[rule];
|
|
53
|
+
if (content == null)
|
|
54
|
+
throw new Error(`No such best-practice rule: '${rule}'. Call with no rule to list available rules.`);
|
|
55
|
+
return { rule, content };
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expose the built-in agent over MCP (#626).
|
|
3
|
+
*
|
|
4
|
+
* The generic MCP operations profile (#617) can surface these ops too, but only
|
|
5
|
+
* when an operator opts them into `mcp.operations.allow` and with generic,
|
|
6
|
+
* schema-inferred descriptions. We register a *curated* agent tool set directly
|
|
7
|
+
* into the registry instead: exposed by default (no allow-list entry needed),
|
|
8
|
+
* with hand-written descriptions and annotations tuned for driving the agent.
|
|
9
|
+
*
|
|
10
|
+
* Each tool dispatches through the SAME path the generic operations profile uses
|
|
11
|
+
* (`makeOperationToolHandler` → `chooseOperation` + `processLocalTransaction`), so
|
|
12
|
+
* Harper's `verifyPerms` runs at call time and enforces the `requiresSuperUser: true`
|
|
13
|
+
* declared on each agent op — plus any scoped `operations: ['agent_*']` delegation
|
|
14
|
+
* role. Listing mirrors that boundary via `canRoleInvokeOperation`. A direct
|
|
15
|
+
* `op.execute(body)` call is NOT used: it would bypass `verifyPerms` entirely and
|
|
16
|
+
* let any authenticated MCP user drive the super_user agent.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { addTool, canRoleInvokeOperation, type AuthedUser } from '../components/mcp/toolRegistry.ts';
|
|
20
|
+
import { makeOperationToolHandler } from '../components/mcp/tools/operations.ts';
|
|
21
|
+
import harperLogger from '../utility/logging/harper_logger.ts';
|
|
22
|
+
import type { OperationDefinition } from '../server/serverHelpers/serverUtilities.ts';
|
|
23
|
+
|
|
24
|
+
const log = harperLogger.loggerWithTag('agent');
|
|
25
|
+
|
|
26
|
+
interface AgentMcpToolMeta {
|
|
27
|
+
description: string;
|
|
28
|
+
inputSchema: object;
|
|
29
|
+
destructive?: boolean;
|
|
30
|
+
readOnly?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Curated client-facing surface for driving the agent. `set_agent_config` is intentionally excluded
|
|
34
|
+
// — it's an operator/config action, not something an MCP client should reach.
|
|
35
|
+
const AGENT_MCP_TOOLS: Record<string, AgentMcpToolMeta> = {
|
|
36
|
+
agent_prompt: {
|
|
37
|
+
description:
|
|
38
|
+
'Send a prompt to the built-in Harper agent. Starts a new session, or continues one when session_id is given. Returns { session_id, status }; poll get_agent_session for the transcript and result.',
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
message: { type: 'string', description: 'The instruction/prompt for the agent.' },
|
|
43
|
+
session_id: { type: 'string', description: 'Optional existing session id to continue the conversation.' },
|
|
44
|
+
},
|
|
45
|
+
required: ['message'],
|
|
46
|
+
},
|
|
47
|
+
destructive: true, // the agent may take actions in response
|
|
48
|
+
},
|
|
49
|
+
get_agent_session: {
|
|
50
|
+
description:
|
|
51
|
+
'Read a built-in-agent session: its status, full transcript (messages, tool calls and results), and any pending approvals.',
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: { session_id: { type: 'string' } },
|
|
55
|
+
required: ['session_id'],
|
|
56
|
+
},
|
|
57
|
+
readOnly: true,
|
|
58
|
+
},
|
|
59
|
+
list_agent_sessions: {
|
|
60
|
+
description: 'List built-in-agent sessions, most recent first.',
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: { limit: { type: 'integer', minimum: 1, description: 'Max sessions to return (default 100).' } },
|
|
64
|
+
},
|
|
65
|
+
readOnly: true,
|
|
66
|
+
},
|
|
67
|
+
approve_agent_action: {
|
|
68
|
+
description:
|
|
69
|
+
'Approve or deny a pending agent tool call (when autoApprove is off), then resume the run. Get the approval_id from get_agent_session.pendingApprovals.',
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
session_id: { type: 'string' },
|
|
74
|
+
approval_id: { type: 'string' },
|
|
75
|
+
approved: { type: 'boolean', description: 'true to approve (default), false to deny.' },
|
|
76
|
+
},
|
|
77
|
+
required: ['session_id', 'approval_id'],
|
|
78
|
+
},
|
|
79
|
+
destructive: true,
|
|
80
|
+
},
|
|
81
|
+
cancel_agent_run: {
|
|
82
|
+
description: 'Cancel an in-progress agent run for a session.',
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: { session_id: { type: 'string' } },
|
|
86
|
+
required: ['session_id'],
|
|
87
|
+
},
|
|
88
|
+
destructive: true,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Register the curated agent tools into the MCP registry (operations profile). Call after the agent
|
|
94
|
+
* operations are registered. Safe to call whether or not the MCP HTTP surface is enabled — the tools
|
|
95
|
+
* simply sit in the registry until an MCP client lists them.
|
|
96
|
+
*/
|
|
97
|
+
export function registerAgentMcpTools(operations: OperationDefinition[]): void {
|
|
98
|
+
const registered = new Set(operations.map((op) => op.name));
|
|
99
|
+
let count = 0;
|
|
100
|
+
for (const [name, meta] of Object.entries(AGENT_MCP_TOOLS)) {
|
|
101
|
+
if (!registered.has(name)) continue; // op not registered (shouldn't happen) — skip rather than expose a dead tool
|
|
102
|
+
const annotations = meta.destructive ? { destructiveHint: true } : meta.readOnly ? { readOnlyHint: true } : {};
|
|
103
|
+
addTool({
|
|
104
|
+
name,
|
|
105
|
+
description: meta.description,
|
|
106
|
+
inputSchema: meta.inputSchema,
|
|
107
|
+
profile: 'operations',
|
|
108
|
+
...(Object.keys(annotations).length > 0 ? { annotations } : {}),
|
|
109
|
+
// List only for users the op's role-level policy would let call it (super_user, or a
|
|
110
|
+
// scoped `operations: ['agent_*']` delegation role). verifyPerms enforces the same at
|
|
111
|
+
// call time inside makeOperationToolHandler — this predicate keeps hidden tools out of
|
|
112
|
+
// the listing, not out of reach.
|
|
113
|
+
visibleTo: (user: AuthedUser) => canRoleInvokeOperation(user, name),
|
|
114
|
+
// Route through the standard operation dispatch (chooseOperation + processLocalTransaction),
|
|
115
|
+
// so verifyPerms runs and the op's requiresSuperUser gate is actually enforced — a direct
|
|
116
|
+
// op.execute() would skip it. Same handler the generic operations profile uses.
|
|
117
|
+
handler: makeOperationToolHandler(name),
|
|
118
|
+
});
|
|
119
|
+
count++;
|
|
120
|
+
}
|
|
121
|
+
log.info?.(`Agent MCP tools registered: ${count}`);
|
|
122
|
+
}
|
package/agent/operations.ts
CHANGED
|
@@ -134,7 +134,16 @@ async function approveAgentAction(op: any, deps: OperationDeps) {
|
|
|
134
134
|
|
|
135
135
|
async function setAgentConfig(op: any, deps: OperationDeps) {
|
|
136
136
|
const patch: Partial<AgentConfig> = {};
|
|
137
|
-
for (const key of [
|
|
137
|
+
for (const key of [
|
|
138
|
+
'enabled',
|
|
139
|
+
'provider',
|
|
140
|
+
'model',
|
|
141
|
+
'maxTurns',
|
|
142
|
+
'maxCostUsd',
|
|
143
|
+
'autoApprove',
|
|
144
|
+
'allowDestructive',
|
|
145
|
+
'systemPromptAppend',
|
|
146
|
+
]) {
|
|
138
147
|
if (op?.[key] !== undefined) (patch as any)[key] = op[key];
|
|
139
148
|
}
|
|
140
149
|
return deps.setConfig(patch);
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry tool composition for the built-in agent (#626).
|
|
3
|
+
*
|
|
4
|
+
* Folds the unified MCP tool registry (#615) — specifically the Operations
|
|
5
|
+
* profile (#617) — into the agent's toolset. The agent consumes the *same*
|
|
6
|
+
* registry the MCP HTTP server exposes, RBAC-filtered for its configured user,
|
|
7
|
+
* so operators get one tool surface with one permission model.
|
|
8
|
+
*
|
|
9
|
+
* Two-step, mirroring the registry's own security model:
|
|
10
|
+
* 1. `visibleTo(agentUser)` narrows *listing* — the LLM only sees tools it is
|
|
11
|
+
* likely allowed to call. This is NOT a security boundary.
|
|
12
|
+
* 2. Real enforcement runs in the operation handler at call time: each call
|
|
13
|
+
* dispatches through Harper's operations runtime with `hdb_user` set to the
|
|
14
|
+
* agent's configured identity, so `verifyPerms` gates it regardless of what
|
|
15
|
+
* the LLM was shown.
|
|
16
|
+
*
|
|
17
|
+
* The Application profile (#618) is intentionally out of scope here; it can fold
|
|
18
|
+
* in the same way once per-Resource tool generation is wired for the agent.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { registerOperationsTools } from '../components/mcp/tools/operations.ts';
|
|
22
|
+
import {
|
|
23
|
+
snapshotProfileTools,
|
|
24
|
+
type AuthedUser,
|
|
25
|
+
type ToolCallContext,
|
|
26
|
+
type ToolDef as RegistryToolDef,
|
|
27
|
+
type ToolResult,
|
|
28
|
+
} from '../components/mcp/toolRegistry.ts';
|
|
29
|
+
import harperLogger from '../utility/logging/harper_logger.ts';
|
|
30
|
+
import type { AgentTool, AgentToolContext } from './types.ts';
|
|
31
|
+
|
|
32
|
+
const log = harperLogger.loggerWithTag('agent');
|
|
33
|
+
|
|
34
|
+
let operationsRegistered = false;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Populate the Operations profile on the main thread. Idempotent: guarded so a
|
|
38
|
+
* second call (e.g. an `agent.enabled` flip via `set_agent_config`) is a no-op
|
|
39
|
+
* rather than re-walking `OPERATION_FUNCTION_MAP` and re-logging. The registry's
|
|
40
|
+
* `addTool` is `Map.set`-backed, so this stays correct whether or not the
|
|
41
|
+
* operator also enabled the `mcp:` HTTP surface.
|
|
42
|
+
*/
|
|
43
|
+
export function ensureOperationsToolsRegistered(): void {
|
|
44
|
+
if (operationsRegistered) return;
|
|
45
|
+
registerOperationsTools();
|
|
46
|
+
operationsRegistered = true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Adapt every Operations-profile registry tool into the agent's {@link AgentTool}
|
|
51
|
+
* shape. Call {@link ensureOperationsToolsRegistered} first so the profile is
|
|
52
|
+
* populated.
|
|
53
|
+
*
|
|
54
|
+
* Two identities, deliberately split:
|
|
55
|
+
* - `listingUser` filters `visibleTo` at compose time — which tools the LLM is
|
|
56
|
+
* *shown*. Listing is not a security boundary, so a startup snapshot is fine.
|
|
57
|
+
* - `resolveIdentity()` is called *per tool call* to set `hdb_user`, so the
|
|
58
|
+
* operation is enforced against the agent user's *current* role. This is what
|
|
59
|
+
* honors a role revocation/change without a restart — the enforcement identity
|
|
60
|
+
* is never cached in the handler closure.
|
|
61
|
+
*/
|
|
62
|
+
export function composeRegistryTools(listingUser: AuthedUser, resolveIdentity: () => Promise<AuthedUser>): AgentTool[] {
|
|
63
|
+
const defs = snapshotProfileTools('operations');
|
|
64
|
+
const out: AgentTool[] = [];
|
|
65
|
+
for (const def of defs) {
|
|
66
|
+
if (!def.visibleTo(listingUser)) continue;
|
|
67
|
+
out.push(adaptRegistryTool(def, resolveIdentity));
|
|
68
|
+
}
|
|
69
|
+
log.info?.(`Agent composed ${out.length} operations tool(s) for user '${listingUser?.username ?? 'unknown'}'`);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function adaptRegistryTool(def: RegistryToolDef, resolveIdentity: () => Promise<AuthedUser>): AgentTool {
|
|
74
|
+
return {
|
|
75
|
+
def: { name: def.name, description: def.description, parameters: def.inputSchema },
|
|
76
|
+
// The loop's approval gate keys off `destructive`; the registry expresses the same
|
|
77
|
+
// intent as the MCP `destructiveHint` annotation.
|
|
78
|
+
destructive: def.annotations?.destructiveHint === true,
|
|
79
|
+
handler: async (args: object, ctx: AgentToolContext) => {
|
|
80
|
+
// Resolve the enforcement identity fresh so a live role change is honored. If the
|
|
81
|
+
// configured user can no longer be resolved, `resolveIdentity` throws (fail closed) —
|
|
82
|
+
// the loop turns that into a recoverable tool error rather than running with stale or
|
|
83
|
+
// escalated privileges.
|
|
84
|
+
const user = await resolveIdentity();
|
|
85
|
+
const context: ToolCallContext = {
|
|
86
|
+
user,
|
|
87
|
+
profile: 'operations',
|
|
88
|
+
sessionId: ctx.sessionId,
|
|
89
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
90
|
+
};
|
|
91
|
+
const result = await def.handler(args, context);
|
|
92
|
+
// The loop records a tool observation from the return value and turns a thrown error
|
|
93
|
+
// into a structured `{ ok: false, error }` note for the next model turn. Mapping
|
|
94
|
+
// `isError` → throw keeps a Harper operation failure a recoverable observation rather
|
|
95
|
+
// than aborting the whole run.
|
|
96
|
+
if (result.isError) throw new Error(errorText(result));
|
|
97
|
+
return result.structuredContent ?? textOf(result);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function textOf(result: ToolResult): string {
|
|
103
|
+
return (result.content ?? [])
|
|
104
|
+
.filter((c) => c.type === 'text' && typeof c.text === 'string')
|
|
105
|
+
.map((c) => c.text)
|
|
106
|
+
.join('\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function errorText(result: ToolResult): string {
|
|
110
|
+
const text = textOf(result);
|
|
111
|
+
return text || 'operation failed';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Test-only: reset the one-time registration guard between specs. */
|
|
115
|
+
export function _resetRegistryToolsForTests(): void {
|
|
116
|
+
operationsRegistered = false;
|
|
117
|
+
}
|
package/agent/session.ts
CHANGED
|
@@ -60,7 +60,13 @@ export async function createSession(opts: CreateSessionOpts): Promise<AgentSessi
|
|
|
60
60
|
createdAt: now,
|
|
61
61
|
updatedAt: now,
|
|
62
62
|
};
|
|
63
|
-
|
|
63
|
+
// Write through the Resource-level `put` (transactional), NOT `primaryStore.put`: the transactional
|
|
64
|
+
// path stages the version/local-timestamp metadata so the record is stored with the metadata prefix.
|
|
65
|
+
// A raw `primaryStore.put` writes a prefix-less record, which — when it begins with classic shared-
|
|
66
|
+
// structure record-id #2 (byte 0x42 == 66) — is misread by the RocksDB decode heuristic as a
|
|
67
|
+
// timestamp-prefixed record (8 bytes stripped → corrupt → "Could not find typed structure"). See
|
|
68
|
+
// RecordEncoder.ts:328-335.
|
|
69
|
+
await getAgentSessionTable().put(row);
|
|
64
70
|
return row;
|
|
65
71
|
}
|
|
66
72
|
|
|
@@ -102,7 +108,7 @@ export function appendMessage(sessionId: string, message: AgentMessage): Promise
|
|
|
102
108
|
const session = await requireSession(sessionId);
|
|
103
109
|
session.messages.push(message);
|
|
104
110
|
session.updatedAt = Date.now();
|
|
105
|
-
await getAgentSessionTable().
|
|
111
|
+
await getAgentSessionTable().put(session);
|
|
106
112
|
return session;
|
|
107
113
|
});
|
|
108
114
|
}
|
|
@@ -113,7 +119,7 @@ export function setStatus(sessionId: string, status: AgentRunStatus, lastError?:
|
|
|
113
119
|
session.status = status;
|
|
114
120
|
session.lastError = lastError;
|
|
115
121
|
session.updatedAt = Date.now();
|
|
116
|
-
await getAgentSessionTable().
|
|
122
|
+
await getAgentSessionTable().put(session);
|
|
117
123
|
return session;
|
|
118
124
|
});
|
|
119
125
|
}
|
|
@@ -128,7 +134,7 @@ export function addPendingApproval(
|
|
|
128
134
|
session.pendingApprovals.push(entry);
|
|
129
135
|
session.status = 'awaiting_approval';
|
|
130
136
|
session.updatedAt = Date.now();
|
|
131
|
-
await getAgentSessionTable().
|
|
137
|
+
await getAgentSessionTable().put(session);
|
|
132
138
|
return entry;
|
|
133
139
|
});
|
|
134
140
|
}
|
|
@@ -142,7 +148,7 @@ export function markApprovalConsumed(sessionId: string, approvalId: string): Pro
|
|
|
142
148
|
if (entry.consumed) return;
|
|
143
149
|
entry.consumed = true;
|
|
144
150
|
session.updatedAt = Date.now();
|
|
145
|
-
await getAgentSessionTable().
|
|
151
|
+
await getAgentSessionTable().put(session);
|
|
146
152
|
});
|
|
147
153
|
}
|
|
148
154
|
|
|
@@ -162,7 +168,7 @@ export function resolveApproval(sessionId: string, approvalId: string, approved:
|
|
|
162
168
|
session.status = 'idle';
|
|
163
169
|
}
|
|
164
170
|
session.updatedAt = Date.now();
|
|
165
|
-
await getAgentSessionTable().
|
|
171
|
+
await getAgentSessionTable().put(session);
|
|
166
172
|
return entry;
|
|
167
173
|
});
|
|
168
174
|
}
|
|
@@ -170,7 +176,10 @@ export function resolveApproval(sessionId: string, approvalId: string, approved:
|
|
|
170
176
|
async function requireSession(sessionId: string): Promise<AgentSessionRow> {
|
|
171
177
|
const session = await getSession(sessionId);
|
|
172
178
|
if (!session) throw new Error(`No agent session ${sessionId}`);
|
|
173
|
-
|
|
179
|
+
// The store returns a frozen record, so the read-modify-write mutators below (status, messages,
|
|
180
|
+
// approvals) would throw "Cannot assign to read only property" if they mutated it in place.
|
|
181
|
+
// Return a mutable deep copy; the mutator writes it back with `put`.
|
|
182
|
+
return structuredClone(session);
|
|
174
183
|
}
|
|
175
184
|
|
|
176
185
|
/**
|