@stratusagent/gateway 0.6.0 → 0.8.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/dist/index.d.ts +74 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +214 -8
- package/dist/index.js.map +1 -1
- package/dist/schedules.d.ts +137 -0
- package/dist/schedules.d.ts.map +1 -0
- package/dist/schedules.js +427 -0
- package/dist/schedules.js.map +1 -0
- package/package.json +8 -7
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { EventBus, type AgentDefinition, type ApprovalAnswer, type ApprovalPolicy, type JsonObject, type Session, type SessionStatus, type SessionStore, type ToolRisk } from '@stratusagent/core';
|
|
2
|
-
import { type ParsedSoul } from '@stratusagent/agents';
|
|
2
|
+
import { type ParsedSoul, type ScheduleRecord } from '@stratusagent/agents';
|
|
3
|
+
import { type SchedulerLimits } from './schedules.ts';
|
|
4
|
+
export { SqliteScheduleStore, createSchedulerRuntime, type ScheduleStoreLike, type SchedulerLimits, type SchedulerRuntime, type SchedulerRuntimeOptions, } from './schedules.ts';
|
|
3
5
|
import { type OptionalModuleHost } from '@stratusagent/plugins';
|
|
4
6
|
import { type PluginsConfig, type RuntimeSelection, type StateEnvironment } from '@stratusagent/state';
|
|
7
|
+
/**
|
|
8
|
+
* Where a daemon with no explicit `sessionDbPath` keeps its database — the
|
|
9
|
+
* one rule for it, exported because `stratus schedules` opens the same file
|
|
10
|
+
* from another process and a re-derived copy of this join is how the CLI
|
|
11
|
+
* ends up auditing a database no daemon writes.
|
|
12
|
+
*/
|
|
13
|
+
export declare const defaultSessionDbPath: (env?: StateEnvironment) => string;
|
|
5
14
|
/**
|
|
6
15
|
* Durable session storage on node:sqlite (unflagged on Node 22.13+). The
|
|
7
16
|
* whole session — messages, status, and metadata, including provider replay
|
|
@@ -93,6 +102,21 @@ export interface GatewayChannelAdapter {
|
|
|
93
102
|
name: string;
|
|
94
103
|
start(gateway: Gateway): Promise<void>;
|
|
95
104
|
stop(): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* The addressable outbound seam, mirroring
|
|
107
|
+
* `@stratusagent/channels`' `ChannelAdapter.resolveOutbound` structurally
|
|
108
|
+
* for the same reason the rest of this interface does. Optional: an
|
|
109
|
+
* adapter with no concept of a destination simply lacks it, and the
|
|
110
|
+
* gateway treats absence as "this channel cannot be spoken through".
|
|
111
|
+
* Implementations MUST reject a destination they could not deliver to,
|
|
112
|
+
* with a message fit to show the person who named it.
|
|
113
|
+
*/
|
|
114
|
+
resolveOutbound?(address: {
|
|
115
|
+
agentId: string;
|
|
116
|
+
to: string;
|
|
117
|
+
}): Promise<{
|
|
118
|
+
post(text: string): Promise<unknown>;
|
|
119
|
+
}>;
|
|
96
120
|
}
|
|
97
121
|
/**
|
|
98
122
|
* Where a session came from, as the gateway reports it. Structural for the
|
|
@@ -115,6 +139,16 @@ export interface SessionRouting {
|
|
|
115
139
|
*/
|
|
116
140
|
export interface ApprovalTransport {
|
|
117
141
|
request: ApprovalRequester;
|
|
142
|
+
/**
|
|
143
|
+
* The schedule carve-out, for a policy that understands destinations
|
|
144
|
+
* (`@stratusagent/permissions`' `destinations` option takes exactly this
|
|
145
|
+
* shape). True when the session is a firing of a schedule that still
|
|
146
|
+
* exists, belongs to this agent, and was approved with this destination
|
|
147
|
+
* — the gateway answers from the schedule row, so cancelling revokes.
|
|
148
|
+
*/
|
|
149
|
+
destinations: {
|
|
150
|
+
isPreauthorized(session: Session, destination: string): boolean;
|
|
151
|
+
};
|
|
118
152
|
}
|
|
119
153
|
/**
|
|
120
154
|
* A parked call, published for anyone who can reach a person. Mirrors
|
|
@@ -199,12 +233,26 @@ export interface GatewayTool {
|
|
|
199
233
|
/** Whether that package is trusted to declare a tool `safe`. */
|
|
200
234
|
trusted?: boolean;
|
|
201
235
|
}
|
|
236
|
+
/** One skill this daemon serves — identity and provenance, never the body. */
|
|
237
|
+
export interface GatewaySkill {
|
|
238
|
+
/** Canonical id: bare for an operator-installed skill, `<package>:<skill>` for a plugin's. */
|
|
239
|
+
id: string;
|
|
240
|
+
name: string;
|
|
241
|
+
description: string;
|
|
242
|
+
/** The bare id a plugin's skill also answers to, while no one else claims it. */
|
|
243
|
+
alias?: string;
|
|
244
|
+
/** The plugin package that contributed it; absent for operator-installed skills. */
|
|
245
|
+
package?: string;
|
|
246
|
+
/** Where the SKILL.md lives. */
|
|
247
|
+
path?: string;
|
|
248
|
+
}
|
|
202
249
|
/** A plugin this daemon was asked to load, and what came of it. */
|
|
203
250
|
export interface GatewayPluginStatus {
|
|
204
251
|
package: string;
|
|
205
252
|
name?: string;
|
|
206
253
|
trusted?: boolean;
|
|
207
254
|
tools?: GatewayTool[];
|
|
255
|
+
skills?: GatewaySkill[];
|
|
208
256
|
/** Present when the plugin did not load, and the only field that is. */
|
|
209
257
|
error?: string;
|
|
210
258
|
}
|
|
@@ -235,6 +283,12 @@ export interface GatewayOptions {
|
|
|
235
283
|
idleTimeoutMs?: number;
|
|
236
284
|
/** Session database path. Default ~/.stratus/sessions.db. */
|
|
237
285
|
sessionDbPath?: string;
|
|
286
|
+
/**
|
|
287
|
+
* Scheduler limits — the interval floor, the per-agent cap on concurrent
|
|
288
|
+
* scheduled turns, and the tick cadence. Defaults are production values;
|
|
289
|
+
* tests lower them.
|
|
290
|
+
*/
|
|
291
|
+
schedules?: SchedulerLimits;
|
|
238
292
|
/**
|
|
239
293
|
* Plugins to load, keyed by package name — the `plugins` block, which the
|
|
240
294
|
* caller has already read from a **trusted** config. The gateway does not
|
|
@@ -308,6 +362,19 @@ export interface Gateway {
|
|
|
308
362
|
activeTurnId(sessionId: string): string | undefined;
|
|
309
363
|
/** Every call parked on a human right now, oldest first. */
|
|
310
364
|
pendingApprovals(): PendingApproval[];
|
|
365
|
+
/**
|
|
366
|
+
* Every schedule the fleet has set, oldest first — the audit list the
|
|
367
|
+
* spec demands: an agent that scheduled something an operator cannot see
|
|
368
|
+
* or stop is a bug. Each row is also the scope of its own destination
|
|
369
|
+
* grant, so this doubles as the list of standing permissions to speak.
|
|
370
|
+
*/
|
|
371
|
+
schedules(): ScheduleRecord[];
|
|
372
|
+
/**
|
|
373
|
+
* Cancel any agent's schedule — the operator's stop button, and the
|
|
374
|
+
* revocation of the destination grant riding on the row. False when no
|
|
375
|
+
* such schedule exists.
|
|
376
|
+
*/
|
|
377
|
+
cancelSchedule(scheduleId: string): boolean;
|
|
311
378
|
/**
|
|
312
379
|
* Every registered tool, with the risk it will actually be judged at and
|
|
313
380
|
* the package it came from.
|
|
@@ -319,6 +386,12 @@ export interface Gateway {
|
|
|
319
386
|
* itself the answer for them.
|
|
320
387
|
*/
|
|
321
388
|
tools(): GatewayTool[];
|
|
389
|
+
/**
|
|
390
|
+
* Every skill this daemon serves — operator-installed and
|
|
391
|
+
* plugin-contributed — with the ids an allowlist can name. Descriptors
|
|
392
|
+
* only: bodies stay behind `skill.read`, on the agent's own turn.
|
|
393
|
+
*/
|
|
394
|
+
skills(): GatewaySkill[];
|
|
322
395
|
/**
|
|
323
396
|
* The plugins this daemon loaded, and the ones it could not — a plugin an
|
|
324
397
|
* operator enabled that failed to load is exactly what they need to see,
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAGL,QAAQ,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
1
9
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
10
|
import { chmodSync, mkdirSync } from 'node:fs';
|
|
3
11
|
import path from 'node:path';
|
|
4
12
|
import { DatabaseSync } from 'node:sqlite';
|
|
5
|
-
import { AgentRegistry, AgentRunner, EventBus, RunAbortedError, ToolRegistry, readPendingApproval, } from '@stratusagent/core';
|
|
6
|
-
import { createDelegateTool, createRememberTool, } from '@stratusagent/agents';
|
|
13
|
+
import { AgentRegistry, AgentRunner, EventBus, RunAbortedError, SkillRegistry, ToolRegistry, createSkillReadTool, missingSkillRequirements, readPendingApproval, } from '@stratusagent/core';
|
|
14
|
+
import { createDelegateTool, createForgetTool, createMessageSendTool, createRecallTool, createRememberTool, createScheduleTools, } from '@stratusagent/agents';
|
|
15
|
+
import { createSchedulerRuntime, isScheduleSessionId, SqliteScheduleStore, SCHEDULE_SESSION_ID_PREFIX, } from "./schedules.js";
|
|
16
|
+
export { SqliteScheduleStore, createSchedulerRuntime, } from "./schedules.js";
|
|
7
17
|
import { createLocalCommandExecutor } from '@stratusagent/executor-local';
|
|
8
18
|
import { loadPlugins, } from '@stratusagent/plugins';
|
|
9
|
-
import { createDemoTool, createFileMemoryStore, createRuntimeProvider, DEFAULT_STRATUS_AGENT, loadRosterSouls, FALLBACK_ACTIVE_METADATA_KEY, loadSoulFile, memoryFilePath, migrateLegacyMemory, ConfigFileError, loadConfigFile, resolveConfigLocation, resolveConfiguredSoul, resolveRuntimeConfig, applySoulPins, stratusHomePath, workspacesDirPath, withLegacyDefaultMemories, } from '@stratusagent/state';
|
|
19
|
+
import { createDemoTool, createFileMemoryStore, createRuntimeProvider, DEFAULT_STRATUS_AGENT, loadOperatorSkills, loadRosterSouls, FALLBACK_ACTIVE_METADATA_KEY, loadSoulFile, memoryFilePath, migrateLegacyMemory, ConfigFileError, loadConfigFile, resolveConfigLocation, resolveConfiguredSoul, resolveRuntimeConfig, applySoulPins, stratusHomePath, workspacesDirPath, withLegacyDefaultMemories, } from '@stratusagent/state';
|
|
10
20
|
const SESSIONS_DB_FILENAME = 'sessions.db';
|
|
21
|
+
/**
|
|
22
|
+
* Where a daemon with no explicit `sessionDbPath` keeps its database — the
|
|
23
|
+
* one rule for it, exported because `stratus schedules` opens the same file
|
|
24
|
+
* from another process and a re-derived copy of this join is how the CLI
|
|
25
|
+
* ends up auditing a database no daemon writes.
|
|
26
|
+
*/
|
|
27
|
+
export const defaultSessionDbPath = (env = {}) => path.join(stratusHomePath(env), SESSIONS_DB_FILENAME);
|
|
11
28
|
const DEFAULT_IDLE_TIMEOUT_MS = 120_000;
|
|
12
29
|
/**
|
|
13
30
|
* How long a parked call waits for a person. Long enough to survive a
|
|
@@ -55,6 +72,12 @@ export class SqliteSessionStore {
|
|
|
55
72
|
// (the user_version pragma is a real page-one write) so the loop
|
|
56
73
|
// below covers them for the connection's whole lifetime.
|
|
57
74
|
this.db.exec('PRAGMA journal_mode = WAL');
|
|
75
|
+
// A busy timeout, because this file has a second writer: `stratus
|
|
76
|
+
// schedules cancel` opens its own connection to the schedules table
|
|
77
|
+
// living here, and WAL serializes writers file-wide — a session save
|
|
78
|
+
// racing that delete would otherwise throw `database is locked` rather
|
|
79
|
+
// than wait its brief turn.
|
|
80
|
+
this.db.exec('PRAGMA busy_timeout = 5000');
|
|
58
81
|
this.db.exec(`
|
|
59
82
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
60
83
|
id TEXT PRIMARY KEY,
|
|
@@ -225,10 +248,48 @@ export const createGateway = (options = {}) => {
|
|
|
225
248
|
// The default database lives in the gateway-owned ~/.stratus, which is
|
|
226
249
|
// tightened to owner-only; a caller-supplied path may sit in a shared
|
|
227
250
|
// directory that must not be chmodded from under other processes.
|
|
251
|
+
const sessionDbPath = options.sessionDbPath ?? defaultSessionDbPath(env);
|
|
228
252
|
const store = options.sessionDbPath
|
|
229
|
-
? new SqliteSessionStore(
|
|
230
|
-
: new SqliteSessionStore(
|
|
253
|
+
? new SqliteSessionStore(sessionDbPath)
|
|
254
|
+
: new SqliteSessionStore(sessionDbPath, { ownedDirectory: true });
|
|
231
255
|
const memory = withLegacyDefaultMemories(createFileMemoryStore(memoryFilePath(env)));
|
|
256
|
+
// ---- schedules ----------------------------------------------------------
|
|
257
|
+
// The same database sessions live in — one file to back up — through the
|
|
258
|
+
// schedule store's own connection (see its docs for why).
|
|
259
|
+
const scheduleStore = new SqliteScheduleStore(sessionDbPath);
|
|
260
|
+
/**
|
|
261
|
+
* The write side of an addressable destination, through whichever
|
|
262
|
+
* started channel serves its kind. Resolution happens per call against
|
|
263
|
+
* the live adapter list, so a channel that failed to start is honestly
|
|
264
|
+
* "cannot address" rather than a stale reference — and rejecting is how
|
|
265
|
+
* both callers (schedule creation, `message.send`) hear why.
|
|
266
|
+
*/
|
|
267
|
+
const outboundFor = async (agentId, destination) => {
|
|
268
|
+
const adapter = startedChannels.find((candidate) => candidate.name === destination.channel && candidate.resolveOutbound);
|
|
269
|
+
if (!adapter?.resolveOutbound) {
|
|
270
|
+
throw new Error(`No running channel can deliver to '${destination.channel}' destinations — `
|
|
271
|
+
+ (startedChannels.length === 0
|
|
272
|
+
? 'no channel adapters are running.'
|
|
273
|
+
: `running channels: ${startedChannels.map((candidate) => candidate.name).join(', ')}.`));
|
|
274
|
+
}
|
|
275
|
+
return adapter.resolveOutbound({ agentId, to: destination.to });
|
|
276
|
+
};
|
|
277
|
+
const scheduler = createSchedulerRuntime({
|
|
278
|
+
store: scheduleStore,
|
|
279
|
+
// Late-bound on purpose: firings go through the gateway's own dispatch
|
|
280
|
+
// path — single-flight, watchdog, refusal while stopping — never a
|
|
281
|
+
// second runner. Through the firing-only entry, which is allowed into
|
|
282
|
+
// the reserved `schedule:` namespace the public `dispatch` refuses.
|
|
283
|
+
dispatch: (input) => dispatchScheduledFiring(input),
|
|
284
|
+
validateDestination: async (agentId, destination) => {
|
|
285
|
+
await outboundFor(agentId, destination);
|
|
286
|
+
},
|
|
287
|
+
sessionStatus: async (sessionId) => (await store.get(sessionId))?.status,
|
|
288
|
+
hasAgent: (agentId) => registry.get(agentId) !== undefined,
|
|
289
|
+
...(options.schedules ? { limits: options.schedules } : {}),
|
|
290
|
+
log,
|
|
291
|
+
warn,
|
|
292
|
+
});
|
|
232
293
|
// ---- approval brokering -------------------------------------------------
|
|
233
294
|
const approvalTimeoutMs = options.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS;
|
|
234
295
|
// Refused at construction, not absorbed: a negative or NaN timeout fails
|
|
@@ -464,7 +525,12 @@ export const createGateway = (options = {}) => {
|
|
|
464
525
|
};
|
|
465
526
|
};
|
|
466
527
|
const configuredApprovals = typeof options.approvals === 'function'
|
|
467
|
-
? options.approvals({
|
|
528
|
+
? options.approvals({
|
|
529
|
+
request: requestApproval,
|
|
530
|
+
destinations: {
|
|
531
|
+
isPreauthorized: (session, destination) => scheduler.isPreauthorized(session, destination),
|
|
532
|
+
},
|
|
533
|
+
})
|
|
468
534
|
: options.approvals;
|
|
469
535
|
/**
|
|
470
536
|
* Every approval opens a tool phase, whatever policy answers it.
|
|
@@ -607,6 +673,17 @@ export const createGateway = (options = {}) => {
|
|
|
607
673
|
registry.unregister(id);
|
|
608
674
|
}
|
|
609
675
|
}
|
|
676
|
+
// Advisory, per the skills spec: a skill may say which toolsets its
|
|
677
|
+
// procedure expects (`requires:`), and an agent enabling it without
|
|
678
|
+
// them gets a warning at load — never a refusal, because a skill is
|
|
679
|
+
// prose and can degrade. The check itself is the kernel's
|
|
680
|
+
// (`missingSkillRequirements`), shared with the CLI's local runs so
|
|
681
|
+
// both hosts warn about the same configuration.
|
|
682
|
+
for (const agent of registry.list()) {
|
|
683
|
+
for (const { skill, missing } of missingSkillRequirements(agent, skillCatalog)) {
|
|
684
|
+
warn(`agent ${agent.id} enables skill ${skill.id}, which expects tools the agent is not allowed: ${missing.join(', ')}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
610
687
|
};
|
|
611
688
|
/**
|
|
612
689
|
* Roster rebuilds, one at a time.
|
|
@@ -675,11 +752,34 @@ export const createGateway = (options = {}) => {
|
|
|
675
752
|
const toolProvenance = new Map();
|
|
676
753
|
let loadedPlugins = [];
|
|
677
754
|
let pluginFailures = [];
|
|
755
|
+
const skillCatalog = new SkillRegistry();
|
|
756
|
+
let operatorSkills = [];
|
|
678
757
|
tools.register(createDemoTool());
|
|
679
758
|
tools.register(createRememberTool(memory));
|
|
759
|
+
tools.register(createRecallTool(memory));
|
|
760
|
+
tools.register(createForgetTool(memory));
|
|
761
|
+
// Registered here rather than left to the first runner, so `tools()`
|
|
762
|
+
// lists the reader before anything dispatches. The allowlist resolver
|
|
763
|
+
// mirrors the runner's own sourcing — the definition travelling with the
|
|
764
|
+
// session, then the roster — so the tool refuses exactly what the
|
|
765
|
+
// runner's gates refuse.
|
|
766
|
+
tools.register(createSkillReadTool(skillCatalog, {
|
|
767
|
+
allowlistFor: (session) => session.agent.skills ?? registry.get(session.agent.id)?.skills,
|
|
768
|
+
}));
|
|
680
769
|
// Delegation is dispatcher-backed, not runner-capturing: the target agent
|
|
681
770
|
// runs through the same per-provider routing as a direct dispatch, so a
|
|
682
771
|
// delegated specialist uses their own provider and credentials.
|
|
772
|
+
// Scheduling and outbound speech are gateway-registered for the same
|
|
773
|
+
// reason delegation is: the tools need the dispatcher, the store, and
|
|
774
|
+
// the channels, and the plugin context carries none of them. Souls still
|
|
775
|
+
// opt in by allowlist (`schedule.*`, `message.send`) like any other tool.
|
|
776
|
+
for (const scheduleTool of createScheduleTools(scheduler.handle)) {
|
|
777
|
+
tools.register(scheduleTool);
|
|
778
|
+
}
|
|
779
|
+
tools.register(createMessageSendTool(async ({ agentId, destination, text }) => {
|
|
780
|
+
const connection = await outboundFor(agentId, destination);
|
|
781
|
+
await connection.post(text);
|
|
782
|
+
}));
|
|
683
783
|
tools.register(createDelegateTool({
|
|
684
784
|
registry,
|
|
685
785
|
dispatch: (input) => dispatchInternal({
|
|
@@ -730,6 +830,7 @@ export const createGateway = (options = {}) => {
|
|
|
730
830
|
store,
|
|
731
831
|
bus,
|
|
732
832
|
agents: registry,
|
|
833
|
+
skills: skillCatalog,
|
|
733
834
|
memory,
|
|
734
835
|
streaming: true,
|
|
735
836
|
...(options.maxTurns !== undefined ? { maxTurns: options.maxTurns } : {}),
|
|
@@ -1243,10 +1344,34 @@ export const createGateway = (options = {}) => {
|
|
|
1243
1344
|
});
|
|
1244
1345
|
return turn;
|
|
1245
1346
|
};
|
|
1347
|
+
/**
|
|
1348
|
+
* A scheduled firing's own dispatch — single-flight and stopping-aware
|
|
1349
|
+
* like the public one, but permitted into the reserved `schedule:`
|
|
1350
|
+
* namespace the public one refuses. Firings are the only writers of that
|
|
1351
|
+
* namespace, which is what keeps the destination carve-out unforgeable:
|
|
1352
|
+
* an external message can never share, resume, or race a firing's
|
|
1353
|
+
* session, so `isPreauthorized` can trust that a `schedule:` session
|
|
1354
|
+
* running is the scheduler's turn and no one else's.
|
|
1355
|
+
*/
|
|
1356
|
+
const dispatchScheduledFiring = (input) => {
|
|
1357
|
+
if (stopping) {
|
|
1358
|
+
throw new Error('The gateway is stopping and no longer accepts new work.');
|
|
1359
|
+
}
|
|
1360
|
+
return onSessionChain(input.sessionId, () => dispatchInternal(input));
|
|
1361
|
+
};
|
|
1246
1362
|
const dispatch = async (input) => {
|
|
1247
1363
|
if (stopping) {
|
|
1248
1364
|
throw new Error('The gateway is stopping and no longer accepts new work.');
|
|
1249
1365
|
}
|
|
1366
|
+
// The reserved namespace is the scheduler's alone. A caller that could
|
|
1367
|
+
// name a firing's session id — derivable from `GET /schedules` — would
|
|
1368
|
+
// otherwise queue a turn of its own behind the firing (single-flight)
|
|
1369
|
+
// and run inside the grant, using `message.send` to the schedule's
|
|
1370
|
+
// approved destination without its own approval. Refused here, at the
|
|
1371
|
+
// one door every external dispatch comes through.
|
|
1372
|
+
if (isScheduleSessionId(input.sessionId)) {
|
|
1373
|
+
throw new Error(`Session ids beginning with "${SCHEDULE_SESSION_ID_PREFIX}" are reserved for scheduled firings and cannot be dispatched externally.`);
|
|
1374
|
+
}
|
|
1250
1375
|
const { turnId } = input;
|
|
1251
1376
|
if (turnId === undefined) {
|
|
1252
1377
|
return onSessionChain(input.sessionId, () => dispatchInternal(input));
|
|
@@ -1283,9 +1408,10 @@ export const createGateway = (options = {}) => {
|
|
|
1283
1408
|
config: configured,
|
|
1284
1409
|
host: options.pluginHost ?? {
|
|
1285
1410
|
resolve: (specifier) => import.meta.resolve(specifier),
|
|
1286
|
-
import: (specifier) => import(specifier),
|
|
1411
|
+
import: (specifier) => import(__rewriteRelativeImportExtension(specifier)),
|
|
1287
1412
|
},
|
|
1288
1413
|
tools,
|
|
1414
|
+
skills: skillCatalog,
|
|
1289
1415
|
bus,
|
|
1290
1416
|
workspaceRoot: workspacesDirPath(env),
|
|
1291
1417
|
});
|
|
@@ -1295,12 +1421,28 @@ export const createGateway = (options = {}) => {
|
|
|
1295
1421
|
for (const tool of plugin.tools) {
|
|
1296
1422
|
toolProvenance.set(tool.name, { package: plugin.package, trusted: plugin.trusted });
|
|
1297
1423
|
}
|
|
1298
|
-
|
|
1424
|
+
const contributed = [
|
|
1425
|
+
...plugin.tools.map((tool) => `${tool.name} (${tool.risk})`),
|
|
1426
|
+
...plugin.skills.map((skill) => `skill ${skill.id}`),
|
|
1427
|
+
];
|
|
1428
|
+
log(`plugin ${plugin.package} loaded — ${contributed.join(', ') || 'no tools'}`);
|
|
1299
1429
|
}
|
|
1300
1430
|
for (const failure of result.failures) {
|
|
1301
1431
|
warn(`plugin ${failure.package} did not load: ${failure.reason}`);
|
|
1302
1432
|
}
|
|
1303
1433
|
};
|
|
1434
|
+
/**
|
|
1435
|
+
* Load the operator's `~/.stratus/skills/` into the catalog. Before the
|
|
1436
|
+
* plugins, deliberately: an operator's bare id outranks a plugin's bare
|
|
1437
|
+
* alias for the same name, and the plugin's skill stays reachable by its
|
|
1438
|
+
* qualified `<package>:<skill>` form.
|
|
1439
|
+
*/
|
|
1440
|
+
const startSkills = async () => {
|
|
1441
|
+
operatorSkills = await loadOperatorSkills(env, skillCatalog, warn);
|
|
1442
|
+
for (const skill of operatorSkills) {
|
|
1443
|
+
log(`skill ${skill.id} loaded from ${skill.path}`);
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1304
1446
|
/**
|
|
1305
1447
|
* Everything after the plugins: the roster, the channels, and the sweeps
|
|
1306
1448
|
* that need both. Split out so `start()` can wrap it in the cleanup a
|
|
@@ -1336,6 +1478,11 @@ export const createGateway = (options = {}) => {
|
|
|
1336
1478
|
}
|
|
1337
1479
|
}
|
|
1338
1480
|
}
|
|
1481
|
+
// After the channels, so a catch-up firing has somewhere to report and
|
|
1482
|
+
// the missed-window log lines land in a gateway whose surfaces are
|
|
1483
|
+
// listening. The sweep inside is quick (store reads); the firings it
|
|
1484
|
+
// may start are tracked, not awaited.
|
|
1485
|
+
await scheduler.start();
|
|
1339
1486
|
// Last, and not awaited: recovery re-asks, so it needs the channels
|
|
1340
1487
|
// above already listening — but a turn parked behind a slow approver
|
|
1341
1488
|
// must not hold up start(), or a daemon with one outstanding request
|
|
@@ -1371,6 +1518,9 @@ export const createGateway = (options = {}) => {
|
|
|
1371
1518
|
// Before the roster and before channels: a turn must never arrive for
|
|
1372
1519
|
// an agent whose soul lists a tool the daemon has not registered yet,
|
|
1373
1520
|
// which would refuse the call as "not permitted" and blame the soul.
|
|
1521
|
+
// Skills first for the same reason — and before the plugins, so the
|
|
1522
|
+
// operator's bare ids win their aliases (see startSkills).
|
|
1523
|
+
await startSkills();
|
|
1374
1524
|
await startPlugins();
|
|
1375
1525
|
try {
|
|
1376
1526
|
await startServing();
|
|
@@ -1394,6 +1544,11 @@ export const createGateway = (options = {}) => {
|
|
|
1394
1544
|
// in-flight turns (already-started dispatches keep running), drain,
|
|
1395
1545
|
// and close.
|
|
1396
1546
|
stopping = true;
|
|
1547
|
+
// No new firings from here: the tick stops before anything drains,
|
|
1548
|
+
// so the drain below is over a set that can only shrink. A tick
|
|
1549
|
+
// already past its `stopping` check loses to `dispatch`'s own
|
|
1550
|
+
// refusal — the second gate is why this needs no handshake.
|
|
1551
|
+
scheduler.stop();
|
|
1397
1552
|
// Deny what is parked before draining, or the drain waits out every
|
|
1398
1553
|
// outstanding approval timeout — a shutdown would hang for as long as
|
|
1399
1554
|
// the longest request had left. Each denial is a real decision the
|
|
@@ -1408,10 +1563,15 @@ export const createGateway = (options = {}) => {
|
|
|
1408
1563
|
await Promise.allSettled(startedChannels.map((adapter) => adapter.stop()));
|
|
1409
1564
|
startedChannels.length = 0;
|
|
1410
1565
|
await Promise.allSettled([...inflight]);
|
|
1566
|
+
// After the turn drain: a firing's wrapper settles after its
|
|
1567
|
+
// dispatch does (it retires spent one-shots), so this is the last
|
|
1568
|
+
// writer of the schedule table.
|
|
1569
|
+
await scheduler.drain();
|
|
1411
1570
|
// After the turns that might still be using them. A plugin holding a
|
|
1412
1571
|
// browser is the reason this exists, and closing it out from under a
|
|
1413
1572
|
// running screenshot would fail that turn rather than tidy up.
|
|
1414
1573
|
await disposePlugins();
|
|
1574
|
+
scheduleStore.close();
|
|
1415
1575
|
store.close();
|
|
1416
1576
|
log('stratusd stopped');
|
|
1417
1577
|
},
|
|
@@ -1439,6 +1599,12 @@ export const createGateway = (options = {}) => {
|
|
|
1439
1599
|
.map((parked) => parked.pending)
|
|
1440
1600
|
.sort((a, b) => a.parkedAt.localeCompare(b.parkedAt));
|
|
1441
1601
|
},
|
|
1602
|
+
schedules() {
|
|
1603
|
+
return scheduler.list();
|
|
1604
|
+
},
|
|
1605
|
+
cancelSchedule(scheduleId) {
|
|
1606
|
+
return scheduler.cancel(scheduleId);
|
|
1607
|
+
},
|
|
1442
1608
|
tools() {
|
|
1443
1609
|
// From the registry, not from the load records: the risk reported is
|
|
1444
1610
|
// the one a call will actually be judged at, floor applied, and a
|
|
@@ -1454,6 +1620,32 @@ export const createGateway = (options = {}) => {
|
|
|
1454
1620
|
};
|
|
1455
1621
|
});
|
|
1456
1622
|
},
|
|
1623
|
+
skills() {
|
|
1624
|
+
// From the registry, not from the load records: the registry is the
|
|
1625
|
+
// live answer for which bare aliases still resolve — a plugin loaded
|
|
1626
|
+
// later can have retired one a record still remembers.
|
|
1627
|
+
const provenance = new Map();
|
|
1628
|
+
for (const info of operatorSkills) {
|
|
1629
|
+
provenance.set(info.id, { path: info.path });
|
|
1630
|
+
}
|
|
1631
|
+
for (const plugin of loadedPlugins) {
|
|
1632
|
+
for (const skill of plugin.skills) {
|
|
1633
|
+
provenance.set(skill.id, { package: plugin.package, path: skill.path });
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
return skillCatalog.list().map((skill) => {
|
|
1637
|
+
const alias = skillCatalog.idsFor(skill.id).find((id) => id !== skill.id);
|
|
1638
|
+
const from = provenance.get(skill.id);
|
|
1639
|
+
return {
|
|
1640
|
+
id: skill.id,
|
|
1641
|
+
name: skill.name,
|
|
1642
|
+
description: skill.description,
|
|
1643
|
+
...(alias !== undefined ? { alias } : {}),
|
|
1644
|
+
...(from?.package !== undefined ? { package: from.package } : {}),
|
|
1645
|
+
...(from !== undefined ? { path: from.path } : {}),
|
|
1646
|
+
};
|
|
1647
|
+
});
|
|
1648
|
+
},
|
|
1457
1649
|
plugins() {
|
|
1458
1650
|
const loaded = loadedPlugins.map((plugin) => ({
|
|
1459
1651
|
package: plugin.package,
|
|
@@ -1466,6 +1658,20 @@ export const createGateway = (options = {}) => {
|
|
|
1466
1658
|
package: plugin.package,
|
|
1467
1659
|
trusted: plugin.trusted,
|
|
1468
1660
|
})),
|
|
1661
|
+
// The alias comes from the live registry, exactly as `skills()`
|
|
1662
|
+
// derives it: a plugin loaded later can have retired it, and this
|
|
1663
|
+
// listing must not advertise a bare id `skill.read` would refuse.
|
|
1664
|
+
skills: plugin.skills.map((skill) => {
|
|
1665
|
+
const alias = skillCatalog.idsFor(skill.id).find((id) => id !== skill.id);
|
|
1666
|
+
return {
|
|
1667
|
+
id: skill.id,
|
|
1668
|
+
name: skill.name,
|
|
1669
|
+
description: skill.description,
|
|
1670
|
+
...(alias !== undefined ? { alias } : {}),
|
|
1671
|
+
package: plugin.package,
|
|
1672
|
+
path: skill.path,
|
|
1673
|
+
};
|
|
1674
|
+
}),
|
|
1469
1675
|
}));
|
|
1470
1676
|
const failed = pluginFailures.map((failure) => ({
|
|
1471
1677
|
package: failure.package,
|