@peakflo/agent-harness 0.2.13 → 0.2.14-rc.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.
@@ -114,15 +114,23 @@ class ControlPlaneClient {
114
114
  getRunnerId() {
115
115
  return this.runnerId;
116
116
  }
117
- /** Diagnostics must never delay a turn. The caller handles any failure. */
118
- async reportColdStart(line) {
117
+ /**
118
+ * All cold-start timing events of one turn in ONE request. Diagnostics
119
+ * must never delay a turn; the caller handles any failure.
120
+ *
121
+ * This replaced one POST per step. The control plane accepts both shapes,
122
+ * so an older harness on an existing sandbox keeps working.
123
+ */
124
+ async reportColdStartBatch(body) {
119
125
  if (!this.runnerId || !this.runnerCredential)
120
126
  return;
127
+ if (body.events.length === 0)
128
+ return;
121
129
  await this.send({
122
130
  method: 'POST',
123
131
  path: contract_1.ROUTES.coldStart(this.runnerId),
124
132
  token: this.runnerCredential,
125
- body: JSON.parse(line),
133
+ body,
126
134
  timeoutMs: 2_000
127
135
  });
128
136
  }
@@ -3,6 +3,9 @@ export interface GitHubProxyCertificateOptions {
3
3
  /** Override only for deterministic tests. Production preserves process.env. */
4
4
  environment?: NodeJS.ProcessEnv;
5
5
  }
6
+ export declare function ambientCaCertificates(environment: NodeJS.ProcessEnv, outputPath: string): Promise<Buffer[]>;
7
+ /** Node's bundled public roots, PEM, newline-terminated. */
8
+ export declare function publicRootCertificates(): string;
6
9
  export interface GitHubProxyCertificates {
7
10
  caCertificate: Buffer;
8
11
  caCertificatePath: string;
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ambientCaCertificates = ambientCaCertificates;
4
+ exports.publicRootCertificates = publicRootCertificates;
3
5
  exports.createGitHubProxyCertificates = createGitHubProxyCertificates;
4
6
  require("reflect-metadata");
5
7
  const node_crypto_1 = require("node:crypto");
@@ -54,6 +56,10 @@ async function ambientCaCertificates(environment, outputPath) {
54
56
  }
55
57
  return certificates;
56
58
  }
59
+ /** Node's bundled public roots, PEM, newline-terminated. */
60
+ function publicRootCertificates() {
61
+ return `${node_tls_1.rootCertificates.join('\n')}\n`;
62
+ }
57
63
  function validity() {
58
64
  const now = Date.now();
59
65
  return {
@@ -103,7 +109,7 @@ async function createGitHubProxyCertificates(stateRoot, options = {}) {
103
109
  const caCertificate = Buffer.from(ca.toString('pem'));
104
110
  const caCertificatePath = (0, node_path_1.join)(root, CA_CERTIFICATE_FILE);
105
111
  const ambientRoots = await ambientCaCertificates(options.environment ?? process.env, caCertificatePath);
106
- const publicRoots = `${node_tls_1.rootCertificates.join('\n')}\n`;
112
+ const publicRoots = publicRootCertificates();
107
113
  await (0, promises_1.writeFile)(caCertificatePath, Buffer.concat([...ambientRoots, Buffer.from(publicRoots), caCertificate]), { mode: 0o644 });
108
114
  await (0, promises_1.chmod)(caCertificatePath, 0o644);
109
115
  const leaves = new Map();
@@ -11,6 +11,12 @@ export interface GitHubWorkspaceOptions {
11
11
  /** Injected in tests: the proxy's peer-uid lookup. See `GitHubTlsProxy`. */
12
12
  peerUid?: PeerUidResolver;
13
13
  }
14
+ /**
15
+ * Hostnames of URLs the agent reaches WITHOUT a proxy (control plane, model
16
+ * gateway, MCP). Shared with the credential proxy's NO_PROXY so both proxies
17
+ * bypass exactly the same hosts.
18
+ */
19
+ export declare function directHostnames(urls: string[]): string[];
14
20
  /** Bare clone plus one worktree per session, adapted from 20x. */
15
21
  export declare class GitHubWorkspaceProvider implements WorkspaceProvider {
16
22
  private readonly options;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GitHubWorkspaceProvider = void 0;
4
+ exports.directHostnames = directHostnames;
4
5
  const node_child_process_1 = require("node:child_process");
5
6
  const node_crypto_1 = require("node:crypto");
6
7
  const node_fs_1 = require("node:fs");
@@ -17,6 +18,25 @@ const DEFAULT_PROXY_PORT = 7780;
17
18
  const LINK_SECRET_FILE = '.github-proxy-link-secret';
18
19
  const WIP_PUSH_FAILED_FILE = '.worktree-wip-push-failed';
19
20
  const WIP_COMMIT_MESSAGE = 'chore(agent): wip — uncommitted at turn end';
21
+ /**
22
+ * Hostnames of URLs the agent reaches WITHOUT a proxy (control plane, model
23
+ * gateway, MCP). Shared with the credential proxy's NO_PROXY so both proxies
24
+ * bypass exactly the same hosts.
25
+ */
26
+ function directHostnames(urls) {
27
+ const hosts = [];
28
+ for (const value of urls) {
29
+ if (!value)
30
+ continue;
31
+ try {
32
+ hosts.push(new URL(value).hostname.toLowerCase());
33
+ }
34
+ catch {
35
+ /* The component that owns an invalid URL reports it. */
36
+ }
37
+ }
38
+ return hosts;
39
+ }
20
40
  /** Bare clone plus one worktree per session, adapted from 20x. */
21
41
  class GitHubWorkspaceProvider {
22
42
  options;
@@ -46,16 +66,8 @@ class GitHubWorkspaceProvider {
46
66
  this.rememberDirectUrls(options.directUrls ?? []);
47
67
  }
48
68
  rememberDirectUrls(urls) {
49
- for (const value of urls) {
50
- if (!value)
51
- continue;
52
- try {
53
- this.directHosts.add(new URL(value).hostname.toLowerCase());
54
- }
55
- catch {
56
- /* The component that owns an invalid URL reports it. */
57
- }
58
- }
69
+ for (const host of directHostnames(urls))
70
+ this.directHosts.add(host);
59
71
  }
60
72
  runGit(args, options = {}) {
61
73
  const { environment, ...execOptions } = options;
package/dist/harness.d.ts CHANGED
@@ -249,6 +249,8 @@ export declare class Harness {
249
249
  private readonly activeLeaseIds;
250
250
  /** Renewal workers that must receive a credential rotation in flight. */
251
251
  private readonly activeRenewals;
252
+ /** Cold-start timing buffered per running turn; sent once at turn end. */
253
+ private readonly coldStartBatches;
252
254
  /** commandIds already applied. `SessionCommand` is idempotent by contract. */
253
255
  private readonly appliedCommands;
254
256
  /** Prune lease-bound command IDs when their session turn ends. */
@@ -473,6 +475,7 @@ export declare class Harness {
473
475
  private observeSandboxToolsPin;
474
476
  /** Stop once. A failed install is then the supervisor's terminal decision. */
475
477
  private requestUpgrade;
478
+ private newColdStartBatch;
476
479
  /**
477
480
  * Run one leased turn.
478
481
  *
package/dist/harness.js CHANGED
@@ -18,6 +18,7 @@ const health_1 = require("./health");
18
18
  const model_probe_1 = require("./model-probe");
19
19
  const ports_default_1 = require("./ports-default");
20
20
  const browser_takeover_1 = require("./runtime/browser-takeover");
21
+ const cold_start_batch_1 = require("./runtime/cold-start-batch");
21
22
  const knowledge_tools_1 = require("./runtime/knowledge-tools");
22
23
  const lease_renewal_1 = require("./runtime/lease-renewal");
23
24
  const mcp_auth_proxy_1 = require("./runtime/mcp-auth-proxy");
@@ -200,7 +201,9 @@ class Harness {
200
201
  mcpCheckState = {
201
202
  checked: new Map(),
202
203
  failed: new Map(),
203
- shownNotices: new Set()
204
+ shownNotices: new Set(),
205
+ // Item 1(b): per-session MCP tool listings, reused on warm turns.
206
+ toolsets: new Map()
204
207
  };
205
208
  startedAt = Date.now();
206
209
  status = 'starting';
@@ -265,6 +268,8 @@ class Harness {
265
268
  activeLeaseIds = new Map();
266
269
  /** Renewal workers that must receive a credential rotation in flight. */
267
270
  activeRenewals = new Set();
271
+ /** Cold-start timing buffered per running turn; sent once at turn end. */
272
+ coldStartBatches = new Set();
268
273
  /** commandIds already applied. `SessionCommand` is idempotent by contract. */
269
274
  appliedCommands = new Set();
270
275
  /** Prune lease-bound command IDs when their session turn ends. */
@@ -515,6 +520,11 @@ class Harness {
515
520
  for (const controller of this.active.values()) {
516
521
  controller.abort();
517
522
  }
523
+ // Send buffered turn timing while the credential is still valid.
524
+ // Deregistration below revokes it. Each send has a 2 s timeout.
525
+ const coldStarts = Array.from(this.coldStartBatches);
526
+ this.coldStartBatches.clear();
527
+ await Promise.all(coldStarts.map((batch) => batch.flush()));
518
528
  /*
519
529
  * A DURABLE RUNNER DOES NOT DEREGISTER.
520
530
  *
@@ -1565,7 +1575,9 @@ class Harness {
1565
1575
  durationMs: Date.now() - pollStartedAt
1566
1576
  });
1567
1577
  log(leaseReceived);
1568
- void this.client.reportColdStart(leaseReceived).catch(() => undefined);
1578
+ // Buffered, not sent: the whole turn's timing goes out as ONE request.
1579
+ const coldStart = this.newColdStartBatch(lease);
1580
+ coldStart.add(leaseReceived);
1569
1581
  this.observeHarnessPin(lease.harnessVersion);
1570
1582
  this.observeSandboxToolsPin(lease.sandboxToolsVersion);
1571
1583
  // Protocol 3 uses a short visibility window until the runner confirms
@@ -1593,10 +1605,12 @@ class Harness {
1593
1605
  }
1594
1606
  }
1595
1607
  if (!leaseAcknowledged) {
1608
+ this.coldStartBatches.delete(coldStart);
1609
+ void coldStart.flush();
1596
1610
  await sleep(LEASE_ERROR_BACKOFF_MS);
1597
1611
  continue;
1598
1612
  }
1599
- void this.runLeasedTurn(lease);
1613
+ void this.runLeasedTurn(lease, coldStart);
1600
1614
  }
1601
1615
  await this.stop();
1602
1616
  }
@@ -1680,6 +1694,11 @@ class Harness {
1680
1694
  this.fatal = new UpgradeRequestedError(harnessVersion);
1681
1695
  await this.stop({ preserveRegistration: true });
1682
1696
  }
1697
+ newColdStartBatch(lease) {
1698
+ const batch = new cold_start_batch_1.ColdStartBatch(lease.session.sessionId, lease.session.turnId, (body) => this.client.reportColdStartBatch(body));
1699
+ this.coldStartBatches.add(batch);
1700
+ return batch;
1701
+ }
1683
1702
  /**
1684
1703
  * Run one leased turn.
1685
1704
  *
@@ -1688,7 +1707,7 @@ class Harness {
1688
1707
  * throws must not end the lease loop, or one bad session would take the whole
1689
1708
  * runner offline.
1690
1709
  */
1691
- async runLeasedTurn(lease) {
1710
+ async runLeasedTurn(lease, coldStart = this.newColdStartBatch(lease)) {
1692
1711
  const { sessionId } = lease.session;
1693
1712
  // The exact loopback-proxy targets this session will reach. Keyed on these
1694
1713
  // urls, this session's MCP token cannot be overwritten or revoked by a
@@ -1852,9 +1871,9 @@ class Harness {
1852
1871
  secrets: this.secrets,
1853
1872
  events: this.events,
1854
1873
  telemetry: this.telemetry,
1855
- coldStartLog: (line) => {
1856
- void this.client.reportColdStart(line).catch(() => undefined);
1857
- },
1874
+ coldStartLog: (line) => coldStart.add(line),
1875
+ runtime: this.options.runtime,
1876
+ directUrls: [this.options.controlPlaneUrl],
1858
1877
  notifier: this.notifier,
1859
1878
  artifacts: this.artifacts,
1860
1879
  conversation: this.conversation,
@@ -1938,6 +1957,10 @@ class Harness {
1938
1957
  finally {
1939
1958
  renewal.stop();
1940
1959
  this.activeRenewals.delete(renewal);
1960
+ // ONE telemetry request for the whole turn, sent before the git
1961
+ // preservation below so slow pushes do not delay it. Fire-and-forget.
1962
+ this.coldStartBatches.delete(coldStart);
1963
+ void coldStart.flush();
1941
1964
  let workProtected = true;
1942
1965
  if ((this.options.autoCommitDirtyWorktree ??
1943
1966
  this.options.runtime === 'sandbox') &&
package/dist/index.d.ts CHANGED
@@ -59,4 +59,4 @@ export * from './adapters';
59
59
  * "ephemeral" means. A rule written twice is a rule that drifts.
60
60
  */
61
61
  export { CONTRACT_ERROR_CODES, DEFAULT_BASE_DELAY_MS, DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_LEASE_TTL_MS, DEFAULT_LEASE_WAIT_SECONDS, DEFAULT_MAX_ATTEMPTS, EPHEMERAL_PART_TYPES, FINAL_HTTP_STATUSES, HARNESS_UPGRADE_EXIT_CODE, MAX_INGEST_PARTS, MAX_INGEST_PART_BYTES, MAX_LEASE_WAIT_SECONDS, MIN_SUPPORTED_PROTOCOL_VERSION, MISSED_HEARTBEATS_BEFORE_OFFLINE, PERSISTED_PART_TYPES, PROTOCOL_VERSION, PROTOCOL_VERSION_HEADER, ROUTES, ingestPartByteLength, isEphemeralPartType, isFinalStatus, isPersistablePart, isProtocolSupported } from './contract';
62
- export type { BootstrapRequest, BootstrapResponse, ControlPlaneErrorBody, CredentialRotateRequest, CredentialRotateResponse, DeregisterRequest, DeregisterResponse, HarnessOptions, HealthReport, IngestRejection, IngestRejectionReason, IngestRequest, IngestResponse, JsonValue, LeaseRenewRequest, LeaseRenewResponse, LeaseResponse, LeasedAgent, LeasedAttachment, LeasedLimits, LeasedMcpServer, LeasedModel, LeasedSession, LeasedSkill, LeasedTurnInput, LeasedWorkspace, LeasedGitHubCredential, LeasedGitHubRepository, ProtocolEnvelope, RunnerBackend, RunnerCapabilities, RunnerHeartbeatRequest, RunnerHeartbeatResponse, RunnerModel, RunnerRegisterRequest, RunnerRegisterResponse, RunnerRuntime, RunnerStatus, SessionCommand, TranscriptPart, TranscriptPartInput, TurnMetadata } from './types';
62
+ export type { BootstrapRequest, BootstrapResponse, ControlPlaneErrorBody, CredentialRotateRequest, CredentialRotateResponse, DeregisterRequest, DeregisterResponse, HarnessOptions, HealthReport, IngestRejection, IngestRejectionReason, IngestRequest, IngestResponse, JsonValue, LeaseRenewRequest, LeaseRenewResponse, LeaseResponse, LeasedAgent, LeasedAttachment, LeasedCredentialProxy, LeasedLimits, LeasedMcpServer, LeasedModel, LeasedSession, LeasedSkill, LeasedTurnInput, LeasedWorkspace, LeasedGitHubCredential, LeasedGitHubRepository, ProtocolEnvelope, RunnerBackend, RunnerCapabilities, RunnerHeartbeatRequest, RunnerHeartbeatResponse, RunnerModel, RunnerRegisterRequest, RunnerRegisterResponse, RunnerRuntime, RunnerStatus, SessionCommand, TranscriptPart, TranscriptPartInput, TurnMetadata } from './types';
@@ -0,0 +1,36 @@
1
+ /**
2
+ * One cold-start telemetry POST per turn instead of one per step.
3
+ *
4
+ * `ColdStartSteps` and the Pi adapter emit 13–15 timing lines per turn. Each
5
+ * used to become its own `POST /runners/:id/cold-start` (auth + turn lookup on
6
+ * the server every time). This buffer collects the lines for ONE turn and the
7
+ * harness sends them as a single batch when the turn ends, or when the
8
+ * harness stops. It is diagnostics only: it never throws, never blocks a turn
9
+ * and never retries.
10
+ */
11
+ /** Events the control plane records. Anything else stays in the local log. */
12
+ export declare const COLD_START_EVENTS: ReadonlySet<string>;
13
+ /** Mirrors the server limit in `services/coding-agents/cold-start-telemetry.ts`. */
14
+ export declare const COLD_START_BATCH_MAX_EVENTS = 64;
15
+ export interface ColdStartBatchBody {
16
+ sessionId: string;
17
+ turnId: string;
18
+ events: Array<Record<string, unknown>>;
19
+ }
20
+ export declare class ColdStartBatch {
21
+ readonly sessionId: string;
22
+ readonly turnId: string;
23
+ private readonly send;
24
+ /** Lines that arrive after the turn-end flush are coalesced this long. */
25
+ private readonly lateFlushMs;
26
+ private events;
27
+ private flushed;
28
+ private lateTimer;
29
+ constructor(sessionId: string, turnId: string, send: (body: ColdStartBatchBody) => Promise<void>,
30
+ /** Lines that arrive after the turn-end flush are coalesced this long. */
31
+ lateFlushMs?: number);
32
+ /** Buffer one JSON line from `ColdStartSteps`. Never throws. */
33
+ add(line: string): void;
34
+ /** Send everything buffered as ONE request. Resolves; never rejects. */
35
+ flush(): Promise<void>;
36
+ }
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ /**
3
+ * One cold-start telemetry POST per turn instead of one per step.
4
+ *
5
+ * `ColdStartSteps` and the Pi adapter emit 13–15 timing lines per turn. Each
6
+ * used to become its own `POST /runners/:id/cold-start` (auth + turn lookup on
7
+ * the server every time). This buffer collects the lines for ONE turn and the
8
+ * harness sends them as a single batch when the turn ends, or when the
9
+ * harness stops. It is diagnostics only: it never throws, never blocks a turn
10
+ * and never retries.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.ColdStartBatch = exports.COLD_START_BATCH_MAX_EVENTS = exports.COLD_START_EVENTS = void 0;
14
+ /** Events the control plane records. Anything else stays in the local log. */
15
+ exports.COLD_START_EVENTS = new Set([
16
+ 'cold_start_step',
17
+ 'cold_start_summary',
18
+ 'model_catalogue_deferred'
19
+ ]);
20
+ /** Mirrors the server limit in `services/coding-agents/cold-start-telemetry.ts`. */
21
+ exports.COLD_START_BATCH_MAX_EVENTS = 64;
22
+ class ColdStartBatch {
23
+ sessionId;
24
+ turnId;
25
+ send;
26
+ lateFlushMs;
27
+ events = [];
28
+ flushed = false;
29
+ lateTimer = null;
30
+ constructor(sessionId, turnId, send,
31
+ /** Lines that arrive after the turn-end flush are coalesced this long. */
32
+ lateFlushMs = 1_000) {
33
+ this.sessionId = sessionId;
34
+ this.turnId = turnId;
35
+ this.send = send;
36
+ this.lateFlushMs = lateFlushMs;
37
+ }
38
+ /** Buffer one JSON line from `ColdStartSteps`. Never throws. */
39
+ add(line) {
40
+ try {
41
+ const event = JSON.parse(line);
42
+ if (!event || typeof event !== 'object' || Array.isArray(event))
43
+ return;
44
+ const record = event;
45
+ if (typeof record.event !== 'string' ||
46
+ !exports.COLD_START_EVENTS.has(record.event) ||
47
+ record.sessionId !== this.sessionId ||
48
+ record.turnId !== this.turnId) {
49
+ return;
50
+ }
51
+ // The batch carries the ids once; per-event copies are redundant bytes.
52
+ const rest = { ...record };
53
+ delete rest.sessionId;
54
+ delete rest.turnId;
55
+ this.events.push(rest);
56
+ if (this.events.length >= exports.COLD_START_BATCH_MAX_EVENTS) {
57
+ void this.flush();
58
+ }
59
+ else if (this.flushed && !this.lateTimer) {
60
+ // A step that ends after the turn-end flush (rare) is still sent,
61
+ // coalesced with any others that follow it.
62
+ this.lateTimer = setTimeout(() => {
63
+ this.lateTimer = null;
64
+ void this.flush();
65
+ }, this.lateFlushMs);
66
+ this.lateTimer.unref?.();
67
+ }
68
+ }
69
+ catch {
70
+ // A diagnostic sink must never stop an agent turn.
71
+ }
72
+ }
73
+ /** Send everything buffered as ONE request. Resolves; never rejects. */
74
+ flush() {
75
+ this.flushed = true;
76
+ if (this.lateTimer) {
77
+ clearTimeout(this.lateTimer);
78
+ this.lateTimer = null;
79
+ }
80
+ if (this.events.length === 0)
81
+ return Promise.resolve();
82
+ const events = this.events;
83
+ this.events = [];
84
+ try {
85
+ return this.send({
86
+ sessionId: this.sessionId,
87
+ turnId: this.turnId,
88
+ events
89
+ }).catch(() => undefined);
90
+ }
91
+ catch {
92
+ return Promise.resolve();
93
+ }
94
+ }
95
+ }
96
+ exports.ColdStartBatch = ColdStartBatch;
@@ -0,0 +1,44 @@
1
+ import type { LeasedCredentialProxy, RunnerRuntime } from '../types';
2
+ export declare const CREDENTIAL_PROXY_CA_BUNDLE_FILE = ".pf-credential-proxy-ca.pem";
3
+ export declare const CREDENTIAL_PROXY_HEALTH_TIMEOUT_MS = 1000;
4
+ export declare const CREDENTIAL_PROXY_TOKEN_REDACTION = "[credential-proxy-token]";
5
+ /** Every key this module may add or replace. Nothing else is ever touched. */
6
+ export declare const CREDENTIAL_PROXY_ENV_KEYS: readonly ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy", "NO_PROXY", "no_proxy", "SSL_CERT_FILE", "CURL_CA_BUNDLE", "NODE_EXTRA_CA_CERTS", "GIT_SSL_CAINFO", "REQUESTS_CA_BUNDLE", "PF_CREDENTIAL_PROXY"];
7
+ export type CredentialProxyUnavailableReason = 'not_offered' | 'runtime_not_sandbox' | 'vendor_backend' | 'invalid_descriptor' | 'health_check_failed' | 'ca_unreadable' | 'bundle_write_failed';
8
+ export interface CredentialProxyEnvironmentInput {
9
+ /** The HARNESS's own runtime (its CLI/env option), not the lease's claim. */
10
+ harnessRuntime: RunnerRuntime | undefined;
11
+ /** The lease's session runtime. Both must say `sandbox`. */
12
+ sessionRuntime: RunnerRuntime | string | undefined;
13
+ backend: string;
14
+ sessionId: string;
15
+ /** Session directory; the merged CA bundle is written here, 0644. */
16
+ sessionDir: string;
17
+ descriptor: LeasedCredentialProxy | undefined | null;
18
+ /** The environment the turn would get today (GitHub's, or undefined). */
19
+ environment: Record<string, string> | undefined;
20
+ /** Hosts the agent reaches directly (gateway, MCP, control plane...). */
21
+ directUrls: string[];
22
+ logger: Pick<Console, 'log' | 'warn'>;
23
+ /** Tests only. Production reads process.env, like the GitHub bundle. */
24
+ ambientEnvironment?: NodeJS.ProcessEnv;
25
+ }
26
+ export interface CredentialProxyEnvironmentResult {
27
+ environment: Record<string, string> | undefined;
28
+ /** Non-null exactly when the proxy variables were applied. */
29
+ token: string | null;
30
+ reason: CredentialProxyUnavailableReason | null;
31
+ }
32
+ /** TCP connect to 127.0.0.1:port; true when it connects within the timeout. */
33
+ export declare function probeLoopbackPort(port: number, timeoutMs?: number): Promise<boolean>;
34
+ /** The GitHub proxy's loopback port, read off the env GitHub already built. */
35
+ export declare function githubProxyPort(environment: Record<string, string> | undefined): number | null;
36
+ export declare function credentialProxyEnvironment(input: CredentialProxyEnvironmentInput): Promise<CredentialProxyEnvironmentResult>;
37
+ /** Replace every occurrence of `token` in a string. No-op without a token. */
38
+ export declare function redactCredentialProxyToken(text: string, token: string | null): string;
39
+ /**
40
+ * Same, for a JSON-shaped value (transcript parts, turn metadata). The token
41
+ * alphabet needs no JSON escaping, so a string split over the serialised form
42
+ * finds every occurrence in every nested string.
43
+ */
44
+ export declare function redactCredentialProxyTokenDeep<T>(value: T, token: string | null): T;