@stratusagent/cli 0.12.6 → 0.13.1
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 +1 -1
- package/dist/approvals.d.ts +33 -13
- package/dist/approvals.d.ts.map +1 -1
- package/dist/approvals.js +188 -15
- package/dist/approvals.js.map +1 -1
- package/dist/autonomy.d.ts +14 -0
- package/dist/autonomy.d.ts.map +1 -1
- package/dist/autonomy.js +32 -4
- package/dist/autonomy.js.map +1 -1
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +5 -2
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +54 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/grants.d.ts.map +1 -1
- package/dist/commands/grants.js +9 -2
- package/dist/commands/grants.js.map +1 -1
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +27 -14
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +122 -2
- package/dist/commands/setup.js.map +1 -1
- package/dist/help.d.ts +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +12 -8
- package/dist/help.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +8 -3
- package/dist/main.js.map +1 -1
- package/dist/npm.d.ts +1 -1
- package/dist/npm.js +1 -1
- package/dist/parse.d.ts +1 -1
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +4 -4
- package/dist/parse.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +5 -2
- package/dist/runtime.js.map +1 -1
- package/dist/trusted-config.d.ts +29 -0
- package/dist/trusted-config.d.ts.map +1 -1
- package/dist/trusted-config.js +69 -5
- package/dist/trusted-config.js.map +1 -1
- package/package.json +25 -25
package/dist/commands/setup.js
CHANGED
|
@@ -5,7 +5,7 @@ import { preflightPlugin, readPluginManifest, PluginConfigError, PluginManifestE
|
|
|
5
5
|
import { DEFAULT_ANTHROPIC_MODEL } from '@stratusagent/provider-anthropic';
|
|
6
6
|
import { DEFAULT_CODEX_MODEL } from '@stratusagent/providers';
|
|
7
7
|
import { formatSoul, parseSoul } from '@stratusagent/agents';
|
|
8
|
-
import { claimSoulFile, collectAvailableModels as collectModels, credentialsPath, defaultApiKeyEnvName, DEFAULT_ANTHROPIC_BASE_URL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_MODEL, DEFAULT_STRATUS_AGENT, globalConfigPath, loadChannelCredentials, loadChannelTransportSecrets, loadCredentials, loadRosterSouls, loadSoulFile, isRegisteredProviderName, languageFor, readNonEmptyString, registeredProviderNameOf, readProcessEnv, readWorkingDirectory, resolveAgentApprovals, resolveRuntimeConfig, DEFAULT_CONFIG_FILENAME, loadConfigFile, saveChannelCredentials, saveChannelTransportSecrets, removeChannelTransportSecrets, CREDENTIAL_NAME_PATTERN, saveConfigFile, saveCredentials, CREDENTIAL_PROVIDER_NAMES, verifyProviderKey, } from '@stratusagent/state';
|
|
8
|
+
import { claimSoulFile, collectAvailableModels as collectModels, credentialsPath, defaultApiKeyEnvName, DEFAULT_ANTHROPIC_BASE_URL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_MODEL, DEFAULT_STRATUS_AGENT, globalConfigPath, loadChannelCredentials, loadChannelTransportSecrets, loadCredentials, loadRosterSouls, loadSoulFile, isRegisteredProviderName, languageFor, readNonEmptyString, registeredProviderNameOf, readProcessEnv, readWorkingDirectory, resolveAgentApprovals, resolveRuntimeConfig, DEFAULT_CONFIG_FILENAME, loadConfigFile, saveChannelCredentials, saveChannelTransportSecrets, removeChannelTransportSecrets, CREDENTIAL_NAME_PATTERN, saveConfigFile, saveCredentials, resolveAgentPrincipals, CREDENTIAL_PROVIDER_NAMES, verifyProviderKey, } from '@stratusagent/state';
|
|
9
9
|
import { installService, readServiceStatus, servicePlatform, serviceUnitPath, uninstallService, } from "../service.js";
|
|
10
10
|
import { classifyApprovalReach, unattendedReachParts } from "../approvals.js";
|
|
11
11
|
import { serviceEnvFor } from "../daemon.js";
|
|
@@ -884,6 +884,9 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
884
884
|
return;
|
|
885
885
|
}
|
|
886
886
|
const slack = { ...(state.channels.slack ?? {}) };
|
|
887
|
+
// The bot this agent had before, if any: evidence of which workspace
|
|
888
|
+
// its current list was written for.
|
|
889
|
+
const previousBotToken = slack[agentId]?.botToken;
|
|
887
890
|
slack[agentId] = { appToken, botToken };
|
|
888
891
|
state.channels = { ...state.channels, slack };
|
|
889
892
|
state.channelsDirty = true;
|
|
@@ -898,6 +901,123 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
898
901
|
const detail = bot.status === 'unreachable' ? bot.detail : app.detail;
|
|
899
902
|
writeLine(streams.stdout, `! Could not reach Slack to verify (${detail}). Saved the tokens anyway — \`${serveCommand()}\` will report on startup.`);
|
|
900
903
|
}
|
|
904
|
+
await askSlackPrincipals(agentId, agentName, bot.status === 'ok' ? bot.identity.teamId : undefined, fetchImpl, previousBotToken);
|
|
905
|
+
};
|
|
906
|
+
/**
|
|
907
|
+
* Whether another agent already on Slack uses the inherited top-level
|
|
908
|
+
* list in workspace `teamId`. Member IDs are workspace-scoped, so a list
|
|
909
|
+
* nobody can place there may name nobody this agent will ever hear from.
|
|
910
|
+
*/
|
|
911
|
+
const inheritingSlackTeams = async (agentId, fetchImpl) => {
|
|
912
|
+
const teams = [];
|
|
913
|
+
for (const [otherId, tokens] of Object.entries(state.channels.slack ?? {})) {
|
|
914
|
+
if (otherId === agentId || !tokens?.botToken || state.principals?.agents?.[otherId]?.slackUsers !== undefined) {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
const other = await verifySlackBotToken(tokens.botToken, fetchImpl);
|
|
918
|
+
// Unverifiable counts as another workspace: it can't be placed.
|
|
919
|
+
teams.push(other.status === 'ok' ? other.identity.teamId : undefined);
|
|
920
|
+
}
|
|
921
|
+
return teams;
|
|
922
|
+
};
|
|
923
|
+
/**
|
|
924
|
+
* Whether the list this agent resolves to is confirmed for workspace
|
|
925
|
+
* `teamId`: a verified bot in that team already uses it. Its users are
|
|
926
|
+
* this agent's previous bot and, for an inherited list, every other
|
|
927
|
+
* agent inheriting it. Nothing else says which workspace a list is for.
|
|
928
|
+
*/
|
|
929
|
+
const listConfirmedIn = async (agentId, own, teamId, fetchImpl, previousBotToken) => {
|
|
930
|
+
if (teamId === undefined) {
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
if (previousBotToken !== undefined) {
|
|
934
|
+
const previous = await verifySlackBotToken(previousBotToken, fetchImpl);
|
|
935
|
+
if (previous.status === 'ok' && previous.identity.teamId === teamId) {
|
|
936
|
+
return true;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return !own && (await inheritingSlackTeams(agentId, fetchImpl)).includes(teamId);
|
|
940
|
+
};
|
|
941
|
+
/**
|
|
942
|
+
* Whether a first list can be everyone's: every other Slack agent that
|
|
943
|
+
* would inherit it is verified into the same workspace (or there is none).
|
|
944
|
+
*/
|
|
945
|
+
const allInheritorsIn = async (agentId, teamId, fetchImpl) => {
|
|
946
|
+
const teams = await inheritingSlackTeams(agentId, fetchImpl);
|
|
947
|
+
return teams.length === 0 || (teamId !== undefined && teams.every((team) => team === teamId));
|
|
948
|
+
};
|
|
949
|
+
/**
|
|
950
|
+
* Only listed Slack users get a turn (`principals.admit` defaults to
|
|
951
|
+
* `principals`), so an agent connected with nobody listed ignores
|
|
952
|
+
* everyone. Asked right after the connect, the one moment the operator
|
|
953
|
+
* is in Slack's settings anyway. Skipped when it admits anyone, and when
|
|
954
|
+
* the list it resolves to (its own or inherited) is confirmed for this
|
|
955
|
+
* workspace (see listConfirmedIn). An unconfirmed list is offered with
|
|
956
|
+
* Enter to keep it, so a re-run never overwrites one silently; member
|
|
957
|
+
* IDs are workspace-scoped, so a list from another workspace names
|
|
958
|
+
* nobody, or someone else.
|
|
959
|
+
*/
|
|
960
|
+
const askSlackPrincipals = async (agentId, agentName, teamId, fetchImpl, previousBotToken) => {
|
|
961
|
+
const resolved = resolveAgentPrincipals(state.principals, agentId);
|
|
962
|
+
// `admit: "anyone"` opened the door on purpose: a list would only label
|
|
963
|
+
// trust there, and the closed-door wording below would be false.
|
|
964
|
+
if (resolved.admit === 'anyone') {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
const listed = resolved.slackUsers ?? [];
|
|
968
|
+
const own = state.principals?.agents?.[agentId]?.slackUsers !== undefined;
|
|
969
|
+
if (listed.length > 0 && await listConfirmedIn(agentId, own, teamId, fetchImpl, previousBotToken)) {
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
const inherited = listed.length > 0 ? listed.join(', ') : undefined;
|
|
973
|
+
writeLine(streams.stdout, `Only the Slack users you list can talk to ${agentName}; everyone else is ignored.`);
|
|
974
|
+
if (inherited !== undefined) {
|
|
975
|
+
const whose = own ? `${agentName}'s Slack list` : `The Slack list ${agentName} would inherit`;
|
|
976
|
+
writeLine(streams.stdout, `${whose} (${inherited}) is not confirmed for this workspace, and member IDs only work in their own.`);
|
|
977
|
+
}
|
|
978
|
+
writeLine(streams.stdout, 'Find your member ID in Slack: your profile → ⋯ → Copy member ID.');
|
|
979
|
+
for (;;) {
|
|
980
|
+
const answer = (await prompter.ask(inherited !== undefined
|
|
981
|
+
? `Slack member IDs who may talk to it here (U…, comma-separated, Enter to keep ${inherited}): `
|
|
982
|
+
: 'Slack member IDs who may talk to it (U…, comma-separated, Enter to skip): ')).trim();
|
|
983
|
+
// Separators alone (`, ,`) name nobody, the same as Enter.
|
|
984
|
+
const ids = answer.split(',').map((value) => value.trim()).filter((value) => value.length > 0);
|
|
985
|
+
if (ids.length === 0 && inherited !== undefined) {
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (ids.length === 0) {
|
|
989
|
+
// The key that decides this agent's list: its own, where it has one.
|
|
990
|
+
const key = state.principals?.agents?.[agentId]?.slackUsers !== undefined ? `principals.agents.${agentId}.slackUsers` : 'principals.slackUsers';
|
|
991
|
+
writeLine(streams.stdout, `! Nobody is listed, so ${agentName} will ignore every Slack message until ${key} names someone.`);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const bad = ids.filter((id) => !/^[UW][A-Z0-9]{2,}$/.test(id));
|
|
995
|
+
if (bad.length > 0) {
|
|
996
|
+
writeLine(streams.stdout, `✗ ${bad.join(', ')} ${bad.length === 1 ? 'is not a member ID' : 'are not member IDs'} (they look like U0123ABCD). Nothing was saved.`);
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
const principals = { ...(state.principals ?? {}) };
|
|
1000
|
+
const anyList = principals.slackUsers !== undefined
|
|
1001
|
+
|| Object.values(principals.agents ?? {}).some((entry) => entry?.slackUsers !== undefined);
|
|
1002
|
+
if (!anyList && await allInheritorsIn(agentId, teamId, fetchImpl)) {
|
|
1003
|
+
// The first list on this install is everyone's: the operator who
|
|
1004
|
+
// connects a second agent is almost always the same person. Once
|
|
1005
|
+
// any agent has a list, a new one is that agent's alone, so it
|
|
1006
|
+
// never opens an agent left unlisted on purpose; so is a first one
|
|
1007
|
+
// when another Slack agent is in (or can't be placed in) a
|
|
1008
|
+
// different workspace, since member IDs only mean something there.
|
|
1009
|
+
principals.slackUsers = ids;
|
|
1010
|
+
}
|
|
1011
|
+
else {
|
|
1012
|
+
principals.agents = {
|
|
1013
|
+
...(principals.agents ?? {}),
|
|
1014
|
+
[agentId]: { ...(principals.agents?.[agentId] ?? {}), slackUsers: ids },
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
state.principals = principals;
|
|
1018
|
+
writeLine(streams.stdout, `✓ ${ids.join(', ')} can talk to ${agentName}. Saved with the rest of setup.`);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
901
1021
|
};
|
|
902
1022
|
/**
|
|
903
1023
|
* Every agent the gateway would dispatch to: the built-in Stratus
|
|
@@ -1769,7 +1889,7 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
1769
1889
|
// wrapped row corrupts every redraw, because the rewind counts
|
|
1770
1890
|
// options rather than rendered rows. Being static is not being short.
|
|
1771
1891
|
const options = [
|
|
1772
|
-
fitMenuRow(`Headless${mode === 'headless' ? ' (current)' : ''} refuse
|
|
1892
|
+
fitMenuRow(`Headless${mode === 'headless' ? ' (current)' : ''} refuse calls that need a person's approval`, reserved),
|
|
1773
1893
|
fitMenuRow(`Ask in Slack${mode === 'remote' ? ' (current)' : ''} park the turn and ask an approver`, reserved),
|
|
1774
1894
|
];
|
|
1775
1895
|
if (mode === 'remote') {
|