@hyperdrive.bot/paseo-server 0.3.40 → 0.3.42
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/dist/server/server/agent/agent-manager.d.ts +15 -0
- package/dist/server/server/agent/agent-manager.js +157 -25
- package/dist/server/server/agent/agent-projections.js +3 -0
- package/dist/server/server/agent/agent-sdk-types.d.ts +23 -0
- package/dist/server/server/agent/agent-storage.d.ts +2 -1
- package/dist/server/server/agent/agent-storage.js +4 -0
- package/dist/server/server/agent/mcp-shared.js +5 -2
- package/dist/server/server/agent/providers/claude/agent.d.ts +34 -0
- package/dist/server/server/agent/providers/claude/agent.js +74 -0
- package/dist/server/server/agent/providers/claude/background-task-tracker.d.ts +38 -1
- package/dist/server/server/agent/providers/claude/background-task-tracker.js +114 -9
- package/dist/server/server/agent/providers/claude/background-work-kinds.d.ts +95 -0
- package/dist/server/server/agent/providers/claude/background-work-kinds.js +73 -0
- package/dist/server/server/agent/providers/claude/pty-session-launcher.d.ts +7 -0
- package/dist/server/server/agent/providers/claude/pty-session-launcher.js +9 -0
- package/dist/server/server/agent/providers/claude/tool-allowlist-guard.d.ts +41 -0
- package/dist/server/server/agent/providers/claude/tool-allowlist-guard.js +93 -0
- package/dist/server/server/agent/providers/claude/tool-allowlist.d.ts +68 -0
- package/dist/server/server/agent/providers/claude/tool-allowlist.js +133 -0
- package/dist/server/server/agent/providers/claude/transport/pty.d.ts +17 -0
- package/dist/server/server/agent/providers/claude/transport/pty.js +51 -1
- package/dist/server/server/agent/providers/claude/transport/tmux.d.ts +74 -0
- package/dist/server/server/agent/providers/claude/transport/tmux.js +157 -0
- package/dist/server/server/agent/providers/claude/transport/types.d.ts +6 -0
- package/dist/server/server/agent/providers/opencode-agent.d.ts +7 -0
- package/dist/server/server/agent/providers/opencode-agent.js +51 -1
- package/dist/server/server/agent/tools/paseo-tools.d.ts +19 -0
- package/dist/server/server/agent/tools/paseo-tools.js +213 -38
- package/dist/server/server/agent/tools/read-only-surface.d.ts +1 -0
- package/dist/server/server/agent/tools/read-only-surface.js +1 -0
- package/dist/server/server/persistence-hooks.js +2 -0
- package/dist/server/server/workspace-directory.js +32 -14
- package/dist/server/web-ui/_expo/static/js/web/{index-73ebfe5c6b82437cad59d50a40d2c8ef.js → index-9c3bcdc334cf1c08001b6bea510e0292.js} +17 -17
- package/dist/server/web-ui/_expo/static/js/web/index-9c3bcdc334cf1c08001b6bea510e0292.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-9c3bcdc334cf1c08001b6bea510e0292.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-73ebfe5c6b82437cad59d50a40d2c8ef.js.map.br → index-9c3bcdc334cf1c08001b6bea510e0292.js.map.br} +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-73ebfe5c6b82437cad59d50a40d2c8ef.js.map.gz → index-9c3bcdc334cf1c08001b6bea510e0292.js.map.gz} +0 -0
- package/dist/server/web-ui/index.html +1 -1
- package/dist/server/web-ui/index.html.br +0 -0
- package/dist/server/web-ui/index.html.gz +0 -0
- package/package.json +6 -6
- package/dist/server/web-ui/_expo/static/js/web/index-73ebfe5c6b82437cad59d50a40d2c8ef.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-73ebfe5c6b82437cad59d50a40d2c8ef.js.gz +0 -0
|
@@ -142,6 +142,14 @@ interface ManagedAgentBase {
|
|
|
142
142
|
lastUsage?: AgentUsage;
|
|
143
143
|
lastError?: string;
|
|
144
144
|
attention: AttentionState;
|
|
145
|
+
/**
|
|
146
|
+
* How many times a "finished" attention event was suppressed on this agent
|
|
147
|
+
* because it still owned armed background work (see checkAndSetAttention).
|
|
148
|
+
* Diagnostic only — never persisted, never projected to clients. It exists so
|
|
149
|
+
* the deliberately-accepted gap (a prose question swallowed mid-monitor) can
|
|
150
|
+
* be MEASURED from real sessions instead of argued about.
|
|
151
|
+
*/
|
|
152
|
+
suppressedAttentionCount?: number;
|
|
145
153
|
foregroundTurnWaiters: Set<ForegroundTurnWaiter>;
|
|
146
154
|
finalizedForegroundTurnIds: Set<string>;
|
|
147
155
|
unsubscribeSession: (() => void) | null;
|
|
@@ -472,6 +480,13 @@ export declare class AgentManager {
|
|
|
472
480
|
private recordTimeline;
|
|
473
481
|
private emitState;
|
|
474
482
|
private syncFeaturesFromSession;
|
|
483
|
+
/**
|
|
484
|
+
* Live background work owned by this agent: monitors, crons, and backgrounded
|
|
485
|
+
* shells the provider is still tracking. Mirrors what `agent-projections`
|
|
486
|
+
* publishes as `activeBackgroundTaskCount`, read through the same optional
|
|
487
|
+
* hook so providers that do not implement it simply report zero.
|
|
488
|
+
*/
|
|
489
|
+
private getActiveBackgroundTaskCount;
|
|
475
490
|
private checkAndSetAttention;
|
|
476
491
|
private enqueueBackgroundPersist;
|
|
477
492
|
private enqueueDurableTimelineAppend;
|
|
@@ -59,6 +59,20 @@ function buildStoredAgentConfig(record) {
|
|
|
59
59
|
return stripInternalPaseoMcpServer(config);
|
|
60
60
|
}
|
|
61
61
|
export { AGENT_LIFECYCLE_STATUSES };
|
|
62
|
+
/**
|
|
63
|
+
* Reject a create call that asks for a tool allowlist the chosen provider cannot
|
|
64
|
+
* enforce. Silently dropping it would hand the caller a guarantee that does not
|
|
65
|
+
* exist, so the failure is loud and names the provider.
|
|
66
|
+
*/
|
|
67
|
+
function requireToolAllowlistSupport(client, config) {
|
|
68
|
+
if (!config.allowedTools || config.allowedTools.length === 0)
|
|
69
|
+
return;
|
|
70
|
+
if (client.supportsToolAllowlist === true)
|
|
71
|
+
return;
|
|
72
|
+
throw new Error(`Provider '${config.provider}' cannot enforce a tool allowlist (allowedTools). ` +
|
|
73
|
+
"Only the claude provider (and providers derived from it) supports runtime tool " +
|
|
74
|
+
"enforcement. Re-run without --allowed-tools, or use a claude-based provider.");
|
|
75
|
+
}
|
|
62
76
|
function resolveInitialAttention(input) {
|
|
63
77
|
if (input == null || !input.requiresAttention) {
|
|
64
78
|
return { requiresAttention: false };
|
|
@@ -214,7 +228,10 @@ export class AgentManager {
|
|
|
214
228
|
this.mcpAuthToken = options?.mcpAuthToken ?? null;
|
|
215
229
|
this.configurePaseoTools(options);
|
|
216
230
|
this.appendSystemPrompt = options.appendSystemPrompt ?? "";
|
|
217
|
-
this.logger = options.logger.child({
|
|
231
|
+
this.logger = options.logger.child({
|
|
232
|
+
module: "agent",
|
|
233
|
+
component: "agent-manager",
|
|
234
|
+
});
|
|
218
235
|
this.rescueTimeouts = {
|
|
219
236
|
reloadSessionCloseMs: options.rescueTimeouts?.reloadSessionCloseMs ?? RELOAD_SESSION_CLOSE_TIMEOUT_MS,
|
|
220
237
|
interruptSessionMs: options.rescueTimeouts?.interruptSessionMs ?? INTERRUPT_SESSION_TIMEOUT_MS,
|
|
@@ -453,7 +470,9 @@ export class AgentManager {
|
|
|
453
470
|
}
|
|
454
471
|
}
|
|
455
472
|
async listDraftCommands(config) {
|
|
456
|
-
const normalizedConfig = await this.normalizeConfig(config, {
|
|
473
|
+
const normalizedConfig = await this.normalizeConfig(config, {
|
|
474
|
+
resolveDefaultModel: false,
|
|
475
|
+
});
|
|
457
476
|
const client = this.requireClient(normalizedConfig.provider);
|
|
458
477
|
if (!normalizedConfig.model) {
|
|
459
478
|
return [];
|
|
@@ -482,7 +501,9 @@ export class AgentManager {
|
|
|
482
501
|
}
|
|
483
502
|
}
|
|
484
503
|
async listDraftFeatures(config) {
|
|
485
|
-
const normalizedConfig = await this.normalizeConfig(config, {
|
|
504
|
+
const normalizedConfig = await this.normalizeConfig(config, {
|
|
505
|
+
resolveDefaultModel: false,
|
|
506
|
+
});
|
|
486
507
|
const client = this.requireClient(normalizedConfig.provider);
|
|
487
508
|
if (!normalizedConfig.model) {
|
|
488
509
|
return [];
|
|
@@ -541,6 +562,7 @@ export class AgentManager {
|
|
|
541
562
|
const client = await this.requireAvailableClient({
|
|
542
563
|
provider: storedConfig.provider,
|
|
543
564
|
});
|
|
565
|
+
requireToolAllowlistSupport(client, storedConfig);
|
|
544
566
|
const launchContext = await this.buildLaunchContext(resolvedAgentId, client, options?.env);
|
|
545
567
|
const providerLaunchConfig = this.resolveProviderLaunchConfig(launchConfig, launchContext);
|
|
546
568
|
const createOptions = this.buildCreateSessionOptions(options);
|
|
@@ -585,7 +607,9 @@ export class AgentManager {
|
|
|
585
607
|
async importProviderSession(input) {
|
|
586
608
|
const resolvedAgentId = validateAgentId(this.idFactory(), "importProviderSession");
|
|
587
609
|
this.requireEnabledProvider(input.provider);
|
|
588
|
-
const client = await this.requireAvailableClient({
|
|
610
|
+
const client = await this.requireAvailableClient({
|
|
611
|
+
provider: input.provider,
|
|
612
|
+
});
|
|
589
613
|
if (!client.importSession) {
|
|
590
614
|
throw new Error(`Provider '${input.provider}' does not support importing sessions`);
|
|
591
615
|
}
|
|
@@ -893,7 +917,10 @@ export class AgentManager {
|
|
|
893
917
|
async markRecordArchived(record) {
|
|
894
918
|
const registry = this.requireRegistry();
|
|
895
919
|
const archivedAt = new Date().toISOString();
|
|
896
|
-
const archivedRecord = buildArchivedAgentRecord(record, {
|
|
920
|
+
const archivedRecord = buildArchivedAgentRecord(record, {
|
|
921
|
+
archivedAt,
|
|
922
|
+
updatedAt: archivedAt,
|
|
923
|
+
});
|
|
897
924
|
await registry.upsert(archivedRecord);
|
|
898
925
|
await this.archiveNativeSessionBestEffort(record.provider, record.persistence);
|
|
899
926
|
if (this.agents.has(record.id)) {
|
|
@@ -1009,7 +1036,10 @@ export class AgentManager {
|
|
|
1009
1036
|
throw new Error("Agent session does not support setting features");
|
|
1010
1037
|
}
|
|
1011
1038
|
await agent.session.setFeature(featureId, value);
|
|
1012
|
-
agent.config.featureValues = {
|
|
1039
|
+
agent.config.featureValues = {
|
|
1040
|
+
...agent.config.featureValues,
|
|
1041
|
+
[featureId]: value,
|
|
1042
|
+
};
|
|
1013
1043
|
this.touchUpdatedAt(agent);
|
|
1014
1044
|
this.emitState(agent);
|
|
1015
1045
|
}
|
|
@@ -1057,7 +1087,9 @@ export class AgentManager {
|
|
|
1057
1087
|
const priorColumn = touchesColumn && this.registry
|
|
1058
1088
|
? ((await this.registry.get(agentId))?.labels?.[KANBAN_LABEL_KEY] ?? null)
|
|
1059
1089
|
: null;
|
|
1060
|
-
const nextRecord = await this.writeStoredMetadata(agentId, {
|
|
1090
|
+
const nextRecord = await this.writeStoredMetadata(agentId, {
|
|
1091
|
+
labels: patch,
|
|
1092
|
+
});
|
|
1061
1093
|
if (touchesColumn) {
|
|
1062
1094
|
this.cardMoveLog?.record(agentId, priorColumn, nextRecord.labels?.[KANBAN_LABEL_KEY] ?? null);
|
|
1063
1095
|
}
|
|
@@ -1092,7 +1124,9 @@ export class AgentManager {
|
|
|
1092
1124
|
}
|
|
1093
1125
|
return { record, live: true, previousParentAgentId: null };
|
|
1094
1126
|
}
|
|
1095
|
-
const { record } = await this.writeLabels(agentId, {
|
|
1127
|
+
const { record } = await this.writeLabels(agentId, {
|
|
1128
|
+
[PARENT_AGENT_ID_LABEL]: null,
|
|
1129
|
+
});
|
|
1096
1130
|
if (!record) {
|
|
1097
1131
|
throw new Error(`Agent not found in storage after detach: ${agentId}`);
|
|
1098
1132
|
}
|
|
@@ -1106,7 +1140,9 @@ export class AgentManager {
|
|
|
1106
1140
|
if (!previousParentAgentId) {
|
|
1107
1141
|
return { record, live: false, previousParentAgentId: null };
|
|
1108
1142
|
}
|
|
1109
|
-
const result = await this.writeLabels(agentId, {
|
|
1143
|
+
const result = await this.writeLabels(agentId, {
|
|
1144
|
+
[PARENT_AGENT_ID_LABEL]: null,
|
|
1145
|
+
});
|
|
1110
1146
|
if (!result.record) {
|
|
1111
1147
|
throw new Error(`Agent not found in storage after detach: ${agentId}`);
|
|
1112
1148
|
}
|
|
@@ -1277,7 +1313,9 @@ export class AgentManager {
|
|
|
1277
1313
|
});
|
|
1278
1314
|
return;
|
|
1279
1315
|
}
|
|
1280
|
-
this.dispatchStream(agent.id, event, {
|
|
1316
|
+
this.dispatchStream(agent.id, event, {
|
|
1317
|
+
timestamp: new Date().toISOString(),
|
|
1318
|
+
});
|
|
1281
1319
|
};
|
|
1282
1320
|
void (async () => {
|
|
1283
1321
|
try {
|
|
@@ -1417,7 +1455,10 @@ export class AgentManager {
|
|
|
1417
1455
|
mutableAgent.lifecycle = nextLifecycle;
|
|
1418
1456
|
const persistenceHandle = mutableAgent.session.describePersistence() ??
|
|
1419
1457
|
(mutableAgent.runtimeInfo?.sessionId
|
|
1420
|
-
? {
|
|
1458
|
+
? {
|
|
1459
|
+
provider: mutableAgent.provider,
|
|
1460
|
+
sessionId: mutableAgent.runtimeInfo.sessionId,
|
|
1461
|
+
}
|
|
1421
1462
|
: null);
|
|
1422
1463
|
if (persistenceHandle) {
|
|
1423
1464
|
mutableAgent.persistence = attachPersistenceCwd(persistenceHandle, mutableAgent.cwd);
|
|
@@ -1574,7 +1615,9 @@ export class AgentManager {
|
|
|
1574
1615
|
const bufferedResolution = agent.bufferedPermissionResolutions.get(requestId);
|
|
1575
1616
|
if (bufferedResolution) {
|
|
1576
1617
|
agent.bufferedPermissionResolutions.delete(requestId);
|
|
1577
|
-
this.dispatchStream(agent.id, bufferedResolution, {
|
|
1618
|
+
this.dispatchStream(agent.id, bufferedResolution, {
|
|
1619
|
+
timestamp: new Date().toISOString(),
|
|
1620
|
+
});
|
|
1578
1621
|
}
|
|
1579
1622
|
return result;
|
|
1580
1623
|
}
|
|
@@ -1611,7 +1654,11 @@ export class AgentManager {
|
|
|
1611
1654
|
continue;
|
|
1612
1655
|
}
|
|
1613
1656
|
for (const task of agent.session.listBackgroundTasks()) {
|
|
1614
|
-
rows.push({
|
|
1657
|
+
rows.push({
|
|
1658
|
+
agentId: agent.id,
|
|
1659
|
+
agentTitle: agent.config.title ?? null,
|
|
1660
|
+
task,
|
|
1661
|
+
});
|
|
1615
1662
|
}
|
|
1616
1663
|
}
|
|
1617
1664
|
return rows;
|
|
@@ -1752,7 +1799,10 @@ export class AgentManager {
|
|
|
1752
1799
|
this.logger.info({ agentId, provider: agent.provider, messageId, mode }, "agent.rewind.start");
|
|
1753
1800
|
await invokeRewindCapability(agent.session, { messageId, mode });
|
|
1754
1801
|
if (mode !== "files") {
|
|
1755
|
-
await this.hydrateTimelineFromProvider(agentId, {
|
|
1802
|
+
await this.hydrateTimelineFromProvider(agentId, {
|
|
1803
|
+
force: true,
|
|
1804
|
+
broadcast: true,
|
|
1805
|
+
});
|
|
1756
1806
|
}
|
|
1757
1807
|
await this.refreshRuntimeInfo(agent);
|
|
1758
1808
|
await this.persistSnapshot(agent);
|
|
@@ -1998,14 +2048,18 @@ export class AgentManager {
|
|
|
1998
2048
|
this.agents.set(resolvedAgentId, managed);
|
|
1999
2049
|
// Initialize previousStatus to track transitions
|
|
2000
2050
|
this.previousStatuses.set(resolvedAgentId, managed.lifecycle);
|
|
2001
|
-
await this.refreshRuntimeInfo(managed, {
|
|
2051
|
+
await this.refreshRuntimeInfo(managed, {
|
|
2052
|
+
emit: !options?.publishWhenReady,
|
|
2053
|
+
});
|
|
2002
2054
|
await this.persistSnapshot(managed, {
|
|
2003
2055
|
title: initialPersistedTitle,
|
|
2004
2056
|
});
|
|
2005
2057
|
if (!options?.publishWhenReady) {
|
|
2006
2058
|
this.emitState(managed, { persist: false });
|
|
2007
2059
|
}
|
|
2008
|
-
await this.refreshSessionState(managed, {
|
|
2060
|
+
await this.refreshSessionState(managed, {
|
|
2061
|
+
emit: !options?.publishWhenReady,
|
|
2062
|
+
});
|
|
2009
2063
|
managed.lifecycle = "idle";
|
|
2010
2064
|
await this.persistSnapshot(managed);
|
|
2011
2065
|
this.emitState(managed, { persist: false });
|
|
@@ -2265,7 +2319,9 @@ export class AgentManager {
|
|
|
2265
2319
|
this.agentStreamCoalescer.flushAndDiscard(agent.id);
|
|
2266
2320
|
await this.deleteCommittedTimeline(agent.id);
|
|
2267
2321
|
this.timelineStore.delete(agent.id);
|
|
2268
|
-
this.timelineStore.initialize(agent.id, {
|
|
2322
|
+
this.timelineStore.initialize(agent.id, {
|
|
2323
|
+
timestamp: new Date().toISOString(),
|
|
2324
|
+
});
|
|
2269
2325
|
agent.historyPrimed = true;
|
|
2270
2326
|
for (const event of historyEvents) {
|
|
2271
2327
|
const row = this.recordTimeline(agent.id, event.item, event.timestamp ? { timestamp: event.timestamp } : undefined);
|
|
@@ -2333,7 +2389,10 @@ export class AgentManager {
|
|
|
2333
2389
|
}
|
|
2334
2390
|
this.agentStreamCoalescer.flushFor(agent.id);
|
|
2335
2391
|
}
|
|
2336
|
-
const flags = {
|
|
2392
|
+
const flags = {
|
|
2393
|
+
shouldDispatchEvent: true,
|
|
2394
|
+
shouldNotifyWaiters: true,
|
|
2395
|
+
};
|
|
2337
2396
|
const dispatchPromise = this.dispatchStreamEventByType({
|
|
2338
2397
|
agent,
|
|
2339
2398
|
event,
|
|
@@ -2349,7 +2408,9 @@ export class AgentManager {
|
|
|
2349
2408
|
this.finalizeForegroundTurn(agent, eventTurnId);
|
|
2350
2409
|
}
|
|
2351
2410
|
if (!options?.fromHistory && flags.shouldDispatchEvent) {
|
|
2352
|
-
this.dispatchStream(agent.id, event, {
|
|
2411
|
+
this.dispatchStream(agent.id, event, {
|
|
2412
|
+
timestamp: new Date().toISOString(),
|
|
2413
|
+
});
|
|
2353
2414
|
}
|
|
2354
2415
|
this.traceHandleStreamEventEnd(agent, event, eventTurnId, flags);
|
|
2355
2416
|
return flags.shouldNotifyWaiters;
|
|
@@ -2402,7 +2463,10 @@ export class AgentManager {
|
|
|
2402
2463
|
agent.currentModeId = event.currentModeId;
|
|
2403
2464
|
agent.availableModes = event.availableModes;
|
|
2404
2465
|
if (agent.runtimeInfo) {
|
|
2405
|
-
agent.runtimeInfo = {
|
|
2466
|
+
agent.runtimeInfo = {
|
|
2467
|
+
...agent.runtimeInfo,
|
|
2468
|
+
modeId: event.currentModeId,
|
|
2469
|
+
};
|
|
2406
2470
|
}
|
|
2407
2471
|
flags.shouldDispatchEvent = false;
|
|
2408
2472
|
this.emitState(agent);
|
|
@@ -2410,7 +2474,10 @@ export class AgentManager {
|
|
|
2410
2474
|
case "model_changed":
|
|
2411
2475
|
agent.runtimeInfo = event.runtimeInfo;
|
|
2412
2476
|
if (!agent.persistence && event.runtimeInfo.sessionId) {
|
|
2413
|
-
agent.persistence = attachPersistenceCwd({
|
|
2477
|
+
agent.persistence = attachPersistenceCwd({
|
|
2478
|
+
provider: agent.provider,
|
|
2479
|
+
sessionId: event.runtimeInfo.sessionId,
|
|
2480
|
+
}, agent.cwd);
|
|
2414
2481
|
}
|
|
2415
2482
|
agent.currentModeId = event.runtimeInfo.modeId ?? agent.currentModeId;
|
|
2416
2483
|
flags.shouldDispatchEvent = false;
|
|
@@ -2427,9 +2494,20 @@ export class AgentManager {
|
|
|
2427
2494
|
this.emitState(agent);
|
|
2428
2495
|
return undefined;
|
|
2429
2496
|
case "timeline":
|
|
2430
|
-
return this.onStreamTimelineEvent({
|
|
2497
|
+
return this.onStreamTimelineEvent({
|
|
2498
|
+
agent,
|
|
2499
|
+
event,
|
|
2500
|
+
options,
|
|
2501
|
+
isForegroundEvent,
|
|
2502
|
+
flags,
|
|
2503
|
+
});
|
|
2431
2504
|
case "turn_completed":
|
|
2432
|
-
this.onStreamTurnCompleted({
|
|
2505
|
+
this.onStreamTurnCompleted({
|
|
2506
|
+
agent,
|
|
2507
|
+
event,
|
|
2508
|
+
eventTurnId,
|
|
2509
|
+
isForegroundEvent,
|
|
2510
|
+
});
|
|
2433
2511
|
return undefined;
|
|
2434
2512
|
case "turn_failed":
|
|
2435
2513
|
return this.onStreamTurnFailed({
|
|
@@ -2440,7 +2518,13 @@ export class AgentManager {
|
|
|
2440
2518
|
options,
|
|
2441
2519
|
});
|
|
2442
2520
|
case "turn_canceled":
|
|
2443
|
-
this.onStreamTurnCanceled({
|
|
2521
|
+
this.onStreamTurnCanceled({
|
|
2522
|
+
agent,
|
|
2523
|
+
event,
|
|
2524
|
+
eventTurnId,
|
|
2525
|
+
isForegroundEvent,
|
|
2526
|
+
options,
|
|
2527
|
+
});
|
|
2444
2528
|
return undefined;
|
|
2445
2529
|
case "turn_started":
|
|
2446
2530
|
this.onStreamTurnStarted({ agent, eventTurnId, isForegroundEvent });
|
|
@@ -2689,6 +2773,18 @@ export class AgentManager {
|
|
|
2689
2773
|
agent.features = agent.session.features;
|
|
2690
2774
|
}
|
|
2691
2775
|
}
|
|
2776
|
+
/**
|
|
2777
|
+
* Live background work owned by this agent: monitors, crons, and backgrounded
|
|
2778
|
+
* shells the provider is still tracking. Mirrors what `agent-projections`
|
|
2779
|
+
* publishes as `activeBackgroundTaskCount`, read through the same optional
|
|
2780
|
+
* hook so providers that do not implement it simply report zero.
|
|
2781
|
+
*/
|
|
2782
|
+
getActiveBackgroundTaskCount(agent) {
|
|
2783
|
+
if (!("session" in agent)) {
|
|
2784
|
+
return 0;
|
|
2785
|
+
}
|
|
2786
|
+
return agent.session?.getActiveBackgroundTaskCount?.() ?? 0;
|
|
2787
|
+
}
|
|
2692
2788
|
checkAndSetAttention(agent) {
|
|
2693
2789
|
const previousStatus = this.previousStatuses.get(agent.id);
|
|
2694
2790
|
const currentStatus = agent.lifecycle;
|
|
@@ -2704,6 +2800,40 @@ export class AgentManager {
|
|
|
2704
2800
|
}
|
|
2705
2801
|
// Check if agent transitioned from running to idle (finished)
|
|
2706
2802
|
if (previousStatus === "running" && currentStatus === "idle") {
|
|
2803
|
+
// ...but "the lifecycle went running -> idle" is NOT the same fact as
|
|
2804
|
+
// "this agent is finished and wants you".
|
|
2805
|
+
//
|
|
2806
|
+
// A monitor tick, a cron firing, or a background shell reporting progress
|
|
2807
|
+
// all WAKE the session: status goes idle -> running, the agent prints a
|
|
2808
|
+
// progress line ("1/12 done, I'll let you know"), the turn ends, and the
|
|
2809
|
+
// lifecycle settles back to idle. Under the old rule that transition set
|
|
2810
|
+
// requiresAttention="finished" on EVERY tick, which lit the Unread chip,
|
|
2811
|
+
// the dock badge and a push notification for output nobody needs to act
|
|
2812
|
+
// on. With several sessions in parallel that made Unread — the one chip
|
|
2813
|
+
// you actually watch — useless.
|
|
2814
|
+
//
|
|
2815
|
+
// So: if the agent still owns armed background work, the turn ending is a
|
|
2816
|
+
// progress beat, not a handoff. Stay quiet and let the bucket derivation
|
|
2817
|
+
// surface it as "pending" (alive, will come back on its own) instead.
|
|
2818
|
+
//
|
|
2819
|
+
// KNOWN GAP, deliberately named rather than silently accepted: a turn that
|
|
2820
|
+
// ends with the agent asking a real question in PROSE (not a permission
|
|
2821
|
+
// request, which still routes through needs_input and is unaffected) is
|
|
2822
|
+
// also suppressed while background work is armed. Prose questions are not
|
|
2823
|
+
// machine-detectable here. The counter below exists so that gap is
|
|
2824
|
+
// MEASURED rather than assumed — feed it to the Jarvis judge in shadow
|
|
2825
|
+
// mode (PASEO_JARVIS_SHADOW=1) to size the real distribution before
|
|
2826
|
+
// deciding whether it needs rescuing.
|
|
2827
|
+
const activeBackgroundTaskCount = this.getActiveBackgroundTaskCount(agent);
|
|
2828
|
+
if (activeBackgroundTaskCount > 0) {
|
|
2829
|
+
agent.suppressedAttentionCount = (agent.suppressedAttentionCount ?? 0) + 1;
|
|
2830
|
+
this.logger.debug({
|
|
2831
|
+
agentId: agent.id,
|
|
2832
|
+
activeBackgroundTaskCount,
|
|
2833
|
+
suppressedAttentionCount: agent.suppressedAttentionCount,
|
|
2834
|
+
}, "Suppressed finished-attention: agent still owns armed background work");
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2707
2837
|
agent.attention = {
|
|
2708
2838
|
requiresAttention: true,
|
|
2709
2839
|
attentionReason: "finished",
|
|
@@ -2923,7 +3053,9 @@ export class AgentManager {
|
|
|
2923
3053
|
if (this.paseoToolsEnabled &&
|
|
2924
3054
|
client.capabilities.supportsNativePaseoTools &&
|
|
2925
3055
|
this.paseoToolCatalogFactory) {
|
|
2926
|
-
context.paseoTools = await this.paseoToolCatalogFactory({
|
|
3056
|
+
context.paseoTools = await this.paseoToolCatalogFactory({
|
|
3057
|
+
callerAgentId: agentId,
|
|
3058
|
+
});
|
|
2927
3059
|
}
|
|
2928
3060
|
return context;
|
|
2929
3061
|
}
|
|
@@ -263,6 +263,9 @@ function buildSerializableConfig(config) {
|
|
|
263
263
|
if (config.mcpServers) {
|
|
264
264
|
serializable.mcpServers = config.mcpServers;
|
|
265
265
|
}
|
|
266
|
+
if (config.allowedTools?.length) {
|
|
267
|
+
serializable.allowedTools = [...config.allowedTools];
|
|
268
|
+
}
|
|
266
269
|
return Object.keys(serializable).length ? serializable : null;
|
|
267
270
|
}
|
|
268
271
|
function sanitizePendingPermissions(pending) {
|
|
@@ -521,6 +521,17 @@ export interface AgentSessionConfig {
|
|
|
521
521
|
claude?: Partial<ClaudeAgentOptions>;
|
|
522
522
|
};
|
|
523
523
|
mcpServers?: Record<string, McpServerConfig>;
|
|
524
|
+
/**
|
|
525
|
+
* Per-run tool allowlist in Claude Code permission-rule syntax, e.g.
|
|
526
|
+
* ["Read", "Bash(git:*)", "mcp__playwright__browser_navigate"].
|
|
527
|
+
*
|
|
528
|
+
* When present, a tool absent from the list is blocked BEFORE it executes and
|
|
529
|
+
* the model is told why. Undefined or empty means no allowlist. Enforced only
|
|
530
|
+
* by clients that set `AgentClient.supportsToolAllowlist`; a create call that
|
|
531
|
+
* asks any other provider for an allowlist is rejected rather than run
|
|
532
|
+
* unenforced.
|
|
533
|
+
*/
|
|
534
|
+
allowedTools?: string[];
|
|
524
535
|
/**
|
|
525
536
|
* Internal agents are hidden from listings and don't trigger notifications.
|
|
526
537
|
* They are used for ephemeral system tasks like commit/PR generation.
|
|
@@ -673,6 +684,18 @@ export interface ProviderCatalog {
|
|
|
673
684
|
export interface AgentClient {
|
|
674
685
|
readonly provider: AgentProvider;
|
|
675
686
|
readonly capabilities: AgentCapabilityFlags;
|
|
687
|
+
/**
|
|
688
|
+
* True when this client can ENFORCE `AgentSessionConfig.allowedTools` at
|
|
689
|
+
* runtime (block a tool outside the list before it executes), not merely
|
|
690
|
+
* accept the field.
|
|
691
|
+
*
|
|
692
|
+
* Absent/false means the create call is rejected when an allowlist is set. A
|
|
693
|
+
* tool allowlist that is quietly dropped is worse than no allowlist: the
|
|
694
|
+
* caller believes a restriction is in force that is not, so this fails loudly
|
|
695
|
+
* instead. Derived providers (`pool` extends `claude`, and so on) reuse the
|
|
696
|
+
* base client class and therefore inherit the correct answer.
|
|
697
|
+
*/
|
|
698
|
+
readonly supportsToolAllowlist?: boolean;
|
|
676
699
|
createSession(config: AgentSessionConfig, launchContext?: AgentLaunchContext, options?: AgentCreateSessionOptions): Promise<AgentSession>;
|
|
677
700
|
resumeSession(handle: AgentPersistenceHandle, overrides?: Partial<AgentSessionConfig>, launchContext?: AgentLaunchContext): Promise<AgentSession>;
|
|
678
701
|
/**
|
|
@@ -62,6 +62,7 @@ declare const STORED_AGENT_SCHEMA: z.ZodObject<{
|
|
|
62
62
|
extra: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
63
63
|
systemPrompt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
64
64
|
mcpServers: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
65
|
+
allowedTools: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
65
66
|
}, z.core.$strip>>>;
|
|
66
67
|
runtimeInfo: z.ZodOptional<z.ZodObject<{
|
|
67
68
|
provider: z.ZodString;
|
|
@@ -112,7 +113,7 @@ declare const STORED_AGENT_SCHEMA: z.ZodObject<{
|
|
|
112
113
|
internal: z.ZodOptional<z.ZodBoolean>;
|
|
113
114
|
archivedAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
114
115
|
}, z.core.$strip>;
|
|
115
|
-
export type SerializableAgentConfig = Pick<AgentSessionConfig, "modeId" | "model" | "thinkingOptionId" | "featureValues" | "extra" | "systemPrompt" | "mcpServers">;
|
|
116
|
+
export type SerializableAgentConfig = Pick<AgentSessionConfig, "modeId" | "model" | "thinkingOptionId" | "featureValues" | "extra" | "systemPrompt" | "mcpServers" | "allowedTools">;
|
|
116
117
|
export type StoredAgentRecord = z.infer<typeof STORED_AGENT_SCHEMA>;
|
|
117
118
|
export declare function parseStoredAgentRecord(value: unknown): StoredAgentRecord;
|
|
118
119
|
export declare class AgentStorage {
|
|
@@ -13,6 +13,10 @@ const SERIALIZABLE_CONFIG_SCHEMA = z
|
|
|
13
13
|
extra: z.record(z.string(), z.any()).nullable().optional(),
|
|
14
14
|
systemPrompt: z.string().nullable().optional(),
|
|
15
15
|
mcpServers: z.record(z.string(), z.any()).nullable().optional(),
|
|
16
|
+
// Persisted so a daemon restart or session resume rebuilds the SAME
|
|
17
|
+
// restriction. An allowlist that silently evaporates on resume is worse than
|
|
18
|
+
// none: the operator still believes the run is fenced.
|
|
19
|
+
allowedTools: z.array(z.string()).nullable().optional(),
|
|
16
20
|
})
|
|
17
21
|
.nullable()
|
|
18
22
|
.optional();
|
|
@@ -168,7 +168,7 @@ export function parseDurationString(input) {
|
|
|
168
168
|
}
|
|
169
169
|
let totalMs = 0;
|
|
170
170
|
let hasMatch = false;
|
|
171
|
-
const regex = /(\d+)([
|
|
171
|
+
const regex = /(\d+)([smhd])/g;
|
|
172
172
|
let match;
|
|
173
173
|
while ((match = regex.exec(trimmed)) !== null) {
|
|
174
174
|
hasMatch = true;
|
|
@@ -183,10 +183,13 @@ export function parseDurationString(input) {
|
|
|
183
183
|
case "h":
|
|
184
184
|
totalMs += value * 60 * 60 * 1000;
|
|
185
185
|
break;
|
|
186
|
+
case "d":
|
|
187
|
+
totalMs += value * 24 * 60 * 60 * 1000;
|
|
188
|
+
break;
|
|
186
189
|
}
|
|
187
190
|
}
|
|
188
191
|
if (!hasMatch) {
|
|
189
|
-
throw new Error(`Invalid duration format: ${input}. Use formats like: 5m, 30s, 1h, 2h30m`);
|
|
192
|
+
throw new Error(`Invalid duration format: ${input}. Use formats like: 5m, 30s, 1h, 2h30m, 1d`);
|
|
190
193
|
}
|
|
191
194
|
return totalMs;
|
|
192
195
|
}
|
|
@@ -50,6 +50,7 @@ export declare function readEventIdentifiers(message: SDKMessage): EventIdentifi
|
|
|
50
50
|
export declare class ClaudeAgentClient implements AgentClient {
|
|
51
51
|
readonly provider: "claude";
|
|
52
52
|
readonly capabilities: AgentCapabilityFlags;
|
|
53
|
+
readonly supportsToolAllowlist = true;
|
|
53
54
|
private readonly defaults?;
|
|
54
55
|
private readonly logger;
|
|
55
56
|
private readonly runtimeSettings?;
|
|
@@ -133,6 +134,13 @@ export declare class ClaudeAgentSession implements AgentSession {
|
|
|
133
134
|
private readonly emittedUserMessageIds;
|
|
134
135
|
private readonly rewindTurnAnchors;
|
|
135
136
|
private pendingFreshSessionId;
|
|
137
|
+
/**
|
|
138
|
+
* Parsed per-run tool allowlist. Empty array = no allowlist configured, which
|
|
139
|
+
* is the historical behaviour (everything the permission mode permits runs).
|
|
140
|
+
*/
|
|
141
|
+
private readonly toolAllowRules;
|
|
142
|
+
/** PTY-transport guard files for the current session; removed on teardown. */
|
|
143
|
+
private toolAllowGuard;
|
|
136
144
|
private cumulativeImageBase64Bytes;
|
|
137
145
|
private cumulativeImageWarnEmitted;
|
|
138
146
|
private recentStderr;
|
|
@@ -240,6 +248,32 @@ export declare class ClaudeAgentSession implements AgentSession {
|
|
|
240
248
|
private buildAppendedSystemPrompt;
|
|
241
249
|
private buildSdkEnv;
|
|
242
250
|
private buildOptions;
|
|
251
|
+
/**
|
|
252
|
+
* Install the per-run tool allowlist as a `PreToolUse` hook on the SDK options.
|
|
253
|
+
*
|
|
254
|
+
* Deliberately NOT `options.allowedTools`: that field is a permission ALLOW rule
|
|
255
|
+
* (pre-approve, do not prompt), and this session also sets
|
|
256
|
+
* allowDangerouslySkipPermissions, under which permission rules are moot. A
|
|
257
|
+
* PreToolUse hook is the only gate that still fires; the SDK says so outright
|
|
258
|
+
* ("PreToolUse hook denies bypass canUseTool"). Applied last so an
|
|
259
|
+
* `extra.claude.hooks` override cannot displace it.
|
|
260
|
+
*/
|
|
261
|
+
private applyToolAllowlistHook;
|
|
262
|
+
/**
|
|
263
|
+
* PTY-transport half of allowlist enforcement: (re)generate the hook script and
|
|
264
|
+
* the `--settings` file that registers it.
|
|
265
|
+
*
|
|
266
|
+
* Returns a spreadable fragment (`{}` when no allowlist is configured) so the
|
|
267
|
+
* caller stays branch-free. Regenerated per spawn because a session can restart
|
|
268
|
+
* (mode swap, model swap, rewind) and the artifacts are keyed to the session id.
|
|
269
|
+
*/
|
|
270
|
+
private refreshToolAllowGuard;
|
|
271
|
+
/**
|
|
272
|
+
* SDK-transport half of allowlist enforcement. The PTY transport enforces the
|
|
273
|
+
* same rules through a generated hook script (see tool-allowlist-guard.ts);
|
|
274
|
+
* both call `isToolAllowed`, so the two transports cannot diverge.
|
|
275
|
+
*/
|
|
276
|
+
private enforceToolAllowlistHook;
|
|
243
277
|
private buildSettingsOptions;
|
|
244
278
|
private resolveFastModeSetting;
|
|
245
279
|
private normalizeMcpServers;
|