imprint-mcp 0.7.0 → 0.7.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 +66 -2
- package/package.json +1 -1
- package/prompts/auth-compile-agent.md +173 -22
- package/prompts/compile-agent.md +45 -8
- package/prompts/component-recovery.md +40 -0
- package/prompts/master-teach-decision.md +28 -3
- package/prompts/master-teach-focused-planner.md +18 -5
- package/src/cli.ts +26 -18
- package/src/imprint/agent-tool.ts +15 -0
- package/src/imprint/agent.ts +2 -11
- package/src/imprint/audit.ts +35 -13
- package/src/imprint/auth-browser-discovery.ts +276 -0
- package/src/imprint/auth-compile-agent.ts +513 -224
- package/src/imprint/auth-compile-tools.ts +213 -25
- package/src/imprint/auth-discovery-evidence.ts +81 -0
- package/src/imprint/auth-plan.ts +28 -0
- package/src/imprint/auth-verifier.ts +234 -20
- package/src/imprint/authentication-interaction.ts +137 -0
- package/src/imprint/backend-ladder.ts +271 -43
- package/src/imprint/build-plan.ts +2 -22
- package/src/imprint/cdp-browser-fetch.ts +242 -60
- package/src/imprint/chromium.ts +93 -29
- package/src/imprint/claude-cli-compile.ts +2 -0
- package/src/imprint/codex-cli-compile.ts +99 -46
- package/src/imprint/compile-agent-types.ts +9 -5
- package/src/imprint/compile-agent.ts +33 -0
- package/src/imprint/compile-tool-executor.ts +783 -0
- package/src/imprint/compile-tools.ts +42 -9
- package/src/imprint/compile-verification.ts +2 -0
- package/src/imprint/compile.ts +3 -0
- package/src/imprint/compiler-log.ts +16 -0
- package/src/imprint/component-recovery.ts +120 -0
- package/src/imprint/cookie-jar.ts +117 -7
- package/src/imprint/credential-extract.ts +9 -2
- package/src/imprint/credential-lock.ts +147 -0
- package/src/imprint/credential-observation.ts +261 -0
- package/src/imprint/credential-store.ts +284 -44
- package/src/imprint/live-component-recovery.ts +73 -0
- package/src/imprint/llm.ts +4 -3
- package/src/imprint/log.ts +16 -1
- package/src/imprint/login.ts +9 -0
- package/src/imprint/master-teach-agent-contracts.ts +40 -3
- package/src/imprint/master-teach-agents.ts +229 -14
- package/src/imprint/master-teach-checks.ts +8 -1
- package/src/imprint/master-teach-controller.ts +681 -138
- package/src/imprint/master-teach-prompt-projections.ts +2 -0
- package/src/imprint/mcp-cdp-sessions.ts +7 -2
- package/src/imprint/mcp-compile-server.ts +62 -850
- package/src/imprint/mcp-server.ts +667 -91
- package/src/imprint/mcp-session-recovery.ts +167 -0
- package/src/imprint/native-teach-agents.ts +232 -108
- package/src/imprint/runtime.ts +416 -47
- package/src/imprint/stealth-chromium.ts +67 -7
- package/src/imprint/teach-browser-resources.ts +88 -0
- package/src/imprint/teach-stage-progress.ts +38 -0
- package/src/imprint/terminal-user-prompt.ts +42 -0
- package/src/imprint/types.ts +51 -18
- package/src/imprint/user-interaction.ts +836 -0
|
@@ -4,9 +4,20 @@
|
|
|
4
4
|
* the requested actions.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import {
|
|
8
|
+
AUTH_DISCOVERY_POOL_KEY,
|
|
9
|
+
type AuthBrowserAction,
|
|
10
|
+
AuthBrowserActionSchema,
|
|
11
|
+
type AuthBrowserObservation,
|
|
12
|
+
} from './auth-browser-discovery.ts';
|
|
7
13
|
import { runWorkflowWithLadder } from './backend-ladder.ts';
|
|
8
|
-
import
|
|
14
|
+
import {
|
|
15
|
+
type CdpBrowserFetch,
|
|
16
|
+
type CdpPageSnapshot,
|
|
17
|
+
createCdpBrowserFetch,
|
|
18
|
+
} from './cdp-browser-fetch.ts';
|
|
9
19
|
import { abortSignalError, withAbortSignal } from './concurrency.ts';
|
|
20
|
+
import { redactFreeformText } from './freeform-redact.ts';
|
|
10
21
|
import { createLog } from './log.ts';
|
|
11
22
|
import type { CredentialStore } from './runtime.ts';
|
|
12
23
|
|
|
@@ -30,6 +41,9 @@ export interface AuthActionResult {
|
|
|
30
41
|
status?: number;
|
|
31
42
|
responseBodyPreview?: string;
|
|
32
43
|
durationMs: number;
|
|
44
|
+
requestStageFacts?: Extract<import('./types.ts').ToolResult, { ok: false }>['requestStageFacts'];
|
|
45
|
+
authExecution?: Extract<import('./types.ts').ToolResult, { ok: false }>['authExecution'];
|
|
46
|
+
responseObservations?: Awaited<ReturnType<LadderRunner>>['responseObservations'];
|
|
33
47
|
}
|
|
34
48
|
|
|
35
49
|
interface AuthPageInspection {
|
|
@@ -43,38 +57,107 @@ interface AuthPageInspection {
|
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
export class AuthVerifier {
|
|
46
|
-
private
|
|
60
|
+
private cdpPool = new Map<string, CdpBrowserFetch>();
|
|
61
|
+
private readonly pendingPools = new Set<Map<string, CdpBrowserFetch>>();
|
|
62
|
+
private readonly retiredPools = new Set<Map<string, CdpBrowserFetch>>();
|
|
63
|
+
private readonly closedBrowsers = new WeakSet<CdpBrowserFetch>();
|
|
64
|
+
private poolRelease: Promise<void> = Promise.resolve();
|
|
47
65
|
private continuation: Record<string, unknown> | undefined;
|
|
48
66
|
private readonly sensitiveValues = new Set<string>();
|
|
67
|
+
private readonly abort = new AbortController();
|
|
68
|
+
private lastAction?: Pick<AuthActionResult, 'action' | 'ok' | 'error' | 'nextAction' | 'status'>;
|
|
69
|
+
|
|
70
|
+
/** Facts for a recovery turn; never expose carried state or browser values. */
|
|
71
|
+
get state(): {
|
|
72
|
+
browserActive: boolean;
|
|
73
|
+
hasContinuation: boolean;
|
|
74
|
+
lastAction?: Pick<AuthActionResult, 'action' | 'ok' | 'error' | 'nextAction' | 'status'>;
|
|
75
|
+
} {
|
|
76
|
+
return {
|
|
77
|
+
browserActive: this.cdpPool.size > 0,
|
|
78
|
+
hasContinuation: this.continuation !== undefined,
|
|
79
|
+
lastAction: this.lastAction,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
49
82
|
|
|
50
83
|
constructor(
|
|
51
84
|
private readonly workflowPath: string,
|
|
52
|
-
private
|
|
85
|
+
private credentials: CredentialStore,
|
|
86
|
+
private readonly discoveryUrl?: string,
|
|
53
87
|
) {
|
|
54
88
|
this.rememberSensitiveValues(credentials.values);
|
|
55
89
|
}
|
|
56
90
|
|
|
91
|
+
/** Refresh future request inputs without discarding the live page or challenge. */
|
|
92
|
+
updateCredentials(credentials: CredentialStore): void {
|
|
93
|
+
if (this.abort.signal.aborted) throw abortSignalError(this.abort.signal);
|
|
94
|
+
this.credentials = credentials;
|
|
95
|
+
this.rememberSensitiveValues(credentials.values);
|
|
96
|
+
}
|
|
97
|
+
|
|
57
98
|
async runAction(
|
|
58
99
|
action: string,
|
|
59
100
|
parameters: Record<string, string | number | boolean> = {},
|
|
60
101
|
options: { freshSession?: boolean; cleanSession?: boolean; signal?: AbortSignal } = {},
|
|
61
102
|
): Promise<AuthActionResult> {
|
|
62
|
-
|
|
103
|
+
const signal = options.signal
|
|
104
|
+
? AbortSignal.any([options.signal, this.abort.signal])
|
|
105
|
+
: this.abort.signal;
|
|
106
|
+
if (signal.aborted) throw abortSignalError(signal);
|
|
107
|
+
await withAbortSignal(() => this.poolRelease, signal);
|
|
63
108
|
if (options.freshSession || options.cleanSession) await this.reset();
|
|
64
109
|
this.rememberSensitiveValues(parameters);
|
|
65
110
|
const previousContinuation = this.continuation;
|
|
111
|
+
const pool = this.cdpPool;
|
|
66
112
|
const startedAt = Date.now();
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
113
|
+
// A cancelled ladder can still be unwinding an uninterruptible CDP await.
|
|
114
|
+
// Detach its map before returning to the caller: no later action may borrow
|
|
115
|
+
// a browser that the older ladder can still close or evict.
|
|
116
|
+
const abort = (): void => {
|
|
117
|
+
if (this.cdpPool === pool) {
|
|
118
|
+
this.lastAction = { action, ok: false, error: 'CANCELLED' };
|
|
119
|
+
}
|
|
120
|
+
void this.retirePool(pool);
|
|
121
|
+
};
|
|
122
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
123
|
+
if (signal.aborted) {
|
|
124
|
+
abort();
|
|
125
|
+
signal.removeEventListener('abort', abort);
|
|
126
|
+
throw abortSignalError(signal);
|
|
127
|
+
}
|
|
128
|
+
this.pendingPools.add(pool);
|
|
129
|
+
let ladder: Awaited<ReturnType<LadderRunner>>;
|
|
130
|
+
try {
|
|
131
|
+
ladder = await withAbortSignal(
|
|
132
|
+
() =>
|
|
133
|
+
Promise.resolve()
|
|
134
|
+
.then(() => {
|
|
135
|
+
if (signal.aborted) throw abortSignalError(signal);
|
|
136
|
+
return ladderRunner({
|
|
137
|
+
workflowPath: this.workflowPath,
|
|
138
|
+
params: { ...parameters, action },
|
|
139
|
+
credentials: options.cleanSession
|
|
140
|
+
? { ...this.credentials, cookies: [], storage: [] }
|
|
141
|
+
: this.credentials,
|
|
142
|
+
cdpPool: pool,
|
|
143
|
+
forceBackend: 'cdp-replay',
|
|
144
|
+
initialState: previousContinuation,
|
|
145
|
+
signal,
|
|
146
|
+
});
|
|
147
|
+
})
|
|
148
|
+
.finally(async () => {
|
|
149
|
+
if (signal.aborted || pool !== this.cdpPool) await this.closePool(pool);
|
|
150
|
+
this.pendingPools.delete(pool);
|
|
151
|
+
this.retiredPools.delete(pool);
|
|
152
|
+
}),
|
|
153
|
+
signal,
|
|
154
|
+
);
|
|
155
|
+
} finally {
|
|
156
|
+
signal.removeEventListener('abort', abort);
|
|
157
|
+
}
|
|
158
|
+
if (signal.aborted) throw abortSignalError(signal);
|
|
159
|
+
if (pool !== this.cdpPool)
|
|
160
|
+
throw new DOMException('Authentication action context was replaced', 'AbortError');
|
|
78
161
|
const durationMs = Date.now() - startedAt;
|
|
79
162
|
const result = ladder.result;
|
|
80
163
|
|
|
@@ -99,6 +182,37 @@ export class AuthVerifier {
|
|
|
99
182
|
? undefined
|
|
100
183
|
: this.sanitizeOptional(result.responseBodyPreview),
|
|
101
184
|
durationMs,
|
|
185
|
+
requestStageFacts: result.ok ? undefined : result.requestStageFacts,
|
|
186
|
+
authExecution:
|
|
187
|
+
result.ok || !result.authExecution
|
|
188
|
+
? undefined
|
|
189
|
+
: {
|
|
190
|
+
...result.authExecution,
|
|
191
|
+
...(result.authExecution.lastFailure
|
|
192
|
+
? {
|
|
193
|
+
lastFailure: {
|
|
194
|
+
error: this.sanitize(result.authExecution.lastFailure.error),
|
|
195
|
+
message: this.sanitize(result.authExecution.lastFailure.message),
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
: {}),
|
|
199
|
+
},
|
|
200
|
+
responseObservations: ladder.responseObservations?.map(
|
|
201
|
+
({ redactedBodyPreview: _preview, ...facts }) => ({
|
|
202
|
+
...facts,
|
|
203
|
+
...(facts.contentType ? { contentType: this.sanitize(facts.contentType) } : {}),
|
|
204
|
+
...(facts.topLevelKeys
|
|
205
|
+
? { topLevelKeys: facts.topLevelKeys.map((key) => this.sanitize(key)) }
|
|
206
|
+
: {}),
|
|
207
|
+
}),
|
|
208
|
+
),
|
|
209
|
+
};
|
|
210
|
+
this.lastAction = {
|
|
211
|
+
action,
|
|
212
|
+
ok: actionResult.ok,
|
|
213
|
+
error: actionResult.error,
|
|
214
|
+
nextAction: actionResult.nextAction,
|
|
215
|
+
status: actionResult.status,
|
|
102
216
|
};
|
|
103
217
|
log(
|
|
104
218
|
`action=${JSON.stringify(action)} backend=${ladder.usedBackend} ok=${result.ok}${
|
|
@@ -111,6 +225,10 @@ export class AuthVerifier {
|
|
|
111
225
|
async inspectPage(
|
|
112
226
|
options: { maxChars?: number; includeCookies?: boolean; signal?: AbortSignal } = {},
|
|
113
227
|
): Promise<AuthPageInspection> {
|
|
228
|
+
const signal = options.signal
|
|
229
|
+
? AbortSignal.any([options.signal, this.abort.signal])
|
|
230
|
+
: this.abort.signal;
|
|
231
|
+
if (signal.aborted) throw abortSignalError(signal);
|
|
114
232
|
const browser = Array.from(this.cdpPool.values()).at(-1);
|
|
115
233
|
if (!browser) {
|
|
116
234
|
return { ok: false, message: 'No active verification browser session exists.' };
|
|
@@ -121,7 +239,7 @@ export class AuthVerifier {
|
|
|
121
239
|
}
|
|
122
240
|
|
|
123
241
|
const maxChars = Math.max(256, Math.min(20_000, Math.floor(options.maxChars ?? 8_000)));
|
|
124
|
-
const snapshot = await withAbortSignal(() => inspectPage.call(browser),
|
|
242
|
+
const snapshot = await withAbortSignal(() => inspectPage.call(browser), signal);
|
|
125
243
|
const bodyText = this.sanitize(snapshot.bodyText);
|
|
126
244
|
return {
|
|
127
245
|
ok: true,
|
|
@@ -146,16 +264,112 @@ export class AuthVerifier {
|
|
|
146
264
|
};
|
|
147
265
|
}
|
|
148
266
|
|
|
267
|
+
async interactBrowser(
|
|
268
|
+
input: AuthBrowserAction,
|
|
269
|
+
callerSignal?: AbortSignal,
|
|
270
|
+
): Promise<AuthBrowserObservation> {
|
|
271
|
+
const signal = callerSignal
|
|
272
|
+
? AbortSignal.any([callerSignal, this.abort.signal])
|
|
273
|
+
: this.abort.signal;
|
|
274
|
+
if (signal.aborted) throw abortSignalError(signal);
|
|
275
|
+
await withAbortSignal(() => this.poolRelease, signal);
|
|
276
|
+
const action = AuthBrowserActionSchema.parse(input);
|
|
277
|
+
if ('value' in action) {
|
|
278
|
+
const credential = /^\$\{credential\.([^}]+)\}$/.exec(action.value);
|
|
279
|
+
if (credential) {
|
|
280
|
+
const value = this.credentials.values[credential[1] ?? ''];
|
|
281
|
+
if (value === undefined)
|
|
282
|
+
throw new Error('The requested credential is unavailable; ask the human for it.');
|
|
283
|
+
action.value = value;
|
|
284
|
+
}
|
|
285
|
+
if (action.action === 'fill' || credential) this.rememberSensitiveValues(action.value);
|
|
286
|
+
}
|
|
287
|
+
const pool = this.cdpPool;
|
|
288
|
+
let browser = Array.from(pool.values()).at(-1);
|
|
289
|
+
if (!browser) {
|
|
290
|
+
const url = action.action === 'navigate' ? action.url : this.discoveryUrl;
|
|
291
|
+
if (!url)
|
|
292
|
+
throw new Error('No verification browser exists; supply a discovery navigation URL.');
|
|
293
|
+
browser = createCdpBrowserFetch({
|
|
294
|
+
baseUrl: url,
|
|
295
|
+
bootstrapUrl: url,
|
|
296
|
+
headed: true,
|
|
297
|
+
seedCookies: this.credentials.cookies,
|
|
298
|
+
seedStorage: this.credentials.storage,
|
|
299
|
+
});
|
|
300
|
+
pool.set(AUTH_DISCOVERY_POOL_KEY, browser);
|
|
301
|
+
}
|
|
302
|
+
const interactBrowser = browser.interactBrowser?.bind(browser);
|
|
303
|
+
if (!interactBrowser)
|
|
304
|
+
throw new Error('The active verification browser cannot perform discovery.');
|
|
305
|
+
const abort = (): void => {
|
|
306
|
+
void this.retirePool(pool);
|
|
307
|
+
};
|
|
308
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
309
|
+
try {
|
|
310
|
+
if (signal.aborted) throw abortSignalError(signal);
|
|
311
|
+
const observation = await withAbortSignal(() => interactBrowser(action), signal);
|
|
312
|
+
if (pool !== this.cdpPool || signal.aborted) throw abortSignalError(signal);
|
|
313
|
+
const replacements = new Map(
|
|
314
|
+
Object.entries(this.credentials.values).map(([name, value]) => [
|
|
315
|
+
value,
|
|
316
|
+
`\${credential.${name}}`,
|
|
317
|
+
]),
|
|
318
|
+
);
|
|
319
|
+
const protect = (value: unknown): unknown => {
|
|
320
|
+
if (typeof value === 'string')
|
|
321
|
+
return this.sanitize(redactFreeformText(value, replacements).redacted);
|
|
322
|
+
if (Array.isArray(value)) return value.map(protect);
|
|
323
|
+
if (value && typeof value === 'object')
|
|
324
|
+
return Object.fromEntries(
|
|
325
|
+
Object.entries(value).map(([key, item]) => [key, protect(item)]),
|
|
326
|
+
);
|
|
327
|
+
return value;
|
|
328
|
+
};
|
|
329
|
+
return protect(observation) as AuthBrowserObservation;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (signal.aborted) throw abortSignalError(signal);
|
|
332
|
+
throw new Error(this.sanitize(error instanceof Error ? error.message : String(error)));
|
|
333
|
+
} finally {
|
|
334
|
+
signal.removeEventListener('abort', abort);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
149
338
|
async drain(): Promise<void> {
|
|
339
|
+
this.abort.abort(new DOMException('Authentication verifier closed', 'AbortError'));
|
|
150
340
|
await this.reset();
|
|
341
|
+
await Promise.allSettled(Array.from(this.retiredPools, (pool) => this.closePool(pool)));
|
|
342
|
+
this.sensitiveValues.clear();
|
|
343
|
+
this.lastAction = undefined;
|
|
151
344
|
}
|
|
152
345
|
|
|
153
346
|
private async reset(): Promise<void> {
|
|
154
|
-
|
|
155
|
-
|
|
347
|
+
await this.retirePool(this.cdpPool);
|
|
348
|
+
await this.poolRelease;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private retirePool(pool: Map<string, CdpBrowserFetch>): Promise<void> {
|
|
352
|
+
if (this.cdpPool === pool) {
|
|
353
|
+
this.cdpPool = new Map();
|
|
354
|
+
this.continuation = undefined;
|
|
156
355
|
}
|
|
157
|
-
this.
|
|
158
|
-
|
|
356
|
+
this.retiredPools.add(pool);
|
|
357
|
+
const release = this.closePool(pool);
|
|
358
|
+
this.poolRelease = Promise.allSettled([this.poolRelease, release]).then(() => {});
|
|
359
|
+
if (!this.pendingPools.has(pool)) void release.then(() => this.retiredPools.delete(pool));
|
|
360
|
+
return release;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private async closePool(pool: Map<string, CdpBrowserFetch>): Promise<void> {
|
|
364
|
+
const browsers = new Set(pool.values());
|
|
365
|
+
pool.clear();
|
|
366
|
+
await Promise.allSettled(
|
|
367
|
+
Array.from(browsers, async (browser) => {
|
|
368
|
+
if (this.closedBrowsers.has(browser)) return;
|
|
369
|
+
this.closedBrowsers.add(browser);
|
|
370
|
+
await browser.close();
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
159
373
|
}
|
|
160
374
|
|
|
161
375
|
private rememberSensitiveValues(value: unknown, seen = new Set<object>()): void {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** Connect agent-selected auth work to the existing live auth compiler. */
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { AuthCompileContext, type compileAuthAgent } from './auth-compile-agent.ts';
|
|
6
|
+
import { isCredentialReference } from './credential-extract.ts';
|
|
7
|
+
import { credentialObservation } from './credential-observation.ts';
|
|
8
|
+
import { loadSiteCredentials } from './credential-store.ts';
|
|
9
|
+
import type { LLMOptions } from './llm.ts';
|
|
10
|
+
import type { RunDeadlineRef } from './provider-retry.ts';
|
|
11
|
+
import type { Session } from './types.ts';
|
|
12
|
+
import { currentUserInteraction } from './user-interaction.ts';
|
|
13
|
+
|
|
14
|
+
export function attachRecordedAuthentication(options: {
|
|
15
|
+
session: Session;
|
|
16
|
+
sessionPath: string;
|
|
17
|
+
root: string;
|
|
18
|
+
values: Record<string, string>;
|
|
19
|
+
llmConfig?: LLMOptions;
|
|
20
|
+
runDeadline?: RunDeadlineRef;
|
|
21
|
+
compile?: typeof compileAuthAgent;
|
|
22
|
+
report?: (message: string) => void;
|
|
23
|
+
}): void {
|
|
24
|
+
const interaction = currentUserInteraction();
|
|
25
|
+
if (!interaction) return;
|
|
26
|
+
const contexts = new Map<string, { context: AuthCompileContext; outDir: string }>();
|
|
27
|
+
const workspaces = new Set<string>();
|
|
28
|
+
const recordingRevision = randomUUID();
|
|
29
|
+
const refreshMetadata = (contextRef: string, outDir: string): void => {
|
|
30
|
+
const metadata: Parameters<typeof interaction.updateAuthenticationContext>[1] = {
|
|
31
|
+
recordingRef: options.sessionPath,
|
|
32
|
+
recordingRevision,
|
|
33
|
+
workflowPath: join(outDir, 'workflow.json'),
|
|
34
|
+
evidenceRefs: [
|
|
35
|
+
join(outDir, '.auth-evidence-session.json'),
|
|
36
|
+
join(outDir, '.auth-live-discovery.json'),
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
try {
|
|
40
|
+
const stat = statSync(join(outDir, 'workflow.json'));
|
|
41
|
+
metadata.workflowRevision = `${stat.mtimeMs}:${stat.size}`;
|
|
42
|
+
} catch {
|
|
43
|
+
/* No artifact has been written yet. */
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const attempt: unknown = JSON.parse(
|
|
47
|
+
readFileSync(join(outDir, '.auth-verification-attempt.json'), 'utf8'),
|
|
48
|
+
);
|
|
49
|
+
if (attempt && typeof attempt === 'object') {
|
|
50
|
+
const value = attempt as Record<string, unknown>;
|
|
51
|
+
if (typeof value.action === 'string') metadata.latestAction = value.action;
|
|
52
|
+
if (typeof value.ok === 'boolean') metadata.latestOk = value.ok;
|
|
53
|
+
metadata.latestStatus = typeof value.status === 'number' ? value.status : undefined;
|
|
54
|
+
metadata.latestError = typeof value.error === 'string' ? value.error : undefined;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
/* Discovery need not have a compiled action attempt. */
|
|
58
|
+
}
|
|
59
|
+
interaction.updateAuthenticationContext(contextRef, metadata);
|
|
60
|
+
};
|
|
61
|
+
interaction.addCleanup(async () => {
|
|
62
|
+
await Promise.allSettled(Array.from(contexts.values(), ({ context }) => context.dispose()));
|
|
63
|
+
contexts.clear();
|
|
64
|
+
});
|
|
65
|
+
interaction.setAuthenticate(async (plan, signal) => {
|
|
66
|
+
const knownSeqs = new Set(options.session.requests.map(({ seq }) => seq));
|
|
67
|
+
if (
|
|
68
|
+
[...plan.credentialRequestSeqs, ...plan.authRequestSeqs].some((seq) => !knownSeqs.has(seq))
|
|
69
|
+
) {
|
|
70
|
+
return {
|
|
71
|
+
status: 'failed',
|
|
72
|
+
message: 'Authentication plan cites a request outside the selected recording.',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
options.report?.(
|
|
76
|
+
`authenticating with ${plan.toolName}; questions appear in the terminal when needed`,
|
|
77
|
+
);
|
|
78
|
+
const compile = options.compile ?? (await import('./auth-compile-agent.ts')).compileAuthAgent;
|
|
79
|
+
// UserInteraction resolves/validates identity before queuing the operation.
|
|
80
|
+
// Its legacy label mapping remains compatible, while explicit refs can cross labels.
|
|
81
|
+
const contextRef = plan.contextRef;
|
|
82
|
+
if (!contextRef) throw new Error('Authentication host requires a resolved contextRef.');
|
|
83
|
+
let retained = contexts.get(contextRef);
|
|
84
|
+
if (!retained) {
|
|
85
|
+
const legacyDir = join(options.root, plan.toolName);
|
|
86
|
+
const outDir = workspaces.has(legacyDir)
|
|
87
|
+
? join(options.root, `${plan.toolName}-${contextRef}`)
|
|
88
|
+
: legacyDir;
|
|
89
|
+
retained = { context: new AuthCompileContext(), outDir };
|
|
90
|
+
contexts.set(contextRef, retained);
|
|
91
|
+
workspaces.add(outDir);
|
|
92
|
+
}
|
|
93
|
+
const { context, outDir } = retained;
|
|
94
|
+
refreshMetadata(contextRef, outDir);
|
|
95
|
+
if (process.env.IMPRINT_CREDENTIAL_DIAGNOSTICS_PATH) {
|
|
96
|
+
try {
|
|
97
|
+
const stored = await loadSiteCredentials(options.session.site);
|
|
98
|
+
const values = Object.fromEntries(
|
|
99
|
+
Object.entries(options.values).filter(([, value]) => !isCredentialReference(value)),
|
|
100
|
+
);
|
|
101
|
+
const observation = credentialObservation(
|
|
102
|
+
{ ...stored, values: { ...stored.values, ...values } },
|
|
103
|
+
'authentication-context-boundary',
|
|
104
|
+
);
|
|
105
|
+
if (observation)
|
|
106
|
+
interaction.updateAuthenticationContext(contextRef, {
|
|
107
|
+
credentialRevision: observation.revision,
|
|
108
|
+
});
|
|
109
|
+
} catch {
|
|
110
|
+
/* Optional context metadata cannot affect authentication. */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const result = await compile({
|
|
114
|
+
context,
|
|
115
|
+
site: options.session.site,
|
|
116
|
+
session: options.session,
|
|
117
|
+
sessionPath: options.sessionPath,
|
|
118
|
+
outDir,
|
|
119
|
+
authToolPlan: plan,
|
|
120
|
+
teachCredentials: { site: options.session.site, values: options.values },
|
|
121
|
+
llmConfig: options.llmConfig,
|
|
122
|
+
runDeadline: options.runDeadline,
|
|
123
|
+
signal,
|
|
124
|
+
onProgress: (progress) => {
|
|
125
|
+
refreshMetadata(contextRef, outDir);
|
|
126
|
+
options.report?.(`${plan.toolName}: ${progress.toolName ?? progress.phase}`);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
refreshMetadata(contextRef, outDir);
|
|
130
|
+
return {
|
|
131
|
+
contextRef,
|
|
132
|
+
status: result.success ? 'authenticated' : 'failed',
|
|
133
|
+
message: result.message,
|
|
134
|
+
...(result.workflowPath ? { workflowPath: result.workflowPath } : {}),
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
}
|