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.
Files changed (58) hide show
  1. package/README.md +66 -2
  2. package/package.json +1 -1
  3. package/prompts/auth-compile-agent.md +173 -22
  4. package/prompts/compile-agent.md +45 -8
  5. package/prompts/component-recovery.md +40 -0
  6. package/prompts/master-teach-decision.md +28 -3
  7. package/prompts/master-teach-focused-planner.md +18 -5
  8. package/src/cli.ts +26 -18
  9. package/src/imprint/agent-tool.ts +15 -0
  10. package/src/imprint/agent.ts +2 -11
  11. package/src/imprint/audit.ts +35 -13
  12. package/src/imprint/auth-browser-discovery.ts +276 -0
  13. package/src/imprint/auth-compile-agent.ts +513 -224
  14. package/src/imprint/auth-compile-tools.ts +213 -25
  15. package/src/imprint/auth-discovery-evidence.ts +81 -0
  16. package/src/imprint/auth-plan.ts +28 -0
  17. package/src/imprint/auth-verifier.ts +234 -20
  18. package/src/imprint/authentication-interaction.ts +137 -0
  19. package/src/imprint/backend-ladder.ts +271 -43
  20. package/src/imprint/build-plan.ts +2 -22
  21. package/src/imprint/cdp-browser-fetch.ts +242 -60
  22. package/src/imprint/chromium.ts +93 -29
  23. package/src/imprint/claude-cli-compile.ts +2 -0
  24. package/src/imprint/codex-cli-compile.ts +99 -46
  25. package/src/imprint/compile-agent-types.ts +9 -5
  26. package/src/imprint/compile-agent.ts +33 -0
  27. package/src/imprint/compile-tool-executor.ts +783 -0
  28. package/src/imprint/compile-tools.ts +42 -9
  29. package/src/imprint/compile-verification.ts +2 -0
  30. package/src/imprint/compile.ts +3 -0
  31. package/src/imprint/compiler-log.ts +16 -0
  32. package/src/imprint/component-recovery.ts +120 -0
  33. package/src/imprint/cookie-jar.ts +117 -7
  34. package/src/imprint/credential-extract.ts +9 -2
  35. package/src/imprint/credential-lock.ts +147 -0
  36. package/src/imprint/credential-observation.ts +261 -0
  37. package/src/imprint/credential-store.ts +284 -44
  38. package/src/imprint/live-component-recovery.ts +73 -0
  39. package/src/imprint/llm.ts +4 -3
  40. package/src/imprint/log.ts +16 -1
  41. package/src/imprint/login.ts +9 -0
  42. package/src/imprint/master-teach-agent-contracts.ts +40 -3
  43. package/src/imprint/master-teach-agents.ts +229 -14
  44. package/src/imprint/master-teach-checks.ts +8 -1
  45. package/src/imprint/master-teach-controller.ts +681 -138
  46. package/src/imprint/master-teach-prompt-projections.ts +2 -0
  47. package/src/imprint/mcp-cdp-sessions.ts +7 -2
  48. package/src/imprint/mcp-compile-server.ts +62 -850
  49. package/src/imprint/mcp-server.ts +667 -91
  50. package/src/imprint/mcp-session-recovery.ts +167 -0
  51. package/src/imprint/native-teach-agents.ts +232 -108
  52. package/src/imprint/runtime.ts +416 -47
  53. package/src/imprint/stealth-chromium.ts +67 -7
  54. package/src/imprint/teach-browser-resources.ts +88 -0
  55. package/src/imprint/teach-stage-progress.ts +38 -0
  56. package/src/imprint/terminal-user-prompt.ts +42 -0
  57. package/src/imprint/types.ts +51 -18
  58. 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 passphrase = await this.passphraseProvider();
262
- const key = deriveKey(passphrase, salt);
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
- writeFileSync(this.filePath, JSON.stringify(wire), 'utf8');
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(site: string): Promise<SiteCredentialView> {
665
- const backend = await getCredentialBackend();
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 (Object.keys(values).length === 0 && cookies.length === 0 && storage.length === 0) {
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
- return { site, cookies: legacy.cookies, values: legacy.values, storage: [] };
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
- return { site, cookies, values, storage };
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(site: string, cookies: CookieRecord[]): Promise<void> {
688
- const backend = await getCredentialBackend();
689
- await backend.setCookies(site, cookies);
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(site: string, storage: StorageRecord[]): Promise<void> {
694
- const backend = await getCredentialBackend();
695
- await backend.setStorage?.(site, storage);
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 backend = await getCredentialBackend();
707
- const previousCookies = await backend.getCookies(site);
708
- const previousSecrets = new Map<string, string | null>();
709
- for (const name of Object.keys(secrets)) {
710
- previousSecrets.set(name, await backend.getSecret(site, name));
711
- }
712
-
713
- try {
714
- await backend.setCookies(site, cookies);
715
- for (const [name, value] of Object.entries(secrets)) {
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
- } catch (err) {
719
- const rollbackFailures: string[] = [];
720
- try {
721
- await backend.setCookies(site, previousCookies);
722
- } catch (rollbackErr) {
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
- for (const [name, value] of previousSecrets) {
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
- if (value === null) await backend.deleteSecret(site, name);
730
- else await backend.setSecret(site, name, value);
946
+ await backend.setCookies(site, previousCookies);
731
947
  } catch (rollbackErr) {
732
948
  rollbackFailures.push(
733
- `${name}: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
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
- const original = err instanceof Error ? err.message : String(err);
738
- throw new Error(
739
- rollbackFailures.length > 0
740
- ? `${original}; rollback also failed for ${rollbackFailures.join(', ')}`
741
- : original,
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
+ }
@@ -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 },
@@ -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
- process.stderr.write(`${prefix} ${msg}\n`);
36
+ const line = `${prefix} ${msg}\n`;
37
+ if (promptDepth > 0) pendingLines.push(line);
38
+ else process.stderr.write(line);
24
39
  };
25
40
  }
@@ -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
- /** Completed request research for every selected operation. Planning runs
117
- * only after these handoffs exist so cross-tool dependencies can be chosen
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']),