@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.
@@ -21,10 +21,11 @@
21
21
  import type { CodingAgentAdapter } from '../adapters';
22
22
  import { SessionModelGateway } from '../adapters/model-gateway';
23
23
  import { ArtifactInspector, ConversationStore, EventSink, ModelMessageRecord, Notifier, SecretProvider, Telemetry, TranscriptStore, WorkspaceProvider } from '../ports';
24
- import { LeaseResponse, LeasedTurnInput, ModelMessageInput, TranscriptPartInput, TurnMetadata } from '../types';
24
+ import { LeaseResponse, LeasedTurnInput, ModelMessageInput, RunnerRuntime, TranscriptPartInput, TurnMetadata } from '../types';
25
25
  import { KnowledgeToolHost, type KnowledgeControl } from './knowledge-tools';
26
26
  import { McpAuthProxy } from './mcp-auth-proxy';
27
27
  import { type FailedMcpCheck } from './mcp-preflight';
28
+ import { type McpToolStore } from './mcp-tool-store';
28
29
  export declare function approvalTimeoutMs(value: string | undefined): number;
29
30
  export interface SessionRunnerDeps {
30
31
  /** Resolves a backend name from the lease to a working adapter. */
@@ -46,6 +47,13 @@ export interface SessionRunnerDeps {
46
47
  telemetry: Telemetry;
47
48
  /** Send a structured timing event to the control plane without waiting. */
48
49
  coldStartLog?: (line: string) => void;
50
+ /**
51
+ * The HARNESS's own runtime. Only `sandbox` may route the agent through the
52
+ * credential proxy, whatever the lease says; absent means "not sandbox".
53
+ */
54
+ runtime?: RunnerRuntime;
55
+ /** Harness-level hosts the agent reaches directly (the control plane). */
56
+ directUrls?: string[];
49
57
  notifier: Notifier;
50
58
  artifacts: ArtifactInspector;
51
59
  /**
@@ -319,6 +327,12 @@ export interface McpCheckState {
319
327
  checked: Map<string, number>;
320
328
  failed: Map<string, FailedMcpCheck>;
321
329
  shownNotices: Set<string>;
330
+ /**
331
+ * Item 1(b): each session's last MCP tool listings, reused on a warm turn
332
+ * while the toolset fingerprint is unchanged. See `mcp-tool-store.ts`.
333
+ * Created on first use when a caller's state predates it.
334
+ */
335
+ toolsets?: McpToolStore;
322
336
  }
323
337
  export declare class SessionRunner {
324
338
  private readonly deps;
@@ -399,6 +413,12 @@ export declare class SessionRunner {
399
413
  private browserPauseStartedAt;
400
414
  /** When the last browser pause ended; the idle clock restarts from here. */
401
415
  private browserPauseEndedAt;
416
+ /**
417
+ * The credential-proxy token in force for this turn, kept ONLY so it can be
418
+ * scrubbed from everything this runner sends or logs. Not a credential
419
+ * value (see `LeaseResponse.credentialProxy`); null when the proxy is off.
420
+ */
421
+ private credentialProxyToken;
402
422
  constructor(deps: SessionRunnerDeps);
403
423
  /**
404
424
  * True while a permission prompt is outstanding. Read by the harness to
@@ -636,6 +656,8 @@ export declare class SessionRunner {
636
656
  * old Space's copies. Never throws; a failed sync is logged and the turn runs.
637
657
  */
638
658
  private syncSpaceFilesForTurn;
659
+ /** URLs this session's agent reaches without a proxy. */
660
+ private sessionDirectUrls;
639
661
  /**
640
662
  * Build the backend's session config.
641
663
  *
@@ -652,5 +674,23 @@ export declare class SessionRunner {
652
674
  * security perimeter and trusting it to filter its own tools is exactly
653
675
  * defect G2.
654
676
  */
677
+ /**
678
+ * The leased toolset as it enters the fingerprint: every server's name,
679
+ * upstream URL and allowlist, plus the knowledge server when bound.
680
+ */
681
+ private mcpToolsetMembers;
682
+ /**
683
+ * The listings the backend may seed from (item 4, and item 1(b) for stored
684
+ * ones). Only a server that is still configured and was listed at its
685
+ * current url is kept.
686
+ */
687
+ private mcpToolSnapshot;
688
+ /**
689
+ * One `mcp_tool_store` line per turn on the harness log. Server names only,
690
+ * no tool data. Not sent to the control plane: its cold-start endpoint
691
+ * accepts only `cold_start_step`/`cold_start_summary`, so it would be one
692
+ * refused request per turn.
693
+ */
694
+ private logMcpToolStore;
655
695
  private buildSessionConfig;
656
696
  }
@@ -38,8 +38,11 @@ const vendor_backends_1 = require("../vendor-backends");
38
38
  const attachments_1 = require("./attachments");
39
39
  const cold_start_steps_1 = require("./cold-start-steps");
40
40
  const conversation_1 = require("./conversation");
41
+ const credential_proxy_1 = require("./credential-proxy");
41
42
  const knowledge_tools_1 = require("./knowledge-tools");
42
43
  const mcp_preflight_1 = require("./mcp-preflight");
44
+ const mcp_tool_store_1 = require("./mcp-tool-store");
45
+ const mcp_toolset_1 = require("./mcp-toolset");
43
46
  const permission_mode_1 = require("./permission-mode");
44
47
  const projection_1 = require("./projection");
45
48
  const space_files_1 = require("./space-files");
@@ -83,6 +86,15 @@ function approvalTimeoutMs(value) {
83
86
  * transcripts are complete.
84
87
  */
85
88
  const IDLE_CONFIRMATIONS = 2;
89
+ /**
90
+ * The pause before the CONFIRMING poll, for a backend whose IDLE is terminal
91
+ * (`CodingAgentAdapter.idleIsTerminal`). The confirmation is kept: a final
92
+ * poll after the backend reported IDLE is what picks up a part that landed
93
+ * between the previous poll and the status read. It just no longer waits a
94
+ * full `POLL_INTERVAL_MS`, which used to add ~2 s to the end of every turn.
95
+ * Any new part in that poll resets the count, exactly as before.
96
+ */
97
+ const TERMINAL_IDLE_CONFIRM_MS = 250;
86
98
  /**
87
99
  * THE PRIMARY GUARD, per the LONG TURNS fix.
88
100
  *
@@ -646,6 +658,12 @@ class SessionRunner {
646
658
  browserPauseStartedAt = null;
647
659
  /** When the last browser pause ended; the idle clock restarts from here. */
648
660
  browserPauseEndedAt = null;
661
+ /**
662
+ * The credential-proxy token in force for this turn, kept ONLY so it can be
663
+ * scrubbed from everything this runner sends or logs. Not a credential
664
+ * value (see `LeaseResponse.credentialProxy`); null when the proxy is off.
665
+ */
666
+ credentialProxyToken = null;
649
667
  constructor(deps) {
650
668
  this.deps = deps;
651
669
  this.logger = deps.logger ?? console;
@@ -713,7 +731,9 @@ class SessionRunner {
713
731
  async run(leased, signal) {
714
732
  const lease = withoutGatewayForVendorBackend(leased);
715
733
  const { session } = lease;
716
- const logColdStart = (line) => {
734
+ this.credentialProxyToken = null;
735
+ const logColdStart = (raw) => {
736
+ const line = (0, credential_proxy_1.redactCredentialProxyToken)(raw, this.credentialProxyToken);
717
737
  this.logger.log(line);
718
738
  this.deps.coldStartLog?.(line);
719
739
  };
@@ -728,6 +748,15 @@ class SessionRunner {
728
748
  let stopReason = 'completed';
729
749
  let error;
730
750
  let gatewaySpendBefore = null;
751
+ /**
752
+ * The start-of-turn `/key/info` read, in flight. Started next to the model
753
+ * catalogue and the workspace prepare instead of before them, and awaited
754
+ * just before `sendPrompt` — the first point this turn can spend — so the
755
+ * baseline is still taken before any model call.
756
+ */
757
+ let gatewaySpendBeforeRead = null;
758
+ /** The end-of-turn read, started when the drain ends. See `finally`. */
759
+ let gatewaySpendAfterRead = null;
731
760
  let repositoryStatuses = [];
732
761
  /** True when the backend session was resumed rather than created. */
733
762
  let resumed = false;
@@ -760,9 +789,12 @@ class SessionRunner {
760
789
  if (refusal)
761
790
  throw new Error(refusal);
762
791
  if (session.agent.model.gatewayUrl && lease.modelToken) {
763
- gatewaySpendBefore = await readGatewaySpend(session.agent.model.gatewayUrl, lease.modelToken);
764
- if (gatewaySpendBefore === null)
765
- this.logger.warn(`[coding-agent] Gateway spend start unavailable for turn ${session.turnId}; this turn cannot be debited from gateway usage`);
792
+ // `readGatewaySpend` never rejects: a failure resolves to null.
793
+ gatewaySpendBeforeRead = readGatewaySpend(session.agent.model.gatewayUrl, lease.modelToken).then((spend) => {
794
+ if (spend === null)
795
+ this.logger.warn(`[coding-agent] Gateway spend start unavailable for turn ${session.turnId}; this turn cannot be debited from gateway usage`);
796
+ return spend;
797
+ });
766
798
  }
767
799
  // The catalogue is advisory. Fetch it while the independent workspace
768
800
  // is prepared. Pi can start with the selected lease model while this
@@ -989,18 +1021,66 @@ class SessionRunner {
989
1021
  const unavailableMcpServers = [];
990
1022
  const config = this.buildSessionConfig(session, workspaceDir, secretToken, signal, knowledgeUrl, unavailableMcpServers);
991
1023
  config.coldStartLog = this.deps.coldStartLog;
1024
+ /*
1025
+ * THE CREDENTIAL PROXY. Layered over the env built above only when the
1026
+ * lease offers it, the runner is a sandbox, and the broker answers; any
1027
+ * failed check leaves `config.environment` exactly as it was.
1028
+ */
1029
+ const credentialProxy = await (0, credential_proxy_1.credentialProxyEnvironment)({
1030
+ harnessRuntime: this.deps.runtime,
1031
+ sessionRuntime: session.runtime,
1032
+ backend: session.agent.backend,
1033
+ sessionId: session.sessionId,
1034
+ sessionDir: workspaceDir,
1035
+ descriptor: lease.credentialProxy,
1036
+ environment: config.environment,
1037
+ directUrls: [
1038
+ ...(this.deps.directUrls ?? []),
1039
+ ...this.sessionDirectUrls(session, knowledgeUrl)
1040
+ ],
1041
+ logger: this.logger
1042
+ });
1043
+ config.environment = credentialProxy.environment;
1044
+ this.credentialProxyToken = credentialProxy.token;
1045
+ /*
1046
+ * ITEM 1(b). A server whose stored listing is still valid for this
1047
+ * toolset and this speaker is not checked again: its tools are seeded
1048
+ * into Pi from the store, so this turn sends it no MCP request before
1049
+ * the prompt. The gateway still checks every tool call.
1050
+ */
1051
+ const toolStore = (this.mcpCheckState.toolsets ??= new Map());
1052
+ const toolsetMembers = this.mcpToolsetMembers(session, knowledgeUrl);
1053
+ const toolScope = session.mcpToolScope
1054
+ ? { tenantId: session.tenantId, mcpToolScope: session.mcpToolScope }
1055
+ : null;
1056
+ const reusePlan = (0, mcp_tool_store_1.planMcpToolReuse)(toolStore, session.sessionId, toolsetMembers, toolScope, config.mcpServers ?? {});
992
1057
  const endMcpPreflight = coldStart.begin('mcp_preflight');
1058
+ const mcpListings = new Map();
993
1059
  try {
994
- unavailableMcpServers.push(...(await (0, mcp_preflight_1.verifyMcpServers)(config.mcpServers ?? {}, signal, this.mcpCheckState.checked, this.mcpCheckState.failed)));
1060
+ unavailableMcpServers.push(...(await (0, mcp_preflight_1.verifyMcpServers)(Object.fromEntries(Object.entries(config.mcpServers ?? {}).filter(([name]) => !(name in reusePlan.reused))), signal, this.mcpCheckState.checked, this.mcpCheckState.failed, mcpListings, reusePlan.relist)));
995
1061
  }
996
1062
  finally {
997
1063
  endMcpPreflight();
998
1064
  }
1065
+ const storedNow = (0, mcp_tool_store_1.rememberMcpToolListings)(toolStore, session.sessionId, toolsetMembers, toolScope, mcpListings);
1066
+ for (const [name, listing] of Object.entries(reusePlan.reused)) {
1067
+ mcpListings.set(name, listing);
1068
+ }
999
1069
  for (const unavailable of unavailableMcpServers) {
1000
1070
  if (!unavailable.keepConfigured) {
1001
1071
  delete config.mcpServers?.[unavailable.name];
1002
1072
  }
1003
1073
  }
1074
+ const toolsetFingerprint = (0, mcp_toolset_1.mcpToolsetFingerprint)(toolsetMembers, toolScope ?? { tenantId: session.tenantId });
1075
+ config.mcpToolSnapshot = this.mcpToolSnapshot(toolsetFingerprint, config, mcpListings);
1076
+ this.logMcpToolStore(session, {
1077
+ fingerprint: toolsetFingerprint.slice(0, 12),
1078
+ scoped: toolScope !== null,
1079
+ reused: Object.keys(reusePlan.reused),
1080
+ listed: [...mcpListings.keys()].filter((name) => !(name in reusePlan.reused)),
1081
+ stored: storedNow,
1082
+ dropped: reusePlan.dropped
1083
+ });
1004
1084
  const modelNotices = unavailableMcpServers.map(({ name, reason, keepConfigured }) => keepConfigured
1005
1085
  ? `MCP server "${name}" did not answer the check (${reason}). Its tools may become available later in this turn.`
1006
1086
  : `MCP server "${name}" is unavailable (${reason}). Its tools cannot be used in this turn.`);
@@ -1201,6 +1281,9 @@ class SessionRunner {
1201
1281
  const sentPromptTexts = [sentPromptParts.join('\n'), ...sentPromptParts];
1202
1282
  replayed = !resumed && memory.messages.length > 0;
1203
1283
  priorPartIdsForLog = priorPartIds;
1284
+ // The spend baseline must be read before the first model call.
1285
+ if (gatewaySpendBeforeRead)
1286
+ gatewaySpendBefore = await gatewaySpendBeforeRead;
1204
1287
  const endPrompt = coldStart.begin('model_prompt_send');
1205
1288
  try {
1206
1289
  await adapter.sendPrompt(backendSessionId, promptParts, config);
@@ -1339,6 +1422,21 @@ class SessionRunner {
1339
1422
  ]);
1340
1423
  }
1341
1424
  finally {
1425
+ /*
1426
+ * THE END-OF-TURN SPEND READ STARTS NOW, NOT AFTER TEARDOWN.
1427
+ *
1428
+ * The drain is over, so the backend has made its last model call. The
1429
+ * read used to be awaited serially after `stopServer`, adding its whole
1430
+ * round trip (up to its 5 s timeout) to every turn before the final
1431
+ * ingest. It now overlaps the rest of teardown and is awaited only where
1432
+ * `costUsd` is needed, on the final ingest that meters the turn.
1433
+ */
1434
+ gatewaySpendAfterRead =
1435
+ gatewaySpendBeforeRead &&
1436
+ session.agent.model.gatewayUrl &&
1437
+ lease.modelToken
1438
+ ? readGatewaySpend(session.agent.model.gatewayUrl, lease.modelToken)
1439
+ : null;
1342
1440
  release();
1343
1441
  this.deps.modelGateway?.revoke();
1344
1442
  /*
@@ -1410,10 +1508,11 @@ class SessionRunner {
1410
1508
  */
1411
1509
  }
1412
1510
  let costUsd;
1413
- if (gatewaySpendBefore !== null &&
1414
- session.agent.model.gatewayUrl &&
1415
- lease.modelToken) {
1416
- const spendAfter = await readGatewaySpend(session.agent.model.gatewayUrl, lease.modelToken);
1511
+ // A turn that failed before `sendPrompt` never awaited the baseline.
1512
+ if (gatewaySpendBeforeRead)
1513
+ gatewaySpendBefore = await gatewaySpendBeforeRead;
1514
+ if (gatewaySpendBefore !== null && gatewaySpendAfterRead) {
1515
+ const spendAfter = await gatewaySpendAfterRead;
1417
1516
  if (spendAfter === null)
1418
1517
  this.logger.warn(`[coding-agent] Gateway spend end unavailable for turn ${session.turnId}; this turn cannot be debited from gateway usage`);
1419
1518
  else
@@ -1886,7 +1985,9 @@ class SessionRunner {
1886
1985
  wake = null;
1887
1986
  resolveWait();
1888
1987
  };
1889
- timer.handle = setTimeout(finish, POLL_INTERVAL_MS);
1988
+ timer.handle = setTimeout(finish, idleSeen > 0 && adapter.idleIsTerminal === true
1989
+ ? TERMINAL_IDLE_CONFIRM_MS
1990
+ : POLL_INTERVAL_MS);
1890
1991
  wake = finish;
1891
1992
  signal.addEventListener('abort', finish, { once: true });
1892
1993
  });
@@ -2396,9 +2497,14 @@ class SessionRunner {
2396
2497
  if (chunks.length === 0) {
2397
2498
  chunks.push([]);
2398
2499
  }
2500
+ // The agent can echo its own env (`env`, a failing curl) into a tool
2501
+ // result; the credential-proxy token must not reach the transcript.
2502
+ const token = this.credentialProxyToken;
2399
2503
  for (let i = 0; i < chunks.length; i += 1) {
2400
2504
  const isLast = i === chunks.length - 1;
2401
- await this.deps.ingest(sessionId, chunks[i], isLast ? turn : undefined, isLast ? final : false, isLast ? modelMessages : undefined);
2505
+ await this.deps.ingest(sessionId, (0, credential_proxy_1.redactCredentialProxyTokenDeep)(chunks[i], token), isLast ? (0, credential_proxy_1.redactCredentialProxyTokenDeep)(turn, token) : undefined, isLast ? final : false, isLast
2506
+ ? (0, credential_proxy_1.redactCredentialProxyTokenDeep)(modelMessages, token)
2507
+ : undefined);
2402
2508
  }
2403
2509
  }
2404
2510
  /* ---------------------------------------------------------------- */
@@ -2666,6 +2772,14 @@ class SessionRunner {
2666
2772
  this.logger.warn(`[SessionRunner] Space files sync failed: ${(error instanceof Error ? error.message : String(error)).replace(/https?:\/\/\S+/gi, '<url>')}`);
2667
2773
  }
2668
2774
  }
2775
+ /** URLs this session's agent reaches without a proxy. */
2776
+ sessionDirectUrls(session, knowledgeUrl) {
2777
+ return [
2778
+ session.agent.model.gatewayUrl ?? '',
2779
+ ...session.agent.mcpServers.map((server) => server.url),
2780
+ ...(knowledgeUrl ? [knowledgeUrl] : [])
2781
+ ];
2782
+ }
2669
2783
  /**
2670
2784
  * Build the backend's session config.
2671
2785
  *
@@ -2682,6 +2796,59 @@ class SessionRunner {
2682
2796
  * security perimeter and trusting it to filter its own tools is exactly
2683
2797
  * defect G2.
2684
2798
  */
2799
+ /**
2800
+ * The leased toolset as it enters the fingerprint: every server's name,
2801
+ * upstream URL and allowlist, plus the knowledge server when bound.
2802
+ */
2803
+ mcpToolsetMembers(session, knowledgeUrl) {
2804
+ return [
2805
+ ...session.agent.mcpServers.map((server) => ({
2806
+ name: server.name,
2807
+ url: server.url,
2808
+ allowedTools: server.allowedTools
2809
+ })),
2810
+ ...(knowledgeUrl
2811
+ ? [
2812
+ {
2813
+ name: knowledge_tools_1.KNOWLEDGE_SERVER_NAME,
2814
+ url: knowledgeUrl,
2815
+ allowedTools: null
2816
+ }
2817
+ ]
2818
+ : [])
2819
+ ];
2820
+ }
2821
+ /**
2822
+ * The listings the backend may seed from (item 4, and item 1(b) for stored
2823
+ * ones). Only a server that is still configured and was listed at its
2824
+ * current url is kept.
2825
+ */
2826
+ mcpToolSnapshot(fingerprint, config, listings) {
2827
+ const servers = {};
2828
+ for (const [name, listing] of listings) {
2829
+ const configured = config.mcpServers?.[name];
2830
+ if (configured?.type === 'http' && configured.url === listing.url) {
2831
+ servers[name] = listing;
2832
+ }
2833
+ }
2834
+ if (Object.keys(servers).length === 0)
2835
+ return undefined;
2836
+ return { fingerprint, servers };
2837
+ }
2838
+ /**
2839
+ * One `mcp_tool_store` line per turn on the harness log. Server names only,
2840
+ * no tool data. Not sent to the control plane: its cold-start endpoint
2841
+ * accepts only `cold_start_step`/`cold_start_summary`, so it would be one
2842
+ * refused request per turn.
2843
+ */
2844
+ logMcpToolStore(session, detail) {
2845
+ this.logger.log(JSON.stringify({
2846
+ event: 'mcp_tool_store',
2847
+ sessionId: session.sessionId,
2848
+ turnId: session.turnId,
2849
+ ...detail
2850
+ }));
2851
+ }
2685
2852
  buildSessionConfig(session, workspaceDir, secretToken, signal, knowledgeUrl = null, unavailable = []) {
2686
2853
  const mcpServers = {};
2687
2854
  for (const server of session.agent.mcpServers) {
@@ -2735,11 +2902,7 @@ class SessionRunner {
2735
2902
  secretBrokerPort: brokerPort ?? undefined,
2736
2903
  secretSessionToken: secretToken ?? undefined,
2737
2904
  secretShellPath: this.deps.secrets.writeShellWrapper() ?? undefined,
2738
- environment: this.deps.workspace.getEnvironment?.(session.sessionId, [
2739
- session.agent.model.gatewayUrl ?? '',
2740
- ...session.agent.mcpServers.map((server) => server.url),
2741
- ...(knowledgeUrl ? [knowledgeUrl] : [])
2742
- ]),
2905
+ environment: this.deps.workspace.getEnvironment?.(session.sessionId, this.sessionDirectUrls(session, knowledgeUrl)),
2743
2906
  /*
2744
2907
  * DEFECT G1, THE OTHER HALF.
2745
2908
  *
package/dist/types.d.ts CHANGED
@@ -516,6 +516,28 @@ export interface LeaseResponse {
516
516
  modelTokenExpiresAt?: string;
517
517
  /** In-memory only. It is never passed through a process environment. */
518
518
  githubCredential?: LeasedGitHubCredential;
519
+ /**
520
+ * Where the agent reaches the sandbox CREDENTIAL PROXY for this turn.
521
+ *
522
+ * NOT A SECRET VALUE. The real credential values live only in the root
523
+ * sandbox broker that runs the proxy; the harness (same uid as the agent)
524
+ * never holds one. `token` is a capability bound to tenant, session, turn
525
+ * and sandbox, which the agent presents to the proxy as the proxy URL's
526
+ * username. It is still never logged.
527
+ *
528
+ * ADDITIVE AND OPTIONAL (no protocol bump). Sent only for `sandbox` leases;
529
+ * the harness also refuses it on any other runtime and health-checks the
530
+ * port before using it. See `runtime/credential-proxy.ts`.
531
+ */
532
+ credentialProxy?: LeasedCredentialProxy;
533
+ }
534
+ export interface LeasedCredentialProxy {
535
+ /** Loopback port of the broker's proxy, e.g. 7790. */
536
+ port: number;
537
+ /** `pfcp_<b64url payload>.<b64url hmac>` — a bound capability, not a credential. */
538
+ token: string;
539
+ /** Public CA certificate (PEM) the proxy signs leaves with. */
540
+ caPath: string;
519
541
  }
520
542
  export interface LeasedSession {
521
543
  sessionId: string;
@@ -523,6 +545,14 @@ export interface LeasedSession {
523
545
  /** One lease is one TURN. A new turn on the same session is a new lease. */
524
546
  turnId: string;
525
547
  runtime: RunnerRuntime;
548
+ /**
549
+ * Opaque. Changes when the speaker or the Space whose live permissions the
550
+ * MCP gateway applies changes, so the tool catalogue may differ. The harness
551
+ * reuses a session's stored MCP tool listings on a later turn only while
552
+ * this is the same. Optional and additive: absent (an older control plane)
553
+ * means nothing is reused across turns.
554
+ */
555
+ mcpToolScope?: string;
526
556
  agent: LeasedAgent;
527
557
  input: LeasedTurnInput;
528
558
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peakflo/agent-harness",
3
- "version": "0.2.13",
3
+ "version": "0.2.14-rc.2",
4
4
  "description": "Headless Peakflo coding-agent harness. One codebase, three hosting modes: a Daytona cloud sandbox, a customer VM, and the 20x desktop.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",