@stratusagent/cli 0.11.1 → 0.11.2
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 +6 -1
- package/dist/index.d.ts +46 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +353 -18
- package/dist/index.js.map +1 -1
- package/package.json +22 -22
package/dist/index.js
CHANGED
|
@@ -13,14 +13,14 @@ import { mkdtemp, readdir, readFile, realpath, rm, stat, writeFile } from 'node:
|
|
|
13
13
|
import os from 'node:os';
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import { createInterface } from 'node:readline';
|
|
16
|
-
import { AgentRunner, AllowAllApprovalPolicy, EventBus, SkillRegistry, ToolRegistry, matchesSkillAllowlist, missingSkillRequirements, describeToolAllowlistFinding, unmatchedToolAllowlist, originOf, } from '@stratusagent/core';
|
|
16
|
+
import { AgentRunner, AllowAllApprovalPolicy, EventBus, SkillRegistry, ToolRegistry, isTrustLevel, matchesSkillAllowlist, escapeControlCharacters, memoryEntryTrust, missingSkillRequirements, describeToolAllowlistFinding, unmatchedToolAllowlist, originOf, TRUST_LEVELS, } from '@stratusagent/core';
|
|
17
17
|
import { createLocalCommandExecutor, } from '@stratusagent/executor-local';
|
|
18
18
|
import { loadPlugins } from '@stratusagent/plugins';
|
|
19
19
|
import { createFileCommandWhitelist, createPermissionPolicy, describeCommandScope, describeOriginScope, } from '@stratusagent/permissions';
|
|
20
20
|
import { DEFAULT_ANTHROPIC_MODEL, redactAnthropicRawTurns, } from '@stratusagent/provider-anthropic';
|
|
21
21
|
import { DEFAULT_CODEX_MODEL } from '@stratusagent/provider-codex';
|
|
22
22
|
import { canonicalDestination, createForgetTool, createRecallTool, createRememberTool, defineAgent, describeCadence, describeSchedule, FORGET_TOOL_NAME, MEMORY_TOOL_NAME, formatSoul, generateAgentName, isLoadableSkillId, isValidAgentId, parseSoul, GATEWAY_ONLY_TOOL_NAMES, } from '@stratusagent/agents';
|
|
23
|
-
import { agentsDirPath, claimSoulFile, collectAvailableModels as collectModels, createDemoTool, createFileCredentialResolver, createFileMemoryStore, createRuntimeProvider, credentialsPath, defaultApiKeyEnvName, DEFAULT_ANTHROPIC_BASE_URL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_MODEL, DEFAULT_STRATUS_AGENT, discoverActiveConfig, globalConfigPath, loadChannelCredentials, loadCredentials, loadNamedCredentials, discoverSkillsInDirectory, installSkillsFromDirectory, loadOperatorSkills, loadRosterSouls, skillsDirPath, listAgentSummaries, loadSoulFile, memoryFilePath, migrateLegacyMemory, parseProviderName, readNonEmptyString, readProcessEnv, resolveConfiguredSoul, logsDirPath, readWorkingDirectory, resolveAgentApprovals, DEFAULT_CONFIG_FILENAME, loadConfigFile, resolveConfigLocation, resolveRuntimeConfig as resolveStateRuntimeConfig, saveChannelCredentials, saveConfigFile, readTrustedConfigBlock, saveCredentials, saveNamedCredentials, servedRuntimes, CREDENTIAL_PROVIDER_NAMES, verifyProviderKey, withLegacyDefaultMemories, workspacesDirPath, gatewayInfoPath, gatewayTokenPath, newerStateMessage, pendingStateMigrations, readStateStamp, runStateMigrations, STATE_SCHEMA_VERSION, } from '@stratusagent/state';
|
|
23
|
+
import { agentsDirPath, claimSoulFile, collectAvailableModels as collectModels, createDemoTool, createFileCredentialResolver, createFileMemoryStore, createRuntimeProvider, credentialsPath, defaultApiKeyEnvName, DEFAULT_ANTHROPIC_BASE_URL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_MODEL, DEFAULT_STRATUS_AGENT, discoverActiveConfig, globalConfigPath, loadChannelCredentials, loadCredentials, loadNamedCredentials, discoverSkillsInDirectory, installSkillsFromDirectory, loadOperatorSkills, loadRosterSouls, skillsDirPath, listAgentSummaries, loadSoulFile, memoryFilePath, migrateLegacyMemory, parseProviderName, readNonEmptyString, readProcessEnv, resolveConfiguredSoul, logsDirPath, readWorkingDirectory, resolveAgentApprovals, resolveAgentPrincipals, DEFAULT_CONFIG_FILENAME, loadConfigFile, resolveConfigLocation, resolveRuntimeConfig as resolveStateRuntimeConfig, saveChannelCredentials, saveConfigFile, readTrustedConfigBlock, saveCredentials, saveNamedCredentials, servedRuntimes, CREDENTIAL_PROVIDER_NAMES, verifyProviderKey, withLegacyDefaultMemories, workspacesDirPath, gatewayInfoPath, gatewayTokenPath, newerStateMessage, pendingStateMigrations, readStateStamp, runStateMigrations, STATE_SCHEMA_VERSION, } from '@stratusagent/state';
|
|
24
24
|
import { installService, readServiceCommand, readServiceStatus, servicePlatform, serviceUnitPath, startService, stopService, uninstallService, } from "./service.js";
|
|
25
25
|
export { installService, launchdPlist, readServiceCommand, readServiceStatus, servicePlatform, serviceUnitPath, startService, stopService, systemdUnit, uninstallService, SERVICE_LABEL, } from "./service.js";
|
|
26
26
|
import { createLogWriter, currentLogPosition, truncateRedirectLogs, formatLogRecord, readRecentRecords, tailLog, } from "./logs.js";
|
|
@@ -133,7 +133,7 @@ const defaultPackageInstaller = async (packages) => {
|
|
|
133
133
|
* something other than this command.
|
|
134
134
|
*/
|
|
135
135
|
export const CREDENTIAL_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9._-]{0,127}$/;
|
|
136
|
-
export const CLI_VERSION = '0.11.
|
|
136
|
+
export const CLI_VERSION = '0.11.2';
|
|
137
137
|
const DASHBOARD_TITLE = 'Stratus Agent Dashboard';
|
|
138
138
|
/**
|
|
139
139
|
* The range every package declares in its `engines` field. Two floors, not
|
|
@@ -259,6 +259,9 @@ Usage:
|
|
|
259
259
|
stratus restart
|
|
260
260
|
stratus schedules
|
|
261
261
|
stratus schedules cancel <id>
|
|
262
|
+
stratus memory list ava
|
|
263
|
+
stratus memory reassert ava --trust user --all-unknown
|
|
264
|
+
stratus session rollover slack:ava:T01ABCDEF:D07GHIJKL
|
|
262
265
|
printf %s "$BRAVE_KEY" | stratus credential set search.apiKey
|
|
263
266
|
stratus credentials
|
|
264
267
|
stratus doctor
|
|
@@ -347,6 +350,24 @@ Commands:
|
|
|
347
350
|
cancel <id>" stops the next firing and revokes the
|
|
348
351
|
destination that was approved with it
|
|
349
352
|
(also: stratus schedule list / schedule cancel <id>)
|
|
353
|
+
memory list Show an agent's live memory with the trust label each
|
|
354
|
+
entry carries — user, agent, unknown (no recorded origin,
|
|
355
|
+
or written in a conversation with someone not configured
|
|
356
|
+
as a principal), external (recorded after reading content
|
|
357
|
+
from outside). --trust <level> filters; --format json
|
|
358
|
+
memory reassert Re-label entries as an operator: stratus memory reassert
|
|
359
|
+
<agent> --trust user <id>..., or --all-unknown for every
|
|
360
|
+
entry with no recorded origin (the upgrade case; an
|
|
361
|
+
entry recorded unknown — a stranger's message, a shell
|
|
362
|
+
command — is left for you to name by id). Appends
|
|
363
|
+
a record to ~/.stratus/memory.jsonl; a running daemon
|
|
364
|
+
sees it on its next turn. The only way a label ever
|
|
365
|
+
rises — no tool can do this
|
|
366
|
+
session rollover Start a conversation over under the same session id,
|
|
367
|
+
archiving its transcript so far — for a long-lived
|
|
368
|
+
session (a Slack DM) that predates provenance tracking
|
|
369
|
+
and would otherwise write unknown forever. Asks the
|
|
370
|
+
running daemon (--gateway, --token)
|
|
350
371
|
agent new Create an agent identity (generates a human-ish name + avatar theme)
|
|
351
372
|
agents List your agents: who they are, where their souls live, what
|
|
352
373
|
they run on, what they remember (also: stratus agent list).
|
|
@@ -392,9 +413,12 @@ Options:
|
|
|
392
413
|
--port dashboard: port for a daemon it starts (default: 4123)
|
|
393
414
|
--host dashboard: host for a daemon it starts (default: 127.0.0.1)
|
|
394
415
|
--no-open Do not open the browser automatically
|
|
395
|
-
--gateway agents / skill reload / restart: a running
|
|
396
|
-
API (
|
|
416
|
+
--gateway agents / skill reload / restart / session rollover: a running
|
|
417
|
+
daemon's control API (all but agents default to the daemon
|
|
397
418
|
~/.stratus/gateway.json names)
|
|
419
|
+
--trust memory list: show only this label; memory reassert: the
|
|
420
|
+
label to record (user, agent, unknown, external)
|
|
421
|
+
--all-unknown memory reassert: every live entry with no recorded origin
|
|
398
422
|
--token Bearer token for --gateway (default: ~/.stratus/gateway-token)
|
|
399
423
|
--no-reload skill add: install without reloading a running daemon
|
|
400
424
|
--reason restart: why, for the daemon's log
|
|
@@ -1046,6 +1070,123 @@ export const parseCommand = (argv, env = {}) => {
|
|
|
1046
1070
|
...(reload ? {} : { reload }),
|
|
1047
1071
|
};
|
|
1048
1072
|
}
|
|
1073
|
+
if (command === 'memory') {
|
|
1074
|
+
const [action, ...memoryRest] = rest;
|
|
1075
|
+
if (action === undefined || action === '--help' || action === '-h') {
|
|
1076
|
+
return { command: 'help' };
|
|
1077
|
+
}
|
|
1078
|
+
if (action !== 'list' && action !== 'reassert') {
|
|
1079
|
+
throw new Error(`Unknown memory subcommand: ${action}. Try: stratus memory list <agent>, stratus memory reassert <agent> --trust user <id>...`);
|
|
1080
|
+
}
|
|
1081
|
+
let agentId;
|
|
1082
|
+
let trust;
|
|
1083
|
+
let format = 'text';
|
|
1084
|
+
let allUnknown = false;
|
|
1085
|
+
const ids = [];
|
|
1086
|
+
for (let index = 0; index < memoryRest.length; index += 1) {
|
|
1087
|
+
const token = memoryRest[index];
|
|
1088
|
+
if (!token) {
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
if (token === '--help' || token === '-h') {
|
|
1092
|
+
return { command: 'help' };
|
|
1093
|
+
}
|
|
1094
|
+
if (token === '--trust') {
|
|
1095
|
+
const value = readOptionValue(memoryRest, index, '--trust');
|
|
1096
|
+
if (!isTrustLevel(value)) {
|
|
1097
|
+
throw new Error(`Unsupported trust level: ${value}. Use one of ${TRUST_LEVELS.join(', ')}.`);
|
|
1098
|
+
}
|
|
1099
|
+
trust = value;
|
|
1100
|
+
index += 1;
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
if (token === '--format') {
|
|
1104
|
+
const value = readOptionValue(memoryRest, index, '--format');
|
|
1105
|
+
if (value !== 'text' && value !== 'json') {
|
|
1106
|
+
throw new Error(`Unsupported format: ${value}`);
|
|
1107
|
+
}
|
|
1108
|
+
format = value;
|
|
1109
|
+
index += 1;
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
if (token === '--all-unknown' && action === 'reassert') {
|
|
1113
|
+
allUnknown = true;
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
if (token.startsWith('--')) {
|
|
1117
|
+
throw new Error(`Unknown option: ${token}`);
|
|
1118
|
+
}
|
|
1119
|
+
if (agentId === undefined) {
|
|
1120
|
+
agentId = token;
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
if (action === 'reassert') {
|
|
1124
|
+
ids.push(token);
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
throw new Error(`Unexpected argument: ${token}. Try: stratus memory list <agent>`);
|
|
1128
|
+
}
|
|
1129
|
+
if (!agentId) {
|
|
1130
|
+
throw new Error(`memory ${action} needs the agent id: stratus memory ${action} <agent>${action === 'reassert' ? ' --trust user <id>...' : ''}.`);
|
|
1131
|
+
}
|
|
1132
|
+
if (action === 'reassert') {
|
|
1133
|
+
if (trust === undefined) {
|
|
1134
|
+
throw new Error('memory reassert needs --trust <user|agent|external|unknown>: the label you are vouching for.');
|
|
1135
|
+
}
|
|
1136
|
+
if (ids.length === 0 && !allUnknown) {
|
|
1137
|
+
throw new Error('memory reassert needs entry ids, or --all-unknown to re-label every entry that has no recorded origin.');
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return {
|
|
1141
|
+
command: 'memory',
|
|
1142
|
+
action,
|
|
1143
|
+
agentId,
|
|
1144
|
+
...(trust !== undefined ? { trust } : {}),
|
|
1145
|
+
ids,
|
|
1146
|
+
allUnknown,
|
|
1147
|
+
format,
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
if (command === 'session') {
|
|
1151
|
+
const [action, ...sessionRest] = rest;
|
|
1152
|
+
if (action === undefined || action === '--help' || action === '-h') {
|
|
1153
|
+
return { command: 'help' };
|
|
1154
|
+
}
|
|
1155
|
+
if (action !== 'rollover') {
|
|
1156
|
+
throw new Error(`Unknown session subcommand: ${action}. Try: stratus session rollover <session-id>`);
|
|
1157
|
+
}
|
|
1158
|
+
const parsed = { command: 'session', action: 'rollover', sessionId: '' };
|
|
1159
|
+
for (let index = 0; index < sessionRest.length; index += 1) {
|
|
1160
|
+
const token = sessionRest[index];
|
|
1161
|
+
if (!token) {
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
if (token === '--help' || token === '-h') {
|
|
1165
|
+
return { command: 'help' };
|
|
1166
|
+
}
|
|
1167
|
+
if (token === '--gateway') {
|
|
1168
|
+
parsed.gateway = readOptionValue(sessionRest, index, '--gateway');
|
|
1169
|
+
index += 1;
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
if (token === '--token') {
|
|
1173
|
+
parsed.token = readOptionValue(sessionRest, index, '--token');
|
|
1174
|
+
index += 1;
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
if (token.startsWith('--')) {
|
|
1178
|
+
throw new Error(`Unknown option: ${token}`);
|
|
1179
|
+
}
|
|
1180
|
+
if (parsed.sessionId.length > 0) {
|
|
1181
|
+
throw new Error(`Unexpected argument: ${token}. Try: stratus session rollover <session-id>`);
|
|
1182
|
+
}
|
|
1183
|
+
parsed.sessionId = token;
|
|
1184
|
+
}
|
|
1185
|
+
if (parsed.sessionId.length === 0) {
|
|
1186
|
+
throw new Error('session rollover needs the session id: stratus session rollover <session-id>. `stratus logs` shows ids in its last column.');
|
|
1187
|
+
}
|
|
1188
|
+
return parsed;
|
|
1189
|
+
}
|
|
1049
1190
|
if (command === 'restart') {
|
|
1050
1191
|
const parsed = { command: 'restart' };
|
|
1051
1192
|
for (let index = 0; index < rest.length; index += 1) {
|
|
@@ -1291,6 +1432,8 @@ export const formatEvent = (event) => {
|
|
|
1291
1432
|
return `• session.completed ${event.sessionId}`;
|
|
1292
1433
|
case 'session.failed':
|
|
1293
1434
|
return `• session.failed ${event.error}`;
|
|
1435
|
+
case 'session.tainted':
|
|
1436
|
+
return `• session.tainted ${event.trust} (${event.source})`;
|
|
1294
1437
|
default:
|
|
1295
1438
|
return null;
|
|
1296
1439
|
}
|
|
@@ -1341,6 +1484,11 @@ export const eventDetail = (event) => {
|
|
|
1341
1484
|
};
|
|
1342
1485
|
case 'session.failed':
|
|
1343
1486
|
return { error: event.error };
|
|
1487
|
+
case 'session.tainted':
|
|
1488
|
+
// The label and what lowered it — a tool's name, or `memory`,
|
|
1489
|
+
// `sender`, `legacy`. Never the content that did: same rule as every
|
|
1490
|
+
// other event here.
|
|
1491
|
+
return { trust: event.trust, source: event.source };
|
|
1344
1492
|
default:
|
|
1345
1493
|
return undefined;
|
|
1346
1494
|
}
|
|
@@ -4301,6 +4449,37 @@ const loadServeApprovals = async (env, configPath, warn) => {
|
|
|
4301
4449
|
* `agentIds` is therefore the agents that can actually be *asked*, not
|
|
4302
4450
|
* every agent with tokens on disk.
|
|
4303
4451
|
*/
|
|
4452
|
+
/**
|
|
4453
|
+
* The `principals` block, read under the same trust rule as `approvals` and
|
|
4454
|
+
* for a sharper reason: it names the people whose messages an agent takes
|
|
4455
|
+
* as its operator's, which is the trust root everything downstream lowers
|
|
4456
|
+
* from. A cloned repo appointing itself the principal would arrive as
|
|
4457
|
+
* `user` in every Slack thread the daemon serves.
|
|
4458
|
+
*/
|
|
4459
|
+
const loadServePrincipals = async (env, configPath, warn) => {
|
|
4460
|
+
const block = await readTrustedConfigBlock('principals', env, configPath);
|
|
4461
|
+
if (block.status === 'untrusted') {
|
|
4462
|
+
warn(`ignoring the principals config in ${block.path}: a project-local config cannot decide whose messages `
|
|
4463
|
+
+ 'this daemon\'s agents treat as their operator\'s. Move it to ~/.stratus/config.json, or pass it with --config.');
|
|
4464
|
+
return {};
|
|
4465
|
+
}
|
|
4466
|
+
if (block.status === 'unreadable') {
|
|
4467
|
+
warn(`ignoring the principals config (${block.error instanceof Error ? block.error.message : String(block.error)}); every Slack sender is unknown`);
|
|
4468
|
+
return {};
|
|
4469
|
+
}
|
|
4470
|
+
return block.status === 'present' ? block.value : {};
|
|
4471
|
+
};
|
|
4472
|
+
/** One line saying whose messages arrive as the operator's, per Slack agent. */
|
|
4473
|
+
const describePrincipals = (principals, agentIds) => {
|
|
4474
|
+
const covered = agentIds.filter((agentId) => (resolveAgentPrincipals(principals, agentId).slackUsers ?? []).length > 0);
|
|
4475
|
+
if (covered.length === 0) {
|
|
4476
|
+
return 'no principals configured, so every Slack sender is unknown and every fact written in Slack carries that label — set principals.slackUsers in ~/.stratus/config.json';
|
|
4477
|
+
}
|
|
4478
|
+
const uncovered = agentIds.filter((agentId) => !covered.includes(agentId));
|
|
4479
|
+
return uncovered.length === 0
|
|
4480
|
+
? `principals set for ${covered.join(', ')}`
|
|
4481
|
+
: `principals set for ${covered.join(', ')}; none for ${uncovered.join(', ')}, whose Slack senders are all unknown`;
|
|
4482
|
+
};
|
|
4304
4483
|
const describeApprovers = (approvals, agentIds) => {
|
|
4305
4484
|
if (agentIds.length === 0) {
|
|
4306
4485
|
return 'but no channel is running to ask through, so gated calls will wait out the approval timeout and then be denied';
|
|
@@ -5142,6 +5321,134 @@ export const runSkillReload = async (command, streams, env = {}) => {
|
|
|
5142
5321
|
writeLine(streams.stdout, `reloaded skills in the daemon at ${base} — ${ids.length} skill(s) serving${ids.length > 0 ? `: ${ids.join(', ')}` : ''}`);
|
|
5143
5322
|
return 0;
|
|
5144
5323
|
};
|
|
5324
|
+
/**
|
|
5325
|
+
* `stratus memory list|reassert` — the operator's half of provenance.
|
|
5326
|
+
*
|
|
5327
|
+
* `list` shows what an agent's store holds with the label each entry
|
|
5328
|
+
* carries, because an operator cannot re-assert what they cannot see.
|
|
5329
|
+
* `reassert` is the one way a label ever rises: it appends a re-assertion
|
|
5330
|
+
* record to the JSONL (never rewrites a line), so a running daemon — which
|
|
5331
|
+
* re-reads the file on every read — sees it at once. Operator only, on
|
|
5332
|
+
* purpose: no tool exposes this, because an agent re-labelling its own
|
|
5333
|
+
* memory as trusted is the attack writing its own permission slip.
|
|
5334
|
+
*
|
|
5335
|
+
* `--all-unknown` exists for the upgrade: every entry written before labels
|
|
5336
|
+
* existed reads `unknown`, and the injected slice of such a store makes
|
|
5337
|
+
* every session `unknown` on its first turn. Re-asserting the entries the
|
|
5338
|
+
* agent actually surfaces, once, is the bounded work that drains it.
|
|
5339
|
+
*/
|
|
5340
|
+
export const runMemory = async (command, streams, env = {}) => {
|
|
5341
|
+
// Only the writer folds a legacy per-directory store in: `list` is the
|
|
5342
|
+
// read-only command a downgraded build is allowed to run against newer
|
|
5343
|
+
// state, and a migration is a write.
|
|
5344
|
+
if (command.action === 'reassert') {
|
|
5345
|
+
await migrateLegacyMemory(env);
|
|
5346
|
+
}
|
|
5347
|
+
const store = withLegacyDefaultMemories(createFileMemoryStore(memoryFilePath(env)));
|
|
5348
|
+
// Two ways an entry reads `unknown`, and only one is the upgrade case:
|
|
5349
|
+
// no label at all (written before labels existed, or added by hand), or a
|
|
5350
|
+
// recorded `unknown` — a session that heard from a sender nobody vouched
|
|
5351
|
+
// for, or ran a shell command. The bulk re-assertion is for the first;
|
|
5352
|
+
// the second may hold a stranger's text and is named by id, once read.
|
|
5353
|
+
const unlabelled = (entry) => entry.trust === undefined;
|
|
5354
|
+
const describeEntry = (entry) => ({
|
|
5355
|
+
id: entry.id,
|
|
5356
|
+
trust: memoryEntryTrust(entry),
|
|
5357
|
+
content: entry.content,
|
|
5358
|
+
createdAt: entry.createdAt,
|
|
5359
|
+
...(entry.origin ? { origin: entry.origin } : {}),
|
|
5360
|
+
});
|
|
5361
|
+
if (command.action === 'list') {
|
|
5362
|
+
const live = (await store.list(command.agentId)).entries
|
|
5363
|
+
.filter((entry) => command.trust === undefined || memoryEntryTrust(entry) === command.trust);
|
|
5364
|
+
if (command.format === 'json') {
|
|
5365
|
+
writeLine(streams.stdout, JSON.stringify({ agentId: command.agentId, entries: live.map(describeEntry) }, null, 2));
|
|
5366
|
+
return 0;
|
|
5367
|
+
}
|
|
5368
|
+
if (live.length === 0) {
|
|
5369
|
+
writeLine(streams.stdout, command.trust === undefined
|
|
5370
|
+
? `${command.agentId} has no live memory entries in ${memoryFilePath(env)}.`
|
|
5371
|
+
: `${command.agentId} has no live memory entries at ${command.trust}.`);
|
|
5372
|
+
return 0;
|
|
5373
|
+
}
|
|
5374
|
+
for (const entry of live) {
|
|
5375
|
+
const taintedBy = entry.origin?.taintedBy ? ` (tainted by ${entry.origin.taintedBy})` : '';
|
|
5376
|
+
const unrecorded = unlabelled(entry) ? ' (no recorded origin)' : '';
|
|
5377
|
+
writeLine(streams.stdout, `${entry.id} [${memoryEntryTrust(entry)}]${taintedBy}${unrecorded}`);
|
|
5378
|
+
writeLine(streams.stdout, ` ${escapeControlCharacters(entry.content)}`);
|
|
5379
|
+
}
|
|
5380
|
+
if (command.trust === undefined) {
|
|
5381
|
+
const unlabelledCount = live.filter(unlabelled).length;
|
|
5382
|
+
const recordedUnknown = live.filter((entry) => !unlabelled(entry) && memoryEntryTrust(entry) === 'unknown').length;
|
|
5383
|
+
if (unlabelledCount > 0) {
|
|
5384
|
+
writeLine(streams.stdout, '');
|
|
5385
|
+
writeLine(streams.stdout, `${unlabelledCount} entr${unlabelledCount === 1 ? 'y has' : 'ies have'} no recorded origin, so any session that reads one writes unknown. `
|
|
5386
|
+
+ `Review them, then: stratus memory reassert ${command.agentId} --trust user --all-unknown (or name ids).`);
|
|
5387
|
+
}
|
|
5388
|
+
if (recordedUnknown > 0) {
|
|
5389
|
+
writeLine(streams.stdout, '');
|
|
5390
|
+
writeLine(streams.stdout, `${recordedUnknown} entr${recordedUnknown === 1 ? 'y was' : 'ies were'} recorded unknown — written after a message from someone not configured as a principal, or after a shell command — `
|
|
5391
|
+
+ 'and may repeat what a stranger said. --all-unknown leaves these alone; re-assert one by id once you have read it.');
|
|
5392
|
+
}
|
|
5393
|
+
}
|
|
5394
|
+
return 0;
|
|
5395
|
+
}
|
|
5396
|
+
if (!store.reassertTrust) {
|
|
5397
|
+
writeLine(streams.stderr, 'Error: this memory store cannot re-assert trust.');
|
|
5398
|
+
return 1;
|
|
5399
|
+
}
|
|
5400
|
+
const trust = command.trust ?? 'user';
|
|
5401
|
+
const live = (await store.list(command.agentId)).entries;
|
|
5402
|
+
const targets = command.allUnknown
|
|
5403
|
+
? [...new Set([...live.filter(unlabelled).map((entry) => entry.id), ...command.ids])]
|
|
5404
|
+
: command.ids;
|
|
5405
|
+
if (targets.length === 0) {
|
|
5406
|
+
writeLine(streams.stdout, `${command.agentId} has no live entries with no recorded origin; nothing to re-assert. An entry recorded unknown is named by id.`);
|
|
5407
|
+
return 0;
|
|
5408
|
+
}
|
|
5409
|
+
const missing = [];
|
|
5410
|
+
let changed = 0;
|
|
5411
|
+
for (const id of targets) {
|
|
5412
|
+
if (await store.reassertTrust(command.agentId, id, trust)) {
|
|
5413
|
+
changed += 1;
|
|
5414
|
+
}
|
|
5415
|
+
else {
|
|
5416
|
+
missing.push(id);
|
|
5417
|
+
}
|
|
5418
|
+
}
|
|
5419
|
+
if (command.format === 'json') {
|
|
5420
|
+
writeLine(streams.stdout, JSON.stringify({ agentId: command.agentId, trust, reasserted: changed, missing }, null, 2));
|
|
5421
|
+
}
|
|
5422
|
+
else {
|
|
5423
|
+
writeLine(streams.stdout, `Re-asserted ${changed} entr${changed === 1 ? 'y' : 'ies'} of ${command.agentId} as ${trust}. A running daemon reads the change on its next turn.`);
|
|
5424
|
+
}
|
|
5425
|
+
for (const id of missing) {
|
|
5426
|
+
writeLine(streams.stderr, `No live memory entry with id ${id} belongs to ${command.agentId}; nothing was re-asserted for it.`);
|
|
5427
|
+
}
|
|
5428
|
+
return missing.length === 0 ? 0 : 1;
|
|
5429
|
+
};
|
|
5430
|
+
/**
|
|
5431
|
+
* `stratus session rollover <id>` — start a conversation over under the
|
|
5432
|
+
* same id, archiving its transcript so far. The daemon does the work, on
|
|
5433
|
+
* the session's own chain, so a turn cannot interleave; this only asks.
|
|
5434
|
+
*/
|
|
5435
|
+
export const runSessionRollover = async (command, streams, env = {}) => {
|
|
5436
|
+
const base = await runningGatewayBase(env, command);
|
|
5437
|
+
if (!base) {
|
|
5438
|
+
writeLine(streams.stderr, `Error: ${noRunningDaemonMessage(env)}`);
|
|
5439
|
+
return 1;
|
|
5440
|
+
}
|
|
5441
|
+
const response = await callRunningGateway(env, command, base, `/api/v1/sessions/${encodeURIComponent(command.sessionId)}/rollover`);
|
|
5442
|
+
if (!response.ok) {
|
|
5443
|
+
writeLine(streams.stderr, `Error: ${await gatewayErrorMessage(response)}`);
|
|
5444
|
+
return 1;
|
|
5445
|
+
}
|
|
5446
|
+
const outcome = await response.json();
|
|
5447
|
+
writeLine(streams.stdout, `Rolled over ${outcome.sessionId ?? command.sessionId}: the next message starts a fresh conversation, `
|
|
5448
|
+
+ `and the transcript so far is archived as ${outcome.archivedAs ?? '(unknown)'}.`);
|
|
5449
|
+
writeLine(streams.stdout, 'A fresh session still reads the memory it injects — if that slice holds entries with no recorded origin, `stratus memory list <agent>` shows them and `stratus memory reassert` re-labels them.');
|
|
5450
|
+
return 0;
|
|
5451
|
+
};
|
|
5145
5452
|
export const runRestart = async (command, streams, env = {}) => {
|
|
5146
5453
|
const base = await runningGatewayBase(env, command);
|
|
5147
5454
|
if (!base) {
|
|
@@ -6022,6 +6329,7 @@ const serveHeldHome = async (command, streams, env, createGateway) => {
|
|
|
6022
6329
|
// approve this" differently from "is anyone being asked at all".
|
|
6023
6330
|
const approvalsConfig = await loadServeApprovals(env, command.configPath, warn);
|
|
6024
6331
|
const approvalMode = command.approvals ?? approvalsConfig.mode ?? 'headless';
|
|
6332
|
+
const principalsConfig = await loadServePrincipals(env, command.configPath, warn);
|
|
6025
6333
|
// Read here rather than inside the gateway for the same reason as the two
|
|
6026
6334
|
// blocks above: the trust boundary is a property of *which file* said it,
|
|
6027
6335
|
// and this is where the file precedence is already understood.
|
|
@@ -6102,12 +6410,14 @@ const serveHeldHome = async (command, streams, env, createGateway) => {
|
|
|
6102
6410
|
channels.push(adapter({
|
|
6103
6411
|
agents: slackAgents.map(([agentId, tokens]) => {
|
|
6104
6412
|
const route = resolveAgentApprovals(approvalsConfig, agentId);
|
|
6413
|
+
const principals = resolveAgentPrincipals(principalsConfig, agentId);
|
|
6105
6414
|
return {
|
|
6106
6415
|
agentId,
|
|
6107
6416
|
appToken: tokens.appToken,
|
|
6108
6417
|
botToken: tokens.botToken,
|
|
6109
6418
|
...(route.slackApprovers ? { approvers: route.slackApprovers } : {}),
|
|
6110
6419
|
...(route.slackChannel ? { approvalChannel: route.slackChannel } : {}),
|
|
6420
|
+
...(principals.slackUsers ? { principals: principals.slackUsers } : {}),
|
|
6111
6421
|
};
|
|
6112
6422
|
}),
|
|
6113
6423
|
log,
|
|
@@ -6214,6 +6524,12 @@ const serveHeldHome = async (command, streams, env, createGateway) => {
|
|
|
6214
6524
|
const askable = slackAdapterUp ? slackAgents.map(([agentId]) => agentId) : [];
|
|
6215
6525
|
log(`approvals: remote — gated calls are parked and asked in Slack (${describeApprovers(approvalsConfig, askable)})`);
|
|
6216
6526
|
}
|
|
6527
|
+
if (slackAdapterUp) {
|
|
6528
|
+
// Said at startup, because the failure it names is silent otherwise: a
|
|
6529
|
+
// daemon with no principals serves every Slack message as `unknown`,
|
|
6530
|
+
// and the operator finds out when their agent's memory reads that way.
|
|
6531
|
+
log(`provenance: ${describePrincipals(principalsConfig, slackAgents.map(([agentId]) => agentId))}`);
|
|
6532
|
+
}
|
|
6217
6533
|
/**
|
|
6218
6534
|
* The one signal handler this daemon has, for the whole of its life —
|
|
6219
6535
|
* installed by the wait below, removed in the finally after the drain.
|
|
@@ -6519,6 +6835,17 @@ export const runCli = async ({ argv, streams = process, env = {} }) => {
|
|
|
6519
6835
|
// to be able to report what is pending rather than having just done it.
|
|
6520
6836
|
if (command.command !== 'update') {
|
|
6521
6837
|
const stamp = await readStateStamp(resolvedEnv);
|
|
6838
|
+
const writesState = command.command === 'serve'
|
|
6839
|
+
|| command.command === 'setup'
|
|
6840
|
+
|| command.command === 'chat'
|
|
6841
|
+
|| command.command === 'run'
|
|
6842
|
+
|| command.command === 'skill-add'
|
|
6843
|
+
|| command.command === 'dashboard'
|
|
6844
|
+
|| (command.command === 'credential' && command.action !== 'list')
|
|
6845
|
+
|| (command.command === 'schedules' && command.action === 'cancel')
|
|
6846
|
+
|| (command.command === 'memory' && command.action === 'reassert')
|
|
6847
|
+
|| command.command === 'session'
|
|
6848
|
+
|| (command.command === 'service' && (command.action === 'install' || command.action === 'start'));
|
|
6522
6849
|
if (stamp.schemaVersion > STATE_SCHEMA_VERSION) {
|
|
6523
6850
|
// Anything that writes under ~/.stratus refuses, not only the
|
|
6524
6851
|
// daemon: a downgraded build's setup, chat, or run can discard
|
|
@@ -6528,15 +6855,6 @@ export const runCli = async ({ argv, streams = process, env = {} }) => {
|
|
|
6528
6855
|
// diagnoses their way OUT of this state; so do `service stop`,
|
|
6529
6856
|
// `status`, and `uninstall`, for the same reason. (`agent new`
|
|
6530
6857
|
// only prints an identity — it writes nothing.)
|
|
6531
|
-
const writesState = command.command === 'serve'
|
|
6532
|
-
|| command.command === 'setup'
|
|
6533
|
-
|| command.command === 'chat'
|
|
6534
|
-
|| command.command === 'run'
|
|
6535
|
-
|| command.command === 'skill-add'
|
|
6536
|
-
|| command.command === 'dashboard'
|
|
6537
|
-
|| (command.command === 'credential' && command.action !== 'list')
|
|
6538
|
-
|| (command.command === 'schedules' && command.action === 'cancel')
|
|
6539
|
-
|| (command.command === 'service' && (command.action === 'install' || command.action === 'start'));
|
|
6540
6858
|
if (writesState) {
|
|
6541
6859
|
writeLine(streams.stderr, newerStateMessage(stamp.schemaVersion));
|
|
6542
6860
|
writeLine(streams.stderr, `Refusing \`stratus ${command.command}\` — it writes state the newer format owns. Read-only commands (logs, agents, doctor, service status/stop) still work.`);
|
|
@@ -6553,10 +6871,21 @@ export const runCli = async ({ argv, streams = process, env = {} }) => {
|
|
|
6553
6871
|
}
|
|
6554
6872
|
}
|
|
6555
6873
|
catch (error) {
|
|
6556
|
-
|
|
6557
|
-
//
|
|
6558
|
-
//
|
|
6559
|
-
|
|
6874
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
6875
|
+
// A command that writes state must not run on a home whose stamp
|
|
6876
|
+
// could not be written: the labels it would persist are what the
|
|
6877
|
+
// stamp protects, and a stamp still reading the old schema lets a
|
|
6878
|
+
// downgraded build in afterwards to read an `external` fact as
|
|
6879
|
+
// ordinary memory — the exact hole the stamp closes. A read-only
|
|
6880
|
+
// command warns and continues, because reading is how someone
|
|
6881
|
+
// diagnoses their way out; silence here is how the migrated and
|
|
6882
|
+
// unmigrated populations diverge.
|
|
6883
|
+
if (writesState) {
|
|
6884
|
+
writeLine(streams.stderr, `State migration failed (${reason}).`);
|
|
6885
|
+
writeLine(streams.stderr, `Refusing \`stratus ${command.command}\` — it writes state the migration stamps. Fix ~/.stratus (is state.json writable?) or run \`stratus update\`; read-only commands (logs, agents, doctor, memory list, service status/stop) still work.`);
|
|
6886
|
+
return 1;
|
|
6887
|
+
}
|
|
6888
|
+
writeLine(streams.stderr, `Warning: state migration failed (${reason}). Continuing on unmigrated state — \`stratus update\` retries it.`);
|
|
6560
6889
|
}
|
|
6561
6890
|
}
|
|
6562
6891
|
}
|
|
@@ -6592,6 +6921,12 @@ export const runCli = async ({ argv, streams = process, env = {} }) => {
|
|
|
6592
6921
|
if (command.command === 'schedules') {
|
|
6593
6922
|
return await runSchedules(command, streams, resolvedEnv);
|
|
6594
6923
|
}
|
|
6924
|
+
if (command.command === 'memory') {
|
|
6925
|
+
return await runMemory(command, streams, resolvedEnv);
|
|
6926
|
+
}
|
|
6927
|
+
if (command.command === 'session') {
|
|
6928
|
+
return await runSessionRollover(command, streams, resolvedEnv);
|
|
6929
|
+
}
|
|
6595
6930
|
if (command.command === 'doctor') {
|
|
6596
6931
|
return await runDoctor(command, streams, resolvedEnv);
|
|
6597
6932
|
}
|