@indigoai-us/hq-cli 5.109.16 → 5.110.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/CHANGELOG.md +28 -0
- package/assets/bot-workers/setup/context/USER-GUIDE.md +363 -0
- package/assets/bot-workers/setup/context/quick-reference.md +199 -0
- package/assets/bot-workers/setup/skills/first-company.md +71 -0
- package/assets/bot-workers/setup/skills/standing-help.md +74 -0
- package/assets/bot-workers/setup/worker.yaml +422 -0
- package/dist/commands/bot-continuity.d.ts +28 -0
- package/dist/commands/bot-continuity.js +68 -0
- package/dist/commands/bot.d.ts +73 -0
- package/dist/commands/bot.js +776 -0
- package/dist/commands/workers.d.ts +2 -14
- package/dist/commands/workers.js +2 -8
- package/dist/lib/bot/api.d.ts +202 -0
- package/dist/lib/bot/api.js +202 -0
- package/dist/lib/bot/company-bind.d.ts +27 -0
- package/dist/lib/bot/company-bind.js +62 -0
- package/dist/lib/bot/config.d.ts +106 -0
- package/dist/lib/bot/config.js +141 -0
- package/dist/lib/bot/continuity-download.d.ts +28 -0
- package/dist/lib/bot/continuity-download.js +75 -0
- package/dist/lib/bot/continuity-install.d.ts +14 -0
- package/dist/lib/bot/continuity-install.js +101 -0
- package/dist/lib/bot/continuity.d.ts +66 -0
- package/dist/lib/bot/continuity.js +301 -0
- package/dist/lib/bot/creds.d.ts +24 -0
- package/dist/lib/bot/creds.js +51 -0
- package/dist/lib/bot/daemon.d.ts +75 -0
- package/dist/lib/bot/daemon.js +316 -0
- package/dist/lib/bot/inbox-state.d.ts +18 -0
- package/dist/lib/bot/inbox-state.js +51 -0
- package/dist/lib/bot/index.d.ts +16 -0
- package/dist/lib/bot/index.js +16 -0
- package/dist/lib/bot/inflight.d.ts +40 -0
- package/dist/lib/bot/inflight.js +44 -0
- package/dist/lib/bot/log.d.ts +13 -0
- package/dist/lib/bot/log.js +59 -0
- package/dist/lib/bot/owner-context.d.ts +75 -0
- package/dist/lib/bot/owner-context.js +151 -0
- package/dist/lib/bot/paths.d.ts +61 -0
- package/dist/lib/bot/paths.js +103 -0
- package/dist/lib/bot/progress.d.ts +84 -0
- package/dist/lib/bot/progress.js +167 -0
- package/dist/lib/bot/promote.d.ts +16 -0
- package/dist/lib/bot/promote.js +106 -0
- package/dist/lib/bot/promotion-hold.d.ts +24 -0
- package/dist/lib/bot/promotion-hold.js +103 -0
- package/dist/lib/bot/promotion-receipt.d.ts +9 -0
- package/dist/lib/bot/promotion-receipt.js +56 -0
- package/dist/lib/bot/promotion-upload.d.ts +16 -0
- package/dist/lib/bot/promotion-upload.js +65 -0
- package/dist/lib/bot/prompt.d.ts +103 -0
- package/dist/lib/bot/prompt.js +329 -0
- package/dist/lib/bot/room-policy.d.ts +53 -0
- package/dist/lib/bot/room-policy.js +73 -0
- package/dist/lib/bot/run.d.ts +98 -0
- package/dist/lib/bot/run.js +787 -0
- package/dist/lib/bot/runtime/claude.d.ts +49 -0
- package/dist/lib/bot/runtime/claude.js +151 -0
- package/dist/lib/bot/runtime/codex.d.ts +28 -0
- package/dist/lib/bot/runtime/codex.js +147 -0
- package/dist/lib/bot/runtime/grok.d.ts +16 -0
- package/dist/lib/bot/runtime/grok.js +67 -0
- package/dist/lib/bot/runtime/index.d.ts +35 -0
- package/dist/lib/bot/runtime/index.js +279 -0
- package/dist/lib/bot/runtime/messages-stream.d.ts +27 -0
- package/dist/lib/bot/runtime/messages-stream.js +85 -0
- package/dist/lib/bot/runtime/types.d.ts +136 -0
- package/dist/lib/bot/runtime/types.js +51 -0
- package/dist/lib/bot/scaffold.d.ts +38 -0
- package/dist/lib/bot/scaffold.js +94 -0
- package/dist/lib/bot/session.d.ts +19 -0
- package/dist/lib/bot/session.js +39 -0
- package/dist/lib/bot/status.d.ts +40 -0
- package/dist/lib/bot/status.js +66 -0
- package/dist/lib/bot/worker-source.d.ts +66 -0
- package/dist/lib/bot/worker-source.js +283 -0
- package/dist/lib/workers-registry/read.d.ts +15 -0
- package/dist/lib/workers-registry/read.js +17 -0
- package/dist/register-all.js +2 -0
- package/package.json +2 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bot.json — non-secret per-bot configuration (local-bots US-003/US-005).
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { botConfigPath } from "./paths.js";
|
|
7
|
+
export const BOT_RUNTIMES = ["claude", "codex", "grok"];
|
|
8
|
+
export function isBotRuntimeId(value) {
|
|
9
|
+
return typeof value === "string" && BOT_RUNTIMES.includes(value);
|
|
10
|
+
}
|
|
11
|
+
/** Longest `intro` (first-start hello DM) a bot may store. */
|
|
12
|
+
export const BOT_INTRO_MAX_CHARS = 500;
|
|
13
|
+
/** Longest `kickoff` (first-start model turn prompt) a bot may store. */
|
|
14
|
+
export const BOT_KICKOFF_MAX_CHARS = 2000;
|
|
15
|
+
/**
|
|
16
|
+
* Where a bot keeps its memory: "synced" = personal/workers/<name>/memory
|
|
17
|
+
* inside the HQ tree (follows the owner across machines); "local" =
|
|
18
|
+
* ~/.hq/bots/<name>/memory, outside the synced tree (this computer only).
|
|
19
|
+
*/
|
|
20
|
+
export const BOT_MEMORY_MODES = ["synced", "local"];
|
|
21
|
+
export function isBotMemoryMode(value) {
|
|
22
|
+
return typeof value === "string" && BOT_MEMORY_MODES.includes(value);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Thinking ("effort") levels each runtime's CLI accepts, verified 2026-09-12:
|
|
26
|
+
* `claude --effort`, `codex -c model_reasoning_effort=…`, `grok --reasoning-effort`.
|
|
27
|
+
*/
|
|
28
|
+
export const BOT_EFFORT_LEVELS = {
|
|
29
|
+
claude: ["low", "medium", "high", "xhigh", "max"],
|
|
30
|
+
codex: ["minimal", "low", "medium", "high", "xhigh"],
|
|
31
|
+
grok: ["low", "medium", "high", "xhigh"],
|
|
32
|
+
};
|
|
33
|
+
/** Every bot thinks at medium unless its owner picks otherwise: quick chat replies. */
|
|
34
|
+
export const DEFAULT_BOT_EFFORT = "medium";
|
|
35
|
+
/** The thinking level a bot actually runs with. */
|
|
36
|
+
export function effectiveBotEffort(config) {
|
|
37
|
+
return config.effort?.trim() || DEFAULT_BOT_EFFORT;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A thinking level for this runtime, or undefined for "default" / empty
|
|
41
|
+
* (the bot then runs at DEFAULT_BOT_EFFORT). Throws an expected error otherwise.
|
|
42
|
+
*/
|
|
43
|
+
export function validateBotEffort(runtime, value) {
|
|
44
|
+
const v = (value ?? "").trim().toLowerCase();
|
|
45
|
+
if (!v || v === "default")
|
|
46
|
+
return undefined;
|
|
47
|
+
const levels = BOT_EFFORT_LEVELS[runtime];
|
|
48
|
+
if (!levels.includes(v)) {
|
|
49
|
+
throw Object.assign(new Error(`--effort for ${runtime} must be one of ${levels.join(", ")} (or default).`), { expected: true });
|
|
50
|
+
}
|
|
51
|
+
return v;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A model id for the runtime CLI, or undefined for "default" / empty (the
|
|
55
|
+
* CLI's own default for the owner's account). Throws an expected error when
|
|
56
|
+
* it cannot be a model id.
|
|
57
|
+
*/
|
|
58
|
+
export function validateBotModel(value) {
|
|
59
|
+
const v = (value ?? "").trim();
|
|
60
|
+
if (!v || v.toLowerCase() === "default")
|
|
61
|
+
return undefined;
|
|
62
|
+
if (v.length > 100 || !/^[A-Za-z0-9][\w.:\[\]/-]*$/.test(v)) {
|
|
63
|
+
throw Object.assign(new Error(`--model "${v.slice(0, 40)}" is not a model id (letters, digits, . _ - : / [ ]).`), { expected: true });
|
|
64
|
+
}
|
|
65
|
+
return v;
|
|
66
|
+
}
|
|
67
|
+
/** Trim and bound the intro text; throws an expected error when unusable. */
|
|
68
|
+
export function validateBotIntro(text) {
|
|
69
|
+
const trimmed = (text ?? "").replace(/\r\n/g, "\n").trim();
|
|
70
|
+
if (!trimmed) {
|
|
71
|
+
throw Object.assign(new Error("--intro needs some text (the hello the bot sends you when it first comes online)."), {
|
|
72
|
+
expected: true,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (trimmed.length > BOT_INTRO_MAX_CHARS) {
|
|
76
|
+
throw Object.assign(new Error(`--intro is too long (${trimmed.length} chars); keep it under ${BOT_INTRO_MAX_CHARS}.`), {
|
|
77
|
+
expected: true,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return trimmed;
|
|
81
|
+
}
|
|
82
|
+
/** Trim and bound the kickoff prompt; throws an expected error when unusable. */
|
|
83
|
+
export function validateBotKickoff(text) {
|
|
84
|
+
const trimmed = (text ?? "").replace(/\r\n/g, "\n").trim();
|
|
85
|
+
if (!trimmed) {
|
|
86
|
+
throw Object.assign(new Error("--kickoff needs some text (the first task the bot starts on, right after its hello)."), {
|
|
87
|
+
expected: true,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (trimmed.length > BOT_KICKOFF_MAX_CHARS) {
|
|
91
|
+
throw Object.assign(new Error(`--kickoff is too long (${trimmed.length} chars); keep it under ${BOT_KICKOFF_MAX_CHARS}.`), {
|
|
92
|
+
expected: true,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return trimmed;
|
|
96
|
+
}
|
|
97
|
+
export function readBotConfig(dir) {
|
|
98
|
+
const p = botConfigPath(dir);
|
|
99
|
+
if (!fs.existsSync(p))
|
|
100
|
+
return null;
|
|
101
|
+
try {
|
|
102
|
+
const raw = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
103
|
+
if (raw.v !== 1 || typeof raw.name !== "string" || typeof raw.agentUid !== "string") {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (!isBotRuntimeId(raw.runtime))
|
|
107
|
+
return null;
|
|
108
|
+
const { intro: rawIntro, kickoff: rawKickoff, ...rest } = raw;
|
|
109
|
+
const intro = typeof rawIntro === "string" && rawIntro.trim() ? rawIntro : undefined;
|
|
110
|
+
const kickoff = typeof rawKickoff === "string" && rawKickoff.trim() ? rawKickoff : undefined;
|
|
111
|
+
return { ...rest, ...(intro ? { intro } : {}), ...(kickoff ? { kickoff } : {}), enabled: raw.enabled !== false };
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function writeBotConfig(dir, config) {
|
|
118
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
119
|
+
const dest = botConfigPath(dir);
|
|
120
|
+
const tmp = path.join(dir, `.bot.tmp.${process.pid}.${Date.now()}`);
|
|
121
|
+
fs.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
122
|
+
fs.renameSync(tmp, dest);
|
|
123
|
+
}
|
|
124
|
+
/** "local" when the memory folder is an absolute path outside the HQ tree. */
|
|
125
|
+
export function botMemoryMode(config) {
|
|
126
|
+
return config.memoryDir && path.isAbsolute(config.memoryDir) ? "local" : "synced";
|
|
127
|
+
}
|
|
128
|
+
/** Absolute memory folder for a bot (synced memory is resolved against hqRoot). */
|
|
129
|
+
export function resolveBotMemoryDir(config) {
|
|
130
|
+
const rel = config.memoryDir?.trim() ? config.memoryDir.replace(/\/+$/, "") : path.join(config.workerDir, "memory");
|
|
131
|
+
return path.isAbsolute(rel) ? rel : path.join(config.hqRoot, rel);
|
|
132
|
+
}
|
|
133
|
+
export function patchBotConfig(dir, patch) {
|
|
134
|
+
const current = readBotConfig(dir);
|
|
135
|
+
if (!current)
|
|
136
|
+
throw new Error(`No bot config at ${botConfigPath(dir)}`);
|
|
137
|
+
const next = { ...current, ...patch, v: 1 };
|
|
138
|
+
writeBotConfig(dir, next);
|
|
139
|
+
return next;
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ensureCognitoIdToken } from "../../utils/cognito-session.js";
|
|
2
|
+
import { vaultApiFetch } from "../../utils/vault-api.js";
|
|
3
|
+
declare function readOwnVault(input: {
|
|
4
|
+
bucket: string;
|
|
5
|
+
key: string;
|
|
6
|
+
credentials: {
|
|
7
|
+
accessKeyId: string;
|
|
8
|
+
secretAccessKey: string;
|
|
9
|
+
sessionToken: string;
|
|
10
|
+
};
|
|
11
|
+
}): Promise<{
|
|
12
|
+
body: ReadableStream<any>;
|
|
13
|
+
close: () => void;
|
|
14
|
+
}>;
|
|
15
|
+
/** The box uses its own HQ machine login; its shared EC2 role gets no vault access. */
|
|
16
|
+
export declare function downloadContinuitySnapshot(input: {
|
|
17
|
+
agentUid: string;
|
|
18
|
+
key: string;
|
|
19
|
+
sha256: string;
|
|
20
|
+
bytes: number;
|
|
21
|
+
destination: string;
|
|
22
|
+
}, deps?: {
|
|
23
|
+
credential: typeof ensureCognitoIdToken;
|
|
24
|
+
api: typeof vaultApiFetch;
|
|
25
|
+
download: typeof readOwnVault;
|
|
26
|
+
}): Promise<void>;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=continuity-download.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { ensureCognitoIdToken } from "../../utils/cognito-session.js";
|
|
5
|
+
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
|
|
6
|
+
import { vaultApiFetch } from "../../utils/vault-api.js";
|
|
7
|
+
async function readOwnVault(input) {
|
|
8
|
+
const client = new S3Client({ region: "us-east-1", credentials: input.credentials });
|
|
9
|
+
try {
|
|
10
|
+
const response = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: input.key }), { abortSignal: AbortSignal.timeout(30000) });
|
|
11
|
+
if (!response.Body)
|
|
12
|
+
throw new Error("Promotion snapshot download failed");
|
|
13
|
+
return { body: response.Body.transformToWebStream(), close: () => client.destroy() };
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
client.destroy();
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** The box uses its own HQ machine login; its shared EC2 role gets no vault access. */
|
|
21
|
+
export async function downloadContinuitySnapshot(input, deps = { credential: ensureCognitoIdToken, api: vaultApiFetch, download: readOwnVault }) {
|
|
22
|
+
if (!/^agt_[0-9A-HJKMNP-TV-Z]{26}$/.test(input.agentUid) || !/^[a-f0-9]{64}$/.test(input.sha256) ||
|
|
23
|
+
!Number.isSafeInteger(input.bytes) || input.bytes < 1 || input.bytes > 48 * 1024 * 1024 ||
|
|
24
|
+
!new RegExp(`^personal/bots/${input.agentUid}/promotions/[A-Za-z0-9_-]{16,128}/${input.sha256}\\.json$`).test(input.key)) {
|
|
25
|
+
throw new Error("Invalid promotion snapshot scope");
|
|
26
|
+
}
|
|
27
|
+
const token = await deps.credential({ interactive: false, tokenSource: "machine" });
|
|
28
|
+
const response = await deps.api({ token, path: "/sts/vend-self", method: "POST",
|
|
29
|
+
body: { personUid: input.agentUid, durationSeconds: 900 }, signal: AbortSignal.timeout(10000) });
|
|
30
|
+
if (!response.ok)
|
|
31
|
+
throw new Error(`Promotion snapshot authorization failed (${response.status})`);
|
|
32
|
+
const payload = await response.json();
|
|
33
|
+
const credentials = payload.credentials;
|
|
34
|
+
if (!credentials || !credentials.accessKeyId || !credentials.secretAccessKey || !credentials.sessionToken)
|
|
35
|
+
throw new Error("Promotion snapshot access was not granted");
|
|
36
|
+
const bucket = `hq-vault-agt-${input.agentUid.slice(4).toLowerCase()}`;
|
|
37
|
+
const download = await deps.download({ bucket, key: input.key, credentials: {
|
|
38
|
+
accessKeyId: credentials.accessKeyId, secretAccessKey: credentials.secretAccessKey, sessionToken: credentials.sessionToken,
|
|
39
|
+
} });
|
|
40
|
+
const chunks = [];
|
|
41
|
+
let count = 0;
|
|
42
|
+
const reader = download.body.getReader();
|
|
43
|
+
try {
|
|
44
|
+
while (true) {
|
|
45
|
+
const next = await reader.read();
|
|
46
|
+
if (next.done)
|
|
47
|
+
break;
|
|
48
|
+
count += next.value.length;
|
|
49
|
+
if (count > input.bytes)
|
|
50
|
+
throw new Error("Promotion snapshot exceeded its expected size");
|
|
51
|
+
chunks.push(Buffer.from(next.value));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
await reader.cancel().catch(() => { });
|
|
56
|
+
download.close();
|
|
57
|
+
}
|
|
58
|
+
const bytes = Buffer.concat(chunks);
|
|
59
|
+
if (bytes.length !== input.bytes || createHash("sha256").update(bytes).digest("hex") !== input.sha256) {
|
|
60
|
+
throw new Error("Promotion snapshot integrity check failed");
|
|
61
|
+
}
|
|
62
|
+
const destination = path.resolve(input.destination);
|
|
63
|
+
if (fs.realpathSync(path.dirname(destination)) !== path.dirname(destination))
|
|
64
|
+
throw new Error("Snapshot directory cannot use symlinks");
|
|
65
|
+
const temporary = `${destination}.${randomUUID()}.tmp`;
|
|
66
|
+
try {
|
|
67
|
+
fs.writeFileSync(temporary, bytes, { mode: 0o600, flag: "wx" });
|
|
68
|
+
fs.renameSync(temporary, destination);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
bytes.fill(0);
|
|
72
|
+
fs.rmSync(temporary, { force: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=continuity-download.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type BotContinuityBundle, type ContinuityTarget } from "./continuity.js";
|
|
2
|
+
export declare function installFleetContinuity(bundle: BotContinuityBundle, target: ContinuityTarget, fleetHome: string): {
|
|
3
|
+
version: number;
|
|
4
|
+
agentUid: string;
|
|
5
|
+
ownerUid: string;
|
|
6
|
+
sourceDigest: string;
|
|
7
|
+
soulSha256: string;
|
|
8
|
+
files: {
|
|
9
|
+
path: string;
|
|
10
|
+
sha256: string;
|
|
11
|
+
sourceSha256: string;
|
|
12
|
+
}[];
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=continuity-install.d.ts.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/** On-box import before the first gateway start. Never replaces an existing bot. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { planFleetContinuity } from "./continuity.js";
|
|
6
|
+
function hash(bytes) { return createHash("sha256").update(bytes).digest("hex"); }
|
|
7
|
+
function directory(root) {
|
|
8
|
+
const absolute = path.resolve(root);
|
|
9
|
+
let cursor = path.parse(absolute).root;
|
|
10
|
+
for (const part of absolute.slice(cursor.length).split(path.sep).filter(Boolean)) {
|
|
11
|
+
cursor = path.join(cursor, part);
|
|
12
|
+
try {
|
|
13
|
+
fs.mkdirSync(cursor, { mode: 0o700 });
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error.code !== "EEXIST")
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
const stat = fs.lstatSync(cursor);
|
|
20
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
21
|
+
throw new Error("Continuity destination must use real directories");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function verify(file, digest, executable = false) {
|
|
25
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
26
|
+
try {
|
|
27
|
+
const stat = fs.fstatSync(fd);
|
|
28
|
+
if (!stat.isFile() || stat.size > 32 * 1024 * 1024 || hash(fs.readFileSync(fd)) !== digest ||
|
|
29
|
+
Boolean(stat.mode & 0o111) !== executable || Boolean(stat.mode & 0o077)) {
|
|
30
|
+
throw new Error("Continuity readback failed");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
fs.closeSync(fd);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function exclusive(file, bytes, executable = false) {
|
|
38
|
+
const fd = fs.openSync(file, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, executable ? 0o700 : 0o600);
|
|
39
|
+
try {
|
|
40
|
+
fs.writeFileSync(fd, bytes);
|
|
41
|
+
fs.fsyncSync(fd);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
fs.closeSync(fd);
|
|
45
|
+
}
|
|
46
|
+
verify(file, hash(bytes), executable);
|
|
47
|
+
}
|
|
48
|
+
export function installFleetContinuity(bundle, target, fleetHome) {
|
|
49
|
+
// Validate every byte, path, tenant and reference before the first mutation.
|
|
50
|
+
const plan = planFleetContinuity(bundle, target);
|
|
51
|
+
if (!path.isAbsolute(fleetHome))
|
|
52
|
+
throw new Error("Fleet home must be absolute");
|
|
53
|
+
const prefix = `personal/bots/${target.agentUid}/continuity/`;
|
|
54
|
+
const destination = path.join(target.hqRoot, prefix);
|
|
55
|
+
const parent = path.dirname(destination);
|
|
56
|
+
const soulPath = path.join(fleetHome, "SOUL.md");
|
|
57
|
+
directory(target.hqRoot);
|
|
58
|
+
directory(parent);
|
|
59
|
+
directory(fleetHome);
|
|
60
|
+
// An existing customized or default persona requires an explicit coordinator
|
|
61
|
+
// decision. Never overwrite it, including when a retry races another import.
|
|
62
|
+
if (fs.existsSync(soulPath) || fs.lstatSync(fleetHome).isSymbolicLink())
|
|
63
|
+
verify(soulPath, plan.soulSha256);
|
|
64
|
+
const receipt = {
|
|
65
|
+
version: 1, agentUid: target.agentUid, ownerUid: target.ownerUid,
|
|
66
|
+
sourceDigest: bundle.digest, soulSha256: plan.soulSha256,
|
|
67
|
+
files: plan.files.map(file => ({ path: file.path, sha256: file.sha256, sourceSha256: file.sourceSha256 })),
|
|
68
|
+
};
|
|
69
|
+
const receiptBytes = JSON.stringify(receipt) + "\n";
|
|
70
|
+
if (!fs.existsSync(destination)) {
|
|
71
|
+
const staging = fs.mkdtempSync(path.join(parent, ".continuity-import-"));
|
|
72
|
+
try {
|
|
73
|
+
directory(path.join(staging, "memory"));
|
|
74
|
+
for (const file of plan.files) {
|
|
75
|
+
const dest = path.join(staging, file.path.slice(prefix.length));
|
|
76
|
+
directory(path.dirname(dest));
|
|
77
|
+
exclusive(dest, file.bytes, file.executable);
|
|
78
|
+
}
|
|
79
|
+
exclusive(path.join(staging, "receipt.json"), receiptBytes);
|
|
80
|
+
// Policy is last: memory cannot become active with unverified imports.
|
|
81
|
+
exclusive(path.join(staging, "memory-policy.json"), plan.memoryPolicy.content);
|
|
82
|
+
fs.renameSync(staging, destination);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
if (fs.existsSync(staging))
|
|
86
|
+
fs.rmSync(staging, { recursive: true });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
directory(destination);
|
|
90
|
+
verify(path.join(destination, "receipt.json"), hash(receiptBytes));
|
|
91
|
+
for (const file of plan.files) {
|
|
92
|
+
directory(path.dirname(path.join(target.hqRoot, file.path)));
|
|
93
|
+
verify(path.join(target.hqRoot, file.path), file.sha256, file.executable);
|
|
94
|
+
}
|
|
95
|
+
verify(path.join(destination, "memory-policy.json"), plan.memoryPolicy.sha256);
|
|
96
|
+
if (!fs.existsSync(soulPath))
|
|
97
|
+
exclusive(soulPath, plan.soul);
|
|
98
|
+
verify(soulPath, plan.soulSha256);
|
|
99
|
+
return receipt;
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=continuity-install.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { BotConfig } from "./config.js";
|
|
2
|
+
export interface ContinuityFile {
|
|
3
|
+
area: "worker" | "memory";
|
|
4
|
+
path: string;
|
|
5
|
+
bytes: number;
|
|
6
|
+
sha256: string;
|
|
7
|
+
executable: boolean;
|
|
8
|
+
base64: string;
|
|
9
|
+
}
|
|
10
|
+
export interface BotContinuityBundle {
|
|
11
|
+
version: 1;
|
|
12
|
+
agentUid: string;
|
|
13
|
+
ownerUid: string;
|
|
14
|
+
name: string;
|
|
15
|
+
companySlug?: string;
|
|
16
|
+
source: {
|
|
17
|
+
hqRoot: string;
|
|
18
|
+
workerDir: string;
|
|
19
|
+
memoryDir: string;
|
|
20
|
+
};
|
|
21
|
+
files: ContinuityFile[];
|
|
22
|
+
digest: string;
|
|
23
|
+
}
|
|
24
|
+
/** Capture original bytes, including supporting scripts and Mac-only memory. */
|
|
25
|
+
export declare function snapshotBotContinuity(config: BotConfig, stateDir: string): BotContinuityBundle;
|
|
26
|
+
/** Validate all bytes and ownership before planning any destination write. */
|
|
27
|
+
export declare function validateBotContinuity(bundle: BotContinuityBundle, identity: {
|
|
28
|
+
agentUid: string;
|
|
29
|
+
ownerUid: string;
|
|
30
|
+
}): void;
|
|
31
|
+
export interface ContinuityImportFile {
|
|
32
|
+
path: string;
|
|
33
|
+
bytes: Buffer;
|
|
34
|
+
executable: boolean;
|
|
35
|
+
sourceSha256: string;
|
|
36
|
+
sha256: string;
|
|
37
|
+
}
|
|
38
|
+
export interface ContinuityTarget {
|
|
39
|
+
agentUid: string;
|
|
40
|
+
ownerUid: string;
|
|
41
|
+
hqRoot: string;
|
|
42
|
+
companySlug?: string;
|
|
43
|
+
}
|
|
44
|
+
/** Produce a reviewable write plan, never mutate a live Fleet installation.
|
|
45
|
+
* Source files remain in the bundle. Only known source roots are relocated;
|
|
46
|
+
* unaccounted Mac paths fail rather than silently producing broken skills.
|
|
47
|
+
*/
|
|
48
|
+
export declare function planBotContinuityImport(bundle: BotContinuityBundle, target: ContinuityTarget): ContinuityImportFile[];
|
|
49
|
+
/** Prepare the existing Fleet SOUL seam before the first gateway session.
|
|
50
|
+
* This is generated from the private continuity copy, not the owner's overlay.
|
|
51
|
+
* Installation, prompt-cache invalidation and gateway activation remain the
|
|
52
|
+
* provisioning coordinator's responsibility; this function performs no writes.
|
|
53
|
+
*/
|
|
54
|
+
export declare function planFleetContinuity(bundle: BotContinuityBundle, target: ContinuityTarget): {
|
|
55
|
+
files: ContinuityImportFile[];
|
|
56
|
+
soul: string;
|
|
57
|
+
soulSha256: string;
|
|
58
|
+
sourceDigest: string;
|
|
59
|
+
/** Install only after all imported file hashes have been verified. */
|
|
60
|
+
memoryPolicy: {
|
|
61
|
+
path: string;
|
|
62
|
+
content: string;
|
|
63
|
+
sha256: string;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=continuity.d.ts.map
|