imprint-mcp 0.6.0 → 0.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/README.md +2 -0
- package/package.json +4 -2
- package/prompts/audit-agent.md +4 -2
- package/prompts/auth-compile-agent.md +149 -158
- package/prompts/build-planning.md +14 -9
- package/prompts/compile-agent.md +46 -23
- package/prompts/live-verifier-agent.md +101 -0
- package/prompts/playbook-compilation.md +8 -1
- package/prompts/tool-candidate-detection.md +9 -29
- package/src/cli.ts +83 -1
- package/src/imprint/audit.ts +219 -20
- package/src/imprint/auth-compile-agent.ts +215 -155
- package/src/imprint/auth-compile-tools.ts +300 -118
- package/src/imprint/auth-verifier.ts +167 -180
- package/src/imprint/backend-cache.ts +18 -11
- package/src/imprint/backend-ladder.ts +225 -177
- package/src/imprint/build-plan.ts +3 -8
- package/src/imprint/cdp-browser-fetch.ts +930 -83
- package/src/imprint/claude-cli-compile.ts +46 -20
- package/src/imprint/codex-cli-compile.ts +436 -139
- package/src/imprint/compile-agent-types.ts +15 -10
- package/src/imprint/compile-agent.ts +88 -28
- package/src/imprint/compile-tools.ts +228 -39
- package/src/imprint/compile-verification.ts +187 -0
- package/src/imprint/compile.ts +71 -2
- package/src/imprint/credential-store.ts +57 -44
- package/src/imprint/emit.ts +3 -1
- package/src/imprint/freeform-redact.ts +27 -6
- package/src/imprint/live-verifier.ts +1665 -0
- package/src/imprint/llm.ts +22 -3
- package/src/imprint/login.ts +107 -25
- package/src/imprint/mcp-compile-server.ts +447 -114
- package/src/imprint/mcp-live-verifier-server.ts +359 -0
- package/src/imprint/mcp-server.ts +116 -35
- package/src/imprint/probe-backends.ts +88 -7
- package/src/imprint/recording-request.ts +36 -0
- package/src/imprint/request-capture.ts +40 -17
- package/src/imprint/runtime.ts +490 -337
- package/src/imprint/teach-state.ts +36 -2
- package/src/imprint/teach.ts +302 -146
- package/src/imprint/tool-candidates.ts +8 -10
- package/src/imprint/tool-loader.ts +12 -4
- package/src/imprint/types.ts +137 -68
package/src/imprint/teach.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* step, and multiple workflows per site (each in its own subdirectory).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
10
11
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
12
|
import { homedir } from 'node:os';
|
|
12
13
|
import {
|
|
@@ -18,12 +19,14 @@ import {
|
|
|
18
19
|
import * as p from '@clack/prompts';
|
|
19
20
|
import type { OnDeadlineReached } from './agent.ts';
|
|
20
21
|
import { compileAuthAgent } from './auth-compile-agent.ts';
|
|
22
|
+
import { authWorkflowHash } from './auth-compile-tools.ts';
|
|
21
23
|
import {
|
|
22
24
|
type SharedModuleManifestEntry,
|
|
23
25
|
buildPlanSidecarPath,
|
|
24
26
|
readBuildPlanFile,
|
|
25
27
|
topoLevelsForTools,
|
|
26
28
|
} from './build-plan.ts';
|
|
29
|
+
import type { CompileAgentResult } from './compile-agent-types.ts';
|
|
27
30
|
import {
|
|
28
31
|
type CompileAgentProgress,
|
|
29
32
|
type TriageResult,
|
|
@@ -55,6 +58,7 @@ import {
|
|
|
55
58
|
generateSkillMd,
|
|
56
59
|
} from './integrations.ts';
|
|
57
60
|
import {
|
|
61
|
+
type LLMOptions,
|
|
58
62
|
type ProviderName,
|
|
59
63
|
type ProviderStatus,
|
|
60
64
|
detectTeachProvider,
|
|
@@ -65,6 +69,7 @@ import { loadJsonFile } from './load-json.ts';
|
|
|
65
69
|
import { createLog, muteLog, unmuteLog } from './log.ts';
|
|
66
70
|
import { MultiProgress } from './multi-progress.ts';
|
|
67
71
|
import { localSiteDir, localToolDir } from './paths.ts';
|
|
72
|
+
import { rebindExistingBackendsCacheToWorkflow } from './probe-backends.ts';
|
|
68
73
|
import { describeAgentActivity, formatElapsed } from './progress.ts';
|
|
69
74
|
import { record } from './record.ts';
|
|
70
75
|
import { detectPageMintedHeaders, redactSession } from './redact.ts';
|
|
@@ -118,6 +123,37 @@ import { setSpanAttributes, shutdownTracing, traced } from './tracing.ts';
|
|
|
118
123
|
import { CronConfigSchema, SessionSchema, WorkflowSchema } from './types.ts';
|
|
119
124
|
import type { CronConfig, Playbook, Session, Workflow } from './types.ts';
|
|
120
125
|
|
|
126
|
+
export function authCompileLlmConfig(provider: ProviderName, model: string): LLMOptions {
|
|
127
|
+
return { provider, model };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function authCompletionMatches(
|
|
131
|
+
completion: WorkflowState['authCompletion'],
|
|
132
|
+
expected: { toolName: string; buildPlanHash: string; workflowHash: string },
|
|
133
|
+
): boolean {
|
|
134
|
+
return Boolean(
|
|
135
|
+
completion &&
|
|
136
|
+
completion.toolName === expected.toolName &&
|
|
137
|
+
completion.buildPlanHash === expected.buildPlanHash &&
|
|
138
|
+
completion.workflowHash === expected.workflowHash,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function hasDurableAuthState(
|
|
143
|
+
workflow: Workflow,
|
|
144
|
+
stored: { values: Record<string, string>; cookies: unknown[]; storage?: unknown[] },
|
|
145
|
+
): boolean {
|
|
146
|
+
const required = workflow.authConfig?.persist ?? [];
|
|
147
|
+
if (required.length > 0) {
|
|
148
|
+
return required.every((name) => String(stored.values[name] ?? '').length > 0);
|
|
149
|
+
}
|
|
150
|
+
return stored.cookies.length > 0 || (stored.storage?.length ?? 0) > 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function fileSha256(path: string): string {
|
|
154
|
+
return createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
155
|
+
}
|
|
156
|
+
|
|
121
157
|
export {
|
|
122
158
|
buildTeachStateFromSession,
|
|
123
159
|
resolveTeachStatePath,
|
|
@@ -126,9 +162,11 @@ export {
|
|
|
126
162
|
|
|
127
163
|
/**
|
|
128
164
|
* How many compile agents run in parallel when more than one tool is selected.
|
|
165
|
+
* Each compiler can now invoke a separate live semantic verifier, so keep this
|
|
166
|
+
* at 2 to avoid provider rate-limit bursts and same-site anti-bot pressure.
|
|
129
167
|
* Single-tool runs still use concurrency 1.
|
|
130
168
|
*/
|
|
131
|
-
const COMPILE_CONCURRENCY =
|
|
169
|
+
const COMPILE_CONCURRENCY = 2;
|
|
132
170
|
|
|
133
171
|
/** Module logger — suppressed during teach's spinner phases via muteLog(). */
|
|
134
172
|
const log = createLog('teach');
|
|
@@ -957,16 +995,12 @@ export async function teach(opts: TeachOptions): Promise<TeachResult> {
|
|
|
957
995
|
allLoginSeqs.length > 0
|
|
958
996
|
? {
|
|
959
997
|
sharedContext: {
|
|
960
|
-
loginRequestSeqs:
|
|
998
|
+
loginRequestSeqs: credentialSeqs,
|
|
961
999
|
credentialNames: [],
|
|
962
1000
|
tokenExtractionNotes: '',
|
|
963
1001
|
sharedHelperNotes: '',
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
twoFactorRequestSeqs: [],
|
|
967
|
-
authCompletionSeqs: [],
|
|
968
|
-
twoFactorContext: [],
|
|
969
|
-
twoFactorNotes: '',
|
|
1002
|
+
authRequestSeqs: allLoginSeqs,
|
|
1003
|
+
authNotes: '',
|
|
970
1004
|
},
|
|
971
1005
|
}
|
|
972
1006
|
: {},
|
|
@@ -1187,21 +1221,36 @@ export async function teach(opts: TeachOptions): Promise<TeachResult> {
|
|
|
1187
1221
|
});
|
|
1188
1222
|
}
|
|
1189
1223
|
// A `--from-step` resume into plan-prereqs/generate reconstructs the prior
|
|
1190
|
-
// run's tools from persisted state (shared-module planning
|
|
1191
|
-
//
|
|
1192
|
-
//
|
|
1224
|
+
// run's tools from persisted state (shared-module planning may need ≥2 tools).
|
|
1225
|
+
// An explicit --all-tools or interactive resume keeps the full set; documented
|
|
1226
|
+
// non-interactive behavior selects only the persisted primary candidate.
|
|
1227
|
+
// selectMultiToolResumePlans scopes reconstruction to the same recording as the
|
|
1193
1228
|
// resume target and to tools that actually reached `startFrom`'s prerequisites,
|
|
1194
1229
|
// so a sibling from a different run can't be compiled against the wrong session
|
|
1195
1230
|
// and one that failed earlier can't crash loading a missing artifact. Confined
|
|
1196
1231
|
// to --from-step so interactive resume keeps its single-tool behavior.
|
|
1197
1232
|
const allCandidatePlans: CandidateCompilePlan[] = [];
|
|
1198
1233
|
if (opts.fromStep) {
|
|
1199
|
-
|
|
1234
|
+
let plannedToolNames: Set<string> | undefined;
|
|
1235
|
+
const persistedBuildPlanPath = resolveTeachStatePath(site, ws?.buildPlanPath);
|
|
1236
|
+
if (persistedBuildPlanPath && existsSync(persistedBuildPlanPath)) {
|
|
1237
|
+
const persistedBuildPlan = readBuildPlanFile(persistedBuildPlanPath);
|
|
1238
|
+
if (persistedBuildPlan) {
|
|
1239
|
+
plannedToolNames = new Set(persistedBuildPlan.perTool.map((tool) => tool.toolName));
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
const selection = selectMultiToolResumePlans(state, workflowKey, startFrom, {
|
|
1243
|
+
noInteractive: opts.noInteractive,
|
|
1244
|
+
allTools: opts.allTools,
|
|
1245
|
+
plannedToolNames,
|
|
1246
|
+
});
|
|
1200
1247
|
for (const { workflowKey: skippedKey, reason } of selection.skipped) {
|
|
1201
1248
|
p.log.warn(
|
|
1202
1249
|
reason === 'different-recording'
|
|
1203
1250
|
? `Skipping tool "${skippedKey}" for --from-step ${startFrom}: it belongs to a different recording than the resume target.`
|
|
1204
|
-
:
|
|
1251
|
+
: reason === 'not-in-build-plan'
|
|
1252
|
+
? `Skipping tool "${skippedKey}" for --from-step ${startFrom}: it is not in the current build plan.`
|
|
1253
|
+
: `Skipping tool "${skippedKey}" for --from-step ${startFrom}: its prior run didn't reach "${startFrom}" — resume an earlier step or re-run it.`,
|
|
1205
1254
|
);
|
|
1206
1255
|
}
|
|
1207
1256
|
for (const sel of selection.plans) {
|
|
@@ -1273,7 +1322,7 @@ export async function teach(opts: TeachOptions): Promise<TeachResult> {
|
|
|
1273
1322
|
// full) while dropping the bulk of telemetry/asset/noise requests — e.g. 415
|
|
1274
1323
|
// vs 4396 requests on a big multi-tool site. Handing the agent the entire
|
|
1275
1324
|
// redacted recording instead makes it burn its turn/time budget exploring
|
|
1276
|
-
// thousands of irrelevant requests (
|
|
1325
|
+
// thousands of irrelevant requests (large auth recordings otherwise may never converge on a
|
|
1277
1326
|
// playbook because of this). The detect-candidates summary already triages;
|
|
1278
1327
|
// this aligns the compile session with it.
|
|
1279
1328
|
const triagedCandidate = redactedPath?.replace(/\.redacted\.json$/, '.triaged.json');
|
|
@@ -1398,92 +1447,142 @@ export async function teach(opts: TeachOptions): Promise<TeachResult> {
|
|
|
1398
1447
|
// before any tool (auth or data) compiles.
|
|
1399
1448
|
if (stopIdx < STEPS.indexOf('generate')) await finishEarly();
|
|
1400
1449
|
|
|
1450
|
+
if (authDetected && willGenerate && !buildPlanPath) {
|
|
1451
|
+
throw new Error(
|
|
1452
|
+
'Authentication was detected, but auth planning did not produce a build plan. Data-tool compilation was not started.',
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1401
1456
|
// ── auth-tool: agentic compile loop + interactive 2FA ──
|
|
1402
1457
|
if (buildPlanPath && willGenerate) {
|
|
1403
1458
|
const buildPlan = readBuildPlanFile(buildPlanPath);
|
|
1404
1459
|
if (buildPlan?.authTool) {
|
|
1405
1460
|
const authPlan = buildPlan.authTool;
|
|
1406
|
-
const
|
|
1407
|
-
|
|
1461
|
+
const buildPlanHash = fileSha256(buildPlanPath);
|
|
1462
|
+
const existingAuthWorkflowPath = pathJoin(
|
|
1463
|
+
localToolDir(site, authPlan.toolName),
|
|
1464
|
+
'workflow.json',
|
|
1408
1465
|
);
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1466
|
+
const existingAuthIndexPath = pathJoin(localToolDir(site, authPlan.toolName), 'index.ts');
|
|
1467
|
+
const existingAuthWorkflow =
|
|
1468
|
+
existsSync(existingAuthWorkflowPath) && existsSync(existingAuthIndexPath)
|
|
1469
|
+
? WorkflowSchema.parse(JSON.parse(readFileSync(existingAuthWorkflowPath, 'utf8')))
|
|
1470
|
+
: undefined;
|
|
1471
|
+
const storedAuthState = existingAuthWorkflow
|
|
1472
|
+
? await loadSiteCredentials(site).catch(() => null)
|
|
1473
|
+
: null;
|
|
1474
|
+
const existingAuthWorkflowHash = existingAuthWorkflow
|
|
1475
|
+
? authWorkflowHash(existingAuthWorkflow)
|
|
1476
|
+
: undefined;
|
|
1477
|
+
const reusableCompletion =
|
|
1478
|
+
existingAuthWorkflow &&
|
|
1479
|
+
existingAuthWorkflowHash &&
|
|
1480
|
+
storedAuthState &&
|
|
1481
|
+
hasDurableAuthState(existingAuthWorkflow, storedAuthState)
|
|
1482
|
+
? plans
|
|
1483
|
+
.map((plan) => state.workflows[plan.workflowKey]?.authCompletion)
|
|
1484
|
+
.find((completion) =>
|
|
1485
|
+
authCompletionMatches(completion, {
|
|
1486
|
+
toolName: authPlan.toolName,
|
|
1487
|
+
buildPlanHash,
|
|
1488
|
+
workflowHash: existingAuthWorkflowHash as string,
|
|
1489
|
+
}),
|
|
1490
|
+
)
|
|
1491
|
+
: undefined;
|
|
1492
|
+
if (reusableCompletion) {
|
|
1493
|
+
p.log.info(
|
|
1494
|
+
`Reusing verified auth tool "${authPlan.toolName}"; auth artifacts are unchanged.`,
|
|
1495
|
+
);
|
|
1496
|
+
} else {
|
|
1497
|
+
const redactedSession = SessionSchema.parse(
|
|
1498
|
+
JSON.parse(readFileSync(compileSessionPath, 'utf8')),
|
|
1499
|
+
);
|
|
1500
|
+
// A resumed generate step may still have valid credentials from an earlier
|
|
1501
|
+
// successful teach. Prefer those over values reconstructed from the old
|
|
1502
|
+
// recording so a stale recording cannot overwrite the credential store.
|
|
1503
|
+
if (!teachCredentials) {
|
|
1504
|
+
const view = await loadSiteCredentials(site).catch(() => null);
|
|
1505
|
+
const storedValues = selectCompleteAuthCredentials(
|
|
1506
|
+
view?.values ?? {},
|
|
1420
1507
|
authPlan.credentialNames,
|
|
1421
1508
|
);
|
|
1422
|
-
if (
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
kind: 'username',
|
|
1429
|
-
description: 'Login identifier',
|
|
1430
|
-
});
|
|
1509
|
+
if (storedValues) {
|
|
1510
|
+
teachCredentials = { site, values: storedValues };
|
|
1511
|
+
if (authPlan.credentialNames.length > 0) {
|
|
1512
|
+
p.log.info(
|
|
1513
|
+
`Using stored credentials for "${site}": ${authPlan.credentialNames.join(', ')}`,
|
|
1514
|
+
);
|
|
1431
1515
|
}
|
|
1432
|
-
applyCredentialPlaceholders(redactedSession, derived.replacements);
|
|
1433
|
-
teachCredentials = { site, values: derived.values };
|
|
1434
|
-
p.log.success(
|
|
1435
|
-
`Derived ${Object.keys(derived.values).length} credential(s) for passwordless login: ${Object.keys(derived.values).join(', ')}`,
|
|
1436
|
-
);
|
|
1437
1516
|
}
|
|
1438
|
-
} catch (err) {
|
|
1439
|
-
p.log.warn(
|
|
1440
|
-
`Credential derivation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1441
|
-
);
|
|
1442
1517
|
}
|
|
1443
|
-
}
|
|
1444
1518
|
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
if (!teachCredentials) {
|
|
1453
|
-
const view = await loadSiteCredentials(site).catch(() => null);
|
|
1454
|
-
if (view && Object.keys(view.values).length > 0) {
|
|
1455
|
-
teachCredentials = { site, values: view.values };
|
|
1456
|
-
p.log.info(
|
|
1457
|
-
`Using stored credentials for "${site}": ${Object.keys(view.values).join(', ')}`,
|
|
1458
|
-
);
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
if (!teachCredentials && !opts.noInteractive && authPlan.credentialNames.length > 0) {
|
|
1462
|
-
let detectedUsername: string | undefined;
|
|
1463
|
-
if (sessionPath && existsSync(sessionPath)) {
|
|
1519
|
+
// Passwordless / OTP-only logins (e.g. email + emailed code, magic link)
|
|
1520
|
+
// carry no password for the username+password extractor to pair, so
|
|
1521
|
+
// teachCredentials can be empty. If no complete stored set exists, derive
|
|
1522
|
+
// the planner-declared credential values from the recorded login request(s),
|
|
1523
|
+
// persist them, and back-fill ${credential.X} into the redacted session the
|
|
1524
|
+
// agent reads.
|
|
1525
|
+
if (!teachCredentials && sessionPath && existsSync(sessionPath)) {
|
|
1464
1526
|
try {
|
|
1465
|
-
|
|
1466
|
-
|
|
1527
|
+
const rawForCreds = SessionSchema.parse(JSON.parse(readFileSync(sessionPath, 'utf8')));
|
|
1528
|
+
const derived = deriveLoginCredentials(
|
|
1529
|
+
rawForCreds,
|
|
1530
|
+
authPlan.credentialRequestSeqs,
|
|
1531
|
+
authPlan.credentialNames,
|
|
1532
|
+
);
|
|
1533
|
+
if (Object.keys(derived.values).length > 0) {
|
|
1534
|
+
const backend = await getCredentialBackend();
|
|
1535
|
+
for (const [name, value] of Object.entries(derived.values)) {
|
|
1536
|
+
await backend.setSecret(site, name, value);
|
|
1537
|
+
upsertManifestEntry(site, {
|
|
1538
|
+
name,
|
|
1539
|
+
kind: 'username',
|
|
1540
|
+
description: 'Login identifier',
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
applyCredentialPlaceholders(redactedSession, derived.replacements);
|
|
1544
|
+
teachCredentials = { site, values: derived.values };
|
|
1545
|
+
p.log.success(
|
|
1546
|
+
`Derived ${Object.keys(derived.values).length} credential(s) for passwordless login: ${Object.keys(derived.values).join(', ')}`,
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
} catch (err) {
|
|
1550
|
+
p.log.warn(
|
|
1551
|
+
`Credential derivation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1467
1552
|
);
|
|
1468
|
-
} catch {
|
|
1469
|
-
/* best-effort pre-fill only */
|
|
1470
1553
|
}
|
|
1471
1554
|
}
|
|
1472
|
-
teachCredentials = await promptForCredentials({
|
|
1473
|
-
site,
|
|
1474
|
-
names: authPlan.credentialNames,
|
|
1475
|
-
detectedUsername,
|
|
1476
|
-
});
|
|
1477
|
-
}
|
|
1478
1555
|
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1556
|
+
// The recording cannot always yield credentials because the capture layer
|
|
1557
|
+
// masks sensitive fields. Reuse a complete stored set or prompt for the
|
|
1558
|
+
// credential names identified during planning.
|
|
1559
|
+
if (!teachCredentials && !opts.noInteractive && authPlan.credentialNames.length > 0) {
|
|
1560
|
+
let detectedUsername: string | undefined;
|
|
1561
|
+
if (sessionPath && existsSync(sessionPath)) {
|
|
1562
|
+
try {
|
|
1563
|
+
detectedUsername = detectRecordedUsername(
|
|
1564
|
+
SessionSchema.parse(JSON.parse(readFileSync(sessionPath, 'utf8'))),
|
|
1565
|
+
);
|
|
1566
|
+
} catch {
|
|
1567
|
+
/* best-effort pre-fill only */
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
teachCredentials = await promptForCredentials({
|
|
1571
|
+
site,
|
|
1572
|
+
names: authPlan.credentialNames,
|
|
1573
|
+
detectedUsername,
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
if (!teachCredentials) {
|
|
1578
|
+
const hint = opts.noInteractive
|
|
1579
|
+
? ` Set them with \`imprint credential set ${site} <name>\` (${authPlan.credentialNames.join(', ') || 'the login credentials'}), then resume with \`imprint teach ${site} --from-step generate\`.`
|
|
1580
|
+
: '';
|
|
1581
|
+
throw new Error(
|
|
1582
|
+
`Auth tool "${authPlan.toolName}" was planned but no credentials are available.${hint} Data-tool compilation was not started.`,
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1487
1586
|
spinner.start(`Compiling auth tool: ${authPlan.toolName}`);
|
|
1488
1587
|
try {
|
|
1489
1588
|
muteLog();
|
|
@@ -1493,23 +1592,15 @@ export async function teach(opts: TeachOptions): Promise<TeachResult> {
|
|
|
1493
1592
|
sessionPath: compileSessionPath,
|
|
1494
1593
|
authToolPlan: authPlan,
|
|
1495
1594
|
teachCredentials,
|
|
1496
|
-
llmConfig:
|
|
1497
|
-
// Browser-minted logins compile on the playbook rung — each live test
|
|
1498
|
-
// launches a real browser (navigate + anti-bot settle + form submit),
|
|
1499
|
-
// which is far slower than an API replay. Give the agent room to write
|
|
1500
|
-
// and iterate a playbook, not just a workflow.json.
|
|
1595
|
+
llmConfig: authCompileLlmConfig(compileProviderName, compileModel),
|
|
1501
1596
|
maxDurationMs: 20 * 60 * 1000,
|
|
1502
1597
|
onProgress: (prog) => spinner.message(formatAuthProgress(prog)),
|
|
1503
|
-
//
|
|
1504
|
-
// the OTP/push challenge, then asks the user — here — for the live second
|
|
1505
|
-
// factor. Stop the spinner + unmute logs around the prompt so it renders
|
|
1506
|
-
// cleanly, then restore. Omitted in --no-interactive (the agent falls
|
|
1507
|
-
// back to an unattended placeholder attempt).
|
|
1598
|
+
// Render agent-requested user input outside the spinner.
|
|
1508
1599
|
onPrompt: opts.noInteractive
|
|
1509
1600
|
? undefined
|
|
1510
1601
|
: async (message, options) => {
|
|
1511
1602
|
unmuteLog();
|
|
1512
|
-
spinner.stop('
|
|
1603
|
+
spinner.stop('Authentication requires your input.');
|
|
1513
1604
|
let answer = '';
|
|
1514
1605
|
if (options && options.length > 0) {
|
|
1515
1606
|
const sel = await p.select({
|
|
@@ -1522,16 +1613,15 @@ export async function teach(opts: TeachOptions): Promise<TeachResult> {
|
|
|
1522
1613
|
answer = p.isCancel(txt) ? '' : String(txt ?? '');
|
|
1523
1614
|
}
|
|
1524
1615
|
muteLog();
|
|
1525
|
-
spinner.start('
|
|
1616
|
+
spinner.start('Continuing authentication verification');
|
|
1526
1617
|
return answer;
|
|
1527
1618
|
},
|
|
1528
|
-
//
|
|
1529
|
-
// user. Bounded to 10 min; overridable via IMPRINT_AUTH_COOLOFF_MS.
|
|
1619
|
+
// Honor an agent-requested delay without firing a live action.
|
|
1530
1620
|
onCooldown: async (minutes, reason) => {
|
|
1531
1621
|
const envMs = Number(process.env.IMPRINT_AUTH_COOLOFF_MS);
|
|
1532
1622
|
const ms = Number.isFinite(envMs)
|
|
1533
1623
|
? Math.max(0, envMs)
|
|
1534
|
-
: Math.
|
|
1624
|
+
: Math.max(0, minutes) * 60_000;
|
|
1535
1625
|
unmuteLog();
|
|
1536
1626
|
p.log.info(
|
|
1537
1627
|
`Cooling off ~${Math.round(ms / 60000)} min before retrying the login${reason ? ` (${reason})` : ''} — no login fires during the wait.`,
|
|
@@ -1543,36 +1633,43 @@ export async function teach(opts: TeachOptions): Promise<TeachResult> {
|
|
|
1543
1633
|
});
|
|
1544
1634
|
unmuteLog();
|
|
1545
1635
|
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1636
|
+
assertSuccessfulAuthCompile(authResult);
|
|
1637
|
+
emit({
|
|
1638
|
+
workflowPath: authResult.workflowPath,
|
|
1639
|
+
outDir: pathDirname(authResult.workflowPath),
|
|
1640
|
+
force: true,
|
|
1641
|
+
});
|
|
1642
|
+
const emittedWorkflow = WorkflowSchema.parse(
|
|
1643
|
+
JSON.parse(readFileSync(authResult.workflowPath, 'utf8')),
|
|
1644
|
+
);
|
|
1645
|
+
const storedState = await loadSiteCredentials(site).catch(() => null);
|
|
1646
|
+
const sessionStored = Boolean(
|
|
1647
|
+
storedState && hasDurableAuthState(emittedWorkflow, storedState),
|
|
1648
|
+
);
|
|
1649
|
+
const completion = {
|
|
1650
|
+
toolName: authPlan.toolName,
|
|
1651
|
+
buildPlanHash,
|
|
1652
|
+
workflowHash: authWorkflowHash(emittedWorkflow),
|
|
1653
|
+
completedAt: new Date().toISOString(),
|
|
1654
|
+
};
|
|
1655
|
+
if (sessionStored) {
|
|
1656
|
+
for (const plan of plans) {
|
|
1657
|
+
const ws = state.workflows[plan.workflowKey];
|
|
1658
|
+
if (ws) ws.authCompletion = completion;
|
|
1564
1659
|
}
|
|
1565
|
-
|
|
1566
|
-
sessionStored
|
|
1567
|
-
? `Auth tool compiled + session stored (${authResult.turns} turns, ${Math.round(authResult.durationMs / 1000)}s) — data tools will reuse it.`
|
|
1568
|
-
: `Auth tool compiled (${authResult.turns} turns) — no live session stored; data tools will be unverified until you run \`imprint login ${site}\`.`,
|
|
1569
|
-
);
|
|
1660
|
+
saveTeachState(site, state);
|
|
1570
1661
|
}
|
|
1662
|
+
spinner.stop(
|
|
1663
|
+
sessionStored
|
|
1664
|
+
? `Auth tool compiled + session stored (${authResult.turns} turns, ${Math.round(authResult.durationMs / 1000)}s) — data tools will reuse it.`
|
|
1665
|
+
: `Auth tool compiled (${authResult.turns} turns) — no live session stored; data tools will be unverified until you run \`imprint login ${site}\`.`,
|
|
1666
|
+
);
|
|
1571
1667
|
} catch (err) {
|
|
1572
1668
|
unmuteLog();
|
|
1573
1669
|
spinner.stop('Auth tool compilation failed.');
|
|
1574
|
-
|
|
1575
|
-
`Auth tool
|
|
1670
|
+
throw new Error(
|
|
1671
|
+
`Auth stage failed; data-tool compilation was not started: ${err instanceof Error ? err.message : String(err)}`,
|
|
1672
|
+
{ cause: err },
|
|
1576
1673
|
);
|
|
1577
1674
|
}
|
|
1578
1675
|
}
|
|
@@ -1602,6 +1699,7 @@ export async function teach(opts: TeachOptions): Promise<TeachResult> {
|
|
|
1602
1699
|
allTools: opts.allTools,
|
|
1603
1700
|
buildPlanPath: buildPlanPath || undefined,
|
|
1604
1701
|
sharedModules: sharedModulesManifest.length > 0 ? sharedModulesManifest : undefined,
|
|
1702
|
+
resumeExistingArtifacts: Boolean(opts.fromStep),
|
|
1605
1703
|
});
|
|
1606
1704
|
} finally {
|
|
1607
1705
|
unmuteLog();
|
|
@@ -1695,6 +1793,21 @@ export async function teach(opts: TeachOptions): Promise<TeachResult> {
|
|
|
1695
1793
|
}
|
|
1696
1794
|
}
|
|
1697
1795
|
|
|
1796
|
+
// Capability reductions are agent decisions, not silent compile losses.
|
|
1797
|
+
// Keep the reasoning in workflow.json and repeat it in the final teach
|
|
1798
|
+
// result so operators can distinguish an intentionally smaller honest tool
|
|
1799
|
+
// from a compiler that simply forgot an input.
|
|
1800
|
+
for (const result of results) {
|
|
1801
|
+
for (const limitation of result.workflow.limitations ?? []) {
|
|
1802
|
+
const omitted = limitation.omittedParameters?.length
|
|
1803
|
+
? `; omitted inputs: ${limitation.omittedParameters.join(', ')}`
|
|
1804
|
+
: '';
|
|
1805
|
+
p.log.warn(
|
|
1806
|
+
`tool "${result.workflow.toolName}" capability limitation — ${limitation.feature}: ${limitation.reason}${omitted}`,
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1698
1811
|
// `--to-step emit/generate/compile-playbook` runs the per-tool compile but stops
|
|
1699
1812
|
// before register (platform integration) — inWindow('register') already skipped
|
|
1700
1813
|
// it above. Finish with the phase-window summary instead of the normal "Done!"
|
|
@@ -1839,6 +1952,9 @@ async function compileCandidatePlans(opts: {
|
|
|
1839
1952
|
buildPlanPath?: string;
|
|
1840
1953
|
/** Shared-module build manifest (verified flags) for this site. */
|
|
1841
1954
|
sharedModules?: SharedModuleManifestEntry[];
|
|
1955
|
+
/** The user explicitly resumed a prior teach phase; generated artifacts that
|
|
1956
|
+
* already exist are revision inputs, not disposable scratch output. */
|
|
1957
|
+
resumeExistingArtifacts?: boolean;
|
|
1842
1958
|
}): Promise<TeachToolResult[]> {
|
|
1843
1959
|
const concurrency = opts.plans.length === 1 ? 1 : COMPILE_CONCURRENCY;
|
|
1844
1960
|
const mp = opts.plans.length > 1 ? new MultiProgress() : null;
|
|
@@ -2058,6 +2174,32 @@ function renderCompileSummary(summary: CompileOutcomeSummary): string[] {
|
|
|
2058
2174
|
return lines;
|
|
2059
2175
|
}
|
|
2060
2176
|
|
|
2177
|
+
type SuccessfulAuthCompile = CompileAgentResult & { success: true; workflowPath: string };
|
|
2178
|
+
|
|
2179
|
+
export function assertSuccessfulAuthCompile(
|
|
2180
|
+
result: CompileAgentResult,
|
|
2181
|
+
): asserts result is SuccessfulAuthCompile {
|
|
2182
|
+
if (!result.success) {
|
|
2183
|
+
throw new Error(`Auth agent did not complete successfully: ${result.message}`);
|
|
2184
|
+
}
|
|
2185
|
+
if (!result.workflowPath) {
|
|
2186
|
+
throw new Error('Auth agent reported success without producing workflow.json.');
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
export function selectCompleteAuthCredentials(
|
|
2191
|
+
values: Record<string, string>,
|
|
2192
|
+
requiredNames: string[],
|
|
2193
|
+
): Record<string, string> | null {
|
|
2194
|
+
const selected: Record<string, string> = {};
|
|
2195
|
+
for (const name of requiredNames) {
|
|
2196
|
+
const value = values[name];
|
|
2197
|
+
if (typeof value !== 'string' || value.length === 0) return null;
|
|
2198
|
+
selected[name] = value;
|
|
2199
|
+
}
|
|
2200
|
+
return selected;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2061
2203
|
async function compileSelectedCandidate(opts: {
|
|
2062
2204
|
plan: CandidateCompilePlan;
|
|
2063
2205
|
site: string;
|
|
@@ -2074,12 +2216,15 @@ async function compileSelectedCandidate(opts: {
|
|
|
2074
2216
|
teachCredentials?: { site: string; values: Record<string, string> };
|
|
2075
2217
|
buildPlanPath?: string;
|
|
2076
2218
|
sharedModules?: SharedModuleManifestEntry[];
|
|
2219
|
+
resumeExistingArtifacts?: boolean;
|
|
2077
2220
|
}): Promise<TeachToolResult> {
|
|
2078
2221
|
const { plan, site, state } = opts;
|
|
2079
2222
|
const startIdx = STEPS.indexOf(plan.startFrom);
|
|
2080
2223
|
const toolName = plan.candidate?.toolName ?? plan.workflowKey;
|
|
2081
2224
|
const workflowDir = localToolDir(site, toolName);
|
|
2082
2225
|
mkdirSync(workflowDir, { recursive: true });
|
|
2226
|
+
const revisionMode =
|
|
2227
|
+
Boolean(opts.resumeExistingArtifacts) && existsSync(pathJoin(workflowDir, 'workflow.json'));
|
|
2083
2228
|
|
|
2084
2229
|
// The per-tool compile (generate → compile-playbook → emit) is ATOMIC by design:
|
|
2085
2230
|
// each phase gates on startIdx ONLY (not the window's stopIdx), so once started it
|
|
@@ -2098,19 +2243,20 @@ async function compileSelectedCandidate(opts: {
|
|
|
2098
2243
|
// mapping, request construction, response parsing, shared-module imports),
|
|
2099
2244
|
// then run a single compile that follows it. Best-effort — a timeout or
|
|
2100
2245
|
// error yields no plan and the compile proceeds exactly as before.
|
|
2101
|
-
const toolPlan =
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2246
|
+
const toolPlan =
|
|
2247
|
+
plan.candidate && !revisionMode
|
|
2248
|
+
? await planToolCompile({
|
|
2249
|
+
site,
|
|
2250
|
+
toolName,
|
|
2251
|
+
candidate: plan.candidate,
|
|
2252
|
+
sharedContext: plan.sharedContext,
|
|
2253
|
+
sessionPath: opts.sessionPath,
|
|
2254
|
+
buildPlanPath: opts.buildPlanPath,
|
|
2255
|
+
sharedModules: opts.sharedModules,
|
|
2256
|
+
providerName: opts.providerName,
|
|
2257
|
+
model: opts.compileModel,
|
|
2258
|
+
})
|
|
2259
|
+
: undefined;
|
|
2114
2260
|
|
|
2115
2261
|
const result = await generate({
|
|
2116
2262
|
sessionPath: opts.sessionPath,
|
|
@@ -2127,6 +2273,7 @@ async function compileSelectedCandidate(opts: {
|
|
|
2127
2273
|
buildPlanPath: opts.buildPlanPath,
|
|
2128
2274
|
sharedModules: opts.sharedModules,
|
|
2129
2275
|
toolPlan,
|
|
2276
|
+
revisionMode,
|
|
2130
2277
|
});
|
|
2131
2278
|
|
|
2132
2279
|
assertCandidateToolName('Compiled workflow', result.workflow.toolName, plan.candidate);
|
|
@@ -2148,6 +2295,16 @@ async function compileSelectedCandidate(opts: {
|
|
|
2148
2295
|
if (!genResult) {
|
|
2149
2296
|
throw new Error(`generate step did not produce a workflow for "${toolName}".`);
|
|
2150
2297
|
}
|
|
2298
|
+
// Semantic verification may revise the workflow after generate() returns
|
|
2299
|
+
// (for example, omitting an unsupported secondary parameter and recording a
|
|
2300
|
+
// limitation). Reload that final artifact before compiling any fallback so
|
|
2301
|
+
// later agents cannot work from the pre-verification object.
|
|
2302
|
+
genResult.workflow = loadJsonFile(
|
|
2303
|
+
genResult.workflowPath,
|
|
2304
|
+
WorkflowSchema,
|
|
2305
|
+
{ notFound: `workflow.json not found at ${genResult.workflowPath}` },
|
|
2306
|
+
'workflow.json',
|
|
2307
|
+
);
|
|
2151
2308
|
|
|
2152
2309
|
// ── Step 2: compile-playbook (after generate — runtime artifact, not needed for dual-pass) ──
|
|
2153
2310
|
let pbResult: { playbook: Playbook; playbookPath: string };
|
|
@@ -2157,6 +2314,7 @@ async function compileSelectedCandidate(opts: {
|
|
|
2157
2314
|
outPath: pathJoin(workflowDir, 'playbook.yaml'),
|
|
2158
2315
|
llmConfig: { provider: opts.providerName },
|
|
2159
2316
|
candidate: plan.candidate,
|
|
2317
|
+
workflow: genResult.workflow,
|
|
2160
2318
|
sharedContext: plan.sharedContext,
|
|
2161
2319
|
preTriagedSession: opts.sharedTriageResult,
|
|
2162
2320
|
});
|
|
@@ -2187,6 +2345,11 @@ async function compileSelectedCandidate(opts: {
|
|
|
2187
2345
|
|
|
2188
2346
|
exportSiteManifest(site, workflowDir, genResult.workflow, pbResult.playbook);
|
|
2189
2347
|
|
|
2348
|
+
// Emission and post-verification annotations can change workflow.json's
|
|
2349
|
+
// metadata hash without changing the already-proven transport behavior.
|
|
2350
|
+
// Reuse the existing no-network rebind so a bounded resume does not trigger
|
|
2351
|
+
// an unnecessary comprehensive probe on its first real call.
|
|
2352
|
+
rebindExistingBackendsCacheToWorkflow(workflowDir);
|
|
2190
2353
|
await writeQuickBackendsCache(workflowDir, genResult.workflow);
|
|
2191
2354
|
|
|
2192
2355
|
return {
|
|
@@ -3022,21 +3185,14 @@ function formatCompileProgress(progress: CompileAgentProgress): string {
|
|
|
3022
3185
|
return `Compiling • ${activity} (${formatElapsed(progress.elapsedMs)}${retry})`;
|
|
3023
3186
|
}
|
|
3024
3187
|
|
|
3025
|
-
/** Build the auth-compile spinner line.
|
|
3026
|
-
*
|
|
3027
|
-
* recent live verification FAILED, surface the reason (phase + error + HTTP
|
|
3028
|
-
* status) and which live-login attempt of the budget it was — so the user sees
|
|
3029
|
-
* what happened and why it's taking longer, not a silently-resetting counter. */
|
|
3188
|
+
/** Build the auth-compile spinner line. Turns stay monotonic across resumed
|
|
3189
|
+
* segments; live failures surface the requested action and observed status. */
|
|
3030
3190
|
export function formatAuthProgress(progress: CompileAgentProgress): string {
|
|
3031
3191
|
const base = `Auth compile: turn ${progress.turn}`;
|
|
3032
3192
|
const lv = progress.lastVerification;
|
|
3033
3193
|
if (lv && !lv.ok) {
|
|
3034
3194
|
const status = typeof lv.status === 'number' ? ` HTTP ${lv.status}` : '';
|
|
3035
|
-
|
|
3036
|
-
typeof progress.attempt === 'number' && typeof progress.maxAttempts === 'number'
|
|
3037
|
-
? `; attempt ${progress.attempt}/${progress.maxAttempts}`
|
|
3038
|
-
: '';
|
|
3039
|
-
return `${base} — verify ${lv.phase} FAILED (${lv.error ?? 'error'}${status})${attempt} — agent retrying`;
|
|
3195
|
+
return `${base} — verify ${lv.action} FAILED (${lv.error ?? 'error'}${status}) — agent revising`;
|
|
3040
3196
|
}
|
|
3041
3197
|
return base;
|
|
3042
3198
|
}
|