@runuai/host 0.9.154 → 0.9.155
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/lib/agent-cli.ts +64 -1
- package/lib/agents/claude.ts +19 -2
- package/lib/agents/codex.ts +15 -1
- package/lib/agents/registry.ts +5 -4
- package/lib/agents/types.ts +4 -1
- package/lib/agents/uai.ts +15 -1
- package/lib/orchestrator.ts +90 -25
- package/lib/task-token.ts +3 -0
- package/package.json +1 -1
- package/src/cloud-credentials.ts +25 -0
- package/src/main.ts +14 -10
- package/src/protocol.ts +42 -0
package/lib/agent-cli.ts
CHANGED
|
@@ -210,7 +210,14 @@ export function agentCliEnv(
|
|
|
210
210
|
return {
|
|
211
211
|
...runtimeAuthority,
|
|
212
212
|
UAI_TASK_TOKEN: signTaskToken(
|
|
213
|
-
{
|
|
213
|
+
{
|
|
214
|
+
taskId,
|
|
215
|
+
userId: ownerUserId,
|
|
216
|
+
permissions,
|
|
217
|
+
agentId: agent.id,
|
|
218
|
+
// ADR-157 §3: binds the token to this agent, not merely its handle.
|
|
219
|
+
...(agent.tokenGeneration ? { generation: agent.tokenGeneration } : {}),
|
|
220
|
+
},
|
|
214
221
|
cliSecret,
|
|
215
222
|
),
|
|
216
223
|
UAI_API_URL: apiUrl,
|
|
@@ -312,6 +319,58 @@ async function main() {
|
|
|
312
319
|
}
|
|
313
320
|
return;
|
|
314
321
|
}
|
|
322
|
+
if (group === "crew") {
|
|
323
|
+
// ADR-157: the task's built-in Astronaut (the @astronaut Uai builds, never a
|
|
324
|
+
// persona) changes its own crew. One agent per call, always with a --why.
|
|
325
|
+
const handleOf = (v) => (typeof v === "string" && v.startsWith("@") ? v.slice(1) : v);
|
|
326
|
+
const launch = (a) => a.kind + (a.model ? "/" + a.model : "") + (a.effort ? "/" + a.effort : "");
|
|
327
|
+
if (action === "list") { out(await api("GET", "/api/agent/crew")); return; }
|
|
328
|
+
if (action === "add") {
|
|
329
|
+
const personaId = pos[0];
|
|
330
|
+
if (!personaId || !flags.why) {
|
|
331
|
+
console.error('uai: crew add needs a persona id (see crew list) and --why "<reason>"');
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
const body = { personaId, why: flags.why };
|
|
335
|
+
if (flags.as) body.handle = handleOf(flags.as);
|
|
336
|
+
if (flags.engine) body.kind = flags.engine;
|
|
337
|
+
if (flags.model) body.model = flags.model;
|
|
338
|
+
if (flags.effort) body.effort = flags.effort;
|
|
339
|
+
const r = await api("POST", "/api/agent/crew", body);
|
|
340
|
+
out("added @" + r.agent.id + " (" + r.agent.label + ") on " + launch(r.agent) + ". It stays idle until you dispatch work to it.");
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (action === "model") {
|
|
344
|
+
const id = handleOf(pos[0] || "");
|
|
345
|
+
if (!id || !flags.why || !(flags.model || flags.engine || flags.effort)) {
|
|
346
|
+
console.error('uai: crew model needs @agent, at least one of --engine, --model and --effort, and --why "<reason>"');
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
// "default" clears the setting back to the engine's own default (null).
|
|
350
|
+
const cleared = (v) => (v === "default" ? null : v);
|
|
351
|
+
const body = { why: flags.why };
|
|
352
|
+
if (flags.engine) body.kind = flags.engine;
|
|
353
|
+
if (flags.model) body.model = cleared(flags.model);
|
|
354
|
+
if (flags.effort) body.effort = cleared(flags.effort);
|
|
355
|
+
const r = await api("PATCH", "/api/agent/crew/" + encodeURIComponent(id), body);
|
|
356
|
+
out(r.changed
|
|
357
|
+
? "@" + id + " now runs on " + launch(r.agent) + "; its session restarts after its current turn"
|
|
358
|
+
: "@" + id + " already runs on " + launch(r.agent) + "; nothing changed");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (action === "remove") {
|
|
362
|
+
const id = handleOf(pos[0] || "");
|
|
363
|
+
if (!id || !flags.why) {
|
|
364
|
+
console.error('uai: crew remove needs @agent and --why "<reason>"');
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
await api("DELETE", "/api/agent/crew/" + encodeURIComponent(id), { why: flags.why });
|
|
368
|
+
out("removed @" + id);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
console.error("uai: crew commands are list, add, model and remove (Astronaut only)");
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
315
374
|
const key = group + " " + (action || "");
|
|
316
375
|
switch (key) {
|
|
317
376
|
case "projects list": out((await api("GET", "/api/agent/projects")).projects); break;
|
|
@@ -497,6 +556,10 @@ async function main() {
|
|
|
497
556
|
" uai task history (list your past tasks)",
|
|
498
557
|
" uai task read <taskId> (full chat of a past task you were on)",
|
|
499
558
|
' uai dispatch @agent [@agent…] "instruction" (Secretary only)',
|
|
559
|
+
" uai crew list (Astronaut only: personas, engines, models, limits, roster)",
|
|
560
|
+
' uai crew add <personaId> [--as <handle>] [--engine <kind>] [--model <m>] [--effort <e>] --why "<reason>"',
|
|
561
|
+
' uai crew model @agent [--engine <kind>] [--model <m|default>] [--effort <e|default>] --why "<reason>" (at least one of the three)',
|
|
562
|
+
' uai crew remove @agent --why "<reason>"',
|
|
500
563
|
" uai react <heart|check|x> [--msg #id] (no --msg = the message you were last handed)",
|
|
501
564
|
" uai whoami",
|
|
502
565
|
].join("\\n"));
|
package/lib/agents/claude.ts
CHANGED
|
@@ -390,6 +390,17 @@ const CLAUDE_FULL_ACCESS_ARGS = [
|
|
|
390
390
|
"--strict-mcp-config",
|
|
391
391
|
];
|
|
392
392
|
|
|
393
|
+
/**
|
|
394
|
+
* ADR-157 Astronaut profile: the normal tool set, minus Claude Code's native
|
|
395
|
+
* subagents. An Astronaut's helpers must be crew agents the task can see and
|
|
396
|
+
* bill; a built-in Agent/Task subagent would run (and spend) outside the crew.
|
|
397
|
+
*/
|
|
398
|
+
const CLAUDE_ASTRONAUT_ARGS = [
|
|
399
|
+
...CLAUDE_FULL_ACCESS_ARGS,
|
|
400
|
+
"--disallowedTools",
|
|
401
|
+
"Agent,Task",
|
|
402
|
+
];
|
|
403
|
+
|
|
393
404
|
/**
|
|
394
405
|
* ADR-083 communicator profile. This is deliberately an engine-level tool
|
|
395
406
|
* boundary, not prompt advice: setting sources, hooks, skills, workflows and
|
|
@@ -440,7 +451,7 @@ export class ClaudeSession implements AgentSession {
|
|
|
440
451
|
agent: RosterAgent;
|
|
441
452
|
environment: TaskEnvironmentAgentSessionSurface;
|
|
442
453
|
systemPreamble: string;
|
|
443
|
-
executionProfile?: "communicator";
|
|
454
|
+
executionProfile?: "communicator" | "astronaut";
|
|
444
455
|
attachCompatibilityKey?: string;
|
|
445
456
|
agentEnv?: Record<string, string>;
|
|
446
457
|
}) {
|
|
@@ -453,7 +464,9 @@ export class ClaudeSession implements AgentSession {
|
|
|
453
464
|
const cliArgs = [
|
|
454
465
|
...(args.executionProfile === "communicator"
|
|
455
466
|
? CLAUDE_COMMUNICATOR_ARGS
|
|
456
|
-
:
|
|
467
|
+
: args.executionProfile === "astronaut"
|
|
468
|
+
? CLAUDE_ASTRONAUT_ARGS
|
|
469
|
+
: CLAUDE_FULL_ACCESS_ARGS),
|
|
457
470
|
];
|
|
458
471
|
if (process.env.UAI_CLAUDE_INCLUDE_PARTIAL_MESSAGES === "1") {
|
|
459
472
|
cliArgs.push("--include-partial-messages");
|
|
@@ -704,6 +717,10 @@ register({
|
|
|
704
717
|
defaultModel: CLAUDE_COMMUNICATOR_MODEL,
|
|
705
718
|
defaultEffort: CLAUDE_COMMUNICATOR_EFFORT,
|
|
706
719
|
},
|
|
720
|
+
{
|
|
721
|
+
id: "astronaut",
|
|
722
|
+
mechanism: "claude-disallowed-agent-task-v1",
|
|
723
|
+
},
|
|
707
724
|
],
|
|
708
725
|
// Usable only when a Claude credential is in the env (injected into task
|
|
709
726
|
// containers at task-up). Gates advertisement (ADR-044 P2).
|
package/lib/agents/codex.ts
CHANGED
|
@@ -391,6 +391,8 @@ export class CodexSession implements AgentSession {
|
|
|
391
391
|
attachCompatibilityKey?: string;
|
|
392
392
|
/** ADR-128: `-c key=value` overrides a delegating adapter prepends. */
|
|
393
393
|
cliConfigOverrides?: string[];
|
|
394
|
+
/** ADR-157: the Astronaut runs without Codex's native multi-agent tools. */
|
|
395
|
+
executionProfile?: "communicator" | "astronaut";
|
|
394
396
|
}) {
|
|
395
397
|
this.agentId = args.agent.id;
|
|
396
398
|
this.systemPreamble = args.systemPreamble;
|
|
@@ -425,8 +427,15 @@ export class CodexSession implements AgentSession {
|
|
|
425
427
|
"shell_snapshot",
|
|
426
428
|
"--disable",
|
|
427
429
|
"shell_snapshot_v2",
|
|
428
|
-
"app-server",
|
|
429
430
|
);
|
|
431
|
+
// ADR-157: an Astronaut's helpers must be crew agents the task can see and
|
|
432
|
+
// bill, so Codex's own subagents (spawn_agent; multi_agent is on by
|
|
433
|
+
// default in 0.157, multi_agent_v2 off) are disabled for it. Also after
|
|
434
|
+
// every override, so nothing can turn them back on.
|
|
435
|
+
if (args.executionProfile === "astronaut") {
|
|
436
|
+
codexArgs.push("--disable", "multi_agent", "--disable", "multi_agent_v2");
|
|
437
|
+
}
|
|
438
|
+
codexArgs.push("app-server");
|
|
430
439
|
// ADR-061: the CLI is owned by an in-container runner (durable across
|
|
431
440
|
// host restarts); legacy pipes behind UAI_DURABLE_SESSIONS=0. Codex
|
|
432
441
|
// re-handshakes per host process (initialize + thread/start live in this
|
|
@@ -801,6 +810,9 @@ register({
|
|
|
801
810
|
defaultModel: CODEX_DEFAULT_MODEL,
|
|
802
811
|
supportedEfforts: () => [...CODEX_EFFORTS],
|
|
803
812
|
defaultEffort: CODEX_DEFAULT_EFFORT,
|
|
813
|
+
// ADR-157: enforceable by construction (the --disable flags above). The
|
|
814
|
+
// communicator profile is Claude-only; Codex never declared it.
|
|
815
|
+
executionProfiles: [{ id: "astronaut", mechanism: "codex-disable-multi-agent-v1" }],
|
|
804
816
|
// Usable only when the owner's ~/.codex login exists — task-up copies it into
|
|
805
817
|
// each container from UAI_OWNER_HOME (default $HOME). Gates advertisement
|
|
806
818
|
// (ADR-044 P2).
|
|
@@ -820,6 +832,7 @@ register({
|
|
|
820
832
|
agentEnv,
|
|
821
833
|
attachCompatibilityKey,
|
|
822
834
|
cliConfigOverrides,
|
|
835
|
+
executionProfile,
|
|
823
836
|
}) =>
|
|
824
837
|
new CodexSession({
|
|
825
838
|
taskId,
|
|
@@ -829,5 +842,6 @@ register({
|
|
|
829
842
|
agentEnv,
|
|
830
843
|
attachCompatibilityKey,
|
|
831
844
|
cliConfigOverrides,
|
|
845
|
+
executionProfile,
|
|
832
846
|
}),
|
|
833
847
|
});
|
package/lib/agents/registry.ts
CHANGED
|
@@ -88,10 +88,11 @@ export function setAdvertisementFallback(
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
* ADR-154: on cloud credentials the runner holds no engine credential of
|
|
92
|
-
* own, so availability and the fallback above say nothing: the kinds
|
|
93
|
-
*
|
|
94
|
-
* the
|
|
91
|
+
* ADR-154 §10: on cloud credentials the runner holds no engine credential of
|
|
92
|
+
* its own, so availability and the fallback above say nothing: the kinds a
|
|
93
|
+
* leased task can run are advertised, and only those (`CLOUD_CREDENTIAL_KINDS`:
|
|
94
|
+
* the leased lanes and the Managed Engine). Null while the runner runs its
|
|
95
|
+
* tasks on its own credentials. Installed by main.ts.
|
|
95
96
|
*/
|
|
96
97
|
let leasedKinds: () => ReadonlySet<string> | null = () => null;
|
|
97
98
|
|
package/lib/agents/types.ts
CHANGED
|
@@ -57,6 +57,9 @@ export const RosterAgent = z.object({
|
|
|
57
57
|
// ADR-128: which upstream protocol a Uai-engine model speaks — resolved
|
|
58
58
|
// cloud-side from the model catalog; selects the delegated adapter.
|
|
59
59
|
uaiProvider: z.enum(["anthropic", "openai"]).optional(),
|
|
60
|
+
builtIn: z.literal("astronaut").optional(), // ADR-157: the cloud-built Astronaut only
|
|
61
|
+
// ADR-157 §3: signed into the agent's task token as `g` (see agentCliEnv).
|
|
62
|
+
tokenGeneration: z.string().min(1).max(64).optional(),
|
|
60
63
|
});
|
|
61
64
|
export type RosterAgent = z.infer<typeof RosterAgent>;
|
|
62
65
|
|
|
@@ -179,7 +182,7 @@ export interface AgentSessionFactory {
|
|
|
179
182
|
systemPreamble: string;
|
|
180
183
|
/** Host-derived restricted execution policy (ADR-083). Never supplied by
|
|
181
184
|
* the browser or stored on the roster entry. */
|
|
182
|
-
executionProfile?: "communicator";
|
|
185
|
+
executionProfile?: "communicator" | "astronaut";
|
|
183
186
|
/** Durable runners are keyed by task+agent. Host-owned launch settings
|
|
184
187
|
* supply a compatibility key so restart recovery replaces, rather than
|
|
185
188
|
* attaches, a process launched with incompatible model/effort/policy. */
|
package/lib/agents/uai.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* the request's `model`, and the gateway maps slug → upstream id.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { factoryFor, register } from "./registry";
|
|
18
|
+
import { factoryFor, register, supportsExecutionProfile } from "./registry";
|
|
19
19
|
import type { AgentSession, AgentSessionFactory } from "./types";
|
|
20
20
|
|
|
21
21
|
const DEFAULT_GATEWAY_URL = "https://inference.runuai.com";
|
|
@@ -80,6 +80,17 @@ const create: AgentSessionFactory["create"] = async (args): Promise<AgentSession
|
|
|
80
80
|
if (!delegate) {
|
|
81
81
|
throw new Error(`uai engine: no ${delegateKind} adapter registered to delegate to`);
|
|
82
82
|
}
|
|
83
|
+
// ADR-157 §7: the runner checked the profile against the `uai` kind, but the
|
|
84
|
+
// delegate is what enforces it. Refuse rather than run an Astronaut on a
|
|
85
|
+
// delegate that would ignore the profile.
|
|
86
|
+
if (
|
|
87
|
+
args.executionProfile &&
|
|
88
|
+
!supportsExecutionProfile(delegateKind, args.executionProfile)
|
|
89
|
+
) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`uai engine: the ${delegateKind} adapter cannot enforce execution profile "${args.executionProfile}"`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
83
94
|
const taskToken = args.agentEnv?.UAI_TASK_TOKEN;
|
|
84
95
|
if (!taskToken) {
|
|
85
96
|
// agentCliEnv mints no token for a permissionless non-secretary agent;
|
|
@@ -110,5 +121,8 @@ register({
|
|
|
110
121
|
// Always available: the engine needs no host-side credential — auth is
|
|
111
122
|
// the per-agent task token, billing is the org's credits.
|
|
112
123
|
available: () => true,
|
|
124
|
+
// ADR-157: `create` forwards the profile to the Claude or Codex delegate,
|
|
125
|
+
// and both enforce it.
|
|
126
|
+
executionProfiles: [{ id: "astronaut", mechanism: "uai-delegated-astronaut-v1" }],
|
|
113
127
|
create,
|
|
114
128
|
});
|
package/lib/orchestrator.ts
CHANGED
|
@@ -202,7 +202,12 @@ import {
|
|
|
202
202
|
rememberLeasedEngineRuntime,
|
|
203
203
|
} from "./credential-cleanup-inventory";
|
|
204
204
|
import { leasedMcpAclEntries } from "./credential-consumers/mcp";
|
|
205
|
-
import {
|
|
205
|
+
import {
|
|
206
|
+
CREDENTIAL_FREE_KINDS,
|
|
207
|
+
ENGINE_ENV_CREDENTIAL_KEYS,
|
|
208
|
+
LEASED_ENGINE_KINDS,
|
|
209
|
+
MANAGED_ENGINE_KIND,
|
|
210
|
+
} from "../src/cloud-credentials";
|
|
206
211
|
import { clearTaskGatewayAcl, setupMcpTaskConfig } from "./mcp-gateway";
|
|
207
212
|
import {
|
|
208
213
|
reactivateTaskGatewayTask,
|
|
@@ -232,6 +237,7 @@ import {
|
|
|
232
237
|
MAX_AGENT_ID_CHARS,
|
|
233
238
|
MAX_SECRETARY_DISPATCH_ID_CHARS,
|
|
234
239
|
SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE,
|
|
240
|
+
isBuiltInAstronaut,
|
|
235
241
|
} from "../src/protocol";
|
|
236
242
|
import {
|
|
237
243
|
isEnvironmentTransportProvider,
|
|
@@ -249,6 +255,10 @@ export type HostEventSubscriber = (event: HostEvent) => void;
|
|
|
249
255
|
const SECRETARY_SESSION_ATTACH_KEY = "secretary-normal-tools-v1";
|
|
250
256
|
const RESTRICTED_SECRETARY_SESSION_ATTACH_KEY =
|
|
251
257
|
"secretary-communicator-v1";
|
|
258
|
+
// ADR-157: the built-in Astronaut's runner was launched without native
|
|
259
|
+
// subagents. Its own generation keeps a normal-tools runner (or an Astronaut
|
|
260
|
+
// launched before the block) from ever being attached in its place.
|
|
261
|
+
const ASTRONAUT_SESSION_ATTACH_KEY = "astronaut-no-subagents-v1";
|
|
252
262
|
|
|
253
263
|
// ---------------------------------------------------------------------------
|
|
254
264
|
// Channel — one task's live conversation.
|
|
@@ -554,14 +564,6 @@ const LEASED_CREDENTIAL_BLANKS: Readonly<Record<string, string>> = Object.freeze
|
|
|
554
564
|
[...new Set(Object.values(ENGINE_ENV_CREDENTIAL_KEYS).flat())].map((name) => [name, ""]),
|
|
555
565
|
),
|
|
556
566
|
);
|
|
557
|
-
/** ADR-128: the Managed Engine. It needs no AI account (it runs on Uai's
|
|
558
|
-
* gateway), but its agent's task token is its bearer: on a leased task its
|
|
559
|
-
* launches are admitted as that token (ADR-154 §6). */
|
|
560
|
-
const MANAGED_ENGINE_KIND = "uai";
|
|
561
|
-
/** ADR-154 §10: kinds whose agents carry no credential at all; every other
|
|
562
|
-
* kind outside the leased lanes (Kimi) stays down on a leased task until
|
|
563
|
-
* its lane exists. */
|
|
564
|
-
const CREDENTIAL_FREE_KINDS: ReadonlySet<string> = new Set(["mock"]);
|
|
565
567
|
/** ADR-154 §8: how soon a failed rewrite of a task's account homes is tried
|
|
566
568
|
* again. */
|
|
567
569
|
const LEASED_SYNC_RETRY_MS = 30_000;
|
|
@@ -634,11 +636,6 @@ function sameLeasedIdentity(a: LeasedIdentity, b: LeasedIdentity): boolean {
|
|
|
634
636
|
return a.subject === b.subject && a.runEpoch === b.runEpoch;
|
|
635
637
|
}
|
|
636
638
|
|
|
637
|
-
/** The engine kinds whose agents run on an AI account from the lease. */
|
|
638
|
-
const LEASED_ENGINE_KINDS: ReadonlySet<string> = new Set([
|
|
639
|
-
...Object.keys(ENGINE_ENV_CREDENTIAL_KEYS),
|
|
640
|
-
...Object.keys(ENGINE_CREDENTIAL_FILES),
|
|
641
|
-
]);
|
|
642
639
|
/** A burned respawn budget resets after this quiet period (see reconcile). */
|
|
643
640
|
const RESPAWN_COOLDOWN_MS = 10 * 60_000;
|
|
644
641
|
/** ADR-053: gap between browser-install retries. Ensures fire every couple of
|
|
@@ -2591,11 +2588,22 @@ export class Orchestrator {
|
|
|
2591
2588
|
}
|
|
2592
2589
|
}
|
|
2593
2590
|
|
|
2594
|
-
/**
|
|
2591
|
+
/** The engine-enforced launch profile for one agent, if any. */
|
|
2595
2592
|
private executionProfileFor(
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
): "communicator" | undefined {
|
|
2593
|
+
channel: Channel,
|
|
2594
|
+
agentId: string,
|
|
2595
|
+
): "communicator" | "astronaut" | undefined {
|
|
2596
|
+
// ADR-157: the built-in Astronaut keeps normal tools but runs without
|
|
2597
|
+
// native subagents, so every helper it uses is an accounted crew agent.
|
|
2598
|
+
// The adapter factory refuses an engine that cannot enforce that.
|
|
2599
|
+
if (
|
|
2600
|
+
isBuiltInAstronaut(
|
|
2601
|
+
channel,
|
|
2602
|
+
channel.roster.find((agent) => agent.id === agentId),
|
|
2603
|
+
)
|
|
2604
|
+
) {
|
|
2605
|
+
return "astronaut";
|
|
2606
|
+
}
|
|
2599
2607
|
// Secretary is an organisational role, not a sandbox. The communicator
|
|
2600
2608
|
// profile remains implemented by adapters for a future explicit opt-in,
|
|
2601
2609
|
// but no task gets tool denial merely by selecting Secretary mode.
|
|
@@ -2611,18 +2619,22 @@ export class Orchestrator {
|
|
|
2611
2619
|
// The restricted profile is dormant today, but giving it the normal-tools
|
|
2612
2620
|
// generation would make a future opt-out able to attach a runner launched
|
|
2613
2621
|
// under tool denial.
|
|
2622
|
+
const profile = this.executionProfileFor(channel, agent.id);
|
|
2614
2623
|
const policy = this.isSecretary(channel, agent.id)
|
|
2615
|
-
?
|
|
2624
|
+
? profile === "communicator"
|
|
2616
2625
|
? RESTRICTED_SECRETARY_SESSION_ATTACH_KEY
|
|
2617
|
-
:
|
|
2626
|
+
: profile === "astronaut"
|
|
2627
|
+
? ASTRONAUT_SESSION_ATTACH_KEY
|
|
2628
|
+
: SECRETARY_SESSION_ATTACH_KEY
|
|
2618
2629
|
: undefined;
|
|
2619
2630
|
const model = agent.model ?? "";
|
|
2620
2631
|
const effort = agent.effort ?? "";
|
|
2632
|
+
const generation = agent.tokenGeneration ?? "";
|
|
2621
2633
|
// Preserve legacy null/policy-only attachment for agents that choose no
|
|
2622
2634
|
// launch flags. Explicit model/effort gets a stable key so a host restart
|
|
2623
2635
|
// cannot attach a runner created under different settings.
|
|
2624
2636
|
const envMode = hostTaskEnvMode(task);
|
|
2625
|
-
if (envMode === "baked" && !model && !effort) return policy;
|
|
2637
|
+
if (envMode === "baked" && !model && !effort && !generation) return policy;
|
|
2626
2638
|
return JSON.stringify([
|
|
2627
2639
|
envMode === "baked" ? "roster-launch-v1" : "roster-launch-v2",
|
|
2628
2640
|
...(envMode === "loader" ? [envMode] : []),
|
|
@@ -2630,6 +2642,10 @@ export class Orchestrator {
|
|
|
2630
2642
|
model,
|
|
2631
2643
|
effort,
|
|
2632
2644
|
policy ?? "",
|
|
2645
|
+
// ADR-157 §3: a runner keeps the token it was launched with, so one
|
|
2646
|
+
// launched for an earlier agent under this handle never attaches to a
|
|
2647
|
+
// later one. Absent without a generation, so no existing key changes.
|
|
2648
|
+
...(generation ? [generation] : []),
|
|
2633
2649
|
]);
|
|
2634
2650
|
}
|
|
2635
2651
|
|
|
@@ -2640,9 +2656,11 @@ export class Orchestrator {
|
|
|
2640
2656
|
}
|
|
2641
2657
|
|
|
2642
2658
|
private isPromptLevelSecretary(channel: Channel, agentId: string): boolean {
|
|
2659
|
+
// The Astronaut profile only removes native subagents; the Astronaut is still
|
|
2660
|
+
// a normal-tools, prompt-level Secretary (CLI dispatch, its own token).
|
|
2643
2661
|
return (
|
|
2644
2662
|
this.isSecretary(channel, agentId) &&
|
|
2645
|
-
this.executionProfileFor(channel, agentId)
|
|
2663
|
+
this.executionProfileFor(channel, agentId) !== "communicator"
|
|
2646
2664
|
);
|
|
2647
2665
|
}
|
|
2648
2666
|
|
|
@@ -6435,6 +6453,12 @@ function rosterLaunchChangeReason(
|
|
|
6435
6453
|
if (previous.kind !== next.kind) changed.push("engine");
|
|
6436
6454
|
if ((previous.model ?? "") !== (next.model ?? "")) changed.push("model");
|
|
6437
6455
|
if ((previous.effort ?? "") !== (next.effort ?? "")) changed.push("effort");
|
|
6456
|
+
// ADR-157 §3: the session holds a token minted for its generation, so a new
|
|
6457
|
+
// one is a new agent under the handle (or one an older cloud release lost),
|
|
6458
|
+
// and only a fresh launch carries a token the cloud accepts.
|
|
6459
|
+
if ((previous.tokenGeneration ?? "") !== (next.tokenGeneration ?? "")) {
|
|
6460
|
+
changed.push("token generation");
|
|
6461
|
+
}
|
|
6438
6462
|
return changed.length > 0 ? `${changed.join("/")} change` : null;
|
|
6439
6463
|
}
|
|
6440
6464
|
|
|
@@ -6821,6 +6845,35 @@ export function buildSystemPreamble(
|
|
|
6821
6845
|
'An epoch-ms `{"__uai":"input","ts":…}` line precedes each',
|
|
6822
6846
|
"unchanged CLI frame, so the adjacent pair identifies a wake input.",
|
|
6823
6847
|
];
|
|
6848
|
+
// ADR-157: the built-in Astronaut, holding tasks.agents.manage, runs its own
|
|
6849
|
+
// crew. The cloud refuses the same commands from anyone else, including a
|
|
6850
|
+
// Secretary persona that holds the grant. It always runs under the Astronaut
|
|
6851
|
+
// profile (§7), so its tools are not an ordinary Secretary's.
|
|
6852
|
+
const isAstronaut = isBuiltInAstronaut({ mode, secretaryAgentId }, agent);
|
|
6853
|
+
const crewBrief =
|
|
6854
|
+
isAstronaut && (agent.permissions ?? []).includes("tasks.agents.manage")
|
|
6855
|
+
? [
|
|
6856
|
+
"## Your crew",
|
|
6857
|
+
"",
|
|
6858
|
+
"You choose this task's crew. `node " + CONTAINER_CLI_PATH + " crew list` shows the",
|
|
6859
|
+
"personas you can hire, the engines and models this task can run (with the",
|
|
6860
|
+
"managed engine's prices), the plan's room, and the current roster. Change it",
|
|
6861
|
+
"one agent at a time, always with a reason the humans will read:",
|
|
6862
|
+
"- `crew add <personaId> [--as <handle>] [--engine <kind>] [--model <m>] [--effort <e>] --why \"…\"`",
|
|
6863
|
+
"- `crew model @agent [--engine <kind>] [--model <m>] [--effort <e>] --why \"…\"`",
|
|
6864
|
+
" (at least one of the three; `default` as the model or effort clears it to",
|
|
6865
|
+
" the engine's own default)",
|
|
6866
|
+
"- `crew remove @agent --why \"…\"`",
|
|
6867
|
+
"Use the smallest crew the job needs, often none: do small work yourself and",
|
|
6868
|
+
"hire only when parallel work or a different skill or model pays for itself.",
|
|
6869
|
+
"A new agent is idle until you `dispatch` to it, so brief it then. Remove an",
|
|
6870
|
+
"agent when its part is done; its handle is not reused, so hiring that",
|
|
6871
|
+
"persona again gives it a new one. A model change restarts that agent and drops",
|
|
6872
|
+
"its prompt cache, so change models between steps, not mid-step. Your own",
|
|
6873
|
+
"engine and model are the human's choice; you cannot change or remove yourself.",
|
|
6874
|
+
"",
|
|
6875
|
+
]
|
|
6876
|
+
: [];
|
|
6824
6877
|
const secretaryRoleBrief = isSecretary
|
|
6825
6878
|
? [
|
|
6826
6879
|
"## Secretary role",
|
|
@@ -6831,9 +6884,20 @@ export function buildSystemPreamble(
|
|
|
6831
6884
|
"Synthesize the",
|
|
6832
6885
|
"crew's replies for the human instead of forwarding a pile of raw updates.",
|
|
6833
6886
|
"",
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6887
|
+
...(isAstronaut
|
|
6888
|
+
? [
|
|
6889
|
+
"Your role is prompt-guided. You have the normal tools, approved MCP",
|
|
6890
|
+
"connections and persona permissions of any task agent, except native",
|
|
6891
|
+
"subagents: helpers your engine starts on its own would work outside the",
|
|
6892
|
+
"crew, unpriced and unseen by the humans, so they are switched off. Work",
|
|
6893
|
+
"for another agent goes to a crew agent through `dispatch`. Your default",
|
|
6894
|
+
"job is to communicate,",
|
|
6895
|
+
]
|
|
6896
|
+
: [
|
|
6897
|
+
"Your Secretary role is prompt-guided, not tool-restricted. You have the",
|
|
6898
|
+
"same normal tools, approved MCP connections, model choice, and persona",
|
|
6899
|
+
"permissions as any other task agent. Your default job is to communicate,",
|
|
6900
|
+
]),
|
|
6837
6901
|
"coordinate, and dispatch implementation work rather than doing it yourself.",
|
|
6838
6902
|
"If the human explicitly asks YOU to make a change, you may use those tools",
|
|
6839
6903
|
"and do it directly; otherwise hand mutations to a crew agent.",
|
|
@@ -7072,6 +7136,7 @@ export function buildSystemPreamble(
|
|
|
7072
7136
|
...sessionInboxBrief,
|
|
7073
7137
|
...(sessionInboxBrief.length > 0 ? [""] : []),
|
|
7074
7138
|
...secretaryRoleBrief,
|
|
7139
|
+
...crewBrief,
|
|
7075
7140
|
...channelConventionsBrief,
|
|
7076
7141
|
...(replyCardBrief.length > 0 ? ["", ...replyCardBrief] : []),
|
|
7077
7142
|
...checkInTranscriptBrief,
|
package/lib/task-token.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface TaskTokenPayload {
|
|
|
11
11
|
userId: string;
|
|
12
12
|
permissions: string[];
|
|
13
13
|
agentId?: string;
|
|
14
|
+
/** ADR-157 §3: the agent's roster generation; the cloud requires it to match. */
|
|
15
|
+
generation?: string;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
const PREFIX = "uai_";
|
|
@@ -31,6 +33,7 @@ export function signTaskToken(payload: TaskTokenPayload, secret: string): string
|
|
|
31
33
|
u: payload.userId,
|
|
32
34
|
p: payload.permissions,
|
|
33
35
|
...(payload.agentId ? { a: payload.agentId } : {}),
|
|
36
|
+
...(payload.generation ? { g: payload.generation } : {}),
|
|
34
37
|
iat: Math.floor(Date.now() / 1000),
|
|
35
38
|
}),
|
|
36
39
|
),
|
package/package.json
CHANGED
package/src/cloud-credentials.ts
CHANGED
|
@@ -286,6 +286,31 @@ export const ENGINE_CREDENTIAL_FILES: Readonly<Record<string, readonly string[]>
|
|
|
286
286
|
opencode: ["auth.json"],
|
|
287
287
|
};
|
|
288
288
|
|
|
289
|
+
/** The engine kinds whose agents run on an AI account from the lease. */
|
|
290
|
+
export const LEASED_ENGINE_KINDS: ReadonlySet<string> = new Set([
|
|
291
|
+
...Object.keys(ENGINE_ENV_CREDENTIAL_KEYS),
|
|
292
|
+
...Object.keys(ENGINE_CREDENTIAL_FILES),
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
/** ADR-128: the Managed Engine. It needs no AI account (it runs on Uai's
|
|
296
|
+
* gateway), but its agent's task token is its bearer: on a leased task its
|
|
297
|
+
* launches are admitted as that token (ADR-154 §6). */
|
|
298
|
+
export const MANAGED_ENGINE_KIND = "uai";
|
|
299
|
+
|
|
300
|
+
/** ADR-154 §10: kinds whose agents carry no credential at all; every other
|
|
301
|
+
* kind outside the leased lanes (Kimi) stays down on a leased task until
|
|
302
|
+
* its lane exists. */
|
|
303
|
+
export const CREDENTIAL_FREE_KINDS: ReadonlySet<string> = new Set(["mock"]);
|
|
304
|
+
|
|
305
|
+
/** ADR-154 §10: every kind a task on cloud credentials can run, and so every
|
|
306
|
+
* kind a runner on them advertises. The Managed Engine is one, though it
|
|
307
|
+
* leases nothing. Kimi, with no lane yet, is not. */
|
|
308
|
+
export const CLOUD_CREDENTIAL_KINDS: ReadonlySet<string> = new Set([
|
|
309
|
+
...LEASED_ENGINE_KINDS,
|
|
310
|
+
MANAGED_ENGINE_KIND,
|
|
311
|
+
...CREDENTIAL_FREE_KINDS,
|
|
312
|
+
]);
|
|
313
|
+
|
|
289
314
|
interface LeasedValueBase {
|
|
290
315
|
v: 1;
|
|
291
316
|
part: "lease";
|
package/src/main.ts
CHANGED
|
@@ -92,8 +92,7 @@ import { cancelMcpCapture, completeMcpCapture, probeMcpCapture } from "../lib/mc
|
|
|
92
92
|
import { captureGithub, mintSsh, readGithubRevision, revokeGithub } from "../lib/person-capture";
|
|
93
93
|
import { handleMcpOp } from "../lib/mcp-connections";
|
|
94
94
|
import {
|
|
95
|
-
|
|
96
|
-
ENGINE_ENV_CREDENTIAL_KEYS,
|
|
95
|
+
CLOUD_CREDENTIAL_KINDS,
|
|
97
96
|
HOST_CLOUD_CREDENTIALS_FEATURE,
|
|
98
97
|
parseCredentialsActivateResultFrame,
|
|
99
98
|
runEpochFromWire,
|
|
@@ -301,6 +300,7 @@ import {
|
|
|
301
300
|
HOST_ENGINE_DISCONNECT_PROTOCOL_FEATURE,
|
|
302
301
|
HOST_ENGINE_LOGIN_PROTOCOL_FEATURE,
|
|
303
302
|
LIVE_ROSTER_EDIT_PROTOCOL_FEATURE,
|
|
303
|
+
AGENT_TOKEN_GENERATION_PROTOCOL_FEATURE,
|
|
304
304
|
isEngineAccountScope,
|
|
305
305
|
HOST_LOGS_PROTOCOL_FEATURE,
|
|
306
306
|
HOST_TASK_INVENTORY_PROTOCOL_FEATURE,
|
|
@@ -703,10 +703,6 @@ setCredentialsModeSource(() => credentialsActivation.mode());
|
|
|
703
703
|
* so the operator turning them on reaches it within minutes. */
|
|
704
704
|
const CREDENTIALS_ACTIVATION_RETRY_MS = 5 * 60_000;
|
|
705
705
|
const CREDENTIALS_ACTIVATION_ANSWER_MS = 60_000;
|
|
706
|
-
const LEASED_ENGINE_KINDS: ReadonlySet<string> = new Set([
|
|
707
|
-
...Object.keys(ENGINE_ENV_CREDENTIAL_KEYS),
|
|
708
|
-
...Object.keys(ENGINE_CREDENTIAL_FILES),
|
|
709
|
-
]);
|
|
710
706
|
// Started here, first fired long after the bridge's state exists.
|
|
711
707
|
const credentialsActivationTimer = setInterval(() => {
|
|
712
708
|
if (stopping || !credentialFramesReady) return;
|
|
@@ -1398,6 +1394,7 @@ function buildCapabilities(): HostCapabilities {
|
|
|
1398
1394
|
HOST_ENGINE_ACCOUNT_MANAGE_PROTOCOL_FEATURE,
|
|
1399
1395
|
HOST_ENGINE_DISCONNECT_PROTOCOL_FEATURE,
|
|
1400
1396
|
LIVE_ROSTER_EDIT_PROTOCOL_FEATURE,
|
|
1397
|
+
AGENT_TOKEN_GENERATION_PROTOCOL_FEATURE,
|
|
1401
1398
|
// The echo adapter cannot execute the in-task CLI. Advertising typed
|
|
1402
1399
|
// dispatch in mock mode would let the composer create a Secretary that
|
|
1403
1400
|
// has no way to wake crew.
|
|
@@ -1899,10 +1896,11 @@ function flushStoppedTaskStatuses(socket: WebSocket | null = ws): void {
|
|
|
1899
1896
|
// ADR-131 D3: a stored account (any scope) makes a kind advertisable even
|
|
1900
1897
|
// when the host's own credential is absent — see hasStoredEngineAccounts.
|
|
1901
1898
|
setAdvertisementFallback((kind) => hasStoredEngineAccounts(kind));
|
|
1902
|
-
// ADR-154: on cloud credentials the runner holds none of its own, so
|
|
1903
|
-
//
|
|
1904
|
-
//
|
|
1905
|
-
|
|
1899
|
+
// ADR-154 §10: on cloud credentials the runner holds none of its own, so it
|
|
1900
|
+
// advertises the kinds a leased task can run, whatever this process's env
|
|
1901
|
+
// says: the leased lanes and the Managed Engine, which leases nothing (Kimi,
|
|
1902
|
+
// with no lane yet, is left out).
|
|
1903
|
+
setLeasedAdvertisement(() => (credentialsMode() === "active" ? CLOUD_CREDENTIAL_KINDS : null));
|
|
1906
1904
|
onRegistryChange(() => sendCapabilities());
|
|
1907
1905
|
onContainerRuntimeChange((next, previous) => {
|
|
1908
1906
|
if (stopping) return;
|
|
@@ -5418,6 +5416,12 @@ function expectTaskAgents(value: unknown): TaskAgent[] {
|
|
|
5418
5416
|
const perms = row.permissions.filter((p): p is string => typeof p === "string");
|
|
5419
5417
|
if (perms.length > 0) out.permissions = perms;
|
|
5420
5418
|
}
|
|
5419
|
+
// ADR-157: the built-in Astronaut's marker, set only by the cloud.
|
|
5420
|
+
if (row.builtIn === "astronaut") out.builtIn = "astronaut";
|
|
5421
|
+
// ADR-157 §3: the generation its task token must carry.
|
|
5422
|
+
if (typeof row.tokenGeneration === "string" && row.tokenGeneration.length > 0) {
|
|
5423
|
+
out.tokenGeneration = row.tokenGeneration;
|
|
5424
|
+
}
|
|
5421
5425
|
return out;
|
|
5422
5426
|
});
|
|
5423
5427
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -82,6 +82,14 @@ export type HostCommandResult<T> =
|
|
|
82
82
|
* An unknown FIELD is ignored by old readers; an unknown CODE is not.
|
|
83
83
|
*/
|
|
84
84
|
origin?: "cloud";
|
|
85
|
+
/**
|
|
86
|
+
* With `origin: "cloud"`: the protocol feature the command's payload
|
|
87
|
+
* needs and this runner does not advertise, so the bridge never sent it
|
|
88
|
+
* (ADR-157 §3: a roster with token generations needs
|
|
89
|
+
* `agent-token-generation-v1`). Additive for the same reason as
|
|
90
|
+
* `origin`; never set by a host.
|
|
91
|
+
*/
|
|
92
|
+
requiresFeature?: string;
|
|
85
93
|
};
|
|
86
94
|
|
|
87
95
|
/** Capability ids shared by host advertisement and fail-closed cloud gates. */
|
|
@@ -156,6 +164,12 @@ export const HOST_ENGINE_ACCOUNT_SCOPES_PROTOCOL_FEATURE =
|
|
|
156
164
|
* unrelated respawn and never stops a removed one, so the cloud refuses to
|
|
157
165
|
* edit or remove a running task's agents on it. */
|
|
158
166
|
export const LIVE_ROSTER_EDIT_PROTOCOL_FEATURE = "live-roster-edit-v1";
|
|
167
|
+
/** ADR-157 §3: the host signs an agent's `tokenGeneration` into its task token
|
|
168
|
+
* (claim `g`). The cloud stamps a fresh generation on an agent that joins a
|
|
169
|
+
* running task only on a host advertising this, and refuses a token whose
|
|
170
|
+
* generation is not its roster entry's, so a token minted for an earlier agent
|
|
171
|
+
* under the same handle stays dead. */
|
|
172
|
+
export const AGENT_TOKEN_GENERATION_PROTOCOL_FEATURE = "agent-token-generation-v1";
|
|
159
173
|
/** ADR-153 declarations + owner bindings. Advertising this means the host
|
|
160
174
|
* understands every additive env frame field, persists bindings separately
|
|
161
175
|
* from values, and reports immutable repository schema observations. Keep it
|
|
@@ -2956,6 +2970,29 @@ export const MAX_HOST_TASK_INVENTORY_TASK_ID_CHARS = 128;
|
|
|
2956
2970
|
export const ENROLLMENT_REPLAY_PROTOCOL_HEADER = "x-uai-enrollment-replay";
|
|
2957
2971
|
export const ENROLLMENT_REPLAY_PROTOCOL_VERSION = "same-host-v1";
|
|
2958
2972
|
export const COMMUNICATOR_EXECUTION_PROFILE = "communicator";
|
|
2973
|
+
/**
|
|
2974
|
+
* ADR-157: the built-in Astronaut. An Astronaut task has exactly one lead agent with
|
|
2975
|
+
* this id, marked `builtIn: "astronaut"` by the cloud when it builds the entry;
|
|
2976
|
+
* no client may send the marker. The lead runs under the `astronaut` execution
|
|
2977
|
+
* profile: normal tools, but no native subagents (Claude's Agent/Task, Codex's
|
|
2978
|
+
* multi-agent), so every helper it uses is an accounted crew agent.
|
|
2979
|
+
*/
|
|
2980
|
+
export const ASTRONAUT_AGENT_ID = "astronaut";
|
|
2981
|
+
export const ASTRONAUT_BUILT_IN = "astronaut";
|
|
2982
|
+
export const ASTRONAUT_EXECUTION_PROFILE = "astronaut";
|
|
2983
|
+
|
|
2984
|
+
/** Whether `agent` is this task's built-in Astronaut (ADR-157 §1). */
|
|
2985
|
+
export function isBuiltInAstronaut(
|
|
2986
|
+
task: { mode?: string | null; secretaryAgentId?: string | null },
|
|
2987
|
+
agent: { id: string; builtIn?: string | null } | null | undefined,
|
|
2988
|
+
): boolean {
|
|
2989
|
+
return (
|
|
2990
|
+
task.mode === "secretary" &&
|
|
2991
|
+
task.secretaryAgentId === ASTRONAUT_AGENT_ID &&
|
|
2992
|
+
agent?.id === ASTRONAUT_AGENT_ID &&
|
|
2993
|
+
agent.builtIn === ASTRONAUT_BUILT_IN
|
|
2994
|
+
);
|
|
2995
|
+
}
|
|
2959
2996
|
export const MAX_AGENT_ID_CHARS = 128;
|
|
2960
2997
|
export const MAX_SECRETARY_DISPATCH_RECIPIENTS = 16;
|
|
2961
2998
|
export const MAX_SECRETARY_DISPATCH_INSTRUCTION_CHARS = 8_000;
|
|
@@ -3023,6 +3060,11 @@ export interface TaskAgent {
|
|
|
3023
3060
|
}>;
|
|
3024
3061
|
/** ADR-048: the source persona's flat permission list for the `uai` CLI. */
|
|
3025
3062
|
permissions?: string[];
|
|
3063
|
+
/** ADR-157: set by the cloud on the built-in Astronaut only. */
|
|
3064
|
+
builtIn?: "astronaut";
|
|
3065
|
+
/** ADR-157 §3: this agent's token generation, stamped by the cloud when it
|
|
3066
|
+
* joined the running task; signed into its task token as `g`. */
|
|
3067
|
+
tokenGeneration?: string;
|
|
3026
3068
|
}
|
|
3027
3069
|
|
|
3028
3070
|
export type ContainerRuntimePreference = "auto" | "docker" | "apple-container";
|