@stratusagent/cli 0.11.2 → 0.11.4
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/README.md +14 -8
- package/dist/approvals.d.ts +93 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +368 -0
- package/dist/approvals.js.map +1 -0
- package/dist/commands/agent-new.d.ts +22 -0
- package/dist/commands/agent-new.d.ts.map +1 -0
- package/dist/commands/agent-new.js +159 -0
- package/dist/commands/agent-new.js.map +1 -0
- package/dist/commands/agents.d.ts +4 -0
- package/dist/commands/agents.d.ts.map +1 -0
- package/dist/commands/agents.js +90 -0
- package/dist/commands/agents.js.map +1 -0
- package/dist/commands/chat.d.ts +4 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +227 -0
- package/dist/commands/chat.js.map +1 -0
- package/dist/commands/credential.d.ts +22 -0
- package/dist/commands/credential.d.ts.map +1 -0
- package/dist/commands/credential.js +93 -0
- package/dist/commands/credential.js.map +1 -0
- package/dist/commands/dashboard.d.ts +14 -0
- package/dist/commands/dashboard.d.ts.map +1 -0
- package/dist/commands/dashboard.js +195 -0
- package/dist/commands/dashboard.js.map +1 -0
- package/dist/commands/doctor.d.ts +40 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +376 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/grants.d.ts +19 -0
- package/dist/commands/grants.d.ts.map +1 -0
- package/dist/commands/grants.js +128 -0
- package/dist/commands/grants.js.map +1 -0
- package/dist/commands/logs.d.ts +9 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +58 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/memory.d.ts +20 -0
- package/dist/commands/memory.d.ts.map +1 -0
- package/dist/commands/memory.js +110 -0
- package/dist/commands/memory.js.map +1 -0
- package/dist/commands/plugins.d.ts +103 -0
- package/dist/commands/plugins.d.ts.map +1 -0
- package/dist/commands/plugins.js +386 -0
- package/dist/commands/plugins.js.map +1 -0
- package/dist/commands/restart.d.ts +4 -0
- package/dist/commands/restart.d.ts.map +1 -0
- package/dist/commands/restart.js +25 -0
- package/dist/commands/restart.js.map +1 -0
- package/dist/commands/schedules.d.ts +11 -0
- package/dist/commands/schedules.d.ts.map +1 -0
- package/dist/commands/schedules.js +50 -0
- package/dist/commands/schedules.js.map +1 -0
- package/dist/commands/serve.d.ts +4 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +633 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/service.d.ts +9 -0
- package/dist/commands/service.d.ts.map +1 -0
- package/dist/commands/service.js +97 -0
- package/dist/commands/service.js.map +1 -0
- package/dist/commands/session.d.ts +9 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +25 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/commands/setup.d.ts +5 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +2467 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/skills.d.ts +15 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +272 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/template.d.ts +4 -0
- package/dist/commands/template.d.ts.map +1 -0
- package/dist/commands/template.js +593 -0
- package/dist/commands/template.js.map +1 -0
- package/dist/commands/update.d.ts +21 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +291 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/daemon.d.ts +74 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +192 -0
- package/dist/daemon.js.map +1 -0
- package/dist/environment.d.ts +114 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +2 -0
- package/dist/environment.js.map +1 -0
- package/dist/events.d.ts +17 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +87 -0
- package/dist/events.js.map +1 -0
- package/dist/help.d.ts +2 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +272 -0
- package/dist/help.js.map +1 -0
- package/dist/index.d.ts +32 -624
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -6981
- package/dist/index.js.map +1 -1
- package/dist/io.d.ts +16 -0
- package/dist/io.d.ts.map +1 -0
- package/dist/io.js +45 -0
- package/dist/io.js.map +1 -0
- package/dist/loaders.d.ts +31 -0
- package/dist/loaders.d.ts.map +1 -0
- package/dist/loaders.js +56 -0
- package/dist/loaders.js.map +1 -0
- package/dist/main.d.ts +8 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +218 -0
- package/dist/main.js.map +1 -0
- package/dist/npm.d.ts +66 -0
- package/dist/npm.d.ts.map +1 -0
- package/dist/npm.js +201 -0
- package/dist/npm.js.map +1 -0
- package/dist/parse.d.ts +245 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +1098 -0
- package/dist/parse.js.map +1 -0
- package/dist/plugin-catalog.d.ts +44 -0
- package/dist/plugin-catalog.d.ts.map +1 -0
- package/dist/plugin-catalog.js +70 -0
- package/dist/plugin-catalog.js.map +1 -0
- package/dist/preflight.d.ts +60 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +102 -0
- package/dist/preflight.js.map +1 -0
- package/dist/prompter.d.ts +84 -0
- package/dist/prompter.d.ts.map +1 -0
- package/dist/prompter.js +347 -0
- package/dist/prompter.js.map +1 -0
- package/dist/roster.d.ts +57 -0
- package/dist/roster.d.ts.map +1 -0
- package/dist/roster.js +78 -0
- package/dist/roster.js.map +1 -0
- package/dist/runtime.d.ts +76 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +372 -0
- package/dist/runtime.js.map +1 -0
- package/dist/slack.d.ts +26 -0
- package/dist/slack.d.ts.map +1 -0
- package/dist/slack.js +113 -0
- package/dist/slack.js.map +1 -0
- package/dist/source.d.ts +24 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +80 -0
- package/dist/source.js.map +1 -0
- package/dist/supervisor.d.ts +69 -0
- package/dist/supervisor.d.ts.map +1 -0
- package/dist/supervisor.js +218 -0
- package/dist/supervisor.js.map +1 -0
- package/dist/trusted-config.d.ts +84 -0
- package/dist/trusted-config.d.ts.map +1 -0
- package/dist/trusted-config.js +159 -0
- package/dist/trusted-config.js.map +1 -0
- package/package.json +27 -23
|
@@ -0,0 +1,633 @@
|
|
|
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
|
+
};
|
|
9
|
+
import { createFileCommandWhitelist, createPermissionPolicy, describeCommandScope, describeOriginScope, describeToolGrant, } from '@stratusagent/permissions';
|
|
10
|
+
import { agentsDirPath, loadChannelCredentials, readProcessEnv, logsDirPath, resolveAgentApprovals, resolveAgentPrincipals, servedRuntimes, discoverIgnoredUntrustedConfig, } from '@stratusagent/state';
|
|
11
|
+
import { createLogWriter, truncateRedirectLogs } from "../logs.js";
|
|
12
|
+
import { describePrincipals, describeApprovers } from "../approvals.js";
|
|
13
|
+
import { HomeHeldError, legacyDaemonServing, describeHeldHome } from "../daemon.js";
|
|
14
|
+
import { formatEvent, eventDetail } from "../events.js";
|
|
15
|
+
import { writeLine } from "../io.js";
|
|
16
|
+
import { loadSlackAdapter, loadControlApi } from "../loaders.js";
|
|
17
|
+
import { warnOnCredentialOverride, warnOnUntrustedConfig, warnOnIgnoredConfig } from "../runtime.js";
|
|
18
|
+
import { RESTART_EXIT_CODE, UNDRAINED_RESTART_EXIT_CODE, SUPERVISED_ENV, BOUND_API_PORT_ENV, defaultSupervisorLink, superviseRestarts, } from "../supervisor.js";
|
|
19
|
+
import { loadServeApprovals, loadServePrincipals, loadServeApi, loadServePlugins, loadServeRuntimeSelection, } from "../trusted-config.js";
|
|
20
|
+
export const runServe = async (command, streams, env = {}) => {
|
|
21
|
+
// First, before anything that can throw. A broken install makes the
|
|
22
|
+
// gateway import below fail, the manager restarts, and the CLI's error
|
|
23
|
+
// and help text append to the redirect file again — the crash loop this
|
|
24
|
+
// bounding exists for. Running it after the import would skip exactly
|
|
25
|
+
// that case.
|
|
26
|
+
if (command.logToFile !== false) {
|
|
27
|
+
await truncateRedirectLogs(logsDirPath(env)).catch(() => undefined);
|
|
28
|
+
}
|
|
29
|
+
// Loaded lazily: the gateway pulls in node:sqlite, which every other CLI
|
|
30
|
+
// command neither needs nor should pay for (Node still prints an
|
|
31
|
+
// experimental warning for it).
|
|
32
|
+
const { createGateway, claimHome, HomeClaimedError } = await import('@stratusagent/gateway');
|
|
33
|
+
// Before anything of this daemon's is written or opened: a second daemon
|
|
34
|
+
// on this home is refused here, with no log line, no store, and no sweep
|
|
35
|
+
// or firing of its own.
|
|
36
|
+
let claim;
|
|
37
|
+
try {
|
|
38
|
+
claim = claimHome(env);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error instanceof HomeClaimedError) {
|
|
42
|
+
throw new HomeHeldError(await describeHeldHome(env));
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
let outcome;
|
|
47
|
+
try {
|
|
48
|
+
// The one daemon the claim cannot see: one that predates it.
|
|
49
|
+
if (await legacyDaemonServing(env)) {
|
|
50
|
+
throw new HomeHeldError(await describeHeldHome(env));
|
|
51
|
+
}
|
|
52
|
+
outcome = await serveHeldHome(command, streams, env, createGateway);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
// After everything: the store closed by stop(), or whatever a throw
|
|
56
|
+
// reached — a preflight that rejected before the gateway existed
|
|
57
|
+
// included, so a host that calls runServe again is not refused for a
|
|
58
|
+
// daemon that never started. And never sooner, so a replacement cannot
|
|
59
|
+
// open the store while this one still writes.
|
|
60
|
+
claim.release();
|
|
61
|
+
}
|
|
62
|
+
// An announced restart, once the home is let go — the replacement claims
|
|
63
|
+
// it next. A daemon the supervisor started asks it for the next one by
|
|
64
|
+
// exiting; the first daemon becomes the supervisor itself. Either way the
|
|
65
|
+
// process the service manager (or the terminal) started is what stays.
|
|
66
|
+
if (outcome.code === RESTART_EXIT_CODE && !readProcessEnv(env)[SUPERVISED_ENV]) {
|
|
67
|
+
return superviseRestarts(command, { ...(outcome.boundApiPort !== undefined ? { boundApiPort: outcome.boundApiPort } : {}), sessions: outcome.sessions }, streams, env);
|
|
68
|
+
}
|
|
69
|
+
return outcome.code;
|
|
70
|
+
};
|
|
71
|
+
const serveHeldHome = async (command, streams, env, createGateway) => {
|
|
72
|
+
// Under a service manager the daemon's stdout is gone, so everything it
|
|
73
|
+
// says is also written to ~/.stratus/logs — that file is what `stratus
|
|
74
|
+
// logs` reads, and the only record of an overnight run.
|
|
75
|
+
const logWriter = command.logToFile === false
|
|
76
|
+
? undefined
|
|
77
|
+
: createLogWriter({
|
|
78
|
+
dir: logsDirPath(env),
|
|
79
|
+
onError: (error) => writeLine(streams.stderr, `Warning: could not write the log file (${error instanceof Error ? error.message : String(error)}); continuing.`),
|
|
80
|
+
});
|
|
81
|
+
const log = (line) => {
|
|
82
|
+
writeLine(streams.stdout, line);
|
|
83
|
+
void logWriter?.write({ ts: new Date().toISOString(), level: 'info', msg: line });
|
|
84
|
+
};
|
|
85
|
+
const warn = (line) => {
|
|
86
|
+
writeLine(streams.stderr, `Warning: ${line}`);
|
|
87
|
+
void logWriter?.write({ ts: new Date().toISOString(), level: 'warn', msg: line });
|
|
88
|
+
};
|
|
89
|
+
// Agents with stored Slack tokens go live in Slack automatically — the
|
|
90
|
+
// tokens are gateway infrastructure secrets in the channels namespace of
|
|
91
|
+
// ~/.stratus/credentials.json (see @stratusagent/channel-slack's README
|
|
92
|
+
// for the 2-minute per-agent app setup).
|
|
93
|
+
// The approval policy and the Slack approver sets come from the same
|
|
94
|
+
// config block, resolved once here: the daemon must not answer "who can
|
|
95
|
+
// approve this" differently from "is anyone being asked at all".
|
|
96
|
+
const approvalsConfig = await loadServeApprovals(env, command.configPath, warn);
|
|
97
|
+
const approvalMode = command.approvals ?? approvalsConfig.mode ?? 'headless';
|
|
98
|
+
const principalsConfig = await loadServePrincipals(env, command.configPath, warn);
|
|
99
|
+
// Read here rather than inside the gateway for the same reason as the two
|
|
100
|
+
// blocks above: the trust boundary is a property of *which file* said it,
|
|
101
|
+
// and this is where the file precedence is already understood.
|
|
102
|
+
const pluginsConfig = await loadServePlugins(env, command.configPath, warn);
|
|
103
|
+
// Which of the loaded plugins' executors and memory stores serve this
|
|
104
|
+
// daemon — the same trust rule, one step on, and refused by the gateway
|
|
105
|
+
// at start when the name is nobody's.
|
|
106
|
+
const executorName = await loadServeRuntimeSelection('executor', env, command.configPath, warn);
|
|
107
|
+
const memoryStoreName = await loadServeRuntimeSelection('memoryStore', env, command.configPath, warn);
|
|
108
|
+
// Every kind of grant an agent holds — command scopes, origins, standing
|
|
109
|
+
// tool grants — in one file per agent beside its soul, through one store
|
|
110
|
+
// instance for the whole daemon. One instance, because the store caches
|
|
111
|
+
// each file for the life of the process: the control API's list and
|
|
112
|
+
// revoke go through this same object, which is what lets a revoke take
|
|
113
|
+
// effect on the policy's very next decision rather than at the next
|
|
114
|
+
// restart. A whitelist that exists and will not read is said here, once,
|
|
115
|
+
// and never written over — the daemon's log is where a grant list going
|
|
116
|
+
// quiet would otherwise go unnoticed.
|
|
117
|
+
const grantStore = createFileCommandWhitelist({ directory: agentsDirPath(env), warn });
|
|
118
|
+
// The control API is a channel adapter like any other: started after the
|
|
119
|
+
// roster loads, stopped before the store drains. It is optional because
|
|
120
|
+
// installing it is how an operator says they want a port open.
|
|
121
|
+
const apiConfig = await loadServeApi(env, command.configPath, warn);
|
|
122
|
+
const apiWanted = command.api ?? apiConfig.enabled ?? true;
|
|
123
|
+
// Typed as the seam, not as whatever the first push happens to be: the
|
|
124
|
+
// list holds the control API and the Slack adapter alike.
|
|
125
|
+
const controlApiChannels = [];
|
|
126
|
+
/** The API adapter itself, for what the seam does not carry: its address, and the sessions it hands across a restart. */
|
|
127
|
+
let boundApi;
|
|
128
|
+
if (apiWanted) {
|
|
129
|
+
const createControlApi = await loadControlApi();
|
|
130
|
+
// Resolved into locals first. Inlined, `a ?? b !== undefined` parses as
|
|
131
|
+
// `a ?? (b !== undefined)` — so an explicit `--api-port 0`, which is how
|
|
132
|
+
// you ask for any free port, was falsy and fell through to the default.
|
|
133
|
+
// Everything still bound and every test still passed, on whichever port
|
|
134
|
+
// nobody happened to be using.
|
|
135
|
+
const apiHost = command.apiHost ?? apiConfig.host;
|
|
136
|
+
const requestedPort = command.apiPort ?? apiConfig.port;
|
|
137
|
+
// The port the last daemon bound, from the supervisor that started
|
|
138
|
+
// this one — taken only where this daemon's own request is still for
|
|
139
|
+
// any free port. See BOUND_API_PORT_ENV.
|
|
140
|
+
const hinted = Number(readProcessEnv(env)[BOUND_API_PORT_ENV]);
|
|
141
|
+
const apiPort = requestedPort === 0 && Number.isInteger(hinted) && hinted > 0 ? hinted : requestedPort;
|
|
142
|
+
if (createControlApi) {
|
|
143
|
+
const api = createControlApi({
|
|
144
|
+
env,
|
|
145
|
+
...(apiHost !== undefined ? { host: apiHost } : {}),
|
|
146
|
+
...(apiPort !== undefined ? { port: apiPort } : {}),
|
|
147
|
+
...(command.configPath ? { configPath: command.configPath } : {}),
|
|
148
|
+
grants: grantStore,
|
|
149
|
+
log,
|
|
150
|
+
warn,
|
|
151
|
+
});
|
|
152
|
+
boundApi = api;
|
|
153
|
+
controlApiChannels.push(api);
|
|
154
|
+
}
|
|
155
|
+
else if (command.api === true || apiConfig.enabled === true) {
|
|
156
|
+
// Only when someone asked for it explicitly. A daemon that was never
|
|
157
|
+
// told to serve an API should not complain about not having one.
|
|
158
|
+
warn('the control API was requested, but @stratusagent/control-api is not installed. '
|
|
159
|
+
+ 'Run `npm install -g @stratusagent/control-api` to bring it online; starting without it.');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Listening before anything else is awaited: what the supervisor hands
|
|
163
|
+
// this daemon is queued in the channel until then, and the API keeps it
|
|
164
|
+
// until it is serving.
|
|
165
|
+
const supervised = Boolean(readProcessEnv(env)[SUPERVISED_ENV]);
|
|
166
|
+
const link = env.supervisorLink ?? defaultSupervisorLink;
|
|
167
|
+
if (supervised) {
|
|
168
|
+
link.receive((message) => {
|
|
169
|
+
if (message.type === 'stratusd.sessions') {
|
|
170
|
+
boundApi?.adoptSessions(message.sessions);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const channelCredentials = await loadChannelCredentials(env);
|
|
175
|
+
const slackAgents = Object.entries(channelCredentials.slack ?? {});
|
|
176
|
+
const channels = [...controlApiChannels];
|
|
177
|
+
// Tracked on its own, never as `channels.length`: the list now holds the
|
|
178
|
+
// control API too, and "can anyone be asked for an approval" is a question
|
|
179
|
+
// about the Slack adapter specifically. Reading it off the list length
|
|
180
|
+
// would tell a daemon with an API and no Slack app that every agent is
|
|
181
|
+
// reachable, and its gated calls would park with nobody rendering them.
|
|
182
|
+
let slackAdapterUp = false;
|
|
183
|
+
if (slackAgents.length > 0) {
|
|
184
|
+
// Channel packages are optional peers: the CLI never bundles a
|
|
185
|
+
// transport nobody asked for (the Slack SDKs alone are ~9 MB). A
|
|
186
|
+
// missing one degrades like a broken app does — the daemon serves
|
|
187
|
+
// every other channel — with an actionable line instead of a
|
|
188
|
+
// module-not-found stack.
|
|
189
|
+
const adapter = await loadSlackAdapter();
|
|
190
|
+
if (adapter) {
|
|
191
|
+
slackAdapterUp = true;
|
|
192
|
+
channels.push(adapter({
|
|
193
|
+
agents: slackAgents.map(([agentId, tokens]) => {
|
|
194
|
+
const route = resolveAgentApprovals(approvalsConfig, agentId);
|
|
195
|
+
const principals = resolveAgentPrincipals(principalsConfig, agentId);
|
|
196
|
+
return {
|
|
197
|
+
agentId,
|
|
198
|
+
appToken: tokens.appToken,
|
|
199
|
+
botToken: tokens.botToken,
|
|
200
|
+
...(route.slackApprovers ? { approvers: route.slackApprovers } : {}),
|
|
201
|
+
...(route.slackChannel ? { approvalChannel: route.slackChannel } : {}),
|
|
202
|
+
...(principals.slackUsers ? { principals: principals.slackUsers } : {}),
|
|
203
|
+
...(principals.admit ? { admit: principals.admit } : {}),
|
|
204
|
+
};
|
|
205
|
+
}),
|
|
206
|
+
log,
|
|
207
|
+
warn,
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
warn(`Slack tokens are stored for ${slackAgents.length} agent(s), but @stratusagent/channel-slack is not installed. `
|
|
212
|
+
+ 'Run `npm install -g @stratusagent/channel-slack` to bring them online; starting without the Slack channel.');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// A daemon is the costliest place for a silently demoted subscription:
|
|
216
|
+
// every dispatch for as long as it runs, with nobody watching. The
|
|
217
|
+
// gateway resolves each agent's soul independently on dispatch, so an
|
|
218
|
+
// agent pinning anthropic can pick up an environment key even when the
|
|
219
|
+
// daemon's own default is openai — checking only the default selection
|
|
220
|
+
// would miss exactly the agent that is being billed. Each distinct
|
|
221
|
+
// selection is resolved once here, and duplicate warnings are collapsed
|
|
222
|
+
// so a ten-agent roster does not print ten identical lines.
|
|
223
|
+
{
|
|
224
|
+
const warned = new Set();
|
|
225
|
+
const collect = (line) => {
|
|
226
|
+
if (warned.has(line)) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
warned.add(line);
|
|
230
|
+
// Through `warn`, not straight to stderr: under a service manager
|
|
231
|
+
// stderr is gone, and a cost warning that only exists there is
|
|
232
|
+
// invisible in exactly the deployment it matters for. `warn` adds
|
|
233
|
+
// its own prefix, so the one already on the line comes off.
|
|
234
|
+
warn(line.replace(/^Warning: /, ''));
|
|
235
|
+
};
|
|
236
|
+
const captured = {
|
|
237
|
+
stdout: { write: () => true },
|
|
238
|
+
stderr: { write: (chunk) => { collect(chunk.replace(/\n$/, '')); return true; } },
|
|
239
|
+
};
|
|
240
|
+
// The same notice `run` and `chat` give, from config discovery rather
|
|
241
|
+
// than from a resolved runtime: servedRuntimes drops a pass that fails
|
|
242
|
+
// to resolve — a real provider with no usable credential — and that
|
|
243
|
+
// daemon still starts, with nothing saying why the clone's persona is
|
|
244
|
+
// not in force. The per-runtime record below repeats it for a pass
|
|
245
|
+
// that did resolve; the dedupe above collapses the two to one line.
|
|
246
|
+
// `serve` takes no --soul, so the notice names the global config instead.
|
|
247
|
+
warnOnIgnoredConfig(await discoverIgnoredUntrustedConfig(command.configPath ? { configPath: command.configPath } : {}, env), captured, false);
|
|
248
|
+
// A pinned soul does not merely add a provider — the gateway DEMOTES
|
|
249
|
+
// the daemon-wide defaults it outranks, including STRATUS_PROVIDER, so
|
|
250
|
+
// each served runtime is resolved the way a dispatch resolves it.
|
|
251
|
+
for (const served of await servedRuntimes(env, command.configPath)) {
|
|
252
|
+
warnOnUntrustedConfig(served.runtime, captured, false);
|
|
253
|
+
await warnOnCredentialOverride(served.runtime, captured, served.env);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// The daemon had no policy at all: createGateway falls back to
|
|
257
|
+
// AllowAllApprovalPolicy, so every tool call from every agent
|
|
258
|
+
// auto-approved, indefinitely, with nobody watching. That was survivable
|
|
259
|
+
// while `serve` was something you ran in a terminal you were sitting at;
|
|
260
|
+
// it is not, now that setup installs it under launchd by default.
|
|
261
|
+
//
|
|
262
|
+
// There is no terminal behind a service manager, so the choice is
|
|
263
|
+
// between refusing every gated call (`headless`) and parking the turn to
|
|
264
|
+
// ask through a channel (`remote`). Headless stays the default: a daemon
|
|
265
|
+
// that starts waiting on people who were never told they are on the hook
|
|
266
|
+
// hangs turns instead of refusing them. Every refusal is logged — an
|
|
267
|
+
// unattended denial that appears nowhere reads like an agent that decided
|
|
268
|
+
// not to bother.
|
|
269
|
+
const onDecision = (decision) => {
|
|
270
|
+
if (decision.allowed) {
|
|
271
|
+
// A call that ran because somebody once said "always" is the one
|
|
272
|
+
// allowed decision the trace records: it is how something happened
|
|
273
|
+
// unattended, which is what an incident reconstruction needs to tell
|
|
274
|
+
// apart from a tool that was simply safe. Name and date, never input.
|
|
275
|
+
if (decision.grant) {
|
|
276
|
+
const by = decision.grant.grantedBy ? ` by ${decision.grant.grantedBy}` : '';
|
|
277
|
+
log(`${decision.agentId}: ${decision.toolName} ran under a standing grant (${describeToolGrant(decision.grant)}, granted ${decision.grant.grantedAt}${by}) (session ${decision.sessionId})`);
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
warn(`${decision.agentId}: ${decision.reason} (session ${decision.sessionId})`);
|
|
282
|
+
};
|
|
283
|
+
// A factory, not a policy: remote mode parks turns on a transport the
|
|
284
|
+
// gateway owns, so the policy cannot exist until the gateway is building
|
|
285
|
+
// it. Headless takes the same path so there is one construction site.
|
|
286
|
+
// The command-scope engine, which only has a caller once a shell pack is
|
|
287
|
+
// installed. Wired unconditionally because it costs nothing without one:
|
|
288
|
+
// a tool that carries no command string is judged by its risk exactly as
|
|
289
|
+
// before. The whitelist lives beside the agent's soul, per agent.
|
|
290
|
+
const commands = {
|
|
291
|
+
whitelist: grantStore,
|
|
292
|
+
onScopeRemembered: ({ agentId, scope }) => {
|
|
293
|
+
// An approval that widens what runs unattended, for every future
|
|
294
|
+
// session, is precisely the decision that must not be the one leaving
|
|
295
|
+
// no trace.
|
|
296
|
+
log(`${agentId}: "${describeCommandScope(scope)}" now runs without asking`);
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
// The origin-scope engine, whose grants live in the same file as the
|
|
300
|
+
// command scopes: one place an operator looks to see what an agent may
|
|
301
|
+
// do unattended. Wired unconditionally for the same reason — a tool that
|
|
302
|
+
// names no origin is judged by its risk exactly as before.
|
|
303
|
+
const origins = {
|
|
304
|
+
whitelist: grantStore,
|
|
305
|
+
onScopeRemembered: ({ agentId, scope }) => {
|
|
306
|
+
log(`${agentId}: ${describeOriginScope(scope)} is now acted on without asking`);
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
const approvals = (transport) => {
|
|
310
|
+
// The standing-grant engine, in the same file again — and the tool's
|
|
311
|
+
// contributor from the gateway, so a grant records which package's
|
|
312
|
+
// tool the operator said yes to and stops applying when that changes.
|
|
313
|
+
const grants = {
|
|
314
|
+
store: grantStore,
|
|
315
|
+
contributorOf: transport.contributorOf,
|
|
316
|
+
onGranted: ({ agentId, grant }) => {
|
|
317
|
+
log(`${agentId}: ${describeToolGrant(grant)} now runs without asking, until revoked${grant.grantedBy ? ` (granted by ${grant.grantedBy})` : ''}`);
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
return createPermissionPolicy(
|
|
321
|
+
// The destination scope rides along in BOTH modes — it is what lets a
|
|
322
|
+
// scheduled turn report to the channel a human approved with the
|
|
323
|
+
// schedule, and headless (where every other gated call is refused) is
|
|
324
|
+
// exactly the deployment it exists for. So do the grants: a standing
|
|
325
|
+
// grant is the only path a scope-less gated tool has to running
|
|
326
|
+
// unattended, and headless is where that matters.
|
|
327
|
+
approvalMode === 'remote'
|
|
328
|
+
? { mode: 'remote', request: transport.request, onDecision, commands, origins, grants, destinations: transport.destinations }
|
|
329
|
+
: { mode: 'headless', onDecision, commands, origins, grants, destinations: transport.destinations });
|
|
330
|
+
};
|
|
331
|
+
if (approvalMode === 'remote') {
|
|
332
|
+
// Only agents whose channel actually came up can be asked: tokens on
|
|
333
|
+
// disk with the Slack package missing means nothing renders the
|
|
334
|
+
// request, and the turn discovers that by hanging.
|
|
335
|
+
const askable = slackAdapterUp ? slackAgents.map(([agentId]) => agentId) : [];
|
|
336
|
+
log(`approvals: remote — gated calls are parked and asked in Slack (${describeApprovers(approvalsConfig, askable)})`);
|
|
337
|
+
}
|
|
338
|
+
if (slackAdapterUp) {
|
|
339
|
+
// Said at startup, because the failure it names is silent otherwise: a
|
|
340
|
+
// daemon with no principals serves every Slack message as `unknown`,
|
|
341
|
+
// and the operator finds out when their agent's memory reads that way.
|
|
342
|
+
log(`provenance: ${describePrincipals(principalsConfig, slackAgents.map(([agentId]) => agentId))}`);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* The one signal handler this daemon has, for the whole of its life —
|
|
346
|
+
* installed by the wait below, removed in the finally after the drain.
|
|
347
|
+
* See the wait for why it is never `once` and never removed sooner.
|
|
348
|
+
*/
|
|
349
|
+
let stopSignal;
|
|
350
|
+
let repeatWarned = false;
|
|
351
|
+
// The gateway's restart hands the process back here once it has stopped:
|
|
352
|
+
// the outcome is kept and the wait below released, and the tail of this
|
|
353
|
+
// function decides what "come back" means for this process.
|
|
354
|
+
let restart;
|
|
355
|
+
/** A signal or the host's abort asked for a stop — set when the wait ends for that reason. */
|
|
356
|
+
let stopRequested = false;
|
|
357
|
+
let requestShutdown = () => { };
|
|
358
|
+
const shutdownRequested = new Promise((resolve) => {
|
|
359
|
+
requestShutdown = resolve;
|
|
360
|
+
});
|
|
361
|
+
const gateway = createGateway({
|
|
362
|
+
env,
|
|
363
|
+
approvals,
|
|
364
|
+
onRestart: (outcome) => {
|
|
365
|
+
restart = outcome;
|
|
366
|
+
requestShutdown();
|
|
367
|
+
},
|
|
368
|
+
...(Object.keys(pluginsConfig).length > 0
|
|
369
|
+
? {
|
|
370
|
+
plugins: pluginsConfig,
|
|
371
|
+
// Resolved from *here*, not from inside the gateway.
|
|
372
|
+
// `import.meta.resolve` answers relative to the module that calls
|
|
373
|
+
// it, and a plugin is installed alongside the thing the operator
|
|
374
|
+
// installed — this CLI. Letting the gateway resolve from its own
|
|
375
|
+
// location works only where the layout happens to be flat.
|
|
376
|
+
pluginHost: {
|
|
377
|
+
resolve: (specifier) => import.meta.resolve(specifier),
|
|
378
|
+
import: (specifier) => import(__rewriteRelativeImportExtension(specifier)),
|
|
379
|
+
},
|
|
380
|
+
}
|
|
381
|
+
: {}),
|
|
382
|
+
...(executorName !== undefined ? { executor: executorName } : {}),
|
|
383
|
+
...(memoryStoreName !== undefined ? { memoryStore: memoryStoreName } : {}),
|
|
384
|
+
...(approvalsConfig.timeoutMs !== undefined ? { approvalTimeoutMs: approvalsConfig.timeoutMs } : {}),
|
|
385
|
+
...(command.configPath ? { selection: { configPath: command.configPath } } : {}),
|
|
386
|
+
...(command.idleTimeoutMs !== undefined ? { idleTimeoutMs: command.idleTimeoutMs } : {}),
|
|
387
|
+
...(channels.length > 0 ? { channels } : {}),
|
|
388
|
+
// The Slack adapter is host-wired, so its (agent, kind) claims are
|
|
389
|
+
// declared here; a plugin channel claiming one of them is refused at
|
|
390
|
+
// load rather than started beside it.
|
|
391
|
+
...(slackAdapterUp ? { hostChannelClaims: [{ kind: 'slack', agents: slackAgents.map(([agentId]) => agentId) }] } : {}),
|
|
392
|
+
log,
|
|
393
|
+
warn,
|
|
394
|
+
});
|
|
395
|
+
if (command.events) {
|
|
396
|
+
gateway.bus.subscribe((event) => {
|
|
397
|
+
const line = formatEvent(event);
|
|
398
|
+
if (line) {
|
|
399
|
+
writeLine(streams.stdout, line);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
if (logWriter) {
|
|
404
|
+
// Only session.created carries the agent id, so it seeds a map the
|
|
405
|
+
// later events in that session read from. A session resumed after a
|
|
406
|
+
// restart never re-creates, so an unmapped id falls back to the
|
|
407
|
+
// durable store — which is exactly the case where attribution matters.
|
|
408
|
+
const agentBySession = new Map();
|
|
409
|
+
// EventBus.emit awaits its subscribers, and a streaming provider awaits
|
|
410
|
+
// the delta sink — so anything awaited here lands on the critical path
|
|
411
|
+
// of every streamed token. This handler is deliberately synchronous:
|
|
412
|
+
// provider.delta is dropped outright (it carries no detail worth
|
|
413
|
+
// keeping, one record per token), the timestamp is taken now, and the
|
|
414
|
+
// write is queued without awaiting it.
|
|
415
|
+
gateway.bus.subscribe((event) => {
|
|
416
|
+
if (event.type === 'provider.delta') {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const ts = new Date().toISOString();
|
|
420
|
+
if (event.type === 'session.created') {
|
|
421
|
+
agentBySession.set(event.sessionId, event.agentId);
|
|
422
|
+
}
|
|
423
|
+
const detail = eventDetail(event);
|
|
424
|
+
const base = {
|
|
425
|
+
ts,
|
|
426
|
+
level: 'event',
|
|
427
|
+
event: event.type,
|
|
428
|
+
sessionId: event.sessionId,
|
|
429
|
+
...(detail ? { detail } : {}),
|
|
430
|
+
};
|
|
431
|
+
const known = agentBySession.get(event.sessionId);
|
|
432
|
+
if (known) {
|
|
433
|
+
void logWriter.write({ ...base, agentId: known });
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
// A session resumed after a restart never re-creates, so its agent
|
|
437
|
+
// is only in the store. That lookup is deferred off this path; the
|
|
438
|
+
// timestamp above keeps ordering honest.
|
|
439
|
+
void gateway.store.get(event.sessionId)
|
|
440
|
+
.then((session) => {
|
|
441
|
+
const agentId = session?.agent.id;
|
|
442
|
+
if (agentId) {
|
|
443
|
+
agentBySession.set(event.sessionId, agentId);
|
|
444
|
+
}
|
|
445
|
+
return logWriter.write({ ...base, ...(agentId ? { agentId } : {}) });
|
|
446
|
+
})
|
|
447
|
+
.catch(() => logWriter.write(base));
|
|
448
|
+
}
|
|
449
|
+
if (event.type === 'session.completed' || event.type === 'session.failed') {
|
|
450
|
+
agentBySession.delete(event.sessionId);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
await gateway.start();
|
|
455
|
+
// Read now, while the API is bound: its stop forgets the address, and
|
|
456
|
+
// a restart's respawn needs it (see ServeOutcome).
|
|
457
|
+
const boundApiPort = boundApi?.url !== undefined ? Number(new URL(boundApi.url).port) : undefined;
|
|
458
|
+
if (supervised && boundApiPort !== undefined && Number.isInteger(boundApiPort) && boundApiPort > 0) {
|
|
459
|
+
link.send({ type: 'stratusd.bound-api-port', port: boundApiPort });
|
|
460
|
+
}
|
|
461
|
+
// Under one finally from here: a throw anywhere after the start — a
|
|
462
|
+
// host's writable refusing a line, say — must still stop the gateway
|
|
463
|
+
// before the claim on the home is released, or a live gateway would be
|
|
464
|
+
// left behind a lock the next daemon can take.
|
|
465
|
+
let redirectTimer;
|
|
466
|
+
try {
|
|
467
|
+
// The roster is only known once the gateway has loaded it, and in remote
|
|
468
|
+
// mode an agent no channel can ask for is the quietest failure this
|
|
469
|
+
// feature has: its gated calls park with nobody rendering them and wait
|
|
470
|
+
// out the whole timeout before being denied. No channel can detect this
|
|
471
|
+
// on its own — a request is a broadcast, and no adapter knows whether
|
|
472
|
+
// another one is about to answer — so it is reported here, where the
|
|
473
|
+
// roster and the channel list are both in view.
|
|
474
|
+
if (approvalMode === 'remote') {
|
|
475
|
+
const askable = new Set(slackAdapterUp ? slackAgents.map(([agentId]) => agentId) : []);
|
|
476
|
+
const unreachable = gateway.agents().map((agent) => agent.id).filter((id) => !askable.has(id));
|
|
477
|
+
if (unreachable.length > 0) {
|
|
478
|
+
warn(`no channel can ask for ${unreachable.join(', ')}, so their gated calls will wait out the `
|
|
479
|
+
+ 'approval timeout and then be denied. Connect a Slack app for them, or run with --approvals headless.');
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
writeLine(streams.stdout, 'Press Ctrl+C to stop.');
|
|
483
|
+
// And periodically, for a long-running daemon that warns steadily
|
|
484
|
+
// without ever writing enough records to rotate. Unref'd, so it never
|
|
485
|
+
// holds the process open. Armed only once the daemon is actually
|
|
486
|
+
// serving: every step above can throw, and runServe is an exported
|
|
487
|
+
// function as much as a process entry point — in a host that survives
|
|
488
|
+
// the failure, a timer armed before the throw would outlive the call
|
|
489
|
+
// and go on truncating that environment's redirect logs. The finally
|
|
490
|
+
// below takes it down on every other exit path.
|
|
491
|
+
if (logWriter) {
|
|
492
|
+
redirectTimer = setInterval(() => void truncateRedirectLogs(logsDirPath(env)), 5 * 60_000);
|
|
493
|
+
redirectTimer.unref?.();
|
|
494
|
+
}
|
|
495
|
+
// Hold the event loop open until a shutdown request, then drain: new
|
|
496
|
+
// dispatches are refused while in-flight turns finish.
|
|
497
|
+
await new Promise((resolve) => {
|
|
498
|
+
const keepAlive = setInterval(() => { }, 2_147_000_000);
|
|
499
|
+
let settled = false;
|
|
500
|
+
const shutdown = () => {
|
|
501
|
+
if (settled) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
settled = true;
|
|
505
|
+
clearInterval(keepAlive);
|
|
506
|
+
resolve();
|
|
507
|
+
};
|
|
508
|
+
stopSignal = () => {
|
|
509
|
+
if (!settled) {
|
|
510
|
+
shutdown();
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
// A signal while a drain is already running — a repeat, or a stop
|
|
514
|
+
// arriving during a restart's drain. Either way the drain goes on,
|
|
515
|
+
// and a daemon told to stop does not come back.
|
|
516
|
+
stopRequested = true;
|
|
517
|
+
if (!repeatWarned) {
|
|
518
|
+
repeatWarned = true;
|
|
519
|
+
// Said from signal dispatch, outside the promise and its finally:
|
|
520
|
+
// a stream that throws on write (a host's injected stderr) would
|
|
521
|
+
// otherwise end the process from here — mid-drain, the very
|
|
522
|
+
// thing this handler exists to prevent. The word is best effort.
|
|
523
|
+
try {
|
|
524
|
+
warn('stop signal received while draining; still draining, and this daemon will not come back (SIGKILL ends it at once)');
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
// Nothing to say it to; the drain is what matters.
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
// `on`, never `once`, and not removed until the drain is over (the
|
|
532
|
+
// finally below). `once` drops the listener before running it, Node
|
|
533
|
+
// uninstalls its native handler the moment no listener is left, and
|
|
534
|
+
// a second SIGTERM landing in that window ends the process by the
|
|
535
|
+
// default action — mid-drain, with nothing said. The second signal
|
|
536
|
+
// is the normal case, not an edge: a stop delivered to the process
|
|
537
|
+
// group, as systemd's KillMode=control-group and a terminal's Ctrl+C
|
|
538
|
+
// deliver it, reaches a supervised daemon once from the kernel and
|
|
539
|
+
// once more forwarded by its supervisor. Reproduced two runs in
|
|
540
|
+
// three: the replacement died and the supervisor exited 1 on a
|
|
541
|
+
// `stratus service stop` after a restart. SIGKILL still ends the
|
|
542
|
+
// process at once.
|
|
543
|
+
process.on('SIGTERM', stopSignal);
|
|
544
|
+
process.on('SIGINT', stopSignal);
|
|
545
|
+
void shutdownRequested.then(shutdown);
|
|
546
|
+
if (env.shutdownSignal?.aborted) {
|
|
547
|
+
shutdown();
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
env.shutdownSignal?.addEventListener('abort', shutdown, { once: true });
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
// Decided now, before the stop below: a restart's drain can still be
|
|
554
|
+
// running when a stop signal releases this wait, and `onRestart` then
|
|
555
|
+
// fires during that stop. A daemon told to stop does not come back,
|
|
556
|
+
// whatever it was told before.
|
|
557
|
+
if (!restart) {
|
|
558
|
+
stopRequested = true;
|
|
559
|
+
}
|
|
560
|
+
if (restart) {
|
|
561
|
+
// Already stopped: the gateway drained and closed before handing the
|
|
562
|
+
// process over, and the stop() below finds nothing to do. Said here,
|
|
563
|
+
// while the structured log still takes writes — the next lines
|
|
564
|
+
// belong to a process that has none.
|
|
565
|
+
if (!restart.drained) {
|
|
566
|
+
// A turn that ignored its abort is still running in this process,
|
|
567
|
+
// possibly still writing sessions. Starting a fresh daemon beside
|
|
568
|
+
// it would be two processes on one store; exiting instead leaves
|
|
569
|
+
// the service manager, where there is one, to bring it back.
|
|
570
|
+
warn(`restart: something did not let go; exiting with status ${UNDRAINED_RESTART_EXIT_CODE} for the service manager to restart, instead of starting a new daemon beside it`);
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
log(`restarting stratusd${restart.reason ? ` (${restart.reason})` : ''}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
writeLine(streams.stdout, 'Stopping — draining in-flight turns.');
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
finally {
|
|
581
|
+
try {
|
|
582
|
+
await gateway.stop();
|
|
583
|
+
// Writes are queued and dropped on the hot path so logging never sits
|
|
584
|
+
// on a streamed token. That makes the tail of the log the part most
|
|
585
|
+
// likely to be lost — and the tail is the shutdown reason, the last
|
|
586
|
+
// warning, the line explaining a restart.
|
|
587
|
+
await logWriter?.flush();
|
|
588
|
+
}
|
|
589
|
+
finally {
|
|
590
|
+
// The shutdown is over, the flush included: a stop signal from here
|
|
591
|
+
// on is the process's to handle by default again — and a host that
|
|
592
|
+
// calls runServe repeatedly (the tests) must not accumulate handlers.
|
|
593
|
+
// Last, and under its own finally, because the listener is on the
|
|
594
|
+
// global process: taken off before the flush, a repeated signal
|
|
595
|
+
// landing during it would end the process by the default action
|
|
596
|
+
// (which a supervisor reports as a failure, and a service manager
|
|
597
|
+
// set to restart on failure would then bring back a daemon an
|
|
598
|
+
// operator just stopped); left on by a stop() that rejects (its last
|
|
599
|
+
// log line can throw through an injected stream), a settled handler
|
|
600
|
+
// would swallow the host's next signal.
|
|
601
|
+
if (stopSignal) {
|
|
602
|
+
process.off('SIGTERM', stopSignal);
|
|
603
|
+
process.off('SIGINT', stopSignal);
|
|
604
|
+
}
|
|
605
|
+
if (redirectTimer) {
|
|
606
|
+
clearInterval(redirectTimer);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
const bound = boundApiPort !== undefined && Number.isInteger(boundApiPort) && boundApiPort > 0
|
|
611
|
+
? { boundApiPort }
|
|
612
|
+
: {};
|
|
613
|
+
if (!restart || stopRequested) {
|
|
614
|
+
return { code: 0, ...bound, sessions: [] };
|
|
615
|
+
}
|
|
616
|
+
if (!restart.drained) {
|
|
617
|
+
(env.exitProcess ?? process.exit)(UNDRAINED_RESTART_EXIT_CODE);
|
|
618
|
+
return { code: UNDRAINED_RESTART_EXIT_CODE, ...bound, sessions: [] };
|
|
619
|
+
}
|
|
620
|
+
// Coming back: the browser sessions live at the stop go to the
|
|
621
|
+
// replacement — up the channel to the supervisor when this daemon has
|
|
622
|
+
// one, else with the outcome to the supervisor this process becomes.
|
|
623
|
+
const sessions = boundApi?.sessionsAtStop() ?? [];
|
|
624
|
+
if (supervised && sessions.length > 0) {
|
|
625
|
+
link.send({ type: 'stratusd.sessions', sessions });
|
|
626
|
+
}
|
|
627
|
+
// Asked to come back. Whether this process starts the next daemon or
|
|
628
|
+
// exits for its supervisor to is `runServe`'s decision, after the home
|
|
629
|
+
// is released: the claim is held until this returns, and a daemon
|
|
630
|
+
// started before that would be refused as a second one on the home.
|
|
631
|
+
return { code: RESTART_EXIT_CODE, ...bound, sessions };
|
|
632
|
+
};
|
|
633
|
+
//# sourceMappingURL=serve.js.map
|