@borgee/agents-host 0.2.97 → 0.2.110
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/README.md +77 -5
- package/dist/agents-host.d.ts +16 -0
- package/dist/agents-host.js +608 -82
- package/dist/catalog-worker.js +21 -0
- package/dist/catalog-worker.js.map +7 -0
- package/dist/chat/chat-control-plane.d.ts +19 -4
- package/dist/chat/normalized-input.d.ts +9 -0
- package/dist/chat/normalized-input.js +24 -0
- package/dist/chat/sdk-chat-control-plane.d.ts +46 -6
- package/dist/chat/sdk-chat-control-plane.js +169 -6
- package/dist/cli-args.d.ts +1 -1
- package/dist/cli-args.js +13 -4
- package/dist/config.d.ts +3 -1
- package/dist/config.js +25 -0
- package/dist/context/injection.d.ts +16 -0
- package/dist/context/injection.js +37 -21
- package/dist/context/prompt.d.ts +12 -0
- package/dist/context/prompt.js +25 -0
- package/dist/context/skill-manual.js +1 -1
- package/dist/context/turn-preparation.d.ts +2 -1
- package/dist/context/turn-preparation.js +15 -0
- package/dist/execution-telemetry.d.ts +96 -0
- package/dist/execution-telemetry.js +583 -0
- package/dist/gateway/channel-file-workspace.d.ts +15 -0
- package/dist/gateway/channel-file-workspace.js +130 -0
- package/dist/gateway/localhost-gateway.js +167 -2
- package/dist/hosted-turn-content.js +3 -2
- package/dist/local-config.js +34 -1
- package/dist/managed-daemon.js +15 -0
- package/dist/native-environment.d.ts +2 -0
- package/dist/native-environment.js +9 -0
- package/dist/native-protocol-types/codec.d.ts +49 -0
- package/dist/native-protocol-types/commands.d.ts +71 -0
- package/dist/native-protocol-types/compatibility.d.ts +5 -0
- package/dist/native-protocol-types/cursor.d.ts +7 -0
- package/dist/native-protocol-types/envelope.d.ts +1105 -0
- package/dist/native-protocol-types/file-changes.d.ts +20 -0
- package/dist/native-protocol-types/history.d.ts +487 -0
- package/dist/native-protocol-types/index.d.ts +18 -0
- package/dist/native-protocol-types/interactions.d.ts +619 -0
- package/dist/native-protocol-types/messages.d.ts +2482 -0
- package/dist/native-protocol-types/public-validation.d.ts +10 -0
- package/dist/native-protocol-types/remote-host-uplink.d.ts +67 -0
- package/dist/native-protocol-types/resources.d.ts +126 -0
- package/dist/native-protocol-types/session-settings.d.ts +22 -0
- package/dist/native-protocol-types/snapshot.d.ts +575 -0
- package/dist/native-protocol-types/timeline.d.ts +1503 -0
- package/dist/native-protocol-types/tool-result.d.ts +26 -0
- package/dist/native-protocol-types/uplink.d.ts +56 -0
- package/dist/native-protocol-types/version.d.ts +5 -0
- package/dist/native-provider-types/commands.d.ts +51 -0
- package/dist/native-provider-types/control.d.ts +192 -0
- package/dist/native-provider-types/file-changes.d.ts +9 -0
- package/dist/native-provider-types/index.d.ts +8 -0
- package/dist/native-provider-types/interactions.d.ts +7 -0
- package/dist/native-provider-types/observation.d.ts +209 -0
- package/dist/native-provider-types/provider.d.ts +124 -0
- package/dist/native-provider-types/session-settings.d.ts +17 -0
- package/dist/native-provider-types/testing.d.ts +27 -0
- package/dist/native-provider-types/tool-result.d.ts +21 -0
- package/dist/native-providers.d.ts +21 -0
- package/dist/native-providers.js +18402 -0
- package/dist/native-providers.js.map +7 -0
- package/dist/plugin-sdk.js +10713 -229
- package/dist/plugin-sdk.js.map +4 -4
- package/dist/policy/copilot-permission.js +1 -8
- package/dist/policy/gateway-authorization.d.ts +1 -1
- package/dist/policy/gateway-authorization.js +22 -2
- package/dist/providers/claude/adapter.js +1 -0
- package/dist/providers/claude/cli-client.js +4 -0
- package/dist/providers/codex/adapter.js +1 -0
- package/dist/providers/codex/cli-client.js +5 -0
- package/dist/providers/copilot/adapter.js +4 -0
- package/dist/providers/copilot/cli-client.js +11 -1
- package/dist/providers/copilot/sdk-session.d.ts +12 -3
- package/dist/providers/copilot/sdk-session.js +230 -6
- package/dist/providers/create-provider.js +24 -0
- package/dist/providers/normalized/adapter.d.ts +107 -0
- package/dist/providers/normalized/adapter.js +1650 -0
- package/dist/providers/normalized/control-policy.d.ts +5 -0
- package/dist/providers/normalized/control-policy.js +12 -0
- package/dist/providers/normalized/interactions.d.ts +3 -0
- package/dist/providers/normalized/interactions.js +39 -0
- package/dist/providers/normalized/legacy-input-ownership.d.ts +34 -0
- package/dist/providers/normalized/legacy-input-ownership.js +123 -0
- package/dist/providers/normalized/session-management.d.ts +67 -0
- package/dist/providers/normalized/session-management.js +334 -0
- package/dist/providers/normalized/session-ownership.d.ts +3 -0
- package/dist/providers/normalized/session-ownership.js +90 -0
- package/dist/providers/prompt-usage.d.ts +8 -0
- package/dist/providers/prompt-usage.js +52 -0
- package/dist/providers/provider-adapter.d.ts +43 -1
- package/dist/providers/provider-adapter.js +8 -0
- package/dist/state-paths.d.ts +2 -0
- package/dist/state-paths.js +6 -0
- package/dist/types.d.ts +66 -0
- package/dist/typing-lease.d.ts +14 -0
- package/dist/typing-lease.js +31 -0
- package/dist/vendor/agent-provider-codex/LICENSE +211 -0
- package/dist/vendor/agent-provider-codex/NOTICE +13 -0
- package/package.json +22 -13
- package/skills/borgee-agent/SKILL.md +12 -4
- package/skills/borgee-agent/scripts/borgee-agent.mjs +110 -0
- package/skills/borgee-agent/scripts/borgee-agent.py +116 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AgentInteractionResponse } from '../../native-providers.js';
|
|
2
|
+
/** Host grants cannot outlive the exact tool request or native turn. */
|
|
3
|
+
export declare function assertHostedInteractionScope(response: AgentInteractionResponse): void;
|
|
4
|
+
/** Only provider-classified ownership errors are safe to publish verbatim. */
|
|
5
|
+
export declare function nativeSessionFailureMessage(error: unknown): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AgentSessionInUseError } from '../../native-providers.js';
|
|
2
|
+
/** Host grants cannot outlive the exact tool request or native turn. */
|
|
3
|
+
export function assertHostedInteractionScope(response) {
|
|
4
|
+
if ((response.kind === 'tool_approval' && response.decision === 'allow' && response.scope !== 'once')
|
|
5
|
+
|| (response.kind === 'permission_approval' && response.decision === 'allow' && response.scope !== 'turn')) {
|
|
6
|
+
throw new Error('Host permissions cannot be widened beyond this interaction or turn.');
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** Only provider-classified ownership errors are safe to publish verbatim. */
|
|
10
|
+
export function nativeSessionFailureMessage(error) {
|
|
11
|
+
return error instanceof AgentSessionInUseError ? error.message : 'The native session could not accept this operation. Check the native runtime before retrying.';
|
|
12
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type ServerRequestContext } from '../../plugin-sdk.js';
|
|
2
|
+
import type { ProviderAdapter } from '../provider-adapter.js';
|
|
3
|
+
export declare function respondToHostedInteraction(provider: ProviderAdapter, action: string, params: unknown, context?: ServerRequestContext): Promise<unknown>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { parseSessionManagementRequest, parseSessionDescriptionRequest, parseSessionObservationRequest, parseSessionDirectoryRequest } from '../../plugin-sdk.js';
|
|
2
|
+
import { isInteractionResponseMessage, PROTOCOL_VERSION } from '../../native-providers.js';
|
|
3
|
+
export async function respondToHostedInteraction(provider, action, params, context) {
|
|
4
|
+
if (action === 'describe_session_management' && context)
|
|
5
|
+
return provider.describeSessionManagement?.() ?? { operations: [] };
|
|
6
|
+
if (['discover_sessions', 'create_session', 'open_session'].includes(action) && provider.manageSession && context)
|
|
7
|
+
return provider.manageSession(parseSessionManagementRequest(params), context);
|
|
8
|
+
if (action === 'list_sessions' && provider.listSessions && context)
|
|
9
|
+
return provider.listSessions(parseSessionDirectoryRequest(params), context);
|
|
10
|
+
if (action === 'describe_session' && provider.describeSession && context)
|
|
11
|
+
return provider.describeSession(parseSessionDescriptionRequest(params), context) ?? null;
|
|
12
|
+
if (action === 'session_observation' && provider.observeSession && context)
|
|
13
|
+
return provider.observeSession(parseSessionObservationRequest(params), context);
|
|
14
|
+
if (action === 'sync_normalized_activity') {
|
|
15
|
+
if (!params || typeof params !== 'object' || !('channel_id' in params) || typeof params.channel_id !== 'string' || !params.channel_id)
|
|
16
|
+
throw new Error('Invalid normalized activity target.');
|
|
17
|
+
return { synced: provider.syncNormalizedActivity?.(params.channel_id) ?? false };
|
|
18
|
+
}
|
|
19
|
+
const input = params && typeof params === 'object' ? params : undefined;
|
|
20
|
+
const fields = ['binding_id', 'native_session_id', 'connection_generation'];
|
|
21
|
+
const targeted = !!input && fields.some(field => field in input);
|
|
22
|
+
if (targeted && (!fields.every(field => typeof input[field] === 'string' && /^[A-Za-z0-9_:.@/-]{1,512}$/.test(input[field])) || !context?.isConnectionCurrent?.()))
|
|
23
|
+
throw new Error('Invalid native Session target.');
|
|
24
|
+
const binding = targeted ? input : undefined;
|
|
25
|
+
if (action === 'stop_turn') {
|
|
26
|
+
if (!input || typeof input.channel_id !== 'string' || !input.channel_id)
|
|
27
|
+
throw new Error('Invalid native stop target.');
|
|
28
|
+
return { aborted: await provider.cancelTurn?.(input.channel_id, binding, context) ?? false };
|
|
29
|
+
}
|
|
30
|
+
if (action !== 'interaction_response' || !provider.respondInteraction)
|
|
31
|
+
throw new Error('Unsupported native Host request.');
|
|
32
|
+
if (!params || typeof params !== 'object')
|
|
33
|
+
throw new Error('Invalid native Host request.');
|
|
34
|
+
if (!input || typeof input.channel_id !== 'string' || (typeof input.task_id !== 'string' && !binding) || !isInteractionResponseMessage(input.body))
|
|
35
|
+
throw new Error('Invalid native interaction envelope.');
|
|
36
|
+
const { agentId, requestId, response } = input.body.payload;
|
|
37
|
+
const receipt = await provider.respondInteraction({ channelId: input.channel_id, taskId: typeof input.task_id === 'string' ? input.task_id : undefined, agentId, requestId, response, binding }, context);
|
|
38
|
+
return { protocolVersion: PROTOCOL_VERSION, type: 'interaction_resolved', payload: { agentId, requestId, response: receipt } };
|
|
39
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AgentMessageReceipt, ProviderObservation } from '../../native-providers.js';
|
|
2
|
+
interface LegacyPublicInput {
|
|
3
|
+
text: string | null;
|
|
4
|
+
prompt: string;
|
|
5
|
+
clientMessageId?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface OwnedObservation<T> {
|
|
8
|
+
owner: T;
|
|
9
|
+
observation: ProviderObservation;
|
|
10
|
+
}
|
|
11
|
+
/** Matches accepted native input identities before any task-scoped publication. */
|
|
12
|
+
export declare class InputOwnership<T> {
|
|
13
|
+
private readonly allowReceiptless;
|
|
14
|
+
constructor(allowReceiptless?: boolean);
|
|
15
|
+
private readonly inputs;
|
|
16
|
+
private pending;
|
|
17
|
+
private pendingBytes;
|
|
18
|
+
private readonly publicInputs;
|
|
19
|
+
private publicInputBytes;
|
|
20
|
+
get size(): number;
|
|
21
|
+
has(owner: T): boolean;
|
|
22
|
+
owners(): T[];
|
|
23
|
+
register(owner: T, publicInput?: LegacyPublicInput): void;
|
|
24
|
+
remove(owner: T): void;
|
|
25
|
+
settle(owner: T, receipt: void | AgentMessageReceipt): OwnedObservation<T>[];
|
|
26
|
+
/** Preserve the legacy display-body contract before the shared canonical formatter. */
|
|
27
|
+
publicObservation(observation: ProviderObservation, owner?: T): ProviderObservation;
|
|
28
|
+
turnId(owner: T): string | undefined;
|
|
29
|
+
retireObservations(): void;
|
|
30
|
+
/** History can confirm a receipt identity, but cannot claim a receipt-less input. */
|
|
31
|
+
recover(owner: T, observations: readonly ProviderObservation[]): string | undefined;
|
|
32
|
+
observe(observation: ProviderObservation): OwnedObservation<T>[];
|
|
33
|
+
}
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** Matches accepted native input identities before any task-scoped publication. */
|
|
2
|
+
export class InputOwnership {
|
|
3
|
+
allowReceiptless;
|
|
4
|
+
constructor(allowReceiptless = false) {
|
|
5
|
+
this.allowReceiptless = allowReceiptless;
|
|
6
|
+
}
|
|
7
|
+
inputs = new Map();
|
|
8
|
+
pending = [];
|
|
9
|
+
pendingBytes = 0;
|
|
10
|
+
publicInputs = new Map();
|
|
11
|
+
publicInputBytes = 0;
|
|
12
|
+
get size() { return this.inputs.size; }
|
|
13
|
+
has(owner) { return this.inputs.has(owner); }
|
|
14
|
+
owners() { return [...this.inputs.keys()]; }
|
|
15
|
+
register(owner, publicInput) {
|
|
16
|
+
if (this.inputs.size >= 32)
|
|
17
|
+
throw new Error('Too many pending native input associations.');
|
|
18
|
+
this.inputs.set(owner, { owner, settled: false, publicInput });
|
|
19
|
+
}
|
|
20
|
+
remove(owner) {
|
|
21
|
+
this.inputs.delete(owner);
|
|
22
|
+
if (!this.inputs.size) {
|
|
23
|
+
this.pending = [];
|
|
24
|
+
this.pendingBytes = 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
settle(owner, receipt) {
|
|
28
|
+
const input = this.inputs.get(owner);
|
|
29
|
+
if (!input)
|
|
30
|
+
return [];
|
|
31
|
+
input.settled = true;
|
|
32
|
+
input.receipt = receipt || undefined;
|
|
33
|
+
input.turnId = receipt?.turnId;
|
|
34
|
+
const pending = this.pending;
|
|
35
|
+
this.pending = [];
|
|
36
|
+
this.pendingBytes = 0;
|
|
37
|
+
const result = [];
|
|
38
|
+
for (const observation of pending)
|
|
39
|
+
result.push(...this.observe(observation));
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
/** Preserve the legacy display-body contract before the shared canonical formatter. */
|
|
43
|
+
publicObservation(observation, owner) {
|
|
44
|
+
const event = observation.event;
|
|
45
|
+
if (event.type !== 'timeline' || event.item.type !== 'user_message')
|
|
46
|
+
return observation;
|
|
47
|
+
const item = event.item;
|
|
48
|
+
const input = owner === undefined ? undefined : this.inputs.get(owner);
|
|
49
|
+
if (input?.settled && input.publicInput && item.messageId && event.turnId === input.turnId) {
|
|
50
|
+
const proven = input.receipt?.messageId === item.messageId || !input.receipt?.messageId && !item.clientMessageId
|
|
51
|
+
&& (input.publicMessageId === item.messageId || !input.publicMessageId && item.text === input.publicInput.prompt);
|
|
52
|
+
if (proven && !this.publicInputs.has(item.messageId)) {
|
|
53
|
+
const display = { text: input.publicInput.text, clientMessageId: input.publicInput.clientMessageId };
|
|
54
|
+
const bytes = Buffer.byteLength(JSON.stringify([item.messageId, display]));
|
|
55
|
+
if (this.publicInputs.size < 4096 && this.publicInputBytes + bytes <= 1048576) {
|
|
56
|
+
input.publicMessageId = item.messageId;
|
|
57
|
+
this.publicInputs.set(item.messageId, display);
|
|
58
|
+
this.publicInputBytes += bytes;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const verified = item.messageId ? this.publicInputs.get(item.messageId) : undefined;
|
|
63
|
+
return { ...observation, event: { ...event, item: { type: 'user_message',
|
|
64
|
+
...(item.messageId ? { messageId: item.messageId } : {}), text: verified?.text ?? 'Input content unavailable.',
|
|
65
|
+
...(verified?.text != null && verified.clientMessageId ? { clientMessageId: verified.clientMessageId } : {}) } } };
|
|
66
|
+
}
|
|
67
|
+
turnId(owner) { return this.inputs.get(owner)?.turnId; }
|
|
68
|
+
retireObservations() { this.pending = []; this.pendingBytes = 0; }
|
|
69
|
+
/** History can confirm a receipt identity, but cannot claim a receipt-less input. */
|
|
70
|
+
recover(owner, observations) {
|
|
71
|
+
const input = this.inputs.get(owner);
|
|
72
|
+
if (!input?.settled || !input.receipt)
|
|
73
|
+
return;
|
|
74
|
+
const matches = new Set(observations.flatMap(({ event }) => event.type === 'timeline' && event.item.type === 'user_message'
|
|
75
|
+
&& input.receipt?.messageId && event.item.messageId === input.receipt.messageId && event.turnId ? [event.turnId] : []));
|
|
76
|
+
if (matches.size > 1 || input.turnId && matches.size && !matches.has(input.turnId))
|
|
77
|
+
return;
|
|
78
|
+
const turnId = input.turnId ?? [...matches][0];
|
|
79
|
+
if (!turnId || [...this.inputs.values()].some(other => other !== input && other.turnId === turnId))
|
|
80
|
+
return;
|
|
81
|
+
input.turnId = turnId;
|
|
82
|
+
return turnId;
|
|
83
|
+
}
|
|
84
|
+
observe(observation) {
|
|
85
|
+
const event = observation.event;
|
|
86
|
+
const turnId = 'turnId' in event ? event.turnId : undefined;
|
|
87
|
+
if (!turnId)
|
|
88
|
+
return [];
|
|
89
|
+
let input = [...this.inputs.values()].find(value => value.settled && value.turnId === turnId);
|
|
90
|
+
if (event.type === 'timeline' && event.item.type === 'user_message' && event.item.messageId) {
|
|
91
|
+
const messageId = event.item.messageId;
|
|
92
|
+
const byMessage = [...this.inputs.values()].find(value => value.settled && value.receipt?.messageId === messageId);
|
|
93
|
+
if (byMessage) {
|
|
94
|
+
if (input && input !== byMessage)
|
|
95
|
+
throw new Error('Native input has conflicting message and turn ownership.');
|
|
96
|
+
if (byMessage.turnId && byMessage.turnId !== turnId)
|
|
97
|
+
return [];
|
|
98
|
+
input = byMessage;
|
|
99
|
+
input.turnId = turnId;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (this.allowReceiptless && !input && ['turn_started', 'turn_canceled', 'turn_failed'].includes(event.type)) {
|
|
103
|
+
// Receipt-less SDK implementations keep their existing ordinary-send contract.
|
|
104
|
+
input = [...this.inputs.values()].find(value => value.settled && !value.receipt && !value.turnId);
|
|
105
|
+
if (input)
|
|
106
|
+
input.turnId = turnId;
|
|
107
|
+
}
|
|
108
|
+
if (input) {
|
|
109
|
+
const ready = this.pending.filter(value => 'turnId' in value.event && value.event.turnId === turnId);
|
|
110
|
+
this.pending = this.pending.filter(value => !ready.includes(value));
|
|
111
|
+
this.pendingBytes = this.pending.reduce((size, value) => size + Buffer.byteLength(JSON.stringify(value)), 0);
|
|
112
|
+
return [...ready.map(value => ({ owner: input.owner, observation: value })), { owner: input.owner, observation }];
|
|
113
|
+
}
|
|
114
|
+
if ([...this.inputs.values()].some(value => !value.settled || (value.receipt?.messageId && !value.turnId))) {
|
|
115
|
+
const bytes = Buffer.byteLength(JSON.stringify(observation));
|
|
116
|
+
if (this.pending.length >= 256 || this.pendingBytes + bytes > 1048576)
|
|
117
|
+
throw new Error('Native input identity observation buffer exceeded.');
|
|
118
|
+
this.pending.push(observation);
|
|
119
|
+
this.pendingBytes += bytes;
|
|
120
|
+
}
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { AgentPersistenceHandle, AgentProviderAdapter, AgentSession } from '../../native-providers.js';
|
|
2
|
+
import type { NativeSessionDirectory, SessionDiscoveryRequest, SessionMutationRequest, SessionManagementReceipt, SessionControlTarget, ServerRequestContext } from '../../plugin-sdk.js';
|
|
3
|
+
import type { CodexChannelSessionStore } from '../codex/session-store.js';
|
|
4
|
+
interface CatalogRow {
|
|
5
|
+
nativeSessionId: string;
|
|
6
|
+
workspace?: string;
|
|
7
|
+
title: string;
|
|
8
|
+
updatedAt: string;
|
|
9
|
+
state: string;
|
|
10
|
+
parentNativeSessionId?: string;
|
|
11
|
+
lineageUnavailable?: true;
|
|
12
|
+
}
|
|
13
|
+
interface CatalogProvider extends AgentProviderAdapter {
|
|
14
|
+
listSessions(options: {
|
|
15
|
+
cursor?: string;
|
|
16
|
+
limit: number;
|
|
17
|
+
includeAllSources: true;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
sessions: CatalogRow[];
|
|
20
|
+
nextCursor?: string;
|
|
21
|
+
}>;
|
|
22
|
+
inspectSession(nativeId: string): Promise<CatalogRow>;
|
|
23
|
+
}
|
|
24
|
+
interface Admission {
|
|
25
|
+
target: SessionControlTarget;
|
|
26
|
+
workspace_path: string;
|
|
27
|
+
}
|
|
28
|
+
type LoadedAdmission = Admission & {
|
|
29
|
+
persistence?: AgentPersistenceHandle;
|
|
30
|
+
};
|
|
31
|
+
export interface HostSessionManagementOptions {
|
|
32
|
+
root?: string;
|
|
33
|
+
provider: CatalogProvider;
|
|
34
|
+
store: CodexChannelSessionStore;
|
|
35
|
+
resolveAgentId(): string | undefined;
|
|
36
|
+
serverUrl: string;
|
|
37
|
+
loadProtectedNativeIds(): Promise<Set<string>>;
|
|
38
|
+
withOwnershipLock?<T>(work: () => Promise<T>): Promise<T>;
|
|
39
|
+
find(nativeId: string, channelId: string, generation: string): LoadedAdmission | undefined | Promise<LoadedAdmission | undefined>;
|
|
40
|
+
register(session: AgentSession, channelId: string, cwd: string, generation: string, current: () => boolean): Promise<Admission>;
|
|
41
|
+
}
|
|
42
|
+
export declare function allocateSessionWorkspace(root: string, date?: Date): Promise<string>;
|
|
43
|
+
/** Native admission shares the Host registry but never selects a channel default. */
|
|
44
|
+
export declare class HostSessionManagement {
|
|
45
|
+
private readonly options;
|
|
46
|
+
private admission;
|
|
47
|
+
private readonly uncommittedClaims;
|
|
48
|
+
private readonly cursors;
|
|
49
|
+
private generation?;
|
|
50
|
+
private knownClaims;
|
|
51
|
+
canExpose(nativeId: string, agentId: string, channelId: string): boolean;
|
|
52
|
+
constructor(options: HostSessionManagementOptions);
|
|
53
|
+
private current;
|
|
54
|
+
private claims;
|
|
55
|
+
private commitClaims;
|
|
56
|
+
private owned;
|
|
57
|
+
withAdmission<T>(work: () => Promise<T>): Promise<T>;
|
|
58
|
+
assertUnclaimed(nativeId: string): Promise<void>;
|
|
59
|
+
/** All observer admission paths share the durable independent identity reservation. */
|
|
60
|
+
admitUnclaimed<T>(nativeId: string, work: () => Promise<T>): Promise<T | undefined>;
|
|
61
|
+
private lineageReason;
|
|
62
|
+
operations(): Promise<Array<'discover_sessions' | 'create_session' | 'open_session'>>;
|
|
63
|
+
discover(request: SessionDiscoveryRequest, context: ServerRequestContext): Promise<NativeSessionDirectory>;
|
|
64
|
+
mutate(request: SessionMutationRequest, context: ServerRequestContext): Promise<SessionManagementReceipt>;
|
|
65
|
+
private admit;
|
|
66
|
+
}
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { lstat, mkdir, realpath, stat } from 'node:fs/promises';
|
|
3
|
+
import { isAbsolute, join } from 'node:path';
|
|
4
|
+
import { AgentSessionInUseError } from '../../native-providers.js';
|
|
5
|
+
export async function allocateSessionWorkspace(root, date = new Date()) {
|
|
6
|
+
if (!isAbsolute(root))
|
|
7
|
+
throw new Error('session_workspace_unavailable');
|
|
8
|
+
const canonical = await realpath(root);
|
|
9
|
+
if (!(await stat(canonical)).isDirectory())
|
|
10
|
+
throw new Error('session_workspace_unavailable');
|
|
11
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
12
|
+
const day = join(canonical, `${pad(date.getFullYear() % 100)}${pad(date.getMonth() + 1)}${pad(date.getDate())}`);
|
|
13
|
+
try {
|
|
14
|
+
await mkdir(day, { mode: 0o700 });
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error.code !== 'EEXIST')
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
if ((await lstat(day)).isSymbolicLink() || await realpath(day) !== day)
|
|
21
|
+
throw new Error('session_workspace_containment_failed');
|
|
22
|
+
for (let attempt = 0; attempt < 16; attempt++) {
|
|
23
|
+
const path = join(day, `${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}${randomBytes(5).toString('hex')}`);
|
|
24
|
+
try {
|
|
25
|
+
await mkdir(path, { mode: 0o700 });
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error.code === 'EEXIST')
|
|
29
|
+
continue;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
if (await realpath(day) !== day || await realpath(path) !== path)
|
|
33
|
+
throw new Error('session_workspace_containment_failed');
|
|
34
|
+
return path;
|
|
35
|
+
}
|
|
36
|
+
throw new Error('session_workspace_capacity');
|
|
37
|
+
}
|
|
38
|
+
function route(request) {
|
|
39
|
+
return { agent_id: request.agent_id, channel_id: request.channel_id, connection_generation: request.connection_generation };
|
|
40
|
+
}
|
|
41
|
+
/** Native admission shares the Host registry but never selects a channel default. */
|
|
42
|
+
export class HostSessionManagement {
|
|
43
|
+
options;
|
|
44
|
+
admission = Promise.resolve();
|
|
45
|
+
uncommittedClaims = new Map();
|
|
46
|
+
cursors = new Map();
|
|
47
|
+
generation;
|
|
48
|
+
knownClaims = Object.create(null);
|
|
49
|
+
canExpose(nativeId, agentId, channelId) {
|
|
50
|
+
const claim = this.knownClaims[nativeId] ?? this.uncommittedClaims.get(nativeId);
|
|
51
|
+
return !claim || this.owned(claim, { agent_id: agentId, channel_id: channelId });
|
|
52
|
+
}
|
|
53
|
+
constructor(options) {
|
|
54
|
+
this.options = options;
|
|
55
|
+
}
|
|
56
|
+
current(request, context) {
|
|
57
|
+
return request.agent_id === this.options.resolveAgentId() && !context.signal.aborted && context.isConnectionCurrent?.() !== false && Date.now() < request.deadline_at;
|
|
58
|
+
}
|
|
59
|
+
async claims() {
|
|
60
|
+
const rows = await this.options.store.load(this.options.resolveAgentId());
|
|
61
|
+
const result = Object.create(null);
|
|
62
|
+
for (const [id, text] of Object.entries(rows)) {
|
|
63
|
+
const value = JSON.parse(text);
|
|
64
|
+
if (!value || typeof value.agentId !== 'string' || typeof value.server !== 'string' || typeof value.channelId !== 'string' || typeof value.cwd !== 'string'
|
|
65
|
+
|| value.handle?.sessionId !== id || value.handle.providerId !== 'codex' || typeof value.handle.opaque !== 'string')
|
|
66
|
+
throw new Error('session_ownership_unavailable');
|
|
67
|
+
result[id] = value;
|
|
68
|
+
}
|
|
69
|
+
for (const [id, claim] of this.uncommittedClaims) {
|
|
70
|
+
const committed = result[id];
|
|
71
|
+
if (committed && (committed.agentId !== claim.agentId || committed.channelId !== claim.channelId || committed.server !== claim.server || committed.cwd !== claim.cwd))
|
|
72
|
+
throw new Error('session_ownership_unavailable');
|
|
73
|
+
result[id] = claim;
|
|
74
|
+
}
|
|
75
|
+
this.knownClaims = result;
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
async commitClaims(claims, agentId) {
|
|
79
|
+
await this.options.store.save(agentId, Object.fromEntries(Object.entries(claims).map(([id, claim]) => [id, JSON.stringify(claim)])));
|
|
80
|
+
this.knownClaims = claims;
|
|
81
|
+
this.uncommittedClaims.clear();
|
|
82
|
+
}
|
|
83
|
+
owned(claim, request) {
|
|
84
|
+
return claim.agentId === request.agent_id && claim.channelId === request.channel_id && claim.server === this.options.serverUrl;
|
|
85
|
+
}
|
|
86
|
+
withAdmission(work) {
|
|
87
|
+
const run = () => this.options.withOwnershipLock ? this.options.withOwnershipLock(work) : work();
|
|
88
|
+
const result = this.admission.catch(() => { }).then(run);
|
|
89
|
+
this.admission = result;
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
async assertUnclaimed(nativeId) {
|
|
93
|
+
if ((await this.claims())[nativeId])
|
|
94
|
+
throw new Error('session_owned_elsewhere');
|
|
95
|
+
}
|
|
96
|
+
/** All observer admission paths share the durable independent identity reservation. */
|
|
97
|
+
async admitUnclaimed(nativeId, work) {
|
|
98
|
+
return this.withAdmission(async () => {
|
|
99
|
+
if ((await this.claims())[nativeId])
|
|
100
|
+
return undefined;
|
|
101
|
+
return work();
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async lineageReason(row, claims, protectedIds, request, loaded) {
|
|
105
|
+
if (protectedIds.has(row.nativeSessionId))
|
|
106
|
+
return 'session_default_owned';
|
|
107
|
+
const claim = claims[row.nativeSessionId];
|
|
108
|
+
if (claim && !this.owned(claim, request))
|
|
109
|
+
return 'session_owned_elsewhere';
|
|
110
|
+
if (claim || loaded) {
|
|
111
|
+
if (loaded && (loaded.target.agent_id !== request.agent_id || loaded.target.channel_id !== request.channel_id
|
|
112
|
+
|| loaded.target.connection_generation !== request.connection_generation || loaded.target.native_session_id !== row.nativeSessionId))
|
|
113
|
+
return 'session_native_identity_changed';
|
|
114
|
+
if (!claim && (!loaded?.persistence || loaded.persistence.sessionId !== row.nativeSessionId
|
|
115
|
+
|| loaded.persistence.providerId !== 'codex' || typeof loaded.persistence.opaque !== 'string'))
|
|
116
|
+
return 'session_native_identity_changed';
|
|
117
|
+
try {
|
|
118
|
+
const cwd = row.workspace && await realpath(row.workspace);
|
|
119
|
+
if (!cwd || claim && cwd !== claim.cwd || loaded && cwd !== loaded.workspace_path)
|
|
120
|
+
return 'session_native_identity_changed';
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return 'session_native_identity_changed';
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const seen = new Set();
|
|
128
|
+
for (let depth = 0; depth < 64; depth++) {
|
|
129
|
+
if (protectedIds.has(row.nativeSessionId))
|
|
130
|
+
return 'session_default_owned';
|
|
131
|
+
if (claims[row.nativeSessionId] && !this.owned(claims[row.nativeSessionId], request))
|
|
132
|
+
return 'session_owned_elsewhere';
|
|
133
|
+
if (row.lineageUnavailable || seen.has(row.nativeSessionId))
|
|
134
|
+
return 'session_lineage_unavailable';
|
|
135
|
+
seen.add(row.nativeSessionId);
|
|
136
|
+
if (!row.parentNativeSessionId)
|
|
137
|
+
return undefined;
|
|
138
|
+
const parentId = row.parentNativeSessionId;
|
|
139
|
+
try {
|
|
140
|
+
row = await this.options.provider.inspectSession(parentId);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return 'session_lineage_unavailable';
|
|
144
|
+
}
|
|
145
|
+
if (row.nativeSessionId !== parentId)
|
|
146
|
+
return 'session_lineage_unavailable';
|
|
147
|
+
}
|
|
148
|
+
return 'session_lineage_unavailable';
|
|
149
|
+
}
|
|
150
|
+
async operations() {
|
|
151
|
+
await this.claims();
|
|
152
|
+
await this.options.loadProtectedNativeIds();
|
|
153
|
+
return ['discover_sessions', 'open_session', ...(this.options.root ? ['create_session'] : [])];
|
|
154
|
+
}
|
|
155
|
+
async discover(request, context) {
|
|
156
|
+
if (!this.current(request, context))
|
|
157
|
+
throw new Error('stale_session_binding');
|
|
158
|
+
if (this.generation !== request.connection_generation) {
|
|
159
|
+
this.cursors.clear();
|
|
160
|
+
this.generation = request.connection_generation;
|
|
161
|
+
}
|
|
162
|
+
const query = JSON.stringify([request.agent_id, request.channel_id, request.actor_id, request.connection_generation, request.workspace_path ?? null, request.limit]);
|
|
163
|
+
let cursor;
|
|
164
|
+
let incomplete = false;
|
|
165
|
+
if (request.cursor) {
|
|
166
|
+
const saved = this.cursors.get(request.cursor);
|
|
167
|
+
if (!saved || saved.query !== query)
|
|
168
|
+
throw new Error('session_catalog_cursor_invalid');
|
|
169
|
+
if (saved.expires < Date.now())
|
|
170
|
+
throw new Error('session_catalog_cursor_expired');
|
|
171
|
+
cursor = saved.cursor;
|
|
172
|
+
incomplete = saved.incomplete;
|
|
173
|
+
}
|
|
174
|
+
const claims = await this.claims();
|
|
175
|
+
const protectedIds = await this.options.loadProtectedNativeIds();
|
|
176
|
+
let page;
|
|
177
|
+
try {
|
|
178
|
+
page = await this.options.provider.listSessions({ includeAllSources: true, limit: request.limit, ...(cursor ? { cursor } : {}) });
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
throw new Error('session_catalog_unavailable');
|
|
182
|
+
}
|
|
183
|
+
if (!this.current(request, context))
|
|
184
|
+
throw new Error('stale_session_binding');
|
|
185
|
+
const eligible = [];
|
|
186
|
+
for (const row of page.sessions) {
|
|
187
|
+
if (request.workspace_path && row.workspace !== request.workspace_path)
|
|
188
|
+
continue;
|
|
189
|
+
const loaded = await this.options.find(row.nativeSessionId, request.channel_id, request.connection_generation);
|
|
190
|
+
const reason = await this.lineageReason(row, claims, protectedIds, request, loaded);
|
|
191
|
+
if (reason === 'session_lineage_unavailable')
|
|
192
|
+
incomplete = true;
|
|
193
|
+
if (!reason)
|
|
194
|
+
eligible.push(row);
|
|
195
|
+
}
|
|
196
|
+
if (!this.current(request, context))
|
|
197
|
+
throw new Error('stale_session_binding');
|
|
198
|
+
const sessions = eligible.map(row => ({ native_session_id: row.nativeSessionId, title: row.title,
|
|
199
|
+
...(row.workspace ? { workspace_path: row.workspace } : {}), updated_at: row.updatedAt,
|
|
200
|
+
state: row.state === 'running' || row.state === 'waiting' ? 'running' : row.state === 'idle' ? 'idle' : 'unknown' }));
|
|
201
|
+
if (!page.nextCursor)
|
|
202
|
+
return incomplete
|
|
203
|
+
? { ...route(request), sessions, complete: false, limitation: 'source_incomplete' }
|
|
204
|
+
: { ...route(request), sessions, complete: true };
|
|
205
|
+
if (this.cursors.size >= 512)
|
|
206
|
+
return { ...route(request), sessions, complete: false, limitation: 'capacity_limit' };
|
|
207
|
+
const token = randomUUID();
|
|
208
|
+
this.cursors.set(token, { query, cursor: page.nextCursor, expires: Date.now() + 300_000, incomplete });
|
|
209
|
+
return { ...route(request), sessions, complete: false, next_cursor: token };
|
|
210
|
+
}
|
|
211
|
+
mutate(request, context) {
|
|
212
|
+
const allocation = { root: this.options.root, date: new Date() };
|
|
213
|
+
return this.withAdmission(() => this.admit(request, context, allocation)).catch(() => ({ ...route(request), operation_id: request.operation_id, status: 'rejected', reason: 'session_ownership_unavailable' }));
|
|
214
|
+
}
|
|
215
|
+
async admit(request, context, allocation) {
|
|
216
|
+
const base = { ...route(request), operation_id: request.operation_id };
|
|
217
|
+
const reject = (reason) => ({ ...base, status: 'rejected', reason });
|
|
218
|
+
if (!this.current(request, context))
|
|
219
|
+
return reject('stale_session_binding');
|
|
220
|
+
const claims = await this.claims();
|
|
221
|
+
const protectedIds = await this.options.loadProtectedNativeIds();
|
|
222
|
+
if (this.uncommittedClaims.size)
|
|
223
|
+
await this.commitClaims(claims, request.agent_id);
|
|
224
|
+
let cwd;
|
|
225
|
+
let nativeId;
|
|
226
|
+
if (request.action === 'open_session') {
|
|
227
|
+
if (request.target === 'channel_default')
|
|
228
|
+
return reject('session_default_open_unsupported');
|
|
229
|
+
nativeId = request.native_session_id;
|
|
230
|
+
if (protectedIds.has(nativeId))
|
|
231
|
+
return reject('session_default_owned');
|
|
232
|
+
const claim = claims[nativeId];
|
|
233
|
+
if (claim && !this.owned(claim, request))
|
|
234
|
+
return reject('session_owned_elsewhere');
|
|
235
|
+
let inspected;
|
|
236
|
+
try {
|
|
237
|
+
inspected = await this.options.provider.inspectSession(nativeId);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return reject('session_lineage_unavailable');
|
|
241
|
+
}
|
|
242
|
+
if (inspected.nativeSessionId !== nativeId)
|
|
243
|
+
return reject('session_lineage_unavailable');
|
|
244
|
+
const loaded = await this.options.find(nativeId, request.channel_id, request.connection_generation);
|
|
245
|
+
const lineage = await this.lineageReason(inspected, claims, protectedIds, request, loaded);
|
|
246
|
+
if (lineage)
|
|
247
|
+
return reject(lineage);
|
|
248
|
+
if (!this.current(request, context))
|
|
249
|
+
return reject('stale_session_binding');
|
|
250
|
+
if (loaded) {
|
|
251
|
+
if (claim && claim.cwd !== loaded.workspace_path)
|
|
252
|
+
return reject('session_native_identity_changed');
|
|
253
|
+
if (!claim) {
|
|
254
|
+
const handle = loaded.persistence;
|
|
255
|
+
if (!handle || handle.sessionId !== nativeId || handle.providerId !== 'codex' || typeof handle.opaque !== 'string')
|
|
256
|
+
return reject('session_native_identity_changed');
|
|
257
|
+
claims[nativeId] = { agentId: request.agent_id, server: this.options.serverUrl, channelId: request.channel_id, cwd: loaded.workspace_path, handle };
|
|
258
|
+
this.uncommittedClaims.set(nativeId, claims[nativeId]);
|
|
259
|
+
try {
|
|
260
|
+
await this.commitClaims(claims, request.agent_id);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return { ...base, status: 'unconfirmed', reason: 'session_admission_unconfirmed', native_session_id: nativeId };
|
|
264
|
+
}
|
|
265
|
+
if (!this.current(request, context))
|
|
266
|
+
return { ...base, status: 'unconfirmed', reason: 'stale_session_binding', native_session_id: nativeId };
|
|
267
|
+
}
|
|
268
|
+
return { ...base, status: 'confirmed', target: loaded.target, workspace_path: loaded.workspace_path };
|
|
269
|
+
}
|
|
270
|
+
if (claim)
|
|
271
|
+
cwd = claim.cwd;
|
|
272
|
+
else {
|
|
273
|
+
let cursor;
|
|
274
|
+
let found;
|
|
275
|
+
for (let scan = 0; scan < 64; scan++) {
|
|
276
|
+
if (!this.current(request, context))
|
|
277
|
+
return reject('stale_session_binding');
|
|
278
|
+
const page = await this.options.provider.listSessions({ includeAllSources: true, limit: 100, ...(cursor ? { cursor } : {}) });
|
|
279
|
+
found = page.sessions.find(row => row.nativeSessionId === nativeId);
|
|
280
|
+
if (found || !page.nextCursor)
|
|
281
|
+
break;
|
|
282
|
+
cursor = page.nextCursor;
|
|
283
|
+
}
|
|
284
|
+
if (!found?.workspace)
|
|
285
|
+
return reject('session_catalog_unavailable');
|
|
286
|
+
cwd = await realpath(found.workspace);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
if (!allocation.root)
|
|
291
|
+
return { ...base, status: 'unsupported', reason: 'session_workspace_unconfigured' };
|
|
292
|
+
try {
|
|
293
|
+
cwd = await allocateSessionWorkspace(allocation.root, allocation.date);
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return reject('session_workspace_unavailable');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!this.current(request, context))
|
|
300
|
+
return reject('stale_session_binding');
|
|
301
|
+
let session;
|
|
302
|
+
let registered = false;
|
|
303
|
+
try {
|
|
304
|
+
session = request.action === 'open_session'
|
|
305
|
+
? await this.options.provider.resumeSession({ providerId: 'codex', sessionId: nativeId, opaque: '{}' })
|
|
306
|
+
: await this.options.provider.createSession({ sessionId: randomUUID(), cwd });
|
|
307
|
+
const info = await session.runtimeInfo();
|
|
308
|
+
if (!info.sessionId || info.providerId !== 'codex' || nativeId && info.sessionId !== nativeId)
|
|
309
|
+
throw new Error('session_native_identity_changed');
|
|
310
|
+
nativeId = info.sessionId;
|
|
311
|
+
if (!info.cwd || await realpath(info.cwd) !== cwd || !info.persistence || info.persistence.sessionId !== nativeId || info.persistence.providerId !== 'codex')
|
|
312
|
+
throw new Error('session_native_identity_changed');
|
|
313
|
+
if (protectedIds.has(nativeId) || claims[nativeId] && !this.owned(claims[nativeId], request))
|
|
314
|
+
throw new Error('session_owned_elsewhere');
|
|
315
|
+
claims[nativeId] = { agentId: request.agent_id, server: this.options.serverUrl, channelId: request.channel_id, cwd, handle: info.persistence };
|
|
316
|
+
this.uncommittedClaims.set(nativeId, claims[nativeId]);
|
|
317
|
+
await this.commitClaims(claims, request.agent_id);
|
|
318
|
+
if (!this.current(request, context))
|
|
319
|
+
throw new Error('stale_session_binding');
|
|
320
|
+
const admitted = await this.options.register(session, request.channel_id, cwd, request.connection_generation, () => this.current(request, context));
|
|
321
|
+
registered = true;
|
|
322
|
+
return { ...base, status: 'confirmed', ...admitted };
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
if (!session && error instanceof AgentSessionInUseError)
|
|
326
|
+
return reject('session_in_use');
|
|
327
|
+
return { ...base, status: 'unconfirmed', reason: 'session_admission_unconfirmed', ...(nativeId ? { native_session_id: nativeId } : {}) };
|
|
328
|
+
}
|
|
329
|
+
finally {
|
|
330
|
+
if (session && !registered)
|
|
331
|
+
await session.dispose().catch(() => { });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Reads ownership evidence without initializing, migrating, or writing default stores. */
|
|
2
|
+
export declare function readProtectedCodexSessions(stateRoot: string): Promise<Set<string>>;
|
|
3
|
+
export declare function createSessionOwnershipLock(directory: string): <T>(work: () => Promise<T>) => Promise<T>;
|