@agentshouse/kit 0.0.1 → 0.1.0-alpha.10
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/LICENSE +21 -0
- package/README.md +1776 -2
- package/bin/connect-linux.sh +173 -0
- package/bin/kit.js +1023 -0
- package/bootstrap.json +17 -0
- package/package.json +37 -3
- package/src/acp.js +99 -0
- package/src/adapter-mcp.js +44 -0
- package/src/adapter.js +252 -0
- package/src/agent-logins.js +65 -0
- package/src/agents.js +504 -0
- package/src/apply.js +29 -0
- package/src/attach.js +52 -0
- package/src/binding.js +90 -0
- package/src/blocks.js +156 -0
- package/src/bridge.js +449 -0
- package/src/bytes.js +89 -0
- package/src/channel.js +37 -0
- package/src/command.js +109 -0
- package/src/configuration.js +465 -0
- package/src/connector-login.js +91 -0
- package/src/connector.js +107 -0
- package/src/continuation.js +61 -0
- package/src/contract.js +363 -0
- package/src/conversation-originals.js +143 -0
- package/src/custom/authorization.js +63 -0
- package/src/declaration.js +35 -0
- package/src/delivery.js +191 -0
- package/src/directory.js +102 -0
- package/src/draft.js +89 -0
- package/src/google/calendar.js +177 -0
- package/src/google/declared.js +146 -0
- package/src/google/drive.js +435 -0
- package/src/google/gateway.js +166 -0
- package/src/google/gmail.js +249 -0
- package/src/google/id.js +1 -0
- package/src/google/oauth.js +273 -0
- package/src/google/source.js +25 -0
- package/src/hook.js +376 -0
- package/src/host.js +121 -0
- package/src/house-main.js +79 -0
- package/src/house.js +1275 -0
- package/src/inbound.js +175 -0
- package/src/instance/google.js +1 -0
- package/src/kit-work-stream.js +84 -0
- package/src/launcher-main.js +19 -0
- package/src/launcher.js +276 -0
- package/src/local-files.js +113 -0
- package/src/login.js +274 -0
- package/src/mcp.js +71 -0
- package/src/migrator.js +248 -0
- package/src/mount.js +582 -0
- package/src/operation.js +20 -0
- package/src/original.js +631 -0
- package/src/peer.js +124 -0
- package/src/plan.js +57 -0
- package/src/question.js +316 -0
- package/src/refusal.js +79 -0
- package/src/repository.js +521 -0
- package/src/session-bin/house +2 -0
- package/src/settlement.js +57 -0
- package/src/setup.js +190 -0
- package/src/source-originals.js +116 -0
- package/src/source.js +290 -0
- package/src/starter-set/grilling/LICENSE +21 -0
- package/src/starter-set/grilling/SKILL.md +30 -0
- package/src/starter-set/grilling/agents/openai.yaml +3 -0
- package/src/starter-set.js +17 -0
- package/src/state.js +157 -0
- package/src/sync-library.js +293 -0
- package/src/sync-state.js +120 -0
- package/src/sync.js +482 -0
- package/src/telegram.js +1061 -0
- package/src/terminal.js +57 -0
- package/src/tmux.js +119 -0
- package/src/worker.js +928 -0
- package/src/workspace.js +500 -0
package/src/agents.js
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFile, readdir, rm } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
compiledContract,
|
|
7
|
+
offeredSessionOptions,
|
|
8
|
+
releaseOrder,
|
|
9
|
+
servedRelease,
|
|
10
|
+
verifyCommandAdapter,
|
|
11
|
+
} from "./adapter.js";
|
|
12
|
+
import { inheritedEnvironment } from "./command.js";
|
|
13
|
+
import {
|
|
14
|
+
ADAPTER_KINDS,
|
|
15
|
+
AGENT_HOME,
|
|
16
|
+
AGENT_INSTALL_TIMEOUT_MS,
|
|
17
|
+
AGENT_RELEASES_HOME,
|
|
18
|
+
AGENT_SIGNED_IN_TIMEOUT_MS,
|
|
19
|
+
AGENT_STATE_FILE,
|
|
20
|
+
AGENT_UPDATE_INTERVAL_MS,
|
|
21
|
+
GUIDED_LOGIN_OUTPUT_MAX_BYTES,
|
|
22
|
+
WORKER_INHERITED_ENVIRONMENT,
|
|
23
|
+
} from "./contract.js";
|
|
24
|
+
import { readConfiguration } from "./configuration.js";
|
|
25
|
+
import { desiredAgents, maintainControl, openSession, reportAgentCapabilities } from "./house.js";
|
|
26
|
+
import { KitRefusal } from "./refusal.js";
|
|
27
|
+
import { readNote, writeNote, writeText } from "./state.js";
|
|
28
|
+
|
|
29
|
+
export function agentDirectory(directory, kind) {
|
|
30
|
+
return join(directory, AGENT_HOME, kind);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function statePath(directory, kind) {
|
|
34
|
+
return join(agentDirectory(directory, kind), AGENT_STATE_FILE);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function releaseDirectory(directory, kind, release) {
|
|
38
|
+
return join(agentDirectory(directory, kind), AGENT_RELEASES_HOME, release);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function releaseCommand(directory, kind, release, contract) {
|
|
42
|
+
return [join(releaseDirectory(directory, kind, release), "node_modules", ".bin", contract.binary)];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function adapterCommand(directory, kind, release, contract) {
|
|
46
|
+
return [...releaseCommand(directory, kind, release, contract), ...contract.arguments];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function readAgentState(directory, kind) {
|
|
50
|
+
const held = await readNote(statePath(directory, kind), null);
|
|
51
|
+
if (held === null) return { release: null, held: null, checkedAt: null };
|
|
52
|
+
return {
|
|
53
|
+
release: typeof held.release === "string" ? held.release : null,
|
|
54
|
+
held: typeof held.held === "string" ? held.held : null,
|
|
55
|
+
checkedAt: typeof held.checked_at === "string" ? held.checked_at : null,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function writeAgentState(directory, kind, state) {
|
|
60
|
+
await writeNote(statePath(directory, kind), {
|
|
61
|
+
release: state.release,
|
|
62
|
+
held: state.held,
|
|
63
|
+
checked_at: state.checkedAt,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function installedAgentCommand(directory, kind) {
|
|
68
|
+
const contract = compiledContract(kind);
|
|
69
|
+
const state = await readAgentState(directory, kind);
|
|
70
|
+
if (state.release === null) {
|
|
71
|
+
throw new KitRefusal(
|
|
72
|
+
"kit_agent_not_installed",
|
|
73
|
+
`${kind}; run kit agents update to install ${contract.release.package}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return adapterCommand(directory, kind, state.release, contract);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ran(command, argumentVector, options) {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
const child = spawn(command, argumentVector, {
|
|
82
|
+
cwd: options.cwd,
|
|
83
|
+
env: options.env,
|
|
84
|
+
stdio: options.stdio ?? ["ignore", "ignore", "ignore"],
|
|
85
|
+
timeout: options.timeoutMs,
|
|
86
|
+
});
|
|
87
|
+
child.on("error", () => resolve(1));
|
|
88
|
+
child.on("close", (status) => resolve(status ?? 1));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function npmReleases(package_) {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const child = spawn("npm", ["view", package_, "versions", "--json"], {
|
|
95
|
+
env: inheritedEnvironment(WORKER_INHERITED_ENVIRONMENT),
|
|
96
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
97
|
+
timeout: AGENT_INSTALL_TIMEOUT_MS,
|
|
98
|
+
});
|
|
99
|
+
let answered = "";
|
|
100
|
+
child.stdout.on("data", (chunk) => {
|
|
101
|
+
answered += chunk.toString("utf8");
|
|
102
|
+
});
|
|
103
|
+
child.on("error", () => reject(new KitRefusal("kit_agent_releases_unavailable", package_)));
|
|
104
|
+
child.on("close", (status) => {
|
|
105
|
+
if (status !== 0) {
|
|
106
|
+
reject(new KitRefusal("kit_agent_releases_unavailable", package_));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const listed = JSON.parse(answered);
|
|
111
|
+
resolve(Array.isArray(listed) ? listed : [listed]);
|
|
112
|
+
} catch {
|
|
113
|
+
reject(new KitRefusal("kit_agent_releases_unavailable", package_));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function npmInstall(package_, release, directory) {
|
|
120
|
+
const status = await ran(
|
|
121
|
+
"npm",
|
|
122
|
+
["install", "--prefix", directory, "--no-save", "--no-audit", "--no-fund", `${package_}@${release}`],
|
|
123
|
+
{
|
|
124
|
+
env: inheritedEnvironment(WORKER_INHERITED_ENVIRONMENT),
|
|
125
|
+
timeoutMs: AGENT_INSTALL_TIMEOUT_MS,
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
if (status !== 0) throw new KitRefusal("kit_agent_install_failed", `${package_}@${release}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function servedCandidate(releases, minimum) {
|
|
132
|
+
const served = releases.filter((release) => servedRelease(release, minimum));
|
|
133
|
+
if (served.length === 0) return null;
|
|
134
|
+
return served.reduce((held, release) => (releaseOrder(release, held) > 0 ? release : held));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function dueForCandidate(state, at) {
|
|
138
|
+
if (state.checkedAt === null) return true;
|
|
139
|
+
return at.getTime() - new Date(state.checkedAt).getTime() >= AGENT_UPDATE_INTERVAL_MS;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function proved(directory, kind, release, contract, dependencies) {
|
|
143
|
+
const command = adapterCommand(directory, kind, release, contract);
|
|
144
|
+
try {
|
|
145
|
+
await verifyCommandAdapter(
|
|
146
|
+
{
|
|
147
|
+
command,
|
|
148
|
+
arguments: [],
|
|
149
|
+
environment: {},
|
|
150
|
+
workingDirectory: directory,
|
|
151
|
+
},
|
|
152
|
+
contract,
|
|
153
|
+
dependencies.adapterInitializeTimeoutMs,
|
|
154
|
+
`${kind} candidate release ${release}`,
|
|
155
|
+
);
|
|
156
|
+
return true;
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function declaredSetting(document, table, key) {
|
|
163
|
+
const dotted = new RegExp(`^\\s*${`${table}.${key}`.replace(/\./gu, "\\.")}\\s*=`, "mu");
|
|
164
|
+
if (dotted.test(document)) return true;
|
|
165
|
+
const header = new RegExp(`^[^\\S\\n]*\\[${table.replace(/\./gu, "\\.")}\\][^\\S\\n]*$`, "mu");
|
|
166
|
+
const found = header.exec(document);
|
|
167
|
+
if (found === null) return false;
|
|
168
|
+
const section = document.slice(found.index + found[0].length).split(/^[^\S\n]*\[/mu)[0];
|
|
169
|
+
return new RegExp(`^[^\\S\\n]*${key}[^\\S\\n]*=`, "mu").test(section);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function applyAgentSettings(contract, dependencies = {}) {
|
|
173
|
+
const home = dependencies.home ?? homedir();
|
|
174
|
+
for (const setting of contract.settings) {
|
|
175
|
+
const path = join(home, setting.file);
|
|
176
|
+
const named = setting.setting.split(".");
|
|
177
|
+
const key = named.at(-1);
|
|
178
|
+
const table = named.slice(0, -1).join(".");
|
|
179
|
+
let document;
|
|
180
|
+
try {
|
|
181
|
+
document = await readFile(path, "utf8");
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (error.code !== "ENOENT") throw new KitRefusal("kit_agent_setting_unwritable", path);
|
|
184
|
+
document = null;
|
|
185
|
+
}
|
|
186
|
+
if (document !== null && declaredSetting(document, table, key)) continue;
|
|
187
|
+
const declaration = `[${table}]\n${key} = ${setting.value}\n`;
|
|
188
|
+
await writeText(
|
|
189
|
+
path,
|
|
190
|
+
document === null ? declaration : `${document.replace(/\n*$/u, "\n")}\n${declaration}`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function advanceAgent(options, kind, dependencies) {
|
|
196
|
+
const contract = compiledContract(kind);
|
|
197
|
+
const directory = options.directory;
|
|
198
|
+
const state = await readAgentState(directory, kind);
|
|
199
|
+
const at = options.at ?? new Date();
|
|
200
|
+
if (state.release !== null && !options.forced && !dueForCandidate(state, at)) {
|
|
201
|
+
return { kind, release: state.release, held: state.held, acted: "held-release" };
|
|
202
|
+
}
|
|
203
|
+
const releases = await (dependencies.agentReleases ?? npmReleases)(contract.release.package);
|
|
204
|
+
const candidate = servedCandidate(releases, contract.release.minimum);
|
|
205
|
+
if (candidate === null) {
|
|
206
|
+
throw new KitRefusal(
|
|
207
|
+
"kit_agent_release_unavailable",
|
|
208
|
+
`${contract.release.package} serves no ${contract.release.minimum} or later release`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
if (candidate === state.release || candidate === state.held) {
|
|
212
|
+
await writeAgentState(directory, kind, { ...state, checkedAt: at.toISOString() });
|
|
213
|
+
return { kind, release: state.release, held: state.held, acted: "held-release" };
|
|
214
|
+
}
|
|
215
|
+
await (dependencies.installAgent ?? npmInstall)(
|
|
216
|
+
contract.release.package,
|
|
217
|
+
candidate,
|
|
218
|
+
releaseDirectory(directory, kind, candidate),
|
|
219
|
+
);
|
|
220
|
+
if (!(await proved(directory, kind, candidate, contract, dependencies))) {
|
|
221
|
+
await rm(releaseDirectory(directory, kind, candidate), { recursive: true, force: true });
|
|
222
|
+
await writeAgentState(directory, kind, {
|
|
223
|
+
release: state.release,
|
|
224
|
+
held: candidate,
|
|
225
|
+
checkedAt: at.toISOString(),
|
|
226
|
+
});
|
|
227
|
+
return { kind, release: state.release, held: candidate, acted: "held-candidate" };
|
|
228
|
+
}
|
|
229
|
+
await applyAgentSettings(contract, dependencies);
|
|
230
|
+
await writeAgentState(directory, kind, {
|
|
231
|
+
release: candidate,
|
|
232
|
+
held: null,
|
|
233
|
+
checkedAt: at.toISOString(),
|
|
234
|
+
});
|
|
235
|
+
if (state.release !== null && state.release !== candidate) {
|
|
236
|
+
await rm(releaseDirectory(directory, kind, state.release), { recursive: true, force: true });
|
|
237
|
+
}
|
|
238
|
+
return { kind, release: candidate, held: null, acted: "installed-release" };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function updateDeclaredAgent(options, kind, dependencies) {
|
|
242
|
+
try {
|
|
243
|
+
return await advanceAgent(options, kind, dependencies);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (error instanceof KitRefusal && error.code === "kit_command_cancelled") throw error;
|
|
246
|
+
const state = await readAgentState(options.directory, kind);
|
|
247
|
+
return {
|
|
248
|
+
kind,
|
|
249
|
+
release: state.release,
|
|
250
|
+
held: state.held,
|
|
251
|
+
acted: "update-failed",
|
|
252
|
+
failure: error instanceof KitRefusal ? error.code : error.message,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function updateDeclaredAgents(options, configuration, dependencies = {}) {
|
|
258
|
+
const updated = [];
|
|
259
|
+
for (const kind of configuration.agents) {
|
|
260
|
+
updated.push(await updateDeclaredAgent(options, kind, dependencies));
|
|
261
|
+
}
|
|
262
|
+
return updated;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function removeUndeclaredAgents(directory, wanted, keep = false) {
|
|
266
|
+
const home = join(directory, AGENT_HOME);
|
|
267
|
+
let entries;
|
|
268
|
+
try {
|
|
269
|
+
entries = await readdir(home, { withFileTypes: true });
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (error.code === "ENOENT") return [];
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
const retained = [];
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
if (!wanted.includes(entry.name)) {
|
|
277
|
+
if (keep && ADAPTER_KINDS.includes(entry.name)) retained.push(entry.name);
|
|
278
|
+
else await rm(join(home, entry.name), { recursive: true, force: true });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return retained;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function reconcileAgentKinds(
|
|
285
|
+
options,
|
|
286
|
+
session,
|
|
287
|
+
configuration,
|
|
288
|
+
dependencies = {},
|
|
289
|
+
signal = session.signal,
|
|
290
|
+
state = {},
|
|
291
|
+
) {
|
|
292
|
+
const selected = { ...configuration, agents: await desiredAgents(session, signal) };
|
|
293
|
+
state.onDesired?.(selected.agents);
|
|
294
|
+
const newlyWanted = selected.agents.filter((kind) => !state.lastDesired?.includes(kind));
|
|
295
|
+
const retained = await removeUndeclaredAgents(options.directory, selected.agents, state.busy);
|
|
296
|
+
const pointerKinds = state.busy
|
|
297
|
+
? (
|
|
298
|
+
await Promise.all(
|
|
299
|
+
newlyWanted.map(async (kind) =>
|
|
300
|
+
(await readAgentState(options.directory, kind)).release === null ? kind : null,
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
).filter((kind) => kind !== null)
|
|
304
|
+
: newlyWanted;
|
|
305
|
+
const updated = await updateDeclaredAgents(
|
|
306
|
+
{ directory: options.directory, at: dependencies.now?.(), forced: state.forced },
|
|
307
|
+
{ ...selected, agents: state.pointer ? pointerKinds : selected.agents },
|
|
308
|
+
dependencies,
|
|
309
|
+
);
|
|
310
|
+
const readiness = await agentReadiness(
|
|
311
|
+
{ directory: options.directory },
|
|
312
|
+
{ ...selected, agents: [...selected.agents, ...retained] },
|
|
313
|
+
dependencies,
|
|
314
|
+
);
|
|
315
|
+
await reportAgentCapabilities(session, readiness.agents, signal);
|
|
316
|
+
return { selected, updated, reported: readiness.agents, refused: readiness.refused, retained };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export async function agentSignedIn(directory, kind, dependencies = {}) {
|
|
320
|
+
const contract = compiledContract(kind);
|
|
321
|
+
const state = await readAgentState(directory, kind);
|
|
322
|
+
if (state.release === null) return false;
|
|
323
|
+
if (!Array.isArray(contract.signedIn)) {
|
|
324
|
+
try {
|
|
325
|
+
const initialized = await verifyCommandAdapter(
|
|
326
|
+
{
|
|
327
|
+
command: adapterCommand(directory, kind, state.release, contract),
|
|
328
|
+
arguments: [],
|
|
329
|
+
environment: {},
|
|
330
|
+
workingDirectory: directory,
|
|
331
|
+
},
|
|
332
|
+
contract,
|
|
333
|
+
dependencies.adapterInitializeTimeoutMs,
|
|
334
|
+
`agent ${kind} sign-in readiness`,
|
|
335
|
+
);
|
|
336
|
+
const method = initialized?._meta?.[contract.signedIn.initializeMeta];
|
|
337
|
+
return typeof method === "string" && method.length > 0;
|
|
338
|
+
} catch {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const [command] = releaseCommand(directory, kind, state.release, contract);
|
|
343
|
+
const status = await (dependencies.runAgentCommand ?? ran)(command, [...contract.signedIn], {
|
|
344
|
+
cwd: directory,
|
|
345
|
+
env: inheritedEnvironment(WORKER_INHERITED_ENVIRONMENT),
|
|
346
|
+
timeoutMs: AGENT_SIGNED_IN_TIMEOUT_MS,
|
|
347
|
+
});
|
|
348
|
+
return status === 0;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function offeredOptions(directory, kind, release, dependencies) {
|
|
352
|
+
if (release === null) return { options: null, refused: null };
|
|
353
|
+
const contract = compiledContract(kind);
|
|
354
|
+
const command = adapterCommand(directory, kind, release, contract);
|
|
355
|
+
try {
|
|
356
|
+
const options = await offeredSessionOptions(
|
|
357
|
+
{ command, arguments: [], environment: {}, workingDirectory: directory },
|
|
358
|
+
contract,
|
|
359
|
+
{
|
|
360
|
+
initialize: dependencies.adapterInitializeTimeoutMs,
|
|
361
|
+
session: dependencies.adapterSessionTimeoutMs,
|
|
362
|
+
},
|
|
363
|
+
`agent ${kind} adapter ${JSON.stringify(command[0])}`,
|
|
364
|
+
);
|
|
365
|
+
return { options, refused: null };
|
|
366
|
+
} catch (refusal) {
|
|
367
|
+
if (!(refusal instanceof KitRefusal) || refusal.code !== "kit_adapter_contract_mismatch") {
|
|
368
|
+
throw refusal;
|
|
369
|
+
}
|
|
370
|
+
return { options: null, refused: refusal };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export async function agentReadiness(options, configuration, dependencies = {}) {
|
|
375
|
+
const agents = [];
|
|
376
|
+
let refused = null;
|
|
377
|
+
for (const kind of configuration.agents) {
|
|
378
|
+
const state = await readAgentState(options.directory, kind);
|
|
379
|
+
const offered = await offeredOptions(options.directory, kind, state.release, dependencies);
|
|
380
|
+
refused ??= offered.refused;
|
|
381
|
+
agents.push({
|
|
382
|
+
kind,
|
|
383
|
+
contract_version: compiledContract(kind).version,
|
|
384
|
+
release: state.release,
|
|
385
|
+
signed_in: await agentSignedIn(options.directory, kind, dependencies),
|
|
386
|
+
hold: state.held === null ? null : { candidate: state.held },
|
|
387
|
+
options: offered.options,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
return { agents, refused };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export async function updateAgents(options, dependencies = {}) {
|
|
394
|
+
const configuration = await readConfiguration(options.configuration);
|
|
395
|
+
const session = await openSession(options, dependencies);
|
|
396
|
+
return maintainControl(
|
|
397
|
+
session,
|
|
398
|
+
async (signal) => {
|
|
399
|
+
const result = await reconcileAgentKinds(
|
|
400
|
+
options,
|
|
401
|
+
session,
|
|
402
|
+
configuration,
|
|
403
|
+
dependencies,
|
|
404
|
+
signal,
|
|
405
|
+
{ forced: true },
|
|
406
|
+
);
|
|
407
|
+
return { updated: result.updated, reported: result.reported };
|
|
408
|
+
},
|
|
409
|
+
options.signal,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export async function signInAgent(options, dependencies = {}) {
|
|
414
|
+
const configuration = await readConfiguration(options.configuration);
|
|
415
|
+
const session = await openSession(options, dependencies);
|
|
416
|
+
return maintainControl(
|
|
417
|
+
session,
|
|
418
|
+
async (signal) => {
|
|
419
|
+
const selected = { ...configuration, agents: await desiredAgents(session, signal) };
|
|
420
|
+
if (!selected.agents.includes(options.kind)) {
|
|
421
|
+
throw new KitRefusal("kit_agent_undeclared", options.kind);
|
|
422
|
+
}
|
|
423
|
+
const status = await signInDeclaredAgent(options, options.kind, dependencies);
|
|
424
|
+
const { agents } = await agentReadiness(options, selected, dependencies);
|
|
425
|
+
await reportAgentCapabilities(session, agents, signal);
|
|
426
|
+
return { status, reported: agents };
|
|
427
|
+
},
|
|
428
|
+
options.signal,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export async function signInDeclaredAgent(options, kind, dependencies = {}) {
|
|
433
|
+
const contract = compiledContract(kind);
|
|
434
|
+
const state = await readAgentState(options.directory, kind);
|
|
435
|
+
if (state.release === null) {
|
|
436
|
+
throw new KitRefusal(
|
|
437
|
+
"kit_agent_not_installed",
|
|
438
|
+
`${kind}; run kit agents update to install ${contract.release.package}`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
const [command] = releaseCommand(options.directory, kind, state.release, contract);
|
|
442
|
+
return (dependencies.runAgentCommand ?? ran)(command, [...contract.signIn], {
|
|
443
|
+
cwd: options.directory,
|
|
444
|
+
env: inheritedEnvironment(WORKER_INHERITED_ENVIRONMENT),
|
|
445
|
+
stdio: "inherit",
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function guidedAgentLogin(directory, kind, recipe) {
|
|
450
|
+
const contract = compiledContract(kind);
|
|
451
|
+
return async ({ present, signal }) => {
|
|
452
|
+
const state = await readAgentState(directory, kind);
|
|
453
|
+
const [command] = releaseCommand(directory, kind, state.release, contract);
|
|
454
|
+
const child = spawn(command, [...contract.signIn], {
|
|
455
|
+
cwd: directory,
|
|
456
|
+
env: inheritedEnvironment(WORKER_INHERITED_ENVIRONMENT),
|
|
457
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
458
|
+
});
|
|
459
|
+
const exited = new Promise((resolve) => {
|
|
460
|
+
child.once("error", () => resolve(1));
|
|
461
|
+
child.once("close", (status) => resolve(status ?? 1));
|
|
462
|
+
});
|
|
463
|
+
const stop = () => child.kill("SIGTERM");
|
|
464
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
465
|
+
const presentation = new AbortController();
|
|
466
|
+
let output = "";
|
|
467
|
+
let answered = "";
|
|
468
|
+
let rejected = false;
|
|
469
|
+
let delivery = null;
|
|
470
|
+
const observe = (chunk) => {
|
|
471
|
+
if (delivery !== null) {
|
|
472
|
+
if (rejected || answered.length >= GUIDED_LOGIN_OUTPUT_MAX_BYTES) return;
|
|
473
|
+
answered = `${answered}${chunk.toString("utf8")}`.slice(0, GUIDED_LOGIN_OUTPUT_MAX_BYTES);
|
|
474
|
+
if (!recipe.rejected(answered)) return;
|
|
475
|
+
rejected = true;
|
|
476
|
+
stop();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (output.length >= GUIDED_LOGIN_OUTPUT_MAX_BYTES) return;
|
|
480
|
+
output = `${output}${chunk.toString("utf8")}`.slice(0, GUIDED_LOGIN_OUTPUT_MAX_BYTES);
|
|
481
|
+
const ceremony = recipe.ceremony(output);
|
|
482
|
+
if (ceremony === null) return;
|
|
483
|
+
delivery = present(ceremony, AbortSignal.any([signal, presentation.signal]))
|
|
484
|
+
.then((collected) => {
|
|
485
|
+
const input = recipe.input(collected);
|
|
486
|
+
if (input !== null) child.stdin.write(input);
|
|
487
|
+
})
|
|
488
|
+
.catch(() => undefined);
|
|
489
|
+
};
|
|
490
|
+
child.stdout.on("data", observe);
|
|
491
|
+
child.stderr.on("data", observe);
|
|
492
|
+
child.stdin.on("error", () => undefined);
|
|
493
|
+
try {
|
|
494
|
+
const status = await exited;
|
|
495
|
+
if (rejected) return "failed";
|
|
496
|
+
if (signal.aborted) return "cancelled";
|
|
497
|
+
return status === 0 ? "ready" : "failed";
|
|
498
|
+
} finally {
|
|
499
|
+
signal.removeEventListener("abort", stop);
|
|
500
|
+
presentation.abort();
|
|
501
|
+
await delivery;
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
}
|
package/src/apply.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { appliedDocument, readConfiguration } from "./configuration.js";
|
|
2
|
+
import { commitSessionStartHook, planSessionStartHook } from "./hook.js";
|
|
3
|
+
import { applyDeclared, maintainControl, openSession } from "./house.js";
|
|
4
|
+
import { KitRefusal } from "./refusal.js";
|
|
5
|
+
|
|
6
|
+
function appliedState(value) {
|
|
7
|
+
if (!Number.isInteger(value?.schedules)) throw new KitRefusal("kit_apply_rejected");
|
|
8
|
+
return { schedules: value.schedules };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function apply(options, dependencies = {}) {
|
|
12
|
+
const configuration = await readConfiguration(options.configuration);
|
|
13
|
+
const plan = await planSessionStartHook(
|
|
14
|
+
{ directory: options.directory, configuration: options.configuration },
|
|
15
|
+
configuration.clients,
|
|
16
|
+
options.signal,
|
|
17
|
+
);
|
|
18
|
+
const session = await openSession(options, dependencies);
|
|
19
|
+
return maintainControl(
|
|
20
|
+
session,
|
|
21
|
+
async (signal) => {
|
|
22
|
+
const answer = await applyDeclared(session, appliedDocument(configuration), signal);
|
|
23
|
+
const applied = appliedState(answer);
|
|
24
|
+
const reconciled = await commitSessionStartHook(plan, answer.session_start_hook, signal);
|
|
25
|
+
return { ...applied, hooks: reconciled.installed, removed: reconciled.removed };
|
|
26
|
+
},
|
|
27
|
+
options.signal,
|
|
28
|
+
);
|
|
29
|
+
}
|
package/src/attach.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readBindings } from "./binding.js";
|
|
3
|
+
import { inheritedEnvironment } from "./command.js";
|
|
4
|
+
import { WORKER_ATTACH_ENVIRONMENT } from "./contract.js";
|
|
5
|
+
import { KitRefusal } from "./refusal.js";
|
|
6
|
+
import { sessionPresent, workerRuntime } from "./tmux.js";
|
|
7
|
+
|
|
8
|
+
function attachment(argv) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const attached = spawn(argv[0], argv.slice(1), {
|
|
11
|
+
env: inheritedEnvironment(WORKER_ATTACH_ENVIRONMENT),
|
|
12
|
+
stdio: "inherit",
|
|
13
|
+
});
|
|
14
|
+
attached.once("error", () => reject(new KitRefusal("kit_command_unavailable", argv[0])));
|
|
15
|
+
attached.once("close", (status) => resolve(status ?? 1));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function chosenBinding(bindings, session) {
|
|
20
|
+
if (session === null) {
|
|
21
|
+
if (bindings.length === 1) return bindings[0];
|
|
22
|
+
throw new KitRefusal(
|
|
23
|
+
bindings.length === 0 ? "kit_attach_unbound" : "kit_attach_ambiguous",
|
|
24
|
+
bindings.length === 0
|
|
25
|
+
? "this installation supervises no WorkerSession"
|
|
26
|
+
: bindings.map((binding) => binding.tmux).join(", "),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const bound = bindings.find((binding) => binding.tmux === session);
|
|
30
|
+
if (bound === undefined) throw new KitRefusal("kit_attach_unbound", session);
|
|
31
|
+
return bound;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function attachSession(options, dependencies = {}) {
|
|
35
|
+
const runtime = workerRuntime(options.directory, dependencies.tmux ?? "tmux");
|
|
36
|
+
const binding = chosenBinding(await readBindings(options.directory), options.session);
|
|
37
|
+
if (!(await sessionPresent(runtime, binding.tmux))) {
|
|
38
|
+
throw new KitRefusal("kit_attach_absent", binding.tmux);
|
|
39
|
+
}
|
|
40
|
+
const status = await (dependencies.attach ?? attachment)([
|
|
41
|
+
runtime.tmux,
|
|
42
|
+
"-L",
|
|
43
|
+
runtime.namespace,
|
|
44
|
+
"-f",
|
|
45
|
+
runtime.configuration,
|
|
46
|
+
"attach-session",
|
|
47
|
+
"-r",
|
|
48
|
+
"-t",
|
|
49
|
+
`=${binding.tmux}`,
|
|
50
|
+
]);
|
|
51
|
+
return { session: binding.tmux, status };
|
|
52
|
+
}
|
package/src/binding.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { readdir, rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { KitRefusal } from "./refusal.js";
|
|
4
|
+
import { readNote, writeNote } from "./state.js";
|
|
5
|
+
import { reportedNote, retainedReport } from "./terminal.js";
|
|
6
|
+
|
|
7
|
+
const TMUX_SESSION = /^[a-f0-9]{16}$/;
|
|
8
|
+
|
|
9
|
+
function bindingDirectory(directory) {
|
|
10
|
+
return join(directory, "state", "sessions");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function bindingPath(directory, tmux) {
|
|
14
|
+
if (!TMUX_SESSION.test(tmux)) throw new KitRefusal("kit_binding_name_invalid", tmux);
|
|
15
|
+
return join(bindingDirectory(directory), `${tmux}.json`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function writeBinding(directory, binding) {
|
|
19
|
+
const reported = binding.reported ?? null;
|
|
20
|
+
await writeNote(bindingPath(directory, binding.tmux), {
|
|
21
|
+
installation: directory,
|
|
22
|
+
run: binding.run,
|
|
23
|
+
session: binding.session,
|
|
24
|
+
tmux: binding.tmux,
|
|
25
|
+
socket: binding.socket,
|
|
26
|
+
nonce: binding.nonce,
|
|
27
|
+
conversation: binding.conversation === true,
|
|
28
|
+
owner_stop: binding.ownerStop ?? null,
|
|
29
|
+
sealed: binding.sealed === null || binding.sealed === undefined
|
|
30
|
+
? null
|
|
31
|
+
: { operation: binding.sealed.operation, blocks: binding.sealed.blocks },
|
|
32
|
+
reported: reported === null ? null : reportedNote(reported),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function removeBinding(directory, tmux) {
|
|
37
|
+
await rm(bindingPath(directory, tmux), { force: true });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function retainedSeal(stored) {
|
|
41
|
+
if (stored === null || stored === undefined) return null;
|
|
42
|
+
if (typeof stored !== "object" || Array.isArray(stored)) return undefined;
|
|
43
|
+
if (typeof stored.operation !== "string" || stored.operation.length === 0) return undefined;
|
|
44
|
+
if (!Array.isArray(stored.blocks) || stored.blocks.length === 0) return undefined;
|
|
45
|
+
return { operation: stored.operation, blocks: stored.blocks };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function completeBinding(directory, stored) {
|
|
49
|
+
if (typeof stored !== "object" || stored === null || Array.isArray(stored)) return null;
|
|
50
|
+
const named = [stored.run, stored.session, stored.tmux, stored.socket, stored.nonce];
|
|
51
|
+
if (named.some((value) => typeof value !== "string" || value.length === 0)) return null;
|
|
52
|
+
if (stored.installation !== directory) return null;
|
|
53
|
+
if (!TMUX_SESSION.test(stored.tmux)) return null;
|
|
54
|
+
if (stored.owner_stop !== null && typeof stored.owner_stop !== "string") return null;
|
|
55
|
+
const sealed = retainedSeal(stored.sealed);
|
|
56
|
+
if (sealed === undefined) return null;
|
|
57
|
+
const reported = retainedReport(stored.reported);
|
|
58
|
+
if (reported === undefined) return null;
|
|
59
|
+
return {
|
|
60
|
+
run: stored.run,
|
|
61
|
+
session: stored.session,
|
|
62
|
+
tmux: stored.tmux,
|
|
63
|
+
socket: stored.socket,
|
|
64
|
+
nonce: stored.nonce,
|
|
65
|
+
conversation: stored.conversation === true,
|
|
66
|
+
ownerStop: stored.owner_stop,
|
|
67
|
+
sealed,
|
|
68
|
+
reported,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function readBindings(directory) {
|
|
73
|
+
let present;
|
|
74
|
+
try {
|
|
75
|
+
present = await readdir(bindingDirectory(directory));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error?.code === "ENOENT") return [];
|
|
78
|
+
throw new KitRefusal("kit_state_unreadable", bindingDirectory(directory));
|
|
79
|
+
}
|
|
80
|
+
const bindings = [];
|
|
81
|
+
for (const name of present.filter((entry) => entry.endsWith(".json")).sort()) {
|
|
82
|
+
const path = join(bindingDirectory(directory), name);
|
|
83
|
+
const stored = await readNote(path, null);
|
|
84
|
+
if (stored === null) continue;
|
|
85
|
+
const binding = completeBinding(directory, stored);
|
|
86
|
+
if (binding === null || `${binding.tmux}.json` !== name) continue;
|
|
87
|
+
bindings.push(binding);
|
|
88
|
+
}
|
|
89
|
+
return bindings;
|
|
90
|
+
}
|