@dotdrelle/wiki-manager 0.15.34 → 0.15.35
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/.env.example +8 -5
- package/agents.docker-compose.yml +11 -16
- package/docker-compose.yml +9 -8
- package/package.json +2 -2
- package/src/agent/graph.js +20 -0
- package/src/commands/slash.js +73 -10
- package/src/commands/slash.test.js +37 -3
- package/src/core/agentsCompose.js +86 -1
- package/src/core/buildInfo.json +2 -2
- package/src/core/compose.js +24 -2
- package/src/core/dockerCompose.test.js +18 -0
- package/src/core/googleGrants.js +38 -0
- package/src/core/googleGrants.test.js +59 -0
- package/src/core/mcp.js +1 -1
- package/src/core/modelFetch.js +241 -27
- package/src/core/modelFetch.test.js +213 -0
- package/src/core/profileServiceStatus.test.js +146 -0
- package/src/core/wikiSetup.js +11 -2
- package/src/core/wikiWorkspace.test.js +9 -3
- package/src/runtime/lifecycle.js +105 -3
- package/src/runtime/lifecycle.test.js +68 -0
- package/src/shell/SetupWizard.tsx +416 -116
- package/src/shell/repl.js +37 -11
- package/src/shell/setupWizardDiscovery.test.js +48 -0
- package/src/shell/setupWizardPlaceholders.test.js +15 -2
- package/src/shell/setupWizardSuggestions.test.js +36 -2
- package/src/shell/wrapText.js +57 -0
- package/src/shell/wrapText.test.js +48 -0
- package/wiki-workspace +115 -32
package/.env.example
CHANGED
|
@@ -57,12 +57,15 @@ CONNECTORS_MCP_AUTH_TOKEN=
|
|
|
57
57
|
CONNECTORS_ENABLED=false
|
|
58
58
|
#
|
|
59
59
|
# The wikiLLM Google OAuth application is baked into the agent-connectors image
|
|
60
|
-
# at build time, from agent-external/agent-connectors/.env.build.local.
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
60
|
+
# at build time, from agent-external/agent-connectors/.env.build.local. Neither
|
|
61
|
+
# value is a security boundary (public Desktop-type client); baking them is what
|
|
62
|
+
# makes the connector work out of the box.
|
|
63
|
+
#
|
|
64
|
+
# Set either key below to use YOUR OWN Google application instead — it overrides
|
|
65
|
+
# the baked default at runtime, no rebuild needed. LEAVE THEM COMMENTED to keep
|
|
66
|
+
# the baked one: an empty value here is forwarded as an empty value and disables
|
|
67
|
+
# the client entirely.
|
|
64
68
|
# GOOGLE_OAUTH_CLIENT_ID=
|
|
65
|
-
# Optional confidential-client compatibility override:
|
|
66
69
|
# GOOGLE_OAUTH_CLIENT_SECRET=
|
|
67
70
|
#
|
|
68
71
|
# The callback URL is generated automatically
|
|
@@ -94,22 +94,13 @@ services:
|
|
|
94
94
|
|
|
95
95
|
connectors:
|
|
96
96
|
profiles: [connectors]
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
build
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
# Without these the ARGs stay unset and the image ships empty
|
|
105
|
-
# WIKILLM_GOOGLE_OAUTH_* values, so the container ends up with no Google
|
|
106
|
-
# client at all. `wiki-workspace agents up` exports them from the
|
|
107
|
-
# connectors repo's .env.build.local before invoking Compose.
|
|
108
|
-
# Deliberately value-less: Compose then forwards each host variable only
|
|
109
|
-
# when it is defined. Never write `${VAR:-}` here — an empty
|
|
110
|
-
# --build-arg pins the ARG to the empty string.
|
|
111
|
-
WIKILLM_GOOGLE_OAUTH_CLIENT_ID:
|
|
112
|
-
WIKILLM_GOOGLE_OAUTH_CLIENT_SECRET:
|
|
97
|
+
# Image only, jamais de `build:` ici. Ce fichier est livré dans le paquet
|
|
98
|
+
# npm, où le dépôt agent-connectors n'existe pas : un contexte de build
|
|
99
|
+
# pointant vers `../agent-external/…` y est irrésolvable. La construction
|
|
100
|
+
# appartient à `build-and-push.sh`, qui cuit l'application OAuth wikiLLM
|
|
101
|
+
# dans l'image publiée. Pour utiliser sa propre application Google, on
|
|
102
|
+
# surcharge GOOGLE_OAUTH_CLIENT_ID / _SECRET dans le .env du manager — sans
|
|
103
|
+
# reconstruire quoi que ce soit.
|
|
113
104
|
image: dotdrelle/agent-connectors:latest
|
|
114
105
|
user: "${UID:-1000}:${GID:-1000}"
|
|
115
106
|
ports:
|
|
@@ -119,6 +110,10 @@ services:
|
|
|
119
110
|
- MCP_AUTH_TOKEN=${CONNECTORS_MCP_AUTH_TOKEN:-}
|
|
120
111
|
- WORKSPACES_ROOT=/workspaces
|
|
121
112
|
- AGENT_DATA_DIR=/data
|
|
113
|
+
# Vides par défaut, et c'est sans conséquence : l'application OAuth
|
|
114
|
+
# embarquée dans l'image est lue depuis un fichier, pas depuis
|
|
115
|
+
# l'environnement. Renseigner l'une de ces deux clés dans le .env du
|
|
116
|
+
# manager la fait gagner sur la valeur embarquée.
|
|
122
117
|
- GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID:-}
|
|
123
118
|
- GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET:-}
|
|
124
119
|
- GOOGLE_OAUTH_CALLBACK_URL=${GOOGLE_OAUTH_CALLBACK_URL:-}
|
package/docker-compose.yml
CHANGED
|
@@ -144,13 +144,14 @@ x-wiki-manager:
|
|
|
144
144
|
description: "Full workspace service set."
|
|
145
145
|
ui:
|
|
146
146
|
targets: [serve]
|
|
147
|
-
description: "
|
|
148
|
-
wiki:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
147
|
+
description: "Chat, wiki browser and Donna's web UI."
|
|
148
|
+
# Pas d'alias `wiki` ici : ce nom est deja celui d'un service Compose (le
|
|
149
|
+
# CLI one-shot de `/wiki run`). L'alias le masquait, et `/start wiki`
|
|
150
|
+
# demarrait mcp-http sans le dire.
|
|
151
|
+
#
|
|
152
|
+
# Pas d'alias `mcp` non plus : mcp-http fait partie du socle demarre par
|
|
153
|
+
# `all` et `services`, personne ne le demarre seul. Il reste adressable
|
|
154
|
+
# sous son nom Compose.
|
|
154
155
|
production:
|
|
155
156
|
targets: [production-mcp]
|
|
156
|
-
description: "
|
|
157
|
+
description: "Ingest, build, export and polish jobs."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdrelle/wiki-manager",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.35",
|
|
4
4
|
"description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
|
|
5
5
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
6
6
|
"author": "dotrelle",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
13
|
"start": "bun ./bin/wiki-manager.js",
|
|
14
|
-
"test": "node --test src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/agentsCompose.test.js src/core/buildInfo.test.js src/core/agentEvents.test.js src/core/runtimeLog.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikirc.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/orchestrator/agentRegistry.test.js src/orchestrator/capabilityRegistry.test.js src/orchestrator/capabilityResolver.test.js src/orchestrator/planValidator.test.js src/orchestrator/planIntegrator.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardPlaceholders.test.js src/shell/setupWizardSuggestions.test.js src/runtime/store.test.js src/runtime/controlMessages.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/donna-contract.test.js src/runtime/auth.test.js",
|
|
14
|
+
"test": "node --test src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/agentsCompose.test.js src/core/profileServiceStatus.test.js src/core/buildInfo.test.js src/core/agentEvents.test.js src/core/runtimeLog.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikirc.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/googleGrants.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/orchestrator/agentRegistry.test.js src/orchestrator/capabilityRegistry.test.js src/orchestrator/capabilityResolver.test.js src/orchestrator/planValidator.test.js src/orchestrator/planIntegrator.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardPlaceholders.test.js src/shell/setupWizardSuggestions.test.js src/shell/setupWizardDiscovery.test.js src/shell/wrapText.test.js src/runtime/lifecycle.test.js src/runtime/store.test.js src/runtime/controlMessages.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/donna-contract.test.js src/runtime/auth.test.js",
|
|
15
15
|
"check-versions": "node scripts/check-versions.js",
|
|
16
16
|
"prepack": "node scripts/check-versions.js",
|
|
17
17
|
"prepublishOnly": "node scripts/check-versions.js",
|
package/src/agent/graph.js
CHANGED
|
@@ -1019,12 +1019,32 @@ export function buildAgentSystemPrompt(state) {
|
|
|
1019
1019
|
? 'The runtime is connected and runtime__delegate is available for any requested capability action that has no matching direct tool. When a matching direct tool is offered, call it directly; otherwise let the runtime resolve the objective from discovered capability contracts.'
|
|
1020
1020
|
: 'No runtime is connected, so you cannot execute actions. State that plainly and name the runtime connection as the missing capability — do not invent a workaround.',
|
|
1021
1021
|
'If the connector or service needed for a requested read or action is absent from the Connected MCP tools above (its service is not running — e.g. CME, documents, or production), say plainly that this service is not connected and name it as the missing capability. Never redirect a simple read (e.g. "give me the CME config") to an "agent action", never invent its result, and never propose a workaround. Only requests you can actually serve with a listed tool are answered with data.',
|
|
1022
|
+
// Cas observé : « récupère ce mail » et « envoie un mail » refusés comme
|
|
1023
|
+
// impossibles, alors que l'agent connectors déclare `external-source.collect`
|
|
1024
|
+
// (écrit dans raw/untracked/) et `communication.send-email`. Les outils de
|
|
1025
|
+
// lecture directs ne rendent que des métadonnées : les prendre pour la
|
|
1026
|
+
// limite de l'agent transforme un travail délégable en refus.
|
|
1027
|
+
'The direct read tools of a connector are a preview, not the measure of what its agent can do. Bringing external content INTO the workspace (retrieve, import, fetch, save, "récupère") is a collect capability, and acting on the outside world (send, publish, notify) is an action capability: both are delegated through runtime__delegate, not answered from a read tool. Never conclude that something is impossible because the listed read tool returns only metadata — check the capabilities the agents declare, and delegate the objective as stated.',
|
|
1028
|
+
// Un droit manquant n'est pas une fonctionnalité absente : l'un se
|
|
1029
|
+
// réautorise en une commande, l'autre n'existe pas. Les confondre envoie
|
|
1030
|
+
// l'utilisateur croire que le produit ne sait pas faire.
|
|
1031
|
+
'When an action fails or is refused for lack of an authorization grant or scope (rather than a missing capability), say exactly that and name the primitive that grants it. Do not describe the feature as unavailable.',
|
|
1022
1032
|
'For an action with no matching direct tool, call runtime__delegate with the user objective only. The runtime chooses the capability, operation, agent and plan, including a validated single task for executor-only agents. Never choose those identifiers yourself. Never call <provider>__agent_plan, <provider>__agent_execute, legacy production__production_start_job, wiki__plan_set, or wiki__plan_done from interactive chat.',
|
|
1023
1033
|
'Do not ask the user which sources, files, connectors, or templates to use for an ingest, build, or export: the specialized agent discovers them from the workspace. When the objective is clear (e.g. "lance une ingestion"), delegate it as stated, without a clarifying question.',
|
|
1024
1034
|
'Promise only what the resolved capability actually exposes in its declared contract (the input schema the specialized agent publishes for that capability). When the user requests an execution parameter — a batch or chunk size, a count "N at a time", concurrency, ordering, priority, or any tuning knob — apply it only if that parameter exists in the target capability\'s published input schema. Otherwise do not confirm or promise it: delegate the objective, and if the user explicitly asked for that parameter, say plainly in one line that you started the work but do not control that aspect (the runtime and the specialized agent decide it). Never state or imply a parameter was applied when the agent contract cannot enforce it.',
|
|
1025
1035
|
'If runtime__delegate returns a blocker or no specialized provider is available, report only that concrete blocker concisely. Never replace the missing execution path with a suggested slash command, skill, MCP tool name, manual file move, administrator escalation, or alternative workflow unless the user explicitly asks for alternatives.',
|
|
1026
1036
|
'For workspace inventory and page listings, use the connected wiki MCP read tools. Never invent or call a /wiki shell command through shell__run_command. Use /workspace init <name> [path] for low-level non-interactive workspace creation; in the interactive TUI, /new <name> opens the setup wizard.',
|
|
1027
1037
|
'If an action requires tools or skills not available yet, explain the limitation and name the expected primitive.',
|
|
1038
|
+
// Les outils help_* étaient exposés — ils font partie de l'allow-list du
|
|
1039
|
+
// mode chat — mais rien dans ce prompt ne disait qu'ils sont LA source des
|
|
1040
|
+
// questions produit. Donna répondait donc de mémoire. Cas observé :
|
|
1041
|
+
// « comment activer les connecteurs ? » → un `cme.yaml`, un « manifeste des
|
|
1042
|
+
// services actifs » et un redémarrage de service, tous inventés, là où la
|
|
1043
|
+
// réponse tient dans un drapeau du `.env` du manager.
|
|
1044
|
+
'The bundled product documentation is your source for how llm-wiki works: what a feature is, how to enable or configure it, where a setting lives, what a message means, how to get started, how to troubleshoot. For any such question, call the documentation search/read tools FIRST and answer from what they return. Do not answer these from memory, even when you feel certain.',
|
|
1045
|
+
// Une réponse fausse et assurée sur la configuration coûte plus cher qu'un
|
|
1046
|
+
// « je ne sais pas » : elle envoie éditer des fichiers qui n'existent pas.
|
|
1047
|
+
'Configuration facts are never answered from memory. File names, environment variables, config keys, directory layouts and enabling procedures must come from a tool result or from the documentation you actually read in this conversation. If the documentation does not cover it, say plainly that you could not find where it is configured — never reconstruct a plausible-looking file name, key or procedure. A confident wrong answer sends the user editing files that do not exist.',
|
|
1028
1048
|
workspaceProfile
|
|
1029
1049
|
? `Workspace profile (.wiki/profile.md) — durable user preferences, apply these to every reply (tone, tutoiement/vouvoiement, formatting, etc.):\n${workspaceProfile}`
|
|
1030
1050
|
: null,
|
package/src/commands/slash.js
CHANGED
|
@@ -3,6 +3,8 @@ import { openExternalUrl } from '../shell/openExternal.js';
|
|
|
3
3
|
import { classifyCommandFailure, failureHint, rawFailureText } from '../core/commandFailure.js';
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
5
|
import { composeServices, listServices, runWikiCli, serviceLogs, serviceNames, serviceStates, startService, stopService } from '../core/compose.js';
|
|
6
|
+
import { agentServiceNames, profileServiceStatus } from '../core/agentsCompose.js';
|
|
7
|
+
import { GOOGLE_GRANTS, GOOGLE_GRANT_LABELS, defaultGoogleGrants } from '../core/googleGrants.js';
|
|
6
8
|
import {
|
|
7
9
|
applyMcpRuntimeStatus,
|
|
8
10
|
buildMcpStatus,
|
|
@@ -1058,6 +1060,12 @@ export async function handleSlashCommand(line, context) {
|
|
|
1058
1060
|
// falls back to the hardcoded COMPOSE_SERVICES constant instead.
|
|
1059
1061
|
const service = args[1];
|
|
1060
1062
|
if (service === 'agents' || service === 'agent') return runAgentCommand(startAgents, 'start');
|
|
1063
|
+
// Un agent nommé appartient à la pile agents (projet Compose distinct),
|
|
1064
|
+
// pas à celle du workspace : le router ici évite un « no such service »
|
|
1065
|
+
// sur un nom que la complétion propose pourtant.
|
|
1066
|
+
if (agentServiceNames().includes(service)) {
|
|
1067
|
+
return runAgentCommand((options) => startAgents({ ...options, services: [service] }), 'start');
|
|
1068
|
+
}
|
|
1061
1069
|
// "all" used to mean "the workspace services", which left the external
|
|
1062
1070
|
// agents down and looked like nothing had happened. It now means what an
|
|
1063
1071
|
// operator reads into it: the whole stack. `/start services` keeps the
|
|
@@ -1094,9 +1102,22 @@ export async function handleSlashCommand(line, context) {
|
|
|
1094
1102
|
case 'stop': {
|
|
1095
1103
|
const service = args[1];
|
|
1096
1104
|
if (service === 'agents') return runAgentCommand(stopAgents, 'stop');
|
|
1105
|
+
if (agentServiceNames().includes(service)) {
|
|
1106
|
+
return runAgentCommand((options) => stopAgents({ ...options, services: [service] }), 'stop');
|
|
1107
|
+
}
|
|
1108
|
+
// Symétrique de `/start all` : « all » désigne toute la pile, agents
|
|
1109
|
+
// compris. Il ne stoppait que les services du workspace et laissait les
|
|
1110
|
+
// agents debout — donc `/start all` puis `/stop all` ne revenait pas à
|
|
1111
|
+
// l'état de départ.
|
|
1112
|
+
const stopsAgents = service === 'all';
|
|
1113
|
+
const stopTarget = service === 'services' ? undefined : service;
|
|
1097
1114
|
try {
|
|
1098
1115
|
step(`Services: stopping ${service ?? 'workspace services'}…`);
|
|
1099
|
-
await stopService(context.session,
|
|
1116
|
+
await stopService(context.session, stopTarget);
|
|
1117
|
+
if (stopsAgents) {
|
|
1118
|
+
const agentsResult = await runAgentCommand(stopAgents, 'stop');
|
|
1119
|
+
if (agentsResult?.failed) return agentsResult;
|
|
1120
|
+
}
|
|
1100
1121
|
step('Services: refreshing MCP runtime…');
|
|
1101
1122
|
await refreshMcpRuntimeStatus(context.session);
|
|
1102
1123
|
return localizedOperationResult({
|
|
@@ -1186,7 +1207,9 @@ export async function handleSlashCommand(line, context) {
|
|
|
1186
1207
|
await refreshMcpRuntimeStatus(context.session);
|
|
1187
1208
|
const connectorMcp = context.session.mcp?.connectors;
|
|
1188
1209
|
if (!connectorMcp || connectorMcp.status !== 'connected') {
|
|
1189
|
-
|
|
1210
|
+
// Le manager sait exactement pourquoi : ne pas renvoyer un constat
|
|
1211
|
+
// vague que Donna comblerait en inventant.
|
|
1212
|
+
return connectorResult(profileServiceStatus('connectors').message);
|
|
1190
1213
|
}
|
|
1191
1214
|
if (subcommand === 'list') {
|
|
1192
1215
|
if (args[2]) return connectorResult('The connector list request has invalid extra arguments.');
|
|
@@ -1198,38 +1221,78 @@ export async function handleSlashCommand(line, context) {
|
|
|
1198
1221
|
{ workspace: context.session.workspace },
|
|
1199
1222
|
);
|
|
1200
1223
|
const payload = parseJsonText(formatMcpToolResult(result));
|
|
1201
|
-
|
|
1202
|
-
|
|
1224
|
+
if (payload?.status !== 'configured') {
|
|
1225
|
+
return connectorResult('google (Gmail): not authorized. Run `/connector auth google` to authorize reading and sending.');
|
|
1226
|
+
}
|
|
1227
|
+
// Le libellé annonçait « read-only » quels que soient les droits
|
|
1228
|
+
// réellement accordés — donc il mentait dès qu'on autorisait l'envoi,
|
|
1229
|
+
// et n'aidait pas à comprendre pourquoi l'envoi échouait sinon.
|
|
1230
|
+
const grants = Array.isArray(payload?.grants) ? payload.grants : [];
|
|
1231
|
+
const missing = GOOGLE_GRANTS.filter((grant) => !grants.includes(grant));
|
|
1232
|
+
const held = grants.map((grant) => `${grant} — ${GOOGLE_GRANT_LABELS[grant] ?? 'unknown grant'}`);
|
|
1233
|
+
const lines = [
|
|
1234
|
+
`google (Gmail): authorized for ${grants.join(', ') || 'nothing'}`,
|
|
1235
|
+
...held.map((line) => ` ✓ ${line}`),
|
|
1236
|
+
...missing.map((grant) => ` ✗ ${grant} — ${GOOGLE_GRANT_LABELS[grant]}`),
|
|
1237
|
+
];
|
|
1238
|
+
if (missing.length > 0) {
|
|
1239
|
+
lines.push(`Run \`/connector auth google ${missing.join(' ')}\` to add the missing grant(s); existing ones are kept.`);
|
|
1240
|
+
}
|
|
1241
|
+
return connectorResult(lines.join('\n'));
|
|
1203
1242
|
} catch (err) {
|
|
1204
|
-
return connectorResult(`google (Gmail
|
|
1243
|
+
return connectorResult(`google (Gmail): unavailable (${err instanceof Error ? err.message : String(err)})`);
|
|
1205
1244
|
}
|
|
1206
1245
|
}
|
|
1207
1246
|
if (subcommand === 'auth') {
|
|
1208
1247
|
const connector = String(args[2] ?? '').toLowerCase();
|
|
1209
1248
|
if (!['google', 'gmail'].includes(connector)) {
|
|
1210
|
-
return connectorResult('The requested connector is unsupported. The available connector is google (Gmail
|
|
1249
|
+
return connectorResult('The requested connector is unsupported. The available connector is google (Gmail).');
|
|
1250
|
+
}
|
|
1251
|
+
// Les droits demandés à Google. L'appel ne les passait pas, et le
|
|
1252
|
+
// serveur retombait sur son défaut `["read"]` : l'agent sait envoyer un
|
|
1253
|
+
// courriel, l'autorisation obtenue ne le permettait pas, et le refus
|
|
1254
|
+
// ressemblait à une fonctionnalité absente. On demande donc lecture ET
|
|
1255
|
+
// envoi par défaut, et les droits restants s'ajoutent à la demande.
|
|
1256
|
+
const requested = args.slice(3).map((value) => String(value).toLowerCase());
|
|
1257
|
+
const unknown = requested.filter((grant) => !GOOGLE_GRANTS.includes(grant));
|
|
1258
|
+
if (unknown.length > 0) {
|
|
1259
|
+
const available = GOOGLE_GRANTS.map((grant) => `${grant} (${GOOGLE_GRANT_LABELS[grant]})`).join('; ');
|
|
1260
|
+
return connectorResult(`Unsupported grant(s): ${unknown.join(', ')}. Available grants: ${available}.`);
|
|
1211
1261
|
}
|
|
1262
|
+
// Par défaut, tout ce que l'agent sait faire — y compris `modify`, sans
|
|
1263
|
+
// quoi les actions que Donna propose d'elle-même (« marquer comme lu »,
|
|
1264
|
+
// « archiver ») échouent après coup. C'est la même incohérence que
|
|
1265
|
+
// l'envoi : promettre une action que l'autorisation ne couvre pas.
|
|
1266
|
+
const grants = requested.length > 0 ? [...new Set(requested)] : defaultGoogleGrants();
|
|
1212
1267
|
try {
|
|
1213
1268
|
const result = await callMcpTool(
|
|
1214
1269
|
context.session.mcp,
|
|
1215
1270
|
'connectors',
|
|
1216
1271
|
'connectors_google_oauth_start',
|
|
1217
|
-
{ workspace: context.session.workspace },
|
|
1272
|
+
{ workspace: context.session.workspace, grants },
|
|
1218
1273
|
);
|
|
1219
1274
|
const payload = parseJsonText(formatMcpToolResult(result));
|
|
1220
1275
|
const authorizationUrl = payload?.authorizationUrl;
|
|
1276
|
+
if (payload?.error === 'send_capability_disabled') {
|
|
1277
|
+
return connectorResult(
|
|
1278
|
+
'Sending is disabled in this deployment (CONNECTORS_SEND_ENABLED=false in the manager .env), so the send grant cannot be authorized. Run `/connector auth google read` for read-only access, or enable sending and restart the connectors agent.',
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1221
1281
|
if (payload?.ok !== true || typeof authorizationUrl !== 'string') {
|
|
1222
1282
|
return connectorResult(`Google authorization could not start (${payload?.error ?? 'missing authorization URL'}).`);
|
|
1223
1283
|
}
|
|
1284
|
+
// L'autorisation est incrémentale côté Google : redemander avec un
|
|
1285
|
+
// droit de plus ne révoque pas les précédents.
|
|
1286
|
+
const scopeNote = `Requested grants: ${grants.join(', ')}.`;
|
|
1224
1287
|
if (openExternalUrl(authorizationUrl)) {
|
|
1225
|
-
return connectorResult(
|
|
1288
|
+
return connectorResult(`Google authorization opened successfully in the user browser. ${scopeNote}`);
|
|
1226
1289
|
}
|
|
1227
|
-
return connectorResult(`Google authorization requires the user to open this URL: ${authorizationUrl}`);
|
|
1290
|
+
return connectorResult(`Google authorization requires the user to open this URL: ${authorizationUrl} — ${scopeNote}`);
|
|
1228
1291
|
} catch (err) {
|
|
1229
1292
|
return connectorResult(`Google authorization could not start (${err instanceof Error ? err.message : String(err)}).`);
|
|
1230
1293
|
}
|
|
1231
1294
|
}
|
|
1232
|
-
return connectorResult(
|
|
1295
|
+
return connectorResult(`The requested connector action is unsupported. Available actions: list, and auth google [${GOOGLE_GRANTS.join('|')}].`);
|
|
1233
1296
|
}
|
|
1234
1297
|
case 'cancel': {
|
|
1235
1298
|
// Alias of /run cancel — people type /cancel when they want out.
|
|
@@ -126,9 +126,43 @@ test('/start completes to the three documented targets', async () => {
|
|
|
126
126
|
for (const expected of ['all', 'agents', 'services']) {
|
|
127
127
|
assert.ok(matches.includes(expected), `${expected} missing from ${JSON.stringify(matches)}`);
|
|
128
128
|
}
|
|
129
|
-
assert.match(completionDescription('all', ['/start']), /services AND
|
|
130
|
-
assert.match(completionDescription('agents', ['/start']), /agents only/);
|
|
131
|
-
assert.match(completionDescription('services', ['/start']), /workspace services only/);
|
|
129
|
+
assert.match(completionDescription('all', ['/start']), /services AND external agents/);
|
|
130
|
+
assert.match(completionDescription('agents', ['/start']), /agents only/i);
|
|
131
|
+
assert.match(completionDescription('services', ['/start']), /workspace services only/i);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('/start offers the operator vocabulary, not the Compose service names twice', async () => {
|
|
135
|
+
const { completionDescription } = await import('../shell/repl.js');
|
|
136
|
+
const { matches } = completionContext('/start ', {});
|
|
137
|
+
|
|
138
|
+
// `all` apparaissait deux fois — mot-clé du shell ET alias Compose — et
|
|
139
|
+
// chaque alias était doublé du service qu'il désigne : dix entrées pour cinq
|
|
140
|
+
// actions.
|
|
141
|
+
assert.equal(new Set(matches).size, matches.length, `duplicates in ${JSON.stringify(matches)}`);
|
|
142
|
+
for (const raw of ['serve', 'mcp-http', 'production-mcp']) {
|
|
143
|
+
assert.ok(!matches.includes(raw), `${raw} is already covered by an alias`);
|
|
144
|
+
}
|
|
145
|
+
for (const alias of ['ui', 'production']) {
|
|
146
|
+
assert.ok(matches.includes(alias), `${alias} missing from ${JSON.stringify(matches)}`);
|
|
147
|
+
assert.notEqual(
|
|
148
|
+
completionDescription(alias, ['/start']),
|
|
149
|
+
'Start this Docker Compose service.',
|
|
150
|
+
`${alias} must explain what it starts`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
// `wiki` est le service one-shot de `/wiki run` : en faire un alias de
|
|
154
|
+
// mcp-http le masquait silencieusement.
|
|
155
|
+
assert.ok(!matches.includes('wiki'), 'wiki must not shadow the one-shot CLI service');
|
|
156
|
+
// mcp-http fait partie du socle demarre par `all` et `services` : son alias
|
|
157
|
+
// n'ajoutait qu'une ligne a lire pour un service que personne ne lance seul.
|
|
158
|
+
assert.ok(!matches.includes('mcp'), 'mcp is covered by services/all');
|
|
159
|
+
// Les agents du socle ne s'adressent pas un par un, et `mailer` n'existe
|
|
160
|
+
// meme pas tant qu'il n'est pas decommente dans l'override de l'operateur.
|
|
161
|
+
for (const agent of ['cme', 'documents', 'mailer']) {
|
|
162
|
+
assert.ok(!matches.includes(agent), `${agent} is started by "agents", not on its own`);
|
|
163
|
+
}
|
|
164
|
+
// Seuls les agents derriere un drapeau de profil se pilotent separement.
|
|
165
|
+
assert.ok(matches.includes('connectors'), 'connectors is opt-in, so it must be addressable');
|
|
132
166
|
});
|
|
133
167
|
|
|
134
168
|
test('/workspace delete removes files and clears current session context after confirmation', async () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import YAML from 'yaml';
|
|
3
4
|
|
|
4
5
|
import { managerComposeOverrideFile, managerEnvFile, readEnvFile } from './env.js';
|
|
5
6
|
import { managerRoot } from './workspaces.js';
|
|
@@ -16,10 +17,94 @@ export const PROFILE_FLAGS = Object.freeze({
|
|
|
16
17
|
connectors: 'CONNECTORS_ENABLED',
|
|
17
18
|
});
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Services réellement déclarés dans la pile agents, override compris.
|
|
22
|
+
*
|
|
23
|
+
* Lue du fichier plutôt que codée en dur : une liste figée avait fini par
|
|
24
|
+
* annoncer `mailer`, qui ne vit que dans l'exemple d'override et n'existe donc
|
|
25
|
+
* pas tant que l'opérateur ne l'a pas décommenté. Proposer un service absent,
|
|
26
|
+
* c'est promettre un « no such service ».
|
|
27
|
+
*
|
|
28
|
+
* Sert au ROUTAGE : la pile agents est un projet Compose distinct de celui du
|
|
29
|
+
* workspace, donc `/start <nom>` doit partir vers `wiki-workspace agents`.
|
|
30
|
+
*/
|
|
31
|
+
export function agentServiceNames({ env = resolvedManagerEnv() } = {}) {
|
|
32
|
+
const { composeFiles } = resolveAgentsComposeContext({ env });
|
|
33
|
+
const names = new Set();
|
|
34
|
+
for (const file of composeFiles) {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = YAML.parse(readFileSync(file, 'utf8')) ?? {};
|
|
37
|
+
for (const name of Object.keys(parsed.services ?? {})) names.add(name);
|
|
38
|
+
} catch { /* fichier illisible : il ne déclare rien d'adressable */ }
|
|
39
|
+
}
|
|
40
|
+
return [...names].sort();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Agents qu'un opérateur pilote un par un, proposés dans les complétions.
|
|
45
|
+
*
|
|
46
|
+
* Uniquement ceux placés derrière un drapeau de profil : ce sont les seuls
|
|
47
|
+
* qu'on active et désactive délibérément. `cme` et `documents` font partie du
|
|
48
|
+
* socle — on les démarre avec `agents`, les distinguer n'apporte rien et
|
|
49
|
+
* allonge une liste que l'opérateur doit lire à chaque fois.
|
|
50
|
+
*/
|
|
51
|
+
export function togglableAgentNames() {
|
|
52
|
+
return Object.keys(PROFILE_FLAGS).sort();
|
|
53
|
+
}
|
|
54
|
+
|
|
19
55
|
export function isEnabled(value) {
|
|
20
56
|
return /^(?:1|true|yes|on)$/i.test(String(value ?? '').trim());
|
|
21
57
|
}
|
|
22
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Pourquoi un service à profil est indisponible, et comment l'activer.
|
|
61
|
+
*
|
|
62
|
+
* Le message d'origine — « le service des connecteurs est indisponible ou
|
|
63
|
+
* désactivé » — ne disait ni lequel des deux, ni où regarder. Donna, à qui on
|
|
64
|
+
* demandait ensuite « comment l'activer », n'avait aucun fait à citer et
|
|
65
|
+
* inventait des fichiers (`cme.yaml`, un « manifeste des services actifs »)
|
|
66
|
+
* qui n'existent nulle part. Une réponse fausse et confiante coûte plus cher
|
|
67
|
+
* qu'un « je ne sais pas » : c'est le message qu'il faut rendre suffisant,
|
|
68
|
+
* pas le modèle qu'il faut espérer plus prudent.
|
|
69
|
+
*
|
|
70
|
+
* @returns {{ enabled: boolean, flag: string, envFile: string, message: string }}
|
|
71
|
+
*/
|
|
72
|
+
export function profileServiceStatus(profile, { env = resolvedManagerEnv() } = {}) {
|
|
73
|
+
const flag = PROFILE_FLAGS[profile];
|
|
74
|
+
const envFile = managerEnvFile();
|
|
75
|
+
if (!flag) {
|
|
76
|
+
return {
|
|
77
|
+
enabled: false,
|
|
78
|
+
flag: null,
|
|
79
|
+
envFile,
|
|
80
|
+
message: `There is no "${profile}" service in this deployment.`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const enabled = isEnabled(env[flag]);
|
|
84
|
+
if (enabled) {
|
|
85
|
+
return {
|
|
86
|
+
enabled: true,
|
|
87
|
+
flag,
|
|
88
|
+
envFile,
|
|
89
|
+
message: `The ${profile} service is enabled (${flag}) but not reachable — its container is probably not running. Start it with \`/start agents\`.`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const current = String(env[flag] ?? '').trim();
|
|
93
|
+
return {
|
|
94
|
+
enabled: false,
|
|
95
|
+
flag,
|
|
96
|
+
envFile,
|
|
97
|
+
message: [
|
|
98
|
+
`The ${profile} service is disabled: ${flag} is ${current === '' ? 'not set' : `"${current}"`} in ${envFile}.`,
|
|
99
|
+
'To enable it:',
|
|
100
|
+
` 1. set ${flag}=true in ${envFile}`,
|
|
101
|
+
' 2. run `/start agents` (or `wiki-workspace agents up`) to start its container',
|
|
102
|
+
` 3. run \`/connector list\` to check it answers`,
|
|
103
|
+
`The ${profile} service runs behind a Compose profile, so while ${flag} is off its container does not exist at all — it will not appear in \`docker compose ps\`.`,
|
|
104
|
+
].join('\n'),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
23
108
|
/**
|
|
24
109
|
* The manager `.env` wins over the ambient process environment.
|
|
25
110
|
*
|
package/src/core/buildInfo.json
CHANGED
package/src/core/compose.js
CHANGED
|
@@ -19,11 +19,17 @@ const execFileAsync = promisify(execFile);
|
|
|
19
19
|
export const COMPOSE_SERVICES = ['serve', 'mcp-http', 'production-mcp'];
|
|
20
20
|
const SERVICE_DESCRIPTION_LABEL = 'wiki-manager.description';
|
|
21
21
|
|
|
22
|
+
// Deux alias seulement, pour les deux services qu'un opérateur pilote vraiment
|
|
23
|
+
// à part. `mcp-http` n'en a plus : il fait partie du socle démarré par `all` et
|
|
24
|
+
// `services`, et son alias `mcp` ajoutait une ligne à lire pour un service que
|
|
25
|
+
// personne ne démarre seul. Il reste adressable sous son nom Compose.
|
|
26
|
+
//
|
|
27
|
+
// Pas d'alias `wiki` non plus : c'est déjà le nom du service Compose one-shot
|
|
28
|
+
// derrière `/wiki run`. En faire un alias de `mcp-http` le masquait, et
|
|
29
|
+
// `/start wiki` démarrait silencieusement un autre service que celui nommé.
|
|
22
30
|
const DEFAULT_SERVICE_ALIASES = {
|
|
23
31
|
all: COMPOSE_SERVICES,
|
|
24
32
|
ui: ['serve'],
|
|
25
|
-
wiki: ['mcp-http'],
|
|
26
|
-
mcp: ['mcp-http'],
|
|
27
33
|
production: ['production-mcp'],
|
|
28
34
|
};
|
|
29
35
|
|
|
@@ -92,6 +98,22 @@ export function serviceNames() {
|
|
|
92
98
|
return [...new Set([...COMPOSE_SERVICES, ...Object.keys(serviceAliases())])].sort();
|
|
93
99
|
}
|
|
94
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Noms proposés à la complétion de `/start`, `/stop` et `/logs`.
|
|
103
|
+
*
|
|
104
|
+
* La liste complète mélangeait les alias et les services Compose qu'ils
|
|
105
|
+
* désignent — `mcp` et `mcp-http`, `ui` et `serve`, `production` et
|
|
106
|
+
* `production-mcp` — et affichait `all` deux fois, une fois comme mot-clé du
|
|
107
|
+
* shell et une fois comme alias Compose. Dix entrées pour cinq actions
|
|
108
|
+
* réelles. On ne propose donc que le vocabulaire destiné à l'opérateur ; les
|
|
109
|
+
* noms Compose bruts restent acceptés si on les tape.
|
|
110
|
+
*/
|
|
111
|
+
export function serviceChoices() {
|
|
112
|
+
const aliases = Object.keys(serviceAliases());
|
|
113
|
+
// `all` est ajouté par l'appelant, en tête : c'est le choix par défaut.
|
|
114
|
+
return aliases.filter((name) => name !== 'all').sort();
|
|
115
|
+
}
|
|
116
|
+
|
|
95
117
|
export function serviceDescription(name) {
|
|
96
118
|
return serviceDescriptions()[name] ?? null;
|
|
97
119
|
}
|
|
@@ -19,11 +19,29 @@ test('workspace compose does not start a per-workspace agent runtime', async ()
|
|
|
19
19
|
);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
test('shipped compose files never carry a build context', async () => {
|
|
23
|
+
// Ces deux fichiers partent dans le paquet npm, où les dépôts frères
|
|
24
|
+
// (`../agent-external/…`) n'existent pas : un `build:` y rend toute commande
|
|
25
|
+
// Compose irrésolvable chez l'utilisateur. Les images sont construites et
|
|
26
|
+
// publiées par build-and-push.sh, jamais par le manager.
|
|
27
|
+
for (const file of ['../../docker-compose.yml', '../../agents.docker-compose.yml']) {
|
|
28
|
+
const compose = YAML.parse(await readFile(new URL(file, import.meta.url), 'utf8'));
|
|
29
|
+
for (const [name, service] of Object.entries(compose.services ?? {})) {
|
|
30
|
+
assert.equal(service.build, undefined, `${file} ${name}: shipped files must reference an image, never build it`);
|
|
31
|
+
assert.ok(service.image, `${file} ${name}: must declare an image`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
22
36
|
test('no compose service relies on a bare environment passthrough', async () => {
|
|
23
37
|
// `- VAR` makes Compose print `The "VAR" variable is not set. Defaulting to a
|
|
24
38
|
// blank string.` for every key the operator left as a commented placeholder —
|
|
25
39
|
// CONNECTORS_MCP_PORT once connectors were enabled. That warning reached the
|
|
26
40
|
// ShellUI looking like a failure. Every entry must carry its own default.
|
|
41
|
+
//
|
|
42
|
+
// Une valeur vide est ici sans danger : l'application OAuth embarquée dans
|
|
43
|
+
// l'image du connecteur est lue depuis un fichier, plus depuis un ENV que la
|
|
44
|
+
// chaîne vide écraserait.
|
|
27
45
|
for (const file of ['../../docker-compose.yml', '../../agents.docker-compose.yml']) {
|
|
28
46
|
const compose = YAML.parse(await readFile(new URL(file, import.meta.url), 'utf8'));
|
|
29
47
|
for (const [name, service] of Object.entries(compose.services ?? {})) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Droits Gmail demandés à Google, et ce qu'ils ouvrent réellement.
|
|
3
|
+
*
|
|
4
|
+
* Les identifiants (`read`, `send`, `modify`) sont ceux de Google — `modify`
|
|
5
|
+
* vient du scope `gmail.modify` — et ceux de `GOOGLE_GRANTS` dans
|
|
6
|
+
* agent-connectors. Leur inventer un synonyme côté manager créerait une
|
|
7
|
+
* troisième orthographe à tenir à jour, exactement le travers qui avait donné
|
|
8
|
+
* une seconde paire de variables OAuth préfixées. On décrit, on ne renomme pas.
|
|
9
|
+
*
|
|
10
|
+
* Cette table est la source unique : la valeur par défaut de
|
|
11
|
+
* `/connector auth`, l'aide de la complétion et le rendu de `/connector list`
|
|
12
|
+
* en découlent tous, donc ils ne peuvent pas diverger.
|
|
13
|
+
*/
|
|
14
|
+
export const GOOGLE_GRANT_LABELS = Object.freeze({
|
|
15
|
+
read: 'read messages and collect them into the workspace',
|
|
16
|
+
send: 'send email from your account (subject to the recipient allow-list)',
|
|
17
|
+
modify: 'mark read/unread, archive, label, star, trash (never permanent deletion)',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const GOOGLE_GRANTS = Object.freeze(Object.keys(GOOGLE_GRANT_LABELS));
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Droits demandés quand l'opérateur n'en nomme aucun.
|
|
24
|
+
*
|
|
25
|
+
* Tout ce que l'agent sait faire. Un défaut plus étroit promet des actions que
|
|
26
|
+
* l'autorisation ne couvre pas : `/connector auth google` ne demandait que
|
|
27
|
+
* `read`, alors que l'agent expose l'envoi et la gestion de boîte — Donna
|
|
28
|
+
* proposait « marquer comme lu », et l'action échouait après coup. Comme
|
|
29
|
+
* l'autorisation Google est incrémentale, un droit ajouté plus tard coûte un
|
|
30
|
+
* aller-retour de consentement supplémentaire, pas moins d'accès.
|
|
31
|
+
*/
|
|
32
|
+
export function defaultGoogleGrants() {
|
|
33
|
+
return [...GOOGLE_GRANTS];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function describeGoogleGrant(grant) {
|
|
37
|
+
return GOOGLE_GRANT_LABELS[grant] ?? null;
|
|
38
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { GOOGLE_GRANTS, GOOGLE_GRANT_LABELS, defaultGoogleGrants } from './googleGrants.js';
|
|
6
|
+
|
|
7
|
+
const connectorsSrc = (file) =>
|
|
8
|
+
readFileSync(fileURLToPath(new URL(`../../../agent-external/agent-connectors/src/${file}`, import.meta.url)), 'utf8');
|
|
9
|
+
|
|
10
|
+
test('the grant names mirror the agent, spelling included', () => {
|
|
11
|
+
// `modify` est le nom de Google (scope gmail.modify) et celui de l'agent.
|
|
12
|
+
// Un synonyme côté manager créerait une troisième orthographe à tenir à jour
|
|
13
|
+
// — le travers qui avait déjà donné une seconde paire de variables OAuth.
|
|
14
|
+
const tokens = connectorsSrc('googleTokens.ts');
|
|
15
|
+
const declared = tokens.match(/GOOGLE_GRANTS: readonly GoogleGrant\[\] = \[([^\]]+)\]/)?.[1] ?? '';
|
|
16
|
+
const agentGrants = [...declared.matchAll(/'([a-z]+)'/g)].map(([, grant]) => grant);
|
|
17
|
+
|
|
18
|
+
assert.deepEqual([...GOOGLE_GRANTS].sort(), agentGrants.sort());
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('every grant is described in plain words, never left as a bare token', () => {
|
|
22
|
+
for (const grant of GOOGLE_GRANTS) {
|
|
23
|
+
const label = GOOGLE_GRANT_LABELS[grant];
|
|
24
|
+
assert.ok(label && label.length > 10, `${grant} needs a human description`);
|
|
25
|
+
assert.notEqual(label, grant);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('the default asks for everything the agent can actually do', () => {
|
|
30
|
+
// Un défaut plus étroit promet des actions que l'autorisation ne couvre pas :
|
|
31
|
+
// `/connector auth google` ne demandait que `read`, et l'envoi comme le
|
|
32
|
+
// marquage échouaient après coup, en ressemblant à des fonctions absentes.
|
|
33
|
+
assert.deepEqual(defaultGoogleGrants().sort(), [...GOOGLE_GRANTS].sort());
|
|
34
|
+
|
|
35
|
+
// Chaque droit du défaut doit ouvrir quelque chose de réellement exposé.
|
|
36
|
+
const server = connectorsSrc('server.ts');
|
|
37
|
+
const contract = connectorsSrc('contract.ts');
|
|
38
|
+
assert.match(contract, /CAPABILITY_ID = 'external-source\.collect'/, 'read feeds the collect capability');
|
|
39
|
+
assert.match(contract, /SEND_CAPABILITY_ID = 'communication\.send-email'/, 'send has a capability');
|
|
40
|
+
assert.match(server, /'connectors_gmail_modify'/, 'modify has a tool');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('the default grants are not mutated by the caller', () => {
|
|
44
|
+
const first = defaultGoogleGrants();
|
|
45
|
+
first.push('bogus');
|
|
46
|
+
assert.deepEqual(defaultGoogleGrants().sort(), [...GOOGLE_GRANTS].sort());
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('the destructive Gmail tool needs approval and stays out of chat mode', () => {
|
|
50
|
+
const script = readFileSync(fileURLToPath(new URL('../../wiki-workspace', import.meta.url)), 'utf8');
|
|
51
|
+
const block = script.slice(script.indexOf('config.mcpServers.connectors ??='), script.indexOf('delete connectorAccess.allowActions'));
|
|
52
|
+
const approval = block.slice(block.indexOf('requireApproval'), block.indexOf('chatAccess'));
|
|
53
|
+
const chatAllow = block.slice(block.indexOf('connectorAccess.allow ='));
|
|
54
|
+
|
|
55
|
+
assert.match(approval, /connectors_gmail_modify/, 'a destructive tool must be approval-gated');
|
|
56
|
+
// /chat est en lecture seule : une mutation n'y a pas sa place, elle passe
|
|
57
|
+
// par /agent où l'allow-list ne s'applique pas.
|
|
58
|
+
assert.ok(!chatAllow.includes('connectors_gmail_modify'), 'chat mode must stay read-only');
|
|
59
|
+
});
|