agent-tempo 1.6.0 → 1.6.2
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/dashboard/package.json +1 -1
- package/dist/activities/hard-terminate.js +8 -2
- package/dist/adapters/claude-api/adapter.js +4 -3
- package/dist/adapters/claude-code-headless/adapter.js +4 -3
- package/dist/adapters/copilot/adapter.js +8 -3
- package/dist/adapters/opencode/adapter.js +3 -1
- package/dist/cli/commands.js +86 -34
- package/dist/cli/config-command.d.ts +14 -0
- package/dist/cli/config-command.js +42 -5
- package/dist/cli/resolve-ensemble.d.ts +17 -0
- package/dist/cli/resolve-ensemble.js +20 -0
- package/dist/cli/sa-preflight.d.ts +8 -0
- package/dist/cli/sa-preflight.js +31 -0
- package/dist/cli.js +5 -1
- package/dist/config.d.ts +63 -3
- package/dist/config.js +72 -12
- package/dist/pi/cue-pump.js +9 -1
- package/dist/spawn.d.ts +49 -3
- package/dist/spawn.js +200 -56
- package/dist/utils/secrets.d.ts +34 -0
- package/dist/utils/secrets.js +47 -0
- package/examples/agents/tempo-conductor.md +12 -0
- package/package.json +1 -1
package/dashboard/package.json
CHANGED
|
@@ -30,6 +30,7 @@ exports.hardTerminateAttachment = hardTerminateAttachment;
|
|
|
30
30
|
const child_process_1 = require("child_process");
|
|
31
31
|
const fs_1 = require("fs");
|
|
32
32
|
const path_1 = require("path");
|
|
33
|
+
const config_1 = require("../config");
|
|
33
34
|
const constants_1 = require("../constants");
|
|
34
35
|
const log = (...args) => console.error('[agent-tempo:hard-terminate]', ...args);
|
|
35
36
|
/**
|
|
@@ -43,8 +44,13 @@ async function hardTerminateAttachment(input) {
|
|
|
43
44
|
log(`hardTerminate start — ensemble=${ensemble} player=${playerName} agent=${agent}`);
|
|
44
45
|
// ── Copilot bridge: PID file is authoritative ──
|
|
45
46
|
if (agent === 'copilot') {
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
// #690 — pid lives at the CENTRAL ~/.agent-tempo/logs/<ensemble>/ path now.
|
|
48
|
+
// Transitional (one version, v1.6.2) READ-ONLY fallback to the legacy per-cwd
|
|
49
|
+
// <workDir>/logs so we can still find+kill an orphan from a pre-upgrade spawn.
|
|
50
|
+
// TODO(v1.7): drop the legacy fallback.
|
|
51
|
+
const centralPid = (0, config_1.bridgeLogPaths)(ensemble, playerName, logDir).pidPath;
|
|
52
|
+
const legacyPid = (0, path_1.join)(logDir || (0, path_1.join)(workDir, 'logs'), `${playerName}.pid`);
|
|
53
|
+
const pidPath = (0, fs_1.existsSync)(centralPid) ? centralPid : legacyPid;
|
|
48
54
|
if ((0, fs_1.existsSync)(pidPath)) {
|
|
49
55
|
try {
|
|
50
56
|
const pidStr = (0, fs_1.readFileSync)(pidPath, 'utf8').trim();
|
|
@@ -332,10 +332,11 @@ class DirectApiAttachment extends base_1.SdkAttachment {
|
|
|
332
332
|
process.exit(1);
|
|
333
333
|
}
|
|
334
334
|
// PID file so callers can find / kill orphaned adapter processes.
|
|
335
|
-
|
|
336
|
-
|
|
335
|
+
// #690 — write/unlink the EXACT path the spawner computed (ENV.PID_FILE) so the
|
|
336
|
+
// adapter pid can't diverge from the spawner's; helper fallback for a manual launch.
|
|
337
|
+
const pidFile = (0, config_1.resolveAdapterPidFile)(config.ensemble, playerIdForWorkflow);
|
|
337
338
|
try {
|
|
338
|
-
fs.mkdirSync(
|
|
339
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true });
|
|
339
340
|
fs.writeFileSync(pidFile, String(process.pid));
|
|
340
341
|
}
|
|
341
342
|
catch (err) {
|
|
@@ -353,10 +353,11 @@ class ClaudeCodeHeadlessAttachment extends base_1.SdkAttachment {
|
|
|
353
353
|
process.exit(1);
|
|
354
354
|
}
|
|
355
355
|
// PID file so callers can find / kill orphaned adapter processes.
|
|
356
|
-
|
|
357
|
-
|
|
356
|
+
// #690 — write/unlink the EXACT path the spawner computed (ENV.PID_FILE) so the
|
|
357
|
+
// adapter pid can't diverge from the spawner's; helper fallback for a manual launch.
|
|
358
|
+
const pidFile = (0, config_1.resolveAdapterPidFile)(config.ensemble, playerIdForWorkflow);
|
|
358
359
|
try {
|
|
359
|
-
fs.mkdirSync(
|
|
360
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true });
|
|
360
361
|
fs.writeFileSync(pidFile, String(process.pid));
|
|
361
362
|
}
|
|
362
363
|
catch (err) {
|
|
@@ -386,8 +386,13 @@ class CopilotSdkAttachment extends base_1.SdkAttachment {
|
|
|
386
386
|
log(`Initial prompt error after ${Date.now()}ms:`, err?.message, err?.stack?.substring(0, 300));
|
|
387
387
|
}
|
|
388
388
|
// PID file paths — computed early so early-exit paths can clean up
|
|
389
|
-
|
|
390
|
-
|
|
389
|
+
// #690 — write/unlink the EXACT path the spawner computed (ENV.PID_FILE), so this
|
|
390
|
+
// adapter's pid file can't diverge from the spawner's. The copilot split-brain was
|
|
391
|
+
// here: spawnCopilotBridge sets PLAYER_NAME='' + BRIDGE_NAME=name, so this
|
|
392
|
+
// re-derivation fell to `playerIdForWorkflow` (→ `copilot-${Date.now()}`) ≠ the
|
|
393
|
+
// spawner's logName. Consuming the passed path removes the re-derivation entirely.
|
|
394
|
+
// Helper fallback only for a manual launch with no env.
|
|
395
|
+
const pidFile = (0, config_1.resolveAdapterPidFile)(config.ensemble, playerName || playerIdForWorkflow);
|
|
391
396
|
// Wait for the MCP server's workflow to register in Temporal.
|
|
392
397
|
// We know the exact workflow ID because we pass AGENT_TEMPO_PLAYER_NAME to the
|
|
393
398
|
// MCP server — no need for a time-window heuristic that could misidentify workflows.
|
|
@@ -485,7 +490,7 @@ class CopilotSdkAttachment extends base_1.SdkAttachment {
|
|
|
485
490
|
// Imported at the top of this module.
|
|
486
491
|
// Write PID file so callers can find/kill orphaned bridge processes
|
|
487
492
|
try {
|
|
488
|
-
fs.mkdirSync(
|
|
493
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true });
|
|
489
494
|
fs.writeFileSync(pidFile, String(process.pid));
|
|
490
495
|
log(`PID file written: ${pidFile}`);
|
|
491
496
|
}
|
|
@@ -239,7 +239,9 @@ class OpenCodeAttachment extends base_1.SdkAttachment {
|
|
|
239
239
|
log(`Synthesized OPENCODE_CONFIG_CONTENT: ${(0, helpers_1.redactSecrets)(configContent)}`);
|
|
240
240
|
// (3) Spawn opencode serve. Stdio redirected to a per-player log file
|
|
241
241
|
// so terminal noise from opencode doesn't clutter the adapter's log.
|
|
242
|
-
|
|
242
|
+
// #690 — central ~/.agent-tempo/logs/<ensemble>/ (the opencode serve subprocess
|
|
243
|
+
// log sits beside the adapter's own log, not in a per-cwd ./logs).
|
|
244
|
+
const logDir = (0, config_1.bridgeLogPaths)(config.ensemble, playerIdForWorkflow).dir;
|
|
243
245
|
fs.mkdirSync(logDir, { recursive: true });
|
|
244
246
|
const opencodeLogFile = path.join(logDir, `opencode-${playerIdForWorkflow}.log`);
|
|
245
247
|
const logFd = fs.openSync(opencodeLogFile, 'a');
|
package/dist/cli/commands.js
CHANGED
|
@@ -805,7 +805,7 @@ async function status(opts) {
|
|
|
805
805
|
const agent = s.agentType === 'copilot' ? out.dim(' [copilot]') : '';
|
|
806
806
|
const statusLabel = phaseLabel(s.phase);
|
|
807
807
|
// Show PID info for copilot bridge sessions
|
|
808
|
-
const pidInfo = s.agentType === 'copilot' ? getBridgePidInfo(s.name) : '';
|
|
808
|
+
const pidInfo = s.agentType === 'copilot' ? getBridgePidInfo(ensemble, s.name) : '';
|
|
809
809
|
const name = out.bold(s.name);
|
|
810
810
|
out.log(` ${name}${role}${statusLabel}${agent}${pidInfo}`);
|
|
811
811
|
if (s.part)
|
|
@@ -938,6 +938,7 @@ function temporalCliExists() {
|
|
|
938
938
|
}
|
|
939
939
|
function registerSearchAttributes(temporalAddress, namespace = 'default') {
|
|
940
940
|
let failed = 0;
|
|
941
|
+
let permissionBlocked = 0;
|
|
941
942
|
for (const attr of sa_preflight_1.REQUIRED_SEARCH_ATTRIBUTES) {
|
|
942
943
|
const r = (0, sa_preflight_1.registerSearchAttribute)(attr, temporalAddress, namespace);
|
|
943
944
|
switch (r.status) {
|
|
@@ -948,17 +949,33 @@ function registerSearchAttributes(temporalAddress, namespace = 'default') {
|
|
|
948
949
|
out.dim(` ${attr.name} (already registered)`);
|
|
949
950
|
break;
|
|
950
951
|
case 'failed':
|
|
951
|
-
//
|
|
952
|
-
//
|
|
953
|
-
//
|
|
954
|
-
//
|
|
955
|
-
// the
|
|
956
|
-
//
|
|
957
|
-
|
|
958
|
-
|
|
952
|
+
// A PERMISSION error (Temporal Cloud namespace API keys can't reach the
|
|
953
|
+
// operator service) means we can't tell whether the SA exists — NOT that
|
|
954
|
+
// it's missing. Don't print a scary per-attr "Failed to register" or count
|
|
955
|
+
// it as a failure; collapse to ONE soft line below and PROCEED. Reserve
|
|
956
|
+
// the per-attr warning + hard "will fail" conclusion for DEFINITIVE
|
|
957
|
+
// failures (e.g. the SQLite dev server's 10-Keyword-per-namespace cap).
|
|
958
|
+
if ((0, sa_preflight_1.isPermissionError)(r.detail)) {
|
|
959
|
+
permissionBlocked++;
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
failed++;
|
|
963
|
+
out.warn(`Failed to register ${attr.name}: ${r.detail}`);
|
|
964
|
+
}
|
|
959
965
|
break;
|
|
960
966
|
}
|
|
961
967
|
}
|
|
968
|
+
// Permission-blocked (normal on Temporal Cloud): one accurate, non-alarming
|
|
969
|
+
// line — we couldn't manage the SAs, but that doesn't mean they're missing.
|
|
970
|
+
if (permissionBlocked > 0) {
|
|
971
|
+
const saList = sa_preflight_1.REQUIRED_SEARCH_ATTRIBUTES.map((a) => `${a.name}:${a.type}`).join(', ');
|
|
972
|
+
out.warn(`Couldn't verify search attributes — this credential lacks permission to manage them ` +
|
|
973
|
+
`(normal on Temporal Cloud, where search attributes are managed via the Cloud UI or tcld). ` +
|
|
974
|
+
`If workflow starts fail with "search attribute ... is not defined", create these ` +
|
|
975
|
+
`${sa_preflight_1.REQUIRED_SEARCH_ATTRIBUTES.length} via the Cloud UI / tcld: ${saList}. ` +
|
|
976
|
+
`Otherwise this is safe to ignore.`);
|
|
977
|
+
}
|
|
978
|
+
// DEFINITIVE failures genuinely block — keep the hard, actionable conclusion.
|
|
962
979
|
if (failed > 0) {
|
|
963
980
|
out.warn(`${failed} search attribute${failed === 1 ? '' : 's'} not registered — ` +
|
|
964
981
|
`workflow starts will fail. Resolve the errors above before continuing.`);
|
|
@@ -1048,6 +1065,9 @@ async function server(opts) {
|
|
|
1048
1065
|
}
|
|
1049
1066
|
async function up(opts) {
|
|
1050
1067
|
const config = (0, config_1.getConfig)(opts);
|
|
1068
|
+
// #689 — best-effort sweep of stale 0600 secret env files (residual from a shell
|
|
1069
|
+
// that died between `source` and `rm`). Owner-only, swallows errors.
|
|
1070
|
+
(0, spawn_1.sweepStaleSecretEnvFiles)();
|
|
1051
1071
|
out.heading('agent-tempo setup');
|
|
1052
1072
|
// Step 1: Check temporal CLI
|
|
1053
1073
|
if (!temporalCliExists()) {
|
|
@@ -1893,8 +1913,13 @@ async function down(opts) {
|
|
|
1893
1913
|
* Read PID info for a copilot bridge session from its PID file.
|
|
1894
1914
|
* Returns a formatted string like " (pid 12345)" or "" if no PID file found.
|
|
1895
1915
|
*/
|
|
1896
|
-
function getBridgePidInfo(name) {
|
|
1897
|
-
|
|
1916
|
+
function getBridgePidInfo(ensemble, name) {
|
|
1917
|
+
// #690 — pid lives at the CENTRAL ~/.agent-tempo/logs/<ensemble>/ path; transitional
|
|
1918
|
+
// READ-ONLY fallback to the legacy per-cwd ./logs for a pre-upgrade bridge.
|
|
1919
|
+
// TODO(v1.7): drop the legacy fallback.
|
|
1920
|
+
const centralPid = (0, config_1.bridgeLogPaths)(ensemble, name).pidPath;
|
|
1921
|
+
const legacyPid = (0, path_1.join)(process.cwd(), 'logs', `${name}.pid`);
|
|
1922
|
+
const pidPath = (0, fs_1.existsSync)(centralPid) ? centralPid : legacyPid;
|
|
1898
1923
|
if (!(0, fs_1.existsSync)(pidPath))
|
|
1899
1924
|
return '';
|
|
1900
1925
|
try {
|
|
@@ -1915,36 +1940,63 @@ function getBridgePidInfo(name) {
|
|
|
1915
1940
|
}
|
|
1916
1941
|
}
|
|
1917
1942
|
/**
|
|
1918
|
-
* Kill all bridge processes found in
|
|
1943
|
+
* Kill all bridge processes found in `*.pid` files and clean up the pid files.
|
|
1944
|
+
*
|
|
1945
|
+
* #690 — bridge pid files moved to the CENTRAL `~/.agent-tempo/logs/<ensemble>/`
|
|
1946
|
+
* dirs. `down` is a GLOBAL teardown (it stops the daemon + Temporal for EVERY
|
|
1947
|
+
* ensemble), so this scans ALL central ensemble subdirs — NOT a single ensemble.
|
|
1948
|
+
*
|
|
1949
|
+
* ⚠️ GLOBAL-TEARDOWN ONLY. The sole caller is `down()`, which has no ensemble (it
|
|
1950
|
+
* stops everything), so scanning all ensembles is correct HERE. A FUTURE
|
|
1951
|
+
* ensemble-scoped teardown (e.g. `down --ensemble X` / a per-ensemble `destroy`)
|
|
1952
|
+
* MUST add an `ensemble` param and scope this to `bridgeLogPaths(ensemble, '').dir`
|
|
1953
|
+
* — do NOT reuse this global scan-all from a scoped op: it would kill OTHER live
|
|
1954
|
+
* ensembles' bridges. The param is intentionally NOT added now (no caller needs it
|
|
1955
|
+
* = speculative; backlogged per the architect's deviation ruling).
|
|
1956
|
+
*
|
|
1957
|
+
* Plus a transitional READ of the legacy per-cwd `./logs` for a pre-upgrade
|
|
1958
|
+
* bridge. TODO(v1.7): drop the legacy `./logs` dir.
|
|
1919
1959
|
*/
|
|
1920
1960
|
function killBridgeProcesses() {
|
|
1921
|
-
const
|
|
1922
|
-
|
|
1923
|
-
return;
|
|
1961
|
+
const centralRoot = (0, config_1.bridgeLogsRoot)();
|
|
1962
|
+
const dirs = [(0, path_1.join)(process.cwd(), 'logs')]; // legacy (transitional)
|
|
1924
1963
|
try {
|
|
1925
|
-
const
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1964
|
+
for (const ent of (0, fs_1.readdirSync)(centralRoot, { withFileTypes: true })) {
|
|
1965
|
+
if (ent.isDirectory())
|
|
1966
|
+
dirs.push((0, path_1.join)(centralRoot, ent.name));
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
catch {
|
|
1970
|
+
// no central logs root yet — nothing central to scan
|
|
1971
|
+
}
|
|
1972
|
+
for (const logsDir of dirs) {
|
|
1973
|
+
if (!(0, fs_1.existsSync)(logsDir))
|
|
1974
|
+
continue;
|
|
1975
|
+
try {
|
|
1976
|
+
const pidFiles = (0, fs_1.readdirSync)(logsDir).filter(f => f.endsWith('.pid'));
|
|
1977
|
+
for (const pidFile of pidFiles) {
|
|
1978
|
+
const pidPath = (0, path_1.join)(logsDir, pidFile);
|
|
1979
|
+
try {
|
|
1980
|
+
const pid = parseInt((0, fs_1.readFileSync)(pidPath, 'utf8').trim(), 10);
|
|
1981
|
+
if (!isNaN(pid)) {
|
|
1982
|
+
try {
|
|
1983
|
+
process.kill(pid);
|
|
1984
|
+
out.log(` ${out.dim(`Killed bridge process ${pidFile.replace('.pid', '')} (pid ${pid})`)}`);
|
|
1985
|
+
}
|
|
1986
|
+
catch {
|
|
1987
|
+
// already dead
|
|
1988
|
+
}
|
|
1937
1989
|
}
|
|
1990
|
+
(0, fs_1.unlinkSync)(pidPath);
|
|
1991
|
+
}
|
|
1992
|
+
catch {
|
|
1993
|
+
// unreadable — skip
|
|
1938
1994
|
}
|
|
1939
|
-
(0, fs_1.unlinkSync)(pidPath);
|
|
1940
|
-
}
|
|
1941
|
-
catch {
|
|
1942
|
-
// unreadable — skip
|
|
1943
1995
|
}
|
|
1944
1996
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1997
|
+
catch {
|
|
1998
|
+
// logs dir unreadable
|
|
1999
|
+
}
|
|
1948
2000
|
}
|
|
1949
2001
|
}
|
|
1950
2002
|
async function agentTypesCommand(opts) {
|
|
@@ -11,8 +11,22 @@ import type { AgentType } from '../types';
|
|
|
11
11
|
* are not offered here.
|
|
12
12
|
* Single source of truth for the interactive selector + `config set` validation
|
|
13
13
|
* (#666 — adds `pi` so the new interactive Pi conductor can be the default).
|
|
14
|
+
*
|
|
15
|
+
* DELIBERATE SUBSET of `AGENT_TYPES` (NOT derived from it): this is a CAPABILITY
|
|
16
|
+
* allowlist (conductor-capable production agents), distinct from `parseAgent`'s
|
|
17
|
+
* type-VALIDITY check, which accepts all of `AGENT_TYPES`. Keep the two separate —
|
|
18
|
+
* #683 was caused by a validity check (`config.ts`) that had been hardcoded to a
|
|
19
|
+
* stale subset; this one is intentionally narrow and must stay that way.
|
|
14
20
|
*/
|
|
15
21
|
export declare const VALID_DEFAULT_AGENTS: readonly AgentType[];
|
|
22
|
+
/**
|
|
23
|
+
* Render a secret for display: a short non-sensitive prefix (when the value is
|
|
24
|
+
* long enough that the prefix reveals only a small fraction) + a masked tail +
|
|
25
|
+
* the char count. NEVER returns the full value. Empty/unset → "(not set)".
|
|
26
|
+
*
|
|
27
|
+
* Examples: `sk-ant-…•••• (set, 47 chars)` · short secret → `•••• (set, 6 chars)`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function maskSecret(value: string | undefined | null): string;
|
|
16
30
|
/** Interactive config setup: `agent-tempo config` */
|
|
17
31
|
export declare function configInteractive(): Promise<void>;
|
|
18
32
|
/** Non-interactive: `agent-tempo config set <key> <value>` */
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.VALID_DEFAULT_AGENTS = void 0;
|
|
37
|
+
exports.maskSecret = maskSecret;
|
|
37
38
|
exports.configInteractive = configInteractive;
|
|
38
39
|
exports.configSet = configSet;
|
|
39
40
|
exports.configShow = configShow;
|
|
@@ -41,6 +42,7 @@ exports.configCommand = configCommand;
|
|
|
41
42
|
const readline = __importStar(require("readline"));
|
|
42
43
|
const config_1 = require("../config");
|
|
43
44
|
const config_2 = require("../config");
|
|
45
|
+
const secrets_1 = require("../utils/secrets");
|
|
44
46
|
const out = __importStar(require("./output"));
|
|
45
47
|
/**
|
|
46
48
|
* Agents valid as a persistent `defaultAgent` — the conductor-capable PRODUCTION
|
|
@@ -54,6 +56,12 @@ const out = __importStar(require("./output"));
|
|
|
54
56
|
* are not offered here.
|
|
55
57
|
* Single source of truth for the interactive selector + `config set` validation
|
|
56
58
|
* (#666 — adds `pi` so the new interactive Pi conductor can be the default).
|
|
59
|
+
*
|
|
60
|
+
* DELIBERATE SUBSET of `AGENT_TYPES` (NOT derived from it): this is a CAPABILITY
|
|
61
|
+
* allowlist (conductor-capable production agents), distinct from `parseAgent`'s
|
|
62
|
+
* type-VALIDITY check, which accepts all of `AGENT_TYPES`. Keep the two separate —
|
|
63
|
+
* #683 was caused by a validity check (`config.ts`) that had been hardcoded to a
|
|
64
|
+
* stale subset; this one is intentionally narrow and must stay that way.
|
|
57
65
|
*/
|
|
58
66
|
exports.VALID_DEFAULT_AGENTS = ['claude', 'copilot', 'pi'];
|
|
59
67
|
// NOTE: `createTemporalConnection` is dynamic-imported inside `configInteractive`'s
|
|
@@ -61,7 +69,29 @@ exports.VALID_DEFAULT_AGENTS = ['claude', 'copilot', 'pi'];
|
|
|
61
69
|
// `@temporalio/client`, defeating the crash-proof property of `config show` /
|
|
62
70
|
// `config set` — both of which are pure fs operations and must remain operable
|
|
63
71
|
// under a broken Temporal SDK install.
|
|
64
|
-
|
|
72
|
+
// #684 — secret-masking. Any config field whose name looks like a credential is
|
|
73
|
+
// masked in EVERY display path (show / interactive default / set echo) so a key is
|
|
74
|
+
// never printed raw (terminal scrollback, screen-share, logs). The classifier
|
|
75
|
+
// (`isSecretKey`) was extracted to `utils/secrets.ts` in #689 so `spawn.ts` shares
|
|
76
|
+
// it — a future secret masks AND stays off the command line everywhere at once.
|
|
77
|
+
/**
|
|
78
|
+
* Render a secret for display: a short non-sensitive prefix (when the value is
|
|
79
|
+
* long enough that the prefix reveals only a small fraction) + a masked tail +
|
|
80
|
+
* the char count. NEVER returns the full value. Empty/unset → "(not set)".
|
|
81
|
+
*
|
|
82
|
+
* Examples: `sk-ant-…•••• (set, 47 chars)` · short secret → `•••• (set, 6 chars)`.
|
|
83
|
+
*/
|
|
84
|
+
function maskSecret(value) {
|
|
85
|
+
if (value == null || value === '')
|
|
86
|
+
return '(not set)';
|
|
87
|
+
const len = value.length;
|
|
88
|
+
// Reveal a prefix only when it's a small fraction of the whole; never for short
|
|
89
|
+
// secrets (so the output can never contain the full input — see the unit test).
|
|
90
|
+
const prefixLen = len >= 12 ? 6 : len >= 8 ? 3 : 0;
|
|
91
|
+
const prefix = value.slice(0, prefixLen);
|
|
92
|
+
const masked = prefixLen > 0 ? `${prefix}…••••` : '••••';
|
|
93
|
+
return `${masked} (set, ${len} chars)`;
|
|
94
|
+
}
|
|
65
95
|
/** Read a line from stdin with a prompt and optional default value. */
|
|
66
96
|
function ask(prompt, defaultVal, mask = false) {
|
|
67
97
|
return new Promise((resolve) => {
|
|
@@ -69,7 +99,11 @@ function ask(prompt, defaultVal, mask = false) {
|
|
|
69
99
|
input: process.stdin,
|
|
70
100
|
output: process.stdout,
|
|
71
101
|
});
|
|
72
|
-
|
|
102
|
+
// #684 — for masked (secret) prompts NEVER echo the raw existing value as the
|
|
103
|
+
// shown default; render a masked hint instead. The real `defaultVal` is still
|
|
104
|
+
// returned on empty input, so an existing key is preserved without exposing it.
|
|
105
|
+
const shownDefault = mask ? maskSecret(defaultVal) : defaultVal;
|
|
106
|
+
const display = defaultVal ? `${prompt} (${shownDefault}): ` : `${prompt}: `;
|
|
73
107
|
if (mask) {
|
|
74
108
|
// For secret input: write prompt manually, mute output
|
|
75
109
|
process.stdout.write(`? ${display}`);
|
|
@@ -212,7 +246,9 @@ function configSet(key, value) {
|
|
|
212
246
|
}
|
|
213
247
|
config[configKey] = value;
|
|
214
248
|
(0, config_1.saveConfigFile)(config);
|
|
215
|
-
|
|
249
|
+
// #684 — echo through the same secret-masking path so `config set temporalApiKey …`
|
|
250
|
+
// never prints the value back raw (and a *Path field still shows its location).
|
|
251
|
+
out.success(`Set ${configKey} = ${(0, secrets_1.isSecretKey)(configKey) ? maskSecret(value) : value}`);
|
|
216
252
|
}
|
|
217
253
|
/** Show current config: `agent-tempo config show` */
|
|
218
254
|
function configShow() {
|
|
@@ -234,8 +270,9 @@ function configShow() {
|
|
|
234
270
|
for (const { key, configKey } of keys) {
|
|
235
271
|
const value = config[configKey];
|
|
236
272
|
const source = sources[configKey];
|
|
237
|
-
|
|
238
|
-
|
|
273
|
+
// #684 — secret-like fields go through maskSecret (prefix + masked tail + char
|
|
274
|
+
// count); everything else shows its value or "(not set)".
|
|
275
|
+
const display = (0, secrets_1.isSecretKey)(key) ? maskSecret(value) : (!value ? '(not set)' : value);
|
|
239
276
|
out.log(` ${key.padEnd(22)} ${display.padEnd(30)} ${out.dim(source)}`);
|
|
240
277
|
}
|
|
241
278
|
console.log();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the target ensemble with the precedence every CLI command uses (#685):
|
|
3
|
+
*
|
|
4
|
+
* `--ensemble` flag > positional arg > `AGENT_TEMPO_ENSEMBLE` env > `'default'`
|
|
5
|
+
*
|
|
6
|
+
* `up` previously passed a bare positional-derived value and IGNORED the
|
|
7
|
+
* `--ensemble` flag (so `agent-tempo up --ensemble pitest` silently launched in
|
|
8
|
+
* `default`). Centralizing the rule here makes it a single, unit-testable source
|
|
9
|
+
* of truth so it can't drift per-command again.
|
|
10
|
+
*
|
|
11
|
+
* Pure: `env` is injectable (defaults to the live `AGENT_TEMPO_ENSEMBLE`) so the
|
|
12
|
+
* precedence is testable without mutating `process.env`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveEnsemble(args: {
|
|
15
|
+
ensemble?: string;
|
|
16
|
+
positional: string[];
|
|
17
|
+
}, env?: string | undefined): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveEnsemble = resolveEnsemble;
|
|
4
|
+
const config_1 = require("../config");
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the target ensemble with the precedence every CLI command uses (#685):
|
|
7
|
+
*
|
|
8
|
+
* `--ensemble` flag > positional arg > `AGENT_TEMPO_ENSEMBLE` env > `'default'`
|
|
9
|
+
*
|
|
10
|
+
* `up` previously passed a bare positional-derived value and IGNORED the
|
|
11
|
+
* `--ensemble` flag (so `agent-tempo up --ensemble pitest` silently launched in
|
|
12
|
+
* `default`). Centralizing the rule here makes it a single, unit-testable source
|
|
13
|
+
* of truth so it can't drift per-command again.
|
|
14
|
+
*
|
|
15
|
+
* Pure: `env` is injectable (defaults to the live `AGENT_TEMPO_ENSEMBLE`) so the
|
|
16
|
+
* precedence is testable without mutating `process.env`.
|
|
17
|
+
*/
|
|
18
|
+
function resolveEnsemble(args, env = process.env[config_1.ENV.ENSEMBLE]) {
|
|
19
|
+
return args.ensemble || args.positional[1] || env || 'default';
|
|
20
|
+
}
|
|
@@ -88,6 +88,14 @@ export interface RegistrationResult {
|
|
|
88
88
|
/** stderr from the temporal CLI, populated when `status === 'failed'`. */
|
|
89
89
|
detail?: string;
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* True when a registration error means "this credential can't manage search
|
|
93
|
+
* attributes" (a permission/authorization failure) rather than a definitive
|
|
94
|
+
* registration failure (e.g. the SQLite dev server's 10-Keyword cap). Used to
|
|
95
|
+
* avoid the false "not registered → starts will fail" conclusion on Temporal
|
|
96
|
+
* Cloud, where SA management lives behind the Cloud UI / `tcld`.
|
|
97
|
+
*/
|
|
98
|
+
export declare function isPermissionError(detail: string | undefined): boolean;
|
|
91
99
|
/**
|
|
92
100
|
* Pure classifier — turn a temporal CLI exit into a {@link RegistrationStatus}.
|
|
93
101
|
* Extracted from {@link registerSearchAttribute} so the matching rules can
|
package/dist/cli/sa-preflight.js
CHANGED
|
@@ -39,6 +39,7 @@ exports.isTemporalCloud = isTemporalCloud;
|
|
|
39
39
|
exports.sdkProbeRegisteredAttributes = sdkProbeRegisteredAttributes;
|
|
40
40
|
exports.formatPreflightError = formatPreflightError;
|
|
41
41
|
exports.verifySearchAttributes = verifySearchAttributes;
|
|
42
|
+
exports.isPermissionError = isPermissionError;
|
|
42
43
|
exports.classifyRegistrationOutput = classifyRegistrationOutput;
|
|
43
44
|
exports.registerSearchAttribute = registerSearchAttribute;
|
|
44
45
|
exports.assertSearchAttributesOrExit = assertSearchAttributesOrExit;
|
|
@@ -284,6 +285,36 @@ async function verifySearchAttributes(opts) {
|
|
|
284
285
|
message: formatPreflightError(missing, opts.temporalNamespace, probeError, cloud),
|
|
285
286
|
};
|
|
286
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Substrings signalling the credential lacks PERMISSION to manage search
|
|
290
|
+
* attributes — distinct from a definitive registration failure. Temporal Cloud
|
|
291
|
+
* namespace API keys can't reach the operator service (Cloud manages SAs via its
|
|
292
|
+
* UI / `tcld`), so `temporal operator search-attribute create/list` returns
|
|
293
|
+
* "Request unauthorized" / PermissionDenied. That means we CANNOT determine
|
|
294
|
+
* whether the SAs are registered — NOT that they're missing. Concluding
|
|
295
|
+
* "not registered → workflow starts will fail" from a permission error is a
|
|
296
|
+
* false alarm: on Cloud the SAs are typically already present and starts succeed.
|
|
297
|
+
*/
|
|
298
|
+
const PERMISSION_ERROR_MARKERS = [
|
|
299
|
+
'request unauthorized',
|
|
300
|
+
'permission denied',
|
|
301
|
+
'permissiondenied',
|
|
302
|
+
'unauthorized',
|
|
303
|
+
'not authorized',
|
|
304
|
+
];
|
|
305
|
+
/**
|
|
306
|
+
* True when a registration error means "this credential can't manage search
|
|
307
|
+
* attributes" (a permission/authorization failure) rather than a definitive
|
|
308
|
+
* registration failure (e.g. the SQLite dev server's 10-Keyword cap). Used to
|
|
309
|
+
* avoid the false "not registered → starts will fail" conclusion on Temporal
|
|
310
|
+
* Cloud, where SA management lives behind the Cloud UI / `tcld`.
|
|
311
|
+
*/
|
|
312
|
+
function isPermissionError(detail) {
|
|
313
|
+
if (!detail)
|
|
314
|
+
return false;
|
|
315
|
+
const d = detail.toLowerCase();
|
|
316
|
+
return PERMISSION_ERROR_MARKERS.some((m) => d.includes(m));
|
|
317
|
+
}
|
|
287
318
|
/**
|
|
288
319
|
* Pure classifier — turn a temporal CLI exit into a {@link RegistrationStatus}.
|
|
289
320
|
* Extracted from {@link registerSearchAttribute} so the matching rules can
|
package/dist/cli.js
CHANGED
|
@@ -62,6 +62,7 @@ const types_1 = require("./types");
|
|
|
62
62
|
const config_1 = require("./config");
|
|
63
63
|
const legacy_migration_1 = require("./cli/legacy-migration");
|
|
64
64
|
const global_wrapper_1 = require("./cli/global-wrapper");
|
|
65
|
+
const resolve_ensemble_1 = require("./cli/resolve-ensemble");
|
|
65
66
|
const grpc_shutdown_guard_1 = require("./utils/grpc-shutdown-guard");
|
|
66
67
|
/** Package root — cli.js compiles to dist/cli.js, so one level up. Used by the inline `version` handler. */
|
|
67
68
|
const PACKAGE_ROOT = (0, path_1.resolve)(__dirname, '..');
|
|
@@ -473,7 +474,10 @@ async function main() {
|
|
|
473
474
|
break;
|
|
474
475
|
case 'up':
|
|
475
476
|
await up({
|
|
476
|
-
ensemble,
|
|
477
|
+
// #685 — honor `--ensemble` (flag > positional > env > 'default'), via the
|
|
478
|
+
// shared resolver. Previously `up` passed the bare positional-derived
|
|
479
|
+
// `ensemble`, so `--ensemble <name>` was silently ignored → launched in `default`.
|
|
480
|
+
ensemble: (0, resolve_ensemble_1.resolveEnsemble)(args),
|
|
477
481
|
name: args.name,
|
|
478
482
|
lineup: args.lineup,
|
|
479
483
|
noHold: args.noHold,
|
package/dist/config.d.ts
CHANGED
|
@@ -130,6 +130,15 @@ export declare const ENV: {
|
|
|
130
130
|
* spawns do NOT set it, so recruited adapters keep the #604 anti-leak ppid-poll.
|
|
131
131
|
*/
|
|
132
132
|
readonly NO_PPID_WATCHDOG: "AGENT_TEMPO_NO_PPID_WATCHDOG";
|
|
133
|
+
/**
|
|
134
|
+
* #690 — absolute path to the bridge pid file, computed ONCE by the spawn
|
|
135
|
+
* helper (`bridgeLogPaths(ensemble, name).pidPath`) and passed to the adapter
|
|
136
|
+
* child. The adapter writes/unlinks THIS path rather than re-deriving its own
|
|
137
|
+
* (which diverged from the spawner's when PLAYER_NAME was empty — the
|
|
138
|
+
* split-brain orphan). PLAIN (non-secret): it's a file location, not a
|
|
139
|
+
* credential — must stay inline under #689's `partitionEnv`.
|
|
140
|
+
*/
|
|
141
|
+
readonly PID_FILE: "AGENT_TEMPO_PID_FILE";
|
|
133
142
|
/**
|
|
134
143
|
* Escape hatch for triple-isolated environments (ADR 0014 §5.3). When
|
|
135
144
|
* set, `resolveTempoHome()` returns this path verbatim — bypassing both
|
|
@@ -226,6 +235,48 @@ export declare function isDevMode(): boolean;
|
|
|
226
235
|
export declare function resolveTempoHome(): string;
|
|
227
236
|
export declare const AGENT_TEMPO_HOME: string;
|
|
228
237
|
export declare const CONFIG_FILE_PATH: string;
|
|
238
|
+
/** Resolved log + pid paths for a recruited bridge/adapter player (#690). */
|
|
239
|
+
export interface BridgeLogPaths {
|
|
240
|
+
/** The directory holding the player's log + pid files. */
|
|
241
|
+
dir: string;
|
|
242
|
+
/** `<dir>/<player>.log`. */
|
|
243
|
+
logPath: string;
|
|
244
|
+
/** `<dir>/<player>.pid`. */
|
|
245
|
+
pidPath: string;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* SINGLE source of truth for where a recruited bridge/adapter's `.log` + `.pid`
|
|
249
|
+
* live (#690). Default is CENTRAL — `~/.agent-tempo/logs/<ensemble>/<player>.*` —
|
|
250
|
+
* NOT the old per-cwd `<workDir>/logs` (which scattered pid files across every
|
|
251
|
+
* recruit directory and orphaned them on `down`). No call site should construct
|
|
252
|
+
* its own `join(..., 'logs', ...)`; route everything through here so the writer
|
|
253
|
+
* (spawn helper) and the readers (status / down / hard-terminate) compute the
|
|
254
|
+
* SAME path and can't split-brain.
|
|
255
|
+
*
|
|
256
|
+
* `overrideDir` is the existing per-spawn `opts.logDir` escape hatch (rarely set);
|
|
257
|
+
* when present it wins over the central default. `ensemble`/`player` are
|
|
258
|
+
* regex-validated upstream (ENSEMBLE_NAME_REGEX / PLAYER_NAME_REGEX — no slashes),
|
|
259
|
+
* but a defensive guard rejects path-traversal as insurance.
|
|
260
|
+
*/
|
|
261
|
+
/**
|
|
262
|
+
* Root of the central bridge-log tree: `~/.agent-tempo/logs`. Per-ensemble dirs
|
|
263
|
+
* live under it. Exposed so a cluster-wide reader (e.g. `down`'s
|
|
264
|
+
* killBridgeProcesses) can enumerate every ensemble's dir without re-constructing
|
|
265
|
+
* the `'logs'` segment itself — {@link bridgeLogPaths} is the only other place
|
|
266
|
+
* that names it.
|
|
267
|
+
*/
|
|
268
|
+
export declare function bridgeLogsRoot(): string;
|
|
269
|
+
export declare function bridgeLogPaths(ensemble: string, player: string, overrideDir?: string): BridgeLogPaths;
|
|
270
|
+
/**
|
|
271
|
+
* The pid path an ADAPTER subprocess should write/unlink (#690). The SPAWNER
|
|
272
|
+
* computes the path once via {@link bridgeLogPaths} and passes it as
|
|
273
|
+
* `ENV.PID_FILE`; the adapter consumes THAT — it does NOT re-derive its own from
|
|
274
|
+
* a (possibly divergent) player identifier. The `bridgeLogPaths` fallback is used
|
|
275
|
+
* ONLY when the env is absent (a manual adapter launch outside the spawner). This
|
|
276
|
+
* is the by-construction fix for the copilot split-brain (PLAYER_NAME='' →
|
|
277
|
+
* `copilot-${Date.now()}` ≠ the spawner's logName).
|
|
278
|
+
*/
|
|
279
|
+
export declare function resolveAdapterPidFile(ensemble: string, fallbackPlayer: string): string;
|
|
229
280
|
/**
|
|
230
281
|
* Daemon-level configuration persisted in `~/.agent-tempo/config.json`
|
|
231
282
|
* alongside the existing `PersistedConfig` fields.
|
|
@@ -334,9 +385,18 @@ export declare function loadTemporalCliConfig(): PersistedConfig;
|
|
|
334
385
|
*/
|
|
335
386
|
export declare function parseTemporalYaml(content: string): PersistedConfig;
|
|
336
387
|
/**
|
|
337
|
-
* Parse an agent value against the {@link
|
|
338
|
-
*
|
|
339
|
-
*
|
|
388
|
+
* Parse an agent value against the canonical {@link AGENT_TYPES} union — the
|
|
389
|
+
* SINGLE SOURCE OF TRUTH for agent validity (shared with `cli.ts`'s `--agent`
|
|
390
|
+
* parser). Throws when `value` is present but not a known agent; returns
|
|
391
|
+
* `'claude'` for empty/unset values so callers can use it as a source-aware default.
|
|
392
|
+
*
|
|
393
|
+
* This is a pure type-VALIDITY check — it accepts EVERY `AgentType` (including
|
|
394
|
+
* `mock` and the headless adapters). Narrower CAPABILITY constraints are gated
|
|
395
|
+
* separately downstream: the recruit pre-flight rejects `mock` outside dev mode,
|
|
396
|
+
* and `config`'s `VALID_DEFAULT_AGENTS` restricts the persistent default to the
|
|
397
|
+
* conductor-capable subset. (#683: the former hardcoded `['claude','copilot']`
|
|
398
|
+
* list was stale — it rejected `defaultAgent=pi` at config LOAD, poisoning every
|
|
399
|
+
* command before the `--agent` flag was even read.)
|
|
340
400
|
*/
|
|
341
401
|
export declare function parseAgent(value: string | undefined, source: ConfigSource): AgentType;
|
|
342
402
|
/**
|