@evomap/evolver-adapter-public 2.0.0-beta.19 → 2.0.0-beta.22

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.
@@ -99,6 +99,7 @@ export function collectIntegrityHashes(packageRoot = resolveAdapterPackageRoot()
99
99
  export function buildHeartbeatAntiAbuseTelemetry(opts = {}) {
100
100
  const env = opts.env ?? process.env;
101
101
  const fp = opts.envFingerprint ?? bootstrap.captureEnvFingerprint({ env });
102
+ const evolverVersion = bootstrap.normalizeEvolverVersion(Object.hasOwn(opts, 'evolverVersion') ? opts.evolverVersion : fp.evolver_version);
102
103
  const salt = opts.salt ?? env['EVOLVER_ANTI_ABUSE_SALT'];
103
104
  const saltId = opts.saltId ?? env['EVOLVER_ANTI_ABUSE_SALT_ID'] ?? (salt ? 'env' : null);
104
105
  const pseudonymStatus = salt ? 'salt_configured' : 'salt_missing';
@@ -159,7 +160,7 @@ export function buildHeartbeatAntiAbuseTelemetry(opts = {}) {
159
160
  // AUTHORITATIVE per-asset model lives on the capsule (threaded from input.model), so 'unknown' on a heartbeat
160
161
  // is "this node didn't say", never a contradiction of a capsule's real model.
161
162
  model: fp.model,
162
- evolver_version: opts.evolverVersion ?? null,
163
+ evolver_version: evolverVersion ?? null,
163
164
  client: '@evomap/evolver-adapter-public',
164
165
  client_version: null,
165
166
  region: fp.region ?? null,
@@ -1,4 +1,4 @@
1
- import { linkSync } from 'node:fs';
1
+ import { linkSync, renameSync } from 'node:fs';
2
2
  import type { hub } from '@evomap/evolver-core';
3
3
  export interface CredentialStoreOptions {
4
4
  /**
@@ -15,6 +15,8 @@ export interface CredentialStoreOptions {
15
15
  windowsParentStateReader?: (path: string) => readonly PathSecurityState[];
16
16
  /** Injectable filesystem primitive for deterministic no-clobber publication tests. */
17
17
  linkFile?: typeof linkSync;
18
+ /** Injectable filesystem primitive for deterministic atomic replacement tests. */
19
+ renameFile?: typeof renameSync;
18
20
  }
19
21
  export interface WindowsCredentialAclOps {
20
22
  assertTrustedParent(path: string, strictCreate: boolean): void;
@@ -24,7 +26,7 @@ export interface WindowsCredentialAclOps {
24
26
  secureFile(path: string): void;
25
27
  }
26
28
  export declare class CredentialStoreError extends Error {
27
- constructor(message: string);
29
+ constructor(message: string, options?: ErrorOptions);
28
30
  }
29
31
  interface FileIdentity {
30
32
  dev: bigint;
@@ -44,6 +46,7 @@ export declare class CredentialStore {
44
46
  private readonly darwinAclReader;
45
47
  private readonly windowsParentStateReader;
46
48
  private readonly linkFile;
49
+ private readonly renameFile;
47
50
  private securedDirectoryState;
48
51
  private securedCredentialState;
49
52
  private readonly securedAncestorStates;
@@ -2,11 +2,12 @@ import { randomBytes } from 'node:crypto';
2
2
  import { execFileSync } from 'node:child_process';
3
3
  import { constants, closeSync, fchmodSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
4
4
  import { basename, dirname, resolve, win32 } from 'node:path';
5
+ import { POWERSHELL_STDIN_SCRIPT_COMMAND, windowsAclFailureDetail, } from './windowsPowerShell.js';
5
6
  const DIRECTORY_MODE = 0o700;
6
7
  const FILE_MODE = 0o600;
7
8
  export class CredentialStoreError extends Error {
8
- constructor(message) {
9
- super(`Unsafe credential path: ${message}`);
9
+ constructor(message, options) {
10
+ super(`Unsafe credential path: ${message}`, options);
10
11
  this.name = 'CredentialStoreError';
11
12
  }
12
13
  }
@@ -18,6 +19,7 @@ export class CredentialStore {
18
19
  darwinAclReader;
19
20
  windowsParentStateReader;
20
21
  linkFile;
22
+ renameFile;
21
23
  // ctime detects in-place ACL drift while dev/ino detects entry replacement.
22
24
  securedDirectoryState = null;
23
25
  securedCredentialState = null;
@@ -32,6 +34,7 @@ export class CredentialStore {
32
34
  this.darwinAclReader = options.darwinAclReader ?? readDarwinAcl;
33
35
  this.windowsParentStateReader = options.windowsParentStateReader ?? parentSecurityStates;
34
36
  this.linkFile = options.linkFile ?? linkSync;
37
+ this.renameFile = options.renameFile ?? renameSync;
35
38
  }
36
39
  load() {
37
40
  if (!this.prepareDirectory(false))
@@ -152,7 +155,7 @@ export class CredentialStore {
152
155
  this.assertDirectoryIdentity(directory, directoryIdentity);
153
156
  if (replaceExisting) {
154
157
  this.assertSafeDestination();
155
- renameSync(temporaryPath, this.path);
158
+ this.renameFile(temporaryPath, this.path);
156
159
  }
157
160
  else if (!this.publishIfAbsent(temporaryPath)) {
158
161
  const incumbentFd = this.openCredentialFile();
@@ -392,8 +395,8 @@ export class CredentialStore {
392
395
  try {
393
396
  this.windowsAclOps.assertTrustedFile(this.path);
394
397
  }
395
- catch {
396
- throw new CredentialStoreError('Windows credential file ACL is not trusted');
398
+ catch (cause) {
399
+ throw windowsCredentialStoreError('Windows credential file ACL is not trusted', cause);
397
400
  }
398
401
  const pathStat = bigLstat(this.path);
399
402
  const fdStat = bigFstat(fd);
@@ -511,8 +514,8 @@ export class CredentialStore {
511
514
  try {
512
515
  this.windowsAclOps.secureDirectory(path);
513
516
  }
514
- catch {
515
- throw new CredentialStoreError('Windows directory ACL could not be secured');
517
+ catch (cause) {
518
+ throw windowsCredentialStoreError('Windows directory ACL could not be secured', cause);
516
519
  }
517
520
  const after = bigLstat(path);
518
521
  if (after.isSymbolicLink() || !after.isDirectory() || !sameIdentity(after, identity)) {
@@ -532,8 +535,8 @@ export class CredentialStore {
532
535
  try {
533
536
  this.windowsAclOps.assertTrustedFile(path);
534
537
  }
535
- catch {
536
- throw new CredentialStoreError('Windows credential file ACL is not trusted');
538
+ catch (cause) {
539
+ throw windowsCredentialStoreError('Windows credential file ACL is not trusted', cause);
537
540
  }
538
541
  if (isCredentialPath && this.securedCredentialState &&
539
542
  sameSecurityState(this.securedCredentialState, before))
@@ -541,8 +544,8 @@ export class CredentialStore {
541
544
  try {
542
545
  this.windowsAclOps.secureFile(path);
543
546
  }
544
- catch {
545
- throw new CredentialStoreError('Windows file ACL could not be secured');
547
+ catch (cause) {
548
+ throw windowsCredentialStoreError('Windows file ACL could not be secured', cause);
546
549
  }
547
550
  const pathStat = bigLstat(path);
548
551
  const fdStat = bigFstat(fd);
@@ -595,7 +598,8 @@ export class CredentialStore {
595
598
  throw new CredentialStoreError(`ancestor directory ${path} changed during ACL validation`);
596
599
  }
597
600
  for (let attempt = 0; attempt < 5; attempt += 1) {
598
- const initialState = securityStateOf(bigFstat(fd));
601
+ const initialMetadata = bigFstat(fd);
602
+ const initialState = securityStateOf(initialMetadata);
599
603
  let output;
600
604
  try {
601
605
  output = this.darwinAclReader(path);
@@ -612,12 +616,33 @@ export class CredentialStore {
612
616
  !sameIdentity(after, identity) || !sameIdentity(openedAfter, identity)) {
613
617
  throw new CredentialStoreError(`ancestor directory ${path} changed during ACL validation`);
614
618
  }
615
- const metadataStable = sameSecurityState(initialState, after) &&
616
- sameSecurityState(initialState, openedAfter);
617
- if (metadataStable) {
619
+ if (sameSecurityState(initialState, after) && sameSecurityState(initialState, openedAfter)) {
618
620
  this.securedAncestorStates.set(path, securityStateOf(openedAfter));
619
621
  return;
620
622
  }
623
+ if (sameDarwinAncestorMetadata(initialMetadata, after)
624
+ && sameDarwinAncestorMetadata(initialMetadata, openedAfter)) {
625
+ let confirmedOutput;
626
+ try {
627
+ confirmedOutput = this.darwinAclReader(path);
628
+ }
629
+ catch {
630
+ throw new CredentialStoreError(`ancestor directory ${path} ACL could not be inspected`);
631
+ }
632
+ if (hasUnsafeDarwinAllowAcl(confirmedOutput, rejectAnyAllow)) {
633
+ throw new CredentialStoreError(`ancestor directory ${path} grants access through an extended ACL`);
634
+ }
635
+ const confirmedPath = bigLstat(path);
636
+ const confirmedOpened = bigFstat(fd);
637
+ if (confirmedOutput === output
638
+ && !confirmedPath.isSymbolicLink()
639
+ && confirmedPath.isDirectory()
640
+ && sameDarwinAncestorMetadata(initialMetadata, confirmedPath)
641
+ && sameDarwinAncestorMetadata(initialMetadata, confirmedOpened)) {
642
+ this.securedAncestorStates.set(path, securityStateOf(confirmedOpened));
643
+ return;
644
+ }
645
+ }
621
646
  }
622
647
  throw new CredentialStoreError(`ancestor directory ${path} changed during ACL validation`);
623
648
  }
@@ -667,16 +692,58 @@ export class CredentialStore {
667
692
  this.trustedWindowsParentStates.set(cacheKey, after);
668
693
  }
669
694
  }
670
- catch {
671
- throw new CredentialStoreError('Windows parent directory chain is not trusted');
695
+ catch (cause) {
696
+ throw windowsCredentialStoreError('Windows parent directory chain is not trusted', cause);
672
697
  }
673
698
  }
674
699
  isPosix() {
675
700
  return this.platform !== 'win32';
676
701
  }
677
702
  }
703
+ function windowsCredentialStoreError(message, cause) {
704
+ const detail = cause instanceof Error ? windowsAclFailureDetail(cause) : '';
705
+ return new CredentialStoreError(detail ? `${message} (${detail})` : message, { cause });
706
+ }
678
707
  const WINDOWS_ACL_SCRIPT = String.raw `
679
708
  $ErrorActionPreference = 'Stop'
709
+ # Progress records can be serialized as CLIXML onto redirected stderr and
710
+ # obscure the actual failure. Suppress them so stderr carries only real errors.
711
+ $ProgressPreference = 'SilentlyContinue'
712
+
713
+ function ConvertTo-OneLineAclDiagnostic([object]$Value) {
714
+ if ($null -eq $Value) { return '' }
715
+ return ([string]$Value -replace '[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+', ' ').Trim()
716
+ }
717
+
718
+ function Throw-CredentialAclFailure(
719
+ [string]$Reason,
720
+ [string]$Path = '',
721
+ [string]$Sid = '',
722
+ [object]$Rights = $null,
723
+ [string]$Principal = ''
724
+ ) {
725
+ $parts = @($Reason)
726
+ $safePath = ConvertTo-OneLineAclDiagnostic $Path
727
+ $safeSid = ConvertTo-OneLineAclDiagnostic $Sid
728
+ $safeRights = ConvertTo-OneLineAclDiagnostic $Rights
729
+ $safePrincipal = ConvertTo-OneLineAclDiagnostic $Principal
730
+ if (-not [string]::IsNullOrWhiteSpace($safePath)) { $parts += ('path=' + $safePath) }
731
+ if (-not [string]::IsNullOrWhiteSpace($safeSid)) { $parts += ('sid=' + $safeSid) }
732
+ if (-not [string]::IsNullOrWhiteSpace($safePrincipal)) { $parts += ('principal=' + $safePrincipal) }
733
+ if (-not [string]::IsNullOrWhiteSpace($safeRights)) { $parts += ('rights=' + $safeRights) }
734
+ throw ($parts -join '; ')
735
+ }
736
+
737
+ # Windows PowerShell can serialize ordinary error-stream writes as CLIXML when
738
+ # stderr is redirected. Write the terminating exception directly to native stderr so
739
+ # Node receives the actionable message rather than only a serialized record.
740
+ trap {
741
+ $message = ConvertTo-OneLineAclDiagnostic $_.Exception.Message
742
+ if ([string]::IsNullOrWhiteSpace($message)) { $message = 'Credential ACL check failed' }
743
+ [Console]::Error.WriteLine($message)
744
+ exit 1
745
+ }
746
+
680
747
  $Target = [Environment]::GetEnvironmentVariable('EVOMAP_CREDENTIAL_ACL_TARGET', 'Process')
681
748
  $Kind = [Environment]::GetEnvironmentVariable('EVOMAP_CREDENTIAL_ACL_KIND', 'Process')
682
749
  if ([string]::IsNullOrEmpty($Target) -or
@@ -711,10 +778,10 @@ function Assert-TrustedParent([string]$ParentPath, [bool]$StrictCreate) {
711
778
  $full = [System.IO.Path]::GetFullPath($ParentPath)
712
779
  $root = [System.IO.Path]::GetPathRoot($full)
713
780
  if ([string]::IsNullOrEmpty($root) -or $root -notmatch '^[A-Za-z]:\\$') {
714
- throw 'Credential parent must be on a local drive'
781
+ Throw-CredentialAclFailure -Reason 'Credential parent must be on a local drive' -Path $full
715
782
  }
716
783
  if ([System.IO.DriveInfo]::new($root).DriveType -ne [System.IO.DriveType]::Fixed) {
717
- throw 'Credential parent must be on a fixed local drive'
784
+ Throw-CredentialAclFailure -Reason 'Credential parent must be on a fixed local drive' -Path $full
718
785
  }
719
786
  $current = $root
720
787
  $relative = $full.Substring($root.Length)
@@ -741,12 +808,12 @@ function Assert-TrustedParent([string]$ParentPath, [bool]$StrictCreate) {
741
808
  $item = Get-Item -LiteralPath $current -Force
742
809
  if (-not $item.PSIsContainer -or
743
810
  (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0)) {
744
- throw 'Credential parent contains a reparse point or non-directory'
811
+ Throw-CredentialAclFailure -Reason 'Credential parent contains a reparse point or non-directory' -Path $current
745
812
  }
746
813
  $parentAcl = Get-Acl -LiteralPath $current
747
814
  $ownerSid = $parentAcl.GetOwner([System.Security.Principal.SecurityIdentifier])
748
815
  if ($trustedSids -notcontains $ownerSid.Value) {
749
- throw 'Credential parent has an untrusted owner'
816
+ Throw-CredentialAclFailure -Reason 'Credential parent has an untrusted owner' -Path $current -Sid $ownerSid.Value
750
817
  }
751
818
  foreach ($parentRule in @($parentAcl.Access)) {
752
819
  if ($parentRule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) {
@@ -783,10 +850,10 @@ function Assert-TrustedParent([string]$ParentPath, [bool]$StrictCreate) {
783
850
  [System.Security.Principal.SecurityIdentifier]
784
851
  )
785
852
  } catch {
786
- throw 'Credential parent contains an unresolvable write principal'
853
+ Throw-CredentialAclFailure -Reason 'Credential parent contains an unresolvable write principal' -Path $current -Principal $parentRule.IdentityReference.Value -Rights $rights
787
854
  }
788
855
  if ($trustedSids -notcontains $parentRuleSid.Value) {
789
- throw 'Credential parent grants write access to an untrusted principal'
856
+ Throw-CredentialAclFailure -Reason 'Credential parent grants write access to an untrusted principal' -Path $current -Sid $parentRuleSid.Value -Rights $rights
790
857
  }
791
858
  }
792
859
  }
@@ -796,12 +863,12 @@ function Assert-TrustedFile([string]$FilePath) {
796
863
  $item = Get-Item -LiteralPath $FilePath -Force
797
864
  if ($item.PSIsContainer -or
798
865
  (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0)) {
799
- throw 'Credential file is a reparse point or not a regular file'
866
+ Throw-CredentialAclFailure -Reason 'Credential file is a reparse point or not a regular file' -Path $FilePath
800
867
  }
801
868
  $fileAcl = Get-Acl -LiteralPath $FilePath
802
869
  $ownerSid = $fileAcl.GetOwner([System.Security.Principal.SecurityIdentifier])
803
870
  if ($trustedSids -notcontains $ownerSid.Value) {
804
- throw 'Credential file has an untrusted owner'
871
+ Throw-CredentialAclFailure -Reason 'Credential file has an untrusted owner' -Path $FilePath -Sid $ownerSid.Value
805
872
  }
806
873
  foreach ($fileRule in @($fileAcl.Access)) {
807
874
  if ($fileRule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) {
@@ -822,10 +889,10 @@ function Assert-TrustedFile([string]$FilePath) {
822
889
  [System.Security.Principal.SecurityIdentifier]
823
890
  )
824
891
  } catch {
825
- throw 'Credential file contains an unresolvable write principal'
892
+ Throw-CredentialAclFailure -Reason 'Credential file contains an unresolvable write principal' -Path $FilePath -Principal $fileRule.IdentityReference.Value -Rights $rights
826
893
  }
827
894
  if ($trustedSids -notcontains $fileRuleSid.Value) {
828
- throw 'Credential file grants write access to an untrusted principal'
895
+ Throw-CredentialAclFailure -Reason 'Credential file grants write access to an untrusted principal' -Path $FilePath -Sid $fileRuleSid.Value -Rights $rights
829
896
  }
830
897
  }
831
898
  }
@@ -896,7 +963,7 @@ $access = [System.Security.AccessControl.FileSystemAccessRule]::new(
896
963
  Set-Acl -LiteralPath $Target -AclObject $acl
897
964
  $verified = Get-Acl -LiteralPath $Target
898
965
  if (-not (Test-CanonicalCredentialAcl $verified $sid $expectedInheritance)) {
899
- throw 'Credential ACL verification failed'
966
+ Throw-CredentialAclFailure -Reason 'Credential ACL verification failed' -Path $Target -Sid $sid.Value
900
967
  }
901
968
  `;
902
969
  class PowerShellWindowsAclOps {
@@ -923,23 +990,37 @@ class PowerShellWindowsAclOps {
923
990
  this.run(path, 'assert-file');
924
991
  }
925
992
  run(path, kind) {
926
- execFileSync(this.executable, [
927
- '-NoLogo',
928
- '-NoProfile',
929
- '-NonInteractive',
930
- '-ExecutionPolicy', 'Bypass',
931
- '-EncodedCommand', Buffer.from(WINDOWS_ACL_SCRIPT, 'utf16le').toString('base64'),
932
- ], {
933
- env: {
934
- SystemRoot: this.systemRoot,
935
- EVOMAP_CREDENTIAL_ACL_TARGET: path,
936
- EVOMAP_CREDENTIAL_ACL_KIND: kind,
937
- },
938
- shell: false,
939
- stdio: 'ignore',
940
- timeout: 15_000,
941
- windowsHide: true,
942
- });
993
+ try {
994
+ execFileSync(this.executable, [
995
+ '-NoLogo',
996
+ '-NoProfile',
997
+ '-NonInteractive',
998
+ '-ExecutionPolicy', 'Bypass',
999
+ // The fixed wrapper parses stdin once. Bare -Command - can execute
1000
+ // PowerShell 5.1 input statement by statement and mask an earlier error.
1001
+ '-Command', POWERSHELL_STDIN_SCRIPT_COMMAND,
1002
+ ], {
1003
+ encoding: 'utf8',
1004
+ env: {
1005
+ SystemRoot: this.systemRoot,
1006
+ EVOMAP_CREDENTIAL_ACL_TARGET: path,
1007
+ EVOMAP_CREDENTIAL_ACL_KIND: kind,
1008
+ },
1009
+ shell: false,
1010
+ // Capture both streams rather than discarding them: the script's own
1011
+ // message names which path level and which SID failed, and without it
1012
+ // every rejection is indistinguishable from "PowerShell is missing".
1013
+ // The script prints nothing on success, so this stays quiet normally.
1014
+ input: WINDOWS_ACL_SCRIPT,
1015
+ stdio: ['pipe', 'pipe', 'pipe'],
1016
+ timeout: 15_000,
1017
+ windowsHide: true,
1018
+ });
1019
+ }
1020
+ catch (cause) {
1021
+ const detail = windowsAclFailureDetail(cause);
1022
+ throw new Error(detail ? `${kind} check failed: ${detail}` : `${kind} check failed`, { cause });
1023
+ }
943
1024
  }
944
1025
  }
945
1026
  function readDarwinAcl(path) {
@@ -999,6 +1080,13 @@ function sameIdentity(left, right) {
999
1080
  function sameSecurityState(left, right) {
1000
1081
  return sameIdentity(left, right) && left.ctimeNs === right.ctimeNs;
1001
1082
  }
1083
+ function sameDarwinAncestorMetadata(left, right) {
1084
+ return sameIdentity(left, right)
1085
+ && right.isDirectory()
1086
+ && !right.isSymbolicLink()
1087
+ && left.uid === right.uid
1088
+ && left.mode === right.mode;
1089
+ }
1002
1090
  function samePathSecurityStates(left, right) {
1003
1091
  return left.length === right.length && left.every((state, index) => {
1004
1092
  const candidate = right[index];
@@ -13,8 +13,8 @@ export type NodeSecretVersionHandler = (nodeSecretVersion?: number) => void;
13
13
  */
14
14
  export type NodeSecretDivergenceHandler = () => void;
15
15
  /**
16
- * node_secret 双轨过渡(M6-5, 6 个月). 包旧 64-hex node_secret 成 AuthProvider.
17
- * **node_secret request body**(gep-a2a 契约, 实测 dev: requireNodeSecret body 非 header), 不轮换.
16
+ * LegacyAuthShim exposes node_secret as a transport-neutral credential field. HubFetch promotes it to
17
+ * Authorization: Bearer for GET and strict GEP envelope endpoints before egress.
18
18
  */
19
19
  export declare class LegacyAuthShim implements hub.AuthProvider {
20
20
  private nodeSecret;
@@ -13,8 +13,8 @@ export function parseNodeSecretVersion(value) {
13
13
  return Number.isSafeInteger(parsed) ? parsed : undefined;
14
14
  }
15
15
  /**
16
- * node_secret 双轨过渡(M6-5, 6 个月). 包旧 64-hex node_secret 成 AuthProvider.
17
- * **node_secret request body**(gep-a2a 契约, 实测 dev: requireNodeSecret body 非 header), 不轮换.
16
+ * LegacyAuthShim exposes node_secret as a transport-neutral credential field. HubFetch promotes it to
17
+ * Authorization: Bearer for GET and strict GEP envelope endpoints before egress.
18
18
  */
19
19
  export class LegacyAuthShim {
20
20
  nodeSecret;
@@ -0,0 +1,3 @@
1
+ export declare const POWERSHELL_STDIN_SCRIPT_COMMAND = "& ([scriptblock]::Create([Console]::In.ReadToEnd()))";
2
+ export declare function windowsAclFailureDetail(cause: unknown): string;
3
+ export declare function stripPowerShellClixml(text: string): string;
@@ -0,0 +1,91 @@
1
+ export const POWERSHELL_STDIN_SCRIPT_COMMAND = '& ([scriptblock]::Create([Console]::In.ReadToEnd()))';
2
+ export function windowsAclFailureDetail(cause) {
3
+ const error = cause;
4
+ const parts = [];
5
+ if (typeof error?.code === 'string' && error.code) {
6
+ parts.push('code ' + normalizePowerShellDiagnostic(error.code));
7
+ }
8
+ if (typeof error?.status === 'number')
9
+ parts.push('exit ' + error.status);
10
+ if (typeof error?.signal === 'string' && error.signal) {
11
+ parts.push('signal ' + normalizePowerShellDiagnostic(error.signal));
12
+ }
13
+ const streams = [error?.stderr, error?.stdout]
14
+ .map((stream) => (typeof stream === 'string'
15
+ ? stream
16
+ : Buffer.isBuffer(stream) ? stream.toString('utf8') : ''))
17
+ .map(stripPowerShellClixml)
18
+ .filter((text) => text.length > 0);
19
+ if (streams.length > 0) {
20
+ parts.push(streams.join(' | '));
21
+ }
22
+ else {
23
+ const message = typeof error?.message === 'string' ? error.message : '';
24
+ const detail = stripPowerShellClixml(message.replace(/-EncodedCommand\s+\S+/g, '-EncodedCommand <omitted>'));
25
+ if (detail)
26
+ parts.push(detail);
27
+ }
28
+ return boundPowerShellDiagnostic(parts.join('; '));
29
+ }
30
+ export function stripPowerShellClixml(text) {
31
+ if (/#<\s*CLIXML\b/i.test(text)) {
32
+ const errorRecords = [];
33
+ const serializedErrors = /<S\b(?=[^>]*\bS=\x22Error\x22)[^>]*>([\s\S]*?)<\/S>/gi;
34
+ for (const match of text.matchAll(serializedErrors)) {
35
+ const record = decodePowerShellClixmlText(match[1] ?? '');
36
+ const hasLocationBoilerplate = /(?:^|\s)At line:\d+ char:\d+/i.test(record);
37
+ const decoded = normalizePowerShellDiagnostic(stripPowerShellLocationBoilerplate(record));
38
+ if (decoded)
39
+ errorRecords.push(decoded);
40
+ if (hasLocationBoilerplate)
41
+ break;
42
+ }
43
+ if (errorRecords.length > 0)
44
+ return [...new Set(errorRecords)].join(' | ');
45
+ }
46
+ return normalizePowerShellDiagnostic(stripPowerShellLocationBoilerplate(text)
47
+ .replace(/<Objs\b[\s\S]*?<\/Objs>/g, ' ')
48
+ .replace(/<Objs\b[^\n]*/g, ' ')
49
+ .replace(/#<\s*CLIXML\b/gi, ' '));
50
+ }
51
+ function stripPowerShellLocationBoilerplate(text) {
52
+ return text.replace(/(?:^|\s)At line:\d+ char:\d+[\s\S]*$/i, ' ');
53
+ }
54
+ function decodePowerShellClixmlText(text) {
55
+ return text
56
+ .replace(/_x([0-9a-f]{4})_/gi, (_match, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
57
+ .replace(/&#x([0-9a-f]+);/gi, (match, value) => decodeXmlCodePoint(match, value, 16))
58
+ .replace(/&#([0-9]+);/g, (match, value) => decodeXmlCodePoint(match, value, 10))
59
+ .replace(/&lt;/g, '<')
60
+ .replace(/&gt;/g, '>')
61
+ .replace(/&quot;/g, String.fromCharCode(34))
62
+ .replace(/&apos;/g, String.fromCharCode(39))
63
+ .replace(/&amp;/g, '&');
64
+ }
65
+ function decodeXmlCodePoint(match, value, radix) {
66
+ const codePoint = Number.parseInt(value, radix);
67
+ if (!Number.isInteger(codePoint) || codePoint < 0 || codePoint > 0x10ffff ||
68
+ (codePoint >= 0xd800 && codePoint <= 0xdfff)) {
69
+ return match;
70
+ }
71
+ return String.fromCodePoint(codePoint);
72
+ }
73
+ const POWERSHELL_DIAGNOSTIC_FORMAT_CHARACTER = /^\p{Cf}$/u;
74
+ function normalizePowerShellDiagnostic(text) {
75
+ const sanitized = [];
76
+ for (const character of text) {
77
+ const codePoint = character.codePointAt(0) ?? 0;
78
+ const isControl = codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
79
+ const isFormat = POWERSHELL_DIAGNOSTIC_FORMAT_CHARACTER.test(character);
80
+ sanitized.push(isControl || isFormat || codePoint === 0x2028 || codePoint === 0x2029 ? ' ' : character);
81
+ }
82
+ return sanitized.join('').replace(/\s+/g, ' ').trim();
83
+ }
84
+ function boundPowerShellDiagnostic(text) {
85
+ const limit = 300;
86
+ if (text.length <= limit)
87
+ return text;
88
+ const headLength = Math.ceil((limit - 3) / 2);
89
+ const tailLength = limit - 3 - headLength;
90
+ return text.slice(0, headLength) + '...' + text.slice(-tailLength);
91
+ }
@@ -58,6 +58,9 @@ export interface AccountAssetListResult {
58
58
  hasMore: boolean;
59
59
  nextCursor?: string;
60
60
  }
61
+ export declare class MalformedAccountAssetPageError extends Error {
62
+ constructor();
63
+ }
61
64
  /** 完整 GEP-A2A 信封(实测 dev: publish/fetch/validate 等协议消息端点必须全信封, 非仅 protocol+message_type). */
62
65
  export declare function gepEnvelope(messageType: string, payload: unknown, options?: {
63
66
  messageId?: string;
@@ -142,12 +145,20 @@ export declare class PublicHubCapability implements hub.HubCapability {
142
145
  readonly auth: hub.AuthProvider;
143
146
  readonly recipes: hub.RecipeCapability;
144
147
  constructor(opts: PublicHubOptions);
148
+ private evolverVersionForWire;
149
+ private envFingerprintForWire;
145
150
  hello(opts: PublicHelloOptions): Promise<PublicHelloResult>;
146
151
  heartbeat(opts?: PublicHeartbeatOptions): Promise<PublicHeartbeatResult>;
147
152
  private heartbeatMeta;
148
153
  publish(bundle: hub.AssetRecord[], options?: hub.PublishOptions): Promise<hub.PublishReceipt>;
149
154
  fetch(query: hub.HubQuery): Promise<hub.AssetRecord[]>;
150
- fetchAssetById(assetId: string): Promise<hub.AssetRecord | null>;
155
+ /**
156
+ * Fetch one asset AND say why, when the answer is not an asset. `fetchAssetById` collapses every outcome to
157
+ * `null`, so a caller could not tell "the hub does not have this" from "the hub delivered something the
158
+ * client refuses" — and the CLI reported both as `not_found` on assets that demonstrably exist (#964).
159
+ */
160
+ fetchAssetDeliveryById(assetId: string, options?: hub.FetchAssetByIdOptions): Promise<hub.AssetDeliveryOutcome>;
161
+ fetchAssetById(assetId: string, options?: hub.FetchAssetByIdOptions): Promise<hub.AssetRecord | null>;
151
162
  /**
152
163
  * #69: search != fetch. Free-text is the hub's vector endpoint (GET /a2a/assets/semantic-search?q=);
153
164
  * signal/id queries use the Hub's free search-only phase on /a2a/fetch. /a2a/fetch does NOT do semantic,
@@ -183,6 +194,8 @@ export declare class PublicHubCapability implements hub.HubCapability {
183
194
  createRecipe(request: hub.RecipeCreateRequest): Promise<hub.RecipeReceipt>;
184
195
  publishRecipe(recipeId: string, options?: hub.RecipePublishOptions): Promise<hub.RecipeReceipt>;
185
196
  getRecipe(recipeId: string): Promise<hub.RecipeFetchReceipt>;
197
+ searchRecipes(request?: hub.RecipeSearchRequest): Promise<hub.RecipeSearchReceipt>;
198
+ listRecipes(request?: hub.RecipeSearchRequest): Promise<hub.RecipeSearchReceipt>;
186
199
  expressRecipe(recipeId: string, request?: hub.RecipeExpressRequest): Promise<hub.RecipeExpressionReceipt>;
187
200
  task: {
188
201
  claim: (taskId: string) => Promise<{