@oharness/engine 0.1.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/agent/agent.d.ts +196 -0
- package/dist/agent/agent.js +758 -0
- package/dist/agent/agent.js.map +1 -0
- package/dist/agent/compaction.d.ts +68 -0
- package/dist/agent/compaction.js +179 -0
- package/dist/agent/compaction.js.map +1 -0
- package/dist/agent/hooks.d.ts +77 -0
- package/dist/agent/hooks.js +152 -0
- package/dist/agent/hooks.js.map +1 -0
- package/dist/agent/memory.d.ts +45 -0
- package/dist/agent/memory.js +147 -0
- package/dist/agent/memory.js.map +1 -0
- package/dist/agent/overflow.d.ts +35 -0
- package/dist/agent/overflow.js +69 -0
- package/dist/agent/overflow.js.map +1 -0
- package/dist/agent/permissions.d.ts +181 -0
- package/dist/agent/permissions.js +289 -0
- package/dist/agent/permissions.js.map +1 -0
- package/dist/agent/prompt.d.ts +25 -0
- package/dist/agent/prompt.js +63 -0
- package/dist/agent/prompt.js.map +1 -0
- package/dist/agent/session.d.ts +108 -0
- package/dist/agent/session.js +231 -0
- package/dist/agent/session.js.map +1 -0
- package/dist/agent/settings.d.ts +37 -0
- package/dist/agent/settings.js +113 -0
- package/dist/agent/settings.js.map +1 -0
- package/dist/agent/skills.d.ts +58 -0
- package/dist/agent/skills.js +225 -0
- package/dist/agent/skills.js.map +1 -0
- package/dist/agent/store.d.ts +308 -0
- package/dist/agent/store.js +596 -0
- package/dist/agent/store.js.map +1 -0
- package/dist/agent/subagent.d.ts +44 -0
- package/dist/agent/subagent.js +138 -0
- package/dist/agent/subagent.js.map +1 -0
- package/dist/agent/titles.d.ts +8 -0
- package/dist/agent/titles.js +23 -0
- package/dist/agent/titles.js.map +1 -0
- package/dist/agent/workspace.d.ts +60 -0
- package/dist/agent/workspace.js +156 -0
- package/dist/agent/workspace.js.map +1 -0
- package/dist/artifacts/publish.d.ts +94 -0
- package/dist/artifacts/publish.js +104 -0
- package/dist/artifacts/publish.js.map +1 -0
- package/dist/artifacts/store.d.ts +324 -0
- package/dist/artifacts/store.js +500 -0
- package/dist/artifacts/store.js.map +1 -0
- package/dist/auth/index.d.ts +13 -0
- package/dist/auth/index.js +10 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/manager.d.ts +83 -0
- package/dist/auth/manager.js +266 -0
- package/dist/auth/manager.js.map +1 -0
- package/dist/auth/oauth.d.ts +83 -0
- package/dist/auth/oauth.js +327 -0
- package/dist/auth/oauth.js.map +1 -0
- package/dist/auth/providers/account.d.ts +59 -0
- package/dist/auth/providers/account.js +282 -0
- package/dist/auth/providers/account.js.map +1 -0
- package/dist/auth/providers/anthropic.d.ts +24 -0
- package/dist/auth/providers/anthropic.js +209 -0
- package/dist/auth/providers/anthropic.js.map +1 -0
- package/dist/auth/providers/generic.d.ts +57 -0
- package/dist/auth/providers/generic.js +155 -0
- package/dist/auth/providers/generic.js.map +1 -0
- package/dist/auth/providers/official.d.ts +3 -0
- package/dist/auth/providers/official.js +90 -0
- package/dist/auth/providers/official.js.map +1 -0
- package/dist/auth/providers/ottoport.d.ts +3 -0
- package/dist/auth/providers/ottoport.js +60 -0
- package/dist/auth/providers/ottoport.js.map +1 -0
- package/dist/auth/store.d.ts +22 -0
- package/dist/auth/store.js +138 -0
- package/dist/auth/store.js.map +1 -0
- package/dist/auth/types.d.ts +167 -0
- package/dist/auth/types.js +16 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/config.d.ts +197 -0
- package/dist/config.js +195 -0
- package/dist/config.js.map +1 -0
- package/dist/core/errors.d.ts +46 -0
- package/dist/core/errors.js +83 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/tokens.d.ts +12 -0
- package/dist/core/tokens.js +56 -0
- package/dist/core/tokens.js.map +1 -0
- package/dist/core/types.d.ts +91 -0
- package/dist/core/types.js +49 -0
- package/dist/core/types.js.map +1 -0
- package/dist/harness.d.ts +302 -0
- package/dist/harness.js +695 -0
- package/dist/harness.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/client.d.ts +55 -0
- package/dist/mcp/client.js +199 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/index.js +8 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/manager.d.ts +77 -0
- package/dist/mcp/manager.js +325 -0
- package/dist/mcp/manager.js.map +1 -0
- package/dist/mcp/oauth.d.ts +63 -0
- package/dist/mcp/oauth.js +302 -0
- package/dist/mcp/oauth.js.map +1 -0
- package/dist/mcp/protocol.d.ts +130 -0
- package/dist/mcp/protocol.js +14 -0
- package/dist/mcp/protocol.js.map +1 -0
- package/dist/mcp/registry.d.ts +52 -0
- package/dist/mcp/registry.js +100 -0
- package/dist/mcp/registry.js.map +1 -0
- package/dist/mcp/registry.json +293 -0
- package/dist/mcp/secrets.d.ts +20 -0
- package/dist/mcp/secrets.js +75 -0
- package/dist/mcp/secrets.js.map +1 -0
- package/dist/mcp/transport.d.ts +51 -0
- package/dist/mcp/transport.js +158 -0
- package/dist/mcp/transport.js.map +1 -0
- package/dist/paths.d.ts +37 -0
- package/dist/paths.js +63 -0
- package/dist/paths.js.map +1 -0
- package/dist/plugins.d.ts +55 -0
- package/dist/plugins.js +201 -0
- package/dist/plugins.js.map +1 -0
- package/dist/providers/builtin.d.ts +23 -0
- package/dist/providers/builtin.js +141 -0
- package/dist/providers/builtin.js.map +1 -0
- package/dist/providers/catalog.d.ts +129 -0
- package/dist/providers/catalog.js +254 -0
- package/dist/providers/catalog.js.map +1 -0
- package/dist/providers/catalog.json +472 -0
- package/dist/providers/discovery.d.ts +16 -0
- package/dist/providers/discovery.js +161 -0
- package/dist/providers/discovery.js.map +1 -0
- package/dist/providers/http.d.ts +24 -0
- package/dist/providers/http.js +139 -0
- package/dist/providers/http.js.map +1 -0
- package/dist/providers/index.d.ts +10 -0
- package/dist/providers/index.js +8 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/piai.d.ts +124 -0
- package/dist/providers/piai.js +551 -0
- package/dist/providers/piai.js.map +1 -0
- package/dist/providers/registry.d.ts +65 -0
- package/dist/providers/registry.js +195 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/resilient.d.ts +46 -0
- package/dist/providers/resilient.js +117 -0
- package/dist/providers/resilient.js.map +1 -0
- package/dist/providers/types.d.ts +182 -0
- package/dist/providers/types.js +50 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/schedule/cron.d.ts +35 -0
- package/dist/schedule/cron.js +125 -0
- package/dist/schedule/cron.js.map +1 -0
- package/dist/schedule/scheduler.d.ts +87 -0
- package/dist/schedule/scheduler.js +237 -0
- package/dist/schedule/scheduler.js.map +1 -0
- package/dist/storage/kv.d.ts +56 -0
- package/dist/storage/kv.js +173 -0
- package/dist/storage/kv.js.map +1 -0
- package/dist/storage/migrate.d.ts +11 -0
- package/dist/storage/migrate.js +93 -0
- package/dist/storage/migrate.js.map +1 -0
- package/dist/storage/project.d.ts +9 -0
- package/dist/storage/project.js +57 -0
- package/dist/storage/project.js.map +1 -0
- package/dist/storage/records.d.ts +32 -0
- package/dist/storage/records.js +48 -0
- package/dist/storage/records.js.map +1 -0
- package/dist/sync/client.d.ts +79 -0
- package/dist/sync/client.js +118 -0
- package/dist/sync/client.js.map +1 -0
- package/dist/sync/mirror.d.ts +85 -0
- package/dist/sync/mirror.js +389 -0
- package/dist/sync/mirror.js.map +1 -0
- package/dist/sync/protocol.d.ts +67 -0
- package/dist/sync/protocol.js +16 -0
- package/dist/sync/protocol.js.map +1 -0
- package/dist/sync/settings.d.ts +56 -0
- package/dist/sync/settings.js +60 -0
- package/dist/sync/settings.js.map +1 -0
- package/dist/tools/builtin/artifact.d.ts +16 -0
- package/dist/tools/builtin/artifact.js +134 -0
- package/dist/tools/builtin/artifact.js.map +1 -0
- package/dist/tools/builtin/bash.d.ts +3 -0
- package/dist/tools/builtin/bash.js +142 -0
- package/dist/tools/builtin/bash.js.map +1 -0
- package/dist/tools/builtin/fs.d.ts +19 -0
- package/dist/tools/builtin/fs.js +280 -0
- package/dist/tools/builtin/fs.js.map +1 -0
- package/dist/tools/builtin/index.d.ts +22 -0
- package/dist/tools/builtin/index.js +42 -0
- package/dist/tools/builtin/index.js.map +1 -0
- package/dist/tools/builtin/mcp-resource.d.ts +3 -0
- package/dist/tools/builtin/mcp-resource.js +220 -0
- package/dist/tools/builtin/mcp-resource.js.map +1 -0
- package/dist/tools/builtin/memory.d.ts +10 -0
- package/dist/tools/builtin/memory.js +76 -0
- package/dist/tools/builtin/memory.js.map +1 -0
- package/dist/tools/builtin/recall.d.ts +13 -0
- package/dist/tools/builtin/recall.js +163 -0
- package/dist/tools/builtin/recall.js.map +1 -0
- package/dist/tools/builtin/search.d.ts +3 -0
- package/dist/tools/builtin/search.js +240 -0
- package/dist/tools/builtin/search.js.map +1 -0
- package/dist/tools/builtin/skill.d.ts +10 -0
- package/dist/tools/builtin/skill.js +46 -0
- package/dist/tools/builtin/skill.js.map +1 -0
- package/dist/tools/builtin/task.d.ts +9 -0
- package/dist/tools/builtin/task.js +68 -0
- package/dist/tools/builtin/task.js.map +1 -0
- package/dist/tools/builtin/todo.d.ts +20 -0
- package/dist/tools/builtin/todo.js +63 -0
- package/dist/tools/builtin/todo.js.map +1 -0
- package/dist/tools/define.d.ts +19 -0
- package/dist/tools/define.js +39 -0
- package/dist/tools/define.js.map +1 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/registry.d.ts +39 -0
- package/dist/tools/registry.js +101 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/types.d.ts +112 -0
- package/dist/tools/types.js +22 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/util/id.d.ts +9 -0
- package/dist/util/id.js +19 -0
- package/dist/util/id.js.map +1 -0
- package/dist/util/json.d.ts +25 -0
- package/dist/util/json.js +100 -0
- package/dist/util/json.js.map +1 -0
- package/dist/util/jwt.d.ts +74 -0
- package/dist/util/jwt.js +177 -0
- package/dist/util/jwt.js.map +1 -0
- package/dist/util/logger.d.ts +13 -0
- package/dist/util/logger.js +59 -0
- package/dist/util/logger.js.map +1 -0
- package/dist/util/queue.d.ts +26 -0
- package/dist/util/queue.js +106 -0
- package/dist/util/queue.js.map +1 -0
- package/dist/util/schema.d.ts +17 -0
- package/dist/util/schema.js +98 -0
- package/dist/util/schema.js.map +1 -0
- package/dist/util/shell.d.ts +20 -0
- package/dist/util/shell.js +24 -0
- package/dist/util/shell.js.map +1 -0
- package/official.json +4 -0
- package/package.json +38 -0
- package/skills/artifact-authoring/SKILL.md +81 -0
- package/skills/canvas/SKILL.md +85 -0
- package/skills/skill-authoring/SKILL.md +85 -0
package/dist/harness.js
ADDED
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
import { AuthManager } from "./auth/manager.js";
|
|
2
|
+
import { ConfigError } from "./core/errors.js";
|
|
3
|
+
import { apiKeyProvider, oauthProvider } from "./auth/providers/generic.js";
|
|
4
|
+
import { Agent } from "./agent/agent.js";
|
|
5
|
+
import { HookRegistry } from "./agent/hooks.js";
|
|
6
|
+
import { Session } from "./agent/session.js";
|
|
7
|
+
import { SessionStore } from "./agent/store.js";
|
|
8
|
+
import { BUILTIN_SUBAGENTS, SubagentRegistry, loadMarkdownSubagents } from "./agent/subagent.js";
|
|
9
|
+
import { SkillRegistry } from "./agent/skills.js";
|
|
10
|
+
import { SettingsStore } from "./agent/settings.js";
|
|
11
|
+
import { createPolicy } from "./agent/workspace.js";
|
|
12
|
+
import { MemoryStore } from "./agent/memory.js";
|
|
13
|
+
import { loadConfig, loadProjectConfig, loadProjectInstructions, mergeConfig } from "./config.js";
|
|
14
|
+
import { discoverPlugins, pluginContributions } from "./plugins.js";
|
|
15
|
+
import { McpManager } from "./mcp/manager.js";
|
|
16
|
+
import { beginMcpLogin, forgetMcpAuth } from "./mcp/oauth.js";
|
|
17
|
+
import { expandSecrets } from "./mcp/secrets.js";
|
|
18
|
+
import { Scheduler } from "./schedule/scheduler.js";
|
|
19
|
+
import { SyncClient } from "./sync/client.js";
|
|
20
|
+
import { SessionMirror } from "./sync/mirror.js";
|
|
21
|
+
import { isExcluded, resolveSync } from "./sync/settings.js";
|
|
22
|
+
import { projectId } from "./storage/project.js";
|
|
23
|
+
import { updateUserConfig } from "./config.js";
|
|
24
|
+
import { ProviderRegistry } from "./providers/registry.js";
|
|
25
|
+
import { loadCatalog, userCatalogPath } from "./providers/catalog.js";
|
|
26
|
+
import { builtinTools } from "./tools/builtin/index.js";
|
|
27
|
+
import { ToolRegistry } from "./tools/registry.js";
|
|
28
|
+
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
import { createLogger } from "./util/logger.js";
|
|
31
|
+
/**
|
|
32
|
+
* Top-level facade: owns providers, tools, MCP connections, hooks and
|
|
33
|
+
* subagents, and hands out `Agent` instances bound to a `Session`.
|
|
34
|
+
*
|
|
35
|
+
* Construct it with `Harness.create()` rather than `new`, because MCP
|
|
36
|
+
* connection and config loading are async.
|
|
37
|
+
*/
|
|
38
|
+
export class Harness {
|
|
39
|
+
cwd;
|
|
40
|
+
config;
|
|
41
|
+
providers;
|
|
42
|
+
tools;
|
|
43
|
+
hooks;
|
|
44
|
+
subagents;
|
|
45
|
+
skills;
|
|
46
|
+
memory;
|
|
47
|
+
settings;
|
|
48
|
+
workspace;
|
|
49
|
+
mcp;
|
|
50
|
+
store;
|
|
51
|
+
auth;
|
|
52
|
+
catalog;
|
|
53
|
+
scheduler;
|
|
54
|
+
ephemeral;
|
|
55
|
+
/** Mirrors this project's transcripts, when sync is on and signed in. */
|
|
56
|
+
mirror;
|
|
57
|
+
syncState = {
|
|
58
|
+
on: false,
|
|
59
|
+
reason: "no-credential",
|
|
60
|
+
};
|
|
61
|
+
/** Active declarative plugin bundles, in their effective override order. */
|
|
62
|
+
plugins;
|
|
63
|
+
logger;
|
|
64
|
+
options;
|
|
65
|
+
projectInstructions;
|
|
66
|
+
constructor(opts, config, catalog, plugins = []) {
|
|
67
|
+
this.options = opts;
|
|
68
|
+
this.cwd = opts.cwd ?? process.cwd();
|
|
69
|
+
this.config = config;
|
|
70
|
+
this.logger = opts.logger ?? createLogger("harness", config.logLevel ?? "warn", opts.logSink);
|
|
71
|
+
this.auth = new AuthManager({ logger: this.logger });
|
|
72
|
+
for (const provider of opts.authProviders ?? [])
|
|
73
|
+
this.auth.register(provider);
|
|
74
|
+
// Subscription vendors declared in config become first-class auth providers.
|
|
75
|
+
for (const vendor of config.oauthProviders ?? []) {
|
|
76
|
+
this.auth.register(oauthProvider(vendor));
|
|
77
|
+
}
|
|
78
|
+
// The resolver is called on every request, so an expiring subscription
|
|
79
|
+
// token refreshes without the session noticing.
|
|
80
|
+
this.catalog = catalog;
|
|
81
|
+
this.plugins = plugins;
|
|
82
|
+
this.providers = ProviderRegistry.fromCatalog(catalog, {
|
|
83
|
+
creds: config.providers,
|
|
84
|
+
auth: (ctx) => this.auth.resolve(ctx),
|
|
85
|
+
});
|
|
86
|
+
// Every catalog provider gets an API-key login entry, so `auth login`
|
|
87
|
+
// covers whatever the catalog declares rather than a hardcoded three.
|
|
88
|
+
for (const entry of this.providers.listCatalogProviders()) {
|
|
89
|
+
if (this.auth.get(entry.id))
|
|
90
|
+
continue; // a richer provider already claimed it
|
|
91
|
+
this.auth.register(apiKeyProvider({
|
|
92
|
+
id: entry.id,
|
|
93
|
+
label: entry.label,
|
|
94
|
+
envVars: entry.envVars,
|
|
95
|
+
placeholder: entry.docsUrl ? `get one at ${entry.docsUrl}` : undefined,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
for (const vendor of config.openAiCompatible ?? []) {
|
|
99
|
+
this.providers.registerOpenAiCompatible(vendor);
|
|
100
|
+
}
|
|
101
|
+
for (const model of config.models ?? [])
|
|
102
|
+
this.providers.registerModel(model);
|
|
103
|
+
this.tools = new ToolRegistry().registerAll(builtinTools);
|
|
104
|
+
for (const tool of opts.tools ?? [])
|
|
105
|
+
this.tools.register(tool);
|
|
106
|
+
this.hooks = new HookRegistry(this.logger).loadShellHooks(config.hooks ?? {});
|
|
107
|
+
this.subagents = new SubagentRegistry([
|
|
108
|
+
...BUILTIN_SUBAGENTS,
|
|
109
|
+
...(config.subagents ?? []),
|
|
110
|
+
]);
|
|
111
|
+
this.skills = new SkillRegistry(this.logger);
|
|
112
|
+
this.settings = new SettingsStore(this.cwd);
|
|
113
|
+
this.workspace = createPolicy(this.cwd, {
|
|
114
|
+
...(config.workspaceMode !== undefined ? { mode: config.workspaceMode } : {}),
|
|
115
|
+
// Every directory a registry scans is one the agent can work in. A skill
|
|
116
|
+
// it is told about but cannot open is a skill it cannot follow, and one
|
|
117
|
+
// it cannot edit is a skill it cannot fix — `skillDirs` reaches outside
|
|
118
|
+
// the project by design, and stopping at the project boundary would put
|
|
119
|
+
// half of what the harness loads permanently out of reach.
|
|
120
|
+
//
|
|
121
|
+
// This includes the plugin directories read out of Claude Code's cache.
|
|
122
|
+
// Those belong to another program's installer, so the agent editing one
|
|
123
|
+
// edits something this harness did not put there; the alternative was a
|
|
124
|
+
// registry that advertises skills nobody can repair, and between the two
|
|
125
|
+
// this is the one that was asked for.
|
|
126
|
+
writableRoots: [
|
|
127
|
+
...(config.writableRoots ?? []),
|
|
128
|
+
...(config.skillDirs ?? []),
|
|
129
|
+
...(config.agentDirs ?? []),
|
|
130
|
+
],
|
|
131
|
+
...(config.readableRoots !== undefined ? { readableRoots: config.readableRoots } : {}),
|
|
132
|
+
});
|
|
133
|
+
this.memory = new MemoryStore(config.memoryDir
|
|
134
|
+
? path.resolve(this.cwd, config.memoryDir)
|
|
135
|
+
: path.join(this.cwd, ".oharness", "memory"));
|
|
136
|
+
this.mcp = new McpManager(this.logger, this.auth.store);
|
|
137
|
+
this.store = new SessionStore(config.sessionDir);
|
|
138
|
+
this.scheduler = new Scheduler(this, config.schedules ?? []);
|
|
139
|
+
this.ephemeral = opts.ephemeral === true;
|
|
140
|
+
}
|
|
141
|
+
static async create(opts = {}) {
|
|
142
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
143
|
+
const baseConfig = opts.config ?? (await loadConfig(cwd));
|
|
144
|
+
const plugins = await discoverPlugins(cwd, baseConfig.plugins);
|
|
145
|
+
// Explicit user/project config is applied after plugins, so it can always
|
|
146
|
+
// tighten policy or replace a plugin's MCP server declaration.
|
|
147
|
+
const config = mergeConfig(pluginContributions(plugins), baseConfig);
|
|
148
|
+
// The catalog is data on disk, so it can be updated without a release.
|
|
149
|
+
const catalog = await loadCatalog(config.catalog ? [config.catalog] : []);
|
|
150
|
+
const harness = new Harness(opts, config, catalog, plugins);
|
|
151
|
+
harness.projectInstructions = await loadProjectInstructions(cwd, config.instructionsFile);
|
|
152
|
+
// Rules the user approved in earlier sessions live in settings files, not
|
|
153
|
+
// in the config the project checks in.
|
|
154
|
+
const persisted = await harness.settings.readAll();
|
|
155
|
+
harness.config.permissions = {
|
|
156
|
+
allow: [...(config.permissions?.allow ?? []), ...(persisted.allow ?? [])],
|
|
157
|
+
deny: [...(config.permissions?.deny ?? []), ...(persisted.deny ?? [])],
|
|
158
|
+
ask: [...(config.permissions?.ask ?? []), ...(persisted.ask ?? [])],
|
|
159
|
+
};
|
|
160
|
+
// Best effort: reclaiming disk should never block or fail a session.
|
|
161
|
+
if (config.sessionRetention && !opts.ephemeral) {
|
|
162
|
+
harness.store
|
|
163
|
+
.prune(config.sessionRetention)
|
|
164
|
+
.then((removed) => {
|
|
165
|
+
if (removed.length > 0) {
|
|
166
|
+
harness.logger.info("pruned old sessions", { count: removed.length });
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
.catch((err) => harness.logger.warn("session pruning failed", { error: String(err) }));
|
|
170
|
+
}
|
|
171
|
+
await harness.skills.discover(cwd, config.skillDirs ?? []);
|
|
172
|
+
// Markdown definitions layer over the JSON ones, so a repo can drop in an
|
|
173
|
+
// agent file without editing config.
|
|
174
|
+
for (const definition of await loadMarkdownSubagents(cwd, config.agentDirs ?? [])) {
|
|
175
|
+
harness.subagents.register(definition);
|
|
176
|
+
}
|
|
177
|
+
if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
|
|
178
|
+
const statuses = await harness.mcp.connectAll(config.mcpServers);
|
|
179
|
+
harness.tools.registerAll(harness.mcp.tools());
|
|
180
|
+
for (const status of statuses) {
|
|
181
|
+
if (status.error) {
|
|
182
|
+
harness.logger.warn("MCP server unavailable", {
|
|
183
|
+
server: status.name,
|
|
184
|
+
error: status.error,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Awaited, though it touches no network: deciding whether to mirror reads
|
|
190
|
+
// config and asks git for the project id, and both are local. Leaving it
|
|
191
|
+
// unawaited made `syncStatus()` lie for the first few milliseconds of
|
|
192
|
+
// every process, which is exactly when a UI asks. The connecting happens
|
|
193
|
+
// inside the mirror and is not waited for.
|
|
194
|
+
await harness.startSync();
|
|
195
|
+
return harness;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Begin mirroring, if this machine is configured and signed in for it.
|
|
199
|
+
*
|
|
200
|
+
* Every reason not to is a normal state rather than an error — not signed
|
|
201
|
+
* in, switched off, this one repository excluded — so nothing here logs a
|
|
202
|
+
* failure for them. Only an actual attempt that goes wrong is worth a line.
|
|
203
|
+
*/
|
|
204
|
+
async startSync() {
|
|
205
|
+
// An ephemeral harness has no session store to mirror; a subagent's would
|
|
206
|
+
// otherwise open a second connection for transcripts nobody keeps.
|
|
207
|
+
if (this.ephemeral)
|
|
208
|
+
return;
|
|
209
|
+
const decision = resolveSync({
|
|
210
|
+
config: this.config.sync,
|
|
211
|
+
accountToken: await this.accountToken(),
|
|
212
|
+
});
|
|
213
|
+
this.syncState = decision;
|
|
214
|
+
if (!decision.on)
|
|
215
|
+
return;
|
|
216
|
+
const project = await projectId(this.cwd);
|
|
217
|
+
if (isExcluded(this.config.sync, { id: project, cwd: this.cwd })) {
|
|
218
|
+
this.syncState = { on: false, reason: "excluded" };
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const configured = this.config.sync?.token ?? process.env["OHARNESS_SYNC_TOKEN"];
|
|
222
|
+
this.mirror = new SessionMirror({
|
|
223
|
+
store: this.store,
|
|
224
|
+
client: new SyncClient({
|
|
225
|
+
url: decision.target.url,
|
|
226
|
+
// Read per request rather than captured: an account token lasts an
|
|
227
|
+
// hour and is refreshed underneath, and a mirror holding the string it
|
|
228
|
+
// started with would stop for good once that hour was up.
|
|
229
|
+
token: configured ? configured : async () => (await this.accountToken()) ?? "",
|
|
230
|
+
...(this.config.sync?.team ? { team: this.config.sync.team } : {}),
|
|
231
|
+
}),
|
|
232
|
+
projectId: project,
|
|
233
|
+
onError: (message) => this.logger.warn(message),
|
|
234
|
+
});
|
|
235
|
+
this.mirror.start();
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The roots this machine was told to allow, as opposed to the ones it
|
|
239
|
+
* worked out.
|
|
240
|
+
*
|
|
241
|
+
* `workspace.writableRoots` is the sum of a choice and a derivation: what
|
|
242
|
+
* the config named, plus every skill and agent directory the installed
|
|
243
|
+
* plugins contributed. Only the first half is editable, and telling them
|
|
244
|
+
* apart is the difference between a settings row that works and one whose
|
|
245
|
+
* changes come back on the next launch.
|
|
246
|
+
*/
|
|
247
|
+
configuredWritableRoots() {
|
|
248
|
+
return [...(this.config.writableRoots ?? [])];
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Replace the roots this machine allows writes to.
|
|
252
|
+
*
|
|
253
|
+
* Takes effect on the next launch rather than now: the workspace policy is
|
|
254
|
+
* built once and handed to every tool, and swapping it under a turn already
|
|
255
|
+
* in flight would let a command that was checked against one answer run
|
|
256
|
+
* against another.
|
|
257
|
+
*/
|
|
258
|
+
async setWritableRoots(roots) {
|
|
259
|
+
const cleaned = [...new Set(roots.map((root) => root.trim()).filter(Boolean))];
|
|
260
|
+
await updateUserConfig((current) => ({ ...current, writableRoots: cleaned }));
|
|
261
|
+
return cleaned;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Whether permission decisions are being recorded, and who decided that.
|
|
265
|
+
*
|
|
266
|
+
* Recording is on unless somebody turned it off: the trail is a handful of
|
|
267
|
+
* short lines per turn, it never leaves the machine, and the state where it
|
|
268
|
+
* is missing is exactly the state where it turns out to have been wanted.
|
|
269
|
+
*
|
|
270
|
+
* The source travels with the answer for the same reason sync carries a
|
|
271
|
+
* reason: a merged config cannot tell a value the user chose from one the
|
|
272
|
+
* repository imposed, and only the first can be changed from here. A switch
|
|
273
|
+
* offered over the second would appear to work and come back on the next
|
|
274
|
+
* launch, which is worse than no switch at all.
|
|
275
|
+
*/
|
|
276
|
+
async auditStatus() {
|
|
277
|
+
// Best effort: a project config that has since become unreadable should
|
|
278
|
+
// not take the settings page down with it. Treated as "not the project's",
|
|
279
|
+
// which is the state the user can still act on.
|
|
280
|
+
const project = await loadProjectConfig(this.cwd).catch(() => undefined);
|
|
281
|
+
const file = this.config.auditLog;
|
|
282
|
+
return {
|
|
283
|
+
on: this.config.audit !== false,
|
|
284
|
+
...(file ? { file, path: path.resolve(this.cwd, file) } : {}),
|
|
285
|
+
source: project?.audit === undefined ? "user" : "project",
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Start or stop recording permission decisions, and remember the choice.
|
|
290
|
+
*
|
|
291
|
+
* Only the recording is a choice here. Where an extra copy is written is
|
|
292
|
+
* `auditLog`, and that stays where somebody typed it: a switch that also
|
|
293
|
+
* deleted a configured path would turn "stop recording for now" into
|
|
294
|
+
* "forget where my collector reads from".
|
|
295
|
+
*
|
|
296
|
+
* On is written as the *absence* of the key, because on is the default and a
|
|
297
|
+
* config file should say what somebody decided, not restate what they left
|
|
298
|
+
* alone. Takes effect on the sessions already open — see `equip`.
|
|
299
|
+
*/
|
|
300
|
+
async setAudit(enabled) {
|
|
301
|
+
const status = await this.auditStatus();
|
|
302
|
+
if (status.source === "project") {
|
|
303
|
+
throw new ConfigError("this project's config decides the audit trail, so it cannot be changed here");
|
|
304
|
+
}
|
|
305
|
+
if (status.on === enabled)
|
|
306
|
+
return status;
|
|
307
|
+
this.config.audit = enabled;
|
|
308
|
+
await updateUserConfig((current) => {
|
|
309
|
+
const { audit: _previous, ...rest } = current;
|
|
310
|
+
return enabled ? rest : { ...rest, audit: false };
|
|
311
|
+
});
|
|
312
|
+
return this.auditStatus();
|
|
313
|
+
}
|
|
314
|
+
/** A session's permission trail, oldest first. Empty for an ephemeral one. */
|
|
315
|
+
async decisions(sessionId) {
|
|
316
|
+
if (this.ephemeral)
|
|
317
|
+
return [];
|
|
318
|
+
return this.store.decisions(sessionId);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Push whatever is waiting, now.
|
|
322
|
+
*
|
|
323
|
+
* The mirror pushes on its own — this is for the person who wants to know it
|
|
324
|
+
* has happened before closing the laptop, which is not a question a
|
|
325
|
+
* background timer can answer. False means there was nothing to push it
|
|
326
|
+
* through: off, excluded, or not signed in.
|
|
327
|
+
*/
|
|
328
|
+
async syncNow() {
|
|
329
|
+
if (!this.mirror)
|
|
330
|
+
return false;
|
|
331
|
+
await this.mirror.flush();
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* The OHarness account access token, if signed in.
|
|
336
|
+
*
|
|
337
|
+
* Only `official` is asked. Other gateway entries in the catalog hold keys
|
|
338
|
+
* to *their* vendor, not to an OHarness account, and handing one of those to
|
|
339
|
+
* the artifact API would authenticate as nobody — the 401 that follows is a
|
|
340
|
+
* far worse way to find out than not trying.
|
|
341
|
+
*
|
|
342
|
+
* Public because publishing an artifact needs it too, and a second copy of
|
|
343
|
+
* this rule in the server package would be a second place to get it wrong.
|
|
344
|
+
*/
|
|
345
|
+
async accountToken() {
|
|
346
|
+
return await this.auth.accessToken("official").catch(() => undefined);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Whether transcripts are leaving this machine, and why not when they are
|
|
350
|
+
* not.
|
|
351
|
+
*
|
|
352
|
+
* The reason is carried rather than collapsed to a boolean because "you
|
|
353
|
+
* turned this off" and "you are not signed in" look identical from outside
|
|
354
|
+
* and need opposite advice.
|
|
355
|
+
*/
|
|
356
|
+
syncStatus() {
|
|
357
|
+
if (this.syncState.on) {
|
|
358
|
+
return { on: true, url: this.syncState.target.url };
|
|
359
|
+
}
|
|
360
|
+
return { on: false, reason: this.syncState.reason };
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Turn sync on or off now, and remember the choice.
|
|
364
|
+
*
|
|
365
|
+
* Written to the user's config rather than held in memory, because a switch
|
|
366
|
+
* that forgets on restart is not a switch — someone who turned sync off did
|
|
367
|
+
* not mean "until the next launch". Stopping aborts the poll in flight, so
|
|
368
|
+
* the connection does not outlive the decision.
|
|
369
|
+
*/
|
|
370
|
+
async setSyncEnabled(enabled) {
|
|
371
|
+
this.config.sync = { ...this.config.sync, enabled };
|
|
372
|
+
await updateUserConfig((current) => ({
|
|
373
|
+
...current,
|
|
374
|
+
sync: { ...current.sync, enabled },
|
|
375
|
+
}));
|
|
376
|
+
if (enabled) {
|
|
377
|
+
if (!this.mirror)
|
|
378
|
+
await this.startSync();
|
|
379
|
+
else
|
|
380
|
+
this.mirror.start();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
this.mirror?.stop();
|
|
384
|
+
this.mirror = undefined;
|
|
385
|
+
this.syncState = { on: false, reason: "disabled" };
|
|
386
|
+
}
|
|
387
|
+
/** Connect an MCP server at runtime and expose its tools immediately. */
|
|
388
|
+
async addMcpServer(name, config) {
|
|
389
|
+
this.tools.unregisterServer(name);
|
|
390
|
+
const tools = await this.mcp.connect(name, config);
|
|
391
|
+
this.tools.registerAll(tools);
|
|
392
|
+
return (this.mcp.status().find((s) => s.name === name) ?? {
|
|
393
|
+
name,
|
|
394
|
+
connected: false,
|
|
395
|
+
transport: config.type,
|
|
396
|
+
toolCount: 0,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Begin an OAuth login for a connector that asked for one.
|
|
401
|
+
*
|
|
402
|
+
* The connector does not have to be connected — a server that answered 401
|
|
403
|
+
* is exactly the case this exists for. On success the server is reconnected,
|
|
404
|
+
* so the tools appear without the user doing anything else.
|
|
405
|
+
*/
|
|
406
|
+
async mcpLogin(name) {
|
|
407
|
+
const config = this.mcp.configFor(name);
|
|
408
|
+
if (!config)
|
|
409
|
+
throw new ConfigError(`no MCP server named "${name}"`);
|
|
410
|
+
if (config.type !== "http") {
|
|
411
|
+
throw new ConfigError(`"${name}" runs locally and has nothing to log in to`);
|
|
412
|
+
}
|
|
413
|
+
const expanded = expandSecrets(config);
|
|
414
|
+
const challenge = this.mcp.authChallenge(name);
|
|
415
|
+
// The challenge's URL is the one the server actually answered on, which is
|
|
416
|
+
// what its authorization server will have been told about.
|
|
417
|
+
const url = challenge?.url ?? (expanded.type === "http" ? expanded.url : "");
|
|
418
|
+
const result = await beginMcpLogin({
|
|
419
|
+
serverUrl: url,
|
|
420
|
+
store: this.auth.store,
|
|
421
|
+
...(challenge?.challenge ? { challenge: challenge.challenge } : {}),
|
|
422
|
+
});
|
|
423
|
+
if (result.method !== "auto")
|
|
424
|
+
return result;
|
|
425
|
+
const finish = result.callback;
|
|
426
|
+
return {
|
|
427
|
+
...result,
|
|
428
|
+
method: "auto",
|
|
429
|
+
callback: async () => {
|
|
430
|
+
const outcome = await finish();
|
|
431
|
+
if (outcome.type === "success")
|
|
432
|
+
await this.addMcpServer(name, config);
|
|
433
|
+
return outcome;
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
/** Forget a connector's credential. It will ask again on the next connect. */
|
|
438
|
+
async mcpLogout(name) {
|
|
439
|
+
const config = this.mcp.configFor(name);
|
|
440
|
+
if (!config || config.type !== "http")
|
|
441
|
+
return false;
|
|
442
|
+
const expanded = expandSecrets(config);
|
|
443
|
+
if (expanded.type !== "http")
|
|
444
|
+
return false;
|
|
445
|
+
return forgetMcpAuth(expanded.url, this.auth.store);
|
|
446
|
+
}
|
|
447
|
+
async removeMcpServer(name) {
|
|
448
|
+
// `remove`, not `disconnect`: an explicitly removed server must not come
|
|
449
|
+
// back through the reconnect path on the next tool call.
|
|
450
|
+
await this.mcp.remove(name);
|
|
451
|
+
this.tools.unregisterServer(name);
|
|
452
|
+
}
|
|
453
|
+
mcpStatus() {
|
|
454
|
+
return this.mcp.status();
|
|
455
|
+
}
|
|
456
|
+
authStatus() {
|
|
457
|
+
return this.auth.status();
|
|
458
|
+
}
|
|
459
|
+
/** Providers that can actually authenticate right now. */
|
|
460
|
+
async configuredProviders() {
|
|
461
|
+
const out = new Set();
|
|
462
|
+
for (const provider of this.providers.listProviders()) {
|
|
463
|
+
if (provider.isConfigured() || (await this.auth.isAuthenticated(provider.id))) {
|
|
464
|
+
out.add(provider.id);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Ask providers which models they actually serve, and persist the result to
|
|
471
|
+
* the user catalog so the next run starts with a current list.
|
|
472
|
+
*/
|
|
473
|
+
async refreshModels(providerId) {
|
|
474
|
+
const discovered = await this.providers.refreshModels(providerId);
|
|
475
|
+
if (discovered.length > 0) {
|
|
476
|
+
await this.saveDiscovered(discovered).catch((err) => this.logger.warn("could not persist the discovered catalog", {
|
|
477
|
+
error: String(err),
|
|
478
|
+
}));
|
|
479
|
+
}
|
|
480
|
+
return discovered;
|
|
481
|
+
}
|
|
482
|
+
/** Merge discovered models into the user catalog on disk. */
|
|
483
|
+
async saveDiscovered(models) {
|
|
484
|
+
const file = userCatalogPath();
|
|
485
|
+
let existing = {};
|
|
486
|
+
try {
|
|
487
|
+
existing = JSON.parse(await readFile(file, "utf8"));
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
// No user catalog yet; start one.
|
|
491
|
+
}
|
|
492
|
+
const byId = new Map((Array.isArray(existing["models"]) ? existing["models"] : [])
|
|
493
|
+
.filter((m) => !!m && typeof m === "object")
|
|
494
|
+
.map((m) => [String(m["id"]), m]));
|
|
495
|
+
for (const model of models)
|
|
496
|
+
byId.set(model.id, model);
|
|
497
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
498
|
+
await writeFile(file, JSON.stringify({ ...existing, discoveredAt: new Date().toISOString(), models: [...byId.values()] }, null, 2), "utf8");
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Re-scan the skill directories.
|
|
502
|
+
*
|
|
503
|
+
* Skills are ordinary markdown on disk and the agent can write them, so
|
|
504
|
+
* without this an authored skill sits unusable until the next restart —
|
|
505
|
+
* which breaks the one loop that would let the agent improve itself.
|
|
506
|
+
*/
|
|
507
|
+
async reloadSkills() {
|
|
508
|
+
await this.skills.discover(this.cwd, this.config.skillDirs ?? []);
|
|
509
|
+
return this.skills.size;
|
|
510
|
+
}
|
|
511
|
+
/** Providers declared by the catalog, for `/models` and login hints. */
|
|
512
|
+
listCatalogProviders() {
|
|
513
|
+
return this.providers.listCatalogProviders();
|
|
514
|
+
}
|
|
515
|
+
listModels(configuredOnly = false) {
|
|
516
|
+
return this.providers.listModels({ configuredOnly });
|
|
517
|
+
}
|
|
518
|
+
listSessions(limit) {
|
|
519
|
+
return this.store.list(limit);
|
|
520
|
+
}
|
|
521
|
+
/** Start a fresh session. */
|
|
522
|
+
createSession(overrides = {}) {
|
|
523
|
+
return this.equip(new Session({
|
|
524
|
+
cwd: this.cwd,
|
|
525
|
+
model: overrides.model ?? this.config.model ?? this.defaultModel(),
|
|
526
|
+
providers: this.providers,
|
|
527
|
+
permissionMode: this.config.permissionMode,
|
|
528
|
+
permissionRules: this.config.permissions,
|
|
529
|
+
store: this.options.ephemeral ? false : this.store,
|
|
530
|
+
title: overrides.title,
|
|
531
|
+
}));
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Give a session the machinery that outlives it: where remembered approvals
|
|
535
|
+
* are written, and where decisions are recorded.
|
|
536
|
+
*/
|
|
537
|
+
auditQueue = Promise.resolve();
|
|
538
|
+
/**
|
|
539
|
+
* The file the queue has already made a directory for.
|
|
540
|
+
*
|
|
541
|
+
* A path rather than a flag, because recording can be switched on and off
|
|
542
|
+
* while sessions are open: a boolean would remember that *some* directory
|
|
543
|
+
* exists and let the first write to a new one ENOENT into a log line.
|
|
544
|
+
*/
|
|
545
|
+
auditReadyFor;
|
|
546
|
+
equip(session) {
|
|
547
|
+
session.permissions.attachSettings(this.settings);
|
|
548
|
+
// Attached whatever the setting says, and the setting read when a decision
|
|
549
|
+
// actually happens. Deciding here instead would make the switch on the
|
|
550
|
+
// settings page a lie for every session already open — one started with
|
|
551
|
+
// recording off could never begin, and one started with it on could never
|
|
552
|
+
// stop.
|
|
553
|
+
session.permissions.onDecision((entry) => {
|
|
554
|
+
if (this.config.audit === false)
|
|
555
|
+
return;
|
|
556
|
+
this.record(session.id, entry);
|
|
557
|
+
});
|
|
558
|
+
return session;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Write one trail entry: to the session's own log, and to the configured
|
|
562
|
+
* file when there is one.
|
|
563
|
+
*
|
|
564
|
+
* Fire-and-forget so recording never delays or fails a tool call, but
|
|
565
|
+
* chained: concurrent appends of separate lines can interleave and a
|
|
566
|
+
* half-written record is worse than none. The chain also carries the
|
|
567
|
+
* one-time mkdir for the file sink — without it every write ENOENTs into a
|
|
568
|
+
* log line and the trail is silently empty.
|
|
569
|
+
*
|
|
570
|
+
* This is the one place this harness parts company with a stricter reading
|
|
571
|
+
* of the same idea, where a decision that cannot be recorded is a decision
|
|
572
|
+
* that does not happen. That trade buys an audit you can rely on at the cost
|
|
573
|
+
* of a tool call that fails because a disk did; here the tool call wins, and
|
|
574
|
+
* a failed write is a warning in the log rather than a refusal. Anything
|
|
575
|
+
* that needs the stronger guarantee needs this to be awaited on the call
|
|
576
|
+
* path, not just louder.
|
|
577
|
+
*/
|
|
578
|
+
record(sessionId, entry) {
|
|
579
|
+
const configured = this.config.auditLog;
|
|
580
|
+
this.auditQueue = this.auditQueue
|
|
581
|
+
.then(async () => {
|
|
582
|
+
// An ephemeral harness has no store to write to, and inventing one
|
|
583
|
+
// for the trail would give a session that deliberately leaves nothing
|
|
584
|
+
// behind a file that outlives it.
|
|
585
|
+
if (!this.ephemeral)
|
|
586
|
+
await this.store.appendDecision(sessionId, entry);
|
|
587
|
+
if (!configured)
|
|
588
|
+
return;
|
|
589
|
+
const file = path.resolve(this.cwd, configured);
|
|
590
|
+
if (this.auditReadyFor !== file) {
|
|
591
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
592
|
+
this.auditReadyFor = file;
|
|
593
|
+
}
|
|
594
|
+
await appendFile(file, `${JSON.stringify({ ...entry, session: sessionId })}\n`, "utf8");
|
|
595
|
+
})
|
|
596
|
+
.catch((err) => this.logger.warn("audit write failed", { error: String(err) }));
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Branch a session. Omit `keepMessages` to branch from where it stands, or
|
|
600
|
+
* pass a count to rewind the branch to an earlier point.
|
|
601
|
+
*/
|
|
602
|
+
async forkSession(id, opts = {}) {
|
|
603
|
+
const forked = await Session.fork(id, {
|
|
604
|
+
model: opts.model ?? this.config.model ?? this.defaultModel(),
|
|
605
|
+
providers: this.providers,
|
|
606
|
+
permissionMode: this.config.permissionMode,
|
|
607
|
+
permissionRules: this.config.permissions,
|
|
608
|
+
store: this.store,
|
|
609
|
+
...(opts.keepMessages !== undefined ? { keepMessages: opts.keepMessages } : {}),
|
|
610
|
+
...(opts.title !== undefined ? { title: opts.title } : {}),
|
|
611
|
+
});
|
|
612
|
+
return forked ? this.equip(forked) : undefined;
|
|
613
|
+
}
|
|
614
|
+
deleteSession(id) {
|
|
615
|
+
return this.store.delete(id);
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Delete every session recorded in one tree.
|
|
619
|
+
*
|
|
620
|
+
* The directory itself is never touched — this throws away what the harness
|
|
621
|
+
* wrote about the work, not the work. Returns the ids, because whoever asked
|
|
622
|
+
* is probably holding some of them open.
|
|
623
|
+
*/
|
|
624
|
+
deleteSessionsIn(cwd) {
|
|
625
|
+
return this.store.deleteIn(cwd);
|
|
626
|
+
}
|
|
627
|
+
renameSession(id, title) {
|
|
628
|
+
return this.store.rename(id, title);
|
|
629
|
+
}
|
|
630
|
+
/** Reload a persisted session by id; returns undefined if it is unknown. */
|
|
631
|
+
async resumeSession(id, model) {
|
|
632
|
+
const resumed = await Session.resume(id, {
|
|
633
|
+
model: model ?? this.config.model ?? this.defaultModel(),
|
|
634
|
+
providers: this.providers,
|
|
635
|
+
permissionMode: this.config.permissionMode,
|
|
636
|
+
permissionRules: this.config.permissions,
|
|
637
|
+
store: this.store,
|
|
638
|
+
});
|
|
639
|
+
return resumed ? this.equip(resumed) : undefined;
|
|
640
|
+
}
|
|
641
|
+
/** Build an agent bound to a session, wired with everything the harness owns. */
|
|
642
|
+
agent(session, overrides = {}) {
|
|
643
|
+
return new Agent({
|
|
644
|
+
session,
|
|
645
|
+
tools: this.tools,
|
|
646
|
+
hooks: this.hooks,
|
|
647
|
+
subagents: this.subagents,
|
|
648
|
+
skills: this.skills,
|
|
649
|
+
memory: this.memory,
|
|
650
|
+
onSkillsChanged: () => this.reloadSkills(),
|
|
651
|
+
mcp: this.mcp,
|
|
652
|
+
permissionPrompt: this.options.permissionPrompt,
|
|
653
|
+
workspace: this.workspace,
|
|
654
|
+
systemPrompt: this.config.systemPrompt,
|
|
655
|
+
smallModel: this.config.smallModel,
|
|
656
|
+
appendSystemPrompt: this.config.appendSystemPrompt,
|
|
657
|
+
projectInstructions: this.projectInstructions,
|
|
658
|
+
toolFilter: this.config.tools,
|
|
659
|
+
thinking: this.config.thinking,
|
|
660
|
+
maxTurns: this.config.maxTurns,
|
|
661
|
+
cache: this.config.cache,
|
|
662
|
+
logger: this.logger,
|
|
663
|
+
...overrides,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* One-shot convenience: run a prompt in a new session and stream events.
|
|
668
|
+
* For multi-turn use, keep a `Session` and reuse the agent.
|
|
669
|
+
*/
|
|
670
|
+
async *run(prompt, opts = {}) {
|
|
671
|
+
const session = this.createSession({ model: opts.model });
|
|
672
|
+
const agent = this.agent(session);
|
|
673
|
+
yield* agent.run(prompt, { signal: opts.signal });
|
|
674
|
+
}
|
|
675
|
+
async close() {
|
|
676
|
+
// Stopped before MCP, because it is the only thing here holding a socket
|
|
677
|
+
// open on purpose — a long poll would otherwise keep the process alive for
|
|
678
|
+
// up to thirty seconds after everything else had shut down.
|
|
679
|
+
this.mirror?.stop();
|
|
680
|
+
await this.mcp.closeAll();
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* First model whose provider has credentials. Only the cheap synchronous
|
|
684
|
+
* check runs here — a subscription-only provider is picked up once the
|
|
685
|
+
* caller has awaited `configuredProviders()`.
|
|
686
|
+
*/
|
|
687
|
+
defaultModel() {
|
|
688
|
+
const configured = this.providers.listModels({ configuredOnly: true });
|
|
689
|
+
const first = configured[0] ?? this.providers.listModels()[0];
|
|
690
|
+
if (!first)
|
|
691
|
+
throw new Error("no models are registered");
|
|
692
|
+
return first.id;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
//# sourceMappingURL=harness.js.map
|