@indigoai-us/hq-cli 5.345.106 → 5.345.107
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 +16 -0
- package/assets/scaffold/core/scripts/hq-task.mjs +259 -0
- package/assets/scaffold/core/scripts/hq-task.sh +31 -0
- package/assets/scaffold/core/scripts/pipeline-worker-table.sh +285 -0
- package/dist/command-catalog.generated.d.ts +135 -5
- package/dist/command-catalog.generated.js +161 -5
- package/dist/commands/auth.js +42 -0
- package/dist/commands/core-hq-task-fast.d.ts +4 -0
- package/dist/commands/core-hq-task-fast.js +11 -0
- package/dist/commands/core-hq-task-native.d.ts +20 -0
- package/dist/commands/core-hq-task-native.js +275 -0
- package/dist/commands/core-native-bucket-c-flag.d.ts +3 -0
- package/dist/commands/core-native-bucket-c-flag.js +6 -0
- package/dist/commands/core-native-family-flag.d.ts +0 -2
- package/dist/commands/core-native-family-flag.js +0 -4
- package/dist/commands/core-native-pipeline-flag.d.ts +3 -0
- package/dist/commands/core-native-pipeline-flag.js +6 -0
- package/dist/commands/core-ontology-native.js +24 -30
- package/dist/commands/core-pipeline-workers-command.d.ts +3 -0
- package/dist/commands/core-pipeline-workers-command.js +37 -0
- package/dist/commands/core-pipeline-workers-native.d.ts +13 -0
- package/dist/commands/core-pipeline-workers-native.js +361 -0
- package/dist/commands/core.js +12 -10
- package/dist/commands/lanes.js +32 -7
- package/dist/commands/outposts.d.ts +5 -0
- package/dist/commands/outposts.js +5 -0
- package/dist/fast-scaffold.d.ts +1 -1
- package/dist/fast-scaffold.js +13 -5
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +56937 -64550
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/daemon/remote-control-pools.d.ts +2 -2
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/reconcile.js +4 -0
- package/dist/lib/lanes/remote-control-lane.d.ts +33 -0
- package/dist/lib/lanes/remote-control-lane.js +276 -0
- package/dist/lib/lanes/spawn.d.ts +5 -0
- package/dist/lib/lanes/spawn.js +65 -14
- package/dist/lib/lanes/store.js +7 -0
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.js +8 -1
- package/dist/utils/cognito-session.d.ts +12 -4
- package/dist/utils/cognito-session.js +42 -9
- package/dist/utils/token-keychain.d.ts +96 -0
- package/dist/utils/token-keychain.js +218 -0
- package/package.json +2 -2
- package/assets/scaffold/core/scripts/ontology-audience-grant.sh +0 -81
- package/assets/scaffold/core/scripts/ontology-brief.mjs +0 -93
- package/assets/scaffold/core/scripts/ontology-candidate.sh +0 -100
- package/assets/scaffold/core/scripts/ontology-garden.mjs +0 -327
- package/assets/scaffold/core/scripts/ontology-source-worklist.sh +0 -226
- package/assets/scaffold/core/scripts/source-yaml-validate.sh +0 -36
|
@@ -25,15 +25,16 @@ import * as os from "os";
|
|
|
25
25
|
import * as path from "path";
|
|
26
26
|
import * as yaml from "js-yaml";
|
|
27
27
|
import chalk from "chalk";
|
|
28
|
-
import { loadCachedTokens, isExpiring, refreshTokens, saveCachedTokens, browserLogin, detectHqCoreVersion, isMachineIdentity, getValidMachineTokens, CognitoRefreshError, accessTokenFingerprint, invalidateCachedTokensByFingerprint, } from "@indigoai-us/hq-cloud";
|
|
28
|
+
import { loadCachedTokens as loadCachedTokensFromFile, isExpiring, refreshTokens, saveCachedTokens, browserLogin, detectHqCoreVersion, isMachineIdentity, getValidMachineTokens, CognitoRefreshError, accessTokenFingerprint, invalidateCachedTokensByFingerprint, } from "@indigoai-us/hq-cloud";
|
|
29
29
|
import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
|
|
30
30
|
import { daemonDir } from "../lib/mesh/live/daemon/paths.js";
|
|
31
31
|
import { workMeshRoot } from "../lib/mesh/live/paths.js";
|
|
32
32
|
import { acquireLockFileExclusively } from "./exclusive-lock.js";
|
|
33
33
|
import { reportNoninteractivePersonRefreshRejection } from "./cognito-refresh-sentry.js";
|
|
34
|
+
import { pickNewerSession, readKeychainTokens } from "./token-keychain.js";
|
|
34
35
|
// Re-export the cached-session primitives for callers that must deliberately
|
|
35
36
|
// avoid the refresh/login behavior in the higher-level helpers below.
|
|
36
|
-
export { isExpiring, isMachineIdentity
|
|
37
|
+
export { isExpiring, isMachineIdentity };
|
|
37
38
|
/**
|
|
38
39
|
* Override for where machine-minted Cognito tokens are cached.
|
|
39
40
|
* Default: the work-mesh daemon state dir (`~/.hq/work-mesh/daemon`), so a
|
|
@@ -313,10 +314,9 @@ export function personTokenCacheFile(home = os.homedir(), env = process.env) {
|
|
|
313
314
|
export function hasPersonCachedSession(env = process.env) {
|
|
314
315
|
try {
|
|
315
316
|
const file = personTokenCacheFile(os.homedir(), env);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (typeof raw.accessToken !== "string" ||
|
|
317
|
+
const raw = readPersonTokenFile(os.homedir(), env);
|
|
318
|
+
if (!raw ||
|
|
319
|
+
typeof raw.accessToken !== "string" ||
|
|
320
320
|
raw.accessToken.length === 0 ||
|
|
321
321
|
typeof raw.idToken !== "string" ||
|
|
322
322
|
raw.idToken.length === 0) {
|
|
@@ -474,12 +474,45 @@ function readTokenFile(file) {
|
|
|
474
474
|
}
|
|
475
475
|
}
|
|
476
476
|
/**
|
|
477
|
-
*
|
|
477
|
+
* The person session from the shared macOS Keychain item (see
|
|
478
|
+
* ./token-keychain.ts), or null. Not read when the token file is missing:
|
|
479
|
+
* no file means signed out. Machine-identity and invalidated tokens are
|
|
480
|
+
* refused, the same as the file loader.
|
|
481
|
+
*/
|
|
482
|
+
function personKeychainTokens(file, home, env, keychain = {}) {
|
|
483
|
+
if (!fs.existsSync(file))
|
|
484
|
+
return null;
|
|
485
|
+
const tokens = readKeychainTokens({ env, home, ...keychain });
|
|
486
|
+
if (!tokens)
|
|
487
|
+
return null;
|
|
488
|
+
const claims = decodeJwtPayload(tokens.idToken);
|
|
489
|
+
const entityType = claims?.["custom:entityType"];
|
|
490
|
+
if (typeof entityType === "string" && entityType.trim().length > 0)
|
|
491
|
+
return null;
|
|
492
|
+
if (fs.existsSync(`${file}.invalid.${accessTokenFingerprint(tokens.accessToken)}`))
|
|
493
|
+
return null;
|
|
494
|
+
return tokens;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Person session for `env`: the Keychain item and the token file, the newer
|
|
498
|
+
* by `expiresAt` (see ./token-keychain.ts). Does not consult hq-cloud's
|
|
478
499
|
* in-memory fallback, which can surface a different session when the file
|
|
479
500
|
* is missing or unreadable.
|
|
480
501
|
*/
|
|
481
|
-
export function readPersonTokenFile(home = os.homedir(), env = process.env) {
|
|
482
|
-
|
|
502
|
+
export function readPersonTokenFile(home = os.homedir(), env = process.env, keychain = {}) {
|
|
503
|
+
const file = personTokenCacheFile(home, env);
|
|
504
|
+
return pickNewerSession(personKeychainTokens(file, home, env, keychain), readTokenFile(file));
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* The person's cached session: the shared Keychain item and hq-cloud's file
|
|
508
|
+
* loader, the newer by `expiresAt`. Writes still go only to the file in this
|
|
509
|
+
* step.
|
|
510
|
+
*/
|
|
511
|
+
export function loadCachedTokens(env = process.env, keychain = {}) {
|
|
512
|
+
const home = os.homedir();
|
|
513
|
+
const file = personTokenCacheFile(home, env);
|
|
514
|
+
const fromFile = loadCachedTokensFromFile();
|
|
515
|
+
return pickNewerSession(personKeychainTokens(file, home, env, keychain), fromFile);
|
|
483
516
|
}
|
|
484
517
|
function restoreTokenFile(file, before) {
|
|
485
518
|
if (before) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* macOS Keychain read of the person Cognito session (step 1 of moving the
|
|
3
|
+
* session off `~/.hq/cognito-tokens.json`).
|
|
4
|
+
*
|
|
5
|
+
* Shared item (must match hq-desktop-app `token_keychain.rs`, desktop #1586,
|
|
6
|
+
* and hq-cloud `src/token-keychain.ts`):
|
|
7
|
+
* - class: generic password, login keychain
|
|
8
|
+
* - service: `ai.indigo.hq.cognito-tokens`
|
|
9
|
+
* - account: `person`
|
|
10
|
+
* - value: the same camelCase JSON as the token file
|
|
11
|
+
* - signer: Team ID HWFZ2QDTMR (login-keychain partition `teamid:HWFZ2QDTMR`)
|
|
12
|
+
*
|
|
13
|
+
* The read goes through `/usr/bin/security`, so there is no native module.
|
|
14
|
+
* macOS asks the user once whether `security` may read the item; HQ accepts
|
|
15
|
+
* that prompt (owner decision) and explains it first:
|
|
16
|
+
* - Before the first read, a one-line note is printed to stderr. It is only
|
|
17
|
+
* shown, and the first read only happens, in an interactive terminal, so
|
|
18
|
+
* a background process never raises the prompt unexplained.
|
|
19
|
+
* - A denied prompt, a timeout or any other failure falls back to the file
|
|
20
|
+
* quietly and is remembered in `~/.hq/keychain-access.json`, so HQ does
|
|
21
|
+
* not ask again on every run. `hq auth keychain --retry` clears it.
|
|
22
|
+
* - "Item not found" is a plain miss and is not remembered as a denial.
|
|
23
|
+
*
|
|
24
|
+
* Choosing between the Keychain and the file (writes still go to the file):
|
|
25
|
+
* - No file means signed out; the Keychain is not read. Older tools sign
|
|
26
|
+
* out by deleting only the file, and the desktop clears the stale item.
|
|
27
|
+
* - Both valid: the newer session by `expiresAt` wins; a tie keeps the
|
|
28
|
+
* Keychain value. This matches the installer's rule.
|
|
29
|
+
* - Only one valid: that one.
|
|
30
|
+
*
|
|
31
|
+
* Non-macOS, an `HQ_STATE_DIR` override and `HQ_TOKEN_KEYCHAIN=0` read the
|
|
32
|
+
* file only. Token values are never printed or logged.
|
|
33
|
+
*/
|
|
34
|
+
export declare const KEYCHAIN_SERVICE = "ai.indigo.hq.cognito-tokens";
|
|
35
|
+
export declare const KEYCHAIN_ACCOUNT = "person";
|
|
36
|
+
export declare const KEYCHAIN_TEAM_ID = "HWFZ2QDTMR";
|
|
37
|
+
/** Read timeout once access has been allowed (no prompt expected). */
|
|
38
|
+
export declare const KEYCHAIN_READ_TIMEOUT_MS = 3000;
|
|
39
|
+
/** First read: long enough for a person to answer the macOS prompt. */
|
|
40
|
+
export declare const KEYCHAIN_FIRST_READ_TIMEOUT_MS = 60000;
|
|
41
|
+
export declare const KEYCHAIN_FIRST_READ_NOTE = "macOS will ask once whether HQ can read your sign-in from the Keychain. Choose Always Allow.";
|
|
42
|
+
export interface KeychainTokenShape {
|
|
43
|
+
accessToken: string;
|
|
44
|
+
idToken: string;
|
|
45
|
+
refreshToken?: string;
|
|
46
|
+
expiresAt?: string | number;
|
|
47
|
+
}
|
|
48
|
+
export interface SecurityResult {
|
|
49
|
+
status: number | null;
|
|
50
|
+
stdout: string;
|
|
51
|
+
timedOut: boolean;
|
|
52
|
+
}
|
|
53
|
+
export type SecurityRunner = (args: string[], timeoutMs: number) => SecurityResult;
|
|
54
|
+
export interface KeychainAccessState {
|
|
55
|
+
noteShownAt?: string;
|
|
56
|
+
allowedAt?: string;
|
|
57
|
+
deniedAt?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface KeychainReadOptions {
|
|
60
|
+
platform?: NodeJS.Platform;
|
|
61
|
+
env?: NodeJS.ProcessEnv;
|
|
62
|
+
home?: string;
|
|
63
|
+
runSecurity?: SecurityRunner;
|
|
64
|
+
/** Defaults to whether stderr is a terminal. */
|
|
65
|
+
interactive?: boolean;
|
|
66
|
+
/** Where the first-read note goes. Defaults to stderr. */
|
|
67
|
+
notify?: (line: string) => void;
|
|
68
|
+
/** Ignore a remembered denial and the read cache (`hq auth keychain --retry`). */
|
|
69
|
+
retry?: boolean;
|
|
70
|
+
now?: () => Date;
|
|
71
|
+
}
|
|
72
|
+
export type KeychainReadOutcome = "hit" | "miss" | "denied" | "skipped-denied" | "skipped-not-interactive" | "disabled";
|
|
73
|
+
export declare function keychainTokenReadEnabled(opts?: KeychainReadOptions): boolean;
|
|
74
|
+
export declare function keychainAccessStateFile(home?: string): string;
|
|
75
|
+
export declare function readKeychainAccessState(home?: string): KeychainAccessState;
|
|
76
|
+
export declare function parseTokenJson(raw: string): KeychainTokenShape | null;
|
|
77
|
+
/** Test seam: forget the in-process read cache. */
|
|
78
|
+
export declare function resetKeychainReadCacheForTests(): void;
|
|
79
|
+
/**
|
|
80
|
+
* Read the shared item, applying the first-read note, the remembered denial
|
|
81
|
+
* and the timeouts described above. Never throws.
|
|
82
|
+
*/
|
|
83
|
+
export declare function readKeychainTokensDetailed(opts?: KeychainReadOptions): {
|
|
84
|
+
tokens: KeychainTokenShape | null;
|
|
85
|
+
outcome: KeychainReadOutcome;
|
|
86
|
+
};
|
|
87
|
+
export declare function readKeychainTokens(opts?: KeychainReadOptions): KeychainTokenShape | null;
|
|
88
|
+
/** Newer session by `expiresAt`; a tie keeps the Keychain value. */
|
|
89
|
+
export declare function pickNewerSession<T extends KeychainTokenShape>(fromKeychain: T | null, fromFile: T | null): T | null;
|
|
90
|
+
export type PersonTokenSource = "file" | "keychain" | "none";
|
|
91
|
+
/** The person session from the Keychain and the file, by the rules above. */
|
|
92
|
+
export declare function readPersonTokensWithKeychain(file?: string, opts?: KeychainReadOptions): {
|
|
93
|
+
tokens: KeychainTokenShape | null;
|
|
94
|
+
source: PersonTokenSource;
|
|
95
|
+
};
|
|
96
|
+
//# sourceMappingURL=token-keychain.d.ts.map
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* macOS Keychain read of the person Cognito session (step 1 of moving the
|
|
3
|
+
* session off `~/.hq/cognito-tokens.json`).
|
|
4
|
+
*
|
|
5
|
+
* Shared item (must match hq-desktop-app `token_keychain.rs`, desktop #1586,
|
|
6
|
+
* and hq-cloud `src/token-keychain.ts`):
|
|
7
|
+
* - class: generic password, login keychain
|
|
8
|
+
* - service: `ai.indigo.hq.cognito-tokens`
|
|
9
|
+
* - account: `person`
|
|
10
|
+
* - value: the same camelCase JSON as the token file
|
|
11
|
+
* - signer: Team ID HWFZ2QDTMR (login-keychain partition `teamid:HWFZ2QDTMR`)
|
|
12
|
+
*
|
|
13
|
+
* The read goes through `/usr/bin/security`, so there is no native module.
|
|
14
|
+
* macOS asks the user once whether `security` may read the item; HQ accepts
|
|
15
|
+
* that prompt (owner decision) and explains it first:
|
|
16
|
+
* - Before the first read, a one-line note is printed to stderr. It is only
|
|
17
|
+
* shown, and the first read only happens, in an interactive terminal, so
|
|
18
|
+
* a background process never raises the prompt unexplained.
|
|
19
|
+
* - A denied prompt, a timeout or any other failure falls back to the file
|
|
20
|
+
* quietly and is remembered in `~/.hq/keychain-access.json`, so HQ does
|
|
21
|
+
* not ask again on every run. `hq auth keychain --retry` clears it.
|
|
22
|
+
* - "Item not found" is a plain miss and is not remembered as a denial.
|
|
23
|
+
*
|
|
24
|
+
* Choosing between the Keychain and the file (writes still go to the file):
|
|
25
|
+
* - No file means signed out; the Keychain is not read. Older tools sign
|
|
26
|
+
* out by deleting only the file, and the desktop clears the stale item.
|
|
27
|
+
* - Both valid: the newer session by `expiresAt` wins; a tie keeps the
|
|
28
|
+
* Keychain value. This matches the installer's rule.
|
|
29
|
+
* - Only one valid: that one.
|
|
30
|
+
*
|
|
31
|
+
* Non-macOS, an `HQ_STATE_DIR` override and `HQ_TOKEN_KEYCHAIN=0` read the
|
|
32
|
+
* file only. Token values are never printed or logged.
|
|
33
|
+
*/
|
|
34
|
+
import { spawnSync } from "node:child_process";
|
|
35
|
+
import * as fs from "node:fs";
|
|
36
|
+
import * as os from "node:os";
|
|
37
|
+
import * as path from "node:path";
|
|
38
|
+
export const KEYCHAIN_SERVICE = "ai.indigo.hq.cognito-tokens";
|
|
39
|
+
export const KEYCHAIN_ACCOUNT = "person";
|
|
40
|
+
export const KEYCHAIN_TEAM_ID = "HWFZ2QDTMR";
|
|
41
|
+
/** Read timeout once access has been allowed (no prompt expected). */
|
|
42
|
+
export const KEYCHAIN_READ_TIMEOUT_MS = 3_000;
|
|
43
|
+
/** First read: long enough for a person to answer the macOS prompt. */
|
|
44
|
+
export const KEYCHAIN_FIRST_READ_TIMEOUT_MS = 60_000;
|
|
45
|
+
/** `security` exit status for errSecItemNotFound. */
|
|
46
|
+
const SECURITY_ITEM_NOT_FOUND = 44;
|
|
47
|
+
/** Reuse a read within one process for this long (avoids a spawn per request). */
|
|
48
|
+
const READ_CACHE_MS = 30_000;
|
|
49
|
+
export const KEYCHAIN_FIRST_READ_NOTE = "macOS will ask once whether HQ can read your sign-in from the Keychain. Choose Always Allow.";
|
|
50
|
+
const defaultRunner = (args, timeoutMs) => {
|
|
51
|
+
try {
|
|
52
|
+
const result = spawnSync("/usr/bin/security", args, {
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
timeout: timeoutMs,
|
|
55
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
56
|
+
});
|
|
57
|
+
const timedOut = result.error?.code === "ETIMEDOUT";
|
|
58
|
+
return { status: result.status, stdout: typeof result.stdout === "string" ? result.stdout : "", timedOut };
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { status: null, stdout: "", timedOut: false };
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
export function keychainTokenReadEnabled(opts = {}) {
|
|
65
|
+
const platform = opts.platform ?? process.platform;
|
|
66
|
+
const env = opts.env ?? process.env;
|
|
67
|
+
if (platform !== "darwin")
|
|
68
|
+
return false;
|
|
69
|
+
const flag = env.HQ_TOKEN_KEYCHAIN?.trim().toLowerCase();
|
|
70
|
+
if (flag === "0" || flag === "false" || flag === "off")
|
|
71
|
+
return false;
|
|
72
|
+
// Only the default ~/.hq cache is mirrored into the shared item.
|
|
73
|
+
if (env.HQ_STATE_DIR?.trim())
|
|
74
|
+
return false;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
export function keychainAccessStateFile(home = os.homedir()) {
|
|
78
|
+
return path.join(home, ".hq", "keychain-access.json");
|
|
79
|
+
}
|
|
80
|
+
export function readKeychainAccessState(home = os.homedir()) {
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(fs.readFileSync(keychainAccessStateFile(home), "utf8"));
|
|
83
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function writeKeychainAccessState(home, state) {
|
|
90
|
+
try {
|
|
91
|
+
const file = keychainAccessStateFile(home);
|
|
92
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
93
|
+
const tmp = `${file}.tmp.${process.pid}`;
|
|
94
|
+
fs.writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
95
|
+
fs.renameSync(tmp, file);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Best effort: without the marker HQ may ask again, which is safe.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** `security -w` prints hex when the secret has non-printable bytes (newlines). */
|
|
102
|
+
function decodeSecurityOutput(stdout) {
|
|
103
|
+
const trimmed = stdout.replace(/\r?\n$/, "");
|
|
104
|
+
if (!trimmed.startsWith("{") && /^(?:[0-9a-fA-F]{2})+$/.test(trimmed)) {
|
|
105
|
+
return Buffer.from(trimmed, "hex").toString("utf8");
|
|
106
|
+
}
|
|
107
|
+
return trimmed;
|
|
108
|
+
}
|
|
109
|
+
export function parseTokenJson(raw) {
|
|
110
|
+
try {
|
|
111
|
+
const value = JSON.parse(raw);
|
|
112
|
+
if (!value ||
|
|
113
|
+
typeof value.accessToken !== "string" ||
|
|
114
|
+
value.accessToken.length === 0 ||
|
|
115
|
+
typeof value.idToken !== "string" ||
|
|
116
|
+
value.idToken.length === 0) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
let readCache = null;
|
|
126
|
+
/** Test seam: forget the in-process read cache. */
|
|
127
|
+
export function resetKeychainReadCacheForTests() {
|
|
128
|
+
readCache = null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Read the shared item, applying the first-read note, the remembered denial
|
|
132
|
+
* and the timeouts described above. Never throws.
|
|
133
|
+
*/
|
|
134
|
+
export function readKeychainTokensDetailed(opts = {}) {
|
|
135
|
+
if (!keychainTokenReadEnabled(opts))
|
|
136
|
+
return { tokens: null, outcome: "disabled" };
|
|
137
|
+
const home = opts.home ?? os.homedir();
|
|
138
|
+
const now = opts.now ?? (() => new Date());
|
|
139
|
+
if (!opts.retry && readCache && readCache.home === home && now().getTime() - readCache.at < READ_CACHE_MS) {
|
|
140
|
+
return { tokens: readCache.tokens, outcome: readCache.outcome };
|
|
141
|
+
}
|
|
142
|
+
const remember = (tokens, outcome) => {
|
|
143
|
+
readCache = { at: now().getTime(), home, tokens, outcome };
|
|
144
|
+
return { tokens, outcome };
|
|
145
|
+
};
|
|
146
|
+
let state = readKeychainAccessState(home);
|
|
147
|
+
if (opts.retry && state.deniedAt) {
|
|
148
|
+
const { deniedAt: _cleared, ...rest } = state;
|
|
149
|
+
state = rest;
|
|
150
|
+
writeKeychainAccessState(home, state);
|
|
151
|
+
}
|
|
152
|
+
if (state.deniedAt)
|
|
153
|
+
return remember(null, "skipped-denied");
|
|
154
|
+
// Until a read has succeeded, a read may raise the macOS prompt, so it only
|
|
155
|
+
// happens in a terminal, after the note (shown once).
|
|
156
|
+
if (!state.allowedAt) {
|
|
157
|
+
const interactive = opts.interactive ?? Boolean(process.stderr.isTTY);
|
|
158
|
+
if (!interactive)
|
|
159
|
+
return remember(null, "skipped-not-interactive");
|
|
160
|
+
if (!state.noteShownAt) {
|
|
161
|
+
(opts.notify ?? ((line) => process.stderr.write(`${line}\n`)))(KEYCHAIN_FIRST_READ_NOTE);
|
|
162
|
+
state = { ...state, noteShownAt: now().toISOString() };
|
|
163
|
+
writeKeychainAccessState(home, state);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const run = opts.runSecurity ?? defaultRunner;
|
|
167
|
+
const result = run(["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT, "-w"], state.allowedAt ? KEYCHAIN_READ_TIMEOUT_MS : KEYCHAIN_FIRST_READ_TIMEOUT_MS);
|
|
168
|
+
if (result.status === 0 && !result.timedOut) {
|
|
169
|
+
if (!state.allowedAt)
|
|
170
|
+
writeKeychainAccessState(home, { ...state, allowedAt: now().toISOString() });
|
|
171
|
+
return remember(parseTokenJson(decodeSecurityOutput(result.stdout)), "hit");
|
|
172
|
+
}
|
|
173
|
+
if (result.status === SECURITY_ITEM_NOT_FOUND && !result.timedOut)
|
|
174
|
+
return remember(null, "miss");
|
|
175
|
+
// Denied, timed out, or failed: fall back quietly and don't ask again.
|
|
176
|
+
writeKeychainAccessState(home, { ...state, deniedAt: now().toISOString() });
|
|
177
|
+
return remember(null, "denied");
|
|
178
|
+
}
|
|
179
|
+
export function readKeychainTokens(opts = {}) {
|
|
180
|
+
return readKeychainTokensDetailed(opts).tokens;
|
|
181
|
+
}
|
|
182
|
+
function expiresAtMs(tokens) {
|
|
183
|
+
const value = tokens.expiresAt;
|
|
184
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
185
|
+
return value;
|
|
186
|
+
if (typeof value === "string") {
|
|
187
|
+
const parsed = Date.parse(value);
|
|
188
|
+
if (Number.isFinite(parsed))
|
|
189
|
+
return parsed;
|
|
190
|
+
}
|
|
191
|
+
return Number.NEGATIVE_INFINITY;
|
|
192
|
+
}
|
|
193
|
+
/** Newer session by `expiresAt`; a tie keeps the Keychain value. */
|
|
194
|
+
export function pickNewerSession(fromKeychain, fromFile) {
|
|
195
|
+
if (!fromKeychain)
|
|
196
|
+
return fromFile;
|
|
197
|
+
if (!fromFile)
|
|
198
|
+
return fromKeychain;
|
|
199
|
+
return expiresAtMs(fromFile) > expiresAtMs(fromKeychain) ? fromFile : fromKeychain;
|
|
200
|
+
}
|
|
201
|
+
/** The person session from the Keychain and the file, by the rules above. */
|
|
202
|
+
export function readPersonTokensWithKeychain(file = path.join(os.homedir(), ".hq", "cognito-tokens.json"), opts = {}) {
|
|
203
|
+
if (!fs.existsSync(file))
|
|
204
|
+
return { tokens: null, source: "none" };
|
|
205
|
+
let fromFile;
|
|
206
|
+
try {
|
|
207
|
+
fromFile = parseTokenJson(fs.readFileSync(file, "utf8"));
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
fromFile = null;
|
|
211
|
+
}
|
|
212
|
+
const fromKeychain = readKeychainTokens(opts);
|
|
213
|
+
const chosen = pickNewerSession(fromKeychain, fromFile);
|
|
214
|
+
if (!chosen)
|
|
215
|
+
return { tokens: null, source: "none" };
|
|
216
|
+
return { tokens: chosen, source: chosen === fromKeychain ? "keychain" : "file" };
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=token-keychain.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.345.
|
|
3
|
+
"version": "5.345.107",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@aws-sdk/client-iot-data-plane": "^3.1096.0",
|
|
49
49
|
"@aws-sdk/client-s3": "^3.1049.0",
|
|
50
50
|
"@aws-sdk/dsql-signer": "^3.1146.0",
|
|
51
|
-
"@indigoai-us/hq-cloud": "~6.18.
|
|
51
|
+
"@indigoai-us/hq-cloud": "~6.18.66",
|
|
52
52
|
"@indigoai-us/hq-flags-client": "^0.1.2",
|
|
53
53
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
54
54
|
"@sentry/node": "^10.49.0",
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# ontology-audience-grant.sh — grant each scoped audience folder to exactly the
|
|
3
|
-
# people in its audience, and prove every grant by readback.
|
|
4
|
-
# Spec: core/knowledge/public/hq-core/ontology-local-spec.md
|
|
5
|
-
#
|
|
6
|
-
# Usage: ontology-audience-grant.sh --company <co> [--dry-run]
|
|
7
|
-
#
|
|
8
|
-
# For every signals/@{key}/_audience.yaml under companies/<co>/:
|
|
9
|
-
# - grants read on signals/@{key}/, ontology/facts/@{key}/, and each
|
|
10
|
-
# sources/{channel}/@{key}/ that exists, to each principal that is an
|
|
11
|
-
# active company member (non-members are skipped with a warning);
|
|
12
|
-
# - one prefix grant per principal per folder, never per file;
|
|
13
|
-
# - each grant is the recursive shared glob `<folder>/*` (a bare `<folder>/`
|
|
14
|
-
# is a private create-only folder since hq-pro #3662);
|
|
15
|
-
# - reads every grant back with `hq files acl --json`; a missing grant exits 3.
|
|
16
|
-
# Company-audience paths get @all read on signals/{type}/ and
|
|
17
|
-
# ontology/facts/@company/ only. Nothing else under ontology/, signals/,
|
|
18
|
-
# sources/ is granted to @all.
|
|
19
|
-
# Local-only companies (company.yaml cloud: false or absent cloud_uid) exit 0
|
|
20
|
-
# with "local-only, no grants".
|
|
21
|
-
set -uo pipefail
|
|
22
|
-
HQ="${HQ_BIN:-hq}"
|
|
23
|
-
root="${HQ_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
24
|
-
co="" dry=0
|
|
25
|
-
while [ $# -gt 0 ]; do
|
|
26
|
-
case "$1" in --company) co="$2"; shift 2 ;; --dry-run) dry=1; shift ;; *) echo "unknown flag $1" >&2; exit 2 ;; esac
|
|
27
|
-
done
|
|
28
|
-
[ -n "$co" ] || { echo "usage: ontology-audience-grant.sh --company <co> [--dry-run]" >&2; exit 2; }
|
|
29
|
-
C="$root/companies/$co"; [ -d "$C" ] || { echo "no company $co" >&2; exit 2; }
|
|
30
|
-
|
|
31
|
-
cloud=no
|
|
32
|
-
grep -Eq '^cloud:[[:space:]]*true' "$C/company.yaml" 2>/dev/null && cloud=yes
|
|
33
|
-
if [ "$cloud" = no ] && [ -f "$root/companies/manifest.yaml" ]; then
|
|
34
|
-
# manifest: companies:\n <co>:\n cloud_uid: ... (two-space company keys)
|
|
35
|
-
awk -v co="$co" '/^ [^ #][^:]*:/{inco=($1==co":")} inco && /^ cloud_uid:[[:space:]]*[^[:space:]]/{f=1} END{exit !f}' "$root/companies/manifest.yaml" && cloud=yes
|
|
36
|
-
fi
|
|
37
|
-
[ "$cloud" = yes ] || { echo "local-only, no grants"; exit 0; }
|
|
38
|
-
|
|
39
|
-
members="$("$HQ" members --company "$co" list 2>/dev/null | awk 'NR>1{print tolower($1)}')"
|
|
40
|
-
[ -n "$members" ] || { echo "could not list members of $co" >&2; exit 3; }
|
|
41
|
-
|
|
42
|
-
granted=0 skipped=0 failed=0
|
|
43
|
-
grant() { # <folder/> <principal>
|
|
44
|
-
# Grant the recursive shared glob `<folder>/*`, never the bare `<folder>/`:
|
|
45
|
-
# since hq-pro #3662 a bare trailing slash is a private create-only folder
|
|
46
|
-
# (read on it exposes no children) and ACL reads echo the pattern verbatim.
|
|
47
|
-
local p="${1%/}/*" who="$2"
|
|
48
|
-
if [ "$dry" = 1 ]; then echo "would grant read $p -> $who"; return 0; fi
|
|
49
|
-
"$HQ" files --company "$co" share "$p" --with "$who" --permission read >/dev/null 2>&1 || true
|
|
50
|
-
if "$HQ" files --company "$co" acl "$p" --json 2>/dev/null | jq -e --arg w "$who" \
|
|
51
|
-
'.direct[]? | select((.granteeId==$w) or ($w=="@all" and .granteeType=="company-wide")) | select(.permission=="read" or .permission=="write" or .permission=="admin")' >/dev/null; then
|
|
52
|
-
granted=$((granted+1)); echo "granted read $p -> $who"
|
|
53
|
-
else
|
|
54
|
-
failed=$((failed+1)); echo "READBACK FAILED: $p -> $who" >&2
|
|
55
|
-
fi
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
for aud in "$C"/signals/@*/_audience.yaml; do
|
|
59
|
-
[ -f "$aud" ] || continue
|
|
60
|
-
key="$(basename "$(dirname "$aud")")"; key="${key#@}"
|
|
61
|
-
principals="$(sed -n 's/^principals:[[:space:]]*\[\(.*\)\]/\1/p' "$aud" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | awk '/@/ { print tolower($0); next } { print }' | grep -v '^$')"
|
|
62
|
-
prefixes="signals/@$key/"
|
|
63
|
-
[ -d "$C/ontology/facts/@$key" ] && prefixes="$prefixes ontology/facts/@$key/"
|
|
64
|
-
for d in "$C"/sources/*/@"$key"; do [ -d "$d" ] && prefixes="$prefixes sources/$(basename "$(dirname "$d")")/@$key/"; done
|
|
65
|
-
for who in $principals; do
|
|
66
|
-
# prs_/agt_ principals come from the source's own vault ACL (e.g. meeting
|
|
67
|
-
# attendees), so they are already company identities; emails must be members.
|
|
68
|
-
case "$who" in
|
|
69
|
-
prs_*|agt_*) ;;
|
|
70
|
-
*) if ! grep -qxF "$who" <<<"$members"; then echo "skip $who (not a member of $co)" >&2; skipped=$((skipped+1)); continue; fi ;;
|
|
71
|
-
esac
|
|
72
|
-
for p in $prefixes; do grant "$p" "$who"; done
|
|
73
|
-
done
|
|
74
|
-
done
|
|
75
|
-
[ -d "$C/ontology/facts/@company" ] && grant "ontology/facts/@company/" "@all"
|
|
76
|
-
for t in action_item commitment decision risk question key_point participant_contribution summary; do
|
|
77
|
-
[ -d "$C/signals/$t" ] && grant "signals/$t/" "@all"
|
|
78
|
-
done
|
|
79
|
-
|
|
80
|
-
echo "audience grants for $co: $granted granted, $skipped skipped, $failed failed"
|
|
81
|
-
[ "$failed" -eq 0 ] || exit 3
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// ontology-brief.mjs — render a company brief from the reader's LOCAL tree.
|
|
3
|
-
// Spec: core/knowledge/public/hq-core/ontology-local-spec.md
|
|
4
|
-
//
|
|
5
|
-
// Usage: node core/scripts/ontology-brief.mjs --company <co> [--hq-root <p>] [--days N] [--top N] [--json]
|
|
6
|
-
//
|
|
7
|
-
// Reads ontology/entities/, every ontology/facts/@*/ and signals/{type}/ +
|
|
8
|
-
// signals/@*/{type}/ present locally. Sync pull only delivers what the reader
|
|
9
|
-
// may read, so the brief shows exactly their visible facts and signals.
|
|
10
|
-
// Writes nothing. Prints markdown (default) or JSON. Exit 0 with an empty brief
|
|
11
|
-
// ("no local ontology") when nothing is there.
|
|
12
|
-
import fs from "node:fs";
|
|
13
|
-
import path from "node:path";
|
|
14
|
-
|
|
15
|
-
const TYPES = ["decision", "commitment", "risk", "question", "action_item", "key_point", "participant_contribution", "summary"];
|
|
16
|
-
const args = process.argv.slice(2);
|
|
17
|
-
const opt = { days: 14, top: 10, json: false };
|
|
18
|
-
for (let i = 0; i < args.length; i++) {
|
|
19
|
-
const k = args[i];
|
|
20
|
-
if (k === "--company") opt.company = args[++i];
|
|
21
|
-
else if (k === "--hq-root") opt.root = args[++i];
|
|
22
|
-
else if (k === "--days") opt.days = Number(args[++i]);
|
|
23
|
-
else if (k === "--top") opt.top = Number(args[++i]);
|
|
24
|
-
else if (k === "--json") opt.json = true;
|
|
25
|
-
else { console.error(`ontology-brief: unknown flag ${k}`); process.exit(2); }
|
|
26
|
-
}
|
|
27
|
-
if (!opt.company) { console.error("usage: ontology-brief.mjs --company <co> [--days N] [--top N] [--json]"); process.exit(2); }
|
|
28
|
-
opt.root = opt.root || process.env.HQ_ROOT || path.resolve(path.dirname(new URL(import.meta.url).pathname), "../..");
|
|
29
|
-
const C = path.join(opt.root, "companies", opt.company);
|
|
30
|
-
if (!fs.existsSync(C)) { console.error(`ontology-brief: no company ${opt.company}`); process.exit(2); }
|
|
31
|
-
|
|
32
|
-
function fm(file) {
|
|
33
|
-
const t = fs.readFileSync(file, "utf8");
|
|
34
|
-
const m = t.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
35
|
-
const out = { _body: (m ? m[2] : t).trim() };
|
|
36
|
-
if (m) for (const line of m[1].split("\n")) {
|
|
37
|
-
const mm = line.match(/^([A-Za-z_]+):\s*(.*)$/);
|
|
38
|
-
if (mm) out[mm[1]] = mm[2].trim().replace(/^"(.*)"$/, "$1");
|
|
39
|
-
}
|
|
40
|
-
return out;
|
|
41
|
-
}
|
|
42
|
-
const walk = (dir, pred) => {
|
|
43
|
-
if (!fs.existsSync(dir)) return [];
|
|
44
|
-
const out = [];
|
|
45
|
-
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
46
|
-
const p = path.join(dir, e.name);
|
|
47
|
-
if (e.isDirectory()) { if (!e.name.startsWith("_")) out.push(...walk(p, pred)); }
|
|
48
|
-
else if (pred(p)) out.push(p);
|
|
49
|
-
}
|
|
50
|
-
return out;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const entities = walk(path.join(C, "ontology", "entities"), p => p.endsWith(".md")).map(f => fm(f));
|
|
54
|
-
const cutoff = Date.now() - opt.days * 86400e3;
|
|
55
|
-
const signals = walk(path.join(C, "signals"), p => p.endsWith(".md") && !p.includes(`${path.sep}_`))
|
|
56
|
-
.map(f => ({ ...fm(f), _scope: f.includes(`${path.sep}signals${path.sep}@`) ? "scoped" : "company" }))
|
|
57
|
-
.filter(s => TYPES.includes(s.type));
|
|
58
|
-
const recent = signals.filter(s => !s.created_at || Date.parse(s.created_at) >= cutoff)
|
|
59
|
-
.sort((a, b) => (b.created_at || "").localeCompare(a.created_at || ""));
|
|
60
|
-
const facts = new Map(); // slug -> [{scope, line}]
|
|
61
|
-
for (const f of walk(path.join(C, "ontology", "facts"), p => p.endsWith(".md"))) {
|
|
62
|
-
const d = fm(f); const scope = f.includes(`${path.sep}@company${path.sep}`) ? "company" : "scoped";
|
|
63
|
-
const slug = d.entity || path.basename(f, ".md");
|
|
64
|
-
for (const line of d._body.split("\n").filter(l => l.startsWith("- "))) {
|
|
65
|
-
if (!facts.has(slug)) facts.set(slug, []);
|
|
66
|
-
facts.get(slug).push({ scope, line: line.slice(2) });
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
const hot = entities.map(e => ({ ...e, _facts: facts.get(e.slug) || [] }))
|
|
70
|
-
.sort((a, b) => (b._facts.length - a._facts.length) || ((+b.signal_count || 0) - (+a.signal_count || 0)))
|
|
71
|
-
.slice(0, opt.top);
|
|
72
|
-
|
|
73
|
-
if (opt.json) {
|
|
74
|
-
console.log(JSON.stringify({ company: opt.company, entities: entities.length, signals: signals.length,
|
|
75
|
-
recent: recent.slice(0, opt.top * 2).map(s => ({ type: s.type, scope: s._scope, text: s.canonical_content || s._body })),
|
|
76
|
-
hot: hot.map(e => ({ name: e.canonical_name, type: e.type, facts: e._facts })) }));
|
|
77
|
-
process.exit(0);
|
|
78
|
-
}
|
|
79
|
-
if (!entities.length && !signals.length) { console.log(`# ${opt.company} — no local ontology yet\n\nNothing under ontology/ or signals/ that you can see.`); process.exit(0); }
|
|
80
|
-
const out = [`# ${opt.company} — company brief (local)`, "", `${entities.length} entities · ${signals.length} signals you can see · last ${opt.days} days`, "", "## Recent Signals"];
|
|
81
|
-
for (const t of TYPES) {
|
|
82
|
-
const rows = recent.filter(s => s.type === t).slice(0, 5);
|
|
83
|
-
if (!rows.length) continue;
|
|
84
|
-
out.push("", `### ${t.replace("_", " ")}`);
|
|
85
|
-
for (const s of rows) out.push(`- ${s.canonical_content || s._body} _(${s._scope})_`);
|
|
86
|
-
}
|
|
87
|
-
out.push("", "## Entities");
|
|
88
|
-
for (const e of hot) {
|
|
89
|
-
out.push("", `### ${e.canonical_name} — ${e.type}`);
|
|
90
|
-
if (!e._facts.length) out.push("- no facts you can see");
|
|
91
|
-
for (const f of e._facts.slice(0, 6)) out.push(`- ${f.line} _(${f.scope})_`);
|
|
92
|
-
}
|
|
93
|
-
console.log(out.join("\n"));
|