@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 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,
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
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(options.sessionDbPath)
230
- : new SqliteSessionStore(path.join(stratusHomePath(env), SESSIONS_DB_FILENAME), { ownedDirectory: true });
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({ request: requestApproval })
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
- log(`plugin ${plugin.package} loaded — ${plugin.tools.map((tool) => `${tool.name} (${tool.risk})`).join(', ') || 'no tools'}`);
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,