@borgee/agents-host 0.2.56 → 0.2.62
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 +11 -10
- package/dist/agents-host.d.ts +1 -0
- package/dist/agents-host.js +103 -9
- package/dist/compatibility-gates.js +2 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +21 -0
- package/dist/context/injection.d.ts +20 -2
- package/dist/context/injection.js +146 -103
- package/dist/context/prompt.js +11 -7
- package/dist/context/resolved-workspace.d.ts +1 -0
- package/dist/context/resolved-workspace.js +50 -8
- package/dist/context/turn-preparation.js +8 -0
- package/dist/gateway/localhost-gateway.js +2 -5
- package/dist/local-config.js +11 -1
- package/dist/managed-daemon.js +18 -13
- package/dist/plugin-sdk.js +0 -17
- package/dist/plugin-sdk.js.map +2 -2
- package/dist/policy/copilot-permission.d.ts +1 -0
- package/dist/policy/copilot-permission.js +18 -0
- package/dist/policy/gateway-authorization.js +2 -2
- package/dist/providers/claude/cli-client.d.ts +1 -1
- package/dist/providers/claude/cli-client.js +30 -3
- package/dist/providers/codex/cli-client.d.ts +2 -0
- package/dist/providers/codex/cli-client.js +25 -21
- package/dist/providers/copilot/cli-client.d.ts +1 -1
- package/dist/providers/copilot/cli-client.js +9 -3
- package/dist/providers/create-provider.d.ts +1 -1
- package/dist/providers/create-provider.js +2 -0
- package/dist/types.d.ts +16 -3
- package/package.json +2 -2
- package/skills/borgee-agent/SKILL.md +4 -4
- package/skills/borgee-agent/references/errors.md +2 -2
- package/skills/borgee-agent/references/task-properties.md +4 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
1
|
+
import { constants as fsConstants, promises as fs } from 'node:fs';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -7,14 +7,16 @@ import { findTaskForThread } from '../task-thread-resolution.js';
|
|
|
7
7
|
import { buildClaudeFileBrief } from './claude-file-brief.js';
|
|
8
8
|
const CHANNEL_CONTEXT_ROOT_DIRNAME = 'channel-context';
|
|
9
9
|
const CHANNEL_CONTEXT_PAYLOAD_FILENAME = 'context.json';
|
|
10
|
+
const TASK_ASSIGNMENT_STATE_ROOT_DIRNAME = 'task-assignment-state';
|
|
11
|
+
const TASK_ASSIGNMENT_STATE_FILENAME = 'context.json';
|
|
10
12
|
const CHANNEL_GATEWAY_CREDENTIAL_FILENAME = '.borgee-agent-gateway.json';
|
|
11
13
|
const CLAUDE_PROJECTED_BRIEF_FILENAME = 'CLAUDE.md';
|
|
12
14
|
const DEFAULT_HOME_BORGEE_DIRNAME = '.borgee';
|
|
13
15
|
const CHANNEL_WORKSPACE_COLLECTION_DIRNAME = 'channels';
|
|
14
16
|
const CHANNEL_WORKSPACE_DIRNAME = 'workspace';
|
|
15
17
|
const TASK_ISOLATED_WORKSPACE_COLLECTION_DIRNAME = 'tasks';
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
+
const TASK_THREAD_SCRATCH_WORKSPACE_DIRNAME = 'scratch';
|
|
19
|
+
const TASK_EXECUTION_LOCAL_DIRECTORY_PROPERTY_KEY = 'execution.local_directory';
|
|
18
20
|
const DEFAULT_TASK_WORKSPACE_RESOLUTION_TIMEOUT_MS = 1_500;
|
|
19
21
|
export class ChannelContextPreparationError extends Error {
|
|
20
22
|
partialContext;
|
|
@@ -43,8 +45,11 @@ const DEFAULT_FILE_SYSTEM = {
|
|
|
43
45
|
async rename(oldPath, newPath) {
|
|
44
46
|
await fs.rename(oldPath, newPath);
|
|
45
47
|
},
|
|
46
|
-
async access(path) {
|
|
47
|
-
await fs.access(path);
|
|
48
|
+
async access(path, mode) {
|
|
49
|
+
await fs.access(path, mode);
|
|
50
|
+
},
|
|
51
|
+
async stat(path) {
|
|
52
|
+
return await fs.stat(path);
|
|
48
53
|
},
|
|
49
54
|
};
|
|
50
55
|
const BORGEE_AGENT_SKILL_DIRNAME = 'borgee-agent';
|
|
@@ -231,9 +236,9 @@ function buildTaskAssignmentContextForTurn(options, existingContext) {
|
|
|
231
236
|
}
|
|
232
237
|
return existingContext;
|
|
233
238
|
}
|
|
234
|
-
async function readExistingTaskAssignmentContext(
|
|
239
|
+
async function readExistingTaskAssignmentContext(statePath, fileSystem) {
|
|
235
240
|
try {
|
|
236
|
-
const raw = await fileSystem.readFile(
|
|
241
|
+
const raw = await fileSystem.readFile(statePath, { encoding: 'utf8' });
|
|
237
242
|
const payload = JSON.parse(raw);
|
|
238
243
|
return sanitizeTaskAssignmentContext(payload.taskAssignmentContext);
|
|
239
244
|
}
|
|
@@ -241,125 +246,124 @@ async function readExistingTaskAssignmentContext(payloadPath, fileSystem) {
|
|
|
241
246
|
return undefined;
|
|
242
247
|
}
|
|
243
248
|
}
|
|
244
|
-
function
|
|
245
|
-
|
|
246
|
-
|
|
249
|
+
function buildChannelResolvedWorkspace(workspaceCollectionRootDir, channelId) {
|
|
250
|
+
return {
|
|
251
|
+
authority: 'channel',
|
|
252
|
+
owningChannelId: channelId,
|
|
253
|
+
rootPath: resolveChannelWorkspaceDirectory(workspaceCollectionRootDir, channelId),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function buildTaskThreadScratchResolvedWorkspace(workspaceCollectionRootDir, channelId, reason = 'missing-or-invalid-execution-target') {
|
|
257
|
+
return {
|
|
258
|
+
authority: 'task-thread-scratch',
|
|
259
|
+
rootPath: resolveTaskThreadScratchWorkspaceDirectory(workspaceCollectionRootDir, channelId),
|
|
260
|
+
reason,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
async function isCrossAgentTaskCreatedByAnotherAgent(params) {
|
|
264
|
+
const selfAgentId = params.resolveStableAgentId?.()?.trim();
|
|
265
|
+
if (!selfAgentId) {
|
|
266
|
+
return true;
|
|
247
267
|
}
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
&& value.owningChannelId.length > 0) {
|
|
251
|
-
return {
|
|
252
|
-
authority: 'channel',
|
|
253
|
-
owningChannelId: value.owningChannelId,
|
|
254
|
-
rootPath: value.rootPath,
|
|
255
|
-
};
|
|
268
|
+
if (params.task.createdBy === selfAgentId) {
|
|
269
|
+
return false;
|
|
256
270
|
}
|
|
257
|
-
if (
|
|
258
|
-
|
|
259
|
-
&& isUsableTaskId(value.taskId.trim())) {
|
|
260
|
-
return {
|
|
261
|
-
authority: 'task',
|
|
262
|
-
taskId: value.taskId.trim(),
|
|
263
|
-
rootPath: value.rootPath,
|
|
264
|
-
};
|
|
271
|
+
if (!params.taskReader) {
|
|
272
|
+
return true;
|
|
265
273
|
}
|
|
266
|
-
|
|
274
|
+
const users = await params.taskReader.listUsers();
|
|
275
|
+
const creator = users.find((user) => user.id === params.task.createdBy);
|
|
276
|
+
if (!creator) {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
return creator.kind === 'agent';
|
|
267
280
|
}
|
|
268
|
-
async function
|
|
281
|
+
async function withDeadline(promise, timeoutMs, timeoutMessage) {
|
|
282
|
+
if (timeoutMs <= 0) {
|
|
283
|
+
return await promise;
|
|
284
|
+
}
|
|
285
|
+
let timer;
|
|
269
286
|
try {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
287
|
+
return await Promise.race([
|
|
288
|
+
promise,
|
|
289
|
+
new Promise((_, reject) => {
|
|
290
|
+
timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
|
|
291
|
+
}),
|
|
292
|
+
]);
|
|
273
293
|
}
|
|
274
|
-
|
|
275
|
-
|
|
294
|
+
finally {
|
|
295
|
+
if (timer) {
|
|
296
|
+
clearTimeout(timer);
|
|
297
|
+
}
|
|
276
298
|
}
|
|
277
299
|
}
|
|
278
|
-
function
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
owningChannelId: channelId,
|
|
282
|
-
rootPath: resolveChannelWorkspaceDirectory(workspaceCollectionRootDir, channelId),
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
function buildStickyChannelResolvedWorkspace(workspaceCollectionRootDir, resolvedWorkspace) {
|
|
286
|
-
if (resolvedWorkspace?.authority !== 'channel') {
|
|
287
|
-
return undefined;
|
|
300
|
+
async function isExistingDirectory(fileSystem, path) {
|
|
301
|
+
if (!isAbsolute(path)) {
|
|
302
|
+
return false;
|
|
288
303
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
304
|
+
try {
|
|
305
|
+
await fileSystem.access(path, fsConstants.R_OK | fsConstants.W_OK | fsConstants.X_OK);
|
|
306
|
+
if (typeof fileSystem.stat !== 'function') {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
return (await fileSystem.stat(path)).isDirectory();
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return false;
|
|
292
313
|
}
|
|
293
|
-
return {
|
|
294
|
-
authority: 'channel',
|
|
295
|
-
owningChannelId: resolvedWorkspace.owningChannelId,
|
|
296
|
-
rootPath: expectedRootPath,
|
|
297
|
-
};
|
|
298
314
|
}
|
|
299
315
|
async function resolveTaskThreadWorkspaceContext(params) {
|
|
300
316
|
const channelWorkspace = buildChannelResolvedWorkspace(params.workspaceCollectionRootDir, params.channelId);
|
|
317
|
+
const discussionOnlyWorkspace = buildTaskThreadScratchResolvedWorkspace(params.workspaceCollectionRootDir, params.channelId);
|
|
301
318
|
const taskAssignmentContext = params.taskAssignmentContext;
|
|
302
319
|
const currentTaskId = taskAssignmentContext?.currentTaskId?.trim();
|
|
303
|
-
if (taskAssignmentContext?.active !== true
|
|
304
|
-
|| !currentTaskId
|
|
305
|
-
|| !params.taskReader) {
|
|
320
|
+
if (taskAssignmentContext?.active !== true) {
|
|
306
321
|
return { taskAssignmentContext, resolvedWorkspace: channelWorkspace };
|
|
307
322
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
authority: 'task',
|
|
312
|
-
taskId: currentTaskId,
|
|
313
|
-
rootPath: resolveTaskWorkspaceDirectory(params.workspaceCollectionRootDir, params.channelId, currentTaskId),
|
|
314
|
-
}
|
|
315
|
-
: undefined;
|
|
316
|
-
const stickyChannelWorkspace = buildStickyChannelResolvedWorkspace(params.workspaceCollectionRootDir, params.existingResolvedWorkspace);
|
|
323
|
+
if (!currentTaskId || !params.taskReader) {
|
|
324
|
+
return { taskAssignmentContext, resolvedWorkspace: discussionOnlyWorkspace };
|
|
325
|
+
}
|
|
317
326
|
try {
|
|
318
327
|
const task = await withDeadline(findTaskForThread(params.taskReader, params.channelId, currentTaskId), params.taskResolutionTimeoutMs, `workspace resolution timed out for task ${currentTaskId}`);
|
|
319
328
|
if (!task) {
|
|
320
329
|
return {
|
|
321
330
|
taskAssignmentContext,
|
|
322
|
-
resolvedWorkspace:
|
|
331
|
+
resolvedWorkspace: discussionOnlyWorkspace,
|
|
323
332
|
};
|
|
324
333
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
new Promise((_, reject) => {
|
|
334
|
-
timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
|
|
335
|
-
}),
|
|
336
|
-
]);
|
|
337
|
-
}
|
|
338
|
-
finally {
|
|
339
|
-
if (timer) {
|
|
340
|
-
clearTimeout(timer);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
334
|
+
const executionLocalDirectory = task.properties[TASK_EXECUTION_LOCAL_DIRECTORY_PROPERTY_KEY];
|
|
335
|
+
if (typeof executionLocalDirectory !== 'string'
|
|
336
|
+
|| executionLocalDirectory.length === 0
|
|
337
|
+
|| !(await isExistingDirectory(params.fileSystem, executionLocalDirectory))) {
|
|
338
|
+
return {
|
|
339
|
+
taskAssignmentContext,
|
|
340
|
+
resolvedWorkspace: discussionOnlyWorkspace,
|
|
341
|
+
};
|
|
343
342
|
}
|
|
344
|
-
if (
|
|
343
|
+
if (!params.allowCrossAgentIndependentWorkspaceHandoff
|
|
344
|
+
&& await isCrossAgentTaskCreatedByAnotherAgent({
|
|
345
|
+
task,
|
|
346
|
+
taskReader: params.taskReader,
|
|
347
|
+
resolveStableAgentId: params.resolveStableAgentId,
|
|
348
|
+
})) {
|
|
345
349
|
return {
|
|
346
350
|
taskAssignmentContext,
|
|
347
|
-
resolvedWorkspace:
|
|
351
|
+
resolvedWorkspace: buildTaskThreadScratchResolvedWorkspace(params.workspaceCollectionRootDir, params.channelId, 'cross-agent-independent-workspace-disabled'),
|
|
348
352
|
};
|
|
349
353
|
}
|
|
350
354
|
return {
|
|
351
355
|
taskAssignmentContext,
|
|
352
356
|
resolvedWorkspace: {
|
|
353
|
-
authority: 'task',
|
|
357
|
+
authority: 'task-execution-target',
|
|
354
358
|
taskId: task.id,
|
|
355
|
-
rootPath:
|
|
359
|
+
rootPath: executionLocalDirectory,
|
|
356
360
|
},
|
|
357
361
|
};
|
|
358
362
|
}
|
|
359
363
|
catch (error) {
|
|
360
364
|
return {
|
|
361
365
|
taskAssignmentContext,
|
|
362
|
-
resolvedWorkspace:
|
|
366
|
+
resolvedWorkspace: discussionOnlyWorkspace,
|
|
363
367
|
};
|
|
364
368
|
}
|
|
365
369
|
}
|
|
@@ -373,6 +377,12 @@ export function resolveChannelContextDirectory(stateRootDir, channelId) {
|
|
|
373
377
|
export function resolveChannelContextPayloadPath(stateRootDir, channelId) {
|
|
374
378
|
return join(resolveChannelContextDirectory(stateRootDir, channelId), CHANNEL_CONTEXT_PAYLOAD_FILENAME);
|
|
375
379
|
}
|
|
380
|
+
export function resolveTaskAssignmentStateDirectory(stateRootDir, channelId) {
|
|
381
|
+
return resolve(stateRootDir, TASK_ASSIGNMENT_STATE_ROOT_DIRNAME, encodeChannelPathSegment(channelId));
|
|
382
|
+
}
|
|
383
|
+
export function resolveTaskAssignmentStatePath(stateRootDir, channelId) {
|
|
384
|
+
return join(resolveTaskAssignmentStateDirectory(stateRootDir, channelId), TASK_ASSIGNMENT_STATE_FILENAME);
|
|
385
|
+
}
|
|
376
386
|
export function resolveTaskWorkspaceRootDirectory(startupWorkspaceRootDir) {
|
|
377
387
|
return join(resolve(startupWorkspaceRootDir), TASK_ISOLATED_WORKSPACE_COLLECTION_DIRNAME);
|
|
378
388
|
}
|
|
@@ -397,6 +407,9 @@ export function resolveTaskIsolatedWorkspaceDirectory(workspaceCollectionRootDir
|
|
|
397
407
|
return join(resolveTaskWorkspaceRootDirectory(resolveChannelWorkspaceRootDirectory(workspaceCollectionRootDir, channelId)), encodeChannelPathSegment(taskId), CHANNEL_WORKSPACE_DIRNAME);
|
|
398
408
|
}
|
|
399
409
|
export const resolveTaskWorkspaceDirectory = resolveTaskIsolatedWorkspaceDirectory;
|
|
410
|
+
export function resolveTaskThreadScratchWorkspaceDirectory(workspaceCollectionRootDir, channelId) {
|
|
411
|
+
return join(resolveChannelWorkspaceRootDirectory(workspaceCollectionRootDir, channelId), TASK_THREAD_SCRATCH_WORKSPACE_DIRNAME, CHANNEL_WORKSPACE_DIRNAME);
|
|
412
|
+
}
|
|
400
413
|
export function resolveChannelGatewayCredentialPath(stateRootDir, channelId) {
|
|
401
414
|
return join(resolveChannelContextDirectory(stateRootDir, channelId), CHANNEL_GATEWAY_CREDENTIAL_FILENAME);
|
|
402
415
|
}
|
|
@@ -499,6 +512,8 @@ export class FileChannelContextStore {
|
|
|
499
512
|
workspaceCollectionRootDir;
|
|
500
513
|
taskReader;
|
|
501
514
|
taskResolutionTimeoutMs;
|
|
515
|
+
allowCrossAgentIndependentWorkspaceHandoff;
|
|
516
|
+
resolveStableAgentId;
|
|
502
517
|
constructor(stateRootDir, options = {}) {
|
|
503
518
|
this.stateRootDir = stateRootDir;
|
|
504
519
|
this.fileSystem = options.fileSystem ?? DEFAULT_FILE_SYSTEM;
|
|
@@ -508,6 +523,9 @@ export class FileChannelContextStore {
|
|
|
508
523
|
this.workspaceCollectionRootDir = resolveManagedWorkspaceCollectionRoot(options.workspaceCollectionRootDir, options.env, options.resolvedHomeDir);
|
|
509
524
|
this.taskReader = options.taskReader;
|
|
510
525
|
this.taskResolutionTimeoutMs = options.taskResolutionTimeoutMs ?? DEFAULT_TASK_WORKSPACE_RESOLUTION_TIMEOUT_MS;
|
|
526
|
+
this.allowCrossAgentIndependentWorkspaceHandoff
|
|
527
|
+
= options.allowCrossAgentIndependentWorkspaceHandoff ?? true;
|
|
528
|
+
this.resolveStableAgentId = options.resolveStableAgentId;
|
|
511
529
|
}
|
|
512
530
|
async prepare(inputOrChannelId, options) {
|
|
513
531
|
const input = typeof inputOrChannelId === 'string'
|
|
@@ -519,10 +537,10 @@ export class FileChannelContextStore {
|
|
|
519
537
|
const auxiliaryCollaborationEnabled = collaborationCommandsEnabled && collaborationRoutesAllowedForTurnMode;
|
|
520
538
|
const directoryPath = resolveChannelContextDirectory(this.stateRootDir, input.channelId);
|
|
521
539
|
const payloadPath = resolveChannelContextPayloadPath(this.stateRootDir, input.channelId);
|
|
540
|
+
const taskAssignmentStatePath = resolveTaskAssignmentStatePath(this.stateRootDir, input.channelId);
|
|
522
541
|
const claudeProjectedBriefPath = resolveClaudeProjectedBriefPathFromPayloadPath(payloadPath);
|
|
523
542
|
const gatewayCredentialPath = resolveGatewayCredentialPathFromPayloadPath(payloadPath);
|
|
524
|
-
const existingTaskAssignmentContext = await readExistingTaskAssignmentContext(
|
|
525
|
-
const existingResolvedWorkspace = await readExistingResolvedWorkspaceContext(payloadPath, this.fileSystem);
|
|
543
|
+
const existingTaskAssignmentContext = await readExistingTaskAssignmentContext(taskAssignmentStatePath, this.fileSystem);
|
|
526
544
|
const skillRuntime = this.skillRuntimeEnabled
|
|
527
545
|
? await this.resolveSkillRuntimeBestEffort()
|
|
528
546
|
: undefined;
|
|
@@ -548,34 +566,40 @@ export class FileChannelContextStore {
|
|
|
548
566
|
: {}),
|
|
549
567
|
}
|
|
550
568
|
: undefined;
|
|
551
|
-
const taskAssignmentContext =
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
569
|
+
const taskAssignmentContext = input.taskAssignmentContextOverride
|
|
570
|
+
?? buildTaskAssignmentContextForTurn({
|
|
571
|
+
incomingMessageType: input.incomingMessageType,
|
|
572
|
+
incomingContent: input.incomingContent,
|
|
573
|
+
}, existingTaskAssignmentContext);
|
|
555
574
|
const workspaceResolution = await resolveTaskThreadWorkspaceContext({
|
|
556
575
|
channelId: input.channelId,
|
|
557
576
|
workspaceCollectionRootDir: this.workspaceCollectionRootDir,
|
|
558
577
|
taskAssignmentContext,
|
|
559
|
-
existingResolvedWorkspace,
|
|
560
578
|
taskReader: this.taskReader,
|
|
561
579
|
taskResolutionTimeoutMs: this.taskResolutionTimeoutMs,
|
|
580
|
+
allowCrossAgentIndependentWorkspaceHandoff: this.allowCrossAgentIndependentWorkspaceHandoff,
|
|
581
|
+
resolveStableAgentId: this.resolveStableAgentId,
|
|
582
|
+
fileSystem: this.fileSystem,
|
|
562
583
|
});
|
|
563
584
|
const resolvedWorkspace = workspaceResolution.resolvedWorkspace;
|
|
585
|
+
const runtimeTaskAssignmentContext = workspaceResolution.taskAssignmentContext ?? taskAssignmentContext;
|
|
586
|
+
const persistedTaskAssignmentContext = input.taskAssignmentContextOverridePersistence === 'ephemeral'
|
|
587
|
+
? existingTaskAssignmentContext
|
|
588
|
+
: runtimeTaskAssignmentContext;
|
|
564
589
|
const runtimeSurface = buildRuntimeSurfaceForTurn({
|
|
565
590
|
localhostGateway,
|
|
566
591
|
collaboration: input.collaboration,
|
|
567
|
-
taskAssignmentContext:
|
|
592
|
+
taskAssignmentContext: runtimeTaskAssignmentContext,
|
|
568
593
|
});
|
|
569
|
-
const payload = buildChannelContextPayload(input.channelId, runtimeSurface, input.collaborationOutcome, input.attentionSnapshot, input.compactionSnapshot, input.taskThreadCollaborationContract, input.collaborationCapabilities, input.missedCollaborationDiagnostic, skillRuntime, localhostGateway,
|
|
594
|
+
const payload = buildChannelContextPayload(input.channelId, runtimeSurface, input.collaborationOutcome, input.attentionSnapshot, input.compactionSnapshot, input.taskThreadCollaborationContract, input.collaborationCapabilities, input.missedCollaborationDiagnostic, skillRuntime, localhostGateway, persistedTaskAssignmentContext || resolvedWorkspace
|
|
570
595
|
? {
|
|
571
|
-
...(
|
|
572
|
-
? { taskAssignmentContext:
|
|
596
|
+
...(persistedTaskAssignmentContext
|
|
597
|
+
? { taskAssignmentContext: persistedTaskAssignmentContext }
|
|
573
598
|
: {}),
|
|
574
599
|
resolvedWorkspace,
|
|
575
600
|
}
|
|
576
601
|
: undefined);
|
|
577
602
|
const gatewayCredential = buildBorgeeAgentGatewayCredential(input.channelId, issuedLocalhostGateway);
|
|
578
|
-
let resolvedWorkspaceMaterialized = false;
|
|
579
603
|
let claudeProjectedBriefWritten = false;
|
|
580
604
|
let payloadWritten = false;
|
|
581
605
|
let gatewayCredentialWritten = false;
|
|
@@ -601,14 +625,24 @@ export class FileChannelContextStore {
|
|
|
601
625
|
}
|
|
602
626
|
try {
|
|
603
627
|
await this.fileSystem.mkdir(directoryPath, { recursive: true, mode: 0o700 });
|
|
604
|
-
await this.fileSystem.mkdir(
|
|
605
|
-
|
|
628
|
+
await this.fileSystem.mkdir(resolveTaskAssignmentStateDirectory(this.stateRootDir, input.channelId), { recursive: true, mode: 0o700 });
|
|
629
|
+
if (resolvedWorkspace.authority !== 'task-execution-target') {
|
|
630
|
+
await this.fileSystem.mkdir(resolvedWorkspace.rootPath, { recursive: true, mode: 0o700 });
|
|
631
|
+
}
|
|
606
632
|
await pruneGatewayCredentialSidecars(this.fileSystem, directoryPath, gatewayCredential ? CHANNEL_GATEWAY_CREDENTIAL_FILENAME : undefined);
|
|
607
633
|
if (!claudeProjectedBriefContent) {
|
|
608
634
|
await unlinkIfPresent(this.fileSystem, claudeProjectedBriefPath);
|
|
609
635
|
}
|
|
610
636
|
await this.fileSystem.writeFile(payloadPath, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
611
637
|
payloadWritten = true;
|
|
638
|
+
if (persistedTaskAssignmentContext) {
|
|
639
|
+
const stagingTaskAssignmentStatePath = join(resolveTaskAssignmentStateDirectory(this.stateRootDir, input.channelId), `.task-assignment-state.${randomUUID()}.json`);
|
|
640
|
+
await this.fileSystem.writeFile(stagingTaskAssignmentStatePath, `${JSON.stringify({ taskAssignmentContext: persistedTaskAssignmentContext }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
641
|
+
await this.fileSystem.rename(stagingTaskAssignmentStatePath, taskAssignmentStatePath);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
await unlinkIfPresent(this.fileSystem, taskAssignmentStatePath);
|
|
645
|
+
}
|
|
612
646
|
if (gatewayCredential) {
|
|
613
647
|
// Replaced atomically: a CLI invocation left over from an earlier turn must read either
|
|
614
648
|
// the old credential or the new one, never a missing or half-written file, because the
|
|
@@ -639,14 +673,23 @@ export class FileChannelContextStore {
|
|
|
639
673
|
}
|
|
640
674
|
const partialContext = {
|
|
641
675
|
directoryPath,
|
|
642
|
-
payload
|
|
676
|
+
payload: {
|
|
677
|
+
...payload,
|
|
678
|
+
...(resolvedWorkspace.authority === 'task-execution-target' && runtimeTaskAssignmentContext?.active === true
|
|
679
|
+
? {
|
|
680
|
+
resolvedWorkspace: buildTaskThreadScratchResolvedWorkspace(this.workspaceCollectionRootDir, input.channelId),
|
|
681
|
+
}
|
|
682
|
+
: {}),
|
|
683
|
+
},
|
|
643
684
|
claudeProjectedBriefPath: claudeProjectedBriefWritten ? claudeProjectedBriefPath : undefined,
|
|
644
685
|
claudeProjectedBriefHash: claudeProjectedBriefWritten ? preparedContext.claudeProjectedBriefHash : undefined,
|
|
645
686
|
payloadPath: payloadWritten ? payloadPath : undefined,
|
|
646
687
|
gatewayCredentialPath: gatewayCredentialWritten ? gatewayCredentialPath : undefined,
|
|
647
688
|
skillRuntime: payloadWritten ? skillRuntime : undefined,
|
|
648
689
|
localhostGateway: payloadWritten ? localhostGateway : undefined,
|
|
649
|
-
resolvedWorkspace:
|
|
690
|
+
resolvedWorkspace: resolvedWorkspace.authority === 'task-execution-target' && runtimeTaskAssignmentContext?.active === true
|
|
691
|
+
? buildTaskThreadScratchResolvedWorkspace(this.workspaceCollectionRootDir, input.channelId)
|
|
692
|
+
: resolvedWorkspace,
|
|
650
693
|
};
|
|
651
694
|
throw new ChannelContextPreparationError('failed to persist channel context payload', partialContext, { cause: error });
|
|
652
695
|
}
|
package/dist/context/prompt.js
CHANGED
|
@@ -39,7 +39,9 @@ function buildLocalhostGatewayPromptLines(context) {
|
|
|
39
39
|
if (!context.skillRuntime) {
|
|
40
40
|
return lines;
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
const taskThreadActive = context.taskAssignmentContext?.active === true
|
|
43
|
+
|| context.runtimeSurface?.task.currentThread === 'task-assignment-thread';
|
|
44
|
+
lines.push(taskThreadActive
|
|
43
45
|
? 'This turn runs inside a task assignment thread.'
|
|
44
46
|
: 'This turn runs in a parent channel, not a task thread.');
|
|
45
47
|
if (context.runtimeSurface?.task.currentThread === 'parent-channel'
|
|
@@ -61,7 +63,7 @@ function buildLocalhostGatewayPromptLines(context) {
|
|
|
61
63
|
const collaborationLive = context.localhostGateway.collaboration?.enabled === true
|
|
62
64
|
&& (context.collaborationTurnMode ?? 'ordinary') === 'ordinary';
|
|
63
65
|
if (!collaborationLive) {
|
|
64
|
-
lines.push('Auxiliary collaboration commands (
|
|
66
|
+
lines.push('Auxiliary collaboration commands (draft, send, mention) are not available this turn.');
|
|
65
67
|
}
|
|
66
68
|
else if (context.collaborationTurnExecutionId) {
|
|
67
69
|
lines.push(`Pass --turn-execution-id ${context.collaborationTurnExecutionId} on send, mention and draft.`);
|
|
@@ -77,13 +79,15 @@ function buildLocalhostGatewayPromptLines(context) {
|
|
|
77
79
|
function buildTaskAssignmentPromptLines(params) {
|
|
78
80
|
const isTaskAssignmentTurn = params.incomingMessageType?.trim() === 'task_assignment';
|
|
79
81
|
const taskAssignmentContext = params.promptContext?.taskAssignmentContext;
|
|
80
|
-
|
|
82
|
+
const taskAssignmentThreadActive = taskAssignmentContext?.active === true
|
|
83
|
+
|| params.promptContext?.runtimeSurface?.task.currentThread === 'task-assignment-thread';
|
|
84
|
+
if (!isTaskAssignmentTurn && !taskAssignmentThreadActive) {
|
|
81
85
|
return [];
|
|
82
86
|
}
|
|
83
87
|
if (params.provider === 'claude'
|
|
84
88
|
&& params.forceInlineForClaude !== true
|
|
85
89
|
&& !isTaskAssignmentTurn
|
|
86
|
-
&&
|
|
90
|
+
&& taskAssignmentThreadActive) {
|
|
87
91
|
return [];
|
|
88
92
|
}
|
|
89
93
|
const lines = isTaskAssignmentTurn
|
|
@@ -180,7 +184,7 @@ function buildClaudeProjectedBriefTurnDeltaLines(context) {
|
|
|
180
184
|
const collaborationLive = context.localhostGateway.collaboration?.enabled === true
|
|
181
185
|
&& (context.collaborationTurnMode ?? 'ordinary') === 'ordinary';
|
|
182
186
|
if (!collaborationLive) {
|
|
183
|
-
lines.push('Auxiliary collaboration commands (
|
|
187
|
+
lines.push('Auxiliary collaboration commands (draft, send, mention) are not available this turn.');
|
|
184
188
|
}
|
|
185
189
|
else if (context.collaborationTurnExecutionId) {
|
|
186
190
|
lines.push(`Pass --turn-execution-id ${context.collaborationTurnExecutionId} on send, mention and draft.`);
|
|
@@ -291,8 +295,8 @@ function buildTurnControlAttentionLines(context) {
|
|
|
291
295
|
const attentionLines = context?.attentionSnapshot
|
|
292
296
|
? [
|
|
293
297
|
'Attention controls are projection-only host hints. They do not bypass server-side mention policy or widen message delivery.',
|
|
294
|
-
`To follow ordinary delivered human messages in this channel, add "attentionUpdate":"follow-channel".`,
|
|
295
|
-
`To
|
|
298
|
+
`To pin a durable follow hint for ordinary delivered human messages in this channel, add "attentionUpdate":"follow-channel".`,
|
|
299
|
+
`To clear an explicit follow hint and return to server-policy wake for this channel, add "attentionUpdate":"unfollow-channel".`,
|
|
296
300
|
`To mute ordinary delivered human wake while keeping explicit mentions and task assignments untouched, add "attentionUpdate":"mute-channel".`,
|
|
297
301
|
`To set a durable channel-level attention hint, add "attentionUpdate":"claim-channel".`,
|
|
298
302
|
...(taskThreadAttentionAvailable
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { PreparedPromptContext, ProviderKind } from '../types.js';
|
|
2
2
|
export declare function buildResolvedWorkspaceGuidanceLines(context: PreparedPromptContext | undefined, provider?: ProviderKind): string[];
|
|
3
|
+
export declare function isDiscussionOnlyResolvedWorkspace(context: PreparedPromptContext | undefined): boolean;
|
|
@@ -3,8 +3,11 @@ function buildWorkspaceRootLine(context) {
|
|
|
3
3
|
if (!resolvedWorkspace) {
|
|
4
4
|
return undefined;
|
|
5
5
|
}
|
|
6
|
-
if (resolvedWorkspace.authority === 'task') {
|
|
7
|
-
return `
|
|
6
|
+
if (resolvedWorkspace.authority === 'task-execution-target') {
|
|
7
|
+
return `Explicit execution local directory for this task thread: ${resolvedWorkspace.rootPath}.`;
|
|
8
|
+
}
|
|
9
|
+
if (resolvedWorkspace.authority === 'task-thread-scratch') {
|
|
10
|
+
return `Discussion-only scratch workspace for this task thread: ${resolvedWorkspace.rootPath}.`;
|
|
8
11
|
}
|
|
9
12
|
if (context.taskAssignmentContext?.active === true) {
|
|
10
13
|
return `Writable workspace root inherited from this task thread's channel: ${resolvedWorkspace.rootPath}.`;
|
|
@@ -15,8 +18,13 @@ function buildWorkspaceModeLine(context) {
|
|
|
15
18
|
if (context.taskAssignmentContext?.active !== true || !context.resolvedWorkspace) {
|
|
16
19
|
return undefined;
|
|
17
20
|
}
|
|
18
|
-
if (context.resolvedWorkspace.authority === 'task') {
|
|
19
|
-
return 'This task thread
|
|
21
|
+
if (context.resolvedWorkspace.authority === 'task-execution-target') {
|
|
22
|
+
return 'This task thread has a valid explicit execution.local_directory target, so execution is bound to that exact existing local directory.';
|
|
23
|
+
}
|
|
24
|
+
if (context.resolvedWorkspace.authority === 'task-thread-scratch') {
|
|
25
|
+
return context.resolvedWorkspace.reason === 'cross-agent-independent-workspace-disabled'
|
|
26
|
+
? 'This task thread has an explicit execution.local_directory target, but this agents-host currently disables cross-agent independent-workspace handoff, so it is discussion-only.'
|
|
27
|
+
: 'This task thread has no valid explicit execution.local_directory target, so it is discussion-only.';
|
|
20
28
|
}
|
|
21
29
|
return 'This task thread inherits the channel workspace rather than using a task-isolated workspace.';
|
|
22
30
|
}
|
|
@@ -24,8 +32,11 @@ function buildWorkspaceLocalityLine(context) {
|
|
|
24
32
|
if (!context.resolvedWorkspace) {
|
|
25
33
|
return undefined;
|
|
26
34
|
}
|
|
27
|
-
if (context.resolvedWorkspace.authority === 'task') {
|
|
28
|
-
return 'This
|
|
35
|
+
if (context.resolvedWorkspace.authority === 'task-execution-target') {
|
|
36
|
+
return 'This is a human-selected existing local directory. The host validates it before use and never creates it.';
|
|
37
|
+
}
|
|
38
|
+
if (context.resolvedWorkspace.authority === 'task-thread-scratch') {
|
|
39
|
+
return 'This scratch workspace is host-managed only for discussion turns. It is not a bound project workspace and does not imply that any target repository is checked out there.';
|
|
29
40
|
}
|
|
30
41
|
if (context.taskAssignmentContext?.active === true) {
|
|
31
42
|
return 'This writable workspace is local to agents-host for this task thread because the task inherits its channel workspace. It does not imply that the target repository has already been checked out there.';
|
|
@@ -36,14 +47,34 @@ function buildCopilotCwdLine(context) {
|
|
|
36
47
|
if (!context.resolvedWorkspace) {
|
|
37
48
|
return undefined;
|
|
38
49
|
}
|
|
39
|
-
if (context.resolvedWorkspace.authority === 'task') {
|
|
40
|
-
return 'GitHub Copilot runs this turn with that
|
|
50
|
+
if (context.resolvedWorkspace.authority === 'task-execution-target') {
|
|
51
|
+
return 'GitHub Copilot runs this turn with that exact local directory as its real cwd.';
|
|
52
|
+
}
|
|
53
|
+
if (context.resolvedWorkspace.authority === 'task-thread-scratch') {
|
|
54
|
+
return 'GitHub Copilot runs this turn with that host-managed scratch workspace as its real cwd.';
|
|
41
55
|
}
|
|
42
56
|
if (context.taskAssignmentContext?.active === true) {
|
|
43
57
|
return 'GitHub Copilot runs this turn with that inherited channel workspace as its real cwd.';
|
|
44
58
|
}
|
|
45
59
|
return 'GitHub Copilot runs this turn with that workspace as its real cwd.';
|
|
46
60
|
}
|
|
61
|
+
function buildDiscussionOnlyPermissionLine(context) {
|
|
62
|
+
if (context.resolvedWorkspace?.authority !== 'task-thread-scratch') {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
return context.resolvedWorkspace.reason === 'cross-agent-independent-workspace-disabled'
|
|
66
|
+
? 'Filesystem and shell tool permissions stay denied in this discussion-only task thread while this agents-host keeps cross-agent independent-workspace handoff disabled.'
|
|
67
|
+
: 'Filesystem and shell tool permissions stay denied in this discussion-only task thread until a human sets a valid execution.local_directory target.';
|
|
68
|
+
}
|
|
69
|
+
function buildProjectEntryPrecheckLine(context) {
|
|
70
|
+
if (context.taskAssignmentContext?.active !== true) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
if (context.resolvedWorkspace?.authority === 'task-execution-target') {
|
|
74
|
+
return 'Precheck before switching this task thread into your own local workspace or project directory: the task\'s parent channel must have exactly one human user. Cross-agent independent-workspace handoff is also governed by this agents-host\'s local policy switch.';
|
|
75
|
+
}
|
|
76
|
+
return 'Before a human switches this task thread into their own local workspace or project directory, verify that the task\'s parent channel has exactly one human user; otherwise keep the thread discussion-only. Cross-agent independent-workspace handoff is also governed by this agents-host\'s local policy switch.';
|
|
77
|
+
}
|
|
47
78
|
export function buildResolvedWorkspaceGuidanceLines(context, provider) {
|
|
48
79
|
if (!context?.resolvedWorkspace) {
|
|
49
80
|
return [];
|
|
@@ -60,5 +91,16 @@ export function buildResolvedWorkspaceGuidanceLines(context, provider) {
|
|
|
60
91
|
if (localityLine) {
|
|
61
92
|
lines.push(localityLine);
|
|
62
93
|
}
|
|
94
|
+
const permissionLine = buildDiscussionOnlyPermissionLine(context);
|
|
95
|
+
if (permissionLine) {
|
|
96
|
+
lines.push(permissionLine);
|
|
97
|
+
}
|
|
98
|
+
const precheckLine = buildProjectEntryPrecheckLine(context);
|
|
99
|
+
if (precheckLine) {
|
|
100
|
+
lines.push(precheckLine);
|
|
101
|
+
}
|
|
63
102
|
return lines;
|
|
64
103
|
}
|
|
104
|
+
export function isDiscussionOnlyResolvedWorkspace(context) {
|
|
105
|
+
return context?.resolvedWorkspace?.authority === 'task-thread-scratch';
|
|
106
|
+
}
|
|
@@ -127,6 +127,14 @@ export class ProviderTurnPreparer {
|
|
|
127
127
|
...(input.incomingMessageType !== undefined && incomingContent !== undefined
|
|
128
128
|
? { incomingContent }
|
|
129
129
|
: {}),
|
|
130
|
+
...(input.taskAssignmentContextOverride
|
|
131
|
+
? {
|
|
132
|
+
taskAssignmentContextOverride: input.taskAssignmentContextOverride,
|
|
133
|
+
...(input.taskAssignmentContextOverridePersistence
|
|
134
|
+
? { taskAssignmentContextOverridePersistence: input.taskAssignmentContextOverridePersistence }
|
|
135
|
+
: {}),
|
|
136
|
+
}
|
|
137
|
+
: {}),
|
|
130
138
|
};
|
|
131
139
|
channelContext = await this.channelContextStore.prepare(prepareInput);
|
|
132
140
|
}
|
|
@@ -550,11 +550,6 @@ class LoopbackLocalhostGatewayController {
|
|
|
550
550
|
return;
|
|
551
551
|
}
|
|
552
552
|
case 'users': {
|
|
553
|
-
if (!binding.payload?.localhostGateway?.collaboration?.enabled) {
|
|
554
|
-
this.sendJson(response, 404, { error: 'not_found' });
|
|
555
|
-
this.recordAudit('not-found', 404, decision.path, request.method ?? 'GET', decision.binding);
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
553
|
this.sendJson(response, 200, {
|
|
559
554
|
users: await this.controlPlane.listUsers(),
|
|
560
555
|
});
|
|
@@ -910,6 +905,8 @@ function mapGatewayControlPlaneError(error, fallbackReason) {
|
|
|
910
905
|
// is broken, and it would retry the same bad call instead of correcting it.
|
|
911
906
|
case 'bpp.task_property_key_unknown':
|
|
912
907
|
return new GatewayHttpError(400, { error: 'unknown_property_key' }, 'bad-request');
|
|
908
|
+
case 'bpp.task_property_write_forbidden':
|
|
909
|
+
return new GatewayHttpError(403, { error: 'forbidden' }, 'forbidden');
|
|
913
910
|
case 'bpp.task_property_value_invalid':
|
|
914
911
|
return new GatewayHttpError(400, { error: 'invalid_property_value' }, 'bad-request');
|
|
915
912
|
case 'bpp.task_property_value_too_long':
|
package/dist/local-config.js
CHANGED
|
@@ -2,7 +2,7 @@ import { promises as fs } from 'node:fs';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
4
|
import { parseDocument, stringify } from 'yaml';
|
|
5
|
-
import { assertProviderCompatibility, assertProviderCommandCompatibility, optionalNonEmptyString, optionalStringArray, parseCopilotSessionTtlMinutesValue, parseProviderIdleShutdownMinutesValue, requireNonEmptyString, resolveProvider, resolveProviderCommandConfig, } from './config.js';
|
|
5
|
+
import { assertProviderCompatibility, assertProviderCommandCompatibility, optionalNonEmptyString, optionalStringArray, parseCopilotSessionTtlMinutesValue, parseBooleanValue, parseProviderIdleShutdownMinutesValue, requireNonEmptyString, resolveProvider, resolveProviderCommandConfig, } from './config.js';
|
|
6
6
|
import { resolveLocalConfigAgentStateRoot } from './state-paths.js';
|
|
7
7
|
const SUPPORTED_CONFIG_EXTENSIONS = new Set(['.json', '.yaml', '.yml']);
|
|
8
8
|
export const DEFAULT_LOCAL_HOST_CONFIG_FILENAME = 'agents-host.yaml';
|
|
@@ -127,6 +127,10 @@ function parseProviderCommandOverrides(value, sourceLabel) {
|
|
|
127
127
|
if (value.providerIdleShutdownMinutes !== undefined && value.providerIdleShutdownMinutes !== null) {
|
|
128
128
|
overrides.providerIdleShutdownMinutes = parseProviderIdleShutdownMinutesValue(value.providerIdleShutdownMinutes, sourceLabel);
|
|
129
129
|
}
|
|
130
|
+
if (value.allowCrossAgentIndependentWorkspaceHandoff !== undefined
|
|
131
|
+
&& value.allowCrossAgentIndependentWorkspaceHandoff !== null) {
|
|
132
|
+
overrides.allowCrossAgentIndependentWorkspaceHandoff = parseBooleanValue(value.allowCrossAgentIndependentWorkspaceHandoff, 'allowCrossAgentIndependentWorkspaceHandoff', sourceLabel);
|
|
133
|
+
}
|
|
130
134
|
return overrides;
|
|
131
135
|
}
|
|
132
136
|
function toAgentsDir(hostConfigPath, rawAgentsDir) {
|
|
@@ -599,6 +603,9 @@ function renderAgentConfigYaml(agent) {
|
|
|
599
603
|
if (agent.providerIdleShutdownMinutes !== undefined) {
|
|
600
604
|
config.providerIdleShutdownMinutes = agent.providerIdleShutdownMinutes;
|
|
601
605
|
}
|
|
606
|
+
if (agent.allowCrossAgentIndependentWorkspaceHandoff !== undefined) {
|
|
607
|
+
config.allowCrossAgentIndependentWorkspaceHandoff = agent.allowCrossAgentIndependentWorkspaceHandoff;
|
|
608
|
+
}
|
|
602
609
|
return stringify(config);
|
|
603
610
|
}
|
|
604
611
|
function buildManagedAgentSnapshot(host, stateRootBaseDir, sourcePath, agent) {
|
|
@@ -638,6 +645,9 @@ function resolveAgentProviderCommandConfig(hostDefaults, agent) {
|
|
|
638
645
|
...(agent.providerIdleShutdownMinutes !== undefined
|
|
639
646
|
? { providerIdleShutdownMinutes: agent.providerIdleShutdownMinutes }
|
|
640
647
|
: {}),
|
|
648
|
+
...(agent.allowCrossAgentIndependentWorkspaceHandoff !== undefined
|
|
649
|
+
? { allowCrossAgentIndependentWorkspaceHandoff: agent.allowCrossAgentIndependentWorkspaceHandoff }
|
|
650
|
+
: {}),
|
|
641
651
|
});
|
|
642
652
|
}
|
|
643
653
|
export async function loadLocalConfigSnapshot(hostConfigPath, deps = {}) {
|