@stratusagent/cli 0.12.6 → 0.13.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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/dist/approvals.d.ts +33 -13
  3. package/dist/approvals.d.ts.map +1 -1
  4. package/dist/approvals.js +188 -15
  5. package/dist/approvals.js.map +1 -1
  6. package/dist/autonomy.d.ts +14 -0
  7. package/dist/autonomy.d.ts.map +1 -1
  8. package/dist/autonomy.js +32 -4
  9. package/dist/autonomy.js.map +1 -1
  10. package/dist/commands/chat.d.ts.map +1 -1
  11. package/dist/commands/chat.js +5 -2
  12. package/dist/commands/chat.js.map +1 -1
  13. package/dist/commands/doctor.d.ts.map +1 -1
  14. package/dist/commands/doctor.js +54 -1
  15. package/dist/commands/doctor.js.map +1 -1
  16. package/dist/commands/grants.d.ts.map +1 -1
  17. package/dist/commands/grants.js +9 -2
  18. package/dist/commands/grants.js.map +1 -1
  19. package/dist/commands/serve.d.ts.map +1 -1
  20. package/dist/commands/serve.js +27 -14
  21. package/dist/commands/serve.js.map +1 -1
  22. package/dist/commands/setup.d.ts.map +1 -1
  23. package/dist/commands/setup.js +122 -2
  24. package/dist/commands/setup.js.map +1 -1
  25. package/dist/help.d.ts +1 -1
  26. package/dist/help.d.ts.map +1 -1
  27. package/dist/help.js +12 -8
  28. package/dist/help.js.map +1 -1
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +2 -2
  32. package/dist/index.js.map +1 -1
  33. package/dist/main.d.ts.map +1 -1
  34. package/dist/main.js +8 -3
  35. package/dist/main.js.map +1 -1
  36. package/dist/npm.d.ts +1 -1
  37. package/dist/npm.js +1 -1
  38. package/dist/parse.d.ts +1 -1
  39. package/dist/parse.d.ts.map +1 -1
  40. package/dist/parse.js +4 -4
  41. package/dist/parse.js.map +1 -1
  42. package/dist/runtime.d.ts.map +1 -1
  43. package/dist/runtime.js +5 -2
  44. package/dist/runtime.js.map +1 -1
  45. package/dist/trusted-config.d.ts +29 -0
  46. package/dist/trusted-config.d.ts.map +1 -1
  47. package/dist/trusted-config.js +69 -5
  48. package/dist/trusted-config.js.map +1 -1
  49. package/package.json +25 -25
@@ -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 gated calls when nobody is watching`, reserved),
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') {