@f-o-h/cli 0.1.61 → 0.1.62

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.
Files changed (3) hide show
  1. package/README.md +17 -13
  2. package/dist/foh.js +62 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -68,16 +68,22 @@ foh agent replay --file ./transcript-export.json --json
68
68
  foh bug improve --from-file foh-proof.json --out foh-improvement.json --json
69
69
  ```
70
70
 
71
- Trusted server-side automation can use a scoped service token without browser
72
- approval:
73
-
74
- ```bash
75
- FOH_SERVICE_TOKEN="$FOH_SERVICE_TOKEN" FOH_ORG_ID="$FOH_ORG_ID" foh auth whoami --json
76
- ```
77
-
78
- `auth signup --web` opens the console signup page when possible and always
79
- prints the fallback URL. `auth login --web` starts browser device
80
- authorization, opens `/cli-auth`, waits for console approval, and stores the
71
+ Trusted server-side automation can use a scoped service token without browser
72
+ approval:
73
+
74
+ ```bash
75
+ FOH_SERVICE_TOKEN="$FOH_SERVICE_TOKEN" FOH_ORG_ID="$FOH_ORG_ID" foh auth whoami --json
76
+ ```
77
+
78
+ `auth whoami --json` includes `auth.expires_at` and `auth.token_ttl_seconds`
79
+ without exposing the token. Refresh short-lived eval tokens before long
80
+ external-agent runs; controlled live eval execution fails closed when explicit
81
+ `FOH_EXTERNAL_AGENT_EVAL_*` credentials are missing, expired, or expire before
82
+ the run budget.
83
+
84
+ `auth signup --web` opens the console signup page when possible and always
85
+ prints the fallback URL. `auth login --web` starts browser device
86
+ authorization, opens `/cli-auth`, waits for console approval, and stores the
81
87
  returned short-lived token. Credential auth remains available as fallback.
82
88
 
83
89
  `foh prove` produces a compact signed proof report across auth, org context,
@@ -305,6 +311,4 @@ foh bug improve \
305
311
 
306
312
  The command emits a redacted `foh_improvement_packet.v1` with stable IDs,
307
313
  reason code, promotion decision, evidence summary, and deterministic next
308
- commands.
309
-
310
-
314
+ commands.
package/dist/foh.js CHANGED
@@ -10073,6 +10073,15 @@ function expiryFromJwt(token) {
10073
10073
  function fallbackExpiry() {
10074
10074
  return new Date(Date.now() + 60 * 60 * 1e3).toISOString();
10075
10075
  }
10076
+ function credentialMillisecondsUntilExpiry(expiresAt, now = Date.now()) {
10077
+ const expiresMs = Date.parse(expiresAt);
10078
+ if (!Number.isFinite(expiresMs)) return null;
10079
+ return expiresMs - now;
10080
+ }
10081
+ function credentialExpiresSoon(expiresAt, minimumTtlMs = 0, now = Date.now()) {
10082
+ const remainingMs = credentialMillisecondsUntilExpiry(expiresAt, now);
10083
+ return remainingMs !== null && remainingMs <= minimumTtlMs;
10084
+ }
10076
10085
  function credentialsFromEnv(apiUrlOverride, env = process.env) {
10077
10086
  const token = normalizeEnv(env[SERVICE_TOKEN_ENV]);
10078
10087
  if (!token) return void 0;
@@ -10090,7 +10099,12 @@ function hasEnvCredentials(env = process.env) {
10090
10099
  }
10091
10100
  function loadCredentials(apiUrlOverride) {
10092
10101
  const envCreds = credentialsFromEnv(apiUrlOverride);
10093
- if (envCreds) return envCreds;
10102
+ if (envCreds) {
10103
+ if (credentialExpiresSoon(envCreds.expiresAt)) {
10104
+ throw new Error("Token expired. Refresh FOH_SERVICE_TOKEN/FOH_TOKEN_EXPIRES_AT or run: foh auth login");
10105
+ }
10106
+ return envCreds;
10107
+ }
10094
10108
  let creds;
10095
10109
  try {
10096
10110
  const raw = (0, import_fs.readFileSync)(CONFIG_PATH, "utf-8");
@@ -10098,7 +10112,7 @@ function loadCredentials(apiUrlOverride) {
10098
10112
  } catch {
10099
10113
  throw new Error("Not authenticated. Run: foh auth login");
10100
10114
  }
10101
- if (creds.expiresAt && new Date(creds.expiresAt) < /* @__PURE__ */ new Date()) {
10115
+ if (creds.expiresAt && credentialExpiresSoon(creds.expiresAt)) {
10102
10116
  throw new Error("Token expired. Run: foh auth login");
10103
10117
  }
10104
10118
  const resolvedApiUrl = resolveApiUrlOverride(apiUrlOverride);
@@ -10457,7 +10471,11 @@ async function storeAuthenticatedSession(params) {
10457
10471
  const output = {
10458
10472
  status: "authenticated",
10459
10473
  apiUrl: params.apiUrl,
10460
- expires_at: params.expiresAt
10474
+ expires_at: params.expiresAt,
10475
+ token_ttl_seconds: (() => {
10476
+ const ttlMs = credentialMillisecondsUntilExpiry(params.expiresAt);
10477
+ return ttlMs === null ? null : Math.max(0, Math.floor(ttlMs / 1e3));
10478
+ })()
10461
10479
  };
10462
10480
  if (autoOrgId) {
10463
10481
  output["default_org_id"] = autoOrgId;
@@ -10631,11 +10649,17 @@ function registerAuth(program3) {
10631
10649
  try {
10632
10650
  const creds = loadCredentials(opts.apiUrl);
10633
10651
  const orgId = opts.org ?? creds.orgId;
10652
+ const ttlMs = credentialMillisecondsUntilExpiry(creds.expiresAt);
10653
+ const authMetadata = {
10654
+ apiUrl: creds.apiUrl,
10655
+ expires_at: creds.expiresAt,
10656
+ token_ttl_seconds: ttlMs === null ? null : Math.max(0, Math.floor(ttlMs / 1e3)),
10657
+ default_org_id: creds.orgId ?? null,
10658
+ auth_source: process.env.FOH_SERVICE_TOKEN ? "env_service_token" : "stored_credentials"
10659
+ };
10634
10660
  if (!orgId) {
10635
10661
  format({
10636
- apiUrl: creds.apiUrl,
10637
- expires_at: creds.expiresAt,
10638
- default_org_id: creds.orgId ?? null,
10662
+ ...authMetadata,
10639
10663
  note: creds.orgId ? void 0 : "No default org set. Run: foh org list then: foh org use --org <id>"
10640
10664
  }, { json: opts.json ?? false });
10641
10665
  return;
@@ -10651,7 +10675,7 @@ function registerAuth(program3) {
10651
10675
  });
10652
10676
  }
10653
10677
  const data = await res.json();
10654
- format(data, { json: opts.json ?? false });
10678
+ format({ ...data, auth: authMetadata }, { json: opts.json ?? false });
10655
10679
  } catch (e) {
10656
10680
  if (e instanceof Error && (e.message.includes("Not authenticated") || e.message.includes("Token expired"))) {
10657
10681
  throw new FohError({
@@ -32853,7 +32877,7 @@ var StdioServerTransport = class {
32853
32877
  };
32854
32878
 
32855
32879
  // src/lib/cli-version.ts
32856
- var CLI_VERSION = "0.1.61";
32880
+ var CLI_VERSION = "0.1.62";
32857
32881
 
32858
32882
  // src/commands/mcp-serve.ts
32859
32883
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -39340,19 +39364,25 @@ function shouldRunExternalAgentEvalAuthPreflight(env = process.env) {
39340
39364
  env.FOH_EXTERNAL_AGENT_EVAL_TOKEN || env.FOH_EXTERNAL_AGENT_EVAL_ORG_ID || env.FOH_EXTERNAL_AGENT_EVAL_API_URL || env.FOH_EXTERNAL_AGENT_EVAL_TOKEN_EXPIRES_AT || env.FOH_SERVICE_TOKEN || env.FOH_ORG_ID || env.FOH_API_URL || env.FOH_TOKEN_EXPIRES_AT
39341
39365
  );
39342
39366
  }
39343
- async function runExternalAgentEvalAuthPreflight(env = process.env) {
39344
- if (!shouldRunExternalAgentEvalAuthPreflight(env)) return null;
39367
+ async function runExternalAgentEvalAuthPreflight(env = process.env, options = {}) {
39368
+ const hasExplicitEvalAuth = Boolean(
39369
+ env.FOH_EXTERNAL_AGENT_EVAL_TOKEN || env.FOH_EXTERNAL_AGENT_EVAL_ORG_ID || env.FOH_EXTERNAL_AGENT_EVAL_API_URL || env.FOH_EXTERNAL_AGENT_EVAL_TOKEN_EXPIRES_AT
39370
+ );
39371
+ if (!shouldRunExternalAgentEvalAuthPreflight(env) && !options.requireExplicitEvalAuth) return null;
39345
39372
  const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
39346
39373
  const auth = readExternalAgentEvalAuthEnv(env);
39374
+ const tokenTtlMs = auth.expiresAt ? Date.parse(auth.expiresAt) - Date.now() : null;
39347
39375
  const base = {
39348
39376
  api_url: auth.apiUrl,
39349
39377
  org_id: auth.orgId || null,
39350
39378
  checked_at: checkedAt,
39351
- token_present: Boolean(auth.token),
39379
+ token_present: Boolean(auth.token) && hasExplicitEvalAuth,
39352
39380
  expires_at: auth.expiresAt || null,
39381
+ token_ttl_seconds: tokenTtlMs === null || !Number.isFinite(tokenTtlMs) ? null : Math.max(0, Math.floor(tokenTtlMs / 1e3)),
39382
+ minimum_required_ttl_seconds: typeof options.minimumTtlMs === "number" ? Math.ceil(options.minimumTtlMs / 1e3) : null,
39353
39383
  matched_org: null
39354
39384
  };
39355
- if (!auth.token || !auth.orgId) {
39385
+ if (!hasExplicitEvalAuth || !auth.token || !auth.orgId) {
39356
39386
  return { ...base, ok: false, reason_code: "eval_auth_missing" };
39357
39387
  }
39358
39388
  if (auth.expiresAt) {
@@ -39360,6 +39390,9 @@ async function runExternalAgentEvalAuthPreflight(env = process.env) {
39360
39390
  if (Number.isFinite(expiresMs) && expiresMs <= Date.now()) {
39361
39391
  return { ...base, ok: false, reason_code: "eval_auth_expired" };
39362
39392
  }
39393
+ if (typeof options.minimumTtlMs === "number" && Number.isFinite(expiresMs) && expiresMs - Date.now() < options.minimumTtlMs) {
39394
+ return { ...base, ok: false, reason_code: "eval_auth_expires_too_soon" };
39395
+ }
39363
39396
  }
39364
39397
  const res = await fetch(`${auth.apiUrl.replace(/\/$/, "")}/v1/console/auth/my-orgs`, {
39365
39398
  headers: {
@@ -39853,6 +39886,7 @@ function createExternalAgentExecutorPlan(options) {
39853
39886
  batch_path: batchPath,
39854
39887
  batch_dir: batchDir,
39855
39888
  private_repo_root: privateRepoRoot,
39889
+ private_repo_root_explicit: Boolean(options.privateRepoRoot),
39856
39890
  workspace_root: workspaceRoot,
39857
39891
  timeout_minutes: timeoutMinutes,
39858
39892
  safety: {
@@ -40171,7 +40205,10 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
40171
40205
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
40172
40206
  const results = [];
40173
40207
  const runnerCommand = options.runnerCommand || resolveRunnerExecutionCommand(plan.runner);
40174
- const authPreflight = options.env ? await runExternalAgentEvalAuthPreflight(options.env) : null;
40208
+ const authPreflight = options.env || options.requireExplicitEvalAuth ? await runExternalAgentEvalAuthPreflight(options.env ?? {}, {
40209
+ requireExplicitEvalAuth: options.requireExplicitEvalAuth,
40210
+ minimumTtlMs: options.minimumEvalAuthTtlMs
40211
+ }) : null;
40175
40212
  if (authPreflight && !authPreflight.ok) {
40176
40213
  const endedAt2 = (/* @__PURE__ */ new Date()).toISOString();
40177
40214
  const blockedResults = plan.runs.map((run) => {
@@ -40322,6 +40359,10 @@ function quoteArg(value) {
40322
40359
  if (/^[A-Za-z0-9_./:=@-]+$/.test(text)) return text;
40323
40360
  return `"${text.replace(/(["$`])/g, "\\$1")}"`;
40324
40361
  }
40362
+ function scanArtifactsCommand(runDir, privateRepoRoot) {
40363
+ const privateRootArg = privateRepoRoot ? ` --private-repo-root ${quoteArg(privateRepoRoot)}` : "";
40364
+ return `foh eval external-agent scan-artifacts --run-dir ${quoteArg(runDir)}${privateRootArg} --write-redacted --json`;
40365
+ }
40325
40366
  function externalAgentSummaryCommand2(root) {
40326
40367
  const summaryPath = (0, import_path14.join)(root, "latest-summary.json");
40327
40368
  const reportPath = (0, import_path14.join)(root, "summary.report.json");
@@ -40681,7 +40722,7 @@ Exit the shell to finalize run.json.
40681
40722
  external.command("scan-artifacts").description("Scan and redact external-agent run artifacts before they are promoted into improvement loops").requiredOption("--run-dir <path>", "External-agent run artifact directory").option("--private-repo-root <path>", "Private repository root that must not appear in artifacts").option("--write-redacted", "Write .redacted copies next to scanned artifacts").option("--json", "Output as JSON").action(async (opts) => {
40682
40723
  const report = scanExternalAgentArtifacts({
40683
40724
  runDir: String(opts.runDir),
40684
- privateRepoRoot: opts.privateRepoRoot ? String(opts.privateRepoRoot) : process.cwd(),
40725
+ privateRepoRoot: opts.privateRepoRoot ? String(opts.privateRepoRoot) : void 0,
40685
40726
  writeRedacted: Boolean(opts.writeRedacted)
40686
40727
  });
40687
40728
  format(cliEnvelope({
@@ -40731,7 +40772,9 @@ Exit the shell to finalize run.json.
40731
40772
  }
40732
40773
  const result = await executeExternalAgentExecutorPlan(plan, {
40733
40774
  privateRepoRoot: plan.private_repo_root,
40734
- env: process.env
40775
+ env: process.env,
40776
+ requireExplicitEvalAuth: true,
40777
+ minimumEvalAuthTtlMs: (plan.timeout_minutes + 5) * 60 * 1e3
40735
40778
  });
40736
40779
  const resultPath = (0, import_path14.join)(plan.batch_dir, "execution-result.json");
40737
40780
  (0, import_fs16.writeFileSync)(resultPath, `${JSON.stringify(result, null, 2)}
@@ -40747,7 +40790,10 @@ Exit the shell to finalize run.json.
40747
40790
  runs: result.results.map((run) => run.artifacts.run)
40748
40791
  },
40749
40792
  nextCommands: [
40750
- ...result.results.map((run) => `foh eval external-agent scan-artifacts --run-dir ${quoteArg(plan.runs.find((item) => item.run_id === run.run_id)?.run_dir || ".")} --private-repo-root ${quoteArg(plan.private_repo_root)} --write-redacted --json`),
40793
+ ...result.results.map((run) => scanArtifactsCommand(
40794
+ plan.runs.find((item) => item.run_id === run.run_id)?.run_dir || ".",
40795
+ plan.private_repo_root_explicit ? plan.private_repo_root : void 0
40796
+ )),
40751
40797
  externalAgentSummaryCommand2(plan.batch_dir)
40752
40798
  ],
40753
40799
  extra: { result }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@f-o-h/cli",
3
- "version": "0.1.61",
3
+ "version": "0.1.62",
4
4
  "description": "FOH CLI - AI-operator provisioning tool for Front Of House",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {