@wacando/agent 2.1.0 → 2.3.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.
- package/README.md +1 -1
- package/STATUS.md +41 -0
- package/bin/wacando-agent.mjs +48 -3
- package/docs/AGENT_TOOLS.md +119 -1
- package/docs/ONBOARDING.md +5 -1
- package/docs/WACANDO_PACKAGES.md +10 -4
- package/package.json +2 -2
- package/packages/mcp/agent-tools.mjs +18 -1
- package/packages/mcp/server.mjs +1 -1
- package/packages/onboarding/network.mjs +36 -6
- package/packages/onboarding/service.mjs +4 -2
- package/packages/onboarding/version.mjs +1 -1
- package/packages/runtime/conversations.mjs +50 -7
- package/packages/runtime/files.mjs +777 -0
- package/packages/runtime/pairing.mjs +6 -1
- package/packages/runtime/runtime.mjs +44 -8
- package/packages/wacando/chunks/{chunk-GASYW3SG.mjs → chunk-53E7SSTF.mjs} +3 -2
- package/packages/wacando/chunks/{chunk-OI3RF7TS.mjs → chunk-PHGOH4AU.mjs} +2 -2
- package/packages/wacando/chunks/chunk-PZDQXGA6.mjs +74 -0
- package/packages/wacando/omemo-wire.mjs +3 -3
- package/packages/wacando/omemo.mjs +568 -11
- package/packages/wacando/xmpp.mjs +41 -3
- package/plugins/wacando/.codex-plugin/plugin.json +1 -1
- package/plugins/wacando/skills/wacando/SKILL.md +1 -0
- package/plugins/wacando-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/wacando-claude/skills/open/SKILL.md +1 -0
- package/plugins/wacando-cursor/.cursor-plugin/plugin.json +1 -1
- package/plugins/wacando-cursor/skills/wacando/SKILL.md +1 -0
- package/provenance.json +20 -17
- package/packages/wacando/chunks/chunk-EIGFLZ2Y.mjs +0 -23
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createPrivateKey, createPublicKey, generateKeyPairSync, randomBytes, sign } from 'node:crypto';
|
|
2
2
|
import { hostname } from 'node:os';
|
|
3
|
-
import { ApiError, post } from '../onboarding/network.mjs';
|
|
3
|
+
import { ApiError, apiNotFound, post } from '../onboarding/network.mjs';
|
|
4
4
|
import { SafeError } from '../onboarding/storage.mjs';
|
|
5
5
|
|
|
6
6
|
// Universal pairing (lbframe/wacando docs/ENROLLMENT_PAIRING_API.md, #36/#60).
|
|
@@ -71,6 +71,10 @@ export async function redeem({ origin, code, host, label, mode, saveKey, request
|
|
|
71
71
|
proof: proof(key.privateKeyPem, redeemMessage(code, key.publicKey, clientNonce, issuedAt)),
|
|
72
72
|
});
|
|
73
73
|
} catch (error) {
|
|
74
|
+
// Not the Wacando API (e.g. the web site answering an HTML 404): a wrong
|
|
75
|
+
// origin, never a code refusal. Every real API refusal keeps one message,
|
|
76
|
+
// so the answer reveals nothing about a code.
|
|
77
|
+
if (error instanceof ApiError && error.unknownRoute) throw new SafeError(apiNotFound(origin));
|
|
74
78
|
if (error instanceof ApiError && [400, 401, 403, 404].includes(error.status)) throw new SafeError(pairingErrors.refused);
|
|
75
79
|
if (error instanceof ApiError && error.status === 429) throw new SafeError('Too many pairing attempts. Wait and retry with the same code.');
|
|
76
80
|
throw error;
|
|
@@ -100,6 +104,7 @@ export async function claim({ pending, request = post, sleep, now = Date.now, de
|
|
|
100
104
|
return data;
|
|
101
105
|
} catch (error) {
|
|
102
106
|
if (!(error instanceof ApiError)) throw error;
|
|
107
|
+
if (error.unknownRoute) throw new SafeError(apiNotFound(pending.origin));
|
|
103
108
|
if (error.status === 429) wait = Math.max(POLL_INTERVAL_MS, (error.retryAfter ?? 5) * 1000);
|
|
104
109
|
else if (error.status !== 401) throw new SafeError(`Pairing service error (HTTP ${error.status}). Retry with the same command.`);
|
|
105
110
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { OnboardingService } from '../onboarding/service.mjs';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { CredentialStore, digest, SafeError } from '../onboarding/storage.mjs';
|
|
4
|
+
import { configuredOrigin, get, legacyOrigins, post } from '../onboarding/network.mjs';
|
|
4
5
|
import { ConversationClient } from './conversations.mjs';
|
|
6
|
+
import { FileTransfers, FileVault, uploadPin } from './files.mjs';
|
|
5
7
|
import { claim, pairingErrors, redeem } from './pairing.mjs';
|
|
6
8
|
|
|
7
9
|
const BUFFER_LIMIT = 200;
|
|
@@ -27,10 +29,16 @@ export function publicIdentity(agent, state) {
|
|
|
27
29
|
* and stop on revocation.
|
|
28
30
|
*/
|
|
29
31
|
export class AgentRuntime {
|
|
30
|
-
|
|
32
|
+
/**
|
|
33
|
+
* `files`: `{ uploadOrigin, quarantineDir }` (defaults: WACANDO_UPLOAD_ORIGIN
|
|
34
|
+
* or the API origin; WACANDO_QUARANTINE_DIR or a directory inside the
|
|
35
|
+
* protected runtime state) plus test hooks (`now`, `sleep`, `pollMs`, `fetch`).
|
|
36
|
+
*/
|
|
37
|
+
constructor({ host = 'cli', origin, store = new CredentialStore(), report, request, connect, onStanza, files = {} } = {}) {
|
|
31
38
|
if (!/^[a-z0-9][a-z0-9_-]{0,31}$/.test(host)) throw new SafeError('Invalid runtime host name.');
|
|
32
39
|
this.host = host;
|
|
33
40
|
this.originSetting = origin;
|
|
41
|
+
this.fileOptions = files;
|
|
34
42
|
this.store = store;
|
|
35
43
|
this.listeners = new Set();
|
|
36
44
|
this.clients = new Map();
|
|
@@ -47,16 +55,34 @@ export class AgentRuntime {
|
|
|
47
55
|
/** Configured service origin (validated on use, like the onboarding tools). */
|
|
48
56
|
get origin() { return configuredOrigin(this.originSetting); }
|
|
49
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Saved identity records for this origin and host. For the production API
|
|
60
|
+
* this includes identities saved under the former default origin (the web
|
|
61
|
+
* app, up to 2.2.0): they are used in place, under their saved origin, so
|
|
62
|
+
* their OMEMO device, file state and locks stay exactly where they are and
|
|
63
|
+
* an older runtime still finds them. Nothing is rewritten or moved.
|
|
64
|
+
*/
|
|
65
|
+
async savedAgents() {
|
|
66
|
+
const agents = [];
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
for (const origin of legacyOrigins(this.origin)) {
|
|
69
|
+
for (const agent of await this.store.list(origin, this.host, 'agent')) {
|
|
70
|
+
if (agent.origin !== origin || seen.has(agent.agentId)) continue;
|
|
71
|
+
seen.add(agent.agentId);
|
|
72
|
+
agents.push(agent);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return agents;
|
|
76
|
+
}
|
|
77
|
+
|
|
50
78
|
/** Saved identities for this origin and host (public fields only). */
|
|
51
79
|
async identities() {
|
|
52
|
-
|
|
53
|
-
return agents.filter(agent => agent.origin === this.origin)
|
|
54
|
-
.map(agent => publicIdentity(agent, this.service.states.get(agent.agentId)));
|
|
80
|
+
return (await this.savedAgents()).map(agent => publicIdentity(agent, this.service.states.get(agent.agentId)));
|
|
55
81
|
}
|
|
56
82
|
|
|
57
83
|
/** Resolve a saved identity by agent ID or handle; a single identity is the default. */
|
|
58
84
|
async resolve(selector) {
|
|
59
|
-
const agents =
|
|
85
|
+
const agents = await this.savedAgents();
|
|
60
86
|
const matches = selector === undefined ? agents
|
|
61
87
|
: agents.filter(agent => agent.agentId === selector || (handlePattern.test(selector) && agent.handle === selector));
|
|
62
88
|
if (matches.length === 0) throw new SafeError('No saved identity matches. Pair this runtime first (wacando-agent pair <code>).');
|
|
@@ -120,7 +146,8 @@ export class AgentRuntime {
|
|
|
120
146
|
let client = this.clients.get(agent.agentId);
|
|
121
147
|
if (client && client.session !== session) this.rebind(agent.agentId, session);
|
|
122
148
|
if (!client) {
|
|
123
|
-
|
|
149
|
+
const files = await this.fileTransfersFor(agent);
|
|
150
|
+
client = new ConversationClient({ runtime: this, agent, session, files });
|
|
124
151
|
this.clients.set(agent.agentId, client);
|
|
125
152
|
for (const stanza of this.buffered.get(agent.agentId) ?? []) client.handle(stanza);
|
|
126
153
|
this.buffered.delete(agent.agentId);
|
|
@@ -191,6 +218,15 @@ export class AgentRuntime {
|
|
|
191
218
|
return { paired: true, ...publicIdentity(agent) };
|
|
192
219
|
}
|
|
193
220
|
|
|
221
|
+
/** File transfer state of one identity: private directories, pinned upload origin. */
|
|
222
|
+
async fileTransfersFor(agent) {
|
|
223
|
+
const { uploadOrigin, quarantineDir = process.env.WACANDO_QUARANTINE_DIR, ...hooks } = this.fileOptions;
|
|
224
|
+
const pin = uploadPin(agent.origin, uploadOrigin ?? process.env.WACANDO_UPLOAD_ORIGIN);
|
|
225
|
+
const base = join(await this.store.directory(agent.origin, this.host), `files-${digest(agent.agentId)}`);
|
|
226
|
+
const vault = new FileVault(base, quarantineDir ? join(quarantineDir, digest(`${agent.origin}\n${agent.agentId}`).slice(0, 32)) : undefined);
|
|
227
|
+
return client => new FileTransfers(client, { vault, pin, ...hooks });
|
|
228
|
+
}
|
|
229
|
+
|
|
194
230
|
/** Subscribe to every stanza of every connected identity. Returns an unsubscribe function. */
|
|
195
231
|
onStanza(listener) {
|
|
196
232
|
this.listeners.add(listener);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// Embedded from lbframe/wacando@
|
|
1
|
+
// Embedded from lbframe/wacando@618922ecaf2213da18949a8b27d343c8767b64c5 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
|
|
2
2
|
import {
|
|
3
3
|
XMPP_DOMAIN
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-PZDQXGA6.mjs";
|
|
5
5
|
|
|
6
6
|
// packages/xmpp/src/muc.ts
|
|
7
7
|
import xml from "@xmpp/xml";
|
|
@@ -408,6 +408,7 @@ function isItemNotFound(err) {
|
|
|
408
408
|
}
|
|
409
409
|
|
|
410
410
|
export {
|
|
411
|
+
defaultTimeoutMs,
|
|
411
412
|
waitForStanza,
|
|
412
413
|
MucJoinError,
|
|
413
414
|
joinRoom,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// Embedded from lbframe/wacando@
|
|
1
|
+
// Embedded from lbframe/wacando@618922ecaf2213da18949a8b27d343c8767b64c5 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
|
|
2
2
|
import {
|
|
3
3
|
DomainError
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-PZDQXGA6.mjs";
|
|
5
5
|
|
|
6
6
|
// packages/omemo/src/ns.ts
|
|
7
7
|
var NS_OMEMO = "eu.siacs.conversations.axolotl";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Embedded from lbframe/wacando@618922ecaf2213da18949a8b27d343c8767b64c5 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
|
|
2
|
+
|
|
3
|
+
// packages/contracts/src/errors.ts
|
|
4
|
+
var DomainError = class extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "DomainError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// packages/contracts/src/files.ts
|
|
14
|
+
var ALLOWED_MEDIA_TYPES = [
|
|
15
|
+
"text/plain",
|
|
16
|
+
"text/markdown",
|
|
17
|
+
"application/pdf",
|
|
18
|
+
"application/zip",
|
|
19
|
+
"application/gzip"
|
|
20
|
+
];
|
|
21
|
+
var MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
22
|
+
var MIN_FILE_BYTES = 16;
|
|
23
|
+
var TEXT_HEAD_MIN_BYTES = 32;
|
|
24
|
+
function startsWith(bytes, sig) {
|
|
25
|
+
return bytes.length >= sig.length && sig.every((b, i) => bytes[i] === b);
|
|
26
|
+
}
|
|
27
|
+
function isGzipHeader(bytes) {
|
|
28
|
+
if (bytes.length < 10 || !startsWith(bytes, [31, 139, 8])) return false;
|
|
29
|
+
const flg = bytes[3];
|
|
30
|
+
const xfl = bytes[8];
|
|
31
|
+
const os = bytes[9];
|
|
32
|
+
return (flg & 224) === 0 && [0, 2, 4].includes(xfl) && (os <= 13 || os === 255);
|
|
33
|
+
}
|
|
34
|
+
function isTextHead(bytes) {
|
|
35
|
+
const head = bytes.subarray(0, Math.min(bytes.length, 512));
|
|
36
|
+
if (head.length < TEXT_HEAD_MIN_BYTES) return false;
|
|
37
|
+
const cuts = head.length < bytes.length ? [0, 1, 2, 3] : [0];
|
|
38
|
+
for (const cut of cuts) {
|
|
39
|
+
let text;
|
|
40
|
+
try {
|
|
41
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(head.subarray(0, head.length - cut));
|
|
42
|
+
} catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
return !/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(text);
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
function looksLikePlaintext(bytes) {
|
|
50
|
+
return startsWith(bytes, [37, 80, 68, 70, 45]) || // %PDF-
|
|
51
|
+
startsWith(bytes, [80, 75, 3, 4]) || // zip local file header
|
|
52
|
+
startsWith(bytes, [80, 75, 5, 6]) || // empty zip
|
|
53
|
+
isGzipHeader(bytes) || isTextHead(bytes);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// packages/contracts/src/internal.ts
|
|
57
|
+
function xmppDomain() {
|
|
58
|
+
const env = globalThis.process?.env;
|
|
59
|
+
return env?.XMPP_DOMAIN ?? "wacando.localhost";
|
|
60
|
+
}
|
|
61
|
+
var XMPP_DOMAIN = xmppDomain();
|
|
62
|
+
var XMPP_MUC_DOMAIN = `conference.${XMPP_DOMAIN}`;
|
|
63
|
+
|
|
64
|
+
// packages/contracts/src/message.ts
|
|
65
|
+
var MAX_MESSAGE_PLAINTEXT_BYTES = 64 * 1024;
|
|
66
|
+
|
|
67
|
+
export {
|
|
68
|
+
DomainError,
|
|
69
|
+
ALLOWED_MEDIA_TYPES,
|
|
70
|
+
MAX_FILE_BYTES,
|
|
71
|
+
MIN_FILE_BYTES,
|
|
72
|
+
looksLikePlaintext,
|
|
73
|
+
XMPP_DOMAIN
|
|
74
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Embedded from lbframe/wacando@
|
|
1
|
+
// Embedded from lbframe/wacando@618922ecaf2213da18949a8b27d343c8767b64c5 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
|
|
2
2
|
import {
|
|
3
3
|
BODY_MARKER,
|
|
4
4
|
GCM_IV_BYTES,
|
|
@@ -29,8 +29,8 @@ import {
|
|
|
29
29
|
signerJid,
|
|
30
30
|
signerRole,
|
|
31
31
|
targetJid
|
|
32
|
-
} from "./chunks/chunk-
|
|
33
|
-
import "./chunks/chunk-
|
|
32
|
+
} from "./chunks/chunk-PHGOH4AU.mjs";
|
|
33
|
+
import "./chunks/chunk-PZDQXGA6.mjs";
|
|
34
34
|
|
|
35
35
|
// packages/omemo/src/policy-wire.ts
|
|
36
36
|
import xml from "@xmpp/xml";
|