impel-cli 0.20.0 → 0.20.2-beta.0
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/package.json +1 -1
- package/src/agents.js +409 -184
- package/src/apps.js +5 -3
- package/src/commands/mcp.js +167 -2
- package/src/nativeAgentTransport.js +1962 -0
- package/src/selfInvocation.js +33 -1
package/src/agents.js
CHANGED
|
@@ -9,8 +9,16 @@ import fs from "node:fs";
|
|
|
9
9
|
import os from "node:os";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
CONFIG_DIR,
|
|
14
|
+
normalizeGatewayUrl,
|
|
15
|
+
redactCredentialText,
|
|
16
|
+
redactSecretText,
|
|
17
|
+
} from "./config.js";
|
|
18
|
+
import {
|
|
19
|
+
IMPEL_NATIVE_AGENT_MCP_TARGET,
|
|
20
|
+
impelNativeAgentMcpInvocation,
|
|
21
|
+
} from "./selfInvocation.js";
|
|
14
22
|
import { normalizeTenantId } from "./tenants.js";
|
|
15
23
|
import { renameWithWindowsRetry } from "./windowsFs.js";
|
|
16
24
|
|
|
@@ -20,14 +28,21 @@ export const MANAGED_AGENT_MANIFEST = ".manifest.json";
|
|
|
20
28
|
export const NATIVE_AGENT_LIST_TOOL = "impel_specialists-list_native_agents";
|
|
21
29
|
export const NATIVE_AGENT_START_TOOL = "impel_specialists-start_native_agent_run";
|
|
22
30
|
export const NATIVE_AGENT_READ_TOOL = "impel_specialists-read_native_agent_run";
|
|
31
|
+
export const NATIVE_AGENT_RUN_TOOL = "run_native_agent";
|
|
32
|
+
export const NATIVE_AGENT_RESUME_TOOL = "resume_native_agent_run";
|
|
33
|
+
export const NATIVE_AGENT_RECOVER_TOOL = "recover_native_agent_runs";
|
|
23
34
|
export const MANAGED_AGENT_MCP_SERVER = "impel_agent";
|
|
24
|
-
export const MANAGED_AGENT_MANIFEST_VERSION =
|
|
35
|
+
export const MANAGED_AGENT_MANIFEST_VERSION = 6;
|
|
25
36
|
|
|
26
37
|
const NATIVE_AGENT_TOOL_NAMES = [
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
NATIVE_AGENT_RUN_TOOL,
|
|
39
|
+
NATIVE_AGENT_RESUME_TOOL,
|
|
40
|
+
NATIVE_AGENT_RECOVER_TOOL,
|
|
30
41
|
];
|
|
42
|
+
const NATIVE_AGENT_RECOVERY_TOOL_NAMES = [NATIVE_AGENT_RESUME_TOOL, NATIVE_AGENT_RECOVER_TOOL];
|
|
43
|
+
const MAX_RETIRED_AGENT_BINDINGS = 50;
|
|
44
|
+
const MAX_NATIVE_AGENT_STATE_BYTES = 512 * 1024;
|
|
45
|
+
const TERMINAL_NATIVE_AGENT_STATUSES = new Set(["succeeded", "failed", "cancelled", "canceled"]);
|
|
31
46
|
const SAFE_AGENT_ID_RE = /^[A-Za-z0-9_.:-]{1,160}$/u;
|
|
32
47
|
const SAFE_SCOPE_PARAM_RE = /^[A-Za-z0-9_.:-]{1,160}$/u;
|
|
33
48
|
const MAX_CATALOG_ITEMS = 500;
|
|
@@ -284,6 +299,222 @@ function agentBindingHash(agent) {
|
|
|
284
299
|
.slice(0, 8);
|
|
285
300
|
}
|
|
286
301
|
|
|
302
|
+
function stablePolicyValue(value) {
|
|
303
|
+
if (Array.isArray(value)) return value.map(stablePolicyValue);
|
|
304
|
+
if (!value || typeof value !== "object") return value;
|
|
305
|
+
const result = {};
|
|
306
|
+
for (const key of Object.keys(value).sort()) result[key] = stablePolicyValue(value[key]);
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Fingerprint the complete synchronized routing and side-effect policy. */
|
|
311
|
+
export function nativeAgentPolicyFingerprint(agent) {
|
|
312
|
+
return crypto.createHash("sha256")
|
|
313
|
+
.update(JSON.stringify(stablePolicyValue(agent)))
|
|
314
|
+
.digest("hex");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function validatedPendingState(filePath, invocationId) {
|
|
318
|
+
let serialized;
|
|
319
|
+
try {
|
|
320
|
+
const stat = fs.lstatSync(filePath);
|
|
321
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.size > MAX_NATIVE_AGENT_STATE_BYTES) {
|
|
322
|
+
throw new Error("unsafe state file");
|
|
323
|
+
}
|
|
324
|
+
serialized = fs.readFileSync(filePath, "utf8");
|
|
325
|
+
} catch {
|
|
326
|
+
throw new Error("native-agent run state is unsafe or invalid during profile sync");
|
|
327
|
+
}
|
|
328
|
+
if (redactCredentialText(serialized) !== serialized) {
|
|
329
|
+
throw new Error("native-agent run state is unsafe or invalid during profile sync");
|
|
330
|
+
}
|
|
331
|
+
let state;
|
|
332
|
+
try {
|
|
333
|
+
state = JSON.parse(serialized);
|
|
334
|
+
} catch {
|
|
335
|
+
throw new Error("native-agent run state is invalid during profile sync");
|
|
336
|
+
}
|
|
337
|
+
let stateTenant;
|
|
338
|
+
try {
|
|
339
|
+
stateTenant = normalizeTenantId(state?.tenantId);
|
|
340
|
+
} catch {
|
|
341
|
+
throw new Error("native-agent run state failed integrity validation during profile sync");
|
|
342
|
+
}
|
|
343
|
+
const allowed = new Set([
|
|
344
|
+
"schema", "invocationId", "tenantId", "agentId", "scopeParam", "policyFingerprint",
|
|
345
|
+
"requestFingerprint", "idempotencyKey", "startArguments", "upstreamRunId", "status",
|
|
346
|
+
"startAttempts", "createdAt", "updatedAt", "fence", "sequence", "lastTransportError",
|
|
347
|
+
"result", "output", "error",
|
|
348
|
+
]);
|
|
349
|
+
if (!state || typeof state !== "object" || Array.isArray(state)
|
|
350
|
+
|| Object.keys(state).some((key) => !allowed.has(key))
|
|
351
|
+
|| state.schema !== "impel.native-agent-state.v1"
|
|
352
|
+
|| state.invocationId !== invocationId
|
|
353
|
+
|| !/^[a-f0-9-]{36}$/u.test(state.invocationId)
|
|
354
|
+
|| stateTenant !== state.tenantId
|
|
355
|
+
|| !SAFE_AGENT_ID_RE.test(state.agentId)
|
|
356
|
+
|| !SAFE_SCOPE_PARAM_RE.test(state.scopeParam)
|
|
357
|
+
|| !/^[a-f0-9]{64}$/u.test(state.policyFingerprint)
|
|
358
|
+
|| !/^[a-f0-9]{64}$/u.test(state.requestFingerprint)
|
|
359
|
+
|| typeof state.idempotencyKey !== "string"
|
|
360
|
+
|| !/^[A-Za-z0-9._:-]{16,200}$/u.test(state.idempotencyKey)
|
|
361
|
+
|| typeof state.status !== "string"
|
|
362
|
+
|| !state.status
|
|
363
|
+
|| !Number.isSafeInteger(state.startAttempts)
|
|
364
|
+
|| state.startAttempts < 0
|
|
365
|
+
|| !Number.isFinite(Date.parse(state.createdAt))
|
|
366
|
+
|| !Number.isFinite(Date.parse(state.updatedAt))
|
|
367
|
+
|| (state.fence !== undefined && (!Number.isSafeInteger(state.fence) || state.fence < 0))
|
|
368
|
+
|| (state.sequence !== undefined && (!Number.isSafeInteger(state.sequence) || state.sequence < 0))
|
|
369
|
+
|| (state.upstreamRunId !== null
|
|
370
|
+
&& (typeof state.upstreamRunId !== "string" || !state.upstreamRunId))) {
|
|
371
|
+
throw new Error("native-agent run state failed integrity validation during profile sync");
|
|
372
|
+
}
|
|
373
|
+
const args = state.startArguments;
|
|
374
|
+
const allowedArgs = new Set([
|
|
375
|
+
"agentId", "scopeParam", "task", "context", "contextKeys", "confirmedSideEffects", "idempotencyKey",
|
|
376
|
+
]);
|
|
377
|
+
if (!args || typeof args !== "object" || Array.isArray(args)
|
|
378
|
+
|| Object.keys(args).some((key) => !allowedArgs.has(key))
|
|
379
|
+
|| args.agentId !== state.agentId
|
|
380
|
+
|| args.scopeParam !== state.scopeParam
|
|
381
|
+
|| args.idempotencyKey !== state.idempotencyKey
|
|
382
|
+
|| typeof args.task !== "string"
|
|
383
|
+
|| !args.task.trim()
|
|
384
|
+
|| args.task.length > 40_000
|
|
385
|
+
|| (args.context !== undefined && (typeof args.context !== "string" || args.context.length > 40_000))
|
|
386
|
+
|| !Array.isArray(args.contextKeys)
|
|
387
|
+
|| args.contextKeys.length > 30
|
|
388
|
+
|| args.contextKeys.some((key) => typeof key !== "string" || !key.trim() || key.length > 160)
|
|
389
|
+
|| new Set(args.contextKeys).size !== args.contextKeys.length
|
|
390
|
+
|| (args.confirmedSideEffects !== undefined && typeof args.confirmedSideEffects !== "boolean")) {
|
|
391
|
+
throw new Error("native-agent run state failed integrity validation during profile sync");
|
|
392
|
+
}
|
|
393
|
+
const fingerprint = crypto.createHash("sha256").update(JSON.stringify(stablePolicyValue({
|
|
394
|
+
tenantId: state.tenantId,
|
|
395
|
+
agentId: state.agentId,
|
|
396
|
+
scopeParam: state.scopeParam,
|
|
397
|
+
policyFingerprint: state.policyFingerprint,
|
|
398
|
+
arguments: args,
|
|
399
|
+
}))).digest("hex");
|
|
400
|
+
if (fingerprint !== state.requestFingerprint) {
|
|
401
|
+
throw new Error("native-agent run state failed integrity validation during profile sync");
|
|
402
|
+
}
|
|
403
|
+
return state;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function hasNativeAgentDeletionTombstone(tenantRoot, invocationId) {
|
|
407
|
+
const tombstonePath = path.join(tenantRoot, `${invocationId}.tombstone`);
|
|
408
|
+
if (!fs.existsSync(tombstonePath)) return false;
|
|
409
|
+
const stat = fs.lstatSync(tombstonePath);
|
|
410
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.size > 1024) {
|
|
411
|
+
throw new Error("native-agent deletion tombstone is unsafe or invalid during profile sync");
|
|
412
|
+
}
|
|
413
|
+
let tombstone;
|
|
414
|
+
try {
|
|
415
|
+
tombstone = JSON.parse(fs.readFileSync(tombstonePath, "utf8"));
|
|
416
|
+
} catch {
|
|
417
|
+
throw new Error("native-agent deletion tombstone is invalid during profile sync");
|
|
418
|
+
}
|
|
419
|
+
const allowed = new Set(["schema", "invocationId", "fence", "deletedAt"]);
|
|
420
|
+
if (!tombstone
|
|
421
|
+
|| typeof tombstone !== "object"
|
|
422
|
+
|| Array.isArray(tombstone)
|
|
423
|
+
|| Object.keys(tombstone).some((key) => !allowed.has(key))
|
|
424
|
+
|| tombstone.schema !== "impel.native-agent-deletion.v1"
|
|
425
|
+
|| tombstone.invocationId !== invocationId
|
|
426
|
+
|| !Number.isSafeInteger(tombstone.fence)
|
|
427
|
+
|| tombstone.fence < 1
|
|
428
|
+
|| !Number.isFinite(Date.parse(tombstone.deletedAt))) {
|
|
429
|
+
throw new Error("native-agent deletion tombstone is invalid during profile sync");
|
|
430
|
+
}
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Discover only pending bindings from the highest valid fenced state revision. */
|
|
435
|
+
export function pendingNativeAgentBindings(tenantId, {
|
|
436
|
+
runsRoot = path.join(CONFIG_DIR, "native-agent-runs"),
|
|
437
|
+
} = {}) {
|
|
438
|
+
const normalizedTenant = normalizeTenantId(tenantId);
|
|
439
|
+
const tenantRoot = path.join(runsRoot, normalizedTenant);
|
|
440
|
+
if (!fs.existsSync(tenantRoot)) return [];
|
|
441
|
+
const rootStat = fs.lstatSync(tenantRoot);
|
|
442
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
443
|
+
throw new Error("native-agent run directory is unsafe or invalid");
|
|
444
|
+
}
|
|
445
|
+
const bindings = new Map();
|
|
446
|
+
for (const name of fs.readdirSync(tenantRoot)) {
|
|
447
|
+
if (!/^[a-f0-9-]{36}\.json$/u.test(name)) continue;
|
|
448
|
+
const invocationId = path.basename(name, ".json");
|
|
449
|
+
if (hasNativeAgentDeletionTombstone(tenantRoot, invocationId)) continue;
|
|
450
|
+
const candidates = [{ filePath: path.join(tenantRoot, name), expected: null }];
|
|
451
|
+
const versionsPath = path.join(tenantRoot, `${invocationId}.state-versions`);
|
|
452
|
+
if (fs.existsSync(versionsPath)) {
|
|
453
|
+
const stat = fs.lstatSync(versionsPath);
|
|
454
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
455
|
+
throw new Error("native-agent state revisions are unsafe or invalid during profile sync");
|
|
456
|
+
}
|
|
457
|
+
for (const revision of fs.readdirSync(versionsPath)) {
|
|
458
|
+
if (!revision.endsWith(".json")) continue;
|
|
459
|
+
const match = /^(\d{16})-(\d{16})-[a-f0-9]{32}\.json$/u.exec(revision);
|
|
460
|
+
const expected = match ? [Number(match[1]), Number(match[2])] : null;
|
|
461
|
+
if (!match
|
|
462
|
+
|| path.basename(revision) !== revision
|
|
463
|
+
|| !expected.every(Number.isSafeInteger)) {
|
|
464
|
+
throw new Error("native-agent state revision is invalid during profile sync");
|
|
465
|
+
}
|
|
466
|
+
candidates.push({
|
|
467
|
+
filePath: path.join(versionsPath, revision),
|
|
468
|
+
expected,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
let latest = null;
|
|
473
|
+
for (const candidate of candidates) {
|
|
474
|
+
const state = validatedPendingState(candidate.filePath, invocationId);
|
|
475
|
+
if (state.tenantId !== normalizedTenant) {
|
|
476
|
+
throw new Error("native-agent run state failed tenant validation during profile sync");
|
|
477
|
+
}
|
|
478
|
+
const coordinates = [state.fence ?? 0, state.sequence ?? 0];
|
|
479
|
+
if (candidate.expected
|
|
480
|
+
&& (coordinates[0] !== candidate.expected[0]
|
|
481
|
+
|| coordinates[1] !== candidate.expected[1])) {
|
|
482
|
+
throw new Error("native-agent state revision failed integrity validation during profile sync");
|
|
483
|
+
}
|
|
484
|
+
if (!latest
|
|
485
|
+
|| coordinates[0] > latest.coordinates[0]
|
|
486
|
+
|| (coordinates[0] === latest.coordinates[0] && coordinates[1] > latest.coordinates[1])) {
|
|
487
|
+
latest = { state, coordinates };
|
|
488
|
+
} else if (coordinates[0] === latest.coordinates[0]
|
|
489
|
+
&& coordinates[1] === latest.coordinates[1]
|
|
490
|
+
&& JSON.stringify(stablePolicyValue(state)) !== JSON.stringify(stablePolicyValue(latest.state))) {
|
|
491
|
+
throw new Error("native-agent state revisions conflict during profile sync");
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (!latest || TERMINAL_NATIVE_AGENT_STATUSES.has(latest.state.status)) continue;
|
|
495
|
+
const state = latest.state;
|
|
496
|
+
const key = `${state.agentId}\0${state.scopeParam}\0${state.policyFingerprint}`;
|
|
497
|
+
const prior = bindings.get(key);
|
|
498
|
+
if (!prior || Date.parse(state.updatedAt) > Date.parse(prior.updatedAt)) {
|
|
499
|
+
bindings.set(key, {
|
|
500
|
+
agentId: state.agentId,
|
|
501
|
+
scopeParam: state.scopeParam,
|
|
502
|
+
policyFingerprint: state.policyFingerprint,
|
|
503
|
+
updatedAt: state.updatedAt,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const result = [...bindings.values()].sort((left, right) =>
|
|
508
|
+
Date.parse(right.updatedAt) - Date.parse(left.updatedAt)
|
|
509
|
+
|| left.agentId.localeCompare(right.agentId)
|
|
510
|
+
|| left.scopeParam.localeCompare(right.scopeParam)
|
|
511
|
+
);
|
|
512
|
+
if (result.length > MAX_RETIRED_AGENT_BINDINGS) {
|
|
513
|
+
throw new Error("too many pending native-agent bindings to preserve recovery adapters");
|
|
514
|
+
}
|
|
515
|
+
return result;
|
|
516
|
+
}
|
|
517
|
+
|
|
287
518
|
function generatedAgentFileStems(tenantId, agents) {
|
|
288
519
|
const used = new Map();
|
|
289
520
|
return agents.map((agent) => {
|
|
@@ -322,21 +553,20 @@ function generatedClientAgentNames(client, agents) {
|
|
|
322
553
|
}
|
|
323
554
|
|
|
324
555
|
function claudeAdapterInstructions(tenantId, agent) {
|
|
325
|
-
const toolName = nativeToolName;
|
|
326
556
|
const contextRequirement = agent.requiredContext.length
|
|
327
557
|
? ` Required context keys are ${JSON.stringify(agent.requiredContext)}; if any are absent, ask for them before starting the run.`
|
|
328
558
|
: "";
|
|
329
559
|
const sideEffectInstruction = agent.sideEffects === "writes"
|
|
330
|
-
? `The
|
|
331
|
-
: `The
|
|
560
|
+
? `The synchronized policy declares that this agent writes user or workspace data. Use this adapter only when the user explicitly selected this exact agent for the current request. If the agent was chosen automatically or the selection is ambiguous, do not start it and ask the user to select it explicitly.`
|
|
561
|
+
: `The synchronized policy declares that this agent is read-only.`;
|
|
332
562
|
return [
|
|
333
563
|
`You are a thin transport adapter for the exact Impel native agent ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
|
|
334
564
|
`Do not perform the assigned task yourself and do not delegate to any other agent.`,
|
|
335
|
-
`
|
|
565
|
+
`Confirm that the request fits the synchronized capabilities ${JSON.stringify(agent.capabilities)} and none of the exclusions ${JSON.stringify(agent.exclusions)}.${contextRequirement}`,
|
|
336
566
|
sideEffectInstruction,
|
|
337
|
-
`Call ${
|
|
338
|
-
`
|
|
339
|
-
`When it succeeds, return
|
|
567
|
+
`Call ${nativeToolName(NATIVE_AGENT_RUN_TOOL)} exactly once with task set to the complete assigned task, optional context set to one string containing all supplied context, and contextKeys naming the fields present in that string.`,
|
|
568
|
+
`If that call returns an ${JSON.stringify("impel.native-agent-run.v1")} handle, call ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} with exactly that same handle until the transport returns a terminal result. If the call is cancelled, the connection closes, or its result is unknown before you receive a handle, call ${nativeToolName(NATIVE_AGENT_RECOVER_TOOL)} with {} and resume the newest returned pending handle; if multiple handles cannot be safely associated with this request, report them instead of choosing. Never call run_native_agent again for this request, and never restart, nudge, replace, or independently poll the upstream run.`,
|
|
569
|
+
`When it succeeds, return finalText byte-for-byte as the answer with no preface or rewriting. When it fails, return the preserved runId, output, and error. Never invent or independently synthesize a replacement result.`,
|
|
340
570
|
].join(" ");
|
|
341
571
|
}
|
|
342
572
|
|
|
@@ -344,179 +574,46 @@ function nativeToolName(toolName) {
|
|
|
344
574
|
return `mcp__${MANAGED_AGENT_MCP_SERVER}__${toolName}`;
|
|
345
575
|
}
|
|
346
576
|
|
|
347
|
-
const CODEX_TASK_PLACEHOLDER = "__IMPEL_COMPLETE_ASSIGNED_TASK_JSON__";
|
|
348
|
-
const CODEX_CONTEXT_PLACEHOLDER = "__IMPEL_OPTIONAL_CONTEXT_STRING_OR_NULL_JSON__";
|
|
349
|
-
const CODEX_CONTEXT_KEYS_PLACEHOLDER = "__IMPEL_SUPPLIED_CONTEXT_KEYS_JSON__";
|
|
350
|
-
const CODEX_IDEMPOTENCY_KEY_PLACEHOLDER = "__IMPEL_LOGICAL_INVOCATION_IDEMPOTENCY_KEY_JSON__";
|
|
351
|
-
|
|
352
|
-
export function renderCodexAdapterOrchestration(tenantId, agent) {
|
|
353
|
-
const expectedTenantId = normalizeTenantId(tenantId);
|
|
354
|
-
const nestedToolName = (toolName) => nativeToolName(toolName).replaceAll("-", "_");
|
|
355
|
-
const listTool = nestedToolName(NATIVE_AGENT_LIST_TOOL);
|
|
356
|
-
const startTool = nestedToolName(NATIVE_AGENT_START_TOOL);
|
|
357
|
-
const readTool = nestedToolName(NATIVE_AGENT_READ_TOOL);
|
|
358
|
-
return [
|
|
359
|
-
'// @exec: {"yield_time_ms": 30000, "max_output_tokens": 30000}',
|
|
360
|
-
`const assignedTask = ${CODEX_TASK_PLACEHOLDER};`,
|
|
361
|
-
`const suppliedContext = ${CODEX_CONTEXT_PLACEHOLDER};`,
|
|
362
|
-
`const suppliedContextKeys = ${CODEX_CONTEXT_KEYS_PLACEHOLDER};`,
|
|
363
|
-
`const idempotencyKey = ${CODEX_IDEMPOTENCY_KEY_PLACEHOLDER};`,
|
|
364
|
-
`const expectedTenantId = ${JSON.stringify(expectedTenantId)};`,
|
|
365
|
-
`const expectedAgent = ${JSON.stringify(agent)};`,
|
|
366
|
-
"const waitSeconds = 20; // Compatible fallback until the bound server advertises a larger ceiling.",
|
|
367
|
-
`const listToolName = ${JSON.stringify(listTool)};`,
|
|
368
|
-
`const startToolName = ${JSON.stringify(startTool)};`,
|
|
369
|
-
`const readToolName = ${JSON.stringify(readTool)};`,
|
|
370
|
-
"",
|
|
371
|
-
"function stableValue(value) {",
|
|
372
|
-
" if (Array.isArray(value)) return value.map(stableValue);",
|
|
373
|
-
' if (!value || typeof value !== "object") return value;',
|
|
374
|
-
" const result = {};",
|
|
375
|
-
" for (const key of Object.keys(value).sort()) result[key] = stableValue(value[key]);",
|
|
376
|
-
" return result;",
|
|
377
|
-
"}",
|
|
378
|
-
"",
|
|
379
|
-
"function contentText(response) {",
|
|
380
|
-
' return response?.content?.find((item) => item?.type === "text" && typeof item.text === "string")?.text;',
|
|
381
|
-
"}",
|
|
382
|
-
"",
|
|
383
|
-
"function toolPayload(response, label) {",
|
|
384
|
-
' if (!response || typeof response !== "object") throw new Error(label + " returned no result");',
|
|
385
|
-
' if (response.isError) throw new Error(contentText(response) || label + " failed");',
|
|
386
|
-
' if (response.structuredContent && typeof response.structuredContent === "object") return response.structuredContent;',
|
|
387
|
-
" const serialized = contentText(response);",
|
|
388
|
-
" if (serialized !== undefined) {",
|
|
389
|
-
" try {",
|
|
390
|
-
" return JSON.parse(serialized);",
|
|
391
|
-
" } catch {",
|
|
392
|
-
' throw new Error(label + " returned invalid JSON");',
|
|
393
|
-
" }",
|
|
394
|
-
" }",
|
|
395
|
-
" return response;",
|
|
396
|
-
"}",
|
|
397
|
-
"",
|
|
398
|
-
"function preservedOutput(run) {",
|
|
399
|
-
' if (run && Object.prototype.hasOwnProperty.call(run, "output")) return run.output;',
|
|
400
|
-
' if (run && Object.prototype.hasOwnProperty.call(run, "result")) return run.result;',
|
|
401
|
-
" return null;",
|
|
402
|
-
"}",
|
|
403
|
-
"",
|
|
404
|
-
"function errorValue(error, fallback) {",
|
|
405
|
-
" if (error === undefined || error === null) return fallback;",
|
|
406
|
-
' if (error instanceof Error) return error.message;',
|
|
407
|
-
" return error;",
|
|
408
|
-
"}",
|
|
409
|
-
"",
|
|
410
|
-
"async function orchestrate() {",
|
|
411
|
-
" let runId = null;",
|
|
412
|
-
" let latestRun = null;",
|
|
413
|
-
" try {",
|
|
414
|
-
' if (typeof assignedTask !== "string" || !assignedTask.trim()) throw new Error("the complete assigned task is required");',
|
|
415
|
-
' if (suppliedContext !== null && typeof suppliedContext !== "string") throw new Error("supplied context must be a string or null");',
|
|
416
|
-
' if (expectedAgent.requiredContext.length && (typeof suppliedContext !== "string" || !suppliedContext.trim())) {',
|
|
417
|
-
' throw new Error("nonblank supplied context is required for required context keys");',
|
|
418
|
-
" }",
|
|
419
|
-
' if (!Array.isArray(suppliedContextKeys)) throw new Error("supplied context keys must be an array");',
|
|
420
|
-
' if (typeof idempotencyKey !== "string" || !/^[A-Za-z0-9._:-]{16,160}$/u.test(idempotencyKey)) {',
|
|
421
|
-
' throw new Error("a unique stable logical-invocation idempotencyKey is required");',
|
|
422
|
-
" }",
|
|
423
|
-
" const missingContext = expectedAgent.requiredContext.filter((key) =>",
|
|
424
|
-
" !suppliedContextKeys.includes(key)",
|
|
425
|
-
" );",
|
|
426
|
-
' if (missingContext.length) throw new Error("missing required context: " + missingContext.join(", "));',
|
|
427
|
-
"",
|
|
428
|
-
" const catalog = toolPayload(await tools[listToolName]({}), \"native-agent catalog\");",
|
|
429
|
-
' if (catalog.orgId !== expectedTenantId) throw new Error("native-agent catalog tenant mismatch");',
|
|
430
|
-
' if (!Array.isArray(catalog.agents)) throw new Error("native-agent catalog returned no agents");',
|
|
431
|
-
" const matches = catalog.agents.filter((candidate) =>",
|
|
432
|
-
" candidate?.agentId === expectedAgent.agentId && candidate?.scopeParam === expectedAgent.scopeParam",
|
|
433
|
-
" );",
|
|
434
|
-
' if (matches.length !== 1) throw new Error("exact native-agent binding is unavailable");',
|
|
435
|
-
" const actualAgent = {};",
|
|
436
|
-
" for (const key of Object.keys(expectedAgent)) actualAgent[key] = matches[0][key];",
|
|
437
|
-
" if (JSON.stringify(stableValue(actualAgent)) !== JSON.stringify(stableValue(expectedAgent))) {",
|
|
438
|
-
' throw new Error("native-agent catalog policy mismatch");',
|
|
439
|
-
" }",
|
|
440
|
-
"",
|
|
441
|
-
" const startArguments = {",
|
|
442
|
-
" agentId: expectedAgent.agentId,",
|
|
443
|
-
" scopeParam: expectedAgent.scopeParam,",
|
|
444
|
-
" task: assignedTask,",
|
|
445
|
-
" contextKeys: suppliedContextKeys,",
|
|
446
|
-
" idempotencyKey,",
|
|
447
|
-
" };",
|
|
448
|
-
" if (suppliedContext !== null) startArguments.context = suppliedContext;",
|
|
449
|
-
' if (expectedAgent.sideEffects === "writes") startArguments.confirmedSideEffects = true;',
|
|
450
|
-
" latestRun = toolPayload(await tools[startToolName](startArguments), \"native-agent start\");",
|
|
451
|
-
' if (typeof latestRun.runId !== "string" || !latestRun.runId) throw new Error("native-agent start returned no runId");',
|
|
452
|
-
" runId = latestRun.runId;",
|
|
453
|
-
"",
|
|
454
|
-
" for (;;) {",
|
|
455
|
-
" const observed = toolPayload(",
|
|
456
|
-
" await tools[readToolName]({ runId, waitSeconds }),",
|
|
457
|
-
' "native-agent read",',
|
|
458
|
-
" );",
|
|
459
|
-
' if (observed.runId !== undefined && observed.runId !== runId) throw new Error("native-agent read returned a different runId");',
|
|
460
|
-
" latestRun = observed;",
|
|
461
|
-
' if (observed.status === "succeeded") {',
|
|
462
|
-
' const finalText = observed.result?.finalText;',
|
|
463
|
-
' if (typeof finalText !== "string") throw new Error("succeeded native-agent run returned no finalText");',
|
|
464
|
-
" text(finalText);",
|
|
465
|
-
" return;",
|
|
466
|
-
" }",
|
|
467
|
-
' if (observed.status === "failed") {',
|
|
468
|
-
" text(JSON.stringify({",
|
|
469
|
-
" runId,",
|
|
470
|
-
" output: preservedOutput(observed),",
|
|
471
|
-
' error: errorValue(observed.error, "native-agent run failed"),',
|
|
472
|
-
" }));",
|
|
473
|
-
" return;",
|
|
474
|
-
" }",
|
|
475
|
-
" }",
|
|
476
|
-
" } catch (error) {",
|
|
477
|
-
" text(JSON.stringify({",
|
|
478
|
-
" runId,",
|
|
479
|
-
" output: preservedOutput(latestRun),",
|
|
480
|
-
' error: errorValue(error, "native-agent adapter failed"),',
|
|
481
|
-
" }));",
|
|
482
|
-
" }",
|
|
483
|
-
"}",
|
|
484
|
-
"",
|
|
485
|
-
"await orchestrate();",
|
|
486
|
-
].join("\n");
|
|
487
|
-
}
|
|
488
|
-
|
|
489
577
|
function codexAdapterInstructions(tenantId, agent) {
|
|
490
578
|
const contextRequirement = agent.requiredContext.length
|
|
491
579
|
? ` Required context keys are ${JSON.stringify(agent.requiredContext)}; if any are absent, ask for them before starting the run.`
|
|
492
580
|
: "";
|
|
493
581
|
const sideEffectInstruction = agent.sideEffects === "writes"
|
|
494
|
-
? `The
|
|
495
|
-
: `The
|
|
496
|
-
const source = renderCodexAdapterOrchestration(tenantId, agent);
|
|
582
|
+
? `The synchronized policy declares that this agent writes user or workspace data. Use this adapter only when the user explicitly selected this exact agent for the current request. If the agent was chosen automatically or the selection is ambiguous, ask the user to select this exact agent explicitly.`
|
|
583
|
+
: `The synchronized policy declares that this agent is read-only.`;
|
|
497
584
|
return [
|
|
498
585
|
`You are a thin transport adapter for the exact Impel native agent ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
|
|
499
586
|
`Callers must spawn this explicit custom Codex agent with fork_turns="none" and must relay your result verbatim; this is caller guidance and cannot enforce host spawn behavior.`,
|
|
500
587
|
`Do not perform the assigned task yourself, do not delegate to any other agent, and do not independently synthesize or rewrite the result.`,
|
|
501
588
|
`Confirm that the assigned request fits the cataloged capabilities ${JSON.stringify(agent.capabilities)} and none of the exclusions ${JSON.stringify(agent.exclusions)} before starting.${contextRequirement}`,
|
|
502
589
|
sideEffectInstruction,
|
|
503
|
-
`
|
|
504
|
-
`
|
|
505
|
-
`
|
|
506
|
-
"",
|
|
507
|
-
source,
|
|
590
|
+
`Call ${nativeToolName(NATIVE_AGENT_RUN_TOOL)} exactly once with the complete task, optional context string, and supplied context keys. If it returns an ${JSON.stringify("impel.native-agent-run.v1")} handle, call ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} with exactly that same handle until the transport returns a terminal result. If the call is cancelled, the connection closes, or its result is unknown before you receive a handle, call ${nativeToolName(NATIVE_AGENT_RECOVER_TOOL)} with {} and resume the newest returned pending handle; if multiple handles cannot be safely associated with this request, report them instead of choosing. Never call run_native_agent again for this request, and never restart, nudge, replace, or independently poll the upstream run.`,
|
|
591
|
+
`While a remote wait is healthy, the parent must not message, follow up with, or interrupt this adapter.`,
|
|
592
|
+
`Return successful finalText byte-for-byte with no preface, rewriting, Markdown changes, or independent synthesis. A failure preserves the durable runId, output, and error.`,
|
|
508
593
|
].join("\n\n");
|
|
509
594
|
}
|
|
510
595
|
|
|
511
|
-
function
|
|
512
|
-
|
|
596
|
+
function retiredAdapterInstructions(tenantId, agent) {
|
|
597
|
+
return [
|
|
598
|
+
`You are a recovery-only transport adapter for retired Impel native-agent binding ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
|
|
599
|
+
`You cannot start new work. Never call run_native_agent and never perform or recreate the assigned task yourself.`,
|
|
600
|
+
`Call ${nativeToolName(NATIVE_AGENT_RECOVER_TOOL)} with {} to obtain integrity-validated pending handles for this fixed binding, then call ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} with the intended handle until it returns a terminal result. If multiple handles cannot be safely associated with the request, report them instead of choosing.`,
|
|
601
|
+
`Return successful finalText byte-for-byte. For failure, return the preserved runId, output, and error without inventing a replacement result.`,
|
|
602
|
+
].join(" ");
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function renderClaudeAgent({ tenantId, agent, name, invocation, recoveryOnly = false }) {
|
|
606
|
+
const description = recoveryOnly
|
|
607
|
+
? `Recovery-only access to pending runs for retired Impel binding ${agent.agentId} in tenant ${tenantId}.`
|
|
608
|
+
: `Explicitly runs ${agent.title} for Impel tenant ${tenantId}${agent.sideEffects === "writes" ? " (may write user or workspace data)" : " (read-only)"}: ${agent.description}`.slice(0, 900);
|
|
609
|
+
const toolNames = recoveryOnly ? NATIVE_AGENT_RECOVERY_TOOL_NAMES : NATIVE_AGENT_TOOL_NAMES;
|
|
513
610
|
const lines = [
|
|
514
611
|
"---",
|
|
515
612
|
`name: ${JSON.stringify(name)}`,
|
|
516
613
|
`description: ${JSON.stringify(description)}`,
|
|
517
614
|
"model: inherit",
|
|
518
615
|
"tools:",
|
|
519
|
-
...
|
|
616
|
+
...toolNames.map((tool) => ` - ${JSON.stringify(nativeToolName(tool))}`),
|
|
520
617
|
"mcpServers:",
|
|
521
618
|
` - ${MANAGED_AGENT_MCP_SERVER}:`,
|
|
522
619
|
" type: stdio",
|
|
@@ -527,14 +624,17 @@ function renderClaudeAgent({ tenantId, agent, name, invocation }) {
|
|
|
527
624
|
...Object.entries(invocation.env || {}).map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`),
|
|
528
625
|
"---",
|
|
529
626
|
"",
|
|
530
|
-
claudeAdapterInstructions(tenantId, agent),
|
|
627
|
+
recoveryOnly ? retiredAdapterInstructions(tenantId, agent) : claudeAdapterInstructions(tenantId, agent),
|
|
531
628
|
"",
|
|
532
629
|
];
|
|
533
630
|
return lines.join("\n");
|
|
534
631
|
}
|
|
535
632
|
|
|
536
|
-
function renderCodexAgent({ tenantId, agent, name, invocation }) {
|
|
537
|
-
const description =
|
|
633
|
+
function renderCodexAgent({ tenantId, agent, name, invocation, recoveryOnly = false }) {
|
|
634
|
+
const description = recoveryOnly
|
|
635
|
+
? `Recovery-only custom agent for pending runs from retired Impel binding ${agent.agentId} in tenant ${tenantId}.`
|
|
636
|
+
: `Explicit custom agent: callers must use fork_turns="none" and relay its result verbatim. Runs ${agent.title} for Impel tenant ${tenantId}${agent.sideEffects === "writes" ? " (may write user or workspace data)" : " (read-only)"}: ${agent.description}`.slice(0, 900);
|
|
637
|
+
const toolNames = recoveryOnly ? NATIVE_AGENT_RECOVERY_TOOL_NAMES : NATIVE_AGENT_TOOL_NAMES;
|
|
538
638
|
const envEntries = Object.entries(invocation.env || {})
|
|
539
639
|
.map(([key, value]) => `${JSON.stringify(key)} = ${JSON.stringify(value)}`)
|
|
540
640
|
.join(", ");
|
|
@@ -542,22 +642,51 @@ function renderCodexAgent({ tenantId, agent, name, invocation }) {
|
|
|
542
642
|
`name = ${JSON.stringify(name)}`,
|
|
543
643
|
`description = ${JSON.stringify(description)}`,
|
|
544
644
|
'sandbox_mode = "read-only"',
|
|
545
|
-
`developer_instructions = ${JSON.stringify(codexAdapterInstructions(tenantId, agent))}`,
|
|
645
|
+
`developer_instructions = ${JSON.stringify(recoveryOnly ? retiredAdapterInstructions(tenantId, agent) : codexAdapterInstructions(tenantId, agent))}`,
|
|
546
646
|
"",
|
|
547
647
|
`[mcp_servers.${MANAGED_AGENT_MCP_SERVER}]`,
|
|
548
648
|
`command = ${JSON.stringify(invocation.command)}`,
|
|
549
649
|
`args = [${invocation.args.map((argument) => JSON.stringify(argument)).join(", ")}]`,
|
|
550
|
-
`enabled_tools = [${
|
|
650
|
+
`enabled_tools = [${toolNames.map((tool) => JSON.stringify(tool)).join(", ")}]`,
|
|
551
651
|
...(envEntries ? [`env = { ${envEntries} }`] : []),
|
|
552
652
|
"",
|
|
553
653
|
];
|
|
554
|
-
for (const tool of
|
|
654
|
+
for (const tool of toolNames) {
|
|
555
655
|
lines.push(`[mcp_servers.${MANAGED_AGENT_MCP_SERVER}.tools.${JSON.stringify(tool)}]`, 'approval_mode = "approve"', "");
|
|
556
656
|
}
|
|
557
657
|
return lines.join("\n");
|
|
558
658
|
}
|
|
559
659
|
|
|
560
|
-
|
|
660
|
+
function boundNativeAgentInvocation(tenantId, agent, invocation, {
|
|
661
|
+
policyFingerprint = nativeAgentPolicyFingerprint(agent),
|
|
662
|
+
recoveryOnly = false,
|
|
663
|
+
} = {}) {
|
|
664
|
+
if (!invocation) {
|
|
665
|
+
return impelNativeAgentMcpInvocation({
|
|
666
|
+
tenantId,
|
|
667
|
+
agentId: agent.agentId,
|
|
668
|
+
scopeParam: agent.scopeParam,
|
|
669
|
+
policyFingerprint,
|
|
670
|
+
recoveryOnly,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
const mcpIndex = invocation.args?.lastIndexOf("mcp") ?? -1;
|
|
674
|
+
if (mcpIndex < 0) throw new Error("native-agent MCP invocation has no mcp command");
|
|
675
|
+
return {
|
|
676
|
+
...invocation,
|
|
677
|
+
args: [
|
|
678
|
+
...invocation.args.slice(0, mcpIndex + 1),
|
|
679
|
+
"--target", IMPEL_NATIVE_AGENT_MCP_TARGET,
|
|
680
|
+
"--tenant", tenantId,
|
|
681
|
+
"--agent-id", agent.agentId,
|
|
682
|
+
"--scope-param", agent.scopeParam,
|
|
683
|
+
"--policy-fingerprint", policyFingerprint,
|
|
684
|
+
...(recoveryOnly ? ["--recovery-only"] : []),
|
|
685
|
+
],
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
export function renderManagedAgents(client, tenantId, agents, invocation = null) {
|
|
561
690
|
if (client !== "claude" && client !== "codex") throw new Error(`unknown agent client ${client}`);
|
|
562
691
|
const normalizedTenant = normalizeTenantId(tenantId);
|
|
563
692
|
const fileStems = generatedAgentFileStems(normalizedTenant, agents);
|
|
@@ -571,10 +700,73 @@ export function renderManagedAgents(client, tenantId, agents, invocation = impel
|
|
|
571
700
|
// builtin and remove it from the @ mention picker. Claude names are already
|
|
572
701
|
// collision-safe, filesystem-safe identifiers, so use them as filenames.
|
|
573
702
|
const fileStem = client === "claude" ? name : fileStems[index];
|
|
703
|
+
const boundInvocation = boundNativeAgentInvocation(normalizedTenant, agent, invocation);
|
|
574
704
|
const contents = client === "claude"
|
|
575
|
-
? renderClaudeAgent({ tenantId: normalizedTenant, agent, name, invocation })
|
|
576
|
-
: renderCodexAgent({ tenantId: normalizedTenant, agent, name, invocation });
|
|
577
|
-
return {
|
|
705
|
+
? renderClaudeAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation })
|
|
706
|
+
: renderCodexAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation });
|
|
707
|
+
return {
|
|
708
|
+
agentId: agent.agentId,
|
|
709
|
+
scopeParam: agent.scopeParam,
|
|
710
|
+
policyFingerprint: nativeAgentPolicyFingerprint(agent),
|
|
711
|
+
retired: false,
|
|
712
|
+
name,
|
|
713
|
+
fileName: `${fileStem}${extension}`,
|
|
714
|
+
contents,
|
|
715
|
+
};
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function renderRetiredManagedAgents(client, tenantId, bindings, active, invocation = null) {
|
|
720
|
+
const usedNames = new Set(active.map(({ name }) => name));
|
|
721
|
+
const usedFiles = new Set(active.map(({ fileName }) => fileName));
|
|
722
|
+
return bindings.map((binding) => {
|
|
723
|
+
const hash = binding.policyFingerprint.slice(0, 8);
|
|
724
|
+
const safeAgent = binding.agentId.toLowerCase().replace(/[^a-z0-9-]+/gu, "-").replace(/^-+|-+$/gu, "") || "agent";
|
|
725
|
+
const baseName = client === "claude"
|
|
726
|
+
? `impel-recover-${safeAgent.slice(0, 35)}-${hash}`
|
|
727
|
+
: `Recover ${binding.agentId} (${hash})`;
|
|
728
|
+
const baseFileStem = client === "claude" ? baseName : `impel-${tenantId}-recovery-${hash}`;
|
|
729
|
+
const extension = client === "claude" ? ".md" : ".toml";
|
|
730
|
+
let name = baseName;
|
|
731
|
+
let fileName = `${baseFileStem}${extension}`;
|
|
732
|
+
for (let collision = 1; usedNames.has(name) || usedFiles.has(fileName); collision += 1) {
|
|
733
|
+
const suffix = `retired-${hash}-${collision}`;
|
|
734
|
+
name = client === "claude"
|
|
735
|
+
? `${baseName.slice(0, Math.max(1, 63 - suffix.length - 1))}-${suffix}`
|
|
736
|
+
: `${baseName} [${suffix}]`;
|
|
737
|
+
fileName = client === "claude"
|
|
738
|
+
? `${name}${extension}`
|
|
739
|
+
: `${baseFileStem}-${suffix}${extension}`;
|
|
740
|
+
}
|
|
741
|
+
usedNames.add(name);
|
|
742
|
+
usedFiles.add(fileName);
|
|
743
|
+
const agent = {
|
|
744
|
+
agentId: binding.agentId,
|
|
745
|
+
title: `Retired ${binding.agentId}`,
|
|
746
|
+
description: "Recovery-only access to pending durable runs.",
|
|
747
|
+
scopeParam: binding.scopeParam,
|
|
748
|
+
provider: "recovery",
|
|
749
|
+
capabilities: [],
|
|
750
|
+
exclusions: ["starting new runs"],
|
|
751
|
+
requiredContext: [],
|
|
752
|
+
sideEffects: "read-only",
|
|
753
|
+
};
|
|
754
|
+
const boundInvocation = boundNativeAgentInvocation(tenantId, agent, invocation, {
|
|
755
|
+
policyFingerprint: binding.policyFingerprint,
|
|
756
|
+
recoveryOnly: true,
|
|
757
|
+
});
|
|
758
|
+
const contents = client === "claude"
|
|
759
|
+
? renderClaudeAgent({ tenantId, agent, name, invocation: boundInvocation, recoveryOnly: true })
|
|
760
|
+
: renderCodexAgent({ tenantId, agent, name, invocation: boundInvocation, recoveryOnly: true });
|
|
761
|
+
return {
|
|
762
|
+
agentId: binding.agentId,
|
|
763
|
+
scopeParam: binding.scopeParam,
|
|
764
|
+
policyFingerprint: binding.policyFingerprint,
|
|
765
|
+
retired: true,
|
|
766
|
+
name,
|
|
767
|
+
fileName,
|
|
768
|
+
contents,
|
|
769
|
+
};
|
|
578
770
|
});
|
|
579
771
|
}
|
|
580
772
|
|
|
@@ -615,7 +807,15 @@ function profileIsFresh(profile, tenantId, now, ttlMs) {
|
|
|
615
807
|
);
|
|
616
808
|
}
|
|
617
809
|
|
|
618
|
-
export function syncAgentProfile({
|
|
810
|
+
export function syncAgentProfile({
|
|
811
|
+
client,
|
|
812
|
+
root,
|
|
813
|
+
label,
|
|
814
|
+
tenantId,
|
|
815
|
+
agents,
|
|
816
|
+
now = Date.now(),
|
|
817
|
+
nativeAgentRunsRoot = path.join(CONFIG_DIR, "native-agent-runs"),
|
|
818
|
+
}) {
|
|
619
819
|
const agentsDir = path.join(root, "agents");
|
|
620
820
|
for (const candidate of [root, agentsDir]) {
|
|
621
821
|
if (fs.existsSync(candidate) && fs.lstatSync(candidate).isSymbolicLink()) {
|
|
@@ -626,9 +826,22 @@ export function syncAgentProfile({ client, root, label, tenantId, agents, now =
|
|
|
626
826
|
privateDirectory(managedDir);
|
|
627
827
|
const manifestPath = path.join(managedDir, MANAGED_AGENT_MANIFEST);
|
|
628
828
|
const prior = readManifest(manifestPath);
|
|
629
|
-
const
|
|
829
|
+
const active = renderManagedAgents(client, tenantId, agents);
|
|
830
|
+
const activeBindings = new Set(active.map((agent) =>
|
|
831
|
+
`${agent.agentId}\0${agent.scopeParam}\0${agent.policyFingerprint}`
|
|
832
|
+
));
|
|
833
|
+
const retiredBindings = pendingNativeAgentBindings(tenantId, { runsRoot: nativeAgentRunsRoot })
|
|
834
|
+
.filter((binding) => !activeBindings.has(
|
|
835
|
+
`${binding.agentId}\0${binding.scopeParam}\0${binding.policyFingerprint}`,
|
|
836
|
+
));
|
|
837
|
+
const retired = renderRetiredManagedAgents(client, tenantId, retiredBindings, active);
|
|
838
|
+
const rendered = [...active, ...retired];
|
|
839
|
+
if (new Set(rendered.map(({ name }) => name)).size !== rendered.length
|
|
840
|
+
|| new Set(rendered.map(({ fileName }) => fileName)).size !== rendered.length) {
|
|
841
|
+
throw new Error("generated native-agent destinations are not unique");
|
|
842
|
+
}
|
|
630
843
|
const priorFiles = new Set(prior?.files || []);
|
|
631
|
-
const priorUsesDiscoveryRoot = [2, 3, MANAGED_AGENT_MANIFEST_VERSION].includes(prior?.version);
|
|
844
|
+
const priorUsesDiscoveryRoot = [2, 3, 4, 5, MANAGED_AGENT_MANIFEST_VERSION].includes(prior?.version);
|
|
632
845
|
|
|
633
846
|
// Native clients discover standalone definitions directly under `agents/`.
|
|
634
847
|
// Preflight every destination before writing so an unmanaged file with the
|
|
@@ -675,13 +888,23 @@ export function syncAgentProfile({ client, root, label, tenantId, agents, now =
|
|
|
675
888
|
client,
|
|
676
889
|
syncedAt: new Date(now).toISOString(),
|
|
677
890
|
files: [...currentFiles].sort(),
|
|
678
|
-
agents: rendered.map((
|
|
679
|
-
agentId,
|
|
680
|
-
scopeParam:
|
|
681
|
-
|
|
891
|
+
agents: rendered.map((agent) => ({
|
|
892
|
+
agentId: agent.agentId,
|
|
893
|
+
scopeParam: agent.scopeParam,
|
|
894
|
+
policyFingerprint: agent.policyFingerprint,
|
|
895
|
+
retired: agent.retired,
|
|
896
|
+
name: agent.name,
|
|
682
897
|
})),
|
|
683
898
|
}, null, 2)}\n`);
|
|
684
|
-
return {
|
|
899
|
+
return {
|
|
900
|
+
client,
|
|
901
|
+
root,
|
|
902
|
+
label,
|
|
903
|
+
synced: true,
|
|
904
|
+
count: active.length,
|
|
905
|
+
recoveryCount: retired.length,
|
|
906
|
+
files: [...currentFiles],
|
|
907
|
+
};
|
|
685
908
|
}
|
|
686
909
|
|
|
687
910
|
export async function syncAgentProfiles({
|
|
@@ -694,6 +917,7 @@ export async function syncAgentProfiles({
|
|
|
694
917
|
now = Date.now(),
|
|
695
918
|
fetchCatalog = fetchNativeAgentCatalog,
|
|
696
919
|
logger = console,
|
|
920
|
+
nativeAgentRunsRoot = path.join(CONFIG_DIR, "native-agent-runs"),
|
|
697
921
|
}) {
|
|
698
922
|
if (process.env.IMPEL_SKIP_AGENT_SYNC === "1" || process.env.IMPEL_SKIP_AGENT_SYNC === "true") {
|
|
699
923
|
return profiles.map((profile) => ({ ...profile, skipped: true, reason: "disabled" }));
|
|
@@ -722,6 +946,7 @@ export async function syncAgentProfiles({
|
|
|
722
946
|
tenantId: normalizedTenant,
|
|
723
947
|
agents: catalog.agents,
|
|
724
948
|
now,
|
|
949
|
+
nativeAgentRunsRoot,
|
|
725
950
|
});
|
|
726
951
|
logger.log(`Agents: ${profile.label || profile.client} up to date (${result.count} explicit tenant agent${result.count === 1 ? "" : "s"}).`);
|
|
727
952
|
results.push(result);
|