@tangle-network/agent-runtime 0.8.0 → 0.11.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 +42 -13
- package/dist/agent.d.ts +537 -0
- package/dist/agent.js +475 -0
- package/dist/agent.js.map +1 -0
- package/dist/analyst-loop.d.ts +26 -0
- package/dist/analyst-loop.js +262 -0
- package/dist/analyst-loop.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/index.d.ts +235 -35
- package/dist/index.js +284 -3
- package/dist/index.js.map +1 -1
- package/dist/platform.d.ts +197 -0
- package/dist/platform.js +187 -0
- package/dist/platform.js.map +1 -0
- package/dist/types-D_MXrmJP.d.ts +245 -0
- package/package.json +39 -14
- package/docs/domain-agent-runtime-integration-issues.md +0 -165
- package/docs/product-runtime-kernel.md +0 -326
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side client for the Tangle platform's cross-site SSO bridge.
|
|
3
|
+
*
|
|
4
|
+
* Consumer apps (gtm-agent, tax-agent, legal-agent, creative-agent, …)
|
|
5
|
+
* use this to:
|
|
6
|
+
* 1. Build an /authorize URL that lands the user on id.tangle.tools
|
|
7
|
+
* and brings them back with a single-use code.
|
|
8
|
+
* 2. Exchange that code for an API key + the user's identity.
|
|
9
|
+
*
|
|
10
|
+
* The platform endpoint contract is documented in
|
|
11
|
+
* `products/platform/api/src/routes/cross-site.ts`. This client only
|
|
12
|
+
* speaks HTTP — no SDK weight, no transitive deps.
|
|
13
|
+
*/
|
|
14
|
+
interface PlatformAuthClientOptions {
|
|
15
|
+
/** Platform base URL, e.g. `https://id.tangle.tools`. */
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
/** App id as registered in the platform's TRUSTED_APPS registry. */
|
|
18
|
+
appId: string;
|
|
19
|
+
/** Override the global fetch (useful for tests + edge runtimes). */
|
|
20
|
+
fetchImpl?: typeof fetch;
|
|
21
|
+
}
|
|
22
|
+
interface AuthorizeUrlOptions {
|
|
23
|
+
/** Required CSRF token; the consumer verifies it on the callback. */
|
|
24
|
+
state: string;
|
|
25
|
+
/**
|
|
26
|
+
* Final redirect URI. Must be one of the URIs registered for `appId`
|
|
27
|
+
* on the platform. Omit to use the first registered URI.
|
|
28
|
+
*/
|
|
29
|
+
redirectUri?: string;
|
|
30
|
+
/** Force the login screen even if a session is already active. */
|
|
31
|
+
prompt?: 'login';
|
|
32
|
+
/** Pre-fill the email field on the login screen. */
|
|
33
|
+
email?: string;
|
|
34
|
+
}
|
|
35
|
+
interface ExchangeCodeResult {
|
|
36
|
+
apiKey: string;
|
|
37
|
+
user: {
|
|
38
|
+
id: string;
|
|
39
|
+
email: string;
|
|
40
|
+
name?: string;
|
|
41
|
+
};
|
|
42
|
+
plan: {
|
|
43
|
+
tier: string;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
declare class PlatformAuthError extends Error {
|
|
47
|
+
readonly status: number;
|
|
48
|
+
readonly body: unknown;
|
|
49
|
+
constructor(message: string, status: number, body: unknown);
|
|
50
|
+
}
|
|
51
|
+
declare class PlatformAuthClient {
|
|
52
|
+
private readonly baseUrl;
|
|
53
|
+
private readonly appId;
|
|
54
|
+
private readonly fetchImpl;
|
|
55
|
+
constructor(options: PlatformAuthClientOptions);
|
|
56
|
+
/**
|
|
57
|
+
* Build the URL the user is redirected to in order to start SSO.
|
|
58
|
+
* The platform redirects back to one of `appId`'s registered
|
|
59
|
+
* `redirectUris` with `?code=...&app=...&state=...`.
|
|
60
|
+
*/
|
|
61
|
+
authorizeUrl(options: AuthorizeUrlOptions): string;
|
|
62
|
+
/**
|
|
63
|
+
* Exchange a single-use auth code (delivered to the consumer's
|
|
64
|
+
* callback by the platform) for an API key + the user's identity.
|
|
65
|
+
* Codes are single-use and expire ~5 minutes after issue.
|
|
66
|
+
*/
|
|
67
|
+
exchange(code: string): Promise<ExchangeCodeResult>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Server-side client for the Tangle platform's integrations hub
|
|
72
|
+
* (`/v1/integrations/*`). Consumer apps use this instead of rolling
|
|
73
|
+
* their own OAuth + connection tables.
|
|
74
|
+
*
|
|
75
|
+
* Auth: the caller supplies a bearer (either the user's API key from
|
|
76
|
+
* cross-site exchange, or a platform service token) on construction.
|
|
77
|
+
* Per-request override via `headers` is supported.
|
|
78
|
+
*
|
|
79
|
+
* Endpoint contract: `products/platform/api/src/routes/integrations.ts`.
|
|
80
|
+
*/
|
|
81
|
+
interface PlatformHubClientOptions {
|
|
82
|
+
/** Platform base URL, e.g. `https://id.tangle.tools`. */
|
|
83
|
+
baseUrl: string;
|
|
84
|
+
/** Bearer credential — user API key or service token. */
|
|
85
|
+
bearer: string;
|
|
86
|
+
/** Override fetch (tests + edge runtimes). */
|
|
87
|
+
fetchImpl?: typeof fetch;
|
|
88
|
+
}
|
|
89
|
+
interface PlatformConnection {
|
|
90
|
+
id: string;
|
|
91
|
+
providerId: string;
|
|
92
|
+
connectorId: string;
|
|
93
|
+
status: 'connected' | 'pending' | 'revoked' | 'expired' | string;
|
|
94
|
+
grantedScopes?: string[];
|
|
95
|
+
account?: {
|
|
96
|
+
identity?: string;
|
|
97
|
+
displayName?: string;
|
|
98
|
+
} & Record<string, unknown>;
|
|
99
|
+
metadata?: Record<string, unknown>;
|
|
100
|
+
expiresAt?: string | null;
|
|
101
|
+
createdAt?: string;
|
|
102
|
+
updatedAt?: string;
|
|
103
|
+
}
|
|
104
|
+
interface PlatformCatalogProvider {
|
|
105
|
+
providerId: string;
|
|
106
|
+
displayName?: string;
|
|
107
|
+
description?: string;
|
|
108
|
+
authMode?: string;
|
|
109
|
+
connectors?: PlatformCatalogConnector[];
|
|
110
|
+
[k: string]: unknown;
|
|
111
|
+
}
|
|
112
|
+
interface PlatformCatalogConnector {
|
|
113
|
+
connectorId: string;
|
|
114
|
+
displayName?: string;
|
|
115
|
+
description?: string;
|
|
116
|
+
scopes?: string[];
|
|
117
|
+
[k: string]: unknown;
|
|
118
|
+
}
|
|
119
|
+
interface StartAuthInput {
|
|
120
|
+
providerId: string;
|
|
121
|
+
connectorId: string;
|
|
122
|
+
/** Where the platform redirects the user back to after OAuth. */
|
|
123
|
+
returnUrl: string;
|
|
124
|
+
requestedScopes?: string[];
|
|
125
|
+
state?: string;
|
|
126
|
+
metadata?: Record<string, unknown>;
|
|
127
|
+
/** Required when the bearer is a service token impersonating a user. */
|
|
128
|
+
ownerUserId?: string;
|
|
129
|
+
}
|
|
130
|
+
interface StartAuthResult {
|
|
131
|
+
authorizationUrl: string;
|
|
132
|
+
state: string;
|
|
133
|
+
}
|
|
134
|
+
interface BundleCapabilityInput {
|
|
135
|
+
manifestId?: string;
|
|
136
|
+
grantIds?: string[];
|
|
137
|
+
subject: {
|
|
138
|
+
type: 'user' | 'team' | 'app';
|
|
139
|
+
id: string;
|
|
140
|
+
};
|
|
141
|
+
ttlMs: number;
|
|
142
|
+
}
|
|
143
|
+
interface BundleCapabilityResult {
|
|
144
|
+
bundle: Record<string, unknown>;
|
|
145
|
+
env: Record<string, string>;
|
|
146
|
+
}
|
|
147
|
+
interface HealthCheck {
|
|
148
|
+
connectionId: string;
|
|
149
|
+
providerId: string;
|
|
150
|
+
connectorId: string;
|
|
151
|
+
status: 'ok' | 'degraded' | 'failing' | 'unknown' | string;
|
|
152
|
+
checks?: Record<string, unknown>;
|
|
153
|
+
checkedAt?: string;
|
|
154
|
+
}
|
|
155
|
+
declare class PlatformHubError extends Error {
|
|
156
|
+
readonly status: number;
|
|
157
|
+
readonly code: string | undefined;
|
|
158
|
+
readonly body: unknown;
|
|
159
|
+
constructor(message: string, status: number, code: string | undefined, body: unknown);
|
|
160
|
+
}
|
|
161
|
+
declare class PlatformHubClient {
|
|
162
|
+
private readonly baseUrl;
|
|
163
|
+
private readonly bearer;
|
|
164
|
+
private readonly fetchImpl;
|
|
165
|
+
constructor(options: PlatformHubClientOptions);
|
|
166
|
+
/** List the integration catalog (providers + connectors). */
|
|
167
|
+
catalog(): Promise<{
|
|
168
|
+
providers: PlatformCatalogProvider[];
|
|
169
|
+
} & Record<string, unknown>>;
|
|
170
|
+
/** List the calling user's integration connections. */
|
|
171
|
+
listConnections(): Promise<PlatformConnection[]>;
|
|
172
|
+
/** Revoke (and disable) a connection by id. */
|
|
173
|
+
revokeConnection(connectionId: string): Promise<{
|
|
174
|
+
connection: PlatformConnection;
|
|
175
|
+
revokedGrants: unknown[];
|
|
176
|
+
providerRevocation: {
|
|
177
|
+
ok: boolean;
|
|
178
|
+
};
|
|
179
|
+
}>;
|
|
180
|
+
/** Begin OAuth — returns the URL to send the user to. */
|
|
181
|
+
startAuth(input: StartAuthInput): Promise<StartAuthResult>;
|
|
182
|
+
/** List connection healthchecks (last known state). */
|
|
183
|
+
listHealthchecks(): Promise<HealthCheck[]>;
|
|
184
|
+
/** Trigger a fresh healthcheck pass. */
|
|
185
|
+
runHealthchecks(): Promise<{
|
|
186
|
+
scheduled: number;
|
|
187
|
+
}>;
|
|
188
|
+
/**
|
|
189
|
+
* Mint a sandbox-injectable capability bundle (env vars + scoped
|
|
190
|
+
* capability tokens) so a sandbox can invoke integrations on the
|
|
191
|
+
* user's behalf without seeing the underlying provider tokens.
|
|
192
|
+
*/
|
|
193
|
+
bundleCapabilities(input: BundleCapabilityInput): Promise<BundleCapabilityResult>;
|
|
194
|
+
private request;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export { type AuthorizeUrlOptions, type BundleCapabilityInput, type BundleCapabilityResult, type ExchangeCodeResult, type HealthCheck, PlatformAuthClient, type PlatformAuthClientOptions, PlatformAuthError, type PlatformCatalogConnector, type PlatformCatalogProvider, type PlatformConnection, PlatformHubClient, type PlatformHubClientOptions, PlatformHubError, type StartAuthInput, type StartAuthResult };
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/platform/auth.ts
|
|
4
|
+
var PlatformAuthError = class extends Error {
|
|
5
|
+
constructor(message, status, body) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.body = body;
|
|
9
|
+
this.name = "PlatformAuthError";
|
|
10
|
+
}
|
|
11
|
+
status;
|
|
12
|
+
body;
|
|
13
|
+
};
|
|
14
|
+
var PlatformAuthClient = class {
|
|
15
|
+
baseUrl;
|
|
16
|
+
appId;
|
|
17
|
+
fetchImpl;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
if (!options.baseUrl) throw new Error("PlatformAuthClient: baseUrl is required");
|
|
20
|
+
if (!options.appId) throw new Error("PlatformAuthClient: appId is required");
|
|
21
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
22
|
+
this.appId = options.appId;
|
|
23
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build the URL the user is redirected to in order to start SSO.
|
|
27
|
+
* The platform redirects back to one of `appId`'s registered
|
|
28
|
+
* `redirectUris` with `?code=...&app=...&state=...`.
|
|
29
|
+
*/
|
|
30
|
+
authorizeUrl(options) {
|
|
31
|
+
if (!options.state) {
|
|
32
|
+
throw new Error("PlatformAuthClient.authorizeUrl: state is required for CSRF");
|
|
33
|
+
}
|
|
34
|
+
const url = new URL("/cross-site/authorize", this.baseUrl);
|
|
35
|
+
url.searchParams.set("app", this.appId);
|
|
36
|
+
url.searchParams.set("state", options.state);
|
|
37
|
+
if (options.redirectUri) url.searchParams.set("redirect", options.redirectUri);
|
|
38
|
+
if (options.prompt) url.searchParams.set("prompt", options.prompt);
|
|
39
|
+
if (options.email) url.searchParams.set("email", options.email);
|
|
40
|
+
return url.toString();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Exchange a single-use auth code (delivered to the consumer's
|
|
44
|
+
* callback by the platform) for an API key + the user's identity.
|
|
45
|
+
* Codes are single-use and expire ~5 minutes after issue.
|
|
46
|
+
*/
|
|
47
|
+
async exchange(code) {
|
|
48
|
+
if (!code) throw new Error("PlatformAuthClient.exchange: code is required");
|
|
49
|
+
const res = await this.fetchImpl(`${this.baseUrl}/cross-site/exchange`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "content-type": "application/json" },
|
|
52
|
+
body: JSON.stringify({ code, app: this.appId })
|
|
53
|
+
});
|
|
54
|
+
const body = await res.json().catch(() => null);
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
const message = body && typeof body === "object" && "error" in body && typeof body.error === "string" ? body.error : `Platform exchange failed (${res.status})`;
|
|
57
|
+
throw new PlatformAuthError(message, res.status, body);
|
|
58
|
+
}
|
|
59
|
+
const result = body;
|
|
60
|
+
if (!result.apiKey || !result.user?.id) {
|
|
61
|
+
throw new PlatformAuthError(
|
|
62
|
+
"Platform exchange response is missing apiKey or user",
|
|
63
|
+
res.status,
|
|
64
|
+
body
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// src/platform/integrations.ts
|
|
72
|
+
var PlatformHubError = class extends Error {
|
|
73
|
+
constructor(message, status, code, body) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.status = status;
|
|
76
|
+
this.code = code;
|
|
77
|
+
this.body = body;
|
|
78
|
+
this.name = "PlatformHubError";
|
|
79
|
+
}
|
|
80
|
+
status;
|
|
81
|
+
code;
|
|
82
|
+
body;
|
|
83
|
+
};
|
|
84
|
+
var PlatformHubClient = class {
|
|
85
|
+
baseUrl;
|
|
86
|
+
bearer;
|
|
87
|
+
fetchImpl;
|
|
88
|
+
constructor(options) {
|
|
89
|
+
if (!options.baseUrl) throw new Error("PlatformHubClient: baseUrl is required");
|
|
90
|
+
if (!options.bearer) throw new Error("PlatformHubClient: bearer is required");
|
|
91
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
92
|
+
this.bearer = options.bearer;
|
|
93
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
94
|
+
}
|
|
95
|
+
/** List the integration catalog (providers + connectors). */
|
|
96
|
+
catalog() {
|
|
97
|
+
return this.request("GET", "/v1/integrations/catalog");
|
|
98
|
+
}
|
|
99
|
+
/** List the calling user's integration connections. */
|
|
100
|
+
async listConnections() {
|
|
101
|
+
const data = await this.request(
|
|
102
|
+
"GET",
|
|
103
|
+
"/v1/integrations/connections"
|
|
104
|
+
);
|
|
105
|
+
return data.connections;
|
|
106
|
+
}
|
|
107
|
+
/** Revoke (and disable) a connection by id. */
|
|
108
|
+
revokeConnection(connectionId) {
|
|
109
|
+
return this.request(
|
|
110
|
+
"DELETE",
|
|
111
|
+
`/v1/integrations/connections/${encodeURIComponent(connectionId)}`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
/** Begin OAuth — returns the URL to send the user to. */
|
|
115
|
+
startAuth(input) {
|
|
116
|
+
return this.request("POST", "/v1/integrations/auth/start", input);
|
|
117
|
+
}
|
|
118
|
+
/** List connection healthchecks (last known state). */
|
|
119
|
+
async listHealthchecks() {
|
|
120
|
+
const data = await this.request(
|
|
121
|
+
"GET",
|
|
122
|
+
"/v1/integrations/healthchecks"
|
|
123
|
+
);
|
|
124
|
+
return data.healthchecks;
|
|
125
|
+
}
|
|
126
|
+
/** Trigger a fresh healthcheck pass. */
|
|
127
|
+
runHealthchecks() {
|
|
128
|
+
return this.request("POST", "/v1/integrations/healthchecks/run", {});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Mint a sandbox-injectable capability bundle (env vars + scoped
|
|
132
|
+
* capability tokens) so a sandbox can invoke integrations on the
|
|
133
|
+
* user's behalf without seeing the underlying provider tokens.
|
|
134
|
+
*/
|
|
135
|
+
bundleCapabilities(input) {
|
|
136
|
+
return this.request("POST", "/v1/integrations/capabilities/bundle", input);
|
|
137
|
+
}
|
|
138
|
+
async request(method, path, body) {
|
|
139
|
+
const headers = {
|
|
140
|
+
authorization: `Bearer ${this.bearer}`,
|
|
141
|
+
accept: "application/json"
|
|
142
|
+
};
|
|
143
|
+
if (body !== void 0) headers["content-type"] = "application/json";
|
|
144
|
+
const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
145
|
+
method,
|
|
146
|
+
headers,
|
|
147
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
148
|
+
});
|
|
149
|
+
const text = await res.text();
|
|
150
|
+
let parsed = null;
|
|
151
|
+
if (text) {
|
|
152
|
+
try {
|
|
153
|
+
parsed = JSON.parse(text);
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!res.ok || parsed && parsed.success === false) {
|
|
158
|
+
const code = parsed?.error && typeof parsed.error === "object" ? parsed.error.code : void 0;
|
|
159
|
+
const message = parsed?.error && typeof parsed.error === "object" && parsed.error.message || (typeof parsed?.error === "string" ? parsed.error : `Platform hub error (${res.status})`);
|
|
160
|
+
throw new PlatformHubError(message, res.status, code, parsed ?? text);
|
|
161
|
+
}
|
|
162
|
+
if (!parsed) {
|
|
163
|
+
throw new PlatformHubError(
|
|
164
|
+
`Platform hub returned non-JSON success (${res.status})`,
|
|
165
|
+
res.status,
|
|
166
|
+
void 0,
|
|
167
|
+
text
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (parsed.data === void 0) {
|
|
171
|
+
throw new PlatformHubError(
|
|
172
|
+
"Platform hub envelope missing `data`",
|
|
173
|
+
res.status,
|
|
174
|
+
void 0,
|
|
175
|
+
parsed
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return parsed.data;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
export {
|
|
182
|
+
PlatformAuthClient,
|
|
183
|
+
PlatformAuthError,
|
|
184
|
+
PlatformHubClient,
|
|
185
|
+
PlatformHubError
|
|
186
|
+
};
|
|
187
|
+
//# sourceMappingURL=platform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/platform/auth.ts","../src/platform/integrations.ts"],"sourcesContent":["/**\n * Server-side client for the Tangle platform's cross-site SSO bridge.\n *\n * Consumer apps (gtm-agent, tax-agent, legal-agent, creative-agent, …)\n * use this to:\n * 1. Build an /authorize URL that lands the user on id.tangle.tools\n * and brings them back with a single-use code.\n * 2. Exchange that code for an API key + the user's identity.\n *\n * The platform endpoint contract is documented in\n * `products/platform/api/src/routes/cross-site.ts`. This client only\n * speaks HTTP — no SDK weight, no transitive deps.\n */\n\nexport interface PlatformAuthClientOptions {\n /** Platform base URL, e.g. `https://id.tangle.tools`. */\n baseUrl: string\n /** App id as registered in the platform's TRUSTED_APPS registry. */\n appId: string\n /** Override the global fetch (useful for tests + edge runtimes). */\n fetchImpl?: typeof fetch\n}\n\nexport interface AuthorizeUrlOptions {\n /** Required CSRF token; the consumer verifies it on the callback. */\n state: string\n /**\n * Final redirect URI. Must be one of the URIs registered for `appId`\n * on the platform. Omit to use the first registered URI.\n */\n redirectUri?: string\n /** Force the login screen even if a session is already active. */\n prompt?: 'login'\n /** Pre-fill the email field on the login screen. */\n email?: string\n}\n\nexport interface ExchangeCodeResult {\n apiKey: string\n user: {\n id: string\n email: string\n name?: string\n }\n plan: {\n tier: string\n }\n}\n\nexport class PlatformAuthError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly body: unknown,\n ) {\n super(message)\n this.name = 'PlatformAuthError'\n }\n}\n\nexport class PlatformAuthClient {\n private readonly baseUrl: string\n private readonly appId: string\n private readonly fetchImpl: typeof fetch\n\n constructor(options: PlatformAuthClientOptions) {\n if (!options.baseUrl) throw new Error('PlatformAuthClient: baseUrl is required')\n if (!options.appId) throw new Error('PlatformAuthClient: appId is required')\n this.baseUrl = options.baseUrl.replace(/\\/+$/, '')\n this.appId = options.appId\n this.fetchImpl = options.fetchImpl ?? fetch\n }\n\n /**\n * Build the URL the user is redirected to in order to start SSO.\n * The platform redirects back to one of `appId`'s registered\n * `redirectUris` with `?code=...&app=...&state=...`.\n */\n authorizeUrl(options: AuthorizeUrlOptions): string {\n if (!options.state) {\n throw new Error('PlatformAuthClient.authorizeUrl: state is required for CSRF')\n }\n const url = new URL('/cross-site/authorize', this.baseUrl)\n url.searchParams.set('app', this.appId)\n url.searchParams.set('state', options.state)\n if (options.redirectUri) url.searchParams.set('redirect', options.redirectUri)\n if (options.prompt) url.searchParams.set('prompt', options.prompt)\n if (options.email) url.searchParams.set('email', options.email)\n return url.toString()\n }\n\n /**\n * Exchange a single-use auth code (delivered to the consumer's\n * callback by the platform) for an API key + the user's identity.\n * Codes are single-use and expire ~5 minutes after issue.\n */\n async exchange(code: string): Promise<ExchangeCodeResult> {\n if (!code) throw new Error('PlatformAuthClient.exchange: code is required')\n const res = await this.fetchImpl(`${this.baseUrl}/cross-site/exchange`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ code, app: this.appId }),\n })\n const body = await res.json().catch(() => null)\n if (!res.ok) {\n const message =\n body && typeof body === 'object' && 'error' in body && typeof body.error === 'string'\n ? body.error\n : `Platform exchange failed (${res.status})`\n throw new PlatformAuthError(message, res.status, body)\n }\n const result = body as Partial<ExchangeCodeResult>\n if (!result.apiKey || !result.user?.id) {\n throw new PlatformAuthError(\n 'Platform exchange response is missing apiKey or user',\n res.status,\n body,\n )\n }\n return result as ExchangeCodeResult\n }\n}\n","/**\n * Server-side client for the Tangle platform's integrations hub\n * (`/v1/integrations/*`). Consumer apps use this instead of rolling\n * their own OAuth + connection tables.\n *\n * Auth: the caller supplies a bearer (either the user's API key from\n * cross-site exchange, or a platform service token) on construction.\n * Per-request override via `headers` is supported.\n *\n * Endpoint contract: `products/platform/api/src/routes/integrations.ts`.\n */\n\nexport interface PlatformHubClientOptions {\n /** Platform base URL, e.g. `https://id.tangle.tools`. */\n baseUrl: string\n /** Bearer credential — user API key or service token. */\n bearer: string\n /** Override fetch (tests + edge runtimes). */\n fetchImpl?: typeof fetch\n}\n\nexport interface PlatformConnection {\n id: string\n providerId: string\n connectorId: string\n status: 'connected' | 'pending' | 'revoked' | 'expired' | string\n grantedScopes?: string[]\n account?: { identity?: string; displayName?: string } & Record<string, unknown>\n metadata?: Record<string, unknown>\n expiresAt?: string | null\n createdAt?: string\n updatedAt?: string\n}\n\nexport interface PlatformCatalogProvider {\n providerId: string\n displayName?: string\n description?: string\n authMode?: string\n connectors?: PlatformCatalogConnector[]\n [k: string]: unknown\n}\n\nexport interface PlatformCatalogConnector {\n connectorId: string\n displayName?: string\n description?: string\n scopes?: string[]\n [k: string]: unknown\n}\n\nexport interface StartAuthInput {\n providerId: string\n connectorId: string\n /** Where the platform redirects the user back to after OAuth. */\n returnUrl: string\n requestedScopes?: string[]\n state?: string\n metadata?: Record<string, unknown>\n /** Required when the bearer is a service token impersonating a user. */\n ownerUserId?: string\n}\n\nexport interface StartAuthResult {\n authorizationUrl: string\n state: string\n}\n\nexport interface BundleCapabilityInput {\n manifestId?: string\n grantIds?: string[]\n subject: { type: 'user' | 'team' | 'app'; id: string }\n ttlMs: number\n}\n\nexport interface BundleCapabilityResult {\n bundle: Record<string, unknown>\n env: Record<string, string>\n}\n\nexport interface HealthCheck {\n connectionId: string\n providerId: string\n connectorId: string\n status: 'ok' | 'degraded' | 'failing' | 'unknown' | string\n checks?: Record<string, unknown>\n checkedAt?: string\n}\n\nexport class PlatformHubError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly code: string | undefined,\n public readonly body: unknown,\n ) {\n super(message)\n this.name = 'PlatformHubError'\n }\n}\n\ninterface PlatformEnvelope<T> {\n success: boolean\n data?: T\n error?: { code?: string; message?: string } | string\n}\n\nexport class PlatformHubClient {\n private readonly baseUrl: string\n private readonly bearer: string\n private readonly fetchImpl: typeof fetch\n\n constructor(options: PlatformHubClientOptions) {\n if (!options.baseUrl) throw new Error('PlatformHubClient: baseUrl is required')\n if (!options.bearer) throw new Error('PlatformHubClient: bearer is required')\n this.baseUrl = options.baseUrl.replace(/\\/+$/, '')\n this.bearer = options.bearer\n this.fetchImpl = options.fetchImpl ?? fetch\n }\n\n /** List the integration catalog (providers + connectors). */\n catalog(): Promise<{ providers: PlatformCatalogProvider[] } & Record<string, unknown>> {\n return this.request('GET', '/v1/integrations/catalog')\n }\n\n /** List the calling user's integration connections. */\n async listConnections(): Promise<PlatformConnection[]> {\n const data = await this.request<{ connections: PlatformConnection[] }>(\n 'GET',\n '/v1/integrations/connections',\n )\n return data.connections\n }\n\n /** Revoke (and disable) a connection by id. */\n revokeConnection(connectionId: string): Promise<{\n connection: PlatformConnection\n revokedGrants: unknown[]\n providerRevocation: { ok: boolean }\n }> {\n return this.request(\n 'DELETE',\n `/v1/integrations/connections/${encodeURIComponent(connectionId)}`,\n )\n }\n\n /** Begin OAuth — returns the URL to send the user to. */\n startAuth(input: StartAuthInput): Promise<StartAuthResult> {\n return this.request('POST', '/v1/integrations/auth/start', input)\n }\n\n /** List connection healthchecks (last known state). */\n async listHealthchecks(): Promise<HealthCheck[]> {\n const data = await this.request<{ healthchecks: HealthCheck[] }>(\n 'GET',\n '/v1/integrations/healthchecks',\n )\n return data.healthchecks\n }\n\n /** Trigger a fresh healthcheck pass. */\n runHealthchecks(): Promise<{ scheduled: number }> {\n return this.request('POST', '/v1/integrations/healthchecks/run', {})\n }\n\n /**\n * Mint a sandbox-injectable capability bundle (env vars + scoped\n * capability tokens) so a sandbox can invoke integrations on the\n * user's behalf without seeing the underlying provider tokens.\n */\n bundleCapabilities(input: BundleCapabilityInput): Promise<BundleCapabilityResult> {\n return this.request('POST', '/v1/integrations/capabilities/bundle', input)\n }\n\n private async request<T>(\n method: 'GET' | 'POST' | 'DELETE' | 'PUT',\n path: string,\n body?: unknown,\n ): Promise<T> {\n const headers: Record<string, string> = {\n authorization: `Bearer ${this.bearer}`,\n accept: 'application/json',\n }\n if (body !== undefined) headers['content-type'] = 'application/json'\n\n const res = await this.fetchImpl(`${this.baseUrl}${path}`, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n })\n const text = await res.text()\n let parsed: PlatformEnvelope<T> | null = null\n if (text) {\n try {\n parsed = JSON.parse(text)\n } catch {\n // fall through to error handling below\n }\n }\n if (!res.ok || (parsed && parsed.success === false)) {\n const code = parsed?.error && typeof parsed.error === 'object' ? parsed.error.code : undefined\n const message =\n (parsed?.error && typeof parsed.error === 'object' && parsed.error.message) ||\n (typeof parsed?.error === 'string' ? parsed.error : `Platform hub error (${res.status})`)\n throw new PlatformHubError(message, res.status, code, parsed ?? text)\n }\n if (!parsed) {\n throw new PlatformHubError(\n `Platform hub returned non-JSON success (${res.status})`,\n res.status,\n undefined,\n text,\n )\n }\n if (parsed.data === undefined) {\n throw new PlatformHubError(\n 'Platform hub envelope missing `data`',\n res.status,\n undefined,\n parsed,\n )\n }\n return parsed.data\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { AnalystRunEvent, AnalystRunInputs, AnalystFinding, AnalystRunResult, FindingsDiff } from '@tangle-network/agent-eval';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public types for the closed-loop analyst orchestrator.
|
|
5
|
+
*
|
|
6
|
+
* The orchestrator is the one call agent apps reach for. It binds:
|
|
7
|
+
* - the AnalystRegistry + a chosen set of analyst kinds
|
|
8
|
+
* - a FindingsStore (durable JSONL ledger + cross-run diff)
|
|
9
|
+
* - an optional KnowledgeAdapter (closes the analysis → wiki side)
|
|
10
|
+
* - an optional ImprovementAdapter (closes the analysis → prompt /
|
|
11
|
+
* tool / scaffolding side)
|
|
12
|
+
*
|
|
13
|
+
* Adapters keep agent-runtime decoupled from the specific storage
|
|
14
|
+
* implementation. Consumers wire agent-knowledge once at app init.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Knowledge-side bridge — consumers wire `proposeFromFindings` from agent-knowledge. */
|
|
18
|
+
interface KnowledgeAdapter<TProposal = unknown> {
|
|
19
|
+
/**
|
|
20
|
+
* Convert a findings batch into proposals. Returns the partitioned
|
|
21
|
+
* result so the loop can report (and optionally fail on) malformed
|
|
22
|
+
* findings. Implementations SHOULD honour the convention "non-
|
|
23
|
+
* knowledge subjects return null and are counted in `skipped`."
|
|
24
|
+
*/
|
|
25
|
+
proposeFromFindings(findings: ReadonlyArray<AnalystFinding>): Promise<KnowledgeProposalBatch<TProposal>> | KnowledgeProposalBatch<TProposal>;
|
|
26
|
+
/**
|
|
27
|
+
* Optional auto-apply. The loop calls this only when
|
|
28
|
+
* `autoApply.knowledge` is true AND the proposal's source-finding
|
|
29
|
+
* confidence ≥ `autoApply.knowledgeConfidenceThreshold`. Anything
|
|
30
|
+
* below the threshold is returned in the report but never written.
|
|
31
|
+
*/
|
|
32
|
+
apply?(proposals: ReadonlyArray<TProposal>): Promise<{
|
|
33
|
+
written: string[];
|
|
34
|
+
warnings: string[];
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
interface KnowledgeProposalBatch<TProposal = unknown> {
|
|
38
|
+
proposals: TProposal[];
|
|
39
|
+
skipped: number;
|
|
40
|
+
errors: Array<{
|
|
41
|
+
findingId: string;
|
|
42
|
+
subject: string;
|
|
43
|
+
message: string;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
/** Improvement-side bridge — proposes / applies prompt + tool + scaffolding edits. */
|
|
47
|
+
interface ImprovementAdapter<TEdit = unknown> {
|
|
48
|
+
proposeFromFindings(findings: ReadonlyArray<AnalystFinding>): Promise<ImprovementEditBatch<TEdit>> | ImprovementEditBatch<TEdit>;
|
|
49
|
+
apply?(edits: ReadonlyArray<TEdit>): Promise<{
|
|
50
|
+
applied: string[];
|
|
51
|
+
warnings: string[];
|
|
52
|
+
}>;
|
|
53
|
+
}
|
|
54
|
+
interface ImprovementEditBatch<TEdit = unknown> {
|
|
55
|
+
edits: TEdit[];
|
|
56
|
+
skipped: number;
|
|
57
|
+
errors: Array<{
|
|
58
|
+
findingId: string;
|
|
59
|
+
subject: string;
|
|
60
|
+
message: string;
|
|
61
|
+
}>;
|
|
62
|
+
}
|
|
63
|
+
/** Tunable safety rails for auto-apply. */
|
|
64
|
+
interface AutoApplyPolicy {
|
|
65
|
+
/** When true AND `knowledgeAdapter.apply` exists, write knowledge proposals. */
|
|
66
|
+
knowledge?: boolean;
|
|
67
|
+
/** Minimum source-finding confidence required to auto-apply a knowledge proposal. */
|
|
68
|
+
knowledgeConfidenceThreshold?: number;
|
|
69
|
+
/** When true AND `improvementAdapter.apply` exists, apply improvement edits. */
|
|
70
|
+
improvement?: boolean;
|
|
71
|
+
/** Minimum source-finding confidence required to auto-apply an improvement edit. */
|
|
72
|
+
improvementConfidenceThreshold?: number;
|
|
73
|
+
}
|
|
74
|
+
interface RunAnalystLoopOpts {
|
|
75
|
+
/** The run id of the work being analysed. */
|
|
76
|
+
runId: string;
|
|
77
|
+
/** The registry — pre-populated with the analyst kinds the consumer wants. */
|
|
78
|
+
registry: AnalystRegistryLike;
|
|
79
|
+
/** Inputs forwarded to `registry.run` — typically `{ traceStore }`. */
|
|
80
|
+
inputs: AnalystRunInputs;
|
|
81
|
+
/**
|
|
82
|
+
* Findings ledger. The loop appends the new run + diffs against the
|
|
83
|
+
* baseline run before running adapters. Pass `null` to skip
|
|
84
|
+
* persistence (useful for one-shot analyses).
|
|
85
|
+
*/
|
|
86
|
+
findingsStore: FindingsStoreLike | null;
|
|
87
|
+
/**
|
|
88
|
+
* Prior run id whose findings the loop reads + provides to analysts
|
|
89
|
+
* as `priorFindings` AND diffs against. When omitted, the loop picks
|
|
90
|
+
* the most recent run in the store (excluding `runId` itself); pass
|
|
91
|
+
* `null` to explicitly start with an empty baseline.
|
|
92
|
+
*/
|
|
93
|
+
baselineRunId?: string | null;
|
|
94
|
+
/** Strategy for forwarding prior findings into `ctx.priorFindings`. */
|
|
95
|
+
priorFindingsStrategy?: 'per-kind' | 'wildcard' | 'none';
|
|
96
|
+
/** Knowledge-side bridge — usually `agent-knowledge`'s `proposeFromFindings`. */
|
|
97
|
+
knowledgeAdapter?: KnowledgeAdapter;
|
|
98
|
+
/** Improvement-side bridge — usually a consumer-specific prompt/tool diff producer. */
|
|
99
|
+
improvementAdapter?: ImprovementAdapter;
|
|
100
|
+
/** Auto-apply rails. Default off; review-then-apply is the safer default. */
|
|
101
|
+
autoApply?: AutoApplyPolicy;
|
|
102
|
+
/** Optional logger. Defaults to `console.log` for `[analyst-loop]` lines. */
|
|
103
|
+
log?: (msg: string, fields?: Record<string, unknown>) => void;
|
|
104
|
+
/**
|
|
105
|
+
* Event sink for live progress. Called for every phase of the loop:
|
|
106
|
+
* baseline resolution, registry events forwarded from `runStream`,
|
|
107
|
+
* ledger persistence, diff, knowledge / improvement proposals +
|
|
108
|
+
* apply outcomes, and the terminal `loop-completed`. Awaited so
|
|
109
|
+
* slow sinks (SSE write, JSONL append) apply backpressure.
|
|
110
|
+
*
|
|
111
|
+
* The callback MUST NOT throw — exceptions propagate and abort the
|
|
112
|
+
* loop. Catch + swallow internally if your sink is unreliable.
|
|
113
|
+
*/
|
|
114
|
+
onEvent?: (event: AnalystLoopEvent) => void | Promise<void>;
|
|
115
|
+
}
|
|
116
|
+
interface RunAnalystLoopResult<TProposal = unknown, TEdit = unknown> {
|
|
117
|
+
runId: string;
|
|
118
|
+
baselineRunId: string | null;
|
|
119
|
+
analystResult: AnalystRunResult;
|
|
120
|
+
diff: FindingsDiff | null;
|
|
121
|
+
knowledge: KnowledgeReport<TProposal> | null;
|
|
122
|
+
improvement: ImprovementReport<TEdit> | null;
|
|
123
|
+
}
|
|
124
|
+
interface KnowledgeReport<TProposal = unknown> {
|
|
125
|
+
proposals: TProposal[];
|
|
126
|
+
applied: string[];
|
|
127
|
+
skipped: number;
|
|
128
|
+
errors: Array<{
|
|
129
|
+
findingId: string;
|
|
130
|
+
subject: string;
|
|
131
|
+
message: string;
|
|
132
|
+
}>;
|
|
133
|
+
withheld_for_review: number;
|
|
134
|
+
}
|
|
135
|
+
interface ImprovementReport<TEdit = unknown> {
|
|
136
|
+
edits: TEdit[];
|
|
137
|
+
applied: string[];
|
|
138
|
+
skipped: number;
|
|
139
|
+
errors: Array<{
|
|
140
|
+
findingId: string;
|
|
141
|
+
subject: string;
|
|
142
|
+
message: string;
|
|
143
|
+
}>;
|
|
144
|
+
withheld_for_review: number;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Narrowed shape we accept for `AnalystRegistry` so the orchestrator
|
|
148
|
+
* remains testable without instantiating the real class. The real
|
|
149
|
+
* class satisfies this trivially.
|
|
150
|
+
*/
|
|
151
|
+
interface AnalystRegistryLike {
|
|
152
|
+
list(): ReadonlyArray<{
|
|
153
|
+
id: string;
|
|
154
|
+
}>;
|
|
155
|
+
run(runId: string, inputs: AnalystRunInputs, opts?: {
|
|
156
|
+
priorFindings?: ReadonlyArray<AnalystFinding> | Record<string, ReadonlyArray<AnalystFinding>>;
|
|
157
|
+
[k: string]: unknown;
|
|
158
|
+
}): Promise<AnalystRunResult>;
|
|
159
|
+
}
|
|
160
|
+
/** Narrowed shape we accept for `FindingsStore`. */
|
|
161
|
+
interface FindingsStoreLike {
|
|
162
|
+
loadAll(): ReadonlyArray<AnalystFinding & {
|
|
163
|
+
run_id: string;
|
|
164
|
+
}>;
|
|
165
|
+
loadRun(runId: string): ReadonlyArray<AnalystFinding & {
|
|
166
|
+
run_id: string;
|
|
167
|
+
}>;
|
|
168
|
+
append(runId: string, findings: ReadonlyArray<AnalystFinding>): Promise<void>;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Narrow the `AnalystRegistryLike` further when we need streaming: the
|
|
173
|
+
* loop checks if the registry exposes `runStream` and uses it when
|
|
174
|
+
* present, falling back to `run()` otherwise. This keeps the type
|
|
175
|
+
* surface backwards-compatible — older registry shims that only
|
|
176
|
+
* implement `run` still work; they just don't forward per-analyst
|
|
177
|
+
* events.
|
|
178
|
+
*/
|
|
179
|
+
interface AnalystRegistryStreamingLike extends AnalystRegistryLike {
|
|
180
|
+
runStream?(runId: string, inputs: AnalystRunInputs, opts?: {
|
|
181
|
+
priorFindings?: ReadonlyArray<AnalystFinding> | Record<string, ReadonlyArray<AnalystFinding>>;
|
|
182
|
+
[k: string]: unknown;
|
|
183
|
+
}): AsyncIterable<AnalystRunEvent>;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Events emitted by `runAnalystLoop` via `opts.onEvent`. UIs and
|
|
187
|
+
* JSONL tail-sinks consume this stream. The loop awaits each
|
|
188
|
+
* callback so a slow sink applies backpressure to the loop's phases
|
|
189
|
+
* (e.g. an SSE write that takes 200ms delays the next phase by
|
|
190
|
+
* 200ms — the loop never out-paces its observer).
|
|
191
|
+
*
|
|
192
|
+
* Forwards registry events verbatim via `analyst` so consumers don't
|
|
193
|
+
* have to wire two streams.
|
|
194
|
+
*/
|
|
195
|
+
type AnalystLoopEvent = {
|
|
196
|
+
type: 'baseline-resolved';
|
|
197
|
+
runId: string;
|
|
198
|
+
baselineRunId: string | null;
|
|
199
|
+
priorFindingCount: number;
|
|
200
|
+
} | {
|
|
201
|
+
type: 'analyst';
|
|
202
|
+
runId: string;
|
|
203
|
+
/** Forwarded verbatim from `AnalystRegistry.runStream`. */
|
|
204
|
+
event: AnalystRunEvent;
|
|
205
|
+
} | {
|
|
206
|
+
type: 'findings-persisted';
|
|
207
|
+
runId: string;
|
|
208
|
+
count: number;
|
|
209
|
+
} | {
|
|
210
|
+
type: 'diff-computed';
|
|
211
|
+
runId: string;
|
|
212
|
+
baselineRunId: string;
|
|
213
|
+
appeared: number;
|
|
214
|
+
disappeared: number;
|
|
215
|
+
persisted: number;
|
|
216
|
+
changed: number;
|
|
217
|
+
} | {
|
|
218
|
+
type: 'knowledge-proposed';
|
|
219
|
+
runId: string;
|
|
220
|
+
proposalCount: number;
|
|
221
|
+
skipped: number;
|
|
222
|
+
errors: number;
|
|
223
|
+
} | {
|
|
224
|
+
type: 'knowledge-applied';
|
|
225
|
+
runId: string;
|
|
226
|
+
writtenCount: number;
|
|
227
|
+
withheldForReview: number;
|
|
228
|
+
} | {
|
|
229
|
+
type: 'improvement-proposed';
|
|
230
|
+
runId: string;
|
|
231
|
+
editCount: number;
|
|
232
|
+
skipped: number;
|
|
233
|
+
errors: number;
|
|
234
|
+
} | {
|
|
235
|
+
type: 'improvement-applied';
|
|
236
|
+
runId: string;
|
|
237
|
+
appliedCount: number;
|
|
238
|
+
withheldForReview: number;
|
|
239
|
+
} | {
|
|
240
|
+
type: 'loop-completed';
|
|
241
|
+
runId: string;
|
|
242
|
+
durationMs: number;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export type { AnalystLoopEvent as A, FindingsStoreLike as F, ImprovementAdapter as I, KnowledgeAdapter as K, RunAnalystLoopOpts as R, RunAnalystLoopResult as a, AnalystRegistryLike as b, AnalystRegistryStreamingLike as c, AutoApplyPolicy as d, ImprovementEditBatch as e, ImprovementReport as f, KnowledgeProposalBatch as g, KnowledgeReport as h };
|