@indigoai-us/hq-cli 5.153.0 → 5.155.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/dist/commands/agent-mcp.d.ts +4 -1
- package/dist/commands/agent-mcp.js +43 -2
- package/dist/commands/files-browse.d.ts +6 -0
- package/dist/commands/files-browse.js +3 -2
- package/dist/commands/lanes.d.ts +3 -11
- package/dist/commands/lanes.js +68 -37
- package/dist/lib/agent-kit/mcp/attachments.d.ts +99 -0
- package/dist/lib/agent-kit/mcp/attachments.js +188 -0
- package/dist/lib/agent-kit/mcp/tools.d.ts +13 -2
- package/dist/lib/agent-kit/mcp/tools.js +66 -3
- package/dist/lib/agent-kit/paths.d.ts +3 -0
- package/dist/lib/agent-kit/paths.js +2 -0
- package/dist/lib/lanes/accounts.d.ts +16 -11
- package/dist/lib/lanes/accounts.js +50 -8
- package/dist/lib/lanes/admission-host.d.ts +19 -0
- package/dist/lib/lanes/admission-host.js +33 -0
- package/dist/lib/lanes/config.js +25 -0
- package/dist/lib/lanes/provider-rejection.d.ts +4 -4
- package/dist/lib/lanes/provider-rejection.js +24 -6
- package/dist/lib/lanes/reconcile.js +30 -1
- package/dist/lib/lanes/spawn.d.ts +9 -1
- package/dist/lib/lanes/spawn.js +420 -14
- package/dist/lib/lanes/types.d.ts +7 -3
- package/dist/lib/lanes/types.js +3 -2
- package/dist/lib/lanes/watcher.d.ts +8 -1
- package/dist/lib/lanes/watcher.js +39 -3
- package/dist/utils/vault-api.d.ts +7 -1
- package/dist/utils/vault-api.js +22 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,12 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.155.0] — 2026-09-24
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Lane respawns keep their recorded account to preserve provider sessions and
|
|
10
|
+
prompt caches. Automatic moves now record why they happened and are limited
|
|
11
|
+
to model changes, usage limits, and unavailable or logged-out accounts.
|
|
12
|
+
- Account retries share the provider admission lock and the host-scoped provider,
|
|
13
|
+
account, and profile checks used by normal admission. Active reservations and
|
|
14
|
+
earlier eligible queue entries keep their place; blocked retries remain queued
|
|
15
|
+
for normal admission. Retries skip accounts already rejected for login and
|
|
16
|
+
retain message, resume, and answer context when queued.
|
|
17
|
+
- Watcher retries resolve the current process hostname and Outpost identity, so
|
|
18
|
+
local-kind and Outpost-kind lanes on one Outpost share admission caps.
|
|
19
|
+
- Account enable and registration now require a positive login check. Provider
|
|
20
|
+
startup messages for logged-out accounts trigger failover to another eligible
|
|
21
|
+
account or a named `account_not_logged_in` failure.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Provider config can list accounts in preference order for a lane's first
|
|
26
|
+
assignment. Disabled, logged-out, usage-limited, and capped accounts are
|
|
27
|
+
skipped. If no preferred account is eligible, hq-cli chooses the least-loaded
|
|
28
|
+
eligible account. Running and resumed lanes keep their recorded account.
|
|
29
|
+
|
|
30
|
+
## [5.154.0] — 2026-09-24
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- Local bots can now open files attached in HQ chat. The new `hq_attachment_fetch` agent tool downloads a chat or DM attachment into a private folder under the bot's agent directory and returns its local path, content type, and size (text files also come back inline). It accepts only the scope-relative `chat/attachments/...` paths that `hq_inbox_read` reports, fetches only from the bot's own company or from a DM the bot is part of, refuses files over 26 MiB, and uses the API the bot enrolled against. Because the attachment reference comes from the sender, any other vault path (for example `knowledge/...`, or a folder) is refused and the bot is pointed at `hq_files_read`. Previously `hq_files_read` rejected those paths and could not read binary files such as .xlsx.
|
|
35
|
+
|
|
5
36
|
## [5.153.0] — 2026-09-23
|
|
6
37
|
|
|
7
38
|
### Fixed
|
|
8
39
|
|
|
9
40
|
- `hq mesh context prd-sync` rebinds a session to the PRD's project when that project is in the same company, then emits `task_status` against the new project so the Board and channel line land there. A PRD in another company is still skipped and does not rebind.
|
|
10
41
|
|
|
42
|
+
### Tests
|
|
43
|
+
|
|
44
|
+
- Cover Outpost admission when account a is at its Codex cap and account b
|
|
45
|
+
has queued work with available capacity.
|
|
46
|
+
- Cover fresh pending resumes and answered-lane resumes against provider
|
|
47
|
+
capacity.
|
|
48
|
+
|
|
11
49
|
## [5.152.1] — 2026-09-23
|
|
12
50
|
|
|
13
51
|
### Fixed
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* hq binary with the machine-identity env pinned.
|
|
10
10
|
*/
|
|
11
11
|
import { Command } from "commander";
|
|
12
|
+
import type { Readable } from "node:stream";
|
|
12
13
|
import { McpServer } from "../lib/agent-kit/mcp/jsonrpc.js";
|
|
13
14
|
import { type HqRunResult, type McpToolClients } from "../lib/agent-kit/mcp/tools.js";
|
|
14
15
|
import { type AgentKitPaths } from "../lib/agent-kit/paths.js";
|
|
@@ -17,7 +18,9 @@ export declare const AGENT_MCP_SERVER_NAME = "hq-agent";
|
|
|
17
18
|
export declare const AGENT_MCP_INSTRUCTIONS: string;
|
|
18
19
|
export declare const HQ_SUBPROCESS_TIMEOUT_MS = 120000;
|
|
19
20
|
export declare function defaultRunHq(nodeBinary: string, hqBinary: string, env: NodeJS.ProcessEnv): (args: string[]) => Promise<HqRunResult>;
|
|
20
|
-
export declare function defaultMcpClients(creds: ExternalMachineCreds, credsPath: string, paths?: Pick<AgentKitPaths, "inboxDir">): McpToolClients;
|
|
21
|
+
export declare function defaultMcpClients(creds: ExternalMachineCreds, credsPath: string, paths?: Pick<AgentKitPaths, "inboxDir" | "attachmentsDir">): McpToolClients;
|
|
22
|
+
/** Drain a stream into memory, refusing past `max` bytes. */
|
|
23
|
+
export declare function readBounded(body: Readable, max: number): Promise<Buffer>;
|
|
21
24
|
export declare function createAgentMcpServer(clients: McpToolClients): McpServer;
|
|
22
25
|
export declare function registerAgentMcpCommand(agent: Command): void;
|
|
23
26
|
//# sourceMappingURL=agent-mcp.d.ts.map
|
|
@@ -12,7 +12,10 @@ import { spawn } from "node:child_process";
|
|
|
12
12
|
import * as os from "node:os";
|
|
13
13
|
import { CLI_VERSION } from "../cli-version.js";
|
|
14
14
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
15
|
-
import {
|
|
15
|
+
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
|
16
|
+
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
17
|
+
import { createCompanyPresignClient } from "./files-browse.js";
|
|
18
|
+
import { MAX_ATTACHMENT_BYTES } from "../lib/agent-kit/mcp/attachments.js";
|
|
16
19
|
import { McpServer } from "../lib/agent-kit/mcp/jsonrpc.js";
|
|
17
20
|
import { buildAgentMcpTools } from "../lib/agent-kit/mcp/tools.js";
|
|
18
21
|
import { readKitConfig } from "../lib/agent-kit/kit-config.js";
|
|
@@ -21,6 +24,7 @@ import { requireExternalCreds } from "./agent-kit.js";
|
|
|
21
24
|
export const AGENT_MCP_SERVER_NAME = "hq-agent";
|
|
22
25
|
export const AGENT_MCP_INSTRUCTIONS = "You are acting inside HQ as an enrolled external agent. Tools run as that machine identity: " +
|
|
23
26
|
"hq_whoami (identity), hq_search / hq_files_list / hq_files_read (company vault), " +
|
|
27
|
+
"hq_attachment_fetch (download a chat attachment — a chat/attachments/… vaultPath on an inbox message — any file type), " +
|
|
24
28
|
"hq_secrets_list / hq_secrets_exec (secrets by NAME only — values are never returned), " +
|
|
25
29
|
"hq_inbox_read / hq_dm_send / hq_inbox_done (read pending HQ messages, reply, then mark each handled), hq_work_mesh_status (what the team is doing). " +
|
|
26
30
|
"Never ask a user for a token or secret value; if a secret is missing, name it and ask an admin to add it.";
|
|
@@ -54,12 +58,34 @@ export function defaultMcpClients(creds, credsPath, paths = agentKitPaths(os.hom
|
|
|
54
58
|
HQ_VAULT_API_URL: creds.apiBaseUrl,
|
|
55
59
|
NO_COLOR: "1",
|
|
56
60
|
};
|
|
61
|
+
const getToken = () => ensureCognitoToken({ tokenSource: "machine", interactive: false });
|
|
62
|
+
let companyUid;
|
|
57
63
|
return {
|
|
58
64
|
creds,
|
|
59
65
|
paths,
|
|
60
66
|
syncFiles: readKitConfig(agentKitPaths(os.homedir(), process.env), CLI_VERSION).syncFiles,
|
|
61
67
|
runHq: defaultRunHq(process.execPath, process.argv[1], env),
|
|
62
|
-
getToken
|
|
68
|
+
getToken,
|
|
69
|
+
resolveCompanyUid: async () => {
|
|
70
|
+
// Pin the enrolled API: DEFAULT_VAULT_API_URL was read from env at module
|
|
71
|
+
// load, before `hq agent mcp` sets HQ_VAULT_API_URL from the creds.
|
|
72
|
+
companyUid ??= getCompanyUid(await getToken(), creds.companySlug, creds.apiBaseUrl);
|
|
73
|
+
try {
|
|
74
|
+
return await companyUid;
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
companyUid = undefined;
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
downloadVaultObject: async (scopeUid, key) => {
|
|
82
|
+
// Same presign path `hq files cat` uses; the vault API's per-key ACL decides.
|
|
83
|
+
const client = createCompanyPresignClient({ token: await getToken(), companyUid: scopeUid, baseUrl: creds.apiBaseUrl });
|
|
84
|
+
const res = await client.send(new GetObjectCommand({ Bucket: scopeUid, Key: key }));
|
|
85
|
+
if (!res.Body)
|
|
86
|
+
throw new Error(`no body returned for '${key}'`);
|
|
87
|
+
return readBounded(res.Body, MAX_ATTACHMENT_BYTES);
|
|
88
|
+
},
|
|
63
89
|
apiJson: async (token, path, init) => {
|
|
64
90
|
const res = await vaultApiFetch({
|
|
65
91
|
token,
|
|
@@ -74,6 +100,21 @@ export function defaultMcpClients(creds, credsPath, paths = agentKitPaths(os.hom
|
|
|
74
100
|
},
|
|
75
101
|
};
|
|
76
102
|
}
|
|
103
|
+
/** Drain a stream into memory, refusing past `max` bytes. */
|
|
104
|
+
export async function readBounded(body, max) {
|
|
105
|
+
const chunks = [];
|
|
106
|
+
let total = 0;
|
|
107
|
+
for await (const chunk of body) {
|
|
108
|
+
const b = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
109
|
+
total += b.length;
|
|
110
|
+
if (total > max) {
|
|
111
|
+
body.destroy();
|
|
112
|
+
throw new Error(`object exceeds the ${max}-byte attachment limit`);
|
|
113
|
+
}
|
|
114
|
+
chunks.push(b);
|
|
115
|
+
}
|
|
116
|
+
return Buffer.concat(chunks);
|
|
117
|
+
}
|
|
77
118
|
export function createAgentMcpServer(clients) {
|
|
78
119
|
return new McpServer({
|
|
79
120
|
name: AGENT_MCP_SERVER_NAME,
|
|
@@ -450,6 +450,12 @@ export declare function createCompanyPresignClient(input: {
|
|
|
450
450
|
* (keys on these errors are bucket-relative and carry no slug).
|
|
451
451
|
*/
|
|
452
452
|
companySlug?: string;
|
|
453
|
+
/**
|
|
454
|
+
* Control-plane origin for list/presign calls. Defaults to
|
|
455
|
+
* DEFAULT_VAULT_API_URL (via vaultApiFetch); an enrolled agent passes its
|
|
456
|
+
* own `apiBaseUrl` so its token only goes to the API it enrolled against.
|
|
457
|
+
*/
|
|
458
|
+
baseUrl?: string;
|
|
453
459
|
}): FilesBrowseS3Client;
|
|
454
460
|
/**
|
|
455
461
|
* Production company-mode client factory — closes over the caller's token and
|
|
@@ -678,7 +678,7 @@ export const PRESIGN_BUFFER_MAX_BYTES = 32 * 1024 * 1024;
|
|
|
678
678
|
* `send(...)` as if it held an S3 client.
|
|
679
679
|
*/
|
|
680
680
|
export function createCompanyPresignClient(input) {
|
|
681
|
-
const { token, companyUid, companySlug } = input;
|
|
681
|
+
const { token, companyUid, companySlug, baseUrl } = input;
|
|
682
682
|
const denied = (message, key, extra) => Object.assign(new Error(message), {
|
|
683
683
|
status: 403,
|
|
684
684
|
key,
|
|
@@ -692,7 +692,7 @@ export function createCompanyPresignClient(input) {
|
|
|
692
692
|
query.prefix = prefix;
|
|
693
693
|
if (cmd.input.ContinuationToken)
|
|
694
694
|
query.cursor = cmd.input.ContinuationToken;
|
|
695
|
-
const res = await vaultApiFetch({ token, path: "/v1/files/list", query });
|
|
695
|
+
const res = await vaultApiFetch({ token, baseUrl, path: "/v1/files/list", query });
|
|
696
696
|
if (!res.ok) {
|
|
697
697
|
const body = (await res.json().catch(() => ({})));
|
|
698
698
|
const message = body.message ?? body.error ?? `files list failed (${res.status})`;
|
|
@@ -723,6 +723,7 @@ export function createCompanyPresignClient(input) {
|
|
|
723
723
|
const key = cmd.input.Key;
|
|
724
724
|
const res = await vaultApiFetch({
|
|
725
725
|
token,
|
|
726
|
+
baseUrl,
|
|
726
727
|
path: "/v1/files/presign",
|
|
727
728
|
method: "POST",
|
|
728
729
|
body: { company: companyUid, key, op: "get" },
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* `watch` and the monitor reminder are the line-oriented text surfaces.
|
|
7
7
|
*/
|
|
8
8
|
import { Command } from "commander";
|
|
9
|
+
import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
10
|
+
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
11
|
+
export type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
9
12
|
import { type OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
|
|
10
13
|
import { emitLaneDecisionEvents, type LaneMesh } from "../lib/lanes/mesh-events.js";
|
|
11
14
|
import { MAX_LANE_DEPTH, type LaneCompanyRef, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
|
|
@@ -16,17 +19,7 @@ import { spawnLaneWorker, type KillProcessFn, type ProveDetachedFn, type SpawnPr
|
|
|
16
19
|
export { spawnLaneWorker };
|
|
17
20
|
export { questionBodyAbsPath };
|
|
18
21
|
export { MAX_LANE_DEPTH };
|
|
19
|
-
export declare const OUTPOST_ID_PATH = "/etc/outpost/outpost-id";
|
|
20
|
-
export declare const OUTPOST_DIR = "/etc/outpost";
|
|
21
22
|
export declare const LANES_RUNS_REL: string;
|
|
22
|
-
export type OutpostIdentity = {
|
|
23
|
-
kind: "id";
|
|
24
|
-
id: string;
|
|
25
|
-
} | {
|
|
26
|
-
kind: "unknown";
|
|
27
|
-
} | {
|
|
28
|
-
kind: "not_outpost";
|
|
29
|
-
};
|
|
30
23
|
export interface SpawnLoginResult {
|
|
31
24
|
status: number;
|
|
32
25
|
}
|
|
@@ -79,7 +72,6 @@ export interface LanesCommandDeps {
|
|
|
79
72
|
locateTranscript?: (home: string, sessionId: string) => string | null;
|
|
80
73
|
monitorEvidence?: MonitorEvidenceReader;
|
|
81
74
|
}
|
|
82
|
-
export declare function readOutpostIdentity(existsSync?: (p: string) => boolean, readFileSync?: (p: string) => string): OutpostIdentity;
|
|
83
75
|
export declare function parseSeniorRef(raw: string): SeniorRef;
|
|
84
76
|
export declare function resolveAnswerChoice(choice: string, options: string[], freeform: boolean): string;
|
|
85
77
|
/**
|
package/dist/commands/lanes.js
CHANGED
|
@@ -12,13 +12,15 @@ import * as os from "node:os";
|
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
14
14
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
15
|
+
import { readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
|
|
16
|
+
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
15
17
|
import { buildOutpostLaneCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
16
18
|
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
17
19
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
18
20
|
import { openMeshTransport } from "../lib/mesh/client.js";
|
|
19
21
|
import { createLaneMesh, emitLaneDecisionEvents, } from "../lib/lanes/mesh-events.js";
|
|
20
22
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
21
|
-
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile,
|
|
23
|
+
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, withLaneAdmissionLock, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
22
24
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
23
25
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
24
26
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
@@ -33,8 +35,6 @@ import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ON
|
|
|
33
35
|
export { spawnLaneWorker };
|
|
34
36
|
export { questionBodyAbsPath };
|
|
35
37
|
export { MAX_LANE_DEPTH };
|
|
36
|
-
export const OUTPOST_ID_PATH = "/etc/outpost/outpost-id";
|
|
37
|
-
export const OUTPOST_DIR = "/etc/outpost";
|
|
38
38
|
export const LANES_RUNS_REL = path.join("workspace", "lanes-runs");
|
|
39
39
|
const VENDOR_LOGIN = {
|
|
40
40
|
claude: { command: "claude", args: ["auth", "login"] },
|
|
@@ -133,27 +133,6 @@ async function defaultFetchText(url) {
|
|
|
133
133
|
}
|
|
134
134
|
return res.text();
|
|
135
135
|
}
|
|
136
|
-
export function readOutpostIdentity(existsSync = (p) => fs.existsSync(p), readFileSync = (p) => fs.readFileSync(p, "utf8")) {
|
|
137
|
-
const hasMarker = existsSync(OUTPOST_ID_PATH);
|
|
138
|
-
const hasDir = existsSync(OUTPOST_DIR);
|
|
139
|
-
if (hasMarker) {
|
|
140
|
-
let text;
|
|
141
|
-
try {
|
|
142
|
-
text = readFileSync(OUTPOST_ID_PATH);
|
|
143
|
-
}
|
|
144
|
-
catch (err) {
|
|
145
|
-
process.stderr.write(`hq lanes: failed to read ${OUTPOST_ID_PATH}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
146
|
-
throw err;
|
|
147
|
-
}
|
|
148
|
-
const id = text.trim();
|
|
149
|
-
if (!id)
|
|
150
|
-
return hasDir ? { kind: "unknown" } : { kind: "not_outpost" };
|
|
151
|
-
return { kind: "id", id };
|
|
152
|
-
}
|
|
153
|
-
if (hasDir)
|
|
154
|
-
return { kind: "unknown" };
|
|
155
|
-
return { kind: "not_outpost" };
|
|
156
|
-
}
|
|
157
136
|
function defaultSpawnLogin(opts) {
|
|
158
137
|
return new Promise((resolve, reject) => {
|
|
159
138
|
const child = spawn(opts.command, opts.args, {
|
|
@@ -263,11 +242,10 @@ function livenessDepsFromCtx(ctx) {
|
|
|
263
242
|
};
|
|
264
243
|
}
|
|
265
244
|
function reconcileHostFromCtx(ctx) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
};
|
|
245
|
+
return resolveAdmissionHost({
|
|
246
|
+
hostname: ctx.hostname,
|
|
247
|
+
readOutpostIdentity: ctx.readOutpostIdentity,
|
|
248
|
+
});
|
|
271
249
|
}
|
|
272
250
|
export function parseSeniorRef(raw) {
|
|
273
251
|
const trimmed = raw.trim();
|
|
@@ -1261,9 +1239,17 @@ export function applyOutpostAccountPickup(hqRoot, laneId, modelDeps = {}) {
|
|
|
1261
1239
|
try {
|
|
1262
1240
|
const resolved = resolveAccount(lane.provider, lane.account, config, {
|
|
1263
1241
|
hqRoot,
|
|
1242
|
+
accountDeps: {
|
|
1243
|
+
probe: modelDeps.probe,
|
|
1244
|
+
env: modelDeps.env,
|
|
1245
|
+
},
|
|
1246
|
+
});
|
|
1247
|
+
const status = accountStatus(lane.provider, resolved.id, config, {
|
|
1248
|
+
existsSync: (p) => fs.existsSync(p),
|
|
1249
|
+
probe: modelDeps.probe,
|
|
1250
|
+
env: modelDeps.env,
|
|
1264
1251
|
});
|
|
1265
|
-
|
|
1266
|
-
if (isLoggedOut(status)) {
|
|
1252
|
+
if (status.logged_in !== true) {
|
|
1267
1253
|
return markPickupFailure(hqRoot, laneId, "account_not_logged_in", {
|
|
1268
1254
|
provider: lane.provider,
|
|
1269
1255
|
account: resolved.id,
|
|
@@ -1549,8 +1535,10 @@ async function runCreate(ctx, opts) {
|
|
|
1549
1535
|
resolveAccount(provider, undefined, config, {
|
|
1550
1536
|
hqRoot,
|
|
1551
1537
|
existsSync: ctx.existsSync,
|
|
1538
|
+
accountDeps: { probe: ctx.probeProvider, env: ctx.env },
|
|
1552
1539
|
cliModel,
|
|
1553
1540
|
model: modelToValidate,
|
|
1541
|
+
preferredAccounts: config.providers[provider].preferred_accounts ?? [],
|
|
1554
1542
|
...(profileAccounts !== undefined
|
|
1555
1543
|
? { allowedAccounts: profileAccounts }
|
|
1556
1544
|
: {}),
|
|
@@ -1563,7 +1551,11 @@ async function runCreate(ctx, opts) {
|
|
|
1563
1551
|
.filter((id) => id !== candidate &&
|
|
1564
1552
|
accounts[id]?.enabled &&
|
|
1565
1553
|
(profileAccounts === undefined || profileAccounts.includes(id)) &&
|
|
1566
|
-
accountStatus(provider, id, config, {
|
|
1554
|
+
accountStatus(provider, id, config, {
|
|
1555
|
+
existsSync: ctx.existsSync,
|
|
1556
|
+
probe: ctx.probeProvider,
|
|
1557
|
+
env: ctx.env,
|
|
1558
|
+
}).logged_in === true)
|
|
1567
1559
|
.sort(),
|
|
1568
1560
|
];
|
|
1569
1561
|
const checkedAccounts = [];
|
|
@@ -1648,8 +1640,10 @@ async function runCreate(ctx, opts) {
|
|
|
1648
1640
|
pinnedAccount = resolveAccount(provider, undefined, config, {
|
|
1649
1641
|
hqRoot,
|
|
1650
1642
|
existsSync: ctx.existsSync,
|
|
1643
|
+
accountDeps: { probe: ctx.probeProvider, env: ctx.env },
|
|
1651
1644
|
cliModel,
|
|
1652
1645
|
model: cliModel ?? profileModel,
|
|
1646
|
+
preferredAccounts: config.providers[provider].preferred_accounts ?? [],
|
|
1653
1647
|
...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
|
|
1654
1648
|
}).id;
|
|
1655
1649
|
}
|
|
@@ -1797,6 +1791,7 @@ async function runCreate(ctx, opts) {
|
|
|
1797
1791
|
probe: ctx.probeProvider,
|
|
1798
1792
|
env: ctx.env,
|
|
1799
1793
|
},
|
|
1794
|
+
currentModelValidated: !outpostId && modelToValidate !== undefined,
|
|
1800
1795
|
existsSync: ctx.existsSync,
|
|
1801
1796
|
spawnProcess: ctx.spawnProcess,
|
|
1802
1797
|
proveDetached: ctx.proveDetached,
|
|
@@ -2196,6 +2191,7 @@ async function runPickup(ctx, opts) {
|
|
|
2196
2191
|
probe: ctx.probeProvider,
|
|
2197
2192
|
env: ctx.env,
|
|
2198
2193
|
},
|
|
2194
|
+
currentModelValidated: afterAccount.model !== undefined,
|
|
2199
2195
|
existsSync: ctx.existsSync,
|
|
2200
2196
|
spawnProcess: ctx.spawnProcess,
|
|
2201
2197
|
proveDetached: ctx.proveDetached,
|
|
@@ -2512,7 +2508,11 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
2512
2508
|
if (!targetCfg.enabled) {
|
|
2513
2509
|
throw new AccountResolutionError("account_disabled", `Account ${JSON.stringify(target)} is disabled${targetCfg.disabled_reason ? `: ${targetCfg.disabled_reason}` : ""}`, { provider: lane.provider, account: target, reason: targetCfg.disabled_reason ?? "" });
|
|
2514
2510
|
}
|
|
2515
|
-
if (
|
|
2511
|
+
if (accountStatus(lane.provider, target, config, {
|
|
2512
|
+
existsSync: ctx.existsSync,
|
|
2513
|
+
probe: ctx.probeProvider,
|
|
2514
|
+
env: ctx.env,
|
|
2515
|
+
}).logged_in !== true) {
|
|
2516
2516
|
throw new LanesError("account_not_logged_in", `Account ${target} for ${lane.provider} is not logged in (no credential file)`, { provider: lane.provider, account: target, lane_id: laneId });
|
|
2517
2517
|
}
|
|
2518
2518
|
const profile = lane.worker_profile !== undefined
|
|
@@ -3829,7 +3829,11 @@ async function runAccountsList(ctx, opts) {
|
|
|
3829
3829
|
// model and effort are what a spawn on each account would start from
|
|
3830
3830
|
// (spec: --model/--effort, then the account, then the provider).
|
|
3831
3831
|
const quota = loadQuotaState(hqRoot);
|
|
3832
|
-
const accounts = providers.flatMap((provider) => listAccountStatuses(provider, config
|
|
3832
|
+
const accounts = providers.flatMap((provider) => listAccountStatuses(provider, config, {
|
|
3833
|
+
existsSync: ctx.existsSync,
|
|
3834
|
+
probe: ctx.probeProvider,
|
|
3835
|
+
env: ctx.env,
|
|
3836
|
+
}).map((status) => {
|
|
3833
3837
|
const accountCfg = config.providers[provider].accounts[status.id];
|
|
3834
3838
|
const model = resolveLaneModel({
|
|
3835
3839
|
accountModel: accountCfg?.model,
|
|
@@ -3930,8 +3934,17 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
|
|
|
3930
3934
|
home,
|
|
3931
3935
|
enabled: true,
|
|
3932
3936
|
};
|
|
3937
|
+
const status = accountStatus(provider, id, config, {
|
|
3938
|
+
existsSync: ctx.existsSync,
|
|
3939
|
+
probe: ctx.probeProvider,
|
|
3940
|
+
env: ctx.env,
|
|
3941
|
+
});
|
|
3942
|
+
if (status.logged_in !== true) {
|
|
3943
|
+
throw new LanesError(status.logged_in === false ? "account_not_logged_in" : "account_login_status_unknown", status.logged_in === false
|
|
3944
|
+
? `Account ${id} for ${provider} is not logged in`
|
|
3945
|
+
: `Could not verify that account ${id} for ${provider} is logged in`, { provider, account: id, home });
|
|
3946
|
+
}
|
|
3933
3947
|
writeLanesConfig(hqRoot, config);
|
|
3934
|
-
const status = accountStatus(provider, id, loadLanesConfig(hqRoot));
|
|
3935
3948
|
printJson({ ok: true, action: "accounts.login", account: status });
|
|
3936
3949
|
}
|
|
3937
3950
|
async function runAccountsEnable(ctx, providerRaw, accountId) {
|
|
@@ -3948,13 +3961,27 @@ async function runAccountsEnable(ctx, providerRaw, accountId) {
|
|
|
3948
3961
|
.map(([id]) => id),
|
|
3949
3962
|
});
|
|
3950
3963
|
}
|
|
3964
|
+
const status = accountStatus(provider, accountId, config, {
|
|
3965
|
+
existsSync: ctx.existsSync,
|
|
3966
|
+
probe: ctx.probeProvider,
|
|
3967
|
+
env: ctx.env,
|
|
3968
|
+
});
|
|
3969
|
+
if (status.logged_in !== true) {
|
|
3970
|
+
throw new LanesError(status.logged_in === false ? "account_not_logged_in" : "account_login_status_unknown", status.logged_in === false
|
|
3971
|
+
? `Account ${accountId} for ${provider} is not logged in`
|
|
3972
|
+
: `Could not verify that account ${accountId} for ${provider} is logged in`, { provider, account: accountId });
|
|
3973
|
+
}
|
|
3951
3974
|
account.enabled = true;
|
|
3952
3975
|
delete account.disabled_reason;
|
|
3953
3976
|
writeLanesConfig(hqRoot, config);
|
|
3954
3977
|
printJson({
|
|
3955
3978
|
ok: true,
|
|
3956
3979
|
action: "accounts.enable",
|
|
3957
|
-
account: accountStatus(provider, accountId, loadLanesConfig(hqRoot)
|
|
3980
|
+
account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
|
|
3981
|
+
existsSync: ctx.existsSync,
|
|
3982
|
+
probe: ctx.probeProvider,
|
|
3983
|
+
env: ctx.env,
|
|
3984
|
+
}),
|
|
3958
3985
|
});
|
|
3959
3986
|
}
|
|
3960
3987
|
async function runAccountsDisable(ctx, providerRaw, accountId, opts) {
|
|
@@ -3978,7 +4005,11 @@ async function runAccountsDisable(ctx, providerRaw, accountId, opts) {
|
|
|
3978
4005
|
printJson({
|
|
3979
4006
|
ok: true,
|
|
3980
4007
|
action: "accounts.disable",
|
|
3981
|
-
account: accountStatus(provider, accountId, loadLanesConfig(hqRoot)
|
|
4008
|
+
account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
|
|
4009
|
+
existsSync: ctx.existsSync,
|
|
4010
|
+
probe: ctx.probeProvider,
|
|
4011
|
+
env: ctx.env,
|
|
4012
|
+
}),
|
|
3982
4013
|
});
|
|
3983
4014
|
}
|
|
3984
4015
|
async function runSessionsList(ctx, opts) {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq_attachment_fetch` — pull a chat attachment named on an inbox item down
|
|
3
|
+
* to a private local file the bot can open.
|
|
4
|
+
*
|
|
5
|
+
* Inbox items carry attachments as `{ vaultPath, companyUid, … }` where
|
|
6
|
+
* `vaultPath` is SCOPE-relative (`chat/attachments/{dm|chan}/<id>/<file>`) and
|
|
7
|
+
* `companyUid` is the vault scope the bytes live in: the company uid for a
|
|
8
|
+
* company channel, or the SENDER's person uid (`prs_…`) for a DM, because a DM
|
|
9
|
+
* attachment is stored in the sender's personal vault. Neither `hq_files_read`
|
|
10
|
+
* (which only accepts `companies/<slug>/…` and returns clamped text) nor
|
|
11
|
+
* anything else on the MCP surface could read those, so bots answered
|
|
12
|
+
* "I can't open the file".
|
|
13
|
+
*
|
|
14
|
+
* Bytes come through the same vault presign path `hq files cat` uses
|
|
15
|
+
* (`createCompanyPresignClient` → POST /v1/files/presign), minted with the
|
|
16
|
+
* bot's own machine token, so the server's per-key ACL stays the authority.
|
|
17
|
+
* The client-side checks here are the tenant boundary the bot can state
|
|
18
|
+
* before any request leaves the machine. They matter because an attachment
|
|
19
|
+
* ref is SENDER-controlled while the server authorizes against the BOT's own
|
|
20
|
+
* access: without them a sender could attach any key the bot can read (say
|
|
21
|
+
* `knowledge/restricted/x.csv` in the bot's company) and have the bot fetch
|
|
22
|
+
* it for them.
|
|
23
|
+
* - the key must be a plain relative path with no traversal, under
|
|
24
|
+
* `chat/attachments/`;
|
|
25
|
+
* - a DM key (`chat/attachments/dm/<a>--<b>/…`) must name this bot in the
|
|
26
|
+
* pair, whichever scope it is read from;
|
|
27
|
+
* - a `cmp_` scope must be the bot's own enrolled company;
|
|
28
|
+
* - a `prs_` scope is only a DM attachment between that person and this
|
|
29
|
+
* bot (the pair naming both).
|
|
30
|
+
*
|
|
31
|
+
* Files land under `<agentDir>/attachments/<scopeUid>/` (dir 0700, file 0600)
|
|
32
|
+
* with a sanitized name, so nothing from the message can pick the location.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Refuse anything larger than this. Chat uploads are capped at 25 MB
|
|
36
|
+
* (MAX_CHAT_ATTACHMENT_BYTES in hq-desktop-app); 26 MiB leaves a little
|
|
37
|
+
* headroom over that without letting a crafted key pull a large vault object.
|
|
38
|
+
*/
|
|
39
|
+
export declare const MAX_ATTACHMENT_BYTES: number;
|
|
40
|
+
/** Upper bound on the inline text rendering returned alongside the path. */
|
|
41
|
+
export declare const MAX_ATTACHMENT_TEXT_CHARS = 20000;
|
|
42
|
+
/** Every chat upload lands under this prefix in its scope. */
|
|
43
|
+
export declare const CHAT_ATTACHMENT_PREFIX = "chat/attachments/";
|
|
44
|
+
/**
|
|
45
|
+
* Validate a scope-relative attachment key. Accepts only `chat/attachments/…`
|
|
46
|
+
* file keys exactly as inbox items carry them. Folder / share refs (`/`,
|
|
47
|
+
* `…/`) get a "not a chat attachment" error pointing at hq_files_read.
|
|
48
|
+
* Rejects absolute paths, traversal, empty / dot segments, backslashes and
|
|
49
|
+
* control characters. Returns the key with any leading slashes stripped.
|
|
50
|
+
*/
|
|
51
|
+
export declare function assertAttachmentKey(p: string): string;
|
|
52
|
+
/** Throws unless `scopeUid` is a well-formed cmp_/prs_ uid. No network. */
|
|
53
|
+
export declare function assertAttachmentScopeUid(scopeUid: string): void;
|
|
54
|
+
/**
|
|
55
|
+
* The checks that need no network: uid format, DM-pair membership for any
|
|
56
|
+
* `chat/attachments/dm/…` key, and the prs_ scope rule. Run before resolving
|
|
57
|
+
* the bot's company so a refused ref never costs (or leaks) a request.
|
|
58
|
+
*/
|
|
59
|
+
export declare function assertAttachmentRef(input: {
|
|
60
|
+
scopeUid: string;
|
|
61
|
+
key: string;
|
|
62
|
+
botEntityUid: string;
|
|
63
|
+
}): void;
|
|
64
|
+
/**
|
|
65
|
+
* Tenant gate. Throws unless the scope is this bot's own company, or a
|
|
66
|
+
* person vault holding a DM attachment between that person and this bot.
|
|
67
|
+
*/
|
|
68
|
+
export declare function assertAttachmentScope(input: {
|
|
69
|
+
scopeUid: string;
|
|
70
|
+
key: string;
|
|
71
|
+
botCompanyUid: string;
|
|
72
|
+
botEntityUid: string;
|
|
73
|
+
}): void;
|
|
74
|
+
/** Reduce an untrusted name to a safe single path component. */
|
|
75
|
+
export declare function sanitizeAttachmentFileName(raw: string): string;
|
|
76
|
+
export interface FetchAttachmentResult {
|
|
77
|
+
localPath: string;
|
|
78
|
+
name: string;
|
|
79
|
+
contentType?: string;
|
|
80
|
+
sizeBytes: number;
|
|
81
|
+
vaultPath: string;
|
|
82
|
+
companyUid: string;
|
|
83
|
+
/** Inline rendering for text-like files (csv, txt, json, …), bounded. */
|
|
84
|
+
text?: string;
|
|
85
|
+
note?: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Download `key` from `scopeUid` and write it privately under `attachmentsDir`.
|
|
89
|
+
* `download` is the presign-backed reader; the caller has already gated scope.
|
|
90
|
+
*/
|
|
91
|
+
export declare function saveAttachment(input: {
|
|
92
|
+
attachmentsDir: string;
|
|
93
|
+
scopeUid: string;
|
|
94
|
+
key: string;
|
|
95
|
+
name?: string;
|
|
96
|
+
contentType?: string;
|
|
97
|
+
download: (scopeUid: string, key: string) => Promise<Buffer>;
|
|
98
|
+
}): Promise<FetchAttachmentResult>;
|
|
99
|
+
//# sourceMappingURL=attachments.d.ts.map
|