@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.
- package/README.md +17 -13
- package/dist/foh.js +62 -16
- 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
|
|
79
|
-
|
|
80
|
-
|
|
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)
|
|
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 &&
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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) :
|
|
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) =>
|
|
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 }
|