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
|
@@ -17,17 +17,21 @@
|
|
|
17
17
|
* a downstream agent (OpenClaw/Hermes) which credentials it needs to ask
|
|
18
18
|
* for when consuming a shared skill.
|
|
19
19
|
*/
|
|
20
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
20
21
|
import {
|
|
21
22
|
existsSync,
|
|
22
23
|
mkdirSync,
|
|
23
24
|
readFileSync,
|
|
24
25
|
readdirSync,
|
|
26
|
+
renameSync,
|
|
25
27
|
unlinkSync,
|
|
26
28
|
writeFileSync,
|
|
27
29
|
} from 'node:fs';
|
|
28
30
|
import { dirname, join as pathJoin } from 'node:path';
|
|
29
31
|
import { argon2id } from '@noble/hashes/argon2.js';
|
|
30
32
|
import envPaths from 'env-paths';
|
|
33
|
+
import { withCredentialLock } from './credential-lock.ts';
|
|
34
|
+
import { credentialObservation, recordCredentialEvent } from './credential-observation.ts';
|
|
31
35
|
|
|
32
36
|
const PATHS = envPaths('imprint', { suffix: '' });
|
|
33
37
|
const SERVICE_NAME = 'imprint';
|
|
@@ -62,6 +66,8 @@ export interface CookieRecord {
|
|
|
62
66
|
sameSite?: string;
|
|
63
67
|
hostOnly?: boolean;
|
|
64
68
|
creationIndex?: number;
|
|
69
|
+
/** Independent HTTP observation retained alongside its exact browser baseline. */
|
|
70
|
+
httpResponse?: { browserSnapshotHash: string | null; deleted?: boolean };
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
export interface StorageRecord {
|
|
@@ -75,6 +81,10 @@ export interface StorageRecord {
|
|
|
75
81
|
* await to keep the call sites uniform. */
|
|
76
82
|
export interface CredentialBackend {
|
|
77
83
|
readonly id: 'keyring' | 'encrypted-file' | 'legacy-json';
|
|
84
|
+
/** One lock for the actual backend namespace, independent of IMPRINT_HOME. */
|
|
85
|
+
readonly lockDirectory?: string;
|
|
86
|
+
/** Refresh process-local data once the namespace lock is held. */
|
|
87
|
+
refresh?(): Promise<void>;
|
|
78
88
|
getSecret(site: string, name: string): Promise<string | null>;
|
|
79
89
|
setSecret(site: string, name: string, value: string): Promise<void>;
|
|
80
90
|
deleteSecret(site: string, name: string): Promise<void>;
|
|
@@ -206,10 +216,19 @@ class EncryptedFileBackend implements CredentialBackend {
|
|
|
206
216
|
// biome-ignore lint/suspicious/noExplicitAny: dynamic libsodium ref
|
|
207
217
|
private sodium: any;
|
|
208
218
|
private cachedKey: Uint8Array | null = null;
|
|
219
|
+
private cachedSaltB64: string | null = null;
|
|
209
220
|
/** Cached decrypted shape — flushed back to disk on every mutation. */
|
|
210
221
|
private cachedData: EncryptedFileShape | null = null;
|
|
211
222
|
private passphraseProvider: () => Promise<string>;
|
|
212
223
|
|
|
224
|
+
get lockDirectory(): string {
|
|
225
|
+
return `${this.filePath}.lock`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async refresh(): Promise<void> {
|
|
229
|
+
this.cachedData = null;
|
|
230
|
+
}
|
|
231
|
+
|
|
213
232
|
constructor(opts: {
|
|
214
233
|
filePath: string;
|
|
215
234
|
// biome-ignore lint/suspicious/noExplicitAny: dynamic libsodium ref
|
|
@@ -226,6 +245,8 @@ class EncryptedFileBackend implements CredentialBackend {
|
|
|
226
245
|
const sodium = this.sodium;
|
|
227
246
|
|
|
228
247
|
if (!existsSync(this.filePath)) {
|
|
248
|
+
this.cachedKey = null;
|
|
249
|
+
this.cachedSaltB64 = null;
|
|
229
250
|
// No store yet. For a READ (forWrite !== true), short-circuit with an
|
|
230
251
|
// empty in-memory shape — there cannot be any credentials to return,
|
|
231
252
|
// and prompting for a passphrase here would hang non-interactive
|
|
@@ -258,8 +279,10 @@ class EncryptedFileBackend implements CredentialBackend {
|
|
|
258
279
|
|
|
259
280
|
// Try cached passphrase first if the user has set $IMPRINT_PASSPHRASE
|
|
260
281
|
// and re-entered process.
|
|
261
|
-
const
|
|
262
|
-
|
|
282
|
+
const key =
|
|
283
|
+
this.cachedKey && this.cachedSaltB64 === parsed.saltB64
|
|
284
|
+
? this.cachedKey
|
|
285
|
+
: deriveKey(await this.passphraseProvider(), salt);
|
|
263
286
|
let plain: Uint8Array;
|
|
264
287
|
try {
|
|
265
288
|
plain = sodium.crypto_secretbox_open_easy(ct, nonce, key);
|
|
@@ -270,6 +293,7 @@ class EncryptedFileBackend implements CredentialBackend {
|
|
|
270
293
|
}
|
|
271
294
|
const text = new TextDecoder().decode(plain);
|
|
272
295
|
this.cachedKey = key;
|
|
296
|
+
this.cachedSaltB64 = parsed.saltB64;
|
|
273
297
|
this.cachedData = JSON.parse(text) as EncryptedFileShape;
|
|
274
298
|
}
|
|
275
299
|
|
|
@@ -286,7 +310,18 @@ class EncryptedFileBackend implements CredentialBackend {
|
|
|
286
310
|
ctB64: b64encode(ct),
|
|
287
311
|
};
|
|
288
312
|
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
289
|
-
|
|
313
|
+
const temporary = `${this.filePath}.${randomUUID()}.tmp`;
|
|
314
|
+
try {
|
|
315
|
+
writeFileSync(temporary, JSON.stringify(wire), { encoding: 'utf8', mode: 0o600 });
|
|
316
|
+
renameSync(temporary, this.filePath);
|
|
317
|
+
this.cachedSaltB64 = wire.saltB64;
|
|
318
|
+
} finally {
|
|
319
|
+
try {
|
|
320
|
+
unlinkSync(temporary);
|
|
321
|
+
} catch {
|
|
322
|
+
// Cleanup must not replace a failed write with a secondary error.
|
|
323
|
+
}
|
|
324
|
+
}
|
|
290
325
|
}
|
|
291
326
|
|
|
292
327
|
/** Persist using the SAME salt the file was loaded with. */
|
|
@@ -652,17 +687,103 @@ export function markLegacyStoreMigrated(site: string): void {
|
|
|
652
687
|
// ─── High-level convenience API ────────────────────────────────────────────
|
|
653
688
|
|
|
654
689
|
/** A site's full credential view: secrets values + cookies. Used by runtime. */
|
|
655
|
-
interface SiteCredentialView {
|
|
690
|
+
export interface SiteCredentialView {
|
|
656
691
|
site: string;
|
|
657
692
|
cookies: CookieRecord[];
|
|
658
693
|
values: Record<string, string>;
|
|
659
694
|
storage: StorageRecord[];
|
|
695
|
+
/** Raw durable material identity, before runtime normalization or env input. */
|
|
696
|
+
revision: string;
|
|
697
|
+
legacy?: true;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function credentialRevision(material: Omit<SiteCredentialView, 'revision'>): string {
|
|
701
|
+
const canonical = (value: unknown): unknown => {
|
|
702
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
703
|
+
if (value && typeof value === 'object')
|
|
704
|
+
return Object.fromEntries(
|
|
705
|
+
Object.entries(value)
|
|
706
|
+
.filter(([, entry]) => entry !== undefined)
|
|
707
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
708
|
+
.map(([key, entry]) => [key, canonical(entry)]),
|
|
709
|
+
);
|
|
710
|
+
return value;
|
|
711
|
+
};
|
|
712
|
+
return createHash('sha256')
|
|
713
|
+
.update(JSON.stringify(canonical(material)))
|
|
714
|
+
.digest('hex');
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function credentialLockDirectory(site: string, backend: CredentialBackend): string {
|
|
718
|
+
return (
|
|
719
|
+
backend.lockDirectory ??
|
|
720
|
+
pathJoin(
|
|
721
|
+
PATHS.config,
|
|
722
|
+
'credential-locks',
|
|
723
|
+
backend.id,
|
|
724
|
+
createHash('sha256').update(site).digest('hex'),
|
|
725
|
+
)
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
async function withSiteCredentialLock<T>(
|
|
730
|
+
site: string,
|
|
731
|
+
signal: AbortSignal | undefined,
|
|
732
|
+
work: (backend: CredentialBackend) => Promise<T>,
|
|
733
|
+
): Promise<T> {
|
|
734
|
+
signal?.throwIfAborted();
|
|
735
|
+
const backend = await getCredentialBackend();
|
|
736
|
+
return withCredentialLock(credentialLockDirectory(site, backend), signal, async () => {
|
|
737
|
+
await backend.refresh?.();
|
|
738
|
+
signal?.throwIfAborted();
|
|
739
|
+
return work(backend);
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** Serializes a complete sign-in independently of the credential reads and commits it performs. */
|
|
744
|
+
export async function acquireSiteAuthRecoveryLock(
|
|
745
|
+
site: string,
|
|
746
|
+
signal: AbortSignal,
|
|
747
|
+
): Promise<() => Promise<void>> {
|
|
748
|
+
signal.throwIfAborted();
|
|
749
|
+
const backend = await getCredentialBackend();
|
|
750
|
+
const directory = pathJoin(
|
|
751
|
+
`${credentialLockDirectory(site, backend)}-auth-recovery`,
|
|
752
|
+
createHash('sha256').update(site).digest('hex'),
|
|
753
|
+
);
|
|
754
|
+
return new Promise((resolve, reject) => {
|
|
755
|
+
const held = withCredentialLock(
|
|
756
|
+
directory,
|
|
757
|
+
signal,
|
|
758
|
+
() =>
|
|
759
|
+
new Promise<void>((release) => {
|
|
760
|
+
resolve(() => {
|
|
761
|
+
release();
|
|
762
|
+
return held;
|
|
763
|
+
});
|
|
764
|
+
}),
|
|
765
|
+
250,
|
|
766
|
+
);
|
|
767
|
+
void held.catch(reject);
|
|
768
|
+
});
|
|
660
769
|
}
|
|
661
770
|
|
|
662
771
|
/** Loads everything we know about a site, falling back through the backends.
|
|
663
772
|
* Used at request time by runtime.executeWorkflow. */
|
|
664
|
-
export async function loadSiteCredentials(
|
|
665
|
-
|
|
773
|
+
export async function loadSiteCredentials(
|
|
774
|
+
site: string,
|
|
775
|
+
options?: { signal?: AbortSignal },
|
|
776
|
+
): Promise<SiteCredentialView> {
|
|
777
|
+
return withSiteCredentialLock(site, options?.signal, (backend) =>
|
|
778
|
+
loadSiteCredentialsUnlocked(site, backend),
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async function loadSiteCredentialsUnlocked(
|
|
783
|
+
site: string,
|
|
784
|
+
backend: CredentialBackend,
|
|
785
|
+
includeLegacy = true,
|
|
786
|
+
): Promise<SiteCredentialView> {
|
|
666
787
|
const names = await backend.listSecrets(site);
|
|
667
788
|
const values: Record<string, string> = {};
|
|
668
789
|
for (const n of names) {
|
|
@@ -673,26 +794,113 @@ export async function loadSiteCredentials(site: string): Promise<SiteCredentialV
|
|
|
673
794
|
const storage = (await backend.getStorage?.(site)) ?? [];
|
|
674
795
|
|
|
675
796
|
// Fall through to legacy if the backend has nothing.
|
|
676
|
-
if (
|
|
797
|
+
if (
|
|
798
|
+
includeLegacy &&
|
|
799
|
+
Object.keys(values).length === 0 &&
|
|
800
|
+
cookies.length === 0 &&
|
|
801
|
+
storage.length === 0
|
|
802
|
+
) {
|
|
677
803
|
const legacy = readLegacyStore(site);
|
|
678
804
|
if (legacy) {
|
|
679
|
-
|
|
805
|
+
const material = structuredClone({
|
|
806
|
+
site,
|
|
807
|
+
cookies: legacy.cookies,
|
|
808
|
+
values: legacy.values,
|
|
809
|
+
storage: [],
|
|
810
|
+
});
|
|
811
|
+
const result = { ...material, revision: credentialRevision(material), legacy: true as const };
|
|
812
|
+
const observation = credentialObservation(result, 'legacy-json');
|
|
813
|
+
if (observation) recordCredentialEvent('load', observation);
|
|
814
|
+
return result;
|
|
680
815
|
}
|
|
681
816
|
}
|
|
682
|
-
|
|
817
|
+
const material = structuredClone({ site, cookies, values, storage });
|
|
818
|
+
const result = { ...material, revision: credentialRevision(material) };
|
|
819
|
+
const observation = credentialObservation(result, backend.id);
|
|
820
|
+
if (observation) recordCredentialEvent('load', observation);
|
|
821
|
+
return result;
|
|
683
822
|
}
|
|
684
823
|
|
|
685
824
|
/** Convenience wrapper to persist cookies after a successful auth tool run.
|
|
686
825
|
* Delegates to the active credential backend's setCookies. */
|
|
687
|
-
export async function saveSiteCookies(
|
|
688
|
-
|
|
689
|
-
|
|
826
|
+
export async function saveSiteCookies(
|
|
827
|
+
site: string,
|
|
828
|
+
cookies: CookieRecord[],
|
|
829
|
+
options?: { signal?: AbortSignal },
|
|
830
|
+
): Promise<void> {
|
|
831
|
+
const signal = options?.signal;
|
|
832
|
+
await withSiteCredentialLock(site, signal, async (backend) => {
|
|
833
|
+
const previous = structuredClone(await backend.getCookies(site));
|
|
834
|
+
signal?.throwIfAborted();
|
|
835
|
+
try {
|
|
836
|
+
await backend.setCookies(site, cookies);
|
|
837
|
+
signal?.throwIfAborted();
|
|
838
|
+
await observeCommittedCredentials(site, backend);
|
|
839
|
+
signal?.throwIfAborted();
|
|
840
|
+
} catch (error) {
|
|
841
|
+
await backend.setCookies(site, previous);
|
|
842
|
+
throw error;
|
|
843
|
+
}
|
|
844
|
+
});
|
|
690
845
|
}
|
|
691
846
|
|
|
692
847
|
/** Persist browser storage through the active credential backend. */
|
|
693
|
-
export async function saveSiteStorage(
|
|
694
|
-
|
|
695
|
-
|
|
848
|
+
export async function saveSiteStorage(
|
|
849
|
+
site: string,
|
|
850
|
+
storage: StorageRecord[],
|
|
851
|
+
options?: { signal?: AbortSignal },
|
|
852
|
+
): Promise<void> {
|
|
853
|
+
const signal = options?.signal;
|
|
854
|
+
await withSiteCredentialLock(site, signal, async (backend) => {
|
|
855
|
+
const previous = structuredClone((await backend.getStorage?.(site)) ?? []);
|
|
856
|
+
signal?.throwIfAborted();
|
|
857
|
+
try {
|
|
858
|
+
await backend.setStorage?.(site, storage);
|
|
859
|
+
signal?.throwIfAborted();
|
|
860
|
+
await observeCommittedCredentials(site, backend);
|
|
861
|
+
signal?.throwIfAborted();
|
|
862
|
+
} catch (error) {
|
|
863
|
+
await backend.setStorage?.(site, previous);
|
|
864
|
+
throw error;
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/** Publish a successful browser's complete cookie snapshot only if its durable
|
|
870
|
+
* starting revision is still current. Never merges removed cookies back in or
|
|
871
|
+
* overwrites newer auth, secrets or storage. Legacy fallback is read-only. */
|
|
872
|
+
export async function compareAndSwapSiteCookies(
|
|
873
|
+
site: string,
|
|
874
|
+
expectedRevision: string,
|
|
875
|
+
cookies: CookieRecord[],
|
|
876
|
+
options?: { signal?: AbortSignal },
|
|
877
|
+
): Promise<{ status: 'updated' | 'unchanged' | 'stale'; revision: string }> {
|
|
878
|
+
const signal = options?.signal;
|
|
879
|
+
const nextCookies = structuredClone(cookies);
|
|
880
|
+
return withSiteCredentialLock(site, signal, async (backend) => {
|
|
881
|
+
const current = await loadSiteCredentialsUnlocked(site, backend, false);
|
|
882
|
+
signal?.throwIfAborted();
|
|
883
|
+
if (current.revision !== expectedRevision)
|
|
884
|
+
return { status: 'stale', revision: current.revision };
|
|
885
|
+
const nextRevision = credentialRevision({
|
|
886
|
+
site,
|
|
887
|
+
cookies: nextCookies,
|
|
888
|
+
values: current.values,
|
|
889
|
+
storage: current.storage,
|
|
890
|
+
});
|
|
891
|
+
if (nextRevision === current.revision)
|
|
892
|
+
return { status: 'unchanged', revision: current.revision };
|
|
893
|
+
try {
|
|
894
|
+
await backend.setCookies(site, nextCookies);
|
|
895
|
+
signal?.throwIfAborted();
|
|
896
|
+
await observeCommittedCredentials(site, backend);
|
|
897
|
+
signal?.throwIfAborted();
|
|
898
|
+
return { status: 'updated', revision: nextRevision };
|
|
899
|
+
} catch (error) {
|
|
900
|
+
await backend.setCookies(site, current.cookies);
|
|
901
|
+
throw error;
|
|
902
|
+
}
|
|
903
|
+
});
|
|
696
904
|
}
|
|
697
905
|
|
|
698
906
|
/** Replace the durable auth material as one logical commit. Backends such as
|
|
@@ -702,43 +910,75 @@ export async function commitSiteAuthState(
|
|
|
702
910
|
site: string,
|
|
703
911
|
cookies: CookieRecord[],
|
|
704
912
|
secrets: Record<string, string>,
|
|
913
|
+
options?: { signal?: AbortSignal; expectedRevision?: string },
|
|
705
914
|
): Promise<void> {
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
await backend.setSecret(site, name, value);
|
|
915
|
+
const signal = options?.signal;
|
|
916
|
+
await withSiteCredentialLock(site, signal, async (backend) => {
|
|
917
|
+
signal?.throwIfAborted();
|
|
918
|
+
let nextSecrets = secrets;
|
|
919
|
+
if (options?.expectedRevision !== undefined) {
|
|
920
|
+
const current = await loadSiteCredentialsUnlocked(site, backend);
|
|
921
|
+
if (current.revision !== options.expectedRevision) {
|
|
922
|
+
throw new Error('the saved session changed while this sign-in was running');
|
|
923
|
+
}
|
|
924
|
+
if (current.legacy) nextSecrets = { ...current.values, ...secrets };
|
|
717
925
|
}
|
|
718
|
-
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
rollbackFailures.push(
|
|
724
|
-
`cookies: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
|
|
725
|
-
);
|
|
926
|
+
const previousCookies = structuredClone(await backend.getCookies(site));
|
|
927
|
+
const previousSecrets = new Map<string, string | null>();
|
|
928
|
+
for (const name of Object.keys(nextSecrets)) {
|
|
929
|
+
signal?.throwIfAborted();
|
|
930
|
+
previousSecrets.set(name, await backend.getSecret(site, name));
|
|
726
931
|
}
|
|
727
|
-
|
|
932
|
+
|
|
933
|
+
try {
|
|
934
|
+
signal?.throwIfAborted();
|
|
935
|
+
await backend.setCookies(site, cookies);
|
|
936
|
+
signal?.throwIfAborted();
|
|
937
|
+
for (const [name, value] of Object.entries(nextSecrets)) {
|
|
938
|
+
await backend.setSecret(site, name, value);
|
|
939
|
+
signal?.throwIfAborted();
|
|
940
|
+
}
|
|
941
|
+
await observeCommittedCredentials(site, backend);
|
|
942
|
+
signal?.throwIfAborted();
|
|
943
|
+
} catch (err) {
|
|
944
|
+
const rollbackFailures: string[] = [];
|
|
728
945
|
try {
|
|
729
|
-
|
|
730
|
-
else await backend.setSecret(site, name, value);
|
|
946
|
+
await backend.setCookies(site, previousCookies);
|
|
731
947
|
} catch (rollbackErr) {
|
|
732
948
|
rollbackFailures.push(
|
|
733
|
-
|
|
949
|
+
`cookies: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
|
|
734
950
|
);
|
|
735
951
|
}
|
|
952
|
+
for (const [name, value] of previousSecrets) {
|
|
953
|
+
try {
|
|
954
|
+
if (value === null) await backend.deleteSecret(site, name);
|
|
955
|
+
else await backend.setSecret(site, name, value);
|
|
956
|
+
} catch (rollbackErr) {
|
|
957
|
+
rollbackFailures.push(
|
|
958
|
+
`${name}: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
const original = err instanceof Error ? err.message : String(err);
|
|
963
|
+
throw new Error(
|
|
964
|
+
rollbackFailures.length > 0
|
|
965
|
+
? `${original}; rollback also failed for ${rollbackFailures.join(', ')}`
|
|
966
|
+
: original,
|
|
967
|
+
);
|
|
736
968
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
async function observeCommittedCredentials(
|
|
973
|
+
site: string,
|
|
974
|
+
backend: CredentialBackend,
|
|
975
|
+
): Promise<void> {
|
|
976
|
+
if (!process.env.IMPRINT_CREDENTIAL_DIAGNOSTICS_PATH) return;
|
|
977
|
+
try {
|
|
978
|
+
const material = await loadSiteCredentialsUnlocked(site, backend);
|
|
979
|
+
const observation = credentialObservation(material, 'durable-commit');
|
|
980
|
+
if (observation) recordCredentialEvent('commit', observation);
|
|
981
|
+
} catch {
|
|
982
|
+
// A diagnostics read failure cannot invalidate a completed credential write.
|
|
743
983
|
}
|
|
744
984
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ComponentRecoveryOwner } from './component-recovery.ts';
|
|
2
|
+
import {
|
|
3
|
+
type LiveComponentRecoveryInput,
|
|
4
|
+
type MasterTeachAgentOptions,
|
|
5
|
+
requestLiveComponentRecovery,
|
|
6
|
+
} from './master-teach-agents.ts';
|
|
7
|
+
import { currentUserInteraction, requestAgentInteraction } from './user-interaction.ts';
|
|
8
|
+
|
|
9
|
+
/** Select an existing owner; never infer one from names or HTTP status. */
|
|
10
|
+
export async function selectLiveComponentRecovery(
|
|
11
|
+
input: Omit<
|
|
12
|
+
LiveComponentRecoveryInput,
|
|
13
|
+
'contexts' | 'components' | 'authenticationAvailable' | 'phase'
|
|
14
|
+
> & {
|
|
15
|
+
phase?: string;
|
|
16
|
+
allowAuthentication?: boolean;
|
|
17
|
+
owners: readonly ComponentRecoveryOwner[];
|
|
18
|
+
agent: MasterTeachAgentOptions;
|
|
19
|
+
},
|
|
20
|
+
): Promise<{ owner: ComponentRecoveryOwner; reason: string } | undefined> {
|
|
21
|
+
const interaction = currentUserInteraction();
|
|
22
|
+
const { agent, owners, allowAuthentication = true, ...failure } = input;
|
|
23
|
+
const applicable = owners.filter((owner) =>
|
|
24
|
+
owner.toolIds.includes(input.toolId ?? input.toolName),
|
|
25
|
+
);
|
|
26
|
+
const authenticationAvailable = allowAuthentication && (interaction?.hasAuthentication ?? false);
|
|
27
|
+
if (!authenticationAvailable && applicable.length === 0) return undefined;
|
|
28
|
+
if (new Set(applicable.map(({ id }) => id)).size !== applicable.length)
|
|
29
|
+
throw new Error('Component recovery owner ids must be unique for a caller');
|
|
30
|
+
const decision = await requestLiveComponentRecovery(
|
|
31
|
+
{
|
|
32
|
+
...failure,
|
|
33
|
+
phase: input.phase ?? 'live',
|
|
34
|
+
authenticationAvailable,
|
|
35
|
+
components: applicable.map(({ recover: _recover, ...component }) => ({
|
|
36
|
+
...component,
|
|
37
|
+
toolIds: [...component.toolIds],
|
|
38
|
+
})),
|
|
39
|
+
contexts:
|
|
40
|
+
interaction?.authenticationContexts().map((context) => ({
|
|
41
|
+
contextRef: context.contextRef,
|
|
42
|
+
label: context.label,
|
|
43
|
+
workflowPath: context.workflowPath,
|
|
44
|
+
latestAction: context.latestAction,
|
|
45
|
+
latestOk: context.latestOk,
|
|
46
|
+
latestStatus: context.latestStatus,
|
|
47
|
+
latestError: context.latestError,
|
|
48
|
+
})) ?? [],
|
|
49
|
+
},
|
|
50
|
+
agent,
|
|
51
|
+
);
|
|
52
|
+
if (decision.action === 'return_failure') return undefined;
|
|
53
|
+
if (decision.action === 'recover_component') {
|
|
54
|
+
const owner = applicable.find((owner) => owner.id === decision.componentId);
|
|
55
|
+
return owner ? { owner, reason: decision.reason } : undefined;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
owner: {
|
|
59
|
+
id: `authentication:${decision.plan.contextRef ?? decision.plan.toolName}`,
|
|
60
|
+
kind: 'authentication',
|
|
61
|
+
label: decision.plan.toolName,
|
|
62
|
+
toolIds: [input.toolId ?? input.toolName],
|
|
63
|
+
recover: async () => {
|
|
64
|
+
const result = await requestAgentInteraction(
|
|
65
|
+
{ action: 'authenticate', plan: decision.plan },
|
|
66
|
+
agent.signal,
|
|
67
|
+
);
|
|
68
|
+
return result.status === 'authenticated';
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
reason: decision.reason,
|
|
72
|
+
};
|
|
73
|
+
}
|
package/src/imprint/llm.ts
CHANGED
|
@@ -1192,7 +1192,7 @@ function createProvider(name: ProviderName, opts: LLMOptions = {}): LLMProvider
|
|
|
1192
1192
|
return new ClaudeCliProvider({ model });
|
|
1193
1193
|
case 'codex-cli':
|
|
1194
1194
|
return new CodexCliProvider({
|
|
1195
|
-
model: opts.model ?? process.env.CODEX_MODEL ?? 'gpt-6-sol',
|
|
1195
|
+
model: opts.model ?? process.env.CODEX_MODEL ?? 'gpt-6.1-sol',
|
|
1196
1196
|
});
|
|
1197
1197
|
case 'cursor-cli':
|
|
1198
1198
|
return new CursorCliProvider({ model: opts.model });
|
|
@@ -1275,7 +1275,7 @@ export function preferredAgentModel(provider: ProviderName): string {
|
|
|
1275
1275
|
case 'claude-cli':
|
|
1276
1276
|
return 'claude-opus-4-8';
|
|
1277
1277
|
case 'codex-cli':
|
|
1278
|
-
return 'gpt-6-sol';
|
|
1278
|
+
return 'gpt-6.1-sol';
|
|
1279
1279
|
case 'cursor-cli':
|
|
1280
1280
|
return 'claude-opus-4-8'; // best-effort; cursor passes through
|
|
1281
1281
|
}
|
|
@@ -1321,7 +1321,8 @@ export function availableModelsForProvider(provider: ProviderName): ModelOption[
|
|
|
1321
1321
|
];
|
|
1322
1322
|
case 'codex-cli':
|
|
1323
1323
|
return [
|
|
1324
|
-
{ model: 'gpt-6-sol', isDefault: true },
|
|
1324
|
+
{ model: 'gpt-6.1-sol', isDefault: true },
|
|
1325
|
+
{ model: 'gpt-6-sol', isDefault: false },
|
|
1325
1326
|
{ model: 'gpt-5.6-sol', isDefault: false },
|
|
1326
1327
|
{ model: 'gpt-5.6-terra', isDefault: false },
|
|
1327
1328
|
{ model: 'gpt-5.6-luna', isDefault: false },
|
package/src/imprint/log.ts
CHANGED
|
@@ -15,11 +15,26 @@ type Log = (msg: string) => void;
|
|
|
15
15
|
|
|
16
16
|
const isQuiet = (): boolean => process.env.IMPRINT_QUIET === '1';
|
|
17
17
|
export const isDebug = (): boolean => process.env.IMPRINT_DEBUG === '1';
|
|
18
|
+
let promptDepth = 0;
|
|
19
|
+
const pendingLines: string[] = [];
|
|
20
|
+
|
|
21
|
+
/** Keep parallel-agent progress from overwriting an active terminal question. */
|
|
22
|
+
export function pauseLogsForPrompt(): () => void {
|
|
23
|
+
promptDepth += 1;
|
|
24
|
+
return () => {
|
|
25
|
+
promptDepth -= 1;
|
|
26
|
+
if (promptDepth === 0) {
|
|
27
|
+
for (const line of pendingLines.splice(0)) process.stderr.write(line);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
18
31
|
|
|
19
32
|
export function createLog(area: string): Log {
|
|
20
33
|
const prefix = `[imprint ${area}]`;
|
|
21
34
|
return (msg: string): void => {
|
|
22
35
|
if (isQuiet()) return;
|
|
23
|
-
|
|
36
|
+
const line = `${prefix} ${msg}\n`;
|
|
37
|
+
if (promptDepth > 0) pendingLines.push(line);
|
|
38
|
+
else process.stderr.write(line);
|
|
24
39
|
};
|
|
25
40
|
}
|
package/src/imprint/login.ts
CHANGED
|
@@ -198,6 +198,15 @@ function collectSessionCaptures(site: string): SessionCaptureBinding[] {
|
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
|
+
const lastStep = action.steps.at(-1);
|
|
202
|
+
const request = lastStep && workflow.requests[lastStep.request];
|
|
203
|
+
if (request) {
|
|
204
|
+
for (const { when } of action.branches ?? []) {
|
|
205
|
+
for (const credentialName of persistedByCapture.get(when.name) ?? []) {
|
|
206
|
+
addCapture(credentialName, when, request);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
201
210
|
}
|
|
202
211
|
}
|
|
203
212
|
return captures;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import type { BackendAttemptFact } from './backend-ladder.ts';
|
|
4
4
|
import type { BodyComparison } from './body-structure.ts';
|
|
5
|
+
import type { CredentialObservation, DispatchObservation } from './credential-observation.ts';
|
|
5
6
|
import {
|
|
6
7
|
ChainEdgeSchema,
|
|
7
8
|
ConcreteTeachingParameterSchema,
|
|
@@ -113,10 +114,25 @@ export const FocusedPlannerInputSchema = strictObject({
|
|
|
113
114
|
siblingToolEvidence: z.array(SiblingToolEvidenceSchema),
|
|
114
115
|
incomingChainEdges: z.array(ChainEdgeSchema),
|
|
115
116
|
outgoingChainEdges: z.array(ChainEdgeSchema),
|
|
116
|
-
/**
|
|
117
|
-
*
|
|
118
|
-
* from tested calls and response shapes rather than guesses. */
|
|
117
|
+
/** Exact research for this tool and its declared dependencies. Other
|
|
118
|
+
* completed operations remain discoverable through apiResearchIndex. */
|
|
119
119
|
apiResearch: z.array(z.lazy(() => ApiResearchHandoffSchema)).default([]),
|
|
120
|
+
apiResearchIndex: z
|
|
121
|
+
.array(
|
|
122
|
+
strictObject({
|
|
123
|
+
toolName: SemanticToolCandidateSchema.shape.toolName,
|
|
124
|
+
researchInputsSha256: PromptShaSchema,
|
|
125
|
+
status: z.enum(['proven', 'partial', 'blocked']),
|
|
126
|
+
summary: utf8Text(1, 2_000),
|
|
127
|
+
observationId: PromptIdSchema.optional(),
|
|
128
|
+
executionMechanism: utf8Text(1, 128).optional(),
|
|
129
|
+
resultOk: z.boolean().optional(),
|
|
130
|
+
/** Read the exact handoff through run-local shared research only
|
|
131
|
+
* when its details matter to a proposed dependency or boundary. */
|
|
132
|
+
handoffRef: ContentAddressedRefSchema.optional(),
|
|
133
|
+
}),
|
|
134
|
+
)
|
|
135
|
+
.default([]),
|
|
120
136
|
evidence: PromptEvidenceProjectionSchema,
|
|
121
137
|
/** Optional input-only handoff from the immediately preceding failed
|
|
122
138
|
* attempt. The planner never echoes this object. */
|
|
@@ -141,6 +157,26 @@ export const ApiResearchCandidateSchema = strictObject({
|
|
|
141
157
|
.optional(),
|
|
142
158
|
});
|
|
143
159
|
export type ApiResearchCandidate = z.infer<typeof ApiResearchCandidateSchema>;
|
|
160
|
+
// Runtime diagnostics cross the same retained research/input/handoff boundary.
|
|
161
|
+
// Keep their typed wire shape aligned without interpreting credential validity.
|
|
162
|
+
const CredentialObservationSchema: z.ZodType<CredentialObservation> = strictObject({
|
|
163
|
+
scope: z.string(),
|
|
164
|
+
revision: z.string(),
|
|
165
|
+
source: z.string(),
|
|
166
|
+
cookieCount: z.number().int().nonnegative(),
|
|
167
|
+
valueCount: z.number().int().nonnegative(),
|
|
168
|
+
storageCount: z.number().int().nonnegative(),
|
|
169
|
+
});
|
|
170
|
+
const DispatchObservationSchema: z.ZodType<DispatchObservation> = strictObject({
|
|
171
|
+
invocationId: z.string(),
|
|
172
|
+
requestIndex: z.number().int().nonnegative(),
|
|
173
|
+
credentials: CredentialObservationSchema,
|
|
174
|
+
effectiveCredentials: CredentialObservationSchema.optional(),
|
|
175
|
+
preparedRequestIdentity: z.string(),
|
|
176
|
+
requestTargetIdentity: z.string(),
|
|
177
|
+
preparedCookieHeaderIdentity: z.string().optional(),
|
|
178
|
+
cookieSource: z.enum(['browser-owned', 'runtime-prepared']),
|
|
179
|
+
});
|
|
144
180
|
const BackendResponseObservationSchema = strictObject({
|
|
145
181
|
backend: z.enum(['fetch', 'fetch-bootstrap', 'cdp-replay', 'stealth-fetch', 'playbook']),
|
|
146
182
|
requestIndex: z.number().int().nonnegative(),
|
|
@@ -151,6 +187,7 @@ const BackendResponseObservationSchema = strictObject({
|
|
|
151
187
|
valueType: z.enum(['null', 'array', 'object', 'string', 'number', 'boolean']),
|
|
152
188
|
topLevelKeys: z.array(utf8Text(0, 200)).max(32).optional(),
|
|
153
189
|
arrayLength: z.number().int().nonnegative().optional(),
|
|
190
|
+
dispatch: DispatchObservationSchema.optional(),
|
|
154
191
|
});
|
|
155
192
|
const PreparedRequestComparisonSchema = strictObject({
|
|
156
193
|
backend: z.enum(['fetch', 'fetch-bootstrap', 'cdp-replay', 'stealth-fetch', 'playbook']),
|