agent-tempo 1.6.0 → 1.6.1

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-tempo-dashboard",
3
3
  "private": true,
4
- "version": "1.6.0",
4
+ "version": "1.6.1",
5
5
  "type": "module",
6
6
  "description": "Web dashboard for agent-tempo. Bundled into the npm package; served by the daemon at /dashboard/*.",
7
7
  "scripts": {
@@ -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
- // Surface the real error pre-#605 this branch was silently
952
- // labeled "already exists" and the operator only discovered the
953
- // problem hours later when workflow start failed with
954
- // INVALID_ARGUMENT. Most common cause on the SQLite dev server is
955
- // the 10-Keyword-per-namespace cap (often hit when a namespace
956
- // accumulates both old + new wire-rename attribute families).
957
- failed++;
958
- out.warn(`Failed to register ${attr.name}: ${r.detail}`);
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.`);
@@ -11,8 +11,24 @@ 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
+ /** True when a config key holds a credential value that must be masked on display. */
23
+ export declare function isSecretKey(key: string): boolean;
24
+ /**
25
+ * Render a secret for display: a short non-sensitive prefix (when the value is
26
+ * long enough that the prefix reveals only a small fraction) + a masked tail +
27
+ * the char count. NEVER returns the full value. Empty/unset → "(not set)".
28
+ *
29
+ * Examples: `sk-ant-…•••• (set, 47 chars)` · short secret → `•••• (set, 6 chars)`.
30
+ */
31
+ export declare function maskSecret(value: string | undefined | null): string;
16
32
  /** Interactive config setup: `agent-tempo config` */
17
33
  export declare function configInteractive(): Promise<void>;
18
34
  /** Non-interactive: `agent-tempo config set <key> <value>` */
@@ -34,6 +34,8 @@ 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.isSecretKey = isSecretKey;
38
+ exports.maskSecret = maskSecret;
37
39
  exports.configInteractive = configInteractive;
38
40
  exports.configSet = configSet;
39
41
  exports.configShow = configShow;
@@ -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,38 @@ 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
- const SECRET_KEYS = new Set(['temporalApiKey']);
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). Generalized on
75
+ // purpose: a future secret added to the config is masked BY DEFAULT, not leaked.
76
+ const SECRET_KEYS = new Set(['temporalApiKey', 'httpToken', 'readToken', 'adminToken']);
77
+ // Matches *_API_KEY / *ApiKey / *Token / *Secret / *Password but NOT path fields
78
+ // (e.g. temporalTlsKeyPath is a file path, not the key — it must stay visible).
79
+ const SECRET_KEY_PATTERN = /(api[_-]?key|token|secret|password)/i;
80
+ /** True when a config key holds a credential value that must be masked on display. */
81
+ function isSecretKey(key) {
82
+ if (/path$/i.test(key))
83
+ return false; // *Path fields are file locations, not secrets
84
+ return SECRET_KEYS.has(key) || SECRET_KEY_PATTERN.test(key);
85
+ }
86
+ /**
87
+ * Render a secret for display: a short non-sensitive prefix (when the value is
88
+ * long enough that the prefix reveals only a small fraction) + a masked tail +
89
+ * the char count. NEVER returns the full value. Empty/unset → "(not set)".
90
+ *
91
+ * Examples: `sk-ant-…•••• (set, 47 chars)` · short secret → `•••• (set, 6 chars)`.
92
+ */
93
+ function maskSecret(value) {
94
+ if (value == null || value === '')
95
+ return '(not set)';
96
+ const len = value.length;
97
+ // Reveal a prefix only when it's a small fraction of the whole; never for short
98
+ // secrets (so the output can never contain the full input — see the unit test).
99
+ const prefixLen = len >= 12 ? 6 : len >= 8 ? 3 : 0;
100
+ const prefix = value.slice(0, prefixLen);
101
+ const masked = prefixLen > 0 ? `${prefix}…••••` : '••••';
102
+ return `${masked} (set, ${len} chars)`;
103
+ }
65
104
  /** Read a line from stdin with a prompt and optional default value. */
66
105
  function ask(prompt, defaultVal, mask = false) {
67
106
  return new Promise((resolve) => {
@@ -69,7 +108,11 @@ function ask(prompt, defaultVal, mask = false) {
69
108
  input: process.stdin,
70
109
  output: process.stdout,
71
110
  });
72
- const display = defaultVal ? `${prompt} (${defaultVal}): ` : `${prompt}: `;
111
+ // #684 for masked (secret) prompts NEVER echo the raw existing value as the
112
+ // shown default; render a masked hint instead. The real `defaultVal` is still
113
+ // returned on empty input, so an existing key is preserved without exposing it.
114
+ const shownDefault = mask ? maskSecret(defaultVal) : defaultVal;
115
+ const display = defaultVal ? `${prompt} (${shownDefault}): ` : `${prompt}: `;
73
116
  if (mask) {
74
117
  // For secret input: write prompt manually, mute output
75
118
  process.stdout.write(`? ${display}`);
@@ -212,7 +255,9 @@ function configSet(key, value) {
212
255
  }
213
256
  config[configKey] = value;
214
257
  (0, config_1.saveConfigFile)(config);
215
- out.success(`Set ${configKey} = ${configKey.includes('Key') ? '****' : value}`);
258
+ // #684 echo through the same secret-masking path so `config set temporalApiKey …`
259
+ // never prints the value back raw (and a *Path field still shows its location).
260
+ out.success(`Set ${configKey} = ${isSecretKey(configKey) ? maskSecret(value) : value}`);
216
261
  }
217
262
  /** Show current config: `agent-tempo config show` */
218
263
  function configShow() {
@@ -234,8 +279,9 @@ function configShow() {
234
279
  for (const { key, configKey } of keys) {
235
280
  const value = config[configKey];
236
281
  const source = sources[configKey];
237
- const isSecret = SECRET_KEYS.has(key);
238
- const display = !value ? '(not set)' : isSecret ? '****' : value;
282
+ // #684 secret-like fields go through maskSecret (prefix + masked tail + char
283
+ // count); everything else shows its value or "(not set)".
284
+ const display = isSecretKey(key) ? maskSecret(value) : (!value ? '(not set)' : value);
239
285
  out.log(` ${key.padEnd(22)} ${display.padEnd(30)} ${out.dim(source)}`);
240
286
  }
241
287
  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
@@ -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
@@ -334,9 +334,18 @@ export declare function loadTemporalCliConfig(): PersistedConfig;
334
334
  */
335
335
  export declare function parseTemporalYaml(content: string): PersistedConfig;
336
336
  /**
337
- * Parse an agent value against the {@link AgentType} union.
338
- * Throws when `value` is present but not a valid agent; returns `'claude'`
339
- * for empty/unset values so callers can use it as a source-aware default.
337
+ * Parse an agent value against the canonical {@link AGENT_TYPES} union — the
338
+ * SINGLE SOURCE OF TRUTH for agent validity (shared with `cli.ts`'s `--agent`
339
+ * parser). Throws when `value` is present but not a known agent; returns
340
+ * `'claude'` for empty/unset values so callers can use it as a source-aware default.
341
+ *
342
+ * This is a pure type-VALIDITY check — it accepts EVERY `AgentType` (including
343
+ * `mock` and the headless adapters). Narrower CAPABILITY constraints are gated
344
+ * separately downstream: the recruit pre-flight rejects `mock` outside dev mode,
345
+ * and `config`'s `VALID_DEFAULT_AGENTS` restricts the persistent default to the
346
+ * conductor-capable subset. (#683: the former hardcoded `['claude','copilot']`
347
+ * list was stale — it rejected `defaultAgent=pi` at config LOAD, poisoning every
348
+ * command before the `--agent` flag was even read.)
340
349
  */
341
350
  export declare function parseAgent(value: string | undefined, source: ConfigSource): AgentType;
342
351
  /**
package/dist/config.js CHANGED
@@ -23,14 +23,8 @@ const fs_1 = require("fs");
23
23
  const path_1 = require("path");
24
24
  const os_1 = require("os");
25
25
  const zod_1 = require("zod");
26
+ const types_1 = require("./types");
26
27
  const validation_1 = require("./utils/validation");
27
- // `'mock'` is a valid `AgentType` value but intentionally NOT in the resolved
28
- // `defaultAgent` set — recruit pre-flight rejects it outside dev mode anyway,
29
- // and it's never a sensible *default* (each mock spawn is configured per call
30
- // via the `agent: 'mock'` flag, not via the resolved chain). Listing it here
31
- // would only enable users to set `defaultAgent=mock` in `~/.agent-tempo/config.json`,
32
- // which the recruit gate would then turn around and reject in production.
33
- const VALID_AGENTS = ['claude', 'copilot'];
34
28
  /** Environment variable name constants — use these instead of string literals. */
35
29
  exports.ENV = {
36
30
  ENSEMBLE: 'AGENT_TEMPO_ENSEMBLE',
@@ -458,16 +452,25 @@ const AGENT_SOURCE_LABELS = {
458
452
  none: 'none',
459
453
  };
460
454
  /**
461
- * Parse an agent value against the {@link AgentType} union.
462
- * Throws when `value` is present but not a valid agent; returns `'claude'`
463
- * for empty/unset values so callers can use it as a source-aware default.
455
+ * Parse an agent value against the canonical {@link AGENT_TYPES} union — the
456
+ * SINGLE SOURCE OF TRUTH for agent validity (shared with `cli.ts`'s `--agent`
457
+ * parser). Throws when `value` is present but not a known agent; returns
458
+ * `'claude'` for empty/unset values so callers can use it as a source-aware default.
459
+ *
460
+ * This is a pure type-VALIDITY check — it accepts EVERY `AgentType` (including
461
+ * `mock` and the headless adapters). Narrower CAPABILITY constraints are gated
462
+ * separately downstream: the recruit pre-flight rejects `mock` outside dev mode,
463
+ * and `config`'s `VALID_DEFAULT_AGENTS` restricts the persistent default to the
464
+ * conductor-capable subset. (#683: the former hardcoded `['claude','copilot']`
465
+ * list was stale — it rejected `defaultAgent=pi` at config LOAD, poisoning every
466
+ * command before the `--agent` flag was even read.)
464
467
  */
465
468
  function parseAgent(value, source) {
466
469
  if (value == null || value === '')
467
470
  return 'claude';
468
- if (!VALID_AGENTS.includes(value)) {
471
+ if (!types_1.AGENT_TYPES.includes(value)) {
469
472
  throw new Error(`Invalid agent "${value}" from ${AGENT_SOURCE_LABELS[source]}. ` +
470
- `Valid values: ${VALID_AGENTS.join(', ')}.`);
473
+ `Valid values: ${types_1.AGENT_TYPES.join(', ')}.`);
471
474
  }
472
475
  return value;
473
476
  }
@@ -18,7 +18,15 @@ function buildPiInjector(rt) {
18
18
  const sendUser = typeof pi?.sendUserMessage === 'function' ? pi.sendUserMessage.bind(pi) : null;
19
19
  return {
20
20
  inject: (msg, opts) => send(msg, opts),
21
- ...(sendUser ? { escalate: (text) => sendUser(text) } : {}),
21
+ // #688 escalate with `deliverAs: 'followUp'`. maybeEscalate can fire while a
22
+ // turn is ALREADY in flight (one that started BEFORE the inject — a busy
23
+ // false-positive), and a bare sendUserMessage (no deliverAs) while Pi is
24
+ // streaming throws "Agent is already processing". followUp is correct in BOTH
25
+ // cases: cold-idle (behavior ignored → the user message still starts a turn,
26
+ // escalation works) and busy (queues + drains in order, no throw). NOT 'steer'
27
+ // — steer would let a peer cue preempt the operator's in-flight turn, breaking
28
+ // the operator-vs-peer guarantee (see file header).
29
+ ...(sendUser ? { escalate: (text) => sendUser(text, { deliverAs: 'followUp' }) } : {}),
22
30
  lastTurnStartAt: () => rt.lastTurnStartAt,
23
31
  };
24
32
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-tempo",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "Many agents, one tempo. Durable coordination for multi-agent work via Temporal.",
5
5
  "keywords": [
6
6
  "mcp",