@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,106 @@
|
|
|
1
|
+
import { recordActivePromotion } from "./promotion-receipt.js";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { botCredsPath } from "./paths.js";
|
|
6
|
+
import { snapshotBotContinuity, validateBotContinuity } from "./continuity.js";
|
|
7
|
+
import { attestPromotionDrain, holdForPromotion, waitForPromotionDrain } from "./promotion-hold.js";
|
|
8
|
+
import { uploadLocalBotPromotion } from "./promotion-upload.js";
|
|
9
|
+
/** Private bounded reads; never expose parser errors containing credential bytes. */
|
|
10
|
+
function readPrivateJson(file, limit) {
|
|
11
|
+
if (fs.realpathSync(file) !== path.resolve(file))
|
|
12
|
+
throw new Error("Promotion files cannot use symlinks");
|
|
13
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
14
|
+
try {
|
|
15
|
+
const stat = fs.fstatSync(fd);
|
|
16
|
+
if (!stat.isFile() || stat.size > limit || (stat.mode & 0o077))
|
|
17
|
+
throw new Error("Promotion requires a bounded private regular file");
|
|
18
|
+
const bytes = Buffer.alloc(stat.size);
|
|
19
|
+
let offset = 0;
|
|
20
|
+
while (offset < bytes.length) {
|
|
21
|
+
const n = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
22
|
+
if (!n)
|
|
23
|
+
throw new Error("Promotion file changed");
|
|
24
|
+
offset += n;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(bytes.toString("utf8"));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new Error("Promotion file contains invalid JSON");
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
bytes.fill(0);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
fs.closeSync(fd);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** One recoverable local-to-cloud step, used by CLI and desktop. Never kills the
|
|
41
|
+
* local runtime, recopies provider login files, or changes another bot.
|
|
42
|
+
*/
|
|
43
|
+
export async function promoteLocalBot(input) {
|
|
44
|
+
const { dir, config, companyUid, api } = input;
|
|
45
|
+
const existing = await api.promotionStatus(config.agentUid, companyUid);
|
|
46
|
+
const holdFile = path.join(dir, "promotion-hold.json");
|
|
47
|
+
const localHold = fs.existsSync(holdFile) ? readPrivateJson(holdFile, 2048) : null;
|
|
48
|
+
const operationId = existing?.operationId ?? localHold?.operationId ?? randomUUID();
|
|
49
|
+
if (!/^[A-Za-z0-9_-]{16,128}$/.test(operationId))
|
|
50
|
+
throw new Error("Invalid saved promotion operation");
|
|
51
|
+
const hold = { agentUid: config.agentUid, ownerUid: config.ownerUid, companyUid, operationId };
|
|
52
|
+
if (existing && (existing.agentUid !== config.agentUid || existing.ownerUid !== config.ownerUid || existing.companyUid !== companyUid))
|
|
53
|
+
throw new Error("Promotion belongs to another bot or destination");
|
|
54
|
+
if (existing?.phase === "cancelled")
|
|
55
|
+
throw new Error("This promotion was cancelled");
|
|
56
|
+
holdForPromotion(dir, hold);
|
|
57
|
+
if (existing?.phase === "active") {
|
|
58
|
+
recordActivePromotion(dir, hold, existing);
|
|
59
|
+
return { promotion: existing, pending: false };
|
|
60
|
+
}
|
|
61
|
+
await waitForPromotionDrain(dir, input.drainTimeoutMs ?? 60_000);
|
|
62
|
+
const file = path.join(dir, `promotion-snapshot-${operationId}.json`);
|
|
63
|
+
if (!fs.existsSync(file)) {
|
|
64
|
+
if (existing)
|
|
65
|
+
throw new Error("The original frozen snapshot is missing. Restore it before retrying.");
|
|
66
|
+
const snapshot = snapshotBotContinuity(config, dir);
|
|
67
|
+
const temp = `${file}.${randomUUID()}.tmp`;
|
|
68
|
+
fs.writeFileSync(temp, JSON.stringify(snapshot), { mode: 0o600, flag: "wx" });
|
|
69
|
+
try {
|
|
70
|
+
fs.linkSync(temp, file);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error.code !== "EEXIST")
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
fs.unlinkSync(temp);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const bundle = readPrivateJson(file, 48 * 1024 * 1024);
|
|
81
|
+
validateBotContinuity(bundle, { agentUid: config.agentUid, ownerUid: config.ownerUid });
|
|
82
|
+
if (existing && existing.snapshotSha256 !== bundle.digest)
|
|
83
|
+
throw new Error("Frozen snapshot does not match the cloud operation");
|
|
84
|
+
let state = existing;
|
|
85
|
+
if (!state || state.phase === "preparing") {
|
|
86
|
+
const machine = readPrivateJson(botCredsPath(dir), 8192);
|
|
87
|
+
if (!machine || typeof machine !== "object" || typeof machine.secret !== "string")
|
|
88
|
+
throw new Error("Invalid bot machine credential");
|
|
89
|
+
try {
|
|
90
|
+
state = await uploadLocalBotPromotion({ api, bundle, machine, companyUid, operationId });
|
|
91
|
+
const proof = await attestPromotionDrain(dir, hold, bundle.digest, machine);
|
|
92
|
+
state = await api.confirmPromotionStopped(config.agentUid, { companyUid, operationId, proof });
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
machine.secret = "";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (state.phase === "local-stopped" || state.phase === "imported" || state.phase === "cloud-ready") {
|
|
99
|
+
const result = await api.resumePromotion(config.agentUid, { companyUid, operationId });
|
|
100
|
+
if (result.promotion.phase === "active")
|
|
101
|
+
recordActivePromotion(dir, hold, result.promotion);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
return { promotion: state, pending: state.phase !== "active" };
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=promote.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BotMachineCreds } from "./creds.js";
|
|
2
|
+
/** Even a malformed marker stops startup; never silently discard a handoff. */
|
|
3
|
+
export declare function hasPromotionHold(dir: string): boolean;
|
|
4
|
+
export interface LocalPromotionHold {
|
|
5
|
+
agentUid: string;
|
|
6
|
+
ownerUid: string;
|
|
7
|
+
companyUid: string;
|
|
8
|
+
operationId: string;
|
|
9
|
+
}
|
|
10
|
+
/** Durable, exclusive intent. The running bot observes it without signalling a
|
|
11
|
+
* possibly reused PID; it finishes its current reply/ACK and exits cleanly.
|
|
12
|
+
*/
|
|
13
|
+
export declare function holdForPromotion(dir: string, hold: LocalPromotionHold): void;
|
|
14
|
+
/** A timeout leaves the hold intact and never aborts the model turn. */
|
|
15
|
+
export declare function waitForPromotionDrain(dir: string, timeoutMs?: number): Promise<void>;
|
|
16
|
+
/** Called after freezing/uploading the snapshot; rechecks the durable hold and
|
|
17
|
+
* exited process immediately before authorizing the server handoff boundary.
|
|
18
|
+
*/
|
|
19
|
+
export declare function attestPromotionDrain(dir: string, hold: LocalPromotionHold, snapshotSha256: string, machine: BotMachineCreds): Promise<string>;
|
|
20
|
+
/** Display-only summary. Any unreadable or mismatched hold still blocks local controls. */
|
|
21
|
+
export declare function promotionHoldSummary(dir: string, agentUid: string, ownerUid: string): {
|
|
22
|
+
companyUid: string | null;
|
|
23
|
+
} | null;
|
|
24
|
+
//# sourceMappingURL=promotion-hold.d.ts.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { createHmac } from "node:crypto";
|
|
4
|
+
import { readPidLock } from "../mesh/live/daemon/pid-lock.js";
|
|
5
|
+
import { daemonPidPath } from "../mesh/live/daemon/paths.js";
|
|
6
|
+
import { isPidAlive, readBotStatus } from "./status.js";
|
|
7
|
+
import { botInflightPath } from "./inflight.js";
|
|
8
|
+
const holdPath = (dir) => path.join(dir, "promotion-hold.json");
|
|
9
|
+
/** Even a malformed marker stops startup; never silently discard a handoff. */
|
|
10
|
+
export function hasPromotionHold(dir) {
|
|
11
|
+
try {
|
|
12
|
+
fs.lstatSync(holdPath(dir));
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error.code === "ENOENT")
|
|
17
|
+
return false;
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Durable, exclusive intent. The running bot observes it without signalling a
|
|
22
|
+
* possibly reused PID; it finishes its current reply/ACK and exits cleanly.
|
|
23
|
+
*/
|
|
24
|
+
export function holdForPromotion(dir, hold) {
|
|
25
|
+
if (!/^agt_[A-Za-z0-9]+$/.test(hold.agentUid) || !/^prs_[A-Za-z0-9]+$/.test(hold.ownerUid) ||
|
|
26
|
+
!/^cmp_[A-Za-z0-9]+$/.test(hold.companyUid) || !/^[A-Za-z0-9_-]{16,128}$/.test(hold.operationId))
|
|
27
|
+
throw new Error("Invalid local promotion operation");
|
|
28
|
+
const serialized = JSON.stringify(hold);
|
|
29
|
+
try {
|
|
30
|
+
fs.writeFileSync(holdPath(dir), serialized, { flag: "wx", mode: 0o600 });
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code !== "EEXIST")
|
|
34
|
+
throw error;
|
|
35
|
+
const stat = fs.lstatSync(holdPath(dir));
|
|
36
|
+
if (!stat.isFile() || stat.size > 2048 || fs.readFileSync(holdPath(dir), "utf8") !== serialized)
|
|
37
|
+
throw new Error("Another promotion owns this local bot");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** A timeout leaves the hold intact and never aborts the model turn. */
|
|
41
|
+
export async function waitForPromotionDrain(dir, timeoutMs = 60_000) {
|
|
42
|
+
if (!hasPromotionHold(dir))
|
|
43
|
+
throw new Error("Promotion hold is missing");
|
|
44
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0 || timeoutMs > 300_000)
|
|
45
|
+
throw new Error("Invalid drain timeout");
|
|
46
|
+
const deadline = Date.now() + timeoutMs;
|
|
47
|
+
do {
|
|
48
|
+
const lock = readPidLock(daemonPidPath(dir));
|
|
49
|
+
if (!lock && fs.existsSync(daemonPidPath(dir)))
|
|
50
|
+
throw new Error("The local process lock could not be verified");
|
|
51
|
+
const status = readBotStatus(dir);
|
|
52
|
+
const alive = (lock && isPidAlive(lock.pid)) || (status && isPidAlive(status.pid));
|
|
53
|
+
if (!alive) {
|
|
54
|
+
if (fs.existsSync(botInflightPath(dir)))
|
|
55
|
+
throw new Error("The local bot has an unfinished reply. Recover it before promotion.");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!status?.canDrainForPromotion)
|
|
59
|
+
throw new Error("Update the local bot before promoting it");
|
|
60
|
+
if (Date.now() >= deadline)
|
|
61
|
+
break;
|
|
62
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(250, deadline - Date.now())));
|
|
63
|
+
} while (Date.now() <= deadline);
|
|
64
|
+
throw new Error("The bot is finishing its current reply. Retry promotion when it has stopped.");
|
|
65
|
+
}
|
|
66
|
+
/** Called after freezing/uploading the snapshot; rechecks the durable hold and
|
|
67
|
+
* exited process immediately before authorizing the server handoff boundary.
|
|
68
|
+
*/
|
|
69
|
+
export async function attestPromotionDrain(dir, hold, snapshotSha256, machine) {
|
|
70
|
+
if (!/^[a-f0-9]{64}$/.test(snapshotSha256) || machine.entityType !== "agent" || machine.entityUid !== hold.agentUid ||
|
|
71
|
+
machine.username !== `agt-${hold.agentUid.slice(4).toLowerCase()}@agents.getindigo.ai` ||
|
|
72
|
+
typeof machine.secret !== "string" || machine.secret.length < 32 || machine.secret.length > 256)
|
|
73
|
+
throw new Error("Invalid promotion signing identity");
|
|
74
|
+
holdForPromotion(dir, hold);
|
|
75
|
+
await waitForPromotionDrain(dir, 0);
|
|
76
|
+
return createHmac("sha256", machine.secret).update(JSON.stringify(["hq-local-bot-drained-v1", hold.agentUid, hold.ownerUid, hold.companyUid, hold.operationId, snapshotSha256])).digest("hex");
|
|
77
|
+
}
|
|
78
|
+
/** Display-only summary. Any unreadable or mismatched hold still blocks local controls. */
|
|
79
|
+
export function promotionHoldSummary(dir, agentUid, ownerUid) {
|
|
80
|
+
if (!hasPromotionHold(dir))
|
|
81
|
+
return null;
|
|
82
|
+
let fd;
|
|
83
|
+
try {
|
|
84
|
+
fd = fs.openSync(holdPath(dir), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
85
|
+
const stat = fs.fstatSync(fd);
|
|
86
|
+
if (!stat.isFile() || stat.size > 2048)
|
|
87
|
+
return { companyUid: null };
|
|
88
|
+
const bytes = Buffer.alloc(stat.size);
|
|
89
|
+
if (fs.readSync(fd, bytes, 0, bytes.length, 0) !== bytes.length)
|
|
90
|
+
return { companyUid: null };
|
|
91
|
+
const hold = JSON.parse(bytes.toString("utf8"));
|
|
92
|
+
return { companyUid: hold?.agentUid === agentUid && hold?.ownerUid === ownerUid &&
|
|
93
|
+
typeof hold.companyUid === "string" && /^cmp_[A-Za-z0-9]+$/.test(hold.companyUid) ? hold.companyUid : null };
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return { companyUid: null };
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
if (fd !== undefined)
|
|
100
|
+
fs.closeSync(fd);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=promotion-hold.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BotPromotionState } from "./api.js";
|
|
2
|
+
import { type LocalPromotionHold } from "./promotion-hold.js";
|
|
3
|
+
/** Cache only the authenticated server's terminal response. The local hold is
|
|
4
|
+
* permanent until an explicit rollback; this receipt controls presentation only.
|
|
5
|
+
*/
|
|
6
|
+
export declare function recordActivePromotion(dir: string, hold: LocalPromotionHold, state: BotPromotionState): void;
|
|
7
|
+
/** Invalid receipts never cause a local bot to be presented as cloud-hosted. */
|
|
8
|
+
export declare function hasActivePromotion(dir: string, agentUid: string, ownerUid: string): boolean;
|
|
9
|
+
//# sourceMappingURL=promotion-receipt.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { holdForPromotion } from "./promotion-hold.js";
|
|
5
|
+
const receiptPath = (dir) => path.join(dir, "promotion-active.json");
|
|
6
|
+
/** Cache only the authenticated server's terminal response. The local hold is
|
|
7
|
+
* permanent until an explicit rollback; this receipt controls presentation only.
|
|
8
|
+
*/
|
|
9
|
+
export function recordActivePromotion(dir, hold, state) {
|
|
10
|
+
if (state.phase !== "active" || !Number.isInteger(state.version) || state.version < 1 ||
|
|
11
|
+
!/^[a-f0-9]{64}$/.test(state.snapshotSha256) ||
|
|
12
|
+
Object.entries(hold).some(([key, value]) => state[key] !== value)) {
|
|
13
|
+
throw new Error("Cloud activation does not match this local bot promotion");
|
|
14
|
+
}
|
|
15
|
+
holdForPromotion(dir, hold);
|
|
16
|
+
const value = { ...hold, phase: "active", version: state.version, snapshotSha256: state.snapshotSha256 };
|
|
17
|
+
const temp = `${receiptPath(dir)}.${randomUUID()}.tmp`;
|
|
18
|
+
fs.writeFileSync(temp, JSON.stringify(value), { flag: "wx", mode: 0o600 });
|
|
19
|
+
try {
|
|
20
|
+
fs.renameSync(temp, receiptPath(dir));
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
fs.rmSync(temp, { force: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Invalid receipts never cause a local bot to be presented as cloud-hosted. */
|
|
27
|
+
export function hasActivePromotion(dir, agentUid, ownerUid) {
|
|
28
|
+
let fd;
|
|
29
|
+
try {
|
|
30
|
+
fd = fs.openSync(receiptPath(dir), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
31
|
+
const stat = fs.fstatSync(fd);
|
|
32
|
+
if (!stat.isFile() || stat.size > 2048 || (stat.mode & 0o077))
|
|
33
|
+
return false;
|
|
34
|
+
const bytes = Buffer.alloc(stat.size);
|
|
35
|
+
if (fs.readSync(fd, bytes, 0, bytes.length, 0) !== bytes.length)
|
|
36
|
+
return false;
|
|
37
|
+
const state = JSON.parse(bytes.toString("utf8"));
|
|
38
|
+
if (state?.agentUid !== agentUid || state?.ownerUid !== ownerUid || state?.phase !== "active" ||
|
|
39
|
+
!Number.isInteger(state.version) || state.version < 1 || !/^[a-f0-9]{64}$/.test(state.snapshotSha256))
|
|
40
|
+
return false;
|
|
41
|
+
const holdFile = path.join(dir, "promotion-hold.json");
|
|
42
|
+
const holdStat = fs.lstatSync(holdFile);
|
|
43
|
+
if (!holdStat.isFile() || holdStat.size > 2048)
|
|
44
|
+
return false;
|
|
45
|
+
const hold = JSON.parse(fs.readFileSync(holdFile, "utf8"));
|
|
46
|
+
return ["agentUid", "ownerUid", "companyUid", "operationId"].every(key => typeof state[key] === "string" && state[key] === hold[key]);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
if (fd !== undefined)
|
|
53
|
+
fs.closeSync(fd);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=promotion-receipt.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BotMachineCreds } from "./creds.js";
|
|
2
|
+
import { BotApi, type BotPromotionState } from "./api.js";
|
|
3
|
+
import { type BotContinuityBundle } from "./continuity.js";
|
|
4
|
+
/** Upload a frozen local snapshot plus ONLY its HQ machine credential. The
|
|
5
|
+
* caller owns draining and a durable local operation lock across this method.
|
|
6
|
+
* No model-provider auth files are read or accepted. No runtime is activated.
|
|
7
|
+
*/
|
|
8
|
+
export declare function uploadLocalBotPromotion(input: {
|
|
9
|
+
api: BotApi;
|
|
10
|
+
bundle: BotContinuityBundle;
|
|
11
|
+
machine: BotMachineCreds;
|
|
12
|
+
companyUid: string;
|
|
13
|
+
operationId: string;
|
|
14
|
+
fetchImpl?: typeof fetch;
|
|
15
|
+
}): Promise<BotPromotionState>;
|
|
16
|
+
//# sourceMappingURL=promotion-upload.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { validateBotContinuity } from "./continuity.js";
|
|
3
|
+
function sha(bytes) { return createHash("sha256").update(bytes).digest("hex"); }
|
|
4
|
+
function assertState(state, bundle, companyUid, operationId) {
|
|
5
|
+
if (!state || state.agentUid !== bundle.agentUid || state.ownerUid !== bundle.ownerUid ||
|
|
6
|
+
state.companyUid !== companyUid || state.operationId !== operationId || state.snapshotSha256 !== bundle.digest ||
|
|
7
|
+
state.phase !== "preparing")
|
|
8
|
+
throw new Error("Promotion preparation returned a different identity or phase");
|
|
9
|
+
}
|
|
10
|
+
/** Only the presigned object request receives these bytes. Owner authorization
|
|
11
|
+
* is handled by BotApi and is never forwarded to S3 or a redirect destination.
|
|
12
|
+
*/
|
|
13
|
+
async function uploadObject(upload, bytes, fetchImpl) {
|
|
14
|
+
const url = new URL(upload.url);
|
|
15
|
+
if (url.protocol !== "https:" || url.username || url.password || url.port ||
|
|
16
|
+
!/^[a-z0-9.-]+\.s3(?:[.-][a-z0-9-]+)?\.amazonaws\.com$/.test(url.hostname) ||
|
|
17
|
+
upload.method !== "PUT" || upload.bytes !== bytes.length || upload.sha256 !== sha(bytes) ||
|
|
18
|
+
upload.headers["x-amz-server-side-encryption"] !== "aws:kms" ||
|
|
19
|
+
!upload.headers["x-amz-server-side-encryption-aws-kms-key-id"] ||
|
|
20
|
+
upload.headers["if-none-match"] !== "*" ||
|
|
21
|
+
upload.headers["x-amz-checksum-sha256"] !== Buffer.from(upload.sha256, "hex").toString("base64") ||
|
|
22
|
+
Object.keys(upload.headers).some(key => /^(authorization|cookie)$/i.test(key))) {
|
|
23
|
+
throw new Error("Invalid private promotion upload destination");
|
|
24
|
+
}
|
|
25
|
+
const response = await fetchImpl(url, { method: "PUT", headers: upload.headers,
|
|
26
|
+
body: new Uint8Array(bytes), redirect: "error", signal: AbortSignal.timeout(60_000) });
|
|
27
|
+
// The key is immutable and content-addressed. A repeat PUT cannot replace it;
|
|
28
|
+
// the server's checksum/KMS readback still verifies it before any import.
|
|
29
|
+
if (!response.ok && response.status !== 412)
|
|
30
|
+
throw new Error(`Promotion upload failed (${response.status})`);
|
|
31
|
+
await response.body?.cancel();
|
|
32
|
+
}
|
|
33
|
+
/** Upload a frozen local snapshot plus ONLY its HQ machine credential. The
|
|
34
|
+
* caller owns draining and a durable local operation lock across this method.
|
|
35
|
+
* No model-provider auth files are read or accepted. No runtime is activated.
|
|
36
|
+
*/
|
|
37
|
+
export async function uploadLocalBotPromotion(input) {
|
|
38
|
+
const { api, bundle, machine, companyUid, operationId } = input;
|
|
39
|
+
validateBotContinuity(bundle, { agentUid: bundle.agentUid, ownerUid: bundle.ownerUid });
|
|
40
|
+
if (!/^cmp_[A-Za-z0-9]+$/.test(companyUid) || !/^[A-Za-z0-9_-]{16,128}$/.test(operationId) ||
|
|
41
|
+
machine.entityType !== "agent" || machine.entityUid !== bundle.agentUid ||
|
|
42
|
+
machine.username !== `agt-${bundle.agentUid.slice(4).toLowerCase()}@agents.getindigo.ai` ||
|
|
43
|
+
typeof machine.secret !== "string" || machine.secret.length < 32 || machine.secret.length > 256) {
|
|
44
|
+
throw new Error("Invalid bot promotion identity");
|
|
45
|
+
}
|
|
46
|
+
const snapshot = Buffer.from(JSON.stringify(bundle));
|
|
47
|
+
const credential = Buffer.from(JSON.stringify({ kind: "hq-bot-machine-credential-v1", agentUid: bundle.agentUid,
|
|
48
|
+
ownerUid: bundle.ownerUid, digest: bundle.digest, operationId, username: machine.username, secret: machine.secret }));
|
|
49
|
+
try {
|
|
50
|
+
let state = await api.preparePromotion(bundle.agentUid, { companyUid, operationId, snapshotSha256: bundle.digest });
|
|
51
|
+
assertState(state, bundle, companyUid, operationId);
|
|
52
|
+
for (const [kind, bytes] of [["snapshot", snapshot], ["machine-credential", credential]]) {
|
|
53
|
+
const signed = await api.promotionUpload(bundle.agentUid, { companyUid, operationId, kind, bytes: bytes.length, sha256: sha(bytes) });
|
|
54
|
+
assertState(signed.promotion, bundle, companyUid, operationId);
|
|
55
|
+
await uploadObject(signed.upload, bytes, input.fetchImpl ?? fetch);
|
|
56
|
+
state = signed.promotion;
|
|
57
|
+
}
|
|
58
|
+
return state;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
credential.fill(0);
|
|
62
|
+
snapshot.fill(0);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=promotion-upload.js.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System context for the bot's model session (local-bots US-003/US-006, Feature 4).
|
|
3
|
+
*
|
|
4
|
+
* Scaffolded bot — assembled from the bot's own worker folder under the HQ root:
|
|
5
|
+
* personal/workers/<name>/worker.yaml identity + capabilities
|
|
6
|
+
* personal/workers/<name>/persona.md voice and standing instructions
|
|
7
|
+
* personal/workers/<name>/memory/*.md durable memory the bot may edit
|
|
8
|
+
*
|
|
9
|
+
* Worker-sourced bot (workerSource "worker") — the bot runs a company worker:
|
|
10
|
+
* companies/<co>/workers/<id>/worker.yaml parsed: instructions, identity.persona,
|
|
11
|
+
* skills (→ skills/<name>.md | skills/<name>/SKILL.md),
|
|
12
|
+
* context.base (inlined under a budget)
|
|
13
|
+
* companies/<co>/policies/*.md listed as paths (hard rules; read before acting)
|
|
14
|
+
* <memoryDir>/*.md the bot's own memory (personal/workers/<bot>/memory,
|
|
15
|
+
* or an absolute ~/.hq/bots/<bot>/memory for local memory)
|
|
16
|
+
*
|
|
17
|
+
* Pure over an injected reader so tests never touch the real HQ.
|
|
18
|
+
*/
|
|
19
|
+
export declare const MEMORY_FILE_BUDGET_BYTES: number;
|
|
20
|
+
export declare const MEMORY_TOTAL_BUDGET_BYTES: number;
|
|
21
|
+
export declare const CONTEXT_FILE_BUDGET_BYTES: number;
|
|
22
|
+
export declare const CONTEXT_TOTAL_BUDGET_BYTES: number;
|
|
23
|
+
export interface WorkerSkillSource {
|
|
24
|
+
name: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
/** hqRoot-relative path of the skill body, when one exists on disk. */
|
|
27
|
+
file?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface WorkerContextFile {
|
|
30
|
+
/** hqRoot-relative path. */
|
|
31
|
+
path: string;
|
|
32
|
+
body: string;
|
|
33
|
+
truncated?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface WorkerPromptSource {
|
|
36
|
+
id?: string;
|
|
37
|
+
name?: string;
|
|
38
|
+
company?: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
instructions?: string;
|
|
41
|
+
persona?: string;
|
|
42
|
+
skills: WorkerSkillSource[];
|
|
43
|
+
/** context.base files inlined (within budget). */
|
|
44
|
+
contextFiles: WorkerContextFile[];
|
|
45
|
+
/** context.base entries that could not be inlined (over budget / unreadable). */
|
|
46
|
+
contextPaths: string[];
|
|
47
|
+
/** companies/<co>/policies/*.md, hqRoot-relative. */
|
|
48
|
+
policyPaths: string[];
|
|
49
|
+
}
|
|
50
|
+
export interface PromptSources {
|
|
51
|
+
workerYaml?: string;
|
|
52
|
+
persona?: string;
|
|
53
|
+
memory: Array<{
|
|
54
|
+
name: string;
|
|
55
|
+
body: string;
|
|
56
|
+
}>;
|
|
57
|
+
/** Present in worker mode only. */
|
|
58
|
+
worker?: WorkerPromptSource;
|
|
59
|
+
}
|
|
60
|
+
export interface PromptFs {
|
|
61
|
+
existsSync: (p: string) => boolean;
|
|
62
|
+
readFileSync: (p: string) => string;
|
|
63
|
+
readdirSync: (p: string) => string[];
|
|
64
|
+
}
|
|
65
|
+
export declare const defaultPromptFs: PromptFs;
|
|
66
|
+
export interface ReadPromptSourcesOptions {
|
|
67
|
+
/** Memory folder: hqRoot-relative (default `${workerDir}/memory`) or absolute. */
|
|
68
|
+
memoryDir?: string;
|
|
69
|
+
workerSource?: "scaffold" | "worker";
|
|
70
|
+
/** Company whose policies are listed (default: `worker.company` from worker.yaml). */
|
|
71
|
+
companySlug?: string;
|
|
72
|
+
}
|
|
73
|
+
export declare function resolveMemoryDir(workerDir: string, memoryDir?: string): string;
|
|
74
|
+
/** `instructions` may be a string, a block scalar, or a list of strings/objects. */
|
|
75
|
+
export declare function normalizeInstructions(raw: unknown): string | undefined;
|
|
76
|
+
export declare function readPromptSources(hqRoot: string, workerDir: string, io?: PromptFs, opts?: ReadPromptSourcesOptions): PromptSources;
|
|
77
|
+
export interface BuildSystemPromptInput {
|
|
78
|
+
botName: string;
|
|
79
|
+
agentUid: string;
|
|
80
|
+
ownerUid: string;
|
|
81
|
+
hqRoot: string;
|
|
82
|
+
workerDir: string;
|
|
83
|
+
sources: PromptSources;
|
|
84
|
+
workerSource?: "scaffold" | "worker";
|
|
85
|
+
workerId?: string;
|
|
86
|
+
companySlug?: string;
|
|
87
|
+
/** hqRoot-relative (default `${workerDir}/memory`) or absolute. */
|
|
88
|
+
memoryDir?: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The standing instructions the bot runs under. The DM body is delivered as
|
|
92
|
+
* the user turn; this text is the system prompt (claude/codex) or the prompt
|
|
93
|
+
* preamble (grok).
|
|
94
|
+
*/
|
|
95
|
+
export declare function buildSystemPrompt(input: BuildSystemPromptInput): string;
|
|
96
|
+
/** Fixed text sent to any non-owner who DMs the bot (never reaches the model). */
|
|
97
|
+
export declare function nonOwnerRefusalText(botName: string): string;
|
|
98
|
+
/**
|
|
99
|
+
* The one-time introduction the bot DMs its owner when it first comes online.
|
|
100
|
+
* A bot created with `--intro` sends that text verbatim instead.
|
|
101
|
+
*/
|
|
102
|
+
export declare function introDmText(botName: string, runtime: string, intro?: string): string;
|
|
103
|
+
//# sourceMappingURL=prompt.d.ts.map
|