@oharness/engine 0.11.0 → 0.12.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.
@@ -0,0 +1,198 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { agentsHome, claudeHome, claudeStateFile, codexHome } from "./paths.js";
4
+ import { claudeServer } from "./plugins.js";
5
+ import { parseJson } from "./util/json.js";
6
+ import { parseToml } from "./util/toml.js";
7
+ /**
8
+ * Skill directories, split by level so each can sit beneath this harness's
9
+ * own directory at the same level: a skill in `~/.oharness/skills` overrides
10
+ * one of the same name in `~/.claude/skills`, and a project's own beats both.
11
+ */
12
+ export function foreignSkillRoots(cwd, config = {}) {
13
+ const user = [];
14
+ const project = [];
15
+ if (config.codex !== false) {
16
+ user.push({ dir: path.join(agentsHome(), "skills"), source: "codex" });
17
+ user.push({ dir: path.join(codexHome(), "skills"), source: "codex" });
18
+ project.push({ dir: path.join(cwd, ".agents", "skills"), source: "codex" });
19
+ }
20
+ if (config.claudeCode !== false) {
21
+ user.push({ dir: path.join(claudeHome(), "skills"), source: "claude" });
22
+ project.push({ dir: path.join(cwd, ".claude", "skills"), source: "claude" });
23
+ }
24
+ return { user, project };
25
+ }
26
+ /** Agent directories, on the same terms. Codex has no agent files to read. */
27
+ export function foreignAgentRoots(cwd, config = {}) {
28
+ if (config.claudeCode === false)
29
+ return { user: [], project: [] };
30
+ return {
31
+ user: [{ dir: path.join(claudeHome(), "agents"), source: "claude" }],
32
+ project: [{ dir: path.join(cwd, ".claude", "agents"), source: "claude" }],
33
+ };
34
+ }
35
+ /**
36
+ * Every server Claude Code and Codex declare for this project, in that order,
37
+ * then those inside their plugins. A plugin from either is loaded for its
38
+ * skills and agents, but its servers are never started on their own — see
39
+ * `pluginContributions` — so they are offered here like any other.
40
+ */
41
+ export async function foreignMcpServers(cwd, config = {}, plugins = []) {
42
+ return [
43
+ ...(config.claudeCode === false ? [] : await claudeMcpServers(cwd)),
44
+ ...(config.codex === false ? [] : await codexMcpServers()),
45
+ ...pluginServers(plugins.filter((plugin) => plugin.source === "claude" ? config.claudeCode !== false : plugin.source === "codex" && config.codex !== false)),
46
+ ];
47
+ }
48
+ function pluginServers(plugins) {
49
+ return plugins.flatMap((plugin) => Object.entries(plugin.mcpServers ?? {}).map(([name, config]) => ({
50
+ name,
51
+ source: plugin.source,
52
+ scope: "plugin",
53
+ plugin: plugin.name,
54
+ ...(plugin.title ? { pluginTitle: plugin.title } : {}),
55
+ file: plugin.directory,
56
+ config,
57
+ ...secretFlag(config),
58
+ })));
59
+ }
60
+ /** Find one by the key the connector list sends back. */
61
+ export async function findForeignServer(cwd, key, config = {}, plugins = []) {
62
+ return (await foreignMcpServers(cwd, config, plugins)).find((one) => one.source === key.source && one.scope === key.scope && one.name === key.name &&
63
+ (one.plugin ?? "") === (key.plugin ?? ""));
64
+ }
65
+ async function claudeMcpServers(cwd) {
66
+ const out = [];
67
+ const state = claudeStateFile();
68
+ const raw = await readJson(state);
69
+ if (isRecord(raw)) {
70
+ out.push(...fromClaude(raw.mcpServers, "user", state));
71
+ const projects = isRecord(raw.projects) ? raw.projects : {};
72
+ const here = projects[path.resolve(cwd)];
73
+ if (isRecord(here))
74
+ out.push(...fromClaude(here.mcpServers, "local", state));
75
+ }
76
+ const shared = path.join(cwd, ".mcp.json");
77
+ const checkedIn = await readJson(shared);
78
+ if (isRecord(checkedIn))
79
+ out.push(...fromClaude(checkedIn.mcpServers, "project", shared));
80
+ return out;
81
+ }
82
+ function fromClaude(servers, scope, file) {
83
+ if (!isRecord(servers))
84
+ return [];
85
+ return Object.entries(servers).map(([name, entry]) => {
86
+ const translated = claudeServer(entry);
87
+ return "config" in translated
88
+ ? { name, source: "claude", scope, file, config: translated.config, ...secretFlag(translated.config) }
89
+ : { name, source: "claude", scope, file, reason: translated.reason };
90
+ });
91
+ }
92
+ async function codexMcpServers() {
93
+ const file = path.join(codexHome(), "config.toml");
94
+ let table;
95
+ try {
96
+ table = parseToml(await readFile(file, "utf8"));
97
+ }
98
+ catch {
99
+ // Missing is the common case; a file Codex itself can read but this
100
+ // cannot is rarer, and either way there is nothing here to offer.
101
+ return [];
102
+ }
103
+ const servers = table["mcp_servers"];
104
+ if (!isTable(servers))
105
+ return [];
106
+ const out = [];
107
+ for (const [name, entry] of Object.entries(servers)) {
108
+ if (!isTable(entry))
109
+ continue;
110
+ // Switched off in Codex is a decision this list should not undo.
111
+ if (entry["enabled"] === false)
112
+ continue;
113
+ const config = codexServer(entry);
114
+ out.push(config
115
+ ? { name, source: "codex", scope: "user", file, config, ...secretFlag(config) }
116
+ : { name, source: "codex", scope: "user", file, reason: "malformed" });
117
+ }
118
+ return out;
119
+ }
120
+ /**
121
+ * One `[mcp_servers.<name>]` table in this harness's shape.
122
+ *
123
+ * Codex names environment variables rather than writing values in a few
124
+ * places — `bearer_token_env_var`, `env_http_headers` — and those become
125
+ * `${env:NAME}` references, so the token stays in the environment here too.
126
+ */
127
+ function codexServer(entry) {
128
+ const timeout = typeof entry["tool_timeout_sec"] === "number" ? { timeoutMs: Math.round(entry["tool_timeout_sec"] * 1000) } : {};
129
+ const allowed = strings(entry["enabled_tools"]);
130
+ const tools = allowed ? { allowedTools: allowed } : {};
131
+ if (typeof entry["command"] === "string") {
132
+ const args = strings(entry["args"]);
133
+ const env = stringTable(entry["env"]);
134
+ return {
135
+ type: "stdio",
136
+ command: entry["command"],
137
+ ...(args ? { args } : {}),
138
+ ...(env ? { env } : {}),
139
+ ...(typeof entry["cwd"] === "string" ? { cwd: entry["cwd"] } : {}),
140
+ ...tools,
141
+ ...timeout,
142
+ };
143
+ }
144
+ if (typeof entry["url"] === "string") {
145
+ const headers = { ...stringTable(entry["http_headers"]) };
146
+ for (const [header, variable] of Object.entries(stringTable(entry["env_http_headers"]) ?? {})) {
147
+ headers[header] = `\${env:${variable}}`;
148
+ }
149
+ if (typeof entry["bearer_token_env_var"] === "string") {
150
+ headers["Authorization"] = `Bearer \${env:${entry["bearer_token_env_var"]}}`;
151
+ }
152
+ return {
153
+ type: "http",
154
+ url: entry["url"],
155
+ ...(Object.keys(headers).length ? { headers } : {}),
156
+ ...tools,
157
+ ...timeout,
158
+ };
159
+ }
160
+ return undefined;
161
+ }
162
+ /**
163
+ * Whether a credential is written out rather than referenced: any header, or
164
+ * an environment variable whose name says it is one. Only those: they are
165
+ * where credentials go, and `NODE_ENV=production` is not worth a warning.
166
+ */
167
+ function secretFlag(config) {
168
+ const entries = Object.entries((config.type === "http" ? config.headers : config.env) ?? {});
169
+ const credential = (name) => config.type === "http" || /key|token|secret|pass|auth|credential/i.test(name);
170
+ const literal = (value) => value !== "" && !/^(Bearer )?\$\{(env|auth):[^}]+\}$/.test(value);
171
+ return entries.some(([name, value]) => credential(name) && literal(value)) ? { inlineSecret: true } : {};
172
+ }
173
+ // ---------------------------------------------------------------------------
174
+ async function readJson(file) {
175
+ try {
176
+ const parsed = parseJson(await readFile(file, "utf8"));
177
+ return parsed.ok ? parsed.value : undefined;
178
+ }
179
+ catch {
180
+ return undefined;
181
+ }
182
+ }
183
+ function strings(value) {
184
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : undefined;
185
+ }
186
+ function stringTable(value) {
187
+ if (!isTable(value))
188
+ return undefined;
189
+ const out = Object.fromEntries(Object.entries(value).filter(([, v]) => typeof v === "string"));
190
+ return Object.keys(out).length ? out : undefined;
191
+ }
192
+ function isTable(value) {
193
+ return !!value && typeof value === "object" && !Array.isArray(value);
194
+ }
195
+ function isRecord(value) {
196
+ return !!value && typeof value === "object" && !Array.isArray(value);
197
+ }
198
+ //# sourceMappingURL=foreign.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"foreign.js","sourceRoot":"","sources":["../src/foreign.ts"],"names":[],"mappings":"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"}
package/dist/harness.d.ts CHANGED
@@ -441,6 +441,33 @@ export declare class Harness {
441
441
  * which breaks the one loop that would let the agent improve itself.
442
442
  */
443
443
  reloadSkills(): Promise<number>;
444
+ /**
445
+ * Switch one skill off or back on, now and for later launches.
446
+ *
447
+ * Off rather than deleted: most skills in the list are files another tool
448
+ * put there — Claude Code's, Codex's, a plugin's — and removing someone
449
+ * else's file to hide it from this harness would take it from that tool
450
+ * too.
451
+ */
452
+ setSkillEnabled(name: string, on: boolean): Promise<void>;
453
+ /**
454
+ * Every markdown agent this harness reads, with the dialect each was
455
+ * written in: a Claude Code plugin's names tools and models the way
456
+ * Claude Code does, and is translated on the way in.
457
+ */
458
+ private markdownAgents;
459
+ /**
460
+ * Take in a plugin that arrived while the harness was running.
461
+ *
462
+ * What `create` does for every plugin at launch, for one: its skills and
463
+ * agents join the registries, its servers connect. Without this an install
464
+ * from the menu would sit inert until a restart, which reads as a broken
465
+ * install. A server name the config already has is left to the config, the
466
+ * same precedence a launch gives it.
467
+ */
468
+ adoptPlugin(directory: string): Promise<PluginDefinition | undefined>;
469
+ /** Undo `adoptPlugin` for a plugin whose directory is gone. */
470
+ dropPlugin(directory: string): Promise<void>;
444
471
  /** Providers declared by the catalog, for `/models` and login hints. */
445
472
  listCatalogProviders(): CatalogProvider[];
446
473
  listModels(configuredOnly?: boolean): ModelSpec[];
package/dist/harness.js CHANGED
@@ -6,6 +6,7 @@ import { HookRegistry } from "./agent/hooks.js";
6
6
  import { Session } from "./agent/session.js";
7
7
  import { SessionStore } from "./agent/store.js";
8
8
  import { BUILTIN_SUBAGENTS, SubagentRegistry, loadMarkdownSubagents } from "./agent/subagent.js";
9
+ import { foreignAgentRoots, foreignSkillRoots } from "./foreign.js";
9
10
  import { claudeAgent } from "./acp/claude.js";
10
11
  import { SkillRegistry } from "./agent/skills.js";
11
12
  import { SettingsStore } from "./agent/settings.js";
@@ -205,7 +206,14 @@ export class Harness {
205
206
  ...(config.skillDirs ?? []),
206
207
  ...(config.agentDirs ?? []),
207
208
  ],
208
- ...(config.readableRoots !== undefined ? { readableRoots: config.readableRoots } : {}),
209
+ // Claude Code's and Codex's skill and agent directories are read, so
210
+ // the agent can open a skill's scripts/, but not written: those belong
211
+ // to the user's other tools, not to anything this harness installed.
212
+ readableRoots: [
213
+ ...(config.readableRoots ?? []),
214
+ ...Object.values(foreignSkillRoots(this.cwd, config.imports)).flat().map((root) => root.dir),
215
+ ...Object.values(foreignAgentRoots(this.cwd, config.imports)).flat().map((root) => root.dir),
216
+ ],
209
217
  });
210
218
  this.mcp = new McpManager(this.logger, this.auth.store, (id) => this.auth.secret(id));
211
219
  this.store = new SessionStore(config.sessionDir);
@@ -241,7 +249,7 @@ export class Harness {
241
249
  : (await ensureProjectManifest(cwd)).id;
242
250
  opts = { ...opts, cwd };
243
251
  const baseConfig = opts.config ?? (await loadConfig(cwd));
244
- const plugins = await discoverPlugins(cwd, baseConfig.plugins);
252
+ const plugins = await discoverPlugins(cwd, pluginOptions(baseConfig));
245
253
  // Explicit user/project config is applied after plugins, so it can always
246
254
  // tighten policy or replace a plugin's MCP server declaration.
247
255
  const config = mergeConfig(pluginContributions(plugins), baseConfig);
@@ -269,7 +277,8 @@ export class Harness {
269
277
  })
270
278
  .catch((err) => harness.logger.warn("session pruning failed", { error: String(err) }));
271
279
  }
272
- await harness.skills.discover(cwd, config.skillDirs ?? []);
280
+ harness.skills.setDisabled(config.disabledSkills ?? []);
281
+ await harness.skills.discover(cwd, config.skillDirs ?? [], config.imports ?? {});
273
282
  // Claude Agent, when the user turned it on and this machine has their
274
283
  // own Claude Code to hand work to. Before the markdown ones, so a
275
284
  // project can redefine it.
@@ -277,7 +286,7 @@ export class Harness {
277
286
  await harness.registerClaudeAgent();
278
287
  // Markdown definitions layer over the JSON ones, so a repo can drop in an
279
288
  // agent file without editing config.
280
- for (const definition of await loadMarkdownSubagents(cwd, config.agentDirs ?? [])) {
289
+ for (const definition of await harness.markdownAgents()) {
281
290
  harness.subagents.register(definition);
282
291
  }
283
292
  if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
@@ -939,9 +948,109 @@ export class Harness {
939
948
  * which breaks the one loop that would let the agent improve itself.
940
949
  */
941
950
  async reloadSkills() {
942
- await this.skills.discover(this.cwd, this.config.skillDirs ?? []);
951
+ await this.skills.discover(this.cwd, this.config.skillDirs ?? [], this.config.imports ?? {});
943
952
  return this.skills.size;
944
953
  }
954
+ /**
955
+ * Switch one skill off or back on, now and for later launches.
956
+ *
957
+ * Off rather than deleted: most skills in the list are files another tool
958
+ * put there — Claude Code's, Codex's, a plugin's — and removing someone
959
+ * else's file to hide it from this harness would take it from that tool
960
+ * too.
961
+ */
962
+ async setSkillEnabled(name, on) {
963
+ await updateUserConfig((current) => {
964
+ const off = new Set(current.disabledSkills ?? []);
965
+ if (on)
966
+ off.delete(name);
967
+ else
968
+ off.add(name);
969
+ return { ...current, disabledSkills: [...off].sort() };
970
+ });
971
+ const off = new Set(this.config.disabledSkills ?? []);
972
+ if (on)
973
+ off.delete(name);
974
+ else
975
+ off.add(name);
976
+ this.config.disabledSkills = [...off];
977
+ this.skills.setDisabled(off);
978
+ }
979
+ /**
980
+ * Every markdown agent this harness reads, with the dialect each was
981
+ * written in: a Claude Code plugin's names tools and models the way
982
+ * Claude Code does, and is translated on the way in.
983
+ */
984
+ async markdownAgents() {
985
+ const claude = new Set(this.plugins.filter((plugin) => plugin.claudeFormat).map((plugin) => plugin.agentDir));
986
+ const roots = (this.config.agentDirs ?? []).map((dir) => claude.has(dir) ? { dir, dialect: "claude" } : { dir });
987
+ return loadMarkdownSubagents(this.cwd, roots, this.config.imports ?? {});
988
+ }
989
+ /**
990
+ * Take in a plugin that arrived while the harness was running.
991
+ *
992
+ * What `create` does for every plugin at launch, for one: its skills and
993
+ * agents join the registries, its servers connect. Without this an install
994
+ * from the menu would sit inert until a restart, which reads as a broken
995
+ * install. A server name the config already has is left to the config, the
996
+ * same precedence a launch gives it.
997
+ */
998
+ async adoptPlugin(directory) {
999
+ const plugin = (await discoverPlugins(this.cwd, pluginOptions(this.config))).find((one) => one.directory === directory);
1000
+ if (!plugin)
1001
+ return undefined;
1002
+ const known = this.plugins.findIndex((one) => one.name === plugin.name);
1003
+ if (known >= 0)
1004
+ this.plugins.splice(known, 1, plugin);
1005
+ else
1006
+ this.plugins.push(plugin);
1007
+ const contributed = pluginContributions([plugin]);
1008
+ this.config.skillDirs = [...new Set([...(this.config.skillDirs ?? []), ...contributed.skillDirs])];
1009
+ this.config.agentDirs = [...new Set([...(this.config.agentDirs ?? []), ...contributed.agentDirs])];
1010
+ await this.reloadSkills();
1011
+ for (const definition of contributed.subagents)
1012
+ this.subagents.register(definition);
1013
+ for (const definition of await this.markdownAgents()) {
1014
+ this.subagents.register(definition);
1015
+ }
1016
+ for (const [name, server] of Object.entries(contributed.mcpServers)) {
1017
+ if (this.config.mcpServers?.[name] || this.mcp.configFor(name))
1018
+ continue;
1019
+ this.config.mcpServers = { ...(this.config.mcpServers ?? {}), [name]: server };
1020
+ // One server that will not start is not a failed install; its row says
1021
+ // why, as any connector's does.
1022
+ await this.addMcpServer(name, server).catch((err) => this.logger.warn("plugin server unavailable", { server: name, error: String(err) }));
1023
+ }
1024
+ return plugin;
1025
+ }
1026
+ /** Undo `adoptPlugin` for a plugin whose directory is gone. */
1027
+ async dropPlugin(directory) {
1028
+ const index = this.plugins.findIndex((one) => one.directory === directory);
1029
+ if (index < 0)
1030
+ return;
1031
+ const [plugin] = this.plugins.splice(index, 1);
1032
+ const contributed = pluginContributions([plugin]);
1033
+ const before = await this.markdownAgents();
1034
+ this.config.skillDirs = (this.config.skillDirs ?? []).filter((dir) => !contributed.skillDirs.includes(dir));
1035
+ this.config.agentDirs = (this.config.agentDirs ?? []).filter((dir) => !contributed.agentDirs.includes(dir));
1036
+ await this.reloadSkills();
1037
+ const after = new Set((await this.markdownAgents()).map((one) => one.name));
1038
+ for (const definition of [...before, ...contributed.subagents]) {
1039
+ if (after.has(definition.name))
1040
+ continue;
1041
+ const registered = this.subagents.get(definition.name);
1042
+ if (registered)
1043
+ this.subagents.unregister(registered);
1044
+ }
1045
+ for (const name of Object.keys(contributed.mcpServers)) {
1046
+ const mine = this.config.mcpServers?.[name] === contributed.mcpServers[name];
1047
+ if (!mine)
1048
+ continue;
1049
+ const { [name]: _gone, ...rest } = this.config.mcpServers ?? {};
1050
+ this.config.mcpServers = rest;
1051
+ await this.removeMcpServer(name);
1052
+ }
1053
+ }
945
1054
  /** Providers declared by the catalog, for `/models` and login hints. */
946
1055
  listCatalogProviders() {
947
1056
  return this.providers.listCatalogProviders();
@@ -1361,4 +1470,18 @@ export class Harness {
1361
1470
  return first.id;
1362
1471
  }
1363
1472
  }
1473
+ /**
1474
+ * Plugin discovery, with `imports` deciding for the two other tools' caches
1475
+ * where `plugins` does not: switching Codex off is switching off its skills,
1476
+ * servers and plugins together.
1477
+ */
1478
+ function pluginOptions(config) {
1479
+ const claudeCode = config.plugins?.claudeCode ?? config.imports?.claudeCode;
1480
+ const codex = config.plugins?.codex ?? config.imports?.codex;
1481
+ return {
1482
+ ...config.plugins,
1483
+ ...(claudeCode !== undefined ? { claudeCode } : {}),
1484
+ ...(codex !== undefined ? { codex } : {}),
1485
+ };
1486
+ }
1364
1487
  //# sourceMappingURL=harness.js.map