@indigoai-us/hq-cli 5.109.16 → 5.111.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 +38 -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/skill.d.ts +12 -3
- package/dist/commands/skill.js +79 -3
- 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,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Company bind for worker-sourced bots (local-bots Feature 4).
|
|
3
|
+
*
|
|
4
|
+
* HQ's company hooks key on workspace/sessions/<sid>/meta.yaml `company_slug`,
|
|
5
|
+
* where <sid> is the Claude session id carried in the hook payload. A bot that
|
|
6
|
+
* runs a company worker writes that file for the session id it is about to
|
|
7
|
+
* use, before every turn, so the hooks resolve the company exactly as they do
|
|
8
|
+
* for the owner. Idempotent: an existing file keeps its other keys and only
|
|
9
|
+
* has `company_slug` replaced.
|
|
10
|
+
*
|
|
11
|
+
* Coverage note: Claude bots get the full company hook set (their session id
|
|
12
|
+
* is in the hook payload). Codex/Grok bots only see HQ_SESSION_ID in the env.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
export const defaultCompanyBindFs = {
|
|
17
|
+
existsSync: (p) => fs.existsSync(p),
|
|
18
|
+
readFileSync: (p) => fs.readFileSync(p, "utf8"),
|
|
19
|
+
writeFileSync: (p, body) => fs.writeFileSync(p, body, { mode: 0o644 }),
|
|
20
|
+
mkdirSync: (p) => fs.mkdirSync(p, { recursive: true }),
|
|
21
|
+
};
|
|
22
|
+
const SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
23
|
+
const COMPANY_SLUG_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
24
|
+
export function botSessionMetaRelPath(sessionId) {
|
|
25
|
+
return path.join("workspace", "sessions", sessionId, "meta.yaml");
|
|
26
|
+
}
|
|
27
|
+
export function ensureBotSessionMeta(hqRoot, sessionId, companySlug, io = defaultCompanyBindFs, now = () => new Date()) {
|
|
28
|
+
if (!SESSION_ID_RE.test(sessionId))
|
|
29
|
+
throw new Error(`Invalid session id for company bind: "${sessionId}"`);
|
|
30
|
+
if (!COMPANY_SLUG_RE.test(companySlug))
|
|
31
|
+
throw new Error(`Invalid company slug for company bind: "${companySlug}"`);
|
|
32
|
+
const file = path.join(hqRoot, botSessionMetaRelPath(sessionId));
|
|
33
|
+
io.mkdirSync(path.dirname(file));
|
|
34
|
+
if (!io.existsSync(file)) {
|
|
35
|
+
const body = `session_id: ${sessionId}\nstarted_at: "${now().toISOString()}"\ncompany_slug: ${companySlug}\n`;
|
|
36
|
+
io.writeFileSync(file, body);
|
|
37
|
+
return { path: file, created: true, changed: true };
|
|
38
|
+
}
|
|
39
|
+
const current = io.readFileSync(file);
|
|
40
|
+
const lines = current.split("\n");
|
|
41
|
+
let replaced = false;
|
|
42
|
+
const next = lines.map((line) => {
|
|
43
|
+
if (/^company_slug\s*:/.test(line)) {
|
|
44
|
+
replaced = true;
|
|
45
|
+
return `company_slug: ${companySlug}`;
|
|
46
|
+
}
|
|
47
|
+
return line;
|
|
48
|
+
});
|
|
49
|
+
if (!replaced) {
|
|
50
|
+
while (next.length > 0 && next[next.length - 1] === "")
|
|
51
|
+
next.pop();
|
|
52
|
+
next.push(`company_slug: ${companySlug}`);
|
|
53
|
+
}
|
|
54
|
+
if (!next.some((l) => /^session_id\s*:/.test(l)))
|
|
55
|
+
next.unshift(`session_id: ${sessionId}`);
|
|
56
|
+
const body = `${next.join("\n").replace(/\n+$/, "")}\n`;
|
|
57
|
+
if (body === current)
|
|
58
|
+
return { path: file, created: false, changed: false };
|
|
59
|
+
io.writeFileSync(file, body);
|
|
60
|
+
return { path: file, created: false, changed: true };
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=company-bind.js.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bot.json — non-secret per-bot configuration (local-bots US-003/US-005).
|
|
3
|
+
*/
|
|
4
|
+
export declare const BOT_RUNTIMES: readonly ["claude", "codex", "grok"];
|
|
5
|
+
export type BotRuntimeId = (typeof BOT_RUNTIMES)[number];
|
|
6
|
+
export declare function isBotRuntimeId(value: unknown): value is BotRuntimeId;
|
|
7
|
+
/** Longest `intro` (first-start hello DM) a bot may store. */
|
|
8
|
+
export declare const BOT_INTRO_MAX_CHARS = 500;
|
|
9
|
+
/** Longest `kickoff` (first-start model turn prompt) a bot may store. */
|
|
10
|
+
export declare const BOT_KICKOFF_MAX_CHARS = 2000;
|
|
11
|
+
/**
|
|
12
|
+
* Where a bot keeps its memory: "synced" = personal/workers/<name>/memory
|
|
13
|
+
* inside the HQ tree (follows the owner across machines); "local" =
|
|
14
|
+
* ~/.hq/bots/<name>/memory, outside the synced tree (this computer only).
|
|
15
|
+
*/
|
|
16
|
+
export declare const BOT_MEMORY_MODES: readonly ["synced", "local"];
|
|
17
|
+
export type BotMemoryMode = (typeof BOT_MEMORY_MODES)[number];
|
|
18
|
+
export declare function isBotMemoryMode(value: unknown): value is BotMemoryMode;
|
|
19
|
+
/**
|
|
20
|
+
* Thinking ("effort") levels each runtime's CLI accepts, verified 2026-09-12:
|
|
21
|
+
* `claude --effort`, `codex -c model_reasoning_effort=…`, `grok --reasoning-effort`.
|
|
22
|
+
*/
|
|
23
|
+
export declare const BOT_EFFORT_LEVELS: Record<BotRuntimeId, readonly string[]>;
|
|
24
|
+
/** Every bot thinks at medium unless its owner picks otherwise: quick chat replies. */
|
|
25
|
+
export declare const DEFAULT_BOT_EFFORT = "medium";
|
|
26
|
+
/** The thinking level a bot actually runs with. */
|
|
27
|
+
export declare function effectiveBotEffort(config: Pick<BotConfig, "effort">): string;
|
|
28
|
+
/**
|
|
29
|
+
* A thinking level for this runtime, or undefined for "default" / empty
|
|
30
|
+
* (the bot then runs at DEFAULT_BOT_EFFORT). Throws an expected error otherwise.
|
|
31
|
+
*/
|
|
32
|
+
export declare function validateBotEffort(runtime: BotRuntimeId, value: string): string | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* A model id for the runtime CLI, or undefined for "default" / empty (the
|
|
35
|
+
* CLI's own default for the owner's account). Throws an expected error when
|
|
36
|
+
* it cannot be a model id.
|
|
37
|
+
*/
|
|
38
|
+
export declare function validateBotModel(value: string): string | undefined;
|
|
39
|
+
/** Trim and bound the intro text; throws an expected error when unusable. */
|
|
40
|
+
export declare function validateBotIntro(text: string): string;
|
|
41
|
+
/** Trim and bound the kickoff prompt; throws an expected error when unusable. */
|
|
42
|
+
export declare function validateBotKickoff(text: string): string;
|
|
43
|
+
export interface BotConfig {
|
|
44
|
+
v: 1;
|
|
45
|
+
name: string;
|
|
46
|
+
/** The bot's own agt_ identity (owned by `ownerUid`). */
|
|
47
|
+
agentUid: string;
|
|
48
|
+
/** The person (prs_) who created and owns the bot. */
|
|
49
|
+
ownerUid: string;
|
|
50
|
+
runtime: BotRuntimeId;
|
|
51
|
+
/** Optional model override handed to the runtime CLI (unset = the CLI's own default). */
|
|
52
|
+
model?: string;
|
|
53
|
+
/** Thinking level for the runtime CLI (see BOT_EFFORT_LEVELS; unset = DEFAULT_BOT_EFFORT). */
|
|
54
|
+
effort?: string;
|
|
55
|
+
/** HQ root the session starts in (and the cwd of every model turn). */
|
|
56
|
+
hqRoot: string;
|
|
57
|
+
/**
|
|
58
|
+
* Run the model with every permission pre-approved (default true): a bot
|
|
59
|
+
* answers DMs headlessly and cannot show an approval prompt, so a gated
|
|
60
|
+
* command simply fails. Set false to keep the runtime's normal gates.
|
|
61
|
+
*/
|
|
62
|
+
autoApprove?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Worker folder relative to hqRoot: personal/workers/<name> for a scaffolded
|
|
65
|
+
* bot, or companies/<co>/workers/<id> when the bot runs a company worker.
|
|
66
|
+
*/
|
|
67
|
+
workerDir: string;
|
|
68
|
+
/** Where the worker definition came from (default "scaffold"). */
|
|
69
|
+
workerSource?: "scaffold" | "worker";
|
|
70
|
+
/** Registry id of the company worker this bot runs (workerSource "worker"). */
|
|
71
|
+
workerId?: string;
|
|
72
|
+
/** Company the bot is bound to on every turn (workerSource "worker"). */
|
|
73
|
+
companySlug?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Memory folder: hqRoot-relative for "synced" memory (default
|
|
76
|
+
* `${workerDir}/memory`; a worker-sourced bot keeps its own memory in
|
|
77
|
+
* personal/workers/<name>/memory so the shared company tree is never
|
|
78
|
+
* written to), or an absolute path for "local" memory that lives outside
|
|
79
|
+
* the synced tree (~/.hq/bots/<name>/memory).
|
|
80
|
+
*/
|
|
81
|
+
memoryDir?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Verbatim hello the bot DMs its owner when it first comes online (and on
|
|
84
|
+
* `hq bot intro`). Unset = the default introduction.
|
|
85
|
+
*/
|
|
86
|
+
intro?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Prompt for ONE model turn the bot runs on its first start, right after the
|
|
89
|
+
* intro DM, as if the owner had sent it (same session, system prompt and
|
|
90
|
+
* permissions); the answer is DMed to the owner. Runs once, never on restart.
|
|
91
|
+
*/
|
|
92
|
+
kickoff?: string;
|
|
93
|
+
createdAt: string;
|
|
94
|
+
/** When `false` the run loop exits 0 immediately (launchd does not respawn). */
|
|
95
|
+
enabled: boolean;
|
|
96
|
+
/** Set once the bot has introduced itself to the owner (US-008 intro). */
|
|
97
|
+
introSentAt?: string;
|
|
98
|
+
}
|
|
99
|
+
export declare function readBotConfig(dir: string): BotConfig | null;
|
|
100
|
+
export declare function writeBotConfig(dir: string, config: BotConfig): void;
|
|
101
|
+
/** "local" when the memory folder is an absolute path outside the HQ tree. */
|
|
102
|
+
export declare function botMemoryMode(config: Pick<BotConfig, "memoryDir">): BotMemoryMode;
|
|
103
|
+
/** Absolute memory folder for a bot (synced memory is resolved against hqRoot). */
|
|
104
|
+
export declare function resolveBotMemoryDir(config: Pick<BotConfig, "hqRoot" | "workerDir" | "memoryDir">): string;
|
|
105
|
+
export declare function patchBotConfig(dir: string, patch: Partial<BotConfig>): BotConfig;
|
|
106
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -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
|