@tangle-network/agent-app 0.49.31 → 0.49.33
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.
|
@@ -1,35 +1,40 @@
|
|
|
1
|
-
import type { AgentProfile, LineVoiceOptions } from '@tangle-network/sandbox';
|
|
2
|
-
import { type Line } from '@tangle-network/sandbox/core';
|
|
1
|
+
import type { AgentProfile, BackendConfig, LineVoiceOptions } from '@tangle-network/sandbox';
|
|
2
|
+
import { type Line, Sandbox } from '@tangle-network/sandbox/core';
|
|
3
|
+
/** Native Hub options. The kit does not keep a second membership or execution store. */
|
|
4
|
+
type NativeAttachment = Parameters<Sandbox['lines']['attach']>[0];
|
|
5
|
+
export type HostedAgentAttachment = Partial<Omit<NativeAttachment, 'number' | 'mode' | 'respond'>>;
|
|
6
|
+
export type HostedAgentTransport = 'imessage' | 'whatsapp' | 'email';
|
|
7
|
+
export interface HostedAgentLineOptions {
|
|
8
|
+
transport?: HostedAgentTransport;
|
|
9
|
+
mode?: 'personal' | 'shared';
|
|
10
|
+
/** Required when creating a WhatsApp line on a connection with several numbers. */
|
|
11
|
+
phoneNumberId?: string;
|
|
12
|
+
voice?: LineVoiceOptions;
|
|
13
|
+
}
|
|
3
14
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* every box, model turn, reply and call.
|
|
7
|
-
*
|
|
8
|
-
* The platform does the work. {@link HostedAgent.attachLine} attaches the
|
|
9
|
-
* line to Tangle Hub with one sandbox per person: Hub routes each text and
|
|
10
|
-
* call to the person's box (the named instance {@link PERSON_KEY_PREFIX} plus
|
|
11
|
-
* a hash of their number), keeps one thread per person, handles STOP and
|
|
12
|
-
* START, counts each person's texts per day, and sends the reply. A call on
|
|
13
|
-
* the line runs in the same box and thread, so text and voice share one memory.
|
|
15
|
+
* Hub owns delivery, member threads, consent and execution. A host keeps its
|
|
16
|
+
* product catalog and billing policy, then passes that policy to this kit.
|
|
14
17
|
*/
|
|
15
18
|
export interface HostedAgentConfig {
|
|
16
|
-
/**
|
|
17
|
-
apiKey
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
* runs {@link DEFAULT_HOSTED_MODEL}, and a profile without `tools` runs with
|
|
21
|
-
* {@link CONVERSATION_TOOLS_OFF} turned off. Set `tools` to choose your own,
|
|
22
|
-
* for example on a harness that cannot turn those tools off.
|
|
23
|
-
*/
|
|
19
|
+
/** Developer's Tangle API key. Required unless an authenticated client is supplied. */
|
|
20
|
+
apiKey?: string;
|
|
21
|
+
/** Reuse the host's authenticated SDK client, including its timeout and tracing. */
|
|
22
|
+
client?: Pick<Sandbox, 'lines'>;
|
|
24
23
|
profile: AgentProfile;
|
|
25
|
-
/** Backend harness type, such as `opencode`; the runtime default when omitted. */
|
|
26
24
|
harness?: string;
|
|
27
|
-
/**
|
|
25
|
+
/** A saved release can supply its complete, already-validated backend unchanged. */
|
|
26
|
+
backend?: BackendConfig;
|
|
27
|
+
/** E.164 phone, or an email address (also accepted as an iMessage Apple ID). */
|
|
28
28
|
owner: string;
|
|
29
|
-
/** Texts Hub answers per person per UTC day. Default 20. */
|
|
30
29
|
freeTurnsPerDay?: number;
|
|
31
30
|
box?: Partial<BoxPolicy>;
|
|
32
31
|
sandboxUrl?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Host-selected members, roles, limits, instance namespace and reference.
|
|
34
|
+
* Use a distinct instance.keyPrefix per assistant to keep their disks apart.
|
|
35
|
+
* Supplying this never bypasses Hub's validation or owner checks.
|
|
36
|
+
*/
|
|
37
|
+
attachment?: HostedAgentAttachment;
|
|
33
38
|
}
|
|
34
39
|
export interface BoxPolicy {
|
|
35
40
|
cpuCores: number;
|
|
@@ -38,37 +43,12 @@ export interface BoxPolicy {
|
|
|
38
43
|
idleTimeoutSeconds: number;
|
|
39
44
|
maxLifetimeSeconds: number;
|
|
40
45
|
deleteAfterStoppedSeconds: number;
|
|
41
|
-
/** Egress allow-list. The default reaches the model router only. */
|
|
42
46
|
allowDomains: string[];
|
|
43
47
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Two cores and a 2 GB disk cost what one core and 10 GB cost: both bill the
|
|
46
|
-
* platform's hourly floor. A person's box starts OpenCode on their first text
|
|
47
|
-
* and after every idle stop, and that start is CPU-bound: on one core it took
|
|
48
|
-
* 6.1 s after a resume and 8.4 s on a new box, on two cores 3.5 s and 3.4 s
|
|
49
|
-
* (production, 2026-09-24). A disk no larger than the platform's warm seed
|
|
50
|
-
* lets a new person's box be claimed from the warm pool: create took 2.1-2.7 s
|
|
51
|
-
* instead of 5.3-7.0 s (2026-09-25).
|
|
52
|
-
*/
|
|
53
48
|
export declare const DEFAULT_BOX_POLICY: BoxPolicy;
|
|
54
|
-
/**
|
|
55
|
-
* Each person's box is the developer's named instance with this prefix. It is
|
|
56
|
-
* the key this kit used before Hub routed its texts, so every existing
|
|
57
|
-
* person keeps their box.
|
|
58
|
-
*/
|
|
49
|
+
/** Legacy default. Hosts running several assistants should supply their own namespace. */
|
|
59
50
|
export declare const PERSON_KEY_PREFIX = "hosted:";
|
|
60
|
-
/**
|
|
61
|
-
* Harness tools a texting or calling assistant does not use. Their
|
|
62
|
-
* descriptions present every turn as coding work: a shell, file search,
|
|
63
|
-
* sub-agents, to-do lists, skills and web fetch. File read, write and edit
|
|
64
|
-
* stay, so a persona can keep notes such as `memory.md`.
|
|
65
|
-
*/
|
|
66
51
|
export declare const CONVERSATION_TOOLS_OFF: readonly ['bash', 'glob', 'grep', 'task', 'todowrite', 'webfetch', 'skill'];
|
|
67
|
-
/**
|
|
68
|
-
* The model for a profile without `model.default`. It gave the most useful
|
|
69
|
-
* on-topic replies among four Router models on the same five texts and calls
|
|
70
|
-
* (2026-09-23), within the latency of the others.
|
|
71
|
-
*/
|
|
72
52
|
export declare const DEFAULT_HOSTED_MODEL = "openai/gpt-5.6-luna";
|
|
73
53
|
export declare class HostedAgentError extends Error {
|
|
74
54
|
readonly code: string;
|
|
@@ -76,27 +56,18 @@ export declare class HostedAgentError extends Error {
|
|
|
76
56
|
}
|
|
77
57
|
export declare function createHostedAgent(config: HostedAgentConfig): {
|
|
78
58
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* (`DELETE /v1/lines/:id/attachment`). Each person keeps their box and
|
|
90
|
-
* its memory, and their thread starts over. Remove any Hub event
|
|
91
|
-
* subscription on the connection first; Hub refuses a line that another
|
|
92
|
-
* route would also answer.
|
|
59
|
+
* Create an address from an owned connection and attach it. Shared email
|
|
60
|
+
* admits declared members only. Hub authenticates each mailbox by reply.
|
|
61
|
+
* Other shared transports can admit isolated guests. Personal admits one
|
|
62
|
+
* owner. Remove competing subscriptions before calling this method.
|
|
63
|
+
*/
|
|
64
|
+
attachLine(connectionId: string, options?: HostedAgentLineOptions): Promise<Line>;
|
|
65
|
+
/**
|
|
66
|
+
* Attach an already-created owner-scoped line. Used during a host cutover
|
|
67
|
+
* so the line id, connection, named-instance namespace and payer survive.
|
|
68
|
+
* A 409 remains a 409. This method never detaches a competing attachment.
|
|
93
69
|
*/
|
|
94
|
-
|
|
95
|
-
transport?: 'imessage' | 'email' | 'whatsapp';
|
|
96
|
-
mode?: 'personal' | 'shared';
|
|
97
|
-
/** Required for WhatsApp because one Linq connection may own several numbers. */
|
|
98
|
-
phoneNumberId?: string;
|
|
99
|
-
voice?: LineVoiceOptions;
|
|
100
|
-
}): Promise<Line>;
|
|
70
|
+
attachExistingLine(lineId: string, options?: HostedAgentLineOptions): Promise<Line>;
|
|
101
71
|
};
|
|
102
72
|
export type HostedAgent = ReturnType<typeof createHostedAgent>;
|
|
73
|
+
export {};
|
|
@@ -16,11 +16,8 @@ function conversationProfile(profile) {
|
|
|
16
16
|
return {
|
|
17
17
|
...profile,
|
|
18
18
|
model: { ...profile.model, default: profile.model?.default ?? DEFAULT_HOSTED_MODEL },
|
|
19
|
-
// A profile that sets `tools` owns its tool set.
|
|
20
19
|
...profile.tools ? {} : {
|
|
21
20
|
tools: Object.fromEntries(CONVERSATION_TOOLS_OFF.map((tool) => [tool, false])),
|
|
22
|
-
// The sandbox's preview policy grants the shell unless its permission
|
|
23
|
-
// is denied, so turning the tool off alone leaves the shell in place.
|
|
24
21
|
permissions: { bash: "deny", ...profile.permissions }
|
|
25
22
|
}
|
|
26
23
|
};
|
|
@@ -37,10 +34,16 @@ var HostedAgentError = class extends Error {
|
|
|
37
34
|
code;
|
|
38
35
|
};
|
|
39
36
|
function createHostedAgent(config) {
|
|
40
|
-
if (!E164.test(config.owner) && !EMAIL.test(config.owner))
|
|
37
|
+
if (!E164.test(config.owner) && !EMAIL.test(config.owner))
|
|
38
|
+
throw new HostedAgentError("owner_not_e164", "owner must be an E.164 phone number or email address.");
|
|
39
|
+
if (!config.client && !config.apiKey)
|
|
40
|
+
throw new HostedAgentError("credential_required", "Supply an API key or an authenticated Sandbox client.");
|
|
41
41
|
const policy = { ...DEFAULT_BOX_POLICY, ...config.box };
|
|
42
|
-
const sandbox = new Sandbox({ apiKey: config.apiKey, baseUrl: config.sandboxUrl ?? "https://sandbox.tangle.tools", timeoutMs: 2e4 });
|
|
43
|
-
const backend =
|
|
42
|
+
const sandbox = config.client ?? new Sandbox({ apiKey: config.apiKey, baseUrl: config.sandboxUrl ?? "https://sandbox.tangle.tools", timeoutMs: 2e4 });
|
|
43
|
+
const backend = config.backend ?? {
|
|
44
|
+
...config.harness ? { type: config.harness } : {},
|
|
45
|
+
profile: conversationProfile(config.profile)
|
|
46
|
+
};
|
|
44
47
|
const create = {
|
|
45
48
|
name: "hosted-person",
|
|
46
49
|
resources: { cpuCores: policy.cpuCores, memoryMB: policy.memoryMB, diskGB: policy.diskGB },
|
|
@@ -49,52 +52,77 @@ function createHostedAgent(config) {
|
|
|
49
52
|
maxLifetimeSeconds: policy.maxLifetimeSeconds,
|
|
50
53
|
deleteAfterStoppedSeconds: policy.deleteAfterStoppedSeconds
|
|
51
54
|
};
|
|
55
|
+
const members = config.attachment?.members ?? [{ address: config.owner, role: "owner" }];
|
|
56
|
+
function validate(transport, options, creating) {
|
|
57
|
+
if (!["imessage", "whatsapp", "email"].includes(transport))
|
|
58
|
+
throw new HostedAgentError("transport_unsupported", "Use an iMessage, WhatsApp or email line.");
|
|
59
|
+
if (options.mode !== void 0 && options.mode !== "personal" && options.mode !== "shared")
|
|
60
|
+
throw new HostedAgentError("mode_invalid", "mode must be personal or shared.");
|
|
61
|
+
for (const address of [config.owner, ...members.map((member) => member.address)]) {
|
|
62
|
+
const valid = transport === "email" ? EMAIL.test(address) : transport === "whatsapp" ? E164.test(address) : E164.test(address) || EMAIL.test(address);
|
|
63
|
+
if (!valid) throw new HostedAgentError("owner_transport_mismatch", `Member address does not match ${transport}.`);
|
|
64
|
+
}
|
|
65
|
+
if (creating && transport === "whatsapp" && !options.phoneNumberId)
|
|
66
|
+
throw new HostedAgentError("phone_number_required", "WhatsApp lines require phoneNumberId.");
|
|
67
|
+
if (transport !== "whatsapp" && options.phoneNumberId)
|
|
68
|
+
throw new HostedAgentError("phone_number_not_allowed", "phoneNumberId is only valid for WhatsApp lines.");
|
|
69
|
+
if (options.voice && transport !== "imessage")
|
|
70
|
+
throw new HostedAgentError("voice_transport_unsupported", "Voice is supported only on iMessage lines.");
|
|
71
|
+
if (transport === "email" && config.attachment?.unknownSenders && config.attachment.unknownSenders !== "reject")
|
|
72
|
+
throw new HostedAgentError("email_declared_members_required", "Email lines admit declared members only.");
|
|
73
|
+
}
|
|
74
|
+
async function attach(line, options) {
|
|
75
|
+
const transport = line.transport;
|
|
76
|
+
validate(transport, options, false);
|
|
77
|
+
if (options.transport && options.transport !== transport)
|
|
78
|
+
throw new HostedAgentError("line_transport_mismatch", "The existing line uses another transport.");
|
|
79
|
+
if (options.phoneNumberId && line.providerNumberId !== options.phoneNumberId)
|
|
80
|
+
throw new HostedAgentError("line_number_mismatch", "The existing line is pinned to another WhatsApp number.");
|
|
81
|
+
const retained = line.attachment?.status === "active" ? line.attachment : void 0;
|
|
82
|
+
if (retained?.unknownSenders === "onboard")
|
|
83
|
+
throw new HostedAgentError("line_policy_migration_required", "This line uses onboard admission. Manage it through Hub; the hosted-agent kit will not replace its policy.");
|
|
84
|
+
const mode = options.mode ?? retained?.mode ?? "shared";
|
|
85
|
+
const declaredMembers = config.attachment?.members ?? (retained ? (await sandbox.lines.members(line.id).list()).filter((member) => member.source === "declared").map((member) => ({ address: member.address, role: member.role, label: member.label ?? void 0 })) : members);
|
|
86
|
+
const keyPrefix = line.attachment?.instance?.keyPrefix ?? (line.clientReference === "hosted-agent" ? PERSON_KEY_PREFIX : `${PERSON_KEY_PREFIX}${line.id}:`);
|
|
87
|
+
await sandbox.lines.attach({
|
|
88
|
+
...config.attachment,
|
|
89
|
+
number: line.id,
|
|
90
|
+
mode,
|
|
91
|
+
members: declaredMembers,
|
|
92
|
+
unknownSenders: config.attachment?.unknownSenders ?? retained?.unknownSenders ?? (mode === "shared" && transport !== "email" ? "guest" : "reject"),
|
|
93
|
+
roles: config.attachment?.roles ?? retained?.roles ?? (mode === "shared" ? { owner: PERSON, member: PERSON, guest: PERSON } : { owner: PERSON }),
|
|
94
|
+
respond: { kind: "agent", backend },
|
|
95
|
+
limits: {
|
|
96
|
+
...retained?.limits,
|
|
97
|
+
turnsPerMemberPerDay: config.freeTurnsPerDay ?? retained?.limits.turnsPerMemberPerDay ?? 20,
|
|
98
|
+
...config.attachment?.limits
|
|
99
|
+
},
|
|
100
|
+
instance: config.attachment?.instance ?? retained?.instance ?? { keyPrefix, create },
|
|
101
|
+
clientReference: config.attachment?.clientReference ?? retained?.clientReference ?? "hosted-agent"
|
|
102
|
+
});
|
|
103
|
+
if (options.voice) await sandbox.lines.enableVoice(line.id, options.voice);
|
|
104
|
+
return sandbox.lines.get(line.id);
|
|
105
|
+
}
|
|
52
106
|
return {
|
|
53
107
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* each get their own box and thread; `personal` admits the owner only.
|
|
59
|
-
* With `voice` (iMessage lines only), calls to the
|
|
60
|
-
* line reach the caller's box and thread through that ph0ny agent; Hub
|
|
61
|
-
* admits only members, so a caller texts once before calling. Safe to
|
|
62
|
-
* repeat with the same config. Hub refuses a changed profile, box or
|
|
63
|
-
* limit on an attached line: detach it first
|
|
64
|
-
* (`DELETE /v1/lines/:id/attachment`). Each person keeps their box and
|
|
65
|
-
* its memory, and their thread starts over. Remove any Hub event
|
|
66
|
-
* subscription on the connection first; Hub refuses a line that another
|
|
67
|
-
* route would also answer.
|
|
108
|
+
* Create an address from an owned connection and attach it. Shared email
|
|
109
|
+
* admits declared members only. Hub authenticates each mailbox by reply.
|
|
110
|
+
* Other shared transports can admit isolated guests. Personal admits one
|
|
111
|
+
* owner. Remove competing subscriptions before calling this method.
|
|
68
112
|
*/
|
|
69
113
|
async attachLine(connectionId, options = {}) {
|
|
70
114
|
const transport = options.transport ?? "imessage";
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const line = await sandbox.lines.fromConnection(
|
|
83
|
-
transport === "whatsapp" ? { connectionId, transport, phoneNumberId: options.phoneNumberId, clientReference: "hosted-agent" } : { connectionId, transport, clientReference: "hosted-agent" }
|
|
84
|
-
);
|
|
85
|
-
await sandbox.lines.attach({
|
|
86
|
-
number: line.id,
|
|
87
|
-
mode,
|
|
88
|
-
members: [{ address: config.owner, role: "owner" }],
|
|
89
|
-
unknownSenders: mode === "shared" ? "guest" : "reject",
|
|
90
|
-
roles: mode === "shared" ? { owner: PERSON, guest: PERSON } : { owner: PERSON },
|
|
91
|
-
respond: { kind: "agent", backend },
|
|
92
|
-
limits: { turnsPerMemberPerDay: config.freeTurnsPerDay ?? 20 },
|
|
93
|
-
instance: { keyPrefix: PERSON_KEY_PREFIX, create },
|
|
94
|
-
clientReference: "hosted-agent"
|
|
95
|
-
});
|
|
96
|
-
if (options.voice) await sandbox.lines.enableVoice(line.id, options.voice);
|
|
97
|
-
return sandbox.lines.get(line.id);
|
|
115
|
+
validate(transport, options, true);
|
|
116
|
+
const line = await sandbox.lines.fromConnection(transport === "whatsapp" ? { connectionId, transport, phoneNumberId: options.phoneNumberId } : { connectionId, transport });
|
|
117
|
+
return attach(line, options);
|
|
118
|
+
},
|
|
119
|
+
/**
|
|
120
|
+
* Attach an already-created owner-scoped line. Used during a host cutover
|
|
121
|
+
* so the line id, connection, named-instance namespace and payer survive.
|
|
122
|
+
* A 409 remains a 409. This method never detaches a competing attachment.
|
|
123
|
+
*/
|
|
124
|
+
async attachExistingLine(lineId, options = {}) {
|
|
125
|
+
return attach(await sandbox.lines.get(lineId), options);
|
|
98
126
|
}
|
|
99
127
|
};
|
|
100
128
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hosted-agent/index.ts"],"sourcesContent":["import type { AgentProfile, BackendConfig, LineVoiceOptions } from '@tangle-network/sandbox'\nimport { type Line, type LineInstanceCreate, Sandbox } from '@tangle-network/sandbox/core'\n\n/**\n * A hosted agent: people text or call a line, and each person is answered\n * from their own isolated sandbox. The developer's Tangle API key pays for\n * every box, model turn, reply and call.\n *\n * The platform does the work. {@link HostedAgent.attachLine} attaches the\n * line to Tangle Hub with one sandbox per person: Hub routes each text and\n * call to the person's box (the named instance {@link PERSON_KEY_PREFIX} plus\n * a hash of their number), keeps one thread per person, handles STOP and\n * START, counts each person's texts per day, and sends the reply. A call on\n * the line runs in the same box and thread, so text and voice share one memory.\n */\n\nexport interface HostedAgentConfig {\n /** The developer's Tangle API key. */\n apiKey: string\n /**\n * The persona every person's box runs. A profile without `model.default`\n * runs {@link DEFAULT_HOSTED_MODEL}, and a profile without `tools` runs with\n * {@link CONVERSATION_TOOLS_OFF} turned off. Set `tools` to choose your own,\n * for example on a harness that cannot turn those tools off.\n */\n profile: AgentProfile\n /** Backend harness type, such as `opencode`; the runtime default when omitted. */\n harness?: string\n /** The owner's address on the chosen transport: E.164 phone or email. */\n owner: string\n /** Texts Hub answers per person per UTC day. Default 20. */\n freeTurnsPerDay?: number\n box?: Partial<BoxPolicy>\n sandboxUrl?: string\n}\n\nexport interface BoxPolicy {\n cpuCores: number\n memoryMB: number\n diskGB: number\n idleTimeoutSeconds: number\n maxLifetimeSeconds: number\n deleteAfterStoppedSeconds: number\n /** Egress allow-list. The default reaches the model router only. */\n allowDomains: string[]\n}\n\n/**\n * Two cores and a 2 GB disk cost what one core and 10 GB cost: both bill the\n * platform's hourly floor. A person's box starts OpenCode on their first text\n * and after every idle stop, and that start is CPU-bound: on one core it took\n * 6.1 s after a resume and 8.4 s on a new box, on two cores 3.5 s and 3.4 s\n * (production, 2026-09-24). A disk no larger than the platform's warm seed\n * lets a new person's box be claimed from the warm pool: create took 2.1-2.7 s\n * instead of 5.3-7.0 s (2026-09-25).\n */\nexport const DEFAULT_BOX_POLICY: BoxPolicy = {\n cpuCores: 2, memoryMB: 2048, diskGB: 2,\n idleTimeoutSeconds: 600, maxLifetimeSeconds: 86_400, deleteAfterStoppedSeconds: 7 * 86_400,\n allowDomains: ['router.tangle.tools'],\n}\n\n/**\n * Each person's box is the developer's named instance with this prefix. It is\n * the key this kit used before Hub routed its texts, so every existing\n * person keeps their box.\n */\nexport const PERSON_KEY_PREFIX = 'hosted:'\n\n/**\n * Harness tools a texting or calling assistant does not use. Their\n * descriptions present every turn as coding work: a shell, file search,\n * sub-agents, to-do lists, skills and web fetch. File read, write and edit\n * stay, so a persona can keep notes such as `memory.md`.\n */\nexport const CONVERSATION_TOOLS_OFF = ['bash', 'glob', 'grep', 'task', 'todowrite', 'webfetch', 'skill'] as const\n\n/**\n * The model for a profile without `model.default`. It gave the most useful\n * on-topic replies among four Router models on the same five texts and calls\n * (2026-09-23), within the latency of the others.\n */\nexport const DEFAULT_HOSTED_MODEL = 'openai/gpt-5.6-luna'\n\n/** The profile a person's box runs: the developer's profile over the conversation defaults. */\nfunction conversationProfile(profile: AgentProfile): AgentProfile {\n return {\n ...profile,\n model: { ...profile.model, default: profile.model?.default ?? DEFAULT_HOSTED_MODEL },\n // A profile that sets `tools` owns its tool set.\n ...(profile.tools ? {} : {\n tools: Object.fromEntries(CONVERSATION_TOOLS_OFF.map(tool => [tool, false])),\n // The sandbox's preview policy grants the shell unless its permission\n // is denied, so turning the tool off alone leaves the shell in place.\n permissions: { bash: 'deny' as const, ...profile.permissions },\n }),\n }\n}\n\n/** Each person runs in their own box, so no member shares a disk and each may use the persona's tools. */\nconst PERSON = { context: 'own', tools: 'act' } as const\nconst E164 = /^\\+[1-9]\\d{6,14}$/\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nexport class HostedAgentError extends Error {\n constructor(readonly code: string, message: string) { super(message); this.name = 'HostedAgentError' }\n}\n\nexport function createHostedAgent(config: HostedAgentConfig) {\n if (!E164.test(config.owner) && !EMAIL.test(config.owner)) throw new HostedAgentError('owner_not_e164', 'owner must be an E.164 phone number or email address.')\n const policy = { ...DEFAULT_BOX_POLICY, ...config.box }\n const sandbox = new Sandbox({ apiKey: config.apiKey, baseUrl: config.sandboxUrl ?? 'https://sandbox.tangle.tools', timeoutMs: 20_000 })\n const backend: BackendConfig = { ...(config.harness ? { type: config.harness as BackendConfig['type'] } : {}), profile: conversationProfile(config.profile) }\n const create: LineInstanceCreate = {\n name: 'hosted-person',\n resources: { cpuCores: policy.cpuCores, memoryMB: policy.memoryMB, diskGB: policy.diskGB },\n egressPolicy: { mode: 'strict', allowDomains: policy.allowDomains, includeImplicitDomains: false },\n idleTimeoutSeconds: policy.idleTimeoutSeconds, maxLifetimeSeconds: policy.maxLifetimeSeconds,\n deleteAfterStoppedSeconds: policy.deleteAfterStoppedSeconds,\n }\n\n return {\n /**\n * Attach a Hub connection the developer owns as this agent's line:\n * an Inkbox iMessage identity (default), its email mailbox, or a Linq\n * WhatsApp number (`phoneNumberId`). The owner's address must match the\n * transport. In `shared` mode (default) the owner and anyone who texts it\n * each get their own box and thread; `personal` admits the owner only.\n * With `voice` (iMessage lines only), calls to the\n * line reach the caller's box and thread through that ph0ny agent; Hub\n * admits only members, so a caller texts once before calling. Safe to\n * repeat with the same config. Hub refuses a changed profile, box or\n * limit on an attached line: detach it first\n * (`DELETE /v1/lines/:id/attachment`). Each person keeps their box and\n * its memory, and their thread starts over. Remove any Hub event\n * subscription on the connection first; Hub refuses a line that another\n * route would also answer.\n */\n async attachLine(connectionId: string, options: {\n transport?: 'imessage' | 'email' | 'whatsapp'\n mode?: 'personal' | 'shared'\n /** Required for WhatsApp because one Linq connection may own several numbers. */\n phoneNumberId?: string\n voice?: LineVoiceOptions\n } = {}): Promise<Line> {\n const transport = options.transport ?? 'imessage'\n const mode = options.mode ?? 'shared'\n if (transport === 'email' && !EMAIL.test(config.owner))\n throw new HostedAgentError('owner_transport_mismatch', 'email lines require an email owner address.')\n if (transport !== 'email' && !E164.test(config.owner))\n throw new HostedAgentError('owner_transport_mismatch', `${transport} lines require an E.164 owner address.`)\n if (transport === 'whatsapp' && !options.phoneNumberId)\n throw new HostedAgentError('phone_number_required', 'WhatsApp lines require phoneNumberId.')\n if (transport !== 'whatsapp' && options.phoneNumberId)\n throw new HostedAgentError('phone_number_not_allowed', 'phoneNumberId is only valid for WhatsApp lines.')\n if (options.voice && transport !== 'imessage')\n throw new HostedAgentError('voice_transport_unsupported', 'Voice is supported only on iMessage lines.')\n const line = await sandbox.lines.fromConnection(\n transport === 'whatsapp'\n ? { connectionId, transport, phoneNumberId: options.phoneNumberId!, clientReference: 'hosted-agent' }\n : { connectionId, transport, clientReference: 'hosted-agent' },\n )\n await sandbox.lines.attach({\n number: line.id,\n mode,\n members: [{ address: config.owner, role: 'owner' }],\n unknownSenders: mode === 'shared' ? 'guest' : 'reject',\n roles: mode === 'shared' ? { owner: PERSON, guest: PERSON } : { owner: PERSON },\n respond: { kind: 'agent', backend },\n limits: { turnsPerMemberPerDay: config.freeTurnsPerDay ?? 20 },\n instance: { keyPrefix: PERSON_KEY_PREFIX, create },\n clientReference: 'hosted-agent',\n })\n if (options.voice) await sandbox.lines.enableVoice(line.id, options.voice)\n return sandbox.lines.get(line.id)\n },\n }\n}\n\nexport type HostedAgent = ReturnType<typeof createHostedAgent>\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/hosted-agent/index.ts"],"sourcesContent":["import type { AgentProfile, BackendConfig, LineVoiceOptions } from '@tangle-network/sandbox'\nimport { type Line, type LineInstanceCreate, Sandbox } from '@tangle-network/sandbox/core'\n\n/** Native Hub options. The kit does not keep a second membership or execution store. */\ntype NativeAttachment = Parameters<Sandbox['lines']['attach']>[0]\nexport type HostedAgentAttachment = Partial<Omit<NativeAttachment, 'number' | 'mode' | 'respond'>>\nexport type HostedAgentTransport = 'imessage' | 'whatsapp' | 'email'\n\nexport interface HostedAgentLineOptions {\n transport?: HostedAgentTransport\n mode?: 'personal' | 'shared'\n /** Required when creating a WhatsApp line on a connection with several numbers. */\n phoneNumberId?: string\n voice?: LineVoiceOptions\n}\n\n/**\n * Hub owns delivery, member threads, consent and execution. A host keeps its\n * product catalog and billing policy, then passes that policy to this kit.\n */\nexport interface HostedAgentConfig {\n /** Developer's Tangle API key. Required unless an authenticated client is supplied. */\n apiKey?: string\n /** Reuse the host's authenticated SDK client, including its timeout and tracing. */\n client?: Pick<Sandbox, 'lines'>\n profile: AgentProfile\n harness?: string\n /** A saved release can supply its complete, already-validated backend unchanged. */\n backend?: BackendConfig\n /** E.164 phone, or an email address (also accepted as an iMessage Apple ID). */\n owner: string\n freeTurnsPerDay?: number\n box?: Partial<BoxPolicy>\n sandboxUrl?: string\n /**\n * Host-selected members, roles, limits, instance namespace and reference.\n * Use a distinct instance.keyPrefix per assistant to keep their disks apart.\n * Supplying this never bypasses Hub's validation or owner checks.\n */\n attachment?: HostedAgentAttachment\n}\n\nexport interface BoxPolicy {\n cpuCores: number\n memoryMB: number\n diskGB: number\n idleTimeoutSeconds: number\n maxLifetimeSeconds: number\n deleteAfterStoppedSeconds: number\n allowDomains: string[]\n}\n\nexport const DEFAULT_BOX_POLICY: BoxPolicy = {\n cpuCores: 2, memoryMB: 2048, diskGB: 2,\n idleTimeoutSeconds: 600, maxLifetimeSeconds: 86_400, deleteAfterStoppedSeconds: 7 * 86_400,\n allowDomains: ['router.tangle.tools'],\n}\n\n/** Legacy default. Hosts running several assistants should supply their own namespace. */\nexport const PERSON_KEY_PREFIX = 'hosted:'\nexport const CONVERSATION_TOOLS_OFF = ['bash', 'glob', 'grep', 'task', 'todowrite', 'webfetch', 'skill'] as const\nexport const DEFAULT_HOSTED_MODEL = 'openai/gpt-5.6-luna'\n\nfunction conversationProfile(profile: AgentProfile): AgentProfile {\n return {\n ...profile,\n model: { ...profile.model, default: profile.model?.default ?? DEFAULT_HOSTED_MODEL },\n ...(profile.tools ? {} : {\n tools: Object.fromEntries(CONVERSATION_TOOLS_OFF.map(tool => [tool, false])),\n permissions: { bash: 'deny' as const, ...profile.permissions },\n }),\n }\n}\n\nconst PERSON = { context: 'own', tools: 'act' } as const\nconst E164 = /^\\+[1-9]\\d{6,14}$/\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nexport class HostedAgentError extends Error {\n constructor(readonly code: string, message: string) { super(message); this.name = 'HostedAgentError' }\n}\n\nexport function createHostedAgent(config: HostedAgentConfig) {\n if (!E164.test(config.owner) && !EMAIL.test(config.owner))\n throw new HostedAgentError('owner_not_e164', 'owner must be an E.164 phone number or email address.')\n if (!config.client && !config.apiKey)\n throw new HostedAgentError('credential_required', 'Supply an API key or an authenticated Sandbox client.')\n const policy = { ...DEFAULT_BOX_POLICY, ...config.box }\n const sandbox = config.client ?? new Sandbox({ apiKey: config.apiKey!, baseUrl: config.sandboxUrl ?? 'https://sandbox.tangle.tools', timeoutMs: 20_000 })\n const backend: BackendConfig = config.backend ?? {\n ...(config.harness ? { type: config.harness as BackendConfig['type'] } : {}),\n profile: conversationProfile(config.profile),\n }\n const create: LineInstanceCreate = {\n name: 'hosted-person',\n resources: { cpuCores: policy.cpuCores, memoryMB: policy.memoryMB, diskGB: policy.diskGB },\n egressPolicy: { mode: 'strict', allowDomains: policy.allowDomains, includeImplicitDomains: false },\n idleTimeoutSeconds: policy.idleTimeoutSeconds, maxLifetimeSeconds: policy.maxLifetimeSeconds,\n deleteAfterStoppedSeconds: policy.deleteAfterStoppedSeconds,\n }\n const members = config.attachment?.members ?? [{ address: config.owner, role: 'owner' }]\n\n function validate(transport: HostedAgentTransport, options: HostedAgentLineOptions, creating: boolean) {\n if (!['imessage', 'whatsapp', 'email'].includes(transport))\n throw new HostedAgentError('transport_unsupported', 'Use an iMessage, WhatsApp or email line.')\n if (options.mode !== undefined && options.mode !== 'personal' && options.mode !== 'shared')\n throw new HostedAgentError('mode_invalid', 'mode must be personal or shared.')\n for (const address of [config.owner, ...members.map(member => member.address)]) {\n const valid = transport === 'email' ? EMAIL.test(address)\n : transport === 'whatsapp' ? E164.test(address) : E164.test(address) || EMAIL.test(address)\n if (!valid) throw new HostedAgentError('owner_transport_mismatch', `Member address does not match ${transport}.`)\n }\n if (creating && transport === 'whatsapp' && !options.phoneNumberId)\n throw new HostedAgentError('phone_number_required', 'WhatsApp lines require phoneNumberId.')\n if (transport !== 'whatsapp' && options.phoneNumberId)\n throw new HostedAgentError('phone_number_not_allowed', 'phoneNumberId is only valid for WhatsApp lines.')\n if (options.voice && transport !== 'imessage')\n throw new HostedAgentError('voice_transport_unsupported', 'Voice is supported only on iMessage lines.')\n if (transport === 'email' && config.attachment?.unknownSenders && config.attachment.unknownSenders !== 'reject')\n throw new HostedAgentError('email_declared_members_required', 'Email lines admit declared members only.')\n }\n\n async function attach(line: Line, options: HostedAgentLineOptions): Promise<Line> {\n const transport = line.transport as HostedAgentTransport\n validate(transport, options, false)\n if (options.transport && options.transport !== transport)\n throw new HostedAgentError('line_transport_mismatch', 'The existing line uses another transport.')\n if (options.phoneNumberId && line.providerNumberId !== options.phoneNumberId)\n throw new HostedAgentError('line_number_mismatch', 'The existing line is pinned to another WhatsApp number.')\n const retained = line.attachment?.status === 'active' ? line.attachment : undefined\n if (retained?.unknownSenders === 'onboard')\n throw new HostedAgentError('line_policy_migration_required', 'This line uses onboard admission. Manage it through Hub; the hosted-agent kit will not replace its policy.')\n const mode = options.mode ?? retained?.mode ?? 'shared'\n const declaredMembers = config.attachment?.members ?? (retained\n ? (await sandbox.lines.members(line.id).list())\n .filter(member => member.source === 'declared')\n .map(member => ({ address: member.address, role: member.role, label: member.label ?? undefined }))\n : members)\n const keyPrefix = line.attachment?.instance?.keyPrefix\n ?? (line.clientReference === 'hosted-agent' ? PERSON_KEY_PREFIX : `${PERSON_KEY_PREFIX}${line.id}:`)\n await sandbox.lines.attach({\n ...config.attachment,\n number: line.id,\n mode,\n members: declaredMembers,\n unknownSenders: config.attachment?.unknownSenders ?? retained?.unknownSenders ?? (mode === 'shared' && transport !== 'email' ? 'guest' : 'reject'),\n roles: config.attachment?.roles ?? retained?.roles ?? (mode === 'shared' ? { owner: PERSON, member: PERSON, guest: PERSON } : { owner: PERSON }),\n respond: { kind: 'agent', backend },\n limits: {\n ...retained?.limits,\n turnsPerMemberPerDay: config.freeTurnsPerDay ?? retained?.limits.turnsPerMemberPerDay ?? 20,\n ...config.attachment?.limits,\n },\n instance: config.attachment?.instance ?? retained?.instance ?? { keyPrefix, create },\n clientReference: config.attachment?.clientReference ?? retained?.clientReference ?? 'hosted-agent',\n })\n if (options.voice) await sandbox.lines.enableVoice(line.id, options.voice)\n return sandbox.lines.get(line.id)\n }\n\n return {\n /**\n * Create an address from an owned connection and attach it. Shared email\n * admits declared members only. Hub authenticates each mailbox by reply.\n * Other shared transports can admit isolated guests. Personal admits one\n * owner. Remove competing subscriptions before calling this method.\n */\n async attachLine(connectionId: string, options: HostedAgentLineOptions = {}): Promise<Line> {\n const transport = options.transport ?? 'imessage'\n validate(transport, options, true)\n // Hub deduplicates by provider identity. A new reference would conflict\n // with an existing WhatsApp line created by an earlier kit.\n const line = await sandbox.lines.fromConnection(transport === 'whatsapp'\n ? { connectionId, transport, phoneNumberId: options.phoneNumberId! }\n : { connectionId, transport })\n return attach(line, options)\n },\n /**\n * Attach an already-created owner-scoped line. Used during a host cutover\n * so the line id, connection, named-instance namespace and payer survive.\n * A 409 remains a 409. This method never detaches a competing attachment.\n */\n async attachExistingLine(lineId: string, options: HostedAgentLineOptions = {}): Promise<Line> {\n return attach(await sandbox.lines.get(lineId), options)\n },\n }\n}\n\nexport type HostedAgent = ReturnType<typeof createHostedAgent>\n"],"mappings":"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","names":[]}
|
|
@@ -37,22 +37,15 @@ export interface ParsedIntegrationAction {
|
|
|
37
37
|
* elsewhere instead of misrouting them to the hub.
|
|
38
38
|
*/
|
|
39
39
|
export declare function resolveIntegrationAction(toolName: string): ParsedIntegrationAction | undefined;
|
|
40
|
-
/**
|
|
41
|
-
* credentials, resolves the connection from the bearer principal, evaluates
|
|
42
|
-
* per-action policy (read → allow, write/destructive → approval), and runs the
|
|
43
|
-
* action server-side. Never throws on a policy block — a gated write is a
|
|
44
|
-
* normal `succeeded: false` outcome. */
|
|
40
|
+
/** Product outcome adapter over the published Hub transport. Never grants approval. */
|
|
45
41
|
export declare class HubExecClient {
|
|
46
|
-
private readonly
|
|
47
|
-
private readonly bearer;
|
|
48
|
-
private readonly fetchImpl;
|
|
42
|
+
private readonly hub;
|
|
49
43
|
constructor(options: HubExecClientOptions);
|
|
50
44
|
exec(input: {
|
|
51
45
|
path: string;
|
|
52
46
|
actionInput?: unknown;
|
|
53
47
|
connectionId?: string;
|
|
54
48
|
}): Promise<HubExecResult>;
|
|
55
|
-
private readEnvelope;
|
|
56
49
|
}
|
|
57
50
|
/** Define input parameters for invoking a hub tool with user ID, tool name, and optional arguments */
|
|
58
51
|
export interface HubInvokeInput {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/integrations/index.ts
|
|
2
|
+
import { HubClient, HubSdkError } from "@tangle-network/hub-sdk";
|
|
2
3
|
import { parseIntegrationToolName } from "@tangle-network/agent-integrations/catalog";
|
|
3
4
|
function resolveIntegrationAction(toolName) {
|
|
4
5
|
let parsed;
|
|
@@ -11,43 +12,26 @@ function resolveIntegrationAction(toolName) {
|
|
|
11
12
|
return { ...parsed, path: `${parsed.providerId}.${parsed.connectorId}.${parsed.actionId}` };
|
|
12
13
|
}
|
|
13
14
|
var HubExecClient = class {
|
|
14
|
-
|
|
15
|
-
bearer;
|
|
16
|
-
fetchImpl;
|
|
15
|
+
hub;
|
|
17
16
|
constructor(options) {
|
|
18
17
|
if (!options.baseUrl) throw new Error("HubExecClient: baseUrl is required");
|
|
19
18
|
if (!options.bearer) throw new Error("HubExecClient: bearer is required");
|
|
20
|
-
this.
|
|
21
|
-
this.bearer = options.bearer;
|
|
22
|
-
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
19
|
+
this.hub = new HubClient({ baseUrl: options.baseUrl, apiKey: options.bearer, fetch: options.fetchImpl });
|
|
23
20
|
}
|
|
24
21
|
async exec(input) {
|
|
25
|
-
const response = await this.fetchImpl(`${this.baseUrl}/v1/hub/exec`, {
|
|
26
|
-
method: "POST",
|
|
27
|
-
headers: { Authorization: `Bearer ${this.bearer}`, "Content-Type": "application/json", Accept: "application/json" },
|
|
28
|
-
body: JSON.stringify({ path: input.path, input: input.actionInput, connectionId: input.connectionId })
|
|
29
|
-
});
|
|
30
|
-
const envelope = await this.readEnvelope(response);
|
|
31
|
-
if (response.ok && envelope.success) return { succeeded: true, result: envelope.data?.result };
|
|
32
|
-
return {
|
|
33
|
-
succeeded: false,
|
|
34
|
-
code: envelope.error?.code ?? `HUB_HTTP_${response.status}`,
|
|
35
|
-
message: envelope.error?.message ?? `Hub /exec returned ${response.status}`,
|
|
36
|
-
approval: envelope.error?.details?.approval
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
async readEnvelope(response) {
|
|
40
|
-
const text = await response.text();
|
|
41
|
-
if (!text) return { success: false, error: { code: `HUB_HTTP_${response.status}`, message: `Hub returned ${response.status} with no body` } };
|
|
42
22
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
23
|
+
const result = await this.hub.tools.invoke(input.path, input.actionInput, { connectionId: input.connectionId });
|
|
24
|
+
return { succeeded: true, result: result.result };
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (!(error instanceof HubSdkError)) throw error;
|
|
27
|
+
const details = error.details;
|
|
28
|
+
const approval = details && typeof details === "object" && "approval" in details ? details.approval : void 0;
|
|
29
|
+
return { succeeded: false, code: error.code, message: error.message, ...approval !== void 0 ? { approval } : {} };
|
|
46
30
|
}
|
|
47
31
|
}
|
|
48
32
|
};
|
|
49
33
|
async function invokeIntegrationHub(input, deps) {
|
|
50
|
-
const env = deps.env ?? process
|
|
34
|
+
const env = deps.env ?? globalThis.process?.env ?? {};
|
|
51
35
|
const baseUrl = deps.baseUrl ?? env.TANGLE_PLATFORM_URL?.trim();
|
|
52
36
|
if (!baseUrl) return { status: 500, body: { error: "TANGLE_PLATFORM_URL is not configured" } };
|
|
53
37
|
const action = resolveIntegrationAction(input.toolName);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/integrations/index.ts"],"sourcesContent":["/**\n * Integration-hub WIRING — the agent→hub invocation path every Tangle agent app\n * forks. The integration ENGINE (catalog, connectors, OAuth, policy) is\n * `@tangle-network/agent-integrations` (a peer dependency); this module is the\n * thin app-side wiring on top: a typed client over the platform hub's\n * `POST /v1/hub/exec`, MCP-tool-name → hub-action-path resolution, and the\n * per-turn invoke flow the `integration_invoke` tool calls.\n *\n * The product supplies its own catalog (which connectors it uses) and its own\n * per-user api-key resolver (the `apiKeyResolver` seam) — this module owns\n * neither credentials nor the action catalog.\n */\nimport { parseIntegrationToolName } from '@tangle-network/agent-integrations/catalog'\n\n/** `{ success: false }` codes the hub returns on `/exec`. */\nexport type HubExecErrorCode =\n | 'HUB_APPROVAL_REQUIRED'\n | 'HUB_POLICY_DENIED'\n | 'HUB_CONNECTION_MISSING'\n | 'HUB_CONNECTION_REVOKED'\n | 'HUB_CONFIG_MISSING'\n | 'HUB_NOT_FOUND'\n | string\n\n/** Outcome of a hub `/exec` call. Callers MUST inspect `succeeded` before\n * reading `result` — a denied or approval-gated write resolves with\n * `succeeded: false` and a populated `code`, never a thrown silent failure. */\nexport type HubExecResult =\n | { succeeded: true; result: unknown }\n | { succeeded: false; code: HubExecErrorCode; message: string; approval?: unknown }\n\n/** Define configuration options for initializing a Hub execution client */\nexport interface HubExecClientOptions {\n /** Platform base URL (e.g. `TANGLE_PLATFORM_URL`). */\n baseUrl: string\n /** Calling user's Tangle API key — the hub principal bearer. */\n bearer: string\n /** Test seam. Defaults to global `fetch`. */\n fetchImpl?: typeof fetch\n}\n\n/** The provider/connector/action a hub action path addresses, plus the dotted\n * `path` the hub `/exec` endpoint expects. */\nexport interface ParsedIntegrationAction {\n providerId: string\n connectorId: string\n actionId: string\n /** `provider.connector.action`. */\n path: string\n}\n\n/**\n * Resolve an MCP tool name (the opaque `int_…` catalog name the agent calls)\n * into the dotted hub action path. Returns `undefined` when the name is not a\n * catalog integration tool, so the chat loop routes non-integration calls\n * elsewhere instead of misrouting them to the hub.\n */\nexport function resolveIntegrationAction(toolName: string): ParsedIntegrationAction | undefined {\n let parsed: { providerId: string; connectorId: string; actionId: string }\n try {\n parsed = parseIntegrationToolName(toolName)\n } catch {\n return undefined\n }\n if (!parsed.providerId || !parsed.connectorId || !parsed.actionId) return undefined\n return { ...parsed, path: `${parsed.providerId}.${parsed.connectorId}.${parsed.actionId}` }\n}\n\ninterface HubEnvelope {\n success: boolean\n data?: { result?: unknown }\n error?: { code?: string; message?: string; details?: { approval?: unknown } }\n}\n\n/** Typed client over the platform hub `/v1/hub/exec`. The hub holds the user's\n * credentials, resolves the connection from the bearer principal, evaluates\n * per-action policy (read → allow, write/destructive → approval), and runs the\n * action server-side. Never throws on a policy block — a gated write is a\n * normal `succeeded: false` outcome. */\nexport class HubExecClient {\n private readonly baseUrl: string\n private readonly bearer: string\n private readonly fetchImpl: typeof fetch\n\n constructor(options: HubExecClientOptions) {\n if (!options.baseUrl) throw new Error('HubExecClient: baseUrl is required')\n if (!options.bearer) throw new Error('HubExecClient: bearer is required')\n this.baseUrl = options.baseUrl.replace(/\\/+$/, '')\n this.bearer = options.bearer\n this.fetchImpl = options.fetchImpl ?? fetch\n }\n\n async exec(input: { path: string; actionInput?: unknown; connectionId?: string }): Promise<HubExecResult> {\n const response = await this.fetchImpl(`${this.baseUrl}/v1/hub/exec`, {\n method: 'POST',\n headers: { Authorization: `Bearer ${this.bearer}`, 'Content-Type': 'application/json', Accept: 'application/json' },\n body: JSON.stringify({ path: input.path, input: input.actionInput, connectionId: input.connectionId }),\n })\n const envelope = await this.readEnvelope(response)\n if (response.ok && envelope.success) return { succeeded: true, result: envelope.data?.result }\n return {\n succeeded: false,\n code: envelope.error?.code ?? `HUB_HTTP_${response.status}`,\n message: envelope.error?.message ?? `Hub /exec returned ${response.status}`,\n approval: envelope.error?.details?.approval,\n }\n }\n\n private async readEnvelope(response: Response): Promise<HubEnvelope> {\n const text = await response.text()\n if (!text) return { success: false, error: { code: `HUB_HTTP_${response.status}`, message: `Hub returned ${response.status} with no body` } }\n try {\n return JSON.parse(text) as HubEnvelope\n } catch {\n return { success: false, error: { code: 'HUB_BAD_RESPONSE', message: `Hub returned non-JSON (${response.status}): ${text.slice(0, 200)}` } }\n }\n }\n}\n\n/** Define input parameters for invoking a hub tool with user ID, tool name, and optional arguments */\nexport interface HubInvokeInput {\n userId: string\n /** The MCP tool name the agent called (`int_<provider>_<connector>_<action>`). */\n toolName: string\n args?: Record<string, unknown>\n}\n/** Describe the outcome of a hub invocation including status and response body */\nexport interface HubInvokeOutcome {\n status: number\n body: Record<string, unknown>\n}\n/** Define dependencies for invoking hub operations including API key resolution and optional configuration */\nexport interface HubInvokeDeps {\n /** Resolve the user's Tangle API key (the hub principal bearer). Required —\n * the product binds its own session-key resolver. Null → user not linked. */\n apiKeyResolver: (userId: string) => Promise<string | null>\n /** Platform base URL. Defaults to `env.TANGLE_PLATFORM_URL`. */\n baseUrl?: string\n fetchImpl?: typeof fetch\n env?: Record<string, string | undefined>\n}\n\n/**\n * Resolve + execute one integration tool call through the hub: resolve the\n * per-user bearer, map the MCP tool name to the hub action path, forward to\n * `/v1/hub/exec`, and shape the route response (200 ok / 401 not-linked /\n * 400 unknown-tool / 409 approval-required / 502 hub-error). A write that's\n * approval-gated surfaces verbatim as 409, never silently executed.\n */\nexport async function invokeIntegrationHub(input: HubInvokeInput, deps: HubInvokeDeps): Promise<HubInvokeOutcome> {\n const env = deps.env ?? (process.env as Record<string, string | undefined>)\n const baseUrl = deps.baseUrl ?? env.TANGLE_PLATFORM_URL?.trim()\n if (!baseUrl) return { status: 500, body: { error: 'TANGLE_PLATFORM_URL is not configured' } }\n\n const action = resolveIntegrationAction(input.toolName)\n if (!action) return { status: 400, body: { error: `Unsupported integration tool: ${input.toolName}` } }\n\n const bearer = await deps.apiKeyResolver(input.userId)\n if (!bearer) return { status: 401, body: { error: 'Tangle account not linked — connect integrations from the app first' } }\n\n const client = new HubExecClient({ baseUrl, bearer, fetchImpl: deps.fetchImpl })\n const outcome = await client.exec({ path: action.path, actionInput: input.args ?? {} })\n\n if (outcome.succeeded) {\n return { status: 200, body: { success: true, path: action.path, providerId: action.providerId, action: action.actionId, result: outcome.result } }\n }\n const status = outcome.code === 'HUB_APPROVAL_REQUIRED' ? 409 : 502\n return {\n status,\n body: { success: false, path: action.path, code: outcome.code, error: outcome.message, ...(outcome.approval ? { approval: outcome.approval } : {}) },\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/integrations/index.ts"],"sourcesContent":["/**\n * Integration-hub WIRING — the agent→hub invocation path every Tangle agent app\n * forks. The integration ENGINE (catalog, connectors, OAuth, policy) is\n * `@tangle-network/agent-integrations` (a peer dependency); this module is the\n * thin app-side wiring on top: an outcome adapter over hub-sdk's\n * `POST /v1/hub/exec`, MCP-tool-name → hub-action-path resolution, and the\n * per-turn invoke flow the `integration_invoke` tool calls.\n *\n * The product supplies its own catalog (which connectors it uses) and its own\n * per-user api-key resolver (the `apiKeyResolver` seam) — this module owns\n * neither credentials nor the action catalog.\n */\nimport { HubClient, HubSdkError } from '@tangle-network/hub-sdk'\nimport { parseIntegrationToolName } from '@tangle-network/agent-integrations/catalog'\n\n/** `{ success: false }` codes the hub returns on `/exec`. */\nexport type HubExecErrorCode =\n | 'HUB_APPROVAL_REQUIRED'\n | 'HUB_POLICY_DENIED'\n | 'HUB_CONNECTION_MISSING'\n | 'HUB_CONNECTION_REVOKED'\n | 'HUB_CONFIG_MISSING'\n | 'HUB_NOT_FOUND'\n | string\n\n/** Outcome of a hub `/exec` call. Callers MUST inspect `succeeded` before\n * reading `result` — a denied or approval-gated write resolves with\n * `succeeded: false` and a populated `code`, never a thrown silent failure. */\nexport type HubExecResult =\n | { succeeded: true; result: unknown }\n | { succeeded: false; code: HubExecErrorCode; message: string; approval?: unknown }\n\n/** Define configuration options for initializing a Hub execution client */\nexport interface HubExecClientOptions {\n /** Platform base URL (e.g. `TANGLE_PLATFORM_URL`). */\n baseUrl: string\n /** Calling user's Tangle API key — the hub principal bearer. */\n bearer: string\n /** Test seam. Defaults to global `fetch`. */\n fetchImpl?: typeof fetch\n}\n\n/** The provider/connector/action a hub action path addresses, plus the dotted\n * `path` the hub `/exec` endpoint expects. */\nexport interface ParsedIntegrationAction {\n providerId: string\n connectorId: string\n actionId: string\n /** `provider.connector.action`. */\n path: string\n}\n\n/**\n * Resolve an MCP tool name (the opaque `int_…` catalog name the agent calls)\n * into the dotted hub action path. Returns `undefined` when the name is not a\n * catalog integration tool, so the chat loop routes non-integration calls\n * elsewhere instead of misrouting them to the hub.\n */\nexport function resolveIntegrationAction(toolName: string): ParsedIntegrationAction | undefined {\n let parsed: { providerId: string; connectorId: string; actionId: string }\n try {\n parsed = parseIntegrationToolName(toolName)\n } catch {\n return undefined\n }\n if (!parsed.providerId || !parsed.connectorId || !parsed.actionId) return undefined\n return { ...parsed, path: `${parsed.providerId}.${parsed.connectorId}.${parsed.actionId}` }\n}\n\n/** Product outcome adapter over the published Hub transport. Never grants approval. */\nexport class HubExecClient {\n private readonly hub: HubClient\n\n constructor(options: HubExecClientOptions) {\n if (!options.baseUrl) throw new Error('HubExecClient: baseUrl is required')\n if (!options.bearer) throw new Error('HubExecClient: bearer is required')\n this.hub = new HubClient({ baseUrl: options.baseUrl, apiKey: options.bearer, fetch: options.fetchImpl })\n }\n\n async exec(input: { path: string; actionInput?: unknown; connectionId?: string }): Promise<HubExecResult> {\n try {\n // No approve flag. Hub still decides whether this exact action is permitted.\n const result = await this.hub.tools.invoke(input.path, input.actionInput, { connectionId: input.connectionId })\n return { succeeded: true, result: result.result }\n } catch (error) {\n if (!(error instanceof HubSdkError)) throw error\n const details = error.details\n const approval = details && typeof details === 'object' && 'approval' in details ? details.approval : undefined\n return { succeeded: false, code: error.code, message: error.message, ...(approval !== undefined ? { approval } : {}) }\n }\n }\n}\n\n/** Define input parameters for invoking a hub tool with user ID, tool name, and optional arguments */\nexport interface HubInvokeInput {\n userId: string\n /** The MCP tool name the agent called (`int_<provider>_<connector>_<action>`). */\n toolName: string\n args?: Record<string, unknown>\n}\n/** Describe the outcome of a hub invocation including status and response body */\nexport interface HubInvokeOutcome {\n status: number\n body: Record<string, unknown>\n}\n/** Define dependencies for invoking hub operations including API key resolution and optional configuration */\nexport interface HubInvokeDeps {\n /** Resolve the user's Tangle API key (the hub principal bearer). Required —\n * the product binds its own session-key resolver. Null → user not linked. */\n apiKeyResolver: (userId: string) => Promise<string | null>\n /** Platform base URL. Defaults to `env.TANGLE_PLATFORM_URL`. */\n baseUrl?: string\n fetchImpl?: typeof fetch\n env?: Record<string, string | undefined>\n}\n\n/**\n * Resolve + execute one integration tool call through the hub: resolve the\n * per-user bearer, map the MCP tool name to the hub action path, forward to\n * `/v1/hub/exec`, and shape the route response (200 ok / 401 not-linked /\n * 400 unknown-tool / 409 approval-required / 502 hub-error). A write that's\n * approval-gated surfaces verbatim as 409, never silently executed.\n */\nexport async function invokeIntegrationHub(input: HubInvokeInput, deps: HubInvokeDeps): Promise<HubInvokeOutcome> {\n const env = deps.env ?? (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {}\n const baseUrl = deps.baseUrl ?? env.TANGLE_PLATFORM_URL?.trim()\n if (!baseUrl) return { status: 500, body: { error: 'TANGLE_PLATFORM_URL is not configured' } }\n\n const action = resolveIntegrationAction(input.toolName)\n if (!action) return { status: 400, body: { error: `Unsupported integration tool: ${input.toolName}` } }\n\n const bearer = await deps.apiKeyResolver(input.userId)\n if (!bearer) return { status: 401, body: { error: 'Tangle account not linked — connect integrations from the app first' } }\n\n const client = new HubExecClient({ baseUrl, bearer, fetchImpl: deps.fetchImpl })\n const outcome = await client.exec({ path: action.path, actionInput: input.args ?? {} })\n\n if (outcome.succeeded) {\n return { status: 200, body: { success: true, path: action.path, providerId: action.providerId, action: action.actionId, result: outcome.result } }\n }\n const status = outcome.code === 'HUB_APPROVAL_REQUIRED' ? 409 : 502\n return {\n status,\n body: { success: false, path: action.path, code: outcome.code, error: outcome.message, ...(outcome.approval ? { approval: outcome.approval } : {}) },\n }\n}\n"],"mappings":";AAYA,SAAS,WAAW,mBAAmB;AACvC,SAAS,gCAAgC;AA6ClC,SAAS,yBAAyB,UAAuD;AAC9F,MAAI;AACJ,MAAI;AACF,aAAS,yBAAyB,QAAQ;AAAA,EAC5C,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,CAAC,OAAO,cAAc,CAAC,OAAO,eAAe,CAAC,OAAO,SAAU,QAAO;AAC1E,SAAO,EAAE,GAAG,QAAQ,MAAM,GAAG,OAAO,UAAU,IAAI,OAAO,WAAW,IAAI,OAAO,QAAQ,GAAG;AAC5F;AAGO,IAAM,gBAAN,MAAoB;AAAA,EACR;AAAA,EAEjB,YAAY,SAA+B;AACzC,QAAI,CAAC,QAAQ,QAAS,OAAM,IAAI,MAAM,oCAAoC;AAC1E,QAAI,CAAC,QAAQ,OAAQ,OAAM,IAAI,MAAM,mCAAmC;AACxE,SAAK,MAAM,IAAI,UAAU,EAAE,SAAS,QAAQ,SAAS,QAAQ,QAAQ,QAAQ,OAAO,QAAQ,UAAU,CAAC;AAAA,EACzG;AAAA,EAEA,MAAM,KAAK,OAA+F;AACxG,QAAI;AAEF,YAAM,SAAS,MAAM,KAAK,IAAI,MAAM,OAAO,MAAM,MAAM,MAAM,aAAa,EAAE,cAAc,MAAM,aAAa,CAAC;AAC9G,aAAO,EAAE,WAAW,MAAM,QAAQ,OAAO,OAAO;AAAA,IAClD,SAAS,OAAO;AACd,UAAI,EAAE,iBAAiB,aAAc,OAAM;AAC3C,YAAM,UAAU,MAAM;AACtB,YAAM,WAAW,WAAW,OAAO,YAAY,YAAY,cAAc,UAAU,QAAQ,WAAW;AACtG,aAAO,EAAE,WAAW,OAAO,MAAM,MAAM,MAAM,SAAS,MAAM,SAAS,GAAI,aAAa,SAAY,EAAE,SAAS,IAAI,CAAC,EAAG;AAAA,IACvH;AAAA,EACF;AACF;AAgCA,eAAsB,qBAAqB,OAAuB,MAAgD;AAChH,QAAM,MAAM,KAAK,OAAQ,WAA0E,SAAS,OAAO,CAAC;AACpH,QAAM,UAAU,KAAK,WAAW,IAAI,qBAAqB,KAAK;AAC9D,MAAI,CAAC,QAAS,QAAO,EAAE,QAAQ,KAAK,MAAM,EAAE,OAAO,wCAAwC,EAAE;AAE7F,QAAM,SAAS,yBAAyB,MAAM,QAAQ;AACtD,MAAI,CAAC,OAAQ,QAAO,EAAE,QAAQ,KAAK,MAAM,EAAE,OAAO,iCAAiC,MAAM,QAAQ,GAAG,EAAE;AAEtG,QAAM,SAAS,MAAM,KAAK,eAAe,MAAM,MAAM;AACrD,MAAI,CAAC,OAAQ,QAAO,EAAE,QAAQ,KAAK,MAAM,EAAE,OAAO,2EAAsE,EAAE;AAE1H,QAAM,SAAS,IAAI,cAAc,EAAE,SAAS,QAAQ,WAAW,KAAK,UAAU,CAAC;AAC/E,QAAM,UAAU,MAAM,OAAO,KAAK,EAAE,MAAM,OAAO,MAAM,aAAa,MAAM,QAAQ,CAAC,EAAE,CAAC;AAEtF,MAAI,QAAQ,WAAW;AACrB,WAAO,EAAE,QAAQ,KAAK,MAAM,EAAE,SAAS,MAAM,MAAM,OAAO,MAAM,YAAY,OAAO,YAAY,QAAQ,OAAO,UAAU,QAAQ,QAAQ,OAAO,EAAE;AAAA,EACnJ;AACA,QAAM,SAAS,QAAQ,SAAS,0BAA0B,MAAM;AAChE,SAAO;AAAA,IACL;AAAA,IACA,MAAM,EAAE,SAAS,OAAO,MAAM,OAAO,MAAM,MAAM,QAAQ,MAAM,OAAO,QAAQ,SAAS,GAAI,QAAQ,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC,EAAG;AAAA,EACrJ;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-app",
|
|
3
|
-
"version": "0.49.
|
|
3
|
+
"version": "0.49.33",
|
|
4
4
|
"packageManager": "pnpm@11.24.0",
|
|
5
5
|
"description": "Build agent applications with typed chat, tools, sandboxes, integrations, billing, and evaluation.",
|
|
6
6
|
"keywords": [
|
|
@@ -545,15 +545,15 @@
|
|
|
545
545
|
"@storybook/react-vite": "^10.5.10",
|
|
546
546
|
"@tailwindcss/postcss": "^4.3.3",
|
|
547
547
|
"@tangle-network/agent-docs": "0.2.1",
|
|
548
|
-
"@tangle-network/agent-eval": "0.
|
|
548
|
+
"@tangle-network/agent-eval": "0.193.2",
|
|
549
549
|
"@tangle-network/agent-gateway": "0.11.4",
|
|
550
550
|
"@tangle-network/agent-integrations": "0.55.0",
|
|
551
|
-
"@tangle-network/agent-interface": "2.
|
|
552
|
-
"@tangle-network/agent-knowledge": "17.1.
|
|
551
|
+
"@tangle-network/agent-interface": "2.13.0",
|
|
552
|
+
"@tangle-network/agent-knowledge": "17.1.8",
|
|
553
553
|
"@tangle-network/agent-profile-materialize": "0.20.1",
|
|
554
554
|
"@tangle-network/agent-runtime": "0.277.0",
|
|
555
555
|
"@tangle-network/brand": "1.5.0",
|
|
556
|
-
"@tangle-network/hub-sdk": "0.19.
|
|
556
|
+
"@tangle-network/hub-sdk": "0.19.3",
|
|
557
557
|
"@tangle-network/sandbox": "0.53.0",
|
|
558
558
|
"@tangle-network/sandbox-ui": "0.113.3",
|
|
559
559
|
"@tangle-network/ui": "11.9.0",
|
|
@@ -614,7 +614,7 @@
|
|
|
614
614
|
"@tangle-network/agent-profile-materialize": ">=0.20.1 <0.21.0",
|
|
615
615
|
"@tangle-network/agent-runtime": ">=0.262.0 <0.278.0",
|
|
616
616
|
"@tangle-network/brand": ">=1.5.0",
|
|
617
|
-
"@tangle-network/hub-sdk": ">=0.19.
|
|
617
|
+
"@tangle-network/hub-sdk": ">=0.19.3 <0.20.0",
|
|
618
618
|
"@tangle-network/sandbox": ">=0.45.0 <0.48.0 || ^0.49.0 || ^0.50.0 || ^0.51.0 || ^0.52.0 || ^0.53.0",
|
|
619
619
|
"@tangle-network/sandbox-ui": ">=0.113.3 <0.114.0",
|
|
620
620
|
"@tangle-network/ui": ">=11.6.0 <12.0.0",
|