@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.
- package/dist/adapters/coding-agent-adapter.d.ts +19 -0
- package/dist/adapters/pi-adapter.d.ts +27 -1
- package/dist/adapters/pi-adapter.js +170 -6
- package/dist/adapters/pi-mcp-cache-seed.d.ts +66 -0
- package/dist/adapters/pi-mcp-cache-seed.js +188 -0
- package/dist/contract.d.ts +1 -1
- package/dist/contract.js +1 -1
- package/dist/control-plane.d.ts +9 -2
- package/dist/control-plane.js +11 -3
- package/dist/github-proxy-certificates.d.ts +3 -0
- package/dist/github-proxy-certificates.js +7 -1
- package/dist/github-workspace.d.ts +6 -0
- package/dist/github-workspace.js +22 -10
- package/dist/harness.d.ts +3 -0
- package/dist/harness.js +30 -7
- package/dist/index.d.ts +1 -1
- package/dist/runtime/cold-start-batch.d.ts +36 -0
- package/dist/runtime/cold-start-batch.js +96 -0
- package/dist/runtime/credential-proxy.d.ts +44 -0
- package/dist/runtime/credential-proxy.js +230 -0
- package/dist/runtime/mcp-preflight.d.ts +15 -1
- package/dist/runtime/mcp-preflight.js +54 -2
- package/dist/runtime/mcp-tool-store.d.ts +79 -0
- package/dist/runtime/mcp-tool-store.js +151 -0
- package/dist/runtime/mcp-toolset.d.ts +51 -0
- package/dist/runtime/mcp-toolset.js +40 -0
- package/dist/runtime/session-runner.d.ts +41 -1
- package/dist/runtime/session-runner.js +179 -16
- package/dist/types.d.ts +30 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
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,
|
|
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
|
|
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.
|
|
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",
|