@indigoai-us/hq-cli 5.345.57 → 5.345.59
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 +15 -0
- package/dist/command-catalog.generated.d.ts +12 -0
- package/dist/command-catalog.generated.js +15 -0
- package/dist/commands/access.d.ts +14 -0
- package/dist/commands/access.js +69 -1
- package/dist/commands/agent-kit.js +20 -10
- package/dist/commands/billing.d.ts +13 -0
- package/dist/commands/billing.js +31 -0
- package/dist/commands/db-dump.js +2 -2
- package/dist/commands/db-remote-deps.js +6 -0
- package/dist/commands/lanes-loop.js +3 -1
- package/dist/commands/lanes-questions.js +16 -0
- package/dist/commands/lanes-session-start.js +19 -3
- package/dist/commands/lanes-shared.d.ts +1 -0
- package/dist/commands/lanes.d.ts +3 -1
- package/dist/commands/lanes.js +52 -2
- package/dist/lib/agent-kit/inbox-poll-backoff-flag.d.ts +19 -0
- package/dist/lib/agent-kit/inbox-poll-backoff-flag.js +70 -0
- package/dist/lib/agent-kit/run/inbox.d.ts +7 -3
- package/dist/lib/agent-kit/run/inbox.js +45 -6
- package/dist/lib/agent-kit/run/stop-signals.d.ts +14 -0
- package/dist/lib/agent-kit/run/stop-signals.js +26 -0
- package/dist/lib/agent-kit/services.d.ts +2 -2
- package/dist/lib/agent-kit/services.js +5 -3
- package/dist/lib/anywhere-runtime-flag-key.d.ts +1 -0
- package/dist/lib/anywhere-runtime-flag-key.js +1 -0
- package/dist/lib/anywhere-runtime-revocation-marker.d.ts +4 -0
- package/dist/lib/anywhere-runtime-revocation-marker.js +26 -0
- package/dist/lib/daemon/run.d.ts +2 -0
- package/dist/lib/daemon/run.js +17 -4
- package/dist/lib/daemon/server.js +7 -0
- package/dist/lib/db/control-plane.d.ts +12 -1
- package/dist/lib/db/usage-format.d.ts +2 -1
- package/dist/lib/db/usage-format.js +19 -0
- package/dist/lib/install/provider-wrappers.js +13 -0
- package/dist/lib/lanes/loop.d.ts +23 -1
- package/dist/lib/lanes/loop.js +134 -19
- package/dist/lib/lanes/questions.d.ts +5 -0
- package/dist/lib/lanes/reconcile.js +3 -0
- package/dist/lib/lanes/story-ledger.js +30 -20
- package/dist/lib/lanes/types.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createFlagClient, } from "@indigoai-us/hq-flags-client";
|
|
2
|
+
import { resolveFlagGate } from "../flag-registry.js";
|
|
3
|
+
import { resolveFlagRegistryEndpoint } from "../flag-registry-endpoint.js";
|
|
4
|
+
export const AGENT_KIT_INBOX_BACKOFF_FLAG = "agents.inbox-poll-backoff-agent-kit";
|
|
5
|
+
export const AGENT_KIT_INBOX_BACKOFF_DEFAULT = true;
|
|
6
|
+
export const AGENT_KIT_INBOX_BACKOFF_CACHE_MS = 3 * 60_000;
|
|
7
|
+
export const AGENT_KIT_INBOX_BACKOFF_TIMEOUT_MS = 2_000;
|
|
8
|
+
/** Read the default-on kill switch; registry outages preserve idle backoff. */
|
|
9
|
+
export function createAgentKitInboxBackoffFlagReader(dependencies) {
|
|
10
|
+
const createClient = dependencies.createClient ?? createFlagClient;
|
|
11
|
+
const now = dependencies.now ?? Date.now;
|
|
12
|
+
const report = dependencies.reportFailure ?? ((error) => {
|
|
13
|
+
const name = error instanceof Error ? error.name : "UnknownError";
|
|
14
|
+
process.stderr.write(`HQ agent-kit inbox backoff flag lookup failed (${name}); keeping the default-on behavior.\n`);
|
|
15
|
+
});
|
|
16
|
+
let cachedValue = AGENT_KIT_INBOX_BACKOFF_DEFAULT;
|
|
17
|
+
let lastReadAt = Number.NEGATIVE_INFINITY;
|
|
18
|
+
let pending = null;
|
|
19
|
+
const read = async () => {
|
|
20
|
+
let client;
|
|
21
|
+
let failed = false;
|
|
22
|
+
const reportOnce = (error) => {
|
|
23
|
+
if (failed)
|
|
24
|
+
return;
|
|
25
|
+
failed = true;
|
|
26
|
+
report(error);
|
|
27
|
+
};
|
|
28
|
+
try {
|
|
29
|
+
client = createClient({
|
|
30
|
+
endpoint: resolveFlagRegistryEndpoint(dependencies.endpoint),
|
|
31
|
+
...(dependencies.companySlug ? { companyIdentifiers: [dependencies.companySlug] } : {}),
|
|
32
|
+
env: {},
|
|
33
|
+
getToken: dependencies.getToken,
|
|
34
|
+
...(dependencies.fetch ? { fetch: dependencies.fetch } : {}),
|
|
35
|
+
refreshIntervalMs: 0,
|
|
36
|
+
requestTimeoutMs: AGENT_KIT_INBOX_BACKOFF_TIMEOUT_MS,
|
|
37
|
+
onError: reportOnce,
|
|
38
|
+
});
|
|
39
|
+
await client.ready();
|
|
40
|
+
cachedValue = failed
|
|
41
|
+
? AGENT_KIT_INBOX_BACKOFF_DEFAULT
|
|
42
|
+
: resolveFlagGate(client, AGENT_KIT_INBOX_BACKOFF_FLAG, { fallback: AGENT_KIT_INBOX_BACKOFF_DEFAULT }, () => AGENT_KIT_INBOX_BACKOFF_DEFAULT);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
reportOnce(error);
|
|
46
|
+
cachedValue = AGENT_KIT_INBOX_BACKOFF_DEFAULT;
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
try {
|
|
50
|
+
client?.close();
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
reportOnce(error);
|
|
54
|
+
cachedValue = AGENT_KIT_INBOX_BACKOFF_DEFAULT;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return cachedValue;
|
|
58
|
+
};
|
|
59
|
+
return () => {
|
|
60
|
+
const currentTime = now();
|
|
61
|
+
if (pending)
|
|
62
|
+
return pending;
|
|
63
|
+
if (currentTime - lastReadAt < AGENT_KIT_INBOX_BACKOFF_CACHE_MS)
|
|
64
|
+
return Promise.resolve(cachedValue);
|
|
65
|
+
lastReadAt = currentTime;
|
|
66
|
+
pending = read().finally(() => { pending = null; });
|
|
67
|
+
return pending;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=inbox-poll-backoff-flag.js.map
|
|
@@ -21,6 +21,7 @@ import { type WakeConfig, type WakeDeps, type WakeNotice, type WakeResult } from
|
|
|
21
21
|
export declare const SEEN_IDS_NAME = "seen-ids.json";
|
|
22
22
|
export declare const INBOX_JSONL_NAME = "inbox.jsonl";
|
|
23
23
|
export declare const MAX_SEEN_IDS = 5000;
|
|
24
|
+
export declare const INBOX_POLL_BACKOFF_MAX_MS = 30000;
|
|
24
25
|
export interface InboxItem {
|
|
25
26
|
id: string;
|
|
26
27
|
raw: Record<string, unknown>;
|
|
@@ -38,10 +39,13 @@ export interface InboxDeps {
|
|
|
38
39
|
ack: boolean;
|
|
39
40
|
getToken: () => Promise<string>;
|
|
40
41
|
log: KitLogger;
|
|
41
|
-
fetchInbox?: (token: string) => Promise<InboxFetchResult>;
|
|
42
|
+
fetchInbox?: (token: string, signal?: AbortSignal) => Promise<InboxFetchResult>;
|
|
42
43
|
ackItem?: (token: string, id: string) => Promise<number>;
|
|
43
44
|
warmCache?: (token: string, agentUid: string) => Promise<unknown>;
|
|
44
|
-
sleep?: (ms: number) => Promise<void>;
|
|
45
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
46
|
+
readInboxBackoffEnabled?: () => Promise<boolean>;
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
companySlug?: string;
|
|
45
49
|
maxPolls?: number;
|
|
46
50
|
/** Override wake config lookup (tests). `null` disables the wake. */
|
|
47
51
|
wakeConfig?: WakeConfig | null;
|
|
@@ -66,7 +70,7 @@ export interface InboxFetchResult {
|
|
|
66
70
|
export declare function agentApiUrl(apiBaseUrl: string, route: string): string;
|
|
67
71
|
export declare function agentInboxUrl(apiBaseUrl: string, agentUid: string): string;
|
|
68
72
|
export declare function truncateBody(text: string, max?: number): string;
|
|
69
|
-
export declare function defaultFetchInbox(agentUid: string, apiBaseUrl: string): (token: string) => Promise<InboxFetchResult>;
|
|
73
|
+
export declare function defaultFetchInbox(agentUid: string, apiBaseUrl: string): (token: string, signal?: AbortSignal) => Promise<InboxFetchResult>;
|
|
70
74
|
export declare function defaultAckItem(agentUid: string, apiBaseUrl: string): (token: string, id: string) => Promise<number>;
|
|
71
75
|
export declare function pollInboxOnce(deps: InboxDeps): Promise<{
|
|
72
76
|
ok: boolean;
|
|
@@ -21,10 +21,12 @@ import { vaultApiFetch } from "../../../utils/vault-api.js";
|
|
|
21
21
|
import { writeComponentStatus } from "../creds.js";
|
|
22
22
|
import { pendingInbox } from "../inbox-state.js";
|
|
23
23
|
import { writePrivateFile } from "./../private-file.js";
|
|
24
|
+
import { createAgentKitInboxBackoffFlagReader } from "../inbox-poll-backoff-flag.js";
|
|
24
25
|
import { buildWakeNotice, currentWakeState, fireWake, readWakeConfig, shouldWake, } from "../wake.js";
|
|
25
26
|
export const SEEN_IDS_NAME = "seen-ids.json";
|
|
26
27
|
export const INBOX_JSONL_NAME = "inbox.jsonl";
|
|
27
28
|
export const MAX_SEEN_IDS = 5000;
|
|
29
|
+
export const INBOX_POLL_BACKOFF_MAX_MS = 30_000;
|
|
28
30
|
/** Pull `{id}` out of each server item; items without a string id are dropped. */
|
|
29
31
|
export function extractInboxItems(body) {
|
|
30
32
|
const rec = body && typeof body === "object" ? body : {};
|
|
@@ -107,12 +109,13 @@ export function truncateBody(text, max = INBOX_ERROR_BODY_MAX) {
|
|
|
107
109
|
return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
|
|
108
110
|
}
|
|
109
111
|
export function defaultFetchInbox(agentUid, apiBaseUrl) {
|
|
110
|
-
return async (token) => {
|
|
112
|
+
return async (token, signal) => {
|
|
111
113
|
const url = new URL(agentInboxUrl(apiBaseUrl, agentUid));
|
|
112
114
|
const res = await vaultApiFetch({
|
|
113
115
|
token,
|
|
114
116
|
baseUrl: url.origin,
|
|
115
117
|
path: url.pathname,
|
|
118
|
+
...(signal ? { signal } : {}),
|
|
116
119
|
});
|
|
117
120
|
const text = await res.text().catch(() => "");
|
|
118
121
|
let body;
|
|
@@ -137,6 +140,8 @@ export function defaultAckItem(agentUid, apiBaseUrl) {
|
|
|
137
140
|
};
|
|
138
141
|
}
|
|
139
142
|
export async function pollInboxOnce(deps) {
|
|
143
|
+
if (deps.signal?.aborted)
|
|
144
|
+
return { ok: false, mirrored: 0 };
|
|
140
145
|
const fetchInbox = deps.fetchInbox ?? defaultFetchInbox(deps.agentUid, deps.apiBaseUrl);
|
|
141
146
|
const ackItem = deps.ackItem ?? defaultAckItem(deps.agentUid, deps.apiBaseUrl);
|
|
142
147
|
// No default cache warm: the human conversation routes 403 for agent callers.
|
|
@@ -152,9 +157,11 @@ export async function pollInboxOnce(deps) {
|
|
|
152
157
|
}
|
|
153
158
|
let res;
|
|
154
159
|
try {
|
|
155
|
-
res = await fetchInbox(token);
|
|
160
|
+
res = await fetchInbox(token, deps.signal);
|
|
156
161
|
}
|
|
157
162
|
catch (err) {
|
|
163
|
+
if (deps.signal?.aborted)
|
|
164
|
+
return { ok: false, mirrored: 0 };
|
|
158
165
|
writeComponentStatus(deps.paths, "inbox", "error");
|
|
159
166
|
deps.log("error", `inbox fetch failed: GET ${agentInboxUrl(deps.apiBaseUrl, deps.agentUid)}: ${err instanceof Error ? err.message : String(err)}`);
|
|
160
167
|
return { ok: false, mirrored: 0 };
|
|
@@ -218,14 +225,46 @@ async function maybeWake(deps, freshCount) {
|
|
|
218
225
|
}
|
|
219
226
|
}
|
|
220
227
|
export async function runInboxLoop(deps) {
|
|
221
|
-
const sleep = deps.sleep ?? ((ms) => new Promise((
|
|
228
|
+
const sleep = deps.sleep ?? ((ms, signal) => new Promise((resolve) => {
|
|
229
|
+
if (signal?.aborted)
|
|
230
|
+
return resolve();
|
|
231
|
+
const timer = setTimeout(done, ms);
|
|
232
|
+
const abort = () => {
|
|
233
|
+
clearTimeout(timer);
|
|
234
|
+
done();
|
|
235
|
+
};
|
|
236
|
+
function done() {
|
|
237
|
+
signal?.removeEventListener("abort", abort);
|
|
238
|
+
resolve();
|
|
239
|
+
}
|
|
240
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
241
|
+
}));
|
|
242
|
+
const readBackoffEnabled = deps.readInboxBackoffEnabled ?? createAgentKitInboxBackoffFlagReader({
|
|
243
|
+
companySlug: deps.companySlug,
|
|
244
|
+
getToken: deps.getToken,
|
|
245
|
+
});
|
|
222
246
|
let polls = 0;
|
|
223
|
-
|
|
224
|
-
|
|
247
|
+
let consecutiveEmptyPolls = 0;
|
|
248
|
+
while (!deps.signal?.aborted) {
|
|
249
|
+
const result = await pollInboxOnce(deps);
|
|
225
250
|
polls += 1;
|
|
226
251
|
if (deps.maxPolls !== undefined && polls >= deps.maxPolls)
|
|
227
252
|
return;
|
|
228
|
-
|
|
253
|
+
if (deps.signal?.aborted)
|
|
254
|
+
return;
|
|
255
|
+
if (!result.ok || result.mirrored > 0)
|
|
256
|
+
consecutiveEmptyPolls = 0;
|
|
257
|
+
const enabled = await readBackoffEnabled();
|
|
258
|
+
const delay = enabled && result.ok && result.mirrored === 0 && consecutiveEmptyPolls > 0
|
|
259
|
+
? Math.min(deps.pollMs * 2 ** consecutiveEmptyPolls, Math.max(deps.pollMs, INBOX_POLL_BACKOFF_MAX_MS))
|
|
260
|
+
: deps.pollMs;
|
|
261
|
+
if (enabled && result.ok && result.mirrored === 0)
|
|
262
|
+
consecutiveEmptyPolls += 1;
|
|
263
|
+
else
|
|
264
|
+
consecutiveEmptyPolls = 0;
|
|
265
|
+
if (deps.signal?.aborted)
|
|
266
|
+
return;
|
|
267
|
+
await sleep(delay, deps.signal);
|
|
229
268
|
}
|
|
230
269
|
}
|
|
231
270
|
//# sourceMappingURL=inbox.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
export declare const KIT_STOP_GRACE_MS = 10000;
|
|
3
|
+
interface KitStopControllerOptions {
|
|
4
|
+
emitter?: Pick<EventEmitter, "once" | "removeListener">;
|
|
5
|
+
forceExit?: (code: number) => void;
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
}
|
|
8
|
+
/** Abort service work on stop, with a deadline if an awaited operation stalls. */
|
|
9
|
+
export declare function createKitStopController(options?: KitStopControllerOptions): {
|
|
10
|
+
signal: AbortSignal;
|
|
11
|
+
dispose(): void;
|
|
12
|
+
};
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=stop-signals.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const KIT_STOP_GRACE_MS = 10_000;
|
|
2
|
+
/** Abort service work on stop, with a deadline if an awaited operation stalls. */
|
|
3
|
+
export function createKitStopController(options = {}) {
|
|
4
|
+
const emitter = options.emitter ?? process;
|
|
5
|
+
const forceExit = options.forceExit ?? ((code) => process.exit(code));
|
|
6
|
+
const controller = new AbortController();
|
|
7
|
+
let deadline;
|
|
8
|
+
const stop = () => {
|
|
9
|
+
if (controller.signal.aborted)
|
|
10
|
+
return;
|
|
11
|
+
controller.abort();
|
|
12
|
+
deadline = setTimeout(() => forceExit(0), options.timeoutMs ?? KIT_STOP_GRACE_MS);
|
|
13
|
+
};
|
|
14
|
+
emitter.once("SIGTERM", stop);
|
|
15
|
+
emitter.once("SIGINT", stop);
|
|
16
|
+
return {
|
|
17
|
+
signal: controller.signal,
|
|
18
|
+
dispose() {
|
|
19
|
+
if (deadline)
|
|
20
|
+
clearTimeout(deadline);
|
|
21
|
+
emitter.removeListener("SIGTERM", stop);
|
|
22
|
+
emitter.removeListener("SIGINT", stop);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=stop-signals.js.map
|
|
@@ -14,8 +14,8 @@ export declare function isKitService(value: string): value is KitService;
|
|
|
14
14
|
* file a loud error instead of a browser-login hang; HQ_AGENT_DIR keeps a
|
|
15
15
|
* relocated kit consistent across restarts.
|
|
16
16
|
*/
|
|
17
|
-
export declare function kitServiceEnv(paths: AgentKitPaths): Record<string, string>;
|
|
18
|
-
export declare function kitServiceSpec(service: KitService, paths: AgentKitPaths): ServiceSpec;
|
|
17
|
+
export declare function kitServiceEnv(paths: AgentKitPaths, env?: NodeJS.ProcessEnv): Record<string, string>;
|
|
18
|
+
export declare function kitServiceSpec(service: KitService, paths: AgentKitPaths, env?: NodeJS.ProcessEnv): ServiceSpec;
|
|
19
19
|
/**
|
|
20
20
|
* Services to install. `sync` is included only when the kit is configured to
|
|
21
21
|
* mirror the vault to disk; by default a bot reads files on demand through the
|
|
@@ -20,14 +20,16 @@ const DESCRIPTIONS = {
|
|
|
20
20
|
* file a loud error instead of a browser-login hang; HQ_AGENT_DIR keeps a
|
|
21
21
|
* relocated kit consistent across restarts.
|
|
22
22
|
*/
|
|
23
|
-
export function kitServiceEnv(paths) {
|
|
23
|
+
export function kitServiceEnv(paths, env = process.env) {
|
|
24
|
+
const flagsApiUrl = env.HQ_FLAGS_API_URL;
|
|
24
25
|
return {
|
|
25
26
|
HQ_AGENT_DIR: paths.agentDir,
|
|
26
27
|
HQ_MACHINE_CREDS_FILE: paths.machineCredsPath,
|
|
27
28
|
HQ_REQUIRE_MACHINE_IDENTITY: "1",
|
|
29
|
+
...(flagsApiUrl?.trim() ? { HQ_FLAGS_API_URL: flagsApiUrl } : {}),
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
|
-
export function kitServiceSpec(service, paths) {
|
|
32
|
+
export function kitServiceSpec(service, paths, env = process.env) {
|
|
31
33
|
return {
|
|
32
34
|
name: service,
|
|
33
35
|
label: `${KIT_LABEL_PREFIX}.${service}`,
|
|
@@ -35,7 +37,7 @@ export function kitServiceSpec(service, paths) {
|
|
|
35
37
|
args: ["agent", "kit", "run", service],
|
|
36
38
|
logPath: serviceLogPath(paths, service),
|
|
37
39
|
workingDir: paths.agentDir,
|
|
38
|
-
env: kitServiceEnv(paths),
|
|
40
|
+
env: kitServiceEnv(paths, env),
|
|
39
41
|
restartSec: 10,
|
|
40
42
|
};
|
|
41
43
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** hq-flags key for the staged HQ Anywhere runtime. The registry default is on. */
|
|
2
2
|
export declare const HQ_ANYWHERE_RUNTIME_FLAG = "hq-anywhere-runtime";
|
|
3
|
+
export declare const HQ_ANYWHERE_RUNTIME_REVOCATION_MARKER = "hq-anywhere-runtime.revoked";
|
|
3
4
|
/** The HQ Anywhere environment kill switch stays compatible with managed provider wrappers. */
|
|
4
5
|
export declare function hqAnywhereRuntimeLocallyDisabled(env?: NodeJS.ProcessEnv): boolean;
|
|
5
6
|
//# sourceMappingURL=anywhere-runtime-flag-key.d.ts.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** hq-flags key for the staged HQ Anywhere runtime. The registry default is on. */
|
|
2
2
|
export const HQ_ANYWHERE_RUNTIME_FLAG = "hq-anywhere-runtime";
|
|
3
|
+
export const HQ_ANYWHERE_RUNTIME_REVOCATION_MARKER = "hq-anywhere-runtime.revoked";
|
|
3
4
|
/** The HQ Anywhere environment kill switch stays compatible with managed provider wrappers. */
|
|
4
5
|
export function hqAnywhereRuntimeLocallyDisabled(env = process.env) {
|
|
5
6
|
const value = env.HQ_FLAG_HQ_ANYWHERE_RUNTIME;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function anywhereRuntimeRevocationMarkerPath(home?: string, env?: NodeJS.ProcessEnv): string;
|
|
2
|
+
/** Keep installed wrappers aligned with the daemon's refreshed access decision. */
|
|
3
|
+
export declare function updateAnywhereRuntimeRevocationMarker(enabled: boolean, home?: string, env?: NodeJS.ProcessEnv): void;
|
|
4
|
+
//# sourceMappingURL=anywhere-runtime-revocation-marker.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { HQ_ANYWHERE_RUNTIME_REVOCATION_MARKER } from "./anywhere-runtime-flag-key.js";
|
|
5
|
+
import { registryDir } from "./registry/registry.js";
|
|
6
|
+
export function anywhereRuntimeRevocationMarkerPath(home = os.homedir(), env = process.env) {
|
|
7
|
+
return path.join(registryDir(home, env), HQ_ANYWHERE_RUNTIME_REVOCATION_MARKER);
|
|
8
|
+
}
|
|
9
|
+
/** Keep installed wrappers aligned with the daemon's refreshed access decision. */
|
|
10
|
+
export function updateAnywhereRuntimeRevocationMarker(enabled, home = os.homedir(), env = process.env) {
|
|
11
|
+
const marker = anywhereRuntimeRevocationMarkerPath(home, env);
|
|
12
|
+
if (!enabled) {
|
|
13
|
+
fs.mkdirSync(path.dirname(marker), { recursive: true, mode: 0o700 });
|
|
14
|
+
fs.writeFileSync(marker, "revoked\n", { mode: 0o600 });
|
|
15
|
+
fs.chmodSync(marker, 0o600);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
fs.unlinkSync(marker);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error.code !== "ENOENT")
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=anywhere-runtime-revocation-marker.js.map
|
package/dist/lib/daemon/run.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface DaemonLockConflictOutcome {
|
|
|
29
29
|
export declare function daemonLockConflictOutcome(lock: DaemonLockResult, anywhereEnabled: boolean): DaemonLockConflictOutcome | null;
|
|
30
30
|
/** hqd is available on every platform while Anywhere is enabled; its listener fails closed if setup is unsafe. */
|
|
31
31
|
export declare function shouldStartHqd(anywhereEnabled: boolean): boolean;
|
|
32
|
+
/** Resolve the supervisor's runtime-access decision and reflect it in the local wrapper marker. */
|
|
33
|
+
export declare function refreshHqdRuntimeDecision(readDecision: () => Promise<boolean>, home: string, env: NodeJS.ProcessEnv, onMarkerError?: (error: unknown) => void): Promise<boolean>;
|
|
32
34
|
/** Route an updater request through the daemon's existing graceful handoff. */
|
|
33
35
|
export declare function handoffForPackageUpdateRequest(requested: boolean, restartDaemon: () => void): void;
|
|
34
36
|
export interface RunDaemonOptions {
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -30,6 +30,7 @@ import { retireLegacyMeshUnits } from "./migrate.js";
|
|
|
30
30
|
import { daemonDir as meshDaemonDir, daemonPidPath as meshDaemonPidPath } from "../mesh/live/daemon/paths.js";
|
|
31
31
|
import { readPidLock } from "../mesh/live/daemon/pid-lock.js";
|
|
32
32
|
import { registryDir, registryFile } from "../registry/registry.js";
|
|
33
|
+
import { updateAnywhereRuntimeRevocationMarker } from "../anywhere-runtime-revocation-marker.js";
|
|
33
34
|
import { daemonRootRefusal } from "./install.js";
|
|
34
35
|
import { detectMachineType } from "./machine-type.js";
|
|
35
36
|
import { appendPrivate, daemonPaths, ensurePrivateDir, openPrivateAppend } from "./paths.js";
|
|
@@ -93,6 +94,17 @@ export function daemonLockConflictOutcome(lock, anywhereEnabled) {
|
|
|
93
94
|
export function shouldStartHqd(anywhereEnabled) {
|
|
94
95
|
return anywhereEnabled;
|
|
95
96
|
}
|
|
97
|
+
/** Resolve the supervisor's runtime-access decision and reflect it in the local wrapper marker. */
|
|
98
|
+
export async function refreshHqdRuntimeDecision(readDecision, home, env, onMarkerError = () => { }) {
|
|
99
|
+
const enabled = await readDecision();
|
|
100
|
+
try {
|
|
101
|
+
updateAnywhereRuntimeRevocationMarker(enabled, home, env);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
onMarkerError(error);
|
|
105
|
+
}
|
|
106
|
+
return enabled;
|
|
107
|
+
}
|
|
96
108
|
/** Route an updater request through the daemon's existing graceful handoff. */
|
|
97
109
|
export function handoffForPackageUpdateRequest(requested, restartDaemon) {
|
|
98
110
|
if (requested)
|
|
@@ -897,13 +909,13 @@ export async function runDaemon(opts) {
|
|
|
897
909
|
onError: (name) => log("warn", `hqd person setting read failed (${name}); keeping the last read value for up to ${HQD_PERSON_SETTING_GRACE_MS / 60_000} minutes`),
|
|
898
910
|
}), { graceMs: HQD_PERSON_SETTING_GRACE_MS });
|
|
899
911
|
const hqdListener = await startHqdRuntimeListener({
|
|
900
|
-
isEnabled: async () => shouldStartHqd(await anywhereRuntimeAllowed(env, {
|
|
912
|
+
isEnabled: async () => refreshHqdRuntimeDecision(async () => shouldStartHqd(await anywhereRuntimeAllowed(env, {
|
|
901
913
|
refreshFlag: async () => {
|
|
902
914
|
const { refreshFlagRegistrySnapshot } = await import("../flag-registry.js");
|
|
903
915
|
await refreshFlagRegistrySnapshot(5_000);
|
|
904
916
|
},
|
|
905
917
|
readSetting: readPersonSetting,
|
|
906
|
-
})),
|
|
918
|
+
})), home, env, () => log("warn", "hqd runtime revocation marker update failed")),
|
|
907
919
|
startServer: async () => {
|
|
908
920
|
const startConfig = readDaemonConfig(paths.configPath);
|
|
909
921
|
return startHqdServer({
|
|
@@ -959,8 +971,9 @@ export async function runDaemon(opts) {
|
|
|
959
971
|
releaseDaemonLock(paths.lockPath);
|
|
960
972
|
resolve(exitCode);
|
|
961
973
|
// Pipes, sockets and unrelated timers can outlive a clean shutdown.
|
|
962
|
-
//
|
|
963
|
-
|
|
974
|
+
// Services and the lock are already stopped; unrelated handles should
|
|
975
|
+
// not hold a restart or signal exit open for another service interval.
|
|
976
|
+
setTimeout(() => process.exit(exitCode), 1_000).unref();
|
|
964
977
|
});
|
|
965
978
|
};
|
|
966
979
|
/** Exit for a restart but leave the services running for the next daemon to adopt. */
|
|
@@ -23,6 +23,7 @@ import { defaultMcpToken, HqMcpBridge, jsonResult, McpOpError, runWorker } from
|
|
|
23
23
|
import { authenticateMachineCredentials, machineTokenExpiresAt, MachineLoginError, parseStdinMachineCredentials } from "./machine-login.js";
|
|
24
24
|
import { HQ_MCP_TOOL_NAMES, HQD_NATIVE_TOOL_NAMES } from "../mcp/tools.js";
|
|
25
25
|
import { HQ_ANYWHERE_RUNTIME_FLAG } from "../../commands/anywhere-runtime-flag.js";
|
|
26
|
+
import { updateAnywhereRuntimeRevocationMarker } from "../anywhere-runtime-revocation-marker.js";
|
|
26
27
|
import { readHqAnywherePersonSetting } from "../anywhere-person-setting.js";
|
|
27
28
|
import { createBoundedDaemonFlagMonitor } from "./flag-monitor.js";
|
|
28
29
|
import { readDaemonFlag } from "./flag.js";
|
|
@@ -416,6 +417,12 @@ export async function startHqdServer(opts) {
|
|
|
416
417
|
return personSetting === true;
|
|
417
418
|
},
|
|
418
419
|
onChange: (enabled) => {
|
|
420
|
+
try {
|
|
421
|
+
updateAnywhereRuntimeRevocationMarker(enabled, env.HOME?.trim() || undefined, env);
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
log("warn", `hqd runtime revocation marker update failed (${error instanceof Error ? error.name : "UnknownError"})`);
|
|
425
|
+
}
|
|
419
426
|
if (!enabled) {
|
|
420
427
|
flagRevocationGeneration += 1;
|
|
421
428
|
ephemeralMachineToken = undefined;
|
|
@@ -71,7 +71,18 @@ export interface RemoteUsageResponse {
|
|
|
71
71
|
apps?: RemoteAppUsage[];
|
|
72
72
|
}
|
|
73
73
|
/** Answer for one app: its own numbers, or the company numbers when it shares the company cluster. */
|
|
74
|
-
export
|
|
74
|
+
export interface RemoteRemovedAppUsage {
|
|
75
|
+
companyUid: string;
|
|
76
|
+
appId: string;
|
|
77
|
+
mode: "app-cluster" | "company-cluster";
|
|
78
|
+
status: "detached" | "purged";
|
|
79
|
+
removedAt: string;
|
|
80
|
+
retainUntil: string | null;
|
|
81
|
+
purgedAt: string | null;
|
|
82
|
+
billed: false;
|
|
83
|
+
exempt: boolean;
|
|
84
|
+
}
|
|
85
|
+
export type RemoteAppUsageResponse = RemoteRemovedAppUsage | (RemoteAppUsage & {
|
|
75
86
|
companyUid: string;
|
|
76
87
|
mode: "app-cluster";
|
|
77
88
|
status?: string;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { RemoteAppUsage, RemoteAppUsageResponse, RemoteUsageResponse } from "./control-plane.js";
|
|
1
|
+
import type { RemoteAppUsage, RemoteAppUsageResponse, RemoteRemovedAppUsage, RemoteUsageResponse } from "./control-plane.js";
|
|
2
2
|
export declare function formatBytes(bytes: number): string;
|
|
3
3
|
export declare function formatUsage(usage: RemoteUsageResponse): string;
|
|
4
4
|
export declare function formatAppUsage(usage: RemoteAppUsage): string;
|
|
5
|
+
export declare function formatRemovedAppUsage(usage: RemoteRemovedAppUsage): string;
|
|
5
6
|
export declare function formatAppUsageResponse(response: RemoteAppUsageResponse): string;
|
|
6
7
|
export declare function formatCompanyApps(apps: RemoteAppUsage[]): string;
|
|
7
8
|
//# sourceMappingURL=usage-format.d.ts.map
|
|
@@ -80,7 +80,26 @@ export function formatAppUsage(usage) {
|
|
|
80
80
|
: `Last measured ${usage.measuredAt}. Numbers can lag by a few minutes.`);
|
|
81
81
|
return lines.join("\n");
|
|
82
82
|
}
|
|
83
|
+
function day(iso) {
|
|
84
|
+
return iso.slice(0, 10);
|
|
85
|
+
}
|
|
86
|
+
export function formatRemovedAppUsage(usage) {
|
|
87
|
+
const lines = [`The database for app ${usage.appId} was removed on ${day(usage.removedAt)}.`];
|
|
88
|
+
if (usage.purgedAt) {
|
|
89
|
+
lines.push(`Its data was deleted for good on ${day(usage.purgedAt)}.`);
|
|
90
|
+
}
|
|
91
|
+
else if (usage.retainUntil) {
|
|
92
|
+
lines.push(`Its data is kept until ${day(usage.retainUntil)}, then deleted for good.`);
|
|
93
|
+
}
|
|
94
|
+
lines.push("It is not billed.");
|
|
95
|
+
return lines.join("\n");
|
|
96
|
+
}
|
|
97
|
+
function isRemoved(response) {
|
|
98
|
+
return "status" in response && (response.status === "detached" || response.status === "purged");
|
|
99
|
+
}
|
|
83
100
|
export function formatAppUsageResponse(response) {
|
|
101
|
+
if (isRemoved(response))
|
|
102
|
+
return formatRemovedAppUsage(response);
|
|
84
103
|
if (response.mode === "company-cluster") {
|
|
85
104
|
return [
|
|
86
105
|
`App ${response.appId} uses the shared company database, so these are the company numbers.`,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { HQ_ANYWHERE_RUNTIME_REVOCATION_MARKER } from '../anywhere-runtime-flag-key.js';
|
|
4
5
|
const marker = '# hq-anywhere provider sign-in wrapper';
|
|
5
6
|
const PROVIDER_PATH_RC_FILES = ['.bashrc', '.bash_profile', '.zshrc', '.zprofile', '.profile'];
|
|
6
7
|
const wrapperHeader = (provider) => `#!/bin/sh\n${marker}: ${provider}\n`;
|
|
@@ -76,6 +77,18 @@ function wrapperSource(provider) {
|
|
|
76
77
|
` ${provider === 'claude' ? '/login|auth' : 'login'}) exec "$real" "$@" ;;`,
|
|
77
78
|
'esac',
|
|
78
79
|
'',
|
|
80
|
+
'# The local marker mirrors registryDir: trim the override and use ~/.hq when empty.',
|
|
81
|
+
'registry_dir=${HQ_REGISTRY_DIR:-}',
|
|
82
|
+
"registry_dir=$(printf '%s' \"$registry_dir\" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')",
|
|
83
|
+
'if [ -z "$registry_dir" ]; then',
|
|
84
|
+
' registry_dir=${HOME:-}',
|
|
85
|
+
' [ -n "$registry_dir" ] && registry_dir="$registry_dir/.hq"',
|
|
86
|
+
'fi',
|
|
87
|
+
'if [ -n "$registry_dir" ]; then',
|
|
88
|
+
' case "$registry_dir" in /*) ;; *) registry_dir="$PWD/$registry_dir" ;; esac',
|
|
89
|
+
` if [ -e "$registry_dir/${HQ_ANYWHERE_RUNTIME_REVOCATION_MARKER}" ]; then exec "$real" "$@"; fi`,
|
|
90
|
+
'fi',
|
|
91
|
+
'',
|
|
79
92
|
'log_dir="$HOME/.hq/logs"',
|
|
80
93
|
'mkdir -p "$log_dir" 2>/dev/null || :',
|
|
81
94
|
'log_file="$log_dir/provider-auth.log"',
|
package/dist/lib/lanes/loop.d.ts
CHANGED
|
@@ -60,6 +60,13 @@ export interface LoopResult {
|
|
|
60
60
|
session_id?: string;
|
|
61
61
|
resumed?: boolean;
|
|
62
62
|
fresh_fallback?: boolean;
|
|
63
|
+
question_id?: string;
|
|
64
|
+
decision?: {
|
|
65
|
+
question_id: string;
|
|
66
|
+
choice: string;
|
|
67
|
+
notes?: string;
|
|
68
|
+
at: string;
|
|
69
|
+
};
|
|
63
70
|
}
|
|
64
71
|
export interface Section8Handoff {
|
|
65
72
|
schema: typeof PHASE_HANDOFF_SCHEMA;
|
|
@@ -103,6 +110,7 @@ export interface LoopRunnerOptions {
|
|
|
103
110
|
envelopeId: string;
|
|
104
111
|
attempt: number;
|
|
105
112
|
error: string;
|
|
113
|
+
questionId: string;
|
|
106
114
|
}): Promise<void> | void;
|
|
107
115
|
defaultDeadlineSeconds?: number;
|
|
108
116
|
now?: () => Date;
|
|
@@ -120,7 +128,6 @@ interface StoredEnvelope {
|
|
|
120
128
|
envelope_id: string;
|
|
121
129
|
queued_at: string;
|
|
122
130
|
deadline_budget_seconds?: number;
|
|
123
|
-
deadline_at_ms?: number;
|
|
124
131
|
envelope: LoopEnvelope;
|
|
125
132
|
}
|
|
126
133
|
/** Validate both hq-cli phase envelopes and the conduct protocol §8 shape. */
|
|
@@ -148,6 +155,21 @@ export declare function interruptLoopQueue(runDir: string): {
|
|
|
148
155
|
withdrawn: string[];
|
|
149
156
|
already_picked_up: string[];
|
|
150
157
|
};
|
|
158
|
+
export declare function interruptLoopQueueForStory(runDir: string, storyId: string, phase?: string): {
|
|
159
|
+
withdrawn: string[];
|
|
160
|
+
already_picked_up: string[];
|
|
161
|
+
};
|
|
162
|
+
export declare function applyLoopStallAnswer(input: {
|
|
163
|
+
runDir: string;
|
|
164
|
+
questionId: string;
|
|
165
|
+
choice: string;
|
|
166
|
+
notes?: string;
|
|
167
|
+
at: string;
|
|
168
|
+
}): {
|
|
169
|
+
lane_id: string;
|
|
170
|
+
envelope_id: string;
|
|
171
|
+
action: "requeued" | "parked";
|
|
172
|
+
} | undefined;
|
|
151
173
|
export declare function loopStatus(runDir: string, laneId: string): LoopStatus;
|
|
152
174
|
export declare function setLoopControllerPid(runDir: string, laneId: string, pid: number | undefined): void;
|
|
153
175
|
export declare function clearLoopControllerPid(runDir: string, laneId: string, pid: number): void;
|