@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
|
@@ -37,33 +37,52 @@ else
|
|
|
37
37
|
fi
|
|
38
38
|
|
|
39
39
|
# ---------------------------------------------------------------------------
|
|
40
|
-
# 0.
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
# on a prompt. `gh` is separate: it authenticates from its own config file,
|
|
46
|
-
# which the host writes via `gh auth login --with-token` with the user's
|
|
47
|
-
# short-lived token. We must NOT set GH_TOKEN in the env — `gh` would prefer
|
|
48
|
-
# it over that stored credential (and re-attribute every PR to it).
|
|
40
|
+
# 0. Git transport selection (ADR-027). A connected user's App token is the
|
|
41
|
+
# primary clone/fetch/push credential over HTTPS; the host writes it into
|
|
42
|
+
# gh's private config and runs `gh auth setup-git` before agents start. SSH
|
|
43
|
+
# transport is only the no-GitHub-connection fallback. The SSH key remains
|
|
44
|
+
# useful in either mode for commit/tag signing.
|
|
49
45
|
# ---------------------------------------------------------------------------
|
|
50
46
|
|
|
51
|
-
if [
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
if [ "${UAI_SKIP_GIT_TRANSPORT:-0}" = "1" ]; then
|
|
48
|
+
# Recovery/connect/disconnect reconciles the current credential immediately
|
|
49
|
+
# before invoking us. Do not overwrite that live decision with the static
|
|
50
|
+
# transport marker captured when Compose was first created.
|
|
51
|
+
log "keeping the reconciled GitHub transport"
|
|
52
|
+
elif [ "${UAI_GIT_TRANSPORT:-anonymous}" = "ssh" ]; then
|
|
53
|
+
# Remove a prior token-mode reverse rewrite if this container is being
|
|
54
|
+
# repaired after disconnect, then route canonical HTTPS origins over SSH.
|
|
55
|
+
/usr/bin/git config --global --unset-all url."https://github.com/".insteadOf \
|
|
56
|
+
>/dev/null 2>&1 || true
|
|
57
|
+
/usr/bin/git config --global url."git@github.com:".insteadOf "https://github.com/"
|
|
58
|
+
/usr/bin/git config --global core.sshCommand "/usr/bin/ssh -o StrictHostKeyChecking=accept-new"
|
|
59
|
+
log "using SSH as the GitHub transport fallback"
|
|
60
|
+
else
|
|
61
|
+
# Heal containers created by the old SSH-only policy. Canonical origins are
|
|
62
|
+
# HTTPS; reverse legacy SSH URLs (including submodules) through gh as well.
|
|
63
|
+
/usr/bin/git config --global --unset-all url."git@github.com:".insteadOf \
|
|
64
|
+
>/dev/null 2>&1 || true
|
|
65
|
+
/usr/bin/git config --global --unset-all core.sshCommand >/dev/null 2>&1 || true
|
|
66
|
+
/usr/bin/git config --global --replace-all url."https://github.com/".insteadOf \
|
|
67
|
+
"git@github.com:"
|
|
68
|
+
/usr/bin/git config --global --add url."https://github.com/".insteadOf \
|
|
69
|
+
"ssh://git@github.com/"
|
|
70
|
+
if [ "${UAI_GIT_TRANSPORT:-}" = "https" ]; then
|
|
71
|
+
log "using the connected GitHub credential for HTTPS Git"
|
|
72
|
+
else
|
|
73
|
+
log "using anonymous HTTPS Git for public repositories"
|
|
74
|
+
fi
|
|
55
75
|
fi
|
|
56
76
|
|
|
57
77
|
# Signed commits (policy): when the uai SSH identity is present, configure git
|
|
58
78
|
# to SSH-sign every commit + tag with it. The pubkey is registered on GitHub
|
|
59
|
-
#
|
|
60
|
-
# git push over SSH (configured above).
|
|
79
|
+
# so commits show as Verified. Signing is independent of Git transport.
|
|
61
80
|
if [ -f "$HOME/.ssh/id_ed25519.pub" ]; then
|
|
62
81
|
log "enabling SSH commit signing with the uai identity"
|
|
63
|
-
git config --global gpg.format ssh
|
|
64
|
-
git config --global user.signingkey "$HOME/.ssh/id_ed25519.pub"
|
|
65
|
-
git config --global commit.gpgsign true
|
|
66
|
-
git config --global tag.gpgsign true
|
|
82
|
+
/usr/bin/git config --global gpg.format ssh
|
|
83
|
+
/usr/bin/git config --global user.signingkey "$HOME/.ssh/id_ed25519.pub"
|
|
84
|
+
/usr/bin/git config --global commit.gpgsign true
|
|
85
|
+
/usr/bin/git config --global tag.gpgsign true
|
|
67
86
|
fi
|
|
68
87
|
|
|
69
88
|
# Attribution policy: never co-author with the agents. Claude Code honors
|
package/lib/agent.ts
CHANGED
|
@@ -33,6 +33,11 @@ import { getHostTask } from "./runtime-state";
|
|
|
33
33
|
import { removeTaskIdentity, writeTaskIdentity } from "./ssh";
|
|
34
34
|
import type { TaskDownInput, TaskLaunchInput } from "../src/protocol";
|
|
35
35
|
|
|
36
|
+
interface TaskUpCredentials {
|
|
37
|
+
/** Path to the task owner's private, ephemeral Git credential cache. */
|
|
38
|
+
githubCredentialSocket?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
36
41
|
// ---------------------------------------------------------------------------
|
|
37
42
|
// Locate the agent scripts. They ship inside the package, so by default we
|
|
38
43
|
// resolve them relative to this module — which works identically in a repo
|
|
@@ -136,11 +141,12 @@ async function runAgent<T extends z.ZodTypeAny>(
|
|
|
136
141
|
dataSchema: T,
|
|
137
142
|
commandDbPath: string,
|
|
138
143
|
extraEnv: Record<string, string> = {},
|
|
144
|
+
stdinPayload?: string,
|
|
139
145
|
): Promise<z.infer<T>> {
|
|
140
146
|
const scriptPath = resolve(agentDir(), scriptName);
|
|
141
147
|
|
|
142
148
|
const child = spawn(scriptPath, args, {
|
|
143
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
149
|
+
stdio: [stdinPayload === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
144
150
|
env: {
|
|
145
151
|
...process.env,
|
|
146
152
|
UAI_DB_PATH: commandDbPath,
|
|
@@ -151,12 +157,19 @@ async function runAgent<T extends z.ZodTypeAny>(
|
|
|
151
157
|
|
|
152
158
|
let stdoutBuf = "";
|
|
153
159
|
let stderrBuf = "";
|
|
154
|
-
child.stdout
|
|
160
|
+
child.stdout?.on("data", (chunk) => {
|
|
155
161
|
stdoutBuf += chunk.toString("utf8");
|
|
156
162
|
});
|
|
157
|
-
child.stderr
|
|
163
|
+
child.stderr?.on("data", (chunk) => {
|
|
158
164
|
stderrBuf += chunk.toString("utf8");
|
|
159
165
|
});
|
|
166
|
+
if (stdinPayload !== undefined && child.stdin) {
|
|
167
|
+
// A script that exits before consuming stdin may close the pipe first.
|
|
168
|
+
// The process exit/error below is authoritative; do not turn EPIPE into an
|
|
169
|
+
// unhandled stream error in the host agent.
|
|
170
|
+
child.stdin.on("error", () => {});
|
|
171
|
+
child.stdin.end(stdinPayload);
|
|
172
|
+
}
|
|
160
173
|
|
|
161
174
|
const exitCode: number = await new Promise((res, rej) => {
|
|
162
175
|
child.on("error", rej);
|
|
@@ -267,27 +280,39 @@ export function toolStderrDetail(stderr: string): string {
|
|
|
267
280
|
// ---------------------------------------------------------------------------
|
|
268
281
|
|
|
269
282
|
export const agent = {
|
|
270
|
-
async taskUp(
|
|
283
|
+
async taskUp(
|
|
284
|
+
input: TaskLaunchInput,
|
|
285
|
+
credentials: TaskUpCredentials = {},
|
|
286
|
+
): Promise<TaskUpResult> {
|
|
271
287
|
const commandDbPath = createTaskUpCommandDb(input);
|
|
272
|
-
// Materialize the creator's per-user SSH key
|
|
273
|
-
//
|
|
288
|
+
// Materialize the creator's per-user SSH key for commit signing and the
|
|
289
|
+
// explicit no-GitHub-connection transport fallback (ADR-029).
|
|
274
290
|
const identityDir = writeTaskIdentity(input.task.id, input.task.ownerUserId);
|
|
275
|
-
// Per-(project, key) env
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
|
|
291
|
+
// Per-(project, key) env values must NEVER enter task-up's host process
|
|
292
|
+
// environment. Besides ordinary app names, projects can declare names such
|
|
293
|
+
// as BASH_ENV, PATH, GIT_CONFIG_*, or UAI_*; exposing those before the host
|
|
294
|
+
// clone would let container-owned configuration influence host commands or
|
|
295
|
+
// read another credential boundary. Send one JSON object over stdin
|
|
296
|
+
// instead. task-up feeds it directly to Compose as an in-memory override,
|
|
297
|
+
// so values reach only the task container and are never written to disk.
|
|
298
|
+
// On a key collision the LEAD project (position 0) wins: iterate in
|
|
299
|
+
// descending position order so position-0 assignments apply last.
|
|
300
|
+
const projectEnv: Record<string, string> = {};
|
|
284
301
|
const orderedProjects = [...input.projects].sort(
|
|
285
302
|
(a, b) => b.position - a.position,
|
|
286
303
|
);
|
|
287
304
|
for (const project of orderedProjects) {
|
|
288
|
-
Object.assign(
|
|
305
|
+
Object.assign(projectEnv, getDecryptedForProject(project.id));
|
|
289
306
|
}
|
|
307
|
+
const extraEnv: Record<string, string> = {};
|
|
290
308
|
if (identityDir) extraEnv.UAI_TASK_IDENTITY_DIR = identityDir;
|
|
309
|
+
// Only a non-secret socket path crosses into Bash. The token itself was
|
|
310
|
+
// seeded over stdin and never enters argv, env, project data, Compose, or
|
|
311
|
+
// the task container.
|
|
312
|
+
if (credentials.githubCredentialSocket) {
|
|
313
|
+
extraEnv.UAI_GITHUB_CREDENTIAL_SOCKET =
|
|
314
|
+
credentials.githubCredentialSocket;
|
|
315
|
+
}
|
|
291
316
|
try {
|
|
292
317
|
return await runAgent(
|
|
293
318
|
"task-up.sh",
|
|
@@ -295,6 +320,7 @@ export const agent = {
|
|
|
295
320
|
TaskUpData,
|
|
296
321
|
commandDbPath,
|
|
297
322
|
extraEnv,
|
|
323
|
+
JSON.stringify(projectEnv),
|
|
298
324
|
);
|
|
299
325
|
} finally {
|
|
300
326
|
removeCommandDb(commandDbPath);
|
package/lib/agents/claude.ts
CHANGED
|
@@ -52,6 +52,13 @@ const CLAUDE_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
|
52
52
|
// High is the default reasoning level. Update alongside CLAUDE_EFFORTS.
|
|
53
53
|
const CLAUDE_DEFAULT_EFFORT = "high";
|
|
54
54
|
|
|
55
|
+
// ADR-083: cheap defaults apply at the enforcement boundary as well as in the
|
|
56
|
+
// picker. Explicit per-agent choices still win; an API client that omits them
|
|
57
|
+
// must not accidentally run the communicator on Claude's costly account
|
|
58
|
+
// defaults merely because it bypassed the web recommendation button.
|
|
59
|
+
const CLAUDE_COMMUNICATOR_MODEL = "haiku";
|
|
60
|
+
const CLAUDE_COMMUNICATOR_EFFORT = "low";
|
|
61
|
+
|
|
55
62
|
// ---------------------------------------------------------------------------
|
|
56
63
|
// Pure protocol mapping — stream-json line → AgentEvent[].
|
|
57
64
|
// ---------------------------------------------------------------------------
|
|
@@ -177,7 +184,7 @@ function safeStringify(v: unknown): string {
|
|
|
177
184
|
// The session.
|
|
178
185
|
// ---------------------------------------------------------------------------
|
|
179
186
|
|
|
180
|
-
const
|
|
187
|
+
const CLAUDE_BASE_ARGS = [
|
|
181
188
|
"--print",
|
|
182
189
|
"--input-format",
|
|
183
190
|
"stream-json",
|
|
@@ -190,6 +197,10 @@ const CLAUDE_ARGS = [
|
|
|
190
197
|
// 400s when a later turn sees a changed thinking block, so keep each
|
|
191
198
|
// managed stream-json process in-memory only.
|
|
192
199
|
"--no-session-persistence",
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
const CLAUDE_FULL_ACCESS_ARGS = [
|
|
203
|
+
...CLAUDE_BASE_ARGS,
|
|
193
204
|
// Full tool access, no per-call prompts. Safe here precisely because
|
|
194
205
|
// a uai task runs in a throwaway, isolated container operating on a
|
|
195
206
|
// disposable worktree (ADR-001 / ADR-010) — the container *is* the
|
|
@@ -208,6 +219,29 @@ const CLAUDE_ARGS = [
|
|
|
208
219
|
"--strict-mcp-config",
|
|
209
220
|
];
|
|
210
221
|
|
|
222
|
+
/**
|
|
223
|
+
* ADR-083 communicator profile. This is deliberately an engine-level tool
|
|
224
|
+
* boundary, not prompt advice: safe mode suppresses project/user extensions,
|
|
225
|
+
* the explicit tool set contains no shell or mutation primitive, `dontAsk`
|
|
226
|
+
* denies anything outside it in headless mode, and the empty strict MCP config
|
|
227
|
+
* prevents a user-installed server from reintroducing a write/deploy tool.
|
|
228
|
+
*/
|
|
229
|
+
const CLAUDE_COMMUNICATOR_ARGS = [
|
|
230
|
+
...CLAUDE_BASE_ARGS,
|
|
231
|
+
"--safe-mode",
|
|
232
|
+
"--disable-slash-commands",
|
|
233
|
+
"--no-chrome",
|
|
234
|
+
"--permission-mode",
|
|
235
|
+
"dontAsk",
|
|
236
|
+
"--tools",
|
|
237
|
+
"Read,Glob,Grep",
|
|
238
|
+
"--disallowedTools",
|
|
239
|
+
"Bash,Edit,Write,NotebookEdit,Agent,Task,WebFetch,WebSearch",
|
|
240
|
+
"--mcp-config",
|
|
241
|
+
'{"mcpServers":{}}',
|
|
242
|
+
"--strict-mcp-config",
|
|
243
|
+
];
|
|
244
|
+
|
|
211
245
|
export class ClaudeSession implements AgentSession {
|
|
212
246
|
readonly agentId: string;
|
|
213
247
|
readonly kind: AgentKind = "claude";
|
|
@@ -228,6 +262,7 @@ export class ClaudeSession implements AgentSession {
|
|
|
228
262
|
agent: RosterAgent;
|
|
229
263
|
containerName: string;
|
|
230
264
|
systemPreamble: string;
|
|
265
|
+
executionProfile?: "communicator";
|
|
231
266
|
agentEnv?: Record<string, string>;
|
|
232
267
|
}) {
|
|
233
268
|
this.agentId = args.agent.id;
|
|
@@ -236,19 +271,31 @@ export class ClaudeSession implements AgentSession {
|
|
|
236
271
|
// project's defaultPrompt) is passed as a real system prompt via
|
|
237
272
|
// `--append-system-prompt`, so it applies to every turn — not
|
|
238
273
|
// smuggled into the first user message.
|
|
239
|
-
const cliArgs = [
|
|
274
|
+
const cliArgs = [
|
|
275
|
+
...(args.executionProfile === "communicator"
|
|
276
|
+
? CLAUDE_COMMUNICATOR_ARGS
|
|
277
|
+
: CLAUDE_FULL_ACCESS_ARGS),
|
|
278
|
+
];
|
|
240
279
|
if (process.env.UAI_CLAUDE_INCLUDE_PARTIAL_MESSAGES === "1") {
|
|
241
280
|
cliArgs.push("--include-partial-messages");
|
|
242
281
|
}
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
282
|
+
// Explicit task configuration wins; the communicator uses its declared
|
|
283
|
+
// fast/cheap defaults when the task left either choice unspecified.
|
|
284
|
+
const model =
|
|
285
|
+
args.agent.model ??
|
|
286
|
+
(args.executionProfile === "communicator"
|
|
287
|
+
? CLAUDE_COMMUNICATOR_MODEL
|
|
288
|
+
: undefined);
|
|
289
|
+
if (model) {
|
|
290
|
+
cliArgs.push("--model", model);
|
|
247
291
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
292
|
+
const effort =
|
|
293
|
+
args.agent.effort ??
|
|
294
|
+
(args.executionProfile === "communicator"
|
|
295
|
+
? CLAUDE_COMMUNICATOR_EFFORT
|
|
296
|
+
: undefined);
|
|
297
|
+
if (effort) {
|
|
298
|
+
cliArgs.push("--effort", effort);
|
|
252
299
|
}
|
|
253
300
|
if (args.systemPreamble.trim().length > 0) {
|
|
254
301
|
cliArgs.push("--append-system-prompt", args.systemPreamble);
|
|
@@ -261,7 +308,10 @@ export class ClaudeSession implements AgentSession {
|
|
|
261
308
|
// ADR-061: durable by default — the CLI is owned by an in-container
|
|
262
309
|
// runner and survives host restarts (attach resumes it); legacy pipes
|
|
263
310
|
// behind UAI_DURABLE_SESSIONS=0. Claude is host-side stateless, so a
|
|
264
|
-
// live runner can be re-attached (allowAttach).
|
|
311
|
+
// live runner can be re-attached (allowAttach). The communicator profile
|
|
312
|
+
// is the exception: attach is keyed only by task + agent identity, not by
|
|
313
|
+
// execution profile, so a pre-existing full-access runner must be stopped
|
|
314
|
+
// and replaced rather than inherited across this security boundary.
|
|
265
315
|
this.proc = createAgentTransport({
|
|
266
316
|
taskId: args.taskId,
|
|
267
317
|
agentId: this.agentId,
|
|
@@ -270,7 +320,7 @@ export class ClaudeSession implements AgentSession {
|
|
|
270
320
|
cliArgs,
|
|
271
321
|
passEnv: ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"],
|
|
272
322
|
explicitEnv: args.agentEnv ?? {},
|
|
273
|
-
allowAttach:
|
|
323
|
+
allowAttach: args.executionProfile !== "communicator",
|
|
274
324
|
kind: "claude",
|
|
275
325
|
debugLabel: `claude:${this.agentId}`,
|
|
276
326
|
});
|
|
@@ -389,6 +439,14 @@ register({
|
|
|
389
439
|
defaultModel: CLAUDE_DEFAULT_MODEL,
|
|
390
440
|
supportedEfforts: () => [...CLAUDE_EFFORTS],
|
|
391
441
|
defaultEffort: CLAUDE_DEFAULT_EFFORT,
|
|
442
|
+
executionProfiles: [
|
|
443
|
+
{
|
|
444
|
+
id: "communicator",
|
|
445
|
+
mechanism: "claude-safe-mode-tool-allowlist-v1",
|
|
446
|
+
defaultModel: CLAUDE_COMMUNICATOR_MODEL,
|
|
447
|
+
defaultEffort: CLAUDE_COMMUNICATOR_EFFORT,
|
|
448
|
+
},
|
|
449
|
+
],
|
|
392
450
|
// Usable only when a Claude credential is in the env (injected into task
|
|
393
451
|
// containers at task-up). Gates advertisement (ADR-044 P2).
|
|
394
452
|
available: () =>
|
|
@@ -397,6 +455,20 @@ register({
|
|
|
397
455
|
process.env.ANTHROPIC_API_KEY ||
|
|
398
456
|
process.env.ANTHROPIC_AUTH_TOKEN,
|
|
399
457
|
),
|
|
400
|
-
create: async ({
|
|
401
|
-
|
|
458
|
+
create: async ({
|
|
459
|
+
taskId,
|
|
460
|
+
agent,
|
|
461
|
+
containerName,
|
|
462
|
+
systemPreamble,
|
|
463
|
+
executionProfile,
|
|
464
|
+
agentEnv,
|
|
465
|
+
}) =>
|
|
466
|
+
new ClaudeSession({
|
|
467
|
+
taskId,
|
|
468
|
+
agent,
|
|
469
|
+
containerName,
|
|
470
|
+
systemPreamble,
|
|
471
|
+
executionProfile,
|
|
472
|
+
agentEnv,
|
|
473
|
+
}),
|
|
402
474
|
});
|
package/lib/agents/factory.ts
CHANGED
|
@@ -23,7 +23,7 @@ import "./cursor";
|
|
|
23
23
|
import "./opencode";
|
|
24
24
|
|
|
25
25
|
import { agentClisReady } from "../standard-image";
|
|
26
|
-
import { factoryFor } from "./registry";
|
|
26
|
+
import { factoryFor, supportsExecutionProfile } from "./registry";
|
|
27
27
|
import type { AgentSession, AgentSessionFactory } from "./types";
|
|
28
28
|
|
|
29
29
|
/** Cap the wait on agentClisReady so a spawn can never hang forever if the
|
|
@@ -54,6 +54,14 @@ export const realAgentFactory: AgentSessionFactory = {
|
|
|
54
54
|
`no agent adapter registered for kind "${args.agent.kind}"`,
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
|
+
if (
|
|
58
|
+
args.executionProfile &&
|
|
59
|
+
!supportsExecutionProfile(args.agent.kind, args.executionProfile)
|
|
60
|
+
) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`agent adapter "${args.agent.kind}" cannot enforce execution profile "${args.executionProfile}"`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
57
65
|
// Don't spawn a CLI until the shared-volume agent CLIs are reconciled:
|
|
58
66
|
// at boot the CLI auto-upgrade briefly removes then reinstalls codex/claude,
|
|
59
67
|
// and a resume that races that window dies with "No codex executable found
|
package/lib/agents/registry.ts
CHANGED
|
@@ -32,6 +32,13 @@ export interface RegisteredAdapter {
|
|
|
32
32
|
supportedEfforts(): string[];
|
|
33
33
|
/** Preferred effort when the user doesn't pick one. */
|
|
34
34
|
defaultEffort?: string;
|
|
35
|
+
/** Restricted profiles this adapter can enforce by construction. */
|
|
36
|
+
executionProfiles?: Array<{
|
|
37
|
+
id: string;
|
|
38
|
+
mechanism: string;
|
|
39
|
+
defaultModel?: string;
|
|
40
|
+
defaultEffort?: string;
|
|
41
|
+
}>;
|
|
35
42
|
/**
|
|
36
43
|
* Whether this kind is usable on THIS host right now — i.e. its credentials
|
|
37
44
|
* are present (ADR-044 P2). Gates advertisement: an unavailable kind is left
|
|
@@ -52,6 +59,12 @@ export interface AgentKindCapability {
|
|
|
52
59
|
defaultModel?: string;
|
|
53
60
|
supportedEfforts: string[];
|
|
54
61
|
defaultEffort?: string;
|
|
62
|
+
executionProfiles?: Array<{
|
|
63
|
+
id: string;
|
|
64
|
+
mechanism: string;
|
|
65
|
+
defaultModel?: string;
|
|
66
|
+
defaultEffort?: string;
|
|
67
|
+
}>;
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
const adapters = new Map<string, RegisteredAdapter>();
|
|
@@ -79,6 +92,15 @@ export function factoryFor(kind: string): AgentSessionFactory | undefined {
|
|
|
79
92
|
return adapter ? { create: adapter.create } : undefined;
|
|
80
93
|
}
|
|
81
94
|
|
|
95
|
+
/** Whether the adapter declares an engine-enforced execution profile. */
|
|
96
|
+
export function supportsExecutionProfile(kind: string, profileId: string): boolean {
|
|
97
|
+
return Boolean(
|
|
98
|
+
adapters
|
|
99
|
+
.get(kind)
|
|
100
|
+
?.executionProfiles?.some((profile) => profile.id === profileId),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
82
104
|
/**
|
|
83
105
|
* The `agentKinds` capability slice, derived from the registered adapters.
|
|
84
106
|
* Each adapter's `supportedModels()` / `supportedEfforts()` is evaluated here.
|
|
@@ -99,6 +121,11 @@ export function capabilities(): AgentKindCapability[] {
|
|
|
99
121
|
if (adapter.defaultEffort !== undefined) {
|
|
100
122
|
out.defaultEffort = adapter.defaultEffort;
|
|
101
123
|
}
|
|
124
|
+
if (adapter.executionProfiles && adapter.executionProfiles.length > 0) {
|
|
125
|
+
out.executionProfiles = adapter.executionProfiles.map((profile) => ({
|
|
126
|
+
...profile,
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
102
129
|
return out;
|
|
103
130
|
});
|
|
104
131
|
}
|
package/lib/agents/types.ts
CHANGED
|
@@ -165,6 +165,9 @@ export interface AgentSessionFactory {
|
|
|
165
165
|
containerName: string;
|
|
166
166
|
/** Initial briefing — project.defaultPrompt — sent on session start. */
|
|
167
167
|
systemPreamble: string;
|
|
168
|
+
/** Host-derived restricted execution policy (ADR-083). Never supplied by
|
|
169
|
+
* the browser or stored on the roster entry. */
|
|
170
|
+
executionProfile?: "communicator";
|
|
168
171
|
/** ADR-048: extra per-agent env for the `docker exec` (e.g. this agent's
|
|
169
172
|
* own UAI_TASK_TOKEN so its `uai` CLI carries only its own permissions). */
|
|
170
173
|
agentEnv?: Record<string, string>;
|