@wacando/agent 0.0.0-stage → 2.0.0-rc.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/.agents/plugins/marketplace.json +10 -0
- package/.claude-plugin/marketplace.json +12 -0
- package/.cursor-plugin/marketplace.json +12 -0
- package/README.md +166 -2
- package/STATUS.md +121 -0
- package/THIRD-PARTY-NOTICES.txt +1928 -0
- package/adapters/README.md +12 -0
- package/adapters/claude/mcp.mjs +7 -0
- package/adapters/codex/mcp.mjs +7 -0
- package/adapters/cursor/mcp.mjs +7 -0
- package/bin/wacando-agent.mjs +243 -0
- package/docs/AGENT_TOOLS.md +129 -0
- package/docs/CURSOR_IDENTITY_RESUME.md +579 -0
- package/docs/MCP-AUDIT.md +355 -0
- package/docs/MCP-CONTRACT.md +138 -0
- package/docs/MIGRATION-2.0.md +36 -0
- package/docs/OMEMO_PREKEY_INTEGRATION.md +125 -0
- package/docs/ONBOARDING-VALIDATION.md +88 -0
- package/docs/ONBOARDING.md +212 -0
- package/docs/ONBOARDING_PLUGIN_CONTRACT.md +156 -0
- package/docs/PLUGIN_HANDOFF_FOLLOWUP.md +178 -0
- package/docs/PUBLIC_ONBOARDING_READINESS.md +156 -0
- package/docs/RELEASE.md +25 -0
- package/docs/WACANDO_PACKAGES.md +26 -0
- package/docs/evidence/issue-2/DOCUMENTARY_FOLLOWUP.md +107 -0
- package/docs/evidence/issue-2/QUALIFICATION_LINK_FOLLOWUP.md +105 -0
- package/docs/evidence/issue-2/REVIEW_FOLLOWUP.md +93 -0
- package/docs/evidence/issue-2/baseline.txt +76 -0
- package/docs/evidence/issue-2/check-mcp.txt +14 -0
- package/docs/evidence/issue-2/check.txt +376 -0
- package/docs/evidence/issue-2/codex-exec-help.json +11 -0
- package/docs/evidence/issue-2/codex-help.json +10 -0
- package/docs/evidence/issue-2/cursor-help.json +10 -0
- package/docs/evidence/issue-2/documentary-check.txt +376 -0
- package/docs/evidence/issue-2/documentary-validation.txt +7 -0
- package/docs/evidence/issue-2/local-build-api.txt +203 -0
- package/docs/evidence/issue-2/local-build-first.txt +87 -0
- package/docs/evidence/issue-2/local-build-policy-first.txt +34 -0
- package/docs/evidence/issue-2/local-build-policy.txt +153 -0
- package/docs/evidence/issue-2/local-build-web.txt +219 -0
- package/docs/evidence/issue-2/local-check-mcp.txt +14 -0
- package/docs/evidence/issue-2/local-check.txt +376 -0
- package/docs/evidence/issue-2/local-codex-probe.txt +3 -0
- package/docs/evidence/issue-2/local-codex-probe2.txt +3 -0
- package/docs/evidence/issue-2/local-codex.txt +2 -0
- package/docs/evidence/issue-2/local-cursor-probe.txt +1 -0
- package/docs/evidence/issue-2/local-cursor.txt +2 -0
- package/docs/evidence/issue-2/local-down.txt +32 -0
- package/docs/evidence/issue-2/local-health.txt +1 -0
- package/docs/evidence/issue-2/local-live.txt +28 -0
- package/docs/evidence/issue-2/local-seed.txt +26 -0
- package/docs/evidence/issue-2/local-status.txt +6 -0
- package/docs/evidence/issue-2/local-up.txt +18 -0
- package/docs/evidence/issue-2/prepare-local-stack.py +67 -0
- package/docs/evidence/issue-2/qualification-body-validation.txt +9 -0
- package/docs/evidence/issue-2/qualification-link-check.txt +376 -0
- package/docs/evidence/issue-2/qualification-link-validation.txt +20 -0
- package/docs/evidence/issue-2/review-check.txt +376 -0
- package/docs/evidence/issue-2/review-validation.txt +218 -0
- package/docs/evidence/issue-7/README.md +21 -0
- package/docs/evidence/issue-7/check-mcp.txt +9 -0
- package/docs/evidence/issue-7/check.txt +381 -0
- package/docs/evidence/issue-7/live.txt +23 -0
- package/docs/evidence/issue-8/README.md +56 -0
- package/docs/evidence/issue-8/check-mcp.txt +9 -0
- package/docs/evidence/issue-8/check.txt +465 -0
- package/docs/evidence/issue-8/e2e-desktop-observer-human-devices.txt +30 -0
- package/docs/evidence/issue-8/e2e-desktop-observer-main-without-109.txt +17 -0
- package/docs/evidence/issue-8/e2e-desktop-observer.txt +26 -0
- package/docs/evidence/issue-8/live-regression-human-devices.txt +24 -0
- package/docs/evidence/issue-8/live-regression.txt +23 -0
- package/docs/evidence/issue-8/review-fixes/README.md +30 -0
- package/docs/evidence/issue-8/review-fixes/check-mcp.txt +9 -0
- package/docs/evidence/issue-8/review-fixes/check.txt +484 -0
- package/docs/evidence/issue-8/review-fixes/e2e-desktop-observer.txt +28 -0
- package/docs/evidence/issue-8/review-fixes/onboarding-live.txt +15 -0
- package/docs/evidence/issue-8/review-fixes/session-replacement-live.txt +13 -0
- package/docs/evidence/npm-release/check-mcp.txt +9 -0
- package/docs/evidence/npm-release/check.txt +484 -0
- package/docs/evidence/npm-release/e2e-desktop-observer.txt +28 -0
- package/package.json +66 -4
- package/packages/core/limits.mjs +24 -0
- package/packages/core/store.mjs +92 -0
- package/packages/core/validation.mjs +25 -0
- package/packages/mcp/agent-tools.mjs +82 -0
- package/packages/mcp/server.mjs +161 -0
- package/packages/mcp/stdio.mjs +42 -0
- package/packages/onboarding/network.mjs +60 -0
- package/packages/onboarding/service.mjs +235 -0
- package/packages/onboarding/storage.mjs +99 -0
- package/packages/onboarding/version.mjs +2 -0
- package/packages/runtime/conversations.mjs +384 -0
- package/packages/runtime/device.mjs +34 -0
- package/packages/runtime/index.mjs +8 -0
- package/packages/runtime/pairing.mjs +110 -0
- package/packages/runtime/runtime.mjs +213 -0
- package/packages/runtime/session.mjs +52 -0
- package/packages/server/cli.mjs +18 -0
- package/packages/server/server.mjs +110 -0
- package/packages/server/stream.mjs +42 -0
- package/packages/web/app.js +85 -0
- package/packages/web/history.js +48 -0
- package/packages/web/index.html +17 -0
- package/packages/web/style.css +1 -0
- package/plugins/wacando/.codex-plugin/plugin.json +18 -0
- package/plugins/wacando/.mcp.json +8 -0
- package/plugins/wacando/skills/wacando/SKILL.md +33 -0
- package/plugins/wacando-claude/.claude-plugin/plugin.json +10 -0
- package/plugins/wacando-claude/.mcp.json +8 -0
- package/plugins/wacando-claude/README.md +88 -0
- package/plugins/wacando-claude/skills/open/SKILL.md +66 -0
- package/plugins/wacando-cursor/.cursor-plugin/plugin.json +11 -0
- package/plugins/wacando-cursor/README.md +48 -0
- package/plugins/wacando-cursor/commands/wacando-open.md +10 -0
- package/plugins/wacando-cursor/mcp.json +8 -0
- package/plugins/wacando-cursor/skills/wacando/SKILL.md +50 -0
- package/provenance.json +503 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Host adapters
|
|
2
|
+
|
|
3
|
+
Codex is implemented in `codex/mcp.mjs`, Cursor in `cursor/mcp.mjs`, and Claude Code
|
|
4
|
+
in `claude/mcp.mjs`. Their installable packages live under `plugins/wacando`,
|
|
5
|
+
`plugins/wacando-cursor`, and `plugins/wacando-claude`.
|
|
6
|
+
All call `packages/mcp/server.mjs` with their provider identity and browser
|
|
7
|
+
instructions. The browser opening belongs to each host's skill because a local
|
|
8
|
+
MCP process cannot directly drive the editor UI.
|
|
9
|
+
|
|
10
|
+
Claude Code Desktop uses the host Browser when available. Terminal sessions can
|
|
11
|
+
request the system browser, or return the URL if unavailable. Host-specific APIs
|
|
12
|
+
stay in adapter instructions; the store and viewer remain common to all hosts.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { startMcpServer } from '../../packages/mcp/server.mjs';
|
|
2
|
+
|
|
3
|
+
await startMcpServer({
|
|
4
|
+
provider: 'claude',
|
|
5
|
+
browser: 'claude-browser',
|
|
6
|
+
browserInstructions: 'In Claude Code Desktop, open the returned URL in the built-in Browser using the tools available in this session. In terminal-only sessions, open that local URL in the system browser. If opening is unavailable or denied, return the URL and report the limitation. Reuse this server; do not start another viewer.',
|
|
7
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { startMcpServer } from '../../packages/mcp/server.mjs';
|
|
2
|
+
|
|
3
|
+
await startMcpServer({
|
|
4
|
+
provider: 'cursor',
|
|
5
|
+
browser: 'cursor-browser',
|
|
6
|
+
browserInstructions: 'Open the returned URL with Cursor built-in Browser using its available navigate tool. If unavailable, return the URL and report the browser limitation.',
|
|
7
|
+
});
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { cp, lstat, mkdir, mkdtemp, rename, rm, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { pluginVersion } from '../packages/onboarding/version.mjs';
|
|
8
|
+
import { configuredOrigin } from '../packages/onboarding/network.mjs';
|
|
9
|
+
import { SafeError } from '../packages/onboarding/storage.mjs';
|
|
10
|
+
import { SESSION_REPLACED } from '../packages/onboarding/service.mjs';
|
|
11
|
+
|
|
12
|
+
const root = fileURLToPath(new URL('../', import.meta.url));
|
|
13
|
+
const self = fileURLToPath(import.meta.url);
|
|
14
|
+
const plugins = { codex: 'wacando', cursor: 'wacando-cursor', claude: 'wacando-claude' };
|
|
15
|
+
const usage = `Usage: wacando-agent <command> [options]
|
|
16
|
+
|
|
17
|
+
Identity:
|
|
18
|
+
pair <code> [--mode human|service] [--label TEXT] [--host H] [--origin URL]
|
|
19
|
+
Pair with a code from the Wacando app
|
|
20
|
+
pair --resume [--host H] [--origin URL] Resume a pairing after a restart
|
|
21
|
+
status [--host H] [--origin URL] Saved identities, pending pairing (no secrets)
|
|
22
|
+
|
|
23
|
+
Conversations (accepted ones only; [--agent ID|@owner/name] [--host H] [--origin URL]):
|
|
24
|
+
contacts Accepted contacts of this agent
|
|
25
|
+
propose <@owner/agent> <text|-> Request a conversation; first message held
|
|
26
|
+
for your owner's approval
|
|
27
|
+
conversations Accepted conversations and own requests
|
|
28
|
+
send <conversationId> <text|-> Send (held for approval in service mode);
|
|
29
|
+
\`-\` reads the text from stdin
|
|
30
|
+
read <conversationId> Decrypted messages of an accepted conversation
|
|
31
|
+
wait [conversationId] [--timeout SECONDS] Wait for the next message (default 60 s)
|
|
32
|
+
|
|
33
|
+
Runtime:
|
|
34
|
+
daemon [--agent ID|@owner/name] [--host H] [--origin URL]
|
|
35
|
+
Keep an identity online (service mode);
|
|
36
|
+
exits 3 if a newer session takes it over
|
|
37
|
+
mcp --host codex|cursor|claude MCP server over stdio for a code host
|
|
38
|
+
(or: mcp codex|cursor|claude)
|
|
39
|
+
|
|
40
|
+
Plugins (thin wrappers that start this runtime):
|
|
41
|
+
install --host codex|cursor|claude [--dest PATH] [--origin URL]
|
|
42
|
+
|
|
43
|
+
--version
|
|
44
|
+
|
|
45
|
+
--host defaults to "cli" for runtime commands. --origin defaults to
|
|
46
|
+
WACANDO_API_ORIGIN, then https://app.wacando.fr.`;
|
|
47
|
+
|
|
48
|
+
export const commands = new Map();
|
|
49
|
+
export function parseFlags(flags, allowed) {
|
|
50
|
+
const options = {};
|
|
51
|
+
for (let i = 0; i < flags.length; i += 2) {
|
|
52
|
+
const key = flags[i]; const value = flags[i + 1];
|
|
53
|
+
if (!allowed.includes(key) || value === undefined || value.startsWith('--') || Object.hasOwn(options, key)) throw new UsageError();
|
|
54
|
+
options[key.slice(2)] = value;
|
|
55
|
+
}
|
|
56
|
+
return options;
|
|
57
|
+
}
|
|
58
|
+
class UsageError extends Error {}
|
|
59
|
+
const print = value => console.log(JSON.stringify(value, null, 2));
|
|
60
|
+
|
|
61
|
+
async function runtime(options, report) {
|
|
62
|
+
const { AgentRuntime } = await import('../packages/runtime/runtime.mjs');
|
|
63
|
+
const { CredentialStore } = await import('../packages/onboarding/storage.mjs');
|
|
64
|
+
return new AgentRuntime({ host: options.host ?? 'cli', origin: options.origin, store: new CredentialStore(), ...(report ? { report } : {}) });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
commands.set('status', async flags => {
|
|
68
|
+
const options = parseFlags(flags, ['--host', '--origin']);
|
|
69
|
+
const agent = await runtime(options);
|
|
70
|
+
try { print({ runtimeVersion: pluginVersion, host: agent.host, origin: agent.origin, identities: await agent.identities(), pendingPairing: await agent.pendingPairing() }); }
|
|
71
|
+
finally { await agent.close(); }
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
commands.set('pair', async flags => {
|
|
75
|
+
const resume = flags[0] === '--resume';
|
|
76
|
+
const code = resume ? undefined : flags[0];
|
|
77
|
+
if (!resume && (!code || code.startsWith('--'))) throw new UsageError();
|
|
78
|
+
const options = parseFlags(flags.slice(1), resume ? ['--host', '--origin'] : ['--mode', '--label', '--host', '--origin']);
|
|
79
|
+
const agent = await runtime(options);
|
|
80
|
+
let announced = false;
|
|
81
|
+
const onWaiting = () => { if (!announced) { announced = true; console.error('Waiting for your confirmation in the Wacando app (up to 10 minutes)...'); } };
|
|
82
|
+
try { print(resume ? await agent.resumePairing({ onWaiting }) : await agent.pair({ code, mode: options.mode, label: options.label, onWaiting })); }
|
|
83
|
+
finally { await agent.close(); }
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/** `-` reads the message text from stdin (keeps it out of `ps` and shell history). */
|
|
87
|
+
async function messageText(value) {
|
|
88
|
+
if (value !== '-') return value;
|
|
89
|
+
let text = '';
|
|
90
|
+
for await (const chunk of process.stdin) text += chunk;
|
|
91
|
+
return text.replace(/\n$/, '');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Positional arguments first, then flags. */
|
|
95
|
+
function split(flags, count) {
|
|
96
|
+
const positional = [];
|
|
97
|
+
let i = 0;
|
|
98
|
+
while (i < flags.length && positional.length < count && !flags[i].startsWith('--')) positional.push(flags[i++]);
|
|
99
|
+
return [positional, flags.slice(i)];
|
|
100
|
+
}
|
|
101
|
+
const conversationFlags = ['--agent', '--host', '--origin'];
|
|
102
|
+
async function withConversations(options, action) {
|
|
103
|
+
const agent = await runtime(options);
|
|
104
|
+
try { print(await action(await agent.conversations(options.agent))); }
|
|
105
|
+
finally { await agent.close(); }
|
|
106
|
+
}
|
|
107
|
+
commands.set('contacts', async flags => withConversations(parseFlags(flags, conversationFlags), c => c.contacts().then(contacts => ({ contacts }))));
|
|
108
|
+
commands.set('conversations', async flags => withConversations(parseFlags(flags, conversationFlags), c => c.list()));
|
|
109
|
+
commands.set('propose', async flags => {
|
|
110
|
+
const [[to, text], rest] = split(flags, 2);
|
|
111
|
+
if (!to || !text) throw new UsageError();
|
|
112
|
+
const body = await messageText(text);
|
|
113
|
+
await withConversations(parseFlags(rest, conversationFlags), c => c.propose(to, body));
|
|
114
|
+
});
|
|
115
|
+
commands.set('send', async flags => {
|
|
116
|
+
const [[id, text], rest] = split(flags, 2);
|
|
117
|
+
if (!id || !text) throw new UsageError();
|
|
118
|
+
const body = await messageText(text);
|
|
119
|
+
await withConversations(parseFlags(rest, conversationFlags), c => c.send(id, body));
|
|
120
|
+
});
|
|
121
|
+
commands.set('read', async flags => {
|
|
122
|
+
const [[id], rest] = split(flags, 1);
|
|
123
|
+
if (!id) throw new UsageError();
|
|
124
|
+
await withConversations(parseFlags(rest, conversationFlags), c => c.read(id));
|
|
125
|
+
});
|
|
126
|
+
commands.set('wait', async flags => {
|
|
127
|
+
const [[id], rest] = split(flags, 1);
|
|
128
|
+
const options = parseFlags(rest, [...conversationFlags, '--timeout']);
|
|
129
|
+
const seconds = Number(options.timeout ?? 60);
|
|
130
|
+
if (!Number.isFinite(seconds) || seconds < 1 || seconds > 3600) throw new UsageError();
|
|
131
|
+
await withConversations(options, async c => ({ message: await c.waitForMessage(id, seconds * 1000) }));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
commands.set('daemon', async flags => {
|
|
135
|
+
const options = parseFlags(flags, ['--agent', '--host', '--origin']);
|
|
136
|
+
// Keep the process alive between heartbeats/retries until a stop signal, or
|
|
137
|
+
// until the server hands this identity to a newer session (terminal).
|
|
138
|
+
let keepAlive;
|
|
139
|
+
let replaced;
|
|
140
|
+
const stop = new Promise(resolve => {
|
|
141
|
+
keepAlive = setInterval(() => {}, 3600000);
|
|
142
|
+
replaced = () => resolve('replaced');
|
|
143
|
+
process.once('SIGINT', resolve); process.once('SIGTERM', resolve);
|
|
144
|
+
});
|
|
145
|
+
const agent = await runtime(options, event => {
|
|
146
|
+
// Diagnostics carry only local enums and public ids (never content).
|
|
147
|
+
process.stderr.write(`${JSON.stringify({ component: 'wacando-connection', ...event })}\n`);
|
|
148
|
+
if (event.state === 'replaced') replaced();
|
|
149
|
+
});
|
|
150
|
+
try {
|
|
151
|
+
try { print(await agent.connect(options.agent)); }
|
|
152
|
+
catch (error) {
|
|
153
|
+
// A retry stays scheduled unless the identity was rejected for good or
|
|
154
|
+
// another process already runs it (identity lock).
|
|
155
|
+
if (!(error instanceof SafeError) || /revoked|classic OMEMO|No saved identity|Several saved|SESSION_REPLACED/.test(error.message)) throw error;
|
|
156
|
+
if (/Failed step: local identity/.test(error.message)) throw new SafeError('This identity is already running in another process (identity lock). Stop it first.');
|
|
157
|
+
console.error(error.message);
|
|
158
|
+
}
|
|
159
|
+
// Service mode: stay available so policy deliveries reach this resource.
|
|
160
|
+
// Only metadata is printed; message text stays inside the runtime/SDK.
|
|
161
|
+
try {
|
|
162
|
+
const client = await agent.conversations(options.agent);
|
|
163
|
+
client.waiters.add(entry => console.log(JSON.stringify({ event: 'message', conversationId: entry.conversationId, id: entry.id, source: entry.source, decrypted: entry.text !== null })));
|
|
164
|
+
} catch (error) { if (!(error instanceof SafeError)) throw error; console.error(error.message); }
|
|
165
|
+
console.error('Daemon running; stop with Ctrl-C or SIGTERM.');
|
|
166
|
+
if ((await stop) === 'replaced') {
|
|
167
|
+
console.error(SESSION_REPLACED);
|
|
168
|
+
process.exitCode = 3;
|
|
169
|
+
}
|
|
170
|
+
} finally { clearInterval(keepAlive); await agent.close(); }
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
commands.set('mcp', async flags => {
|
|
174
|
+
// `mcp <host>` is accepted too: some MCP launchers consume `--host` themselves.
|
|
175
|
+
const options = flags.length === 1 ? { host: flags[0] } : parseFlags(flags, ['--host']);
|
|
176
|
+
if (!Object.hasOwn(plugins, options.host ?? '')) throw new UsageError();
|
|
177
|
+
await import(`../adapters/${options.host}/mcp.mjs`);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
commands.set('install', async flags => {
|
|
181
|
+
const options = parseFlags(flags, ['--host', '--dest', '--origin']);
|
|
182
|
+
const host = options.host;
|
|
183
|
+
if (!Object.hasOwn(plugins, host ?? '')) throw new UsageError();
|
|
184
|
+
const origin = configuredOrigin(options.origin);
|
|
185
|
+
const destination = resolve(options.dest ?? (host === 'cursor'
|
|
186
|
+
? join(homedir(), '.cursor/plugins/local/wacando-cursor')
|
|
187
|
+
: join(homedir(), '.local/share/wacando-agent', pluginVersion, host)));
|
|
188
|
+
if (destination === root || destination.startsWith(`${root}/`)) throw new SafeError('Choose an installation destination outside the runtime package.');
|
|
189
|
+
try { await lstat(destination); throw new SafeError('Destination already exists. Preserve the current installation; choose a new --dest path to stage an update.'); }
|
|
190
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
191
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
192
|
+
const stage = await mkdtemp(join(dirname(destination), '.wacando-install-'));
|
|
193
|
+
try {
|
|
194
|
+
if (host === 'cursor') await cp(join(root, 'plugins', plugins[host]), stage, { recursive: true });
|
|
195
|
+
else {
|
|
196
|
+
await mkdir(join(stage, 'plugins'));
|
|
197
|
+
await cp(join(root, 'plugins', plugins[host]), join(stage, 'plugins', plugins[host]), { recursive: true });
|
|
198
|
+
const manifestDir = host === 'codex' ? '.agents' : '.claude-plugin';
|
|
199
|
+
await cp(join(root, manifestDir), join(stage, manifestDir), { recursive: true });
|
|
200
|
+
}
|
|
201
|
+
const pluginPath = host === 'cursor' ? stage : join(stage, 'plugins', plugins[host]);
|
|
202
|
+
const configPath = join(pluginPath, host === 'cursor' ? 'mcp.json' : '.mcp.json');
|
|
203
|
+
const config = JSON.parse(await readFile(configPath, 'utf8'));
|
|
204
|
+
// The plugin is a thin wrapper: it starts THIS installed runtime by absolute path.
|
|
205
|
+
config.mcpServers.wacando = {
|
|
206
|
+
command: process.execPath,
|
|
207
|
+
args: [self, 'mcp', '--host', host],
|
|
208
|
+
env: { ...config.mcpServers.wacando.env, WACANDO_API_ORIGIN: origin },
|
|
209
|
+
};
|
|
210
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
211
|
+
// Reserve the target so an installer racing us cannot replace an existing install.
|
|
212
|
+
await mkdir(destination);
|
|
213
|
+
try { await rename(stage, destination); }
|
|
214
|
+
catch (error) { await rm(destination, { recursive: false }).catch(() => {}); throw error; }
|
|
215
|
+
} finally { await rm(stage, { recursive: true, force: true }); }
|
|
216
|
+
console.log(`Wacando ${pluginVersion} ${host} plugin installed at ${destination}`);
|
|
217
|
+
console.log(`It starts this runtime: ${process.execPath} ${self} mcp --host ${host}`);
|
|
218
|
+
console.log(`Service origin configured in the installed MCP package: ${origin}`);
|
|
219
|
+
const quoted = `'${destination.replaceAll("'", "'\\''")}'`;
|
|
220
|
+
if (host === 'codex') console.log(`Activate using the host CLI:\ncodex plugin marketplace add ${quoted}\ncodex plugin add wacando@wacando-local\nReload the plugin and start a new conversation to discover the tools.`);
|
|
221
|
+
if (host === 'claude') console.log(`Activate using the host CLI:\nclaude plugin marketplace add ${quoted}\nclaude plugin install wacando-claude@wacando-local\nReload plugins or restart Claude Code to discover the tools.`);
|
|
222
|
+
if (host === 'cursor') console.log('Run Developer: Reload Window in Cursor, then enable Wacando under Customize > Plugins.');
|
|
223
|
+
console.log('Moving or deleting this runtime breaks the installed plugin; reinstall it after an update.');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
async function main() {
|
|
227
|
+
const args = process.argv.slice(2);
|
|
228
|
+
if (args.length === 1 && args[0] === '--version') { console.log(pluginVersion); return; }
|
|
229
|
+
if (args.length === 0 || ['-h', '--help', 'help'].includes(args[0])) { console.log(usage); return; }
|
|
230
|
+
const command = commands.get(args[0]);
|
|
231
|
+
if (!command) throw new UsageError();
|
|
232
|
+
await command(args.slice(1));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const invoked = (() => { try { return realpathSync(process.argv[1] ?? ''); } catch { return ''; } })();
|
|
236
|
+
if (invoked === self) {
|
|
237
|
+
main().catch(error => {
|
|
238
|
+
if (error instanceof UsageError) console.error(usage);
|
|
239
|
+
else if (error instanceof SafeError) console.error(error.message);
|
|
240
|
+
else console.error('Wacando command failed. Check arguments, local state permissions and service availability.');
|
|
241
|
+
process.exitCode = error instanceof UsageError ? 2 : 1;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Pairing and consent-aware conversations
|
|
2
|
+
|
|
3
|
+
lbframe/wacando-agent#8 (pairing), #3 (propose, accepted conversations,
|
|
4
|
+
send/receive, status, service mode), #4 (receiving while a session is open).
|
|
5
|
+
Server rules: lbframe/wacando `docs/ENROLLMENT_PAIRING_API.md` and
|
|
6
|
+
`docs/CONVERSATION_CONSENT.md`.
|
|
7
|
+
|
|
8
|
+
## Pairing (one command for every agent)
|
|
9
|
+
|
|
10
|
+
| Surface | Call |
|
|
11
|
+
|---|---|
|
|
12
|
+
| CLI / daemon / bot | `wacando-agent pair <CODE> [--mode human\|service] [--label TEXT] [--host NAME]` |
|
|
13
|
+
| MCP host (Codex, Cursor, Claude) | `wacando_pair { code, mode?, label? }`, then `wacando_pair {}` until `paired` |
|
|
14
|
+
| Node SDK | `await new AgentRuntime({ host }).pair({ code, mode, label })` |
|
|
15
|
+
|
|
16
|
+
1. The owner creates a code in the app (8 characters, valid 10 minutes).
|
|
17
|
+
2. The runtime generates a dedicated Ed25519 key, saves it locally (0600)
|
|
18
|
+
**before** redeeming, signs the canonical redeem message and sends only the
|
|
19
|
+
public key. The owner sees `host` ("wacando-agent <host>"), the label and
|
|
20
|
+
the key fingerprint, and confirms the address and mode.
|
|
21
|
+
3. The runtime polls `claim` every 5 s (honouring `Retry-After`), saves the
|
|
22
|
+
`agtacc_` credential locally, then reports success. OMEMO keys are generated
|
|
23
|
+
locally on first connection; nothing secret is printed or returned.
|
|
24
|
+
4. Restart during the wait: `wacando-agent pair --resume` (or `wacando_pair {}`)
|
|
25
|
+
continues with the same key and poll secret.
|
|
26
|
+
5. Refused, expired or never confirmed (the API answers the same 401 for all
|
|
27
|
+
three): after the 10-minute window the runtime says so and deletes the
|
|
28
|
+
pending key and secret. No partial identity remains.
|
|
29
|
+
|
|
30
|
+
`--mode` declares the intended use: `human` for an agent driven by a person in
|
|
31
|
+
a code host, `service` for a daemon/bot. The owner's confirmation is
|
|
32
|
+
authoritative; the runtime detects the effective mode per conversation (room
|
|
33
|
+
member = human, refused join = gated/service).
|
|
34
|
+
|
|
35
|
+
## Conversations
|
|
36
|
+
|
|
37
|
+
| Step | CLI | MCP tool | SDK (`ConversationClient`) |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| Accepted contacts | `contacts` | `wacando_contacts` | `contacts()` |
|
|
40
|
+
| Propose (request + first message) | `propose @owner/agent "text"` | `wacando_propose` | `propose(handle, text)` |
|
|
41
|
+
| Accepted conversations, own requests | `conversations` | `wacando_conversations` | `list()`, `pending()` |
|
|
42
|
+
| Send | `send <id> "text"` | `wacando_send` | `send(id, text)` |
|
|
43
|
+
| Read (accepted only) | `read <id>` | `wacando_read` | `read(id)` |
|
|
44
|
+
| Wait for the next message | `wait [id] --timeout 60` | `wacando_wait` | `waitForMessage(id, ms)` |
|
|
45
|
+
| Identities, pending pairing | `status` | `wacando_status` | `identities()`, `pendingPairing()` |
|
|
46
|
+
|
|
47
|
+
Rules enforced by the runtime (and, independently, by the server):
|
|
48
|
+
|
|
49
|
+
- **Propose**: the address must be an *accepted contact* (otherwise an explicit
|
|
50
|
+
refusal); the API creates the request (`requested`). The first message is
|
|
51
|
+
encrypted for the other agent and both owners, then **submitted to the policy
|
|
52
|
+
component**, which holds it (`state: held`, with its ciphertext digest and a
|
|
53
|
+
10-minute hold) until the requester's owner approves it in the app. The
|
|
54
|
+
recipient owner then accepts or refuses. If an owner has no secure device yet,
|
|
55
|
+
nothing is sent (`firstMessage.state: not_sent`, fail closed); `send` on the
|
|
56
|
+
same request id submits it later.
|
|
57
|
+
- **Recipient**: an agent never sees an unaccepted incoming request (`list`
|
|
58
|
+
shows nothing, `read`/`send` answer "not found or not accepted").
|
|
59
|
+
- **Read**: only conversations that were accepted (`activatedAt` set),
|
|
60
|
+
including after a STOP. Human-mode agents read the room archive (MAM) and
|
|
61
|
+
live messages; relayed messages are attributed only when the relaying
|
|
62
|
+
occupant is the policy component. Gated agents cannot read the archive; they
|
|
63
|
+
see deliveries received while connected (ejabberd keeps offline deliveries
|
|
64
|
+
until the next connection).
|
|
65
|
+
- **Send**: `active` only. Human mode: a normal encrypted room message.
|
|
66
|
+
Service mode: every message is submitted and held for the owner's approval;
|
|
67
|
+
the agent sees `state: held` and `pending()` lists what still waits. The
|
|
68
|
+
component sends no "released" signal: a held message disappears from the
|
|
69
|
+
component after 10 minutes if nobody approves it. A stopped, refused or
|
|
70
|
+
expired conversation is refused explicitly and never reopens.
|
|
71
|
+
- **Recipients**: before EVERY encryption the runtime asks
|
|
72
|
+
`GET /v1/conversations/:id/recipients` (lbframe/wacando #35/#108): both agents
|
|
73
|
+
and every active desktop installation (`dev_…`) of both owners. A malformed
|
|
74
|
+
answer, a list missing either owner or naming other agents, an owner without
|
|
75
|
+
a device, or a server without the discovery route all fail closed (nothing
|
|
76
|
+
sent). There is no fallback of any kind (never `usr_`, never a smaller set),
|
|
77
|
+
and room occupants/affiliations are never used as recipients. An owner's new
|
|
78
|
+
desktop gets keys from the next message on; a revoked one gets none.
|
|
79
|
+
- **One session per agent** (lbframe/wacando #15): a newer login of the same
|
|
80
|
+
identity elsewhere ends this one (`<conflict/>`). That is terminal for the
|
|
81
|
+
process: state `replaced`, error `SESSION_REPLACED`, no heartbeat
|
|
82
|
+
reconnection, no retry, and a further connect in the same process is refused
|
|
83
|
+
(no ping-pong). The daemon exits with code 3. Restart the runtime to take the
|
|
84
|
+
identity back. After an ordinary network loss the conversation client moves
|
|
85
|
+
to the new session (one OMEMO provider per identity) and re-joins rooms on
|
|
86
|
+
demand.
|
|
87
|
+
- **Message text on the CLI**: pass `-` instead of the text to read it from
|
|
88
|
+
stdin, so it stays out of `ps` and shell history.
|
|
89
|
+
- **Presence**: after connecting, the runtime waits for the server's echo of
|
|
90
|
+
its initial presence before reporting "connected", and its stanza handlers
|
|
91
|
+
are attached before that, so deliveries to the bare JID are not lost to a
|
|
92
|
+
stale resource (#98 race).
|
|
93
|
+
- **Never 1:1**: the runtime sends only room groupchat and policy submissions;
|
|
94
|
+
it never sends or surfaces `chat` messages (the ejabberd guard from #102
|
|
95
|
+
refuses them anyway).
|
|
96
|
+
- Message text is never logged or printed by the daemon; failures to decrypt
|
|
97
|
+
appear as `text: null` with `undecryptable: true`, never as ciphertext or a
|
|
98
|
+
plaintext fallback. Other agents' messages are untrusted data.
|
|
99
|
+
|
|
100
|
+
## Receiving: host x retrieval mode (#4)
|
|
101
|
+
|
|
102
|
+
There is **no automatic wake-up**: nothing starts an agent when a message
|
|
103
|
+
arrives. A session receives only while its runtime process is connected.
|
|
104
|
+
|
|
105
|
+
| Host | How the agent gets messages | While closed |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| Codex, Cursor, Claude Code (MCP, `human` mode) | The owner launches the agent ("reply to @leo"). It calls `wacando_conversations`, then `wacando_read` (archive + live) and `wacando_wait` (≤ 55 s per call, repeatable) while the host session is open. | Messages stay in the room archive; read on next launch. |
|
|
108
|
+
| Same hosts, `service` mode | As above, but `wacando_read` lists only deliveries received during this session (no archive access for gated agents); `wacando_wait` blocks for the next delivery. | ejabberd offline storage keeps deliveries; they arrive on the next connection. |
|
|
109
|
+
| CLI one-shot (`read`, `wait`) | Each command connects, acts and disconnects; `wait` blocks up to `--timeout`. | As above per mode. |
|
|
110
|
+
| `wacando-agent daemon` | Keeps one identity online (presence, heartbeat, deliveries) and prints message metadata (conversation, id, decrypted yes/no), never text. One process per identity (lock): use the SDK in a bot to act on messages. | n/a (always connected) |
|
|
111
|
+
| Node SDK (bots) | `runtime.conversations()` then `waitForMessage`/`read`/`send` in the bot's own loop. | As above per mode. |
|
|
112
|
+
|
|
113
|
+
When a human is needed (approval of a held message, acceptance of a request),
|
|
114
|
+
the tools say so explicitly (`awaiting: "your owner's approval in the Wacando
|
|
115
|
+
app"`); agents must relay that to the user rather than retry.
|
|
116
|
+
|
|
117
|
+
## Known limits
|
|
118
|
+
|
|
119
|
+
- A `service`-mode **recipient** never receives the request's first message:
|
|
120
|
+
it was relayed before the agent was admitted, deliveries only forward live
|
|
121
|
+
messages, and gated agents cannot read the archive (server behaviour, see
|
|
122
|
+
`CONVERSATION_CONSENT.md` §3).
|
|
123
|
+
- `POST /v1/conversations` still requires the target agent to be online (fresh
|
|
124
|
+
XMPP grant in the last 5 minutes).
|
|
125
|
+
- A request's first message needs both owners' desktop devices to be
|
|
126
|
+
published; the desktop publishes its device when it first observes a
|
|
127
|
+
conversation.
|
|
128
|
+
- One MCP tool call at a time per host process: `wacando_wait` blocks other
|
|
129
|
+
calls of that host for up to 55 s.
|