@ours.network/fleet 0.18.0 → 0.19.0-nightly.1
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/README.md +57 -121
- package/dist/application/fleet-query-service.js +0 -12
- package/dist/application/role-creation-service.js +10 -7
- package/dist/application/types.d.ts +0 -11
- package/dist/briefing.js +6 -15
- package/dist/build-info.json +6 -7
- package/dist/capabilities.d.ts +1 -3
- package/dist/capabilities.js +0 -3
- package/dist/cli.js +7 -83
- package/dist/config.d.ts +3 -11
- package/dist/config.js +15 -40
- package/dist/creation.d.ts +38 -22
- package/dist/creation.js +111 -24
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +46 -120
- package/dist/doctor.d.ts +5 -1
- package/dist/doctor.js +18 -11
- package/dist/fleet-proxy.d.ts +0 -5
- package/dist/harness/acp-agent.js +6 -11
- package/dist/harness/claude-code.js +11 -204
- package/dist/harness/codex.d.ts +1 -4
- package/dist/harness/codex.js +12 -74
- package/dist/harness/types.d.ts +4 -54
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -1
- package/dist/loops/manager.d.ts +1 -30
- package/dist/loops/manager.js +6 -69
- package/dist/loops/state.d.ts +0 -18
- package/dist/loops/state.js +0 -4
- package/dist/monitor.js +1 -1
- package/dist/ops.d.ts +3 -0
- package/dist/ops.js +8 -3
- package/dist/owner-channel/attachments.d.ts +25 -2
- package/dist/owner-channel/attachments.js +61 -5
- package/dist/owner-channel/channel.d.ts +29 -30
- package/dist/owner-channel/channel.js +291 -291
- package/dist/owner-channel/message-recovery.d.ts +25 -0
- package/dist/owner-channel/message-recovery.js +114 -0
- package/dist/owner-channel/notices.d.ts +0 -7
- package/dist/owner-channel/notices.js +0 -9
- package/dist/owner-channel/ours-client.d.ts +148 -0
- package/dist/owner-channel/ours-client.js +231 -0
- package/dist/resolved-plan.js +0 -1
- package/dist/runner.d.ts +0 -48
- package/dist/runner.js +94 -236
- package/dist/session/acp.d.ts +0 -104
- package/dist/session/acp.js +10 -213
- package/dist/session/conversation-normalizer.d.ts +0 -6
- package/dist/session/conversation-normalizer.js +10 -153
- package/dist/session/conversation-types.d.ts +4 -23
- package/dist/session/types.d.ts +0 -35
- package/dist/spawn.js +26 -33
- package/dist/supervisor/systemd.js +29 -2
- package/dist/watchdog/briefing.js +0 -7
- package/dist/watchdog/run.js +3 -3
- package/dist/web-app/assets/{TerminalView-C_G1ID2P.js → TerminalView-BAVk1Bot.js} +1 -1
- package/dist/web-app/assets/{index-BCBK78hw.js → index-C3S-xFRU.js} +5 -5
- package/dist/web-app/index.html +1 -1
- package/dist/worklog.d.ts +1 -7
- package/dist/worklog.js +39 -191
- package/package.json +3 -1
- package/dist/harness-plugins.d.ts +0 -48
- package/dist/harness-plugins.js +0 -309
- package/dist/model-env.d.ts +0 -71
- package/dist/model-env.js +0 -106
- package/dist/owner-channel/mcp.d.ts +0 -24
- package/dist/owner-channel/mcp.js +0 -145
- package/dist/session/activity.d.ts +0 -31
- package/dist/session/activity.js +0 -48
package/dist/harness-plugins.js
DELETED
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { join, resolve } from 'node:path';
|
|
3
|
-
import { replaceFileAtomically, withFileLock } from './atomic-file.js';
|
|
4
|
-
import { realExec } from './exec.js';
|
|
5
|
-
import { stateRoot } from './paths.js';
|
|
6
|
-
export const HARNESS_PLUGIN_IDS = ['codex', 'claude-code'];
|
|
7
|
-
const REGISTRY = 'https://registry.npmjs.org';
|
|
8
|
-
const SPECS = {
|
|
9
|
-
codex: {
|
|
10
|
-
package: '@ours.network/codex',
|
|
11
|
-
marketplaceName: 'ours-fleet-codex-lock',
|
|
12
|
-
marketplaceManifest: '.agents/plugins/marketplace.json',
|
|
13
|
-
executable: 'codex',
|
|
14
|
-
},
|
|
15
|
-
'claude-code': {
|
|
16
|
-
package: '@ours.network/claude-code',
|
|
17
|
-
marketplaceName: 'ours-fleet-claude-lock',
|
|
18
|
-
marketplaceManifest: '.claude-plugin/marketplace.json',
|
|
19
|
-
executable: 'claude',
|
|
20
|
-
},
|
|
21
|
-
};
|
|
22
|
-
const EXACT_SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
23
|
-
const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
24
|
-
export function resolveHarnessPluginConfigs(raw, file = 'fleet.yaml') {
|
|
25
|
-
const resolved = {
|
|
26
|
-
codex: { plugin_channel: 'stable' },
|
|
27
|
-
'claude-code': { plugin_channel: 'stable' },
|
|
28
|
-
};
|
|
29
|
-
if (raw === undefined)
|
|
30
|
-
return resolved;
|
|
31
|
-
if (!isRecord(raw))
|
|
32
|
-
throw new Error(`${file}: harnesses must be a map`);
|
|
33
|
-
const unknown = Object.keys(raw).filter(key => !HARNESS_PLUGIN_IDS.includes(key));
|
|
34
|
-
if (unknown.length)
|
|
35
|
-
throw new Error(`${file}: harnesses has unknown harness(es) ${unknown.join(', ')}; allowed: ${HARNESS_PLUGIN_IDS.join(', ')}`);
|
|
36
|
-
for (const harness of HARNESS_PLUGIN_IDS) {
|
|
37
|
-
const value = raw[harness];
|
|
38
|
-
if (value === undefined)
|
|
39
|
-
continue;
|
|
40
|
-
if (!isRecord(value))
|
|
41
|
-
throw new Error(`${file}: harnesses.${harness} must be a map`);
|
|
42
|
-
const bad = Object.keys(value).filter(key => key !== 'plugin_channel');
|
|
43
|
-
if (bad.length)
|
|
44
|
-
throw new Error(`${file}: harnesses.${harness} has unknown key(s) ${bad.join(', ')}; allowed: plugin_channel`);
|
|
45
|
-
const channel = value.plugin_channel;
|
|
46
|
-
if (channel !== 'stable' && channel !== 'nightly')
|
|
47
|
-
throw new Error(`${file}: harnesses.${harness}.plugin_channel must be one of: stable, nightly`);
|
|
48
|
-
resolved[harness] = { plugin_channel: channel };
|
|
49
|
-
}
|
|
50
|
-
return resolved;
|
|
51
|
-
}
|
|
52
|
-
export const harnessPluginRoot = (harness) => join(stateRoot(), 'harness-plugins', harness);
|
|
53
|
-
export const harnessPluginLockPath = (harness) => join(harnessPluginRoot(harness), 'plugin-lock.json');
|
|
54
|
-
export const harnessPluginMarketplaceRoot = (harness) => join(harnessPluginRoot(harness), 'marketplace');
|
|
55
|
-
export const harnessPluginMarketplacePath = (harness) => join(harnessPluginMarketplaceRoot(harness), SPECS[harness].marketplaceManifest);
|
|
56
|
-
function validateLock(value, harness, path) {
|
|
57
|
-
const spec = SPECS[harness];
|
|
58
|
-
const fail = (detail) => {
|
|
59
|
-
throw new Error(`invalid harness plugin lock ${path}: ${detail}; run \`ours-fleet plugins update ${harness}\``);
|
|
60
|
-
};
|
|
61
|
-
if (!isRecord(value))
|
|
62
|
-
return fail('expected a JSON object');
|
|
63
|
-
if (value.schemaVersion !== 1)
|
|
64
|
-
fail('unsupported schemaVersion');
|
|
65
|
-
if (value.harness !== harness)
|
|
66
|
-
fail(`harness must be '${harness}'`);
|
|
67
|
-
if (value.channel !== 'stable' && value.channel !== 'nightly')
|
|
68
|
-
fail('channel must be stable or nightly');
|
|
69
|
-
const expectedTag = value.channel === 'stable' ? 'latest' : 'nightly';
|
|
70
|
-
if (value.distTag !== expectedTag)
|
|
71
|
-
fail(`distTag must be '${expectedTag}' for channel '${value.channel}'`);
|
|
72
|
-
if (value.package !== spec.package)
|
|
73
|
-
fail(`package must be '${spec.package}'`);
|
|
74
|
-
if (typeof value.version !== 'string' || !EXACT_SEMVER.test(value.version))
|
|
75
|
-
return fail('version must be an exact semver');
|
|
76
|
-
const version = value.version;
|
|
77
|
-
if (value.channel === 'stable' && version.includes('-'))
|
|
78
|
-
fail('stable channel resolved to a prerelease');
|
|
79
|
-
if (value.channel === 'nightly' && !version.includes('-nightly.'))
|
|
80
|
-
fail('nightly channel did not resolve to a -nightly.N prerelease');
|
|
81
|
-
if (value.registry !== REGISTRY)
|
|
82
|
-
fail(`registry must be '${REGISTRY}'`);
|
|
83
|
-
if (typeof value.resolvedAt !== 'string' || !Number.isFinite(Date.parse(value.resolvedAt)))
|
|
84
|
-
fail('resolvedAt must be an ISO timestamp');
|
|
85
|
-
return value;
|
|
86
|
-
}
|
|
87
|
-
export function readHarnessPluginLock(harness) {
|
|
88
|
-
const path = harnessPluginLockPath(harness);
|
|
89
|
-
if (!existsSync(path))
|
|
90
|
-
return undefined;
|
|
91
|
-
let parsed;
|
|
92
|
-
try {
|
|
93
|
-
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
throw new Error(`invalid harness plugin lock ${path}: ${error.message}; `
|
|
97
|
-
+ `run \`ours-fleet plugins update ${harness}\``);
|
|
98
|
-
}
|
|
99
|
-
return validateLock(parsed, harness, path);
|
|
100
|
-
}
|
|
101
|
-
function marketplaceDocument(lock) {
|
|
102
|
-
const spec = SPECS[lock.harness];
|
|
103
|
-
const source = lock.harness === 'codex'
|
|
104
|
-
? { source: 'npm', package: lock.package, version: lock.version, registry: lock.registry }
|
|
105
|
-
: { source: 'npm', package: lock.package, version: lock.version };
|
|
106
|
-
if (lock.harness === 'codex') {
|
|
107
|
-
return {
|
|
108
|
-
name: spec.marketplaceName,
|
|
109
|
-
interface: { displayName: `ours.network (${lock.channel}, locked ${lock.version})` },
|
|
110
|
-
plugins: [{
|
|
111
|
-
name: 'ours', source,
|
|
112
|
-
policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' },
|
|
113
|
-
category: 'Productivity',
|
|
114
|
-
}],
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
return {
|
|
118
|
-
$schema: 'https://json.schemastore.org/claude-code-marketplace.json',
|
|
119
|
-
name: spec.marketplaceName,
|
|
120
|
-
owner: { name: 'Adapt Framework Solutions Ltd', url: 'https://ours.network' },
|
|
121
|
-
description: `Generated by ours-fleet; @ours.network/claude-code is locked to ${lock.version}.`,
|
|
122
|
-
plugins: [{
|
|
123
|
-
name: 'ours', source,
|
|
124
|
-
description: 'Secure ours.network messaging for Claude Code.',
|
|
125
|
-
}],
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
function writeMarketplace(lock) {
|
|
129
|
-
const path = harnessPluginMarketplacePath(lock.harness);
|
|
130
|
-
replaceFileAtomically(path, `${JSON.stringify(marketplaceDocument(lock), null, 2)}\n`, 0o644);
|
|
131
|
-
return path;
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Re-materialize the generated marketplace from the persisted exact lock.
|
|
135
|
-
* This is the ONLY ordinary startup/reconciliation path: it performs no exec,
|
|
136
|
-
* no network call, and cannot observe a moving npm dist-tag.
|
|
137
|
-
*/
|
|
138
|
-
export function restoreLockedHarnessMarketplace(harness, expectedChannel) {
|
|
139
|
-
const lock = readHarnessPluginLock(harness);
|
|
140
|
-
if (!lock) {
|
|
141
|
-
if (expectedChannel === 'nightly')
|
|
142
|
-
throw new Error(`harness '${harness}' requests nightly but has no exact lock; `
|
|
143
|
-
+ `run \`ours-fleet plugins install ${harness}\``);
|
|
144
|
-
return undefined;
|
|
145
|
-
}
|
|
146
|
-
if (expectedChannel && lock.channel !== expectedChannel)
|
|
147
|
-
throw new Error(`harness '${harness}' requests ${expectedChannel} but its exact lock is ${lock.channel} `
|
|
148
|
-
+ `(${lock.version}); run \`ours-fleet plugins update ${harness}\``);
|
|
149
|
-
if (lock)
|
|
150
|
-
writeMarketplace(lock);
|
|
151
|
-
return lock;
|
|
152
|
-
}
|
|
153
|
-
async function checked(exec, command, args, action) {
|
|
154
|
-
const result = await exec(command, args);
|
|
155
|
-
if (result.code !== 0)
|
|
156
|
-
throw new Error(`${action} failed (${command} ${args.join(' ')}): ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`);
|
|
157
|
-
return result;
|
|
158
|
-
}
|
|
159
|
-
function parseJson(output, action) {
|
|
160
|
-
try {
|
|
161
|
-
return JSON.parse(output);
|
|
162
|
-
}
|
|
163
|
-
catch (error) {
|
|
164
|
-
throw new Error(`${action} returned invalid JSON: ${error.message}`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
async function resolveExactVersion(harness, channel, exec, now) {
|
|
168
|
-
const spec = SPECS[harness];
|
|
169
|
-
const distTag = channel === 'stable' ? 'latest' : 'nightly';
|
|
170
|
-
const result = await checked(exec, 'npm', ['view', `${spec.package}@${distTag}`, 'version', '--json'], `resolve ${spec.package} ${distTag}`);
|
|
171
|
-
const version = parseJson(result.stdout, `npm view ${spec.package}@${distTag}`);
|
|
172
|
-
if (typeof version !== 'string' || !EXACT_SEMVER.test(version))
|
|
173
|
-
throw new Error(`npm view ${spec.package}@${distTag} did not return one exact semver`);
|
|
174
|
-
if (channel === 'stable' && version.includes('-'))
|
|
175
|
-
throw new Error(`refusing stable ${spec.package}: npm latest resolved to prerelease ${version}`);
|
|
176
|
-
if (channel === 'nightly' && !version.includes('-nightly.'))
|
|
177
|
-
throw new Error(`refusing nightly ${spec.package}: npm nightly resolved to ${version}`);
|
|
178
|
-
return {
|
|
179
|
-
schemaVersion: 1,
|
|
180
|
-
harness,
|
|
181
|
-
channel,
|
|
182
|
-
distTag,
|
|
183
|
-
package: spec.package,
|
|
184
|
-
version,
|
|
185
|
-
registry: REGISTRY,
|
|
186
|
-
resolvedAt: now().toISOString(),
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
function marketplaceEntries(harness, output) {
|
|
190
|
-
const parsed = parseJson(output, `${SPECS[harness].executable} plugin marketplace list`);
|
|
191
|
-
if (harness === 'codex') {
|
|
192
|
-
if (!isRecord(parsed) || !Array.isArray(parsed.marketplaces))
|
|
193
|
-
throw new Error('codex plugin marketplace list returned an unexpected shape');
|
|
194
|
-
return parsed.marketplaces.filter(isRecord);
|
|
195
|
-
}
|
|
196
|
-
if (!Array.isArray(parsed))
|
|
197
|
-
throw new Error('claude plugin marketplace list returned an unexpected shape');
|
|
198
|
-
return parsed.filter(isRecord);
|
|
199
|
-
}
|
|
200
|
-
async function ensureMarketplaceRegistered(harness, exec) {
|
|
201
|
-
const spec = SPECS[harness];
|
|
202
|
-
const root = harnessPluginMarketplaceRoot(harness);
|
|
203
|
-
const result = await checked(exec, spec.executable, ['plugin', 'marketplace', 'list', '--json'], `list ${harness} marketplaces`);
|
|
204
|
-
const existing = marketplaceEntries(harness, result.stdout)
|
|
205
|
-
.find(entry => entry.name === spec.marketplaceName);
|
|
206
|
-
if (existing) {
|
|
207
|
-
if (harness === 'codex') {
|
|
208
|
-
const declared = isRecord(existing.marketplaceSource)
|
|
209
|
-
? existing.marketplaceSource.source : existing.root;
|
|
210
|
-
if (typeof declared !== 'string' || resolve(declared) !== resolve(root))
|
|
211
|
-
throw new Error(`marketplace '${spec.marketplaceName}' already exists but does not point to ${root}; `
|
|
212
|
-
+ 'refusing to replace an unrelated marketplace');
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
// Claude copies marketplace contents into its cache, but retains the
|
|
216
|
-
// source kind in list output. Refresh that copy after each lock update.
|
|
217
|
-
if (existing.source !== 'directory')
|
|
218
|
-
throw new Error(`marketplace '${spec.marketplaceName}' already exists but is not a local directory; `
|
|
219
|
-
+ 'refusing to replace an unrelated marketplace');
|
|
220
|
-
await checked(exec, 'claude', ['plugin', 'marketplace', 'update', spec.marketplaceName], `refresh ${harness} locked marketplace`);
|
|
221
|
-
}
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
const args = harness === 'codex'
|
|
225
|
-
? ['plugin', 'marketplace', 'add', root, '--json']
|
|
226
|
-
: ['plugin', 'marketplace', 'add', root, '--scope', 'user'];
|
|
227
|
-
await checked(exec, spec.executable, args, `register ${harness} locked marketplace`);
|
|
228
|
-
}
|
|
229
|
-
async function removeLegacyPluginSelections(harness, exec) {
|
|
230
|
-
const spec = SPECS[harness];
|
|
231
|
-
const result = await checked(exec, spec.executable, ['plugin', 'list', '--json'], `list ${harness} plugins`);
|
|
232
|
-
const parsed = parseJson(result.stdout, `${spec.executable} plugin list`);
|
|
233
|
-
if (harness === 'codex' && (!isRecord(parsed) || !Array.isArray(parsed.installed)))
|
|
234
|
-
throw new Error('codex plugin list returned an unexpected shape');
|
|
235
|
-
if (harness === 'claude-code' && !Array.isArray(parsed))
|
|
236
|
-
throw new Error('claude plugin list returned an unexpected shape');
|
|
237
|
-
const entries = (harness === 'codex'
|
|
238
|
-
? parsed.installed
|
|
239
|
-
: parsed).filter(isRecord);
|
|
240
|
-
const ids = new Set(entries
|
|
241
|
-
.map(entry => harness === 'codex' ? entry.pluginId : entry.id)
|
|
242
|
-
.filter((id) => typeof id === 'string'));
|
|
243
|
-
const legacy = harness === 'codex'
|
|
244
|
-
? ['ours@ours-codex-marketplace', 'ours-fleet@ours-codex-marketplace']
|
|
245
|
-
: ['ours@ours', 'ours@ours.network'];
|
|
246
|
-
for (const selector of legacy.filter(id => ids.has(id))) {
|
|
247
|
-
const args = harness === 'codex'
|
|
248
|
-
? ['plugin', 'remove', selector, '--json']
|
|
249
|
-
: ['plugin', 'uninstall', selector, '--scope', 'user', '--keep-data'];
|
|
250
|
-
await checked(exec, spec.executable, args, `remove legacy moving selection ${selector}`);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
async function installFromLock(lock, exec) {
|
|
254
|
-
const spec = SPECS[lock.harness];
|
|
255
|
-
// The Codex package also owns the ours-codex launcher. Pin that executable to
|
|
256
|
-
// the same exact artifact as the local marketplace; never invoke its broad
|
|
257
|
-
// installer, which may select daemon/SDK packages outside this feature.
|
|
258
|
-
if (lock.harness === 'codex') {
|
|
259
|
-
await checked(exec, 'npm', ['install', '--global', `${lock.package}@${lock.version}`], `install ${lock.package}@${lock.version}`);
|
|
260
|
-
}
|
|
261
|
-
await ensureMarketplaceRegistered(lock.harness, exec);
|
|
262
|
-
const selector = `ours@${spec.marketplaceName}`;
|
|
263
|
-
const args = lock.harness === 'codex'
|
|
264
|
-
? ['plugin', 'add', selector, '--json']
|
|
265
|
-
: ['plugin', 'install', selector, '--scope', 'user'];
|
|
266
|
-
await checked(exec, spec.executable, args, `install ${selector}`);
|
|
267
|
-
// The same plugin under an older Git marketplace is a distinct harness
|
|
268
|
-
// selection and can remain enabled beside the lock. Remove only the known
|
|
269
|
-
// ours selectors, after the exact local selection is installed successfully.
|
|
270
|
-
await removeLegacyPluginSelections(lock.harness, exec);
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Explicit install/update transaction.
|
|
274
|
-
*
|
|
275
|
-
* install reuses an existing same-channel lock (deterministic repair/reinstall);
|
|
276
|
-
* update always resolves the requested channel once and advances the lock.
|
|
277
|
-
*/
|
|
278
|
-
export async function installHarnessPlugin(harness, channel, options = {}) {
|
|
279
|
-
const exec = options.exec ?? realExec;
|
|
280
|
-
const root = harnessPluginRoot(harness);
|
|
281
|
-
return withFileLock(`${root}.lock`, async () => {
|
|
282
|
-
const existing = readHarnessPluginLock(harness);
|
|
283
|
-
const mustResolve = options.update === true || !existing || existing.channel !== channel;
|
|
284
|
-
const lock = mustResolve
|
|
285
|
-
? await resolveExactVersion(harness, channel, exec, options.now ?? (() => new Date()))
|
|
286
|
-
: existing;
|
|
287
|
-
if (mustResolve)
|
|
288
|
-
replaceFileAtomically(harnessPluginLockPath(harness), `${JSON.stringify(lock, null, 2)}\n`);
|
|
289
|
-
// Publish the lock first. If installation is interrupted, every retry uses
|
|
290
|
-
// this same exact version; no half-finished transaction can observe a newer
|
|
291
|
-
// dist-tag on its own.
|
|
292
|
-
const marketplacePath = writeMarketplace(lock);
|
|
293
|
-
await installFromLock(lock, exec);
|
|
294
|
-
return {
|
|
295
|
-
lock,
|
|
296
|
-
lockPath: harnessPluginLockPath(harness),
|
|
297
|
-
marketplacePath,
|
|
298
|
-
resolved: mustResolve,
|
|
299
|
-
};
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
export function selectedHarnessPluginIds(values) {
|
|
303
|
-
if (!values.length)
|
|
304
|
-
return [...HARNESS_PLUGIN_IDS];
|
|
305
|
-
const unknown = values.filter(value => !HARNESS_PLUGIN_IDS.includes(value));
|
|
306
|
-
if (unknown.length)
|
|
307
|
-
throw new Error(`unknown harness(es) ${unknown.join(', ')}; allowed: ${HARNESS_PLUGIN_IDS.join(', ')}`);
|
|
308
|
-
return [...new Set(values)];
|
|
309
|
-
}
|
package/dist/model-env.d.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import type { ResolvedRole } from './config.js';
|
|
2
|
-
/**
|
|
3
|
-
* Which environment variable a harness reads to pin the model it RUNS.
|
|
4
|
-
*
|
|
5
|
-
* This is not a convenience: for `claude-code` it is the only channel that
|
|
6
|
-
* reaches the ACP backend at all. `buildLaunch` (tmux) passes `--model`, but
|
|
7
|
-
* `buildAcpLaunch` launches the ACP adapter with no model argument, and that
|
|
8
|
-
* adapter resolves its model in this order — ANTHROPIC_MODEL, then
|
|
9
|
-
* `settings.model`, then a resumed session's live model, then its first
|
|
10
|
-
* catalogue entry. A role's declared model was therefore invisible to every
|
|
11
|
-
* ACP role, and a fleet-wide `defaults.env.ANTHROPIC_MODEL` silently outranked
|
|
12
|
-
* an explicitly requested one.
|
|
13
|
-
*/
|
|
14
|
-
export declare const MODEL_ENV_BY_HARNESS: Readonly<Record<string, string>>;
|
|
15
|
-
/** The model-pin variable for a harness, or undefined if it pins no model by env. */
|
|
16
|
-
export declare function modelEnvVar(harness: string | undefined): string | undefined;
|
|
17
|
-
export interface RoleModelEnvInput {
|
|
18
|
-
harness: string;
|
|
19
|
-
/** Already resolved by `resolveRoleModel` — may come from the fleet default. */
|
|
20
|
-
model: string | undefined;
|
|
21
|
-
/** True when the role (or `--model`) named a model, including `model: null`. */
|
|
22
|
-
modelWasExplicit: boolean;
|
|
23
|
-
defaultsEnv?: Record<string, string>;
|
|
24
|
-
roleEnv?: Record<string, string>;
|
|
25
|
-
authProxyBaseUrl?: string;
|
|
26
|
-
}
|
|
27
|
-
export interface RoleModelEnv {
|
|
28
|
-
env: Record<string, string>;
|
|
29
|
-
/**
|
|
30
|
-
* The model the harness will actually run. Equal to `env[pin]` for a harness
|
|
31
|
-
* that pins by env, so anything reporting this value reports the runtime.
|
|
32
|
-
*/
|
|
33
|
-
model: string | undefined;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Resolve a role's environment and its runtime model TOGETHER, so the two can
|
|
37
|
-
* never disagree.
|
|
38
|
-
*
|
|
39
|
-
* Precedence, highest first:
|
|
40
|
-
* 1. an explicit `model:` / `--model` on the role
|
|
41
|
-
* 2. the role's own `env:` pin
|
|
42
|
-
* 3. the fleet `defaults.model`
|
|
43
|
-
* 4. the fleet `defaults.env` pin
|
|
44
|
-
*
|
|
45
|
-
* Inheriting the fleet default remains correct when the role names no model
|
|
46
|
-
* (2, 3, 4); an explicitly named one wins (1). Where both are explicit and they
|
|
47
|
-
* disagree, there is no defensible winner, so this refuses rather than picking
|
|
48
|
-
* one silently — the silence is what let a day of "Fable" work run on Opus.
|
|
49
|
-
*
|
|
50
|
-
* `model: null` explicitly asks for no fleet-chosen model, so it also clears an
|
|
51
|
-
* inherited pin instead of leaving one in place to act as a hidden default.
|
|
52
|
-
*/
|
|
53
|
-
export declare function resolveRoleModelEnv(input: RoleModelEnvInput, describe?: (message: string) => Error): RoleModelEnv;
|
|
54
|
-
/**
|
|
55
|
-
* The model a role will actually run, read back from the environment it was
|
|
56
|
-
* resolved with. Use this wherever a model is reported to a human.
|
|
57
|
-
*/
|
|
58
|
-
export declare function effectiveRoleModel(role: ResolvedRole): string | undefined;
|
|
59
|
-
/**
|
|
60
|
-
* Move a role's env pin onto a new model. Anything that changes the model a
|
|
61
|
-
* role runs after resolution — model-chain recovery is the live example — must
|
|
62
|
-
* go through this, or it changes only the label.
|
|
63
|
-
*/
|
|
64
|
-
export declare function repinModelEnv(role: ResolvedRole, model: string | undefined): Record<string, string> | undefined;
|
|
65
|
-
/**
|
|
66
|
-
* Last line of defence, at the exact point a child's environment is composed:
|
|
67
|
-
* refuse to launch a role whose child would run a model other than the one the
|
|
68
|
-
* role declares and the banner reports. A spawn that cannot keep those two in
|
|
69
|
-
* agreement must fail loudly, not start and be believed.
|
|
70
|
-
*/
|
|
71
|
-
export declare function assertModelPinReachesChild(role: ResolvedRole, childEnv: Record<string, string | undefined>): void;
|
package/dist/model-env.js
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Which environment variable a harness reads to pin the model it RUNS.
|
|
3
|
-
*
|
|
4
|
-
* This is not a convenience: for `claude-code` it is the only channel that
|
|
5
|
-
* reaches the ACP backend at all. `buildLaunch` (tmux) passes `--model`, but
|
|
6
|
-
* `buildAcpLaunch` launches the ACP adapter with no model argument, and that
|
|
7
|
-
* adapter resolves its model in this order — ANTHROPIC_MODEL, then
|
|
8
|
-
* `settings.model`, then a resumed session's live model, then its first
|
|
9
|
-
* catalogue entry. A role's declared model was therefore invisible to every
|
|
10
|
-
* ACP role, and a fleet-wide `defaults.env.ANTHROPIC_MODEL` silently outranked
|
|
11
|
-
* an explicitly requested one.
|
|
12
|
-
*/
|
|
13
|
-
export const MODEL_ENV_BY_HARNESS = {
|
|
14
|
-
'claude-code': 'ANTHROPIC_MODEL',
|
|
15
|
-
};
|
|
16
|
-
/** The model-pin variable for a harness, or undefined if it pins no model by env. */
|
|
17
|
-
export function modelEnvVar(harness) {
|
|
18
|
-
return harness === undefined ? undefined : MODEL_ENV_BY_HARNESS[harness];
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Resolve a role's environment and its runtime model TOGETHER, so the two can
|
|
22
|
-
* never disagree.
|
|
23
|
-
*
|
|
24
|
-
* Precedence, highest first:
|
|
25
|
-
* 1. an explicit `model:` / `--model` on the role
|
|
26
|
-
* 2. the role's own `env:` pin
|
|
27
|
-
* 3. the fleet `defaults.model`
|
|
28
|
-
* 4. the fleet `defaults.env` pin
|
|
29
|
-
*
|
|
30
|
-
* Inheriting the fleet default remains correct when the role names no model
|
|
31
|
-
* (2, 3, 4); an explicitly named one wins (1). Where both are explicit and they
|
|
32
|
-
* disagree, there is no defensible winner, so this refuses rather than picking
|
|
33
|
-
* one silently — the silence is what let a day of "Fable" work run on Opus.
|
|
34
|
-
*
|
|
35
|
-
* `model: null` explicitly asks for no fleet-chosen model, so it also clears an
|
|
36
|
-
* inherited pin instead of leaving one in place to act as a hidden default.
|
|
37
|
-
*/
|
|
38
|
-
export function resolveRoleModelEnv(input, describe = message => new Error(message)) {
|
|
39
|
-
const env = {
|
|
40
|
-
...(input.defaultsEnv ?? {}),
|
|
41
|
-
...(input.roleEnv ?? {}),
|
|
42
|
-
...(input.authProxyBaseUrl ? { ANTHROPIC_BASE_URL: input.authProxyBaseUrl } : {}),
|
|
43
|
-
};
|
|
44
|
-
const pin = modelEnvVar(input.harness);
|
|
45
|
-
if (!pin)
|
|
46
|
-
return { env, model: input.model };
|
|
47
|
-
const rolePin = input.roleEnv?.[pin];
|
|
48
|
-
if (input.modelWasExplicit) {
|
|
49
|
-
if (rolePin !== undefined && rolePin !== input.model)
|
|
50
|
-
throw describe(`model '${input.model ?? '(none)'}' contradicts env.${pin} '${rolePin}'; `
|
|
51
|
-
+ `remove one — ${pin} is what the harness actually runs`);
|
|
52
|
-
if (input.model === undefined)
|
|
53
|
-
delete env[pin];
|
|
54
|
-
else
|
|
55
|
-
env[pin] = input.model;
|
|
56
|
-
return { env, model: input.model };
|
|
57
|
-
}
|
|
58
|
-
// Not explicit: a role-level pin is the most specific thing said about this
|
|
59
|
-
// role, so it decides — and the reported model follows it.
|
|
60
|
-
if (rolePin !== undefined)
|
|
61
|
-
return { env, model: rolePin };
|
|
62
|
-
if (input.model !== undefined)
|
|
63
|
-
env[pin] = input.model;
|
|
64
|
-
return { env, model: input.model ?? env[pin] };
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* The model a role will actually run, read back from the environment it was
|
|
68
|
-
* resolved with. Use this wherever a model is reported to a human.
|
|
69
|
-
*/
|
|
70
|
-
export function effectiveRoleModel(role) {
|
|
71
|
-
const pin = modelEnvVar(role.harness);
|
|
72
|
-
return (pin ? role.env?.[pin] : undefined) ?? role.model;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Move a role's env pin onto a new model. Anything that changes the model a
|
|
76
|
-
* role runs after resolution — model-chain recovery is the live example — must
|
|
77
|
-
* go through this, or it changes only the label.
|
|
78
|
-
*/
|
|
79
|
-
export function repinModelEnv(role, model) {
|
|
80
|
-
const pin = modelEnvVar(role.harness);
|
|
81
|
-
if (!pin)
|
|
82
|
-
return role.env;
|
|
83
|
-
const env = { ...(role.env ?? {}) };
|
|
84
|
-
if (model === undefined)
|
|
85
|
-
delete env[pin];
|
|
86
|
-
else
|
|
87
|
-
env[pin] = model;
|
|
88
|
-
return Object.keys(env).length ? env : undefined;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Last line of defence, at the exact point a child's environment is composed:
|
|
92
|
-
* refuse to launch a role whose child would run a model other than the one the
|
|
93
|
-
* role declares and the banner reports. A spawn that cannot keep those two in
|
|
94
|
-
* agreement must fail loudly, not start and be believed.
|
|
95
|
-
*/
|
|
96
|
-
export function assertModelPinReachesChild(role, childEnv) {
|
|
97
|
-
const pin = modelEnvVar(role.harness);
|
|
98
|
-
if (!pin || role.model === undefined)
|
|
99
|
-
return;
|
|
100
|
-
const actual = childEnv[pin];
|
|
101
|
-
if (actual === role.model)
|
|
102
|
-
return;
|
|
103
|
-
throw new Error(`[${role.name}] refusing to launch: role model is '${role.model}' but the child's `
|
|
104
|
-
+ `${pin} is ${actual === undefined ? 'unset' : `'${actual}'`} — the session would run a `
|
|
105
|
-
+ 'different model than the one reported');
|
|
106
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export declare class OursMcpError extends Error {
|
|
2
|
-
}
|
|
3
|
-
export interface OursToolClient {
|
|
4
|
-
start(): Promise<void>;
|
|
5
|
-
callTool(name: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
6
|
-
close(): Promise<void>;
|
|
7
|
-
}
|
|
8
|
-
/** Minimal MCP stdio client. One instance owns exactly one ours identity binding. */
|
|
9
|
-
export declare class OursMcpClient implements OursToolClient {
|
|
10
|
-
private readonly command;
|
|
11
|
-
private readonly env;
|
|
12
|
-
private readonly log;
|
|
13
|
-
private child?;
|
|
14
|
-
private nextId;
|
|
15
|
-
private tail;
|
|
16
|
-
constructor(command?: string, env?: Record<string, string>, log?: (line: string) => void);
|
|
17
|
-
start(): Promise<void>;
|
|
18
|
-
callTool(name: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
19
|
-
close(): Promise<void>;
|
|
20
|
-
private request;
|
|
21
|
-
private requestNow;
|
|
22
|
-
private notify;
|
|
23
|
-
private write;
|
|
24
|
-
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { createInterface } from 'node:readline';
|
|
4
|
-
export class OursMcpError extends Error {
|
|
5
|
-
}
|
|
6
|
-
/** Minimal MCP stdio client. One instance owns exactly one ours identity binding. */
|
|
7
|
-
export class OursMcpClient {
|
|
8
|
-
command;
|
|
9
|
-
env;
|
|
10
|
-
log;
|
|
11
|
-
child;
|
|
12
|
-
nextId = 0;
|
|
13
|
-
tail = Promise.resolve();
|
|
14
|
-
constructor(command = 'ours-mcp', env = {}, log = () => undefined) {
|
|
15
|
-
this.command = command;
|
|
16
|
-
this.env = env;
|
|
17
|
-
this.log = log;
|
|
18
|
-
}
|
|
19
|
-
async start() {
|
|
20
|
-
if (this.child && this.child.exitCode === null)
|
|
21
|
-
return;
|
|
22
|
-
// ours-mcp normally records the long-lived client PID so an identity lease
|
|
23
|
-
// survives connector churn. An owner-channel connector has the opposite
|
|
24
|
-
// lifecycle: each supervised attempt owns a fresh connector and must make
|
|
25
|
-
// its lease reclaimable when that connector exits, even though the fleet
|
|
26
|
-
// supervisor itself remains alive. POSIX exec preserves the shell PID as
|
|
27
|
-
// the proxy PID, giving the daemon an exact process-lifetime fence without
|
|
28
|
-
// interpolating the command path into shell text.
|
|
29
|
-
const child = spawn('/bin/sh', [
|
|
30
|
-
'-c', 'OURS_CLIENT_PID=$$; export OURS_CLIENT_PID; exec "$1" "$2"',
|
|
31
|
-
'ours-fleet-owner-proxy', this.command, 'proxy',
|
|
32
|
-
], {
|
|
33
|
-
env: {
|
|
34
|
-
...process.env,
|
|
35
|
-
...this.env,
|
|
36
|
-
// Bindings are keyed by this value. Sharing it would silently rebind a
|
|
37
|
-
// role's normal mailbox or another owner channel.
|
|
38
|
-
CLAUDE_CODE_SESSION_ID: `ours-fleet-owner-${process.pid}-${randomUUID()}`,
|
|
39
|
-
},
|
|
40
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
41
|
-
});
|
|
42
|
-
await new Promise((resolve, reject) => {
|
|
43
|
-
child.once('spawn', resolve);
|
|
44
|
-
child.once('error', reject);
|
|
45
|
-
});
|
|
46
|
-
this.child = child;
|
|
47
|
-
child.once('exit', (code, signal) => {
|
|
48
|
-
this.log(`ours-mcp proxy launcher exited (${code ?? signal ?? 'unknown'})`);
|
|
49
|
-
});
|
|
50
|
-
child.stdin.on('error', error => this.log(`ours-mcp stdin: ${error.message}`));
|
|
51
|
-
createInterface({ input: child.stderr }).on('line', line => this.log(`ours-mcp: ${line}`));
|
|
52
|
-
try {
|
|
53
|
-
await this.request('initialize', {
|
|
54
|
-
protocolVersion: '2025-03-26', capabilities: {},
|
|
55
|
-
clientInfo: { name: 'ours-fleet-owner-channel', version: '1' },
|
|
56
|
-
});
|
|
57
|
-
await this.notify('notifications/initialized', {});
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
await this.close();
|
|
61
|
-
throw error;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
async callTool(name, args = {}) {
|
|
65
|
-
const result = await this.request('tools/call', { name, arguments: args });
|
|
66
|
-
const text = (result.content ?? [])
|
|
67
|
-
.filter(item => item.type === 'text').map(item => item.text ?? '').join('\n').trim();
|
|
68
|
-
if (result.isError)
|
|
69
|
-
throw new OursMcpError(text || `ours tool ${name} failed`);
|
|
70
|
-
if (result.structuredContent !== undefined)
|
|
71
|
-
return result.structuredContent;
|
|
72
|
-
if (!text)
|
|
73
|
-
return {};
|
|
74
|
-
try {
|
|
75
|
-
return JSON.parse(text);
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
return text;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
async close() {
|
|
82
|
-
const child = this.child;
|
|
83
|
-
this.child = undefined;
|
|
84
|
-
if (!child || child.exitCode !== null)
|
|
85
|
-
return;
|
|
86
|
-
// EOF asks the proxy to close normally. Once this exact process exits, the
|
|
87
|
-
// daemon can reclaim its lease even while the supervisor stays alive.
|
|
88
|
-
child.stdin.end();
|
|
89
|
-
const exited = await new Promise(resolve => {
|
|
90
|
-
const timer = setTimeout(() => resolve(false), 1_000);
|
|
91
|
-
child.once('exit', () => { clearTimeout(timer); resolve(true); });
|
|
92
|
-
});
|
|
93
|
-
if (exited || child.exitCode !== null)
|
|
94
|
-
return;
|
|
95
|
-
child.kill('SIGTERM');
|
|
96
|
-
await new Promise(resolve => {
|
|
97
|
-
const timer = setTimeout(() => { child.kill('SIGKILL'); resolve(); }, 5_000);
|
|
98
|
-
child.once('exit', () => { clearTimeout(timer); resolve(); });
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
request(method, params) {
|
|
102
|
-
const run = this.tail.then(() => this.requestNow(method, params));
|
|
103
|
-
this.tail = run.then(() => undefined, () => undefined);
|
|
104
|
-
return run;
|
|
105
|
-
}
|
|
106
|
-
async requestNow(method, params) {
|
|
107
|
-
const child = this.child;
|
|
108
|
-
if (!child || child.exitCode !== null)
|
|
109
|
-
throw new OursMcpError('ours-mcp proxy is not running');
|
|
110
|
-
const id = ++this.nextId;
|
|
111
|
-
await this.write(child, { jsonrpc: '2.0', id, method, params });
|
|
112
|
-
const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
113
|
-
try {
|
|
114
|
-
for await (const line of lines) {
|
|
115
|
-
let response;
|
|
116
|
-
try {
|
|
117
|
-
response = JSON.parse(line);
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (response.id !== id)
|
|
123
|
-
continue;
|
|
124
|
-
if (response.error !== undefined)
|
|
125
|
-
throw new OursMcpError(JSON.stringify(response.error));
|
|
126
|
-
return response.result ?? {};
|
|
127
|
-
}
|
|
128
|
-
throw new OursMcpError('ours-mcp proxy closed its output');
|
|
129
|
-
}
|
|
130
|
-
finally {
|
|
131
|
-
lines.close();
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
async notify(method, params) {
|
|
135
|
-
const child = this.child;
|
|
136
|
-
if (!child || child.exitCode !== null)
|
|
137
|
-
throw new OursMcpError('ours-mcp proxy is not running');
|
|
138
|
-
await this.write(child, { jsonrpc: '2.0', method, params });
|
|
139
|
-
}
|
|
140
|
-
write(child, value) {
|
|
141
|
-
return new Promise((resolve, reject) => {
|
|
142
|
-
child.stdin.write(JSON.stringify(value) + '\n', error => error ? reject(error) : resolve());
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
}
|