@runuai/host 0.9.0 → 0.9.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/images/standard/container/uai-init +38 -19
- package/lib/agent.ts +42 -16
- package/lib/agents/claude.ts +86 -14
- package/lib/agents/factory.ts +9 -1
- package/lib/agents/registry.ts +27 -0
- package/lib/agents/types.ts +3 -0
- package/lib/git-identity.ts +349 -70
- package/lib/github-git-auth.ts +207 -0
- package/lib/github-tokens.ts +756 -110
- package/lib/orchestrator.ts +303 -91
- package/lib/repo-clone.ts +12 -101
- package/lib/ssh.ts +11 -8
- package/lib/transcript.ts +17 -2
- package/package.json +1 -1
- package/scripts/agent/_common.sh +214 -0
- package/scripts/agent/task-down.sh +35 -59
- package/scripts/agent/task-up.sh +746 -72
- package/src/index.ts +81 -7
- package/src/main.ts +112 -31
- package/src/protocol.ts +51 -0
package/src/index.ts
CHANGED
|
@@ -4,7 +4,14 @@ import { cloneRepo } from "../lib/repo-clone";
|
|
|
4
4
|
import { handleFilesOp } from "../lib/shared-files";
|
|
5
5
|
import { getOrchestrator } from "../lib/orchestrator";
|
|
6
6
|
import { storeTaskCliSecret } from "../lib/agent-cli";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
clearRefresh,
|
|
9
|
+
reconcileTaskGitAuth,
|
|
10
|
+
} from "../lib/github-tokens";
|
|
11
|
+
import {
|
|
12
|
+
prepareTaskGithubGitCredential,
|
|
13
|
+
type TaskGithubGitCredential,
|
|
14
|
+
} from "../lib/github-git-auth";
|
|
8
15
|
import { readAttachment, writeAttachment } from "../lib/attachments";
|
|
9
16
|
import { appendTranscript as writeTranscript } from "../lib/transcript";
|
|
10
17
|
import { buildTaskDiff } from "../lib/task-diff";
|
|
@@ -67,6 +74,7 @@ export {
|
|
|
67
74
|
factoryFor as agentFactoryFor,
|
|
68
75
|
list as listAgentAdapters,
|
|
69
76
|
register as registerAgentAdapter,
|
|
77
|
+
supportsExecutionProfile,
|
|
70
78
|
} from "../lib/agents/registry";
|
|
71
79
|
export type {
|
|
72
80
|
AgentKindCapability,
|
|
@@ -218,7 +226,37 @@ export const hostCommands: HostCommands = {
|
|
|
218
226
|
{},
|
|
219
227
|
);
|
|
220
228
|
}
|
|
221
|
-
|
|
229
|
+
let gitCredential: TaskGithubGitCredential | null;
|
|
230
|
+
try {
|
|
231
|
+
gitCredential = input.projects.some((project) =>
|
|
232
|
+
isGithubRepo(project.repoUrl),
|
|
233
|
+
)
|
|
234
|
+
? await prepareTaskGithubGitCredential(input.task.ownerUserId)
|
|
235
|
+
: null;
|
|
236
|
+
} catch {
|
|
237
|
+
throw new AgentError(
|
|
238
|
+
"GITHUB_AUTH_UNAVAILABLE",
|
|
239
|
+
"GitHub is connected on this host, but Uai could not prepare its credential for Git. Retry the task; if it persists, reconnect GitHub on this host.",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
return gitCredential
|
|
244
|
+
? await gitCredential.run(() =>
|
|
245
|
+
agent.taskUp(input, {
|
|
246
|
+
githubCredentialSocket: gitCredential.socketPath,
|
|
247
|
+
}),
|
|
248
|
+
)
|
|
249
|
+
: await agent.taskUp(input);
|
|
250
|
+
} finally {
|
|
251
|
+
await gitCredential?.close().catch((error: unknown) => {
|
|
252
|
+
// Cleanup must not replace the actual task-up result/error. The
|
|
253
|
+
// credential is short-lived and close() also removes its private
|
|
254
|
+
// directory in a finally block; retain diagnostics for operators.
|
|
255
|
+
console.warn(
|
|
256
|
+
`[github] task ${input.task.id}: credential cleanup failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
222
260
|
});
|
|
223
261
|
if (result.ok) {
|
|
224
262
|
orchestrator.allowChannel(input.task.id);
|
|
@@ -236,10 +274,19 @@ export const hostCommands: HostCommands = {
|
|
|
236
274
|
// background — it must never block or fail task-up. Awaiting it here would
|
|
237
275
|
// couple the command result to a network token-exchange: a slow/hung
|
|
238
276
|
// exchange (e.g. cloud mid-deploy) would trip the cloud's command timeout
|
|
239
|
-
// and mark a running task as errored.
|
|
277
|
+
// and mark a running task as errored. The reconciler injects + schedules
|
|
240
278
|
// (or emits a system note on failure) on its own; the agents come up
|
|
241
279
|
// regardless and the token lands well before the first `gh` call.
|
|
242
|
-
void
|
|
280
|
+
void reconcileTaskGitAuth(
|
|
281
|
+
input.task.id,
|
|
282
|
+
input.task.ownerUserId,
|
|
283
|
+
).catch((err) =>
|
|
284
|
+
console.warn(
|
|
285
|
+
`[github] task ${input.task.id}: post-start reconciliation failed: ${
|
|
286
|
+
err instanceof Error ? err.message : String(err)
|
|
287
|
+
}`,
|
|
288
|
+
),
|
|
289
|
+
);
|
|
243
290
|
} else {
|
|
244
291
|
recordTaskError(input.task.id);
|
|
245
292
|
}
|
|
@@ -482,10 +529,10 @@ export const hostCommands: HostCommands = {
|
|
|
482
529
|
}
|
|
483
530
|
},
|
|
484
531
|
|
|
485
|
-
async appendTranscript(_ctx, taskId, author, text) {
|
|
532
|
+
async appendTranscript(_ctx, taskId, author, text, targets) {
|
|
486
533
|
// Per-message + high-frequency, so no logCommand (avoid log spam).
|
|
487
534
|
try {
|
|
488
|
-
writeTranscript(taskId, author, text);
|
|
535
|
+
writeTranscript(taskId, author, text, targets);
|
|
489
536
|
return ok(undefined);
|
|
490
537
|
} catch (err) {
|
|
491
538
|
return failFromUnknown(err);
|
|
@@ -553,6 +600,14 @@ function mapAgentError(code: string): HostErrorCode {
|
|
|
553
600
|
return HostErrorCode.WorktreeFailed;
|
|
554
601
|
case "CLONE_FAILED":
|
|
555
602
|
return HostErrorCode.CloneFailed;
|
|
603
|
+
case "GITHUB_SSH_ACCESS_DENIED":
|
|
604
|
+
return HostErrorCode.GitHubSshAccessDenied;
|
|
605
|
+
case "GITHUB_TOKEN_ACCESS_DENIED":
|
|
606
|
+
return HostErrorCode.GitHubTokenAccessDenied;
|
|
607
|
+
case "GITHUB_AUTH_UNAVAILABLE":
|
|
608
|
+
return HostErrorCode.GitHubAuthUnavailable;
|
|
609
|
+
case "GITHUB_CONNECTION_REQUIRED":
|
|
610
|
+
return HostErrorCode.GitHubConnectionRequired;
|
|
556
611
|
case "FETCH_FAILED":
|
|
557
612
|
return HostErrorCode.FetchFailed;
|
|
558
613
|
case "RENDER_FAILED":
|
|
@@ -566,6 +621,21 @@ function mapAgentError(code: string): HostErrorCode {
|
|
|
566
621
|
}
|
|
567
622
|
}
|
|
568
623
|
|
|
624
|
+
/** Host-side predicate only; API-boundary normalization already canonicalizes
|
|
625
|
+
* normal GitHub projects. Keep scratchpads/GitLab/local remotes independent of
|
|
626
|
+
* an unrelated broken GitHub connection on the same host. */
|
|
627
|
+
function isGithubRepo(input: string): boolean {
|
|
628
|
+
const value = input.trim();
|
|
629
|
+
if (/^(?:[^@/]+@)?(?:www\.)?github\.com:/i.test(value)) return true;
|
|
630
|
+
if (/^[\w.-]+\/[\w.-]+(?:\.git)?\/?$/i.test(value)) return true;
|
|
631
|
+
try {
|
|
632
|
+
const url = new URL(value);
|
|
633
|
+
return /^(?:www\.)?github\.com$/i.test(url.hostname);
|
|
634
|
+
} catch {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
569
639
|
function mapDeliverError(message: string): HostErrorCode {
|
|
570
640
|
if (message.includes("not found")) return HostErrorCode.TaskNotFound;
|
|
571
641
|
if (message.includes("not running")) return HostErrorCode.TaskNotRunning;
|
|
@@ -574,7 +644,11 @@ function mapDeliverError(message: string): HostErrorCode {
|
|
|
574
644
|
}
|
|
575
645
|
|
|
576
646
|
function isRetryableAgentError(code: string): boolean {
|
|
577
|
-
return
|
|
647
|
+
return (
|
|
648
|
+
code === "FETCH_FAILED" ||
|
|
649
|
+
code === "CONTAINER_INIT_FAILED" ||
|
|
650
|
+
code === "GITHUB_AUTH_UNAVAILABLE"
|
|
651
|
+
);
|
|
578
652
|
}
|
|
579
653
|
|
|
580
654
|
function toLegacyDecision(decision: PermissionDecision): "accept" | "decline" {
|
package/src/main.ts
CHANGED
|
@@ -30,11 +30,17 @@ import {
|
|
|
30
30
|
onConnectClear,
|
|
31
31
|
onConnectSet,
|
|
32
32
|
onGithubChange,
|
|
33
|
+
reconcileTaskGitAuth,
|
|
34
|
+
runGithubConnectionTransition,
|
|
33
35
|
setAuthExpiredHandler,
|
|
34
36
|
} from "../lib/github-tokens";
|
|
37
|
+
import { invalidateTaskGithubGitCredentials } from "../lib/github-git-auth";
|
|
38
|
+
import {
|
|
39
|
+
deleteUserSshIdentity,
|
|
40
|
+
removeTaskSshIdentityFromContainer,
|
|
41
|
+
} from "../lib/git-identity";
|
|
35
42
|
import { reinjectCodexRunningTasks, watchCodexAuth } from "../lib/codex-auth";
|
|
36
43
|
import {
|
|
37
|
-
deleteKey as deleteSshKey,
|
|
38
44
|
ensureKeyForUser as ensureSshKeyForUser,
|
|
39
45
|
getPublicKey as getSshPublicKey,
|
|
40
46
|
} from "../lib/ssh";
|
|
@@ -72,6 +78,7 @@ import { ensureStandardImage, standardRuntimes } from "../lib/standard-image";
|
|
|
72
78
|
import { hostCommands, hostEvents } from "./index";
|
|
73
79
|
import {
|
|
74
80
|
HostErrorCode,
|
|
81
|
+
TRANSCRIPT_TARGETS_PROTOCOL_FEATURE,
|
|
75
82
|
type CloudToHost,
|
|
76
83
|
type McpOp,
|
|
77
84
|
type CommandContext,
|
|
@@ -81,6 +88,8 @@ import {
|
|
|
81
88
|
type HostCommands,
|
|
82
89
|
type HostToCloud,
|
|
83
90
|
type PermissionDecision,
|
|
91
|
+
parseChannelMode,
|
|
92
|
+
parseTranscriptTargets,
|
|
84
93
|
type TaskAgent,
|
|
85
94
|
type TaskCommandProject,
|
|
86
95
|
type TaskCommandTask,
|
|
@@ -212,6 +221,7 @@ async function startLocalUi(): Promise<void> {
|
|
|
212
221
|
function buildCapabilities(): HostCapabilities {
|
|
213
222
|
return {
|
|
214
223
|
version: packageVersion(),
|
|
224
|
+
protocolFeatures: [TRANSCRIPT_TARGETS_PROTOCOL_FEATURE],
|
|
215
225
|
agentKinds: agentKindCapabilities(),
|
|
216
226
|
runtimes: standardRuntimes(),
|
|
217
227
|
githubUsers: connectedUserIds(),
|
|
@@ -331,45 +341,108 @@ function connect(): void {
|
|
|
331
341
|
closeTunnel(socket, frame.tunnelId, frame.reason);
|
|
332
342
|
break;
|
|
333
343
|
case "gh.connect.set": {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
:
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
344
|
+
// Account switches fence every host-side Git operation using the old
|
|
345
|
+
// credential before the replacement grant is stored or reinjected.
|
|
346
|
+
void runGithubConnectionTransition(frame.userId, () =>
|
|
347
|
+
onConnectSet(frame, {
|
|
348
|
+
invalidateCredentials: invalidateTaskGithubGitCredentials,
|
|
349
|
+
reconcile: (taskId, userId) =>
|
|
350
|
+
getOrchestrator().runTaskLifecycle(taskId, () =>
|
|
351
|
+
reconcileTaskGitAuth(taskId, userId),
|
|
352
|
+
),
|
|
353
|
+
}),
|
|
354
|
+
).then(
|
|
355
|
+
(result) =>
|
|
356
|
+
send(
|
|
357
|
+
socket,
|
|
358
|
+
result.ok
|
|
359
|
+
? { kind: "gh.connect.ack", userId: frame.userId, ok: true }
|
|
360
|
+
: {
|
|
361
|
+
kind: "gh.connect.ack",
|
|
362
|
+
userId: frame.userId,
|
|
363
|
+
ok: false,
|
|
364
|
+
error: result.error ?? "store failed",
|
|
365
|
+
},
|
|
366
|
+
),
|
|
367
|
+
(err) => {
|
|
368
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
369
|
+
console.warn(`[github] connect.set failed: ${error}`);
|
|
370
|
+
send(socket, {
|
|
371
|
+
kind: "gh.connect.ack",
|
|
372
|
+
userId: frame.userId,
|
|
373
|
+
ok: false,
|
|
374
|
+
error,
|
|
375
|
+
});
|
|
376
|
+
},
|
|
345
377
|
);
|
|
346
378
|
break;
|
|
347
379
|
}
|
|
348
|
-
case "gh.connect.clear":
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
void
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
380
|
+
case "gh.connect.clear": {
|
|
381
|
+
// Local deletion happens synchronously inside onConnectClear and the
|
|
382
|
+
// remote revoke starts immediately in the background. Delay the ack
|
|
383
|
+
// only for live-container logout + transport transition, so the UI
|
|
384
|
+
// cannot report Disconnected while a task can still use the old token.
|
|
385
|
+
void runGithubConnectionTransition(frame.userId, () =>
|
|
386
|
+
onConnectClear(frame.userId, {
|
|
387
|
+
invalidateCredentials: invalidateTaskGithubGitCredentials,
|
|
388
|
+
reconcile: (taskId, userId) =>
|
|
389
|
+
getOrchestrator().runTaskLifecycle(taskId, () =>
|
|
390
|
+
reconcileTaskGitAuth(taskId, userId),
|
|
391
|
+
),
|
|
392
|
+
}),
|
|
393
|
+
).then(
|
|
394
|
+
() =>
|
|
395
|
+
send(socket, {
|
|
396
|
+
kind: "gh.connect.ack",
|
|
397
|
+
userId: frame.userId,
|
|
398
|
+
ok: true,
|
|
399
|
+
}),
|
|
400
|
+
(err) => {
|
|
401
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
402
|
+
console.warn(`[github] connect.clear failed: ${error}`);
|
|
403
|
+
send(socket, {
|
|
404
|
+
kind: "gh.connect.ack",
|
|
405
|
+
userId: frame.userId,
|
|
406
|
+
ok: false,
|
|
407
|
+
error,
|
|
408
|
+
});
|
|
409
|
+
},
|
|
357
410
|
);
|
|
358
|
-
send(socket, { kind: "gh.connect.ack", userId: frame.userId, ok: true });
|
|
359
411
|
break;
|
|
412
|
+
}
|
|
360
413
|
case "ssh.key.get":
|
|
361
414
|
case "ssh.key.ensure":
|
|
362
415
|
case "ssh.key.delete": {
|
|
416
|
+
if (frame.kind === "ssh.key.delete") {
|
|
417
|
+
void deleteUserSshIdentity(frame.userId, {
|
|
418
|
+
removeFromContainer: (taskId) =>
|
|
419
|
+
getOrchestrator().runTaskLifecycle(taskId, () =>
|
|
420
|
+
removeTaskSshIdentityFromContainer(taskId),
|
|
421
|
+
),
|
|
422
|
+
}).then(
|
|
423
|
+
() =>
|
|
424
|
+
send(socket, {
|
|
425
|
+
kind: "ssh.key.ack",
|
|
426
|
+
userId: frame.userId,
|
|
427
|
+
ok: true,
|
|
428
|
+
publicKey: null,
|
|
429
|
+
}),
|
|
430
|
+
(err) =>
|
|
431
|
+
send(socket, {
|
|
432
|
+
kind: "ssh.key.ack",
|
|
433
|
+
userId: frame.userId,
|
|
434
|
+
ok: false,
|
|
435
|
+
error:
|
|
436
|
+
err instanceof Error ? err.message : "ssh key delete failed",
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
363
441
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
} else if (frame.kind === "ssh.key.ensure") {
|
|
369
|
-
publicKey = ensureSshKeyForUser(frame.userId);
|
|
370
|
-
} else {
|
|
371
|
-
publicKey = getSshPublicKey(frame.userId);
|
|
372
|
-
}
|
|
442
|
+
const publicKey =
|
|
443
|
+
frame.kind === "ssh.key.ensure"
|
|
444
|
+
? ensureSshKeyForUser(frame.userId)
|
|
445
|
+
: getSshPublicKey(frame.userId);
|
|
373
446
|
send(socket, { kind: "ssh.key.ack", userId: frame.userId, ok: true, publicKey });
|
|
374
447
|
} catch (err) {
|
|
375
448
|
send(socket, {
|
|
@@ -956,6 +1029,7 @@ function dispatchCommand(
|
|
|
956
1029
|
expectString(args, 0),
|
|
957
1030
|
expectString(args, 1),
|
|
958
1031
|
expectString(args, 2),
|
|
1032
|
+
parseTranscriptTargets(args[3]),
|
|
959
1033
|
);
|
|
960
1034
|
case "previewEnsure":
|
|
961
1035
|
return hostCommands.previewEnsure(
|
|
@@ -1402,6 +1476,13 @@ function expectChannelEnsureInput(
|
|
|
1402
1476
|
if (typeof input.globalContext === "string") {
|
|
1403
1477
|
out.globalContext = input.globalContext;
|
|
1404
1478
|
}
|
|
1479
|
+
// ADR-083: secretary identity drives the role-specific transcript preamble.
|
|
1480
|
+
// Both are optional for wire compatibility with older clouds.
|
|
1481
|
+
const mode = parseChannelMode(input.mode);
|
|
1482
|
+
if (mode !== undefined) out.mode = mode;
|
|
1483
|
+
if (typeof input.secretaryAgentId === "string") {
|
|
1484
|
+
out.secretaryAgentId = input.secretaryAgentId;
|
|
1485
|
+
}
|
|
1405
1486
|
// ADR-053: browser testing flag (optional; tolerant of absence).
|
|
1406
1487
|
if (input.browserTesting === true) out.browserTesting = true;
|
|
1407
1488
|
// ADR-049: humans in the chat (optional; tolerant of absence for older
|
package/src/protocol.ts
CHANGED
|
@@ -9,6 +9,10 @@ export enum HostErrorCode {
|
|
|
9
9
|
ComposeDownFailed = "compose_down_failed",
|
|
10
10
|
WorktreeFailed = "worktree_failed",
|
|
11
11
|
CloneFailed = "clone_failed",
|
|
12
|
+
GitHubSshAccessDenied = "github_ssh_access_denied",
|
|
13
|
+
GitHubTokenAccessDenied = "github_token_access_denied",
|
|
14
|
+
GitHubAuthUnavailable = "github_auth_unavailable",
|
|
15
|
+
GitHubConnectionRequired = "github_connection_required",
|
|
12
16
|
FetchFailed = "fetch_failed",
|
|
13
17
|
RenderFailed = "render_failed",
|
|
14
18
|
DbFailed = "db_failed",
|
|
@@ -27,6 +31,10 @@ export type HostCommandResult<T> =
|
|
|
27
31
|
retryable?: boolean;
|
|
28
32
|
};
|
|
29
33
|
|
|
34
|
+
/** Capability ids shared by host advertisement and fail-closed cloud gates. */
|
|
35
|
+
export const TRANSCRIPT_TARGETS_PROTOCOL_FEATURE = "transcript-targets-v1";
|
|
36
|
+
export const COMMUNICATOR_EXECUTION_PROFILE = "communicator";
|
|
37
|
+
|
|
30
38
|
export interface CommandContext {
|
|
31
39
|
commandId: string;
|
|
32
40
|
}
|
|
@@ -70,6 +78,9 @@ export interface HostCapabilities {
|
|
|
70
78
|
/** The host-agent package version — the cloud UI shows it on the host page
|
|
71
79
|
* and flags when npm has a newer release. Optional (older hosts omit it). */
|
|
72
80
|
version?: string;
|
|
81
|
+
/** Optional protocol features. Absence means an older host and fails closed
|
|
82
|
+
* for features whose fallback would weaken isolation (ADR-083). */
|
|
83
|
+
protocolFeatures?: string[];
|
|
73
84
|
agentKinds: Array<{
|
|
74
85
|
kind: string;
|
|
75
86
|
label: string;
|
|
@@ -77,6 +88,13 @@ export interface HostCapabilities {
|
|
|
77
88
|
defaultModel?: string;
|
|
78
89
|
supportedEfforts: string[];
|
|
79
90
|
defaultEffort?: string;
|
|
91
|
+
/** Execution profiles this adapter enforces, not prompt-only claims. */
|
|
92
|
+
executionProfiles?: Array<{
|
|
93
|
+
id: string;
|
|
94
|
+
mechanism: string;
|
|
95
|
+
defaultModel?: string;
|
|
96
|
+
defaultEffort?: string;
|
|
97
|
+
}>;
|
|
80
98
|
}>;
|
|
81
99
|
runtimes: Array<{
|
|
82
100
|
kind: string;
|
|
@@ -88,6 +106,34 @@ export interface HostCapabilities {
|
|
|
88
106
|
githubUsers?: string[];
|
|
89
107
|
}
|
|
90
108
|
|
|
109
|
+
/** One transcript projection the cloud asks the host to append to (ADR-083). */
|
|
110
|
+
export type TranscriptTarget = "chat" | "chat-front";
|
|
111
|
+
|
|
112
|
+
/** Parse the optional channel mode without turning malformed-new-cloud input
|
|
113
|
+
* into legacy Open mode. Absence is the only backwards-compatible fallback. */
|
|
114
|
+
export function parseChannelMode(
|
|
115
|
+
value: unknown,
|
|
116
|
+
): "open" | "secretary" | undefined {
|
|
117
|
+
if (value === undefined) return undefined;
|
|
118
|
+
if (value === "open" || value === "secretary") return value;
|
|
119
|
+
throw new Error("invalid command args: expected channel mode");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Parse the appendTranscript wire argument. `undefined` alone is the legacy
|
|
123
|
+
* three-argument command and maps to chat.md during a rolling deploy. */
|
|
124
|
+
export function parseTranscriptTargets(value: unknown): TranscriptTarget[] {
|
|
125
|
+
if (value === undefined) return ["chat"];
|
|
126
|
+
if (
|
|
127
|
+
!Array.isArray(value) ||
|
|
128
|
+
value.length === 0 ||
|
|
129
|
+
value.length > 2 ||
|
|
130
|
+
value.some((target) => target !== "chat" && target !== "chat-front")
|
|
131
|
+
) {
|
|
132
|
+
throw new Error("invalid command args: expected transcript targets");
|
|
133
|
+
}
|
|
134
|
+
return [...new Set(value)];
|
|
135
|
+
}
|
|
136
|
+
|
|
91
137
|
export interface TaskUpResult {
|
|
92
138
|
composeProject: string;
|
|
93
139
|
worktreePath: string;
|
|
@@ -200,6 +246,10 @@ export interface ChannelHuman {
|
|
|
200
246
|
export interface ChannelEnsureInput {
|
|
201
247
|
taskId: string;
|
|
202
248
|
agents: TaskAgent[];
|
|
249
|
+
/** ADR-083: channel projection/routing mode. Optional for older clouds. */
|
|
250
|
+
mode?: "open" | "secretary";
|
|
251
|
+
/** The one human-facing communicator in secretary mode. */
|
|
252
|
+
secretaryAgentId?: string;
|
|
203
253
|
/** ADR-049: the humans in the chat. Optional for wire back-compat; absent
|
|
204
254
|
* or single-entry behaves exactly like the pre-ADR-049 single-human task. */
|
|
205
255
|
humans?: ChannelHuman[];
|
|
@@ -382,6 +432,7 @@ export interface HostCommands {
|
|
|
382
432
|
taskId: string,
|
|
383
433
|
author: string,
|
|
384
434
|
text: string,
|
|
435
|
+
targets: TranscriptTarget[],
|
|
385
436
|
): Promise<HostCommandResult<void>>;
|
|
386
437
|
}
|
|
387
438
|
|