agenttwin 0.3.1 → 0.3.3
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/dist/api.js +801 -48
- package/dist/cli.js +860 -738
- package/dist/dashboard/assets/dashboard/dashboard.css +2 -0
- package/dist/dashboard/assets/dashboard/dashboard.js +20 -7
- package/dist/dashboard/index.html +1 -1
- package/dist/gateway.js +801 -48
- package/package.json +2 -1
package/dist/api.js
CHANGED
|
@@ -91,33 +91,785 @@ var init_dashboard_transfer = __esm({
|
|
|
91
91
|
}
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
// src/lib/files.ts
|
|
95
|
+
import { readdir as readdir2, readFile as readFile4, stat } from "fs/promises";
|
|
96
|
+
import { join as join4, relative, sep } from "path";
|
|
97
|
+
function shouldIgnoreDir(name) {
|
|
98
|
+
return IGNORED_DIRS.has(name) || name === ".git";
|
|
99
|
+
}
|
|
100
|
+
async function walkDir(root, maxFiles, budgetMs) {
|
|
101
|
+
const out = [];
|
|
102
|
+
const limits = [];
|
|
103
|
+
const stack = [root];
|
|
104
|
+
const started = Date.now();
|
|
105
|
+
while (stack.length > 0 && out.length < maxFiles && Date.now() - started < budgetMs) {
|
|
106
|
+
const dir = stack.pop();
|
|
107
|
+
let entries;
|
|
108
|
+
try {
|
|
109
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
110
|
+
} catch {
|
|
111
|
+
limits.push(`Directory could not be read: ${relative(root, dir) || "."}`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
const full = join4(dir, entry.name);
|
|
117
|
+
if (entry.isDirectory()) {
|
|
118
|
+
if (!shouldIgnoreDir(entry.name)) stack.push(full);
|
|
119
|
+
} else if (entry.isFile()) {
|
|
120
|
+
out.push(relative(root, full).split(sep).join("/"));
|
|
121
|
+
if (out.length >= maxFiles) break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (out.length >= maxFiles) limits.push(`File scan limit reached (${maxFiles} files); additional files may be unscanned`);
|
|
126
|
+
if (stack.length && Date.now() - started >= budgetMs) limits.push(`File walk time budget reached (${budgetMs}ms); directories remain unscanned`);
|
|
127
|
+
return { files: out.sort(), limits };
|
|
128
|
+
}
|
|
129
|
+
async function createFileIndex(cwd) {
|
|
130
|
+
return new RepoFileIndex(cwd);
|
|
131
|
+
}
|
|
132
|
+
var IGNORED_DIRS, MAX_FILES, MAX_BYTES, WALK_BUDGET_MS, RepoFileIndex;
|
|
133
|
+
var init_files = __esm({
|
|
134
|
+
"src/lib/files.ts"() {
|
|
135
|
+
"use strict";
|
|
136
|
+
IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
137
|
+
".git",
|
|
138
|
+
".agenttwin",
|
|
139
|
+
"node_modules",
|
|
140
|
+
"dist",
|
|
141
|
+
"build",
|
|
142
|
+
"out",
|
|
143
|
+
"coverage",
|
|
144
|
+
".next",
|
|
145
|
+
".nuxt",
|
|
146
|
+
".venv",
|
|
147
|
+
"venv",
|
|
148
|
+
"env",
|
|
149
|
+
"site-packages",
|
|
150
|
+
"__pycache__",
|
|
151
|
+
".pytest_cache",
|
|
152
|
+
".mypy_cache",
|
|
153
|
+
".ruff_cache",
|
|
154
|
+
".tox",
|
|
155
|
+
"vendor",
|
|
156
|
+
"target",
|
|
157
|
+
".turbo",
|
|
158
|
+
".cache",
|
|
159
|
+
".idea",
|
|
160
|
+
".vscode",
|
|
161
|
+
".terraform",
|
|
162
|
+
"models",
|
|
163
|
+
"data",
|
|
164
|
+
"datasets",
|
|
165
|
+
"media",
|
|
166
|
+
"uploads",
|
|
167
|
+
"tmp",
|
|
168
|
+
"logs",
|
|
169
|
+
"gen"
|
|
170
|
+
]);
|
|
171
|
+
MAX_FILES = 2e4;
|
|
172
|
+
MAX_BYTES = 1e6;
|
|
173
|
+
WALK_BUDGET_MS = 8e3;
|
|
174
|
+
RepoFileIndex = class {
|
|
175
|
+
cwd;
|
|
176
|
+
cache = { contents: /* @__PURE__ */ new Map(), limits: [] };
|
|
177
|
+
bounds;
|
|
178
|
+
constructor(cwd, bounds = { maxFiles: MAX_FILES, budgetMs: WALK_BUDGET_MS }) {
|
|
179
|
+
this.cwd = cwd;
|
|
180
|
+
this.bounds = bounds;
|
|
181
|
+
}
|
|
182
|
+
async walk() {
|
|
183
|
+
if (!this.cache.files) {
|
|
184
|
+
const result = await walkDir(this.cwd, this.bounds.maxFiles, this.bounds.budgetMs);
|
|
185
|
+
this.cache.files = result.files;
|
|
186
|
+
this.cache.limits.push(...result.limits);
|
|
187
|
+
}
|
|
188
|
+
return this.cache.files;
|
|
189
|
+
}
|
|
190
|
+
scanLimits() {
|
|
191
|
+
return [...new Set(this.cache.limits)].sort();
|
|
192
|
+
}
|
|
193
|
+
async exists(path) {
|
|
194
|
+
try {
|
|
195
|
+
await stat(join4(this.cwd, path));
|
|
196
|
+
return true;
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async read(path, maxBytes = MAX_BYTES) {
|
|
202
|
+
const cached = this.cache.contents.get(path);
|
|
203
|
+
if (cached !== void 0) return cached;
|
|
204
|
+
const full = join4(this.cwd, path);
|
|
205
|
+
try {
|
|
206
|
+
const info = await stat(full);
|
|
207
|
+
if (!info.isFile()) return void 0;
|
|
208
|
+
if (info.size > maxBytes) {
|
|
209
|
+
this.cache.limits.push(`File excluded by size limit (${maxBytes} bytes): ${path}`);
|
|
210
|
+
return void 0;
|
|
211
|
+
}
|
|
212
|
+
const text = await readFile4(full, "utf8");
|
|
213
|
+
this.cache.contents.set(path, text);
|
|
214
|
+
return text;
|
|
215
|
+
} catch {
|
|
216
|
+
return void 0;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async byExt(...exts) {
|
|
220
|
+
const wanted = new Set(exts.map((e) => e.replace(/^\./, "").toLowerCase()));
|
|
221
|
+
const files = await this.walk();
|
|
222
|
+
return files.filter((f) => {
|
|
223
|
+
const dot = f.lastIndexOf(".");
|
|
224
|
+
if (dot < 0) return false;
|
|
225
|
+
return wanted.has(f.slice(dot + 1).toLowerCase());
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
async byName(pattern) {
|
|
229
|
+
const files = await this.walk();
|
|
230
|
+
return files.filter((f) => pattern.test(f));
|
|
231
|
+
}
|
|
232
|
+
line(ref) {
|
|
233
|
+
const idx = ref.lastIndexOf(":");
|
|
234
|
+
if (idx < 0) return void 0;
|
|
235
|
+
const path = ref.slice(0, idx);
|
|
236
|
+
const num = Number(ref.slice(idx + 1));
|
|
237
|
+
if (!Number.isInteger(num)) return void 0;
|
|
238
|
+
const text = this.cache.contents.get(path);
|
|
239
|
+
if (text === void 0) return void 0;
|
|
240
|
+
const lines = text.split(/\r?\n/);
|
|
241
|
+
return lines[num - 1];
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// src/inventory/entrypoints.ts
|
|
248
|
+
import { parse } from "@babel/parser";
|
|
249
|
+
function exportedEntrypoints(source) {
|
|
250
|
+
const result = [];
|
|
251
|
+
let file;
|
|
252
|
+
try {
|
|
253
|
+
file = parse(source, { sourceType: "unambiguous", plugins: ["typescript", "jsx"], errorRecovery: true });
|
|
254
|
+
} catch {
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
for (const node of file.program.body) {
|
|
258
|
+
if (node.type !== "ExportNamedDeclaration" && node.type !== "ExportDefaultDeclaration") continue;
|
|
259
|
+
const declaration = node.declaration;
|
|
260
|
+
if (!declaration) continue;
|
|
261
|
+
if (declaration.type === "FunctionDeclaration" && declaration.id && declaration.body.loc) result.push({ name: declaration.id.name, startLine: node.loc.start.line, endLine: declaration.body.loc.end.line });
|
|
262
|
+
if (declaration.type === "VariableDeclaration") for (const variable of declaration.declarations) {
|
|
263
|
+
const fn = variable.init;
|
|
264
|
+
if (variable.id.type === "Identifier" && fn && (fn.type === "ArrowFunctionExpression" || fn.type === "FunctionExpression") && fn.body.type === "BlockStatement" && fn.body.loc) result.push({ name: variable.id.name, startLine: node.loc.start.line, endLine: fn.body.loc.end.line });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
var init_entrypoints = __esm({
|
|
270
|
+
"src/inventory/entrypoints.ts"() {
|
|
271
|
+
"use strict";
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// src/discovery/masking.ts
|
|
276
|
+
function maskSecret(value) {
|
|
277
|
+
return value.replace(TOKENISH, (match) => `${match.slice(0, 4)}***`);
|
|
278
|
+
}
|
|
279
|
+
function maskUrl(value) {
|
|
280
|
+
try {
|
|
281
|
+
const url = new URL(value);
|
|
282
|
+
return `${url.protocol}//${url.host}${url.pathname}`;
|
|
283
|
+
} catch {
|
|
284
|
+
return maskSecret(value);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
var TOKENISH;
|
|
288
|
+
var init_masking = __esm({
|
|
289
|
+
"src/discovery/masking.ts"() {
|
|
290
|
+
"use strict";
|
|
291
|
+
TOKENISH = /\b(sk|rk|pk|ghp|gho|ghs|xox[baprs]|AKIA|AIza)[A-Za-z0-9_-]{6,}\b/g;
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// src/discovery/probes/providers/stripe.ts
|
|
296
|
+
var stripeProbe;
|
|
297
|
+
var init_stripe = __esm({
|
|
298
|
+
"src/discovery/probes/providers/stripe.ts"() {
|
|
299
|
+
"use strict";
|
|
300
|
+
init_masking();
|
|
301
|
+
stripeProbe = {
|
|
302
|
+
id: "stripe.readonly",
|
|
303
|
+
providerId: "stripe",
|
|
304
|
+
title: "Enumerate Stripe account configuration (read-only)",
|
|
305
|
+
description: "Reads products, prices and webhook endpoints. Read-only; issues no writes.",
|
|
306
|
+
readScope: {
|
|
307
|
+
endpoints: [
|
|
308
|
+
"GET /v1/products",
|
|
309
|
+
"GET /v1/prices",
|
|
310
|
+
"GET /v1/webhook_endpoints"
|
|
311
|
+
],
|
|
312
|
+
methods: ["GET"]
|
|
313
|
+
},
|
|
314
|
+
credentials: [
|
|
315
|
+
{
|
|
316
|
+
envVar: "STRIPE_SECRET_KEY",
|
|
317
|
+
label: "Stripe restricted key (read scopes)",
|
|
318
|
+
required: true,
|
|
319
|
+
scopes: ["products:read", "prices:read", "webhook_endpoints:read"],
|
|
320
|
+
hint: "Prefer a restricted key (rk_...) with read scopes over a full sk_live_ key."
|
|
321
|
+
}
|
|
322
|
+
],
|
|
323
|
+
async run(ctx) {
|
|
324
|
+
const spec = this.credentials[0];
|
|
325
|
+
const key = ctx.credential(spec);
|
|
326
|
+
if (!key) {
|
|
327
|
+
return {
|
|
328
|
+
providerId: this.providerId,
|
|
329
|
+
ok: false,
|
|
330
|
+
summary: "no credential present",
|
|
331
|
+
resources: [],
|
|
332
|
+
evidence: []
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const get = async (path) => {
|
|
336
|
+
const response = await ctx.fetch(`https://api.stripe.com${path}`, {
|
|
337
|
+
method: "GET",
|
|
338
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
339
|
+
signal: ctx.signal
|
|
340
|
+
});
|
|
341
|
+
if (!response.ok) {
|
|
342
|
+
throw new Error(`Stripe ${path} -> HTTP ${response.status}`);
|
|
343
|
+
}
|
|
344
|
+
return await response.json();
|
|
345
|
+
};
|
|
346
|
+
try {
|
|
347
|
+
const [products, prices, hooks] = await Promise.all([
|
|
348
|
+
get("/v1/products?limit=100"),
|
|
349
|
+
get("/v1/prices?limit=100"),
|
|
350
|
+
get("/v1/webhook_endpoints?limit=100")
|
|
351
|
+
]);
|
|
352
|
+
return {
|
|
353
|
+
providerId: this.providerId,
|
|
354
|
+
ok: true,
|
|
355
|
+
summary: `${products.data.length} products, ${prices.data.length} prices, ${hooks.data.length} webhook endpoints`,
|
|
356
|
+
resources: [
|
|
357
|
+
{
|
|
358
|
+
kind: "products",
|
|
359
|
+
count: products.data.length,
|
|
360
|
+
sample: products.data.slice(0, 5).map((p) => ({
|
|
361
|
+
id: p.id,
|
|
362
|
+
name: p.name,
|
|
363
|
+
active: p.active
|
|
364
|
+
}))
|
|
365
|
+
},
|
|
366
|
+
{ kind: "prices", count: prices.data.length },
|
|
367
|
+
{
|
|
368
|
+
kind: "webhook_endpoints",
|
|
369
|
+
count: hooks.data.length,
|
|
370
|
+
sample: hooks.data.slice(0, 5).map((h) => ({
|
|
371
|
+
url: typeof h.url === "string" ? maskUrl(h.url) : void 0,
|
|
372
|
+
status: h.status
|
|
373
|
+
}))
|
|
374
|
+
}
|
|
375
|
+
],
|
|
376
|
+
evidence: [
|
|
377
|
+
{
|
|
378
|
+
ref: "probe:stripe",
|
|
379
|
+
pass: "probe",
|
|
380
|
+
signal: "probe",
|
|
381
|
+
detail: "account reachable via read scopes"
|
|
382
|
+
}
|
|
383
|
+
]
|
|
384
|
+
};
|
|
385
|
+
} catch (error) {
|
|
386
|
+
return {
|
|
387
|
+
providerId: this.providerId,
|
|
388
|
+
ok: false,
|
|
389
|
+
error: { message: error instanceof Error ? error.message : String(error) },
|
|
390
|
+
summary: "probe failed",
|
|
391
|
+
resources: [],
|
|
392
|
+
evidence: []
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// src/discovery/probes/providers/github.ts
|
|
401
|
+
var githubProbe;
|
|
402
|
+
var init_github = __esm({
|
|
403
|
+
"src/discovery/probes/providers/github.ts"() {
|
|
404
|
+
"use strict";
|
|
405
|
+
githubProbe = {
|
|
406
|
+
id: "github.readonly",
|
|
407
|
+
providerId: "github",
|
|
408
|
+
title: "Enumerate GitHub account (read-only)",
|
|
409
|
+
description: "Reads the authenticated user and their repositories. Read-only.",
|
|
410
|
+
readScope: {
|
|
411
|
+
endpoints: ["GET /user", "GET /user/repos"],
|
|
412
|
+
methods: ["GET"]
|
|
413
|
+
},
|
|
414
|
+
credentials: [
|
|
415
|
+
{
|
|
416
|
+
envVar: "GITHUB_TOKEN",
|
|
417
|
+
label: "GitHub token (read:user, repo:read)",
|
|
418
|
+
required: true,
|
|
419
|
+
scopes: ["read:user", "public_repo"],
|
|
420
|
+
hint: "A fine-grained token with read-only repository access is sufficient."
|
|
421
|
+
}
|
|
422
|
+
],
|
|
423
|
+
async run(ctx) {
|
|
424
|
+
const key = ctx.credential(this.credentials[0]);
|
|
425
|
+
if (!key) {
|
|
426
|
+
return {
|
|
427
|
+
providerId: this.providerId,
|
|
428
|
+
ok: false,
|
|
429
|
+
summary: "no credential present",
|
|
430
|
+
resources: [],
|
|
431
|
+
evidence: []
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
const headers = {
|
|
435
|
+
Authorization: `Bearer ${key}`,
|
|
436
|
+
Accept: "application/vnd.github+json",
|
|
437
|
+
"User-Agent": "agenttwin-discovery"
|
|
438
|
+
};
|
|
439
|
+
try {
|
|
440
|
+
const userResponse = await ctx.fetch("https://api.github.com/user", {
|
|
441
|
+
method: "GET",
|
|
442
|
+
headers,
|
|
443
|
+
signal: ctx.signal
|
|
444
|
+
});
|
|
445
|
+
if (!userResponse.ok) {
|
|
446
|
+
throw new Error(`GitHub /user -> HTTP ${userResponse.status}`);
|
|
447
|
+
}
|
|
448
|
+
const user = await userResponse.json();
|
|
449
|
+
const reposResponse = await ctx.fetch(
|
|
450
|
+
"https://api.github.com/user/repos?per_page=100&sort=updated",
|
|
451
|
+
{ method: "GET", headers, signal: ctx.signal }
|
|
452
|
+
);
|
|
453
|
+
const repos = reposResponse.ok ? await reposResponse.json() : [];
|
|
454
|
+
return {
|
|
455
|
+
providerId: this.providerId,
|
|
456
|
+
ok: true,
|
|
457
|
+
summary: `${repos.length} repositories readable for @${user.login ?? "unknown"}`,
|
|
458
|
+
resources: [
|
|
459
|
+
{
|
|
460
|
+
kind: "repositories",
|
|
461
|
+
count: repos.length,
|
|
462
|
+
sample: repos.slice(0, 5).map((r) => ({
|
|
463
|
+
name: r.name,
|
|
464
|
+
private: r.private,
|
|
465
|
+
archived: r.archived
|
|
466
|
+
}))
|
|
467
|
+
}
|
|
468
|
+
],
|
|
469
|
+
evidence: [
|
|
470
|
+
{
|
|
471
|
+
ref: "probe:github",
|
|
472
|
+
pass: "probe",
|
|
473
|
+
signal: "probe",
|
|
474
|
+
detail: "account reachable via read scopes"
|
|
475
|
+
}
|
|
476
|
+
]
|
|
477
|
+
};
|
|
478
|
+
} catch (error) {
|
|
479
|
+
return {
|
|
480
|
+
providerId: this.providerId,
|
|
481
|
+
ok: false,
|
|
482
|
+
error: { message: error instanceof Error ? error.message : String(error) },
|
|
483
|
+
summary: "probe failed",
|
|
484
|
+
resources: [],
|
|
485
|
+
evidence: []
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// src/discovery/providers/catalog.ts
|
|
494
|
+
function planned(unknown) {
|
|
495
|
+
return { supported: [], verified: [], unknown, implemented: false };
|
|
496
|
+
}
|
|
497
|
+
var NO_TWIN, PROVIDERS;
|
|
498
|
+
var init_catalog = __esm({
|
|
499
|
+
"src/discovery/providers/catalog.ts"() {
|
|
500
|
+
"use strict";
|
|
501
|
+
init_stripe();
|
|
502
|
+
init_github();
|
|
503
|
+
NO_TWIN = ["all behavior (no twin implemented)"];
|
|
504
|
+
PROVIDERS = [
|
|
505
|
+
{
|
|
506
|
+
id: "stripe",
|
|
507
|
+
title: "Stripe",
|
|
508
|
+
category: "payments",
|
|
509
|
+
twin: planned(["refunds", "payouts", "disputes", "webhook ordering"]),
|
|
510
|
+
probe: stripeProbe,
|
|
511
|
+
signatures: {
|
|
512
|
+
packages: {
|
|
513
|
+
node: ["stripe"],
|
|
514
|
+
python: ["stripe"],
|
|
515
|
+
go: ["stripe-go"],
|
|
516
|
+
java: ["stripe-java"],
|
|
517
|
+
ruby: ["stripe"],
|
|
518
|
+
php: ["stripe/stripe-php"],
|
|
519
|
+
dotnet: ["Stripe.net"]
|
|
520
|
+
},
|
|
521
|
+
imports: ["new Stripe\\(", `from ["']stripe["']`, `require\\(['"]stripe['"]\\)`, "stripe-go"],
|
|
522
|
+
hosts: ["api.stripe.com"],
|
|
523
|
+
envPrefixes: ["STRIPE_"],
|
|
524
|
+
webhookPaths: ["/webhooks/stripe", "/stripe/webhook", "/webhook/stripe"]
|
|
525
|
+
},
|
|
526
|
+
docs: "https://stripe.com/docs/api"
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
id: "slack",
|
|
530
|
+
title: "Slack",
|
|
531
|
+
category: "comms",
|
|
532
|
+
twin: planned(NO_TWIN),
|
|
533
|
+
signatures: {
|
|
534
|
+
packages: {
|
|
535
|
+
node: ["@slack/web-api", "@slack/bolt", "@slack/events-api", "@slack/socket-mode"],
|
|
536
|
+
python: ["slack_sdk", "slack-bolt"],
|
|
537
|
+
go: ["slack-go"],
|
|
538
|
+
java: ["slack-java-sdk"],
|
|
539
|
+
ruby: ["slack-ruby-client"]
|
|
540
|
+
},
|
|
541
|
+
imports: ["@slack/web-api", "WebClient\\(", "slack_sdk"],
|
|
542
|
+
hosts: ["slack.com", "api.slack.com", "hooks.slack.com"],
|
|
543
|
+
envPrefixes: ["SLACK_"]
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
id: "github",
|
|
548
|
+
title: "GitHub",
|
|
549
|
+
category: "vcs",
|
|
550
|
+
twin: planned(NO_TWIN),
|
|
551
|
+
probe: githubProbe,
|
|
552
|
+
signatures: {
|
|
553
|
+
packages: {
|
|
554
|
+
node: ["@octokit/rest", "@octokit/core", "@octokit/auth", "octokit"],
|
|
555
|
+
python: ["PyGithub", "github"],
|
|
556
|
+
go: ["google/go-github"],
|
|
557
|
+
java: ["github-api"],
|
|
558
|
+
ruby: ["octokit"]
|
|
559
|
+
},
|
|
560
|
+
imports: ["Octokit", "@octokit/", "PyGithub"],
|
|
561
|
+
hosts: ["api.github.com"],
|
|
562
|
+
envPrefixes: ["GITHUB_", "GH_"],
|
|
563
|
+
webhookPaths: ["/webhooks/github", "/github/webhook"]
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
id: "aws",
|
|
568
|
+
title: "AWS",
|
|
569
|
+
category: "cloud",
|
|
570
|
+
twin: planned(["service coverage (LocalStack passthrough planned)"]),
|
|
571
|
+
envWeak: true,
|
|
572
|
+
signatures: {
|
|
573
|
+
packages: {
|
|
574
|
+
node: ["@aws-sdk/", "aws-sdk"],
|
|
575
|
+
python: ["boto3", "botocore"],
|
|
576
|
+
go: ["aws-sdk-go", "aws-sdk-go-v2"],
|
|
577
|
+
java: ["software.amazon.awssdk", "aws-java-sdk"],
|
|
578
|
+
ruby: ["aws-sdk-"],
|
|
579
|
+
php: ["aws/aws-sdk-php"],
|
|
580
|
+
dotnet: ["AWSSDK."]
|
|
581
|
+
},
|
|
582
|
+
imports: ["@aws-sdk/", "boto3", "aws-sdk-go"],
|
|
583
|
+
hosts: ["amazonaws.com"],
|
|
584
|
+
envPrefixes: ["AWS_"]
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
id: "google",
|
|
589
|
+
title: "Google (Gmail / Calendar / Workspace)",
|
|
590
|
+
category: "comms",
|
|
591
|
+
twin: planned(NO_TWIN),
|
|
592
|
+
envWeak: true,
|
|
593
|
+
signatures: {
|
|
594
|
+
packages: {
|
|
595
|
+
node: ["googleapis", "google-auth-library"],
|
|
596
|
+
python: ["google-api-python-client", "google-cloud", "google-auth"],
|
|
597
|
+
go: ["google.golang.org/api"],
|
|
598
|
+
java: ["google-api-client"],
|
|
599
|
+
ruby: ["google-apis-"]
|
|
600
|
+
},
|
|
601
|
+
imports: ["googleapis", "google-api-python-client", "google.golang.org/api"],
|
|
602
|
+
hosts: ["googleapis.com", "gmail.googleapis.com", "www.googleapis.com"],
|
|
603
|
+
envPrefixes: ["GOOGLE_", "GCP_", "GMAIL_"]
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
id: "openai",
|
|
608
|
+
title: "OpenAI",
|
|
609
|
+
category: "ai",
|
|
610
|
+
twin: planned(NO_TWIN),
|
|
611
|
+
signatures: {
|
|
612
|
+
packages: {
|
|
613
|
+
node: ["openai"],
|
|
614
|
+
python: ["openai"],
|
|
615
|
+
go: ["go-openai"],
|
|
616
|
+
ruby: ["ruby-openai"]
|
|
617
|
+
},
|
|
618
|
+
imports: ["OpenAI\\(", `from ["']openai["']`, "go-openai"],
|
|
619
|
+
hosts: ["api.openai.com"],
|
|
620
|
+
envPrefixes: ["OPENAI_"]
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
id: "anthropic",
|
|
625
|
+
title: "Anthropic",
|
|
626
|
+
category: "ai",
|
|
627
|
+
twin: planned(NO_TWIN),
|
|
628
|
+
signatures: {
|
|
629
|
+
packages: {
|
|
630
|
+
node: ["@anthropic-ai/sdk"],
|
|
631
|
+
python: ["anthropic"],
|
|
632
|
+
go: ["anthropic-sdk-go"]
|
|
633
|
+
},
|
|
634
|
+
imports: ["@anthropic-ai/sdk", "Anthropic\\("],
|
|
635
|
+
hosts: ["api.anthropic.com"],
|
|
636
|
+
envPrefixes: ["ANTHROPIC_"]
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
id: "shopify",
|
|
641
|
+
title: "Shopify",
|
|
642
|
+
category: "commerce",
|
|
643
|
+
twin: planned(NO_TWIN),
|
|
644
|
+
signatures: {
|
|
645
|
+
packages: {
|
|
646
|
+
node: ["@shopify/", "shopify-api"],
|
|
647
|
+
python: ["ShopifyAPI"],
|
|
648
|
+
ruby: ["shopify_api"]
|
|
649
|
+
},
|
|
650
|
+
imports: ["@shopify/", "ShopifyAPI"],
|
|
651
|
+
hosts: ["myshopify.com", "shopify.com"],
|
|
652
|
+
envPrefixes: ["SHOPIFY_"]
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
id: "twilio",
|
|
657
|
+
title: "Twilio",
|
|
658
|
+
category: "comms",
|
|
659
|
+
twin: planned(NO_TWIN),
|
|
660
|
+
signatures: {
|
|
661
|
+
packages: {
|
|
662
|
+
node: ["twilio"],
|
|
663
|
+
python: ["twilio"],
|
|
664
|
+
java: ["twilio"],
|
|
665
|
+
ruby: ["twilio-ruby"],
|
|
666
|
+
php: ["twilio/sdk"]
|
|
667
|
+
},
|
|
668
|
+
imports: ["twilio"],
|
|
669
|
+
hosts: ["api.twilio.com"],
|
|
670
|
+
envPrefixes: ["TWILIO_"]
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
id: "sendgrid",
|
|
675
|
+
title: "SendGrid",
|
|
676
|
+
category: "comms",
|
|
677
|
+
twin: planned(NO_TWIN),
|
|
678
|
+
signatures: {
|
|
679
|
+
packages: {
|
|
680
|
+
node: ["@sendgrid/mail", "@sendgrid/client"],
|
|
681
|
+
python: ["sendgrid"],
|
|
682
|
+
ruby: ["sendgrid-ruby"]
|
|
683
|
+
},
|
|
684
|
+
imports: ["@sendgrid/", "sendgrid"],
|
|
685
|
+
hosts: ["api.sendgrid.com"],
|
|
686
|
+
envPrefixes: ["SENDGRID_"]
|
|
687
|
+
}
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
id: "hubspot",
|
|
691
|
+
title: "HubSpot",
|
|
692
|
+
category: "crm",
|
|
693
|
+
twin: planned(NO_TWIN),
|
|
694
|
+
signatures: {
|
|
695
|
+
packages: {
|
|
696
|
+
node: ["@hubspot/api-client"],
|
|
697
|
+
python: ["hubspot-api-client"],
|
|
698
|
+
ruby: ["hubspot-api-client"]
|
|
699
|
+
},
|
|
700
|
+
imports: ["@hubspot/api-client", "hubspot-api-client"],
|
|
701
|
+
hosts: ["api.hubapi.com"],
|
|
702
|
+
envPrefixes: ["HUBSPOT_"]
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
id: "salesforce",
|
|
707
|
+
title: "Salesforce",
|
|
708
|
+
category: "crm",
|
|
709
|
+
twin: planned(NO_TWIN),
|
|
710
|
+
signatures: {
|
|
711
|
+
packages: {
|
|
712
|
+
node: ["jsforce", "@salesforce/"],
|
|
713
|
+
python: ["simple-salesforce"],
|
|
714
|
+
java: ["force-wsc"]
|
|
715
|
+
},
|
|
716
|
+
imports: ["jsforce", "simple-salesforce"],
|
|
717
|
+
hosts: ["salesforce.com", "force.com"],
|
|
718
|
+
envPrefixes: ["SALESFORCE_", "SF_"]
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
id: "zendesk",
|
|
723
|
+
title: "Zendesk",
|
|
724
|
+
category: "support",
|
|
725
|
+
twin: planned(NO_TWIN),
|
|
726
|
+
signatures: {
|
|
727
|
+
packages: {
|
|
728
|
+
node: ["node-zendesk"],
|
|
729
|
+
python: ["zendesk", "zenpy"],
|
|
730
|
+
ruby: ["zendesk_api"]
|
|
731
|
+
},
|
|
732
|
+
imports: ["node-zendesk", "zenpy"],
|
|
733
|
+
hosts: ["zendesk.com"],
|
|
734
|
+
envPrefixes: ["ZENDESK_"]
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
id: "linear",
|
|
739
|
+
title: "Linear",
|
|
740
|
+
category: "pm",
|
|
741
|
+
twin: planned(NO_TWIN),
|
|
742
|
+
signatures: {
|
|
743
|
+
packages: { node: ["@linear/sdk"], python: ["linear"] },
|
|
744
|
+
imports: ["@linear/sdk"],
|
|
745
|
+
hosts: ["api.linear.app"],
|
|
746
|
+
envPrefixes: ["LINEAR_"]
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
id: "notion",
|
|
751
|
+
title: "Notion",
|
|
752
|
+
category: "docs",
|
|
753
|
+
twin: planned(NO_TWIN),
|
|
754
|
+
signatures: {
|
|
755
|
+
packages: { node: ["@notionhq/client"], python: ["notion-client"] },
|
|
756
|
+
imports: ["@notionhq/client", "notion-client"],
|
|
757
|
+
hosts: ["api.notion.com"],
|
|
758
|
+
envPrefixes: ["NOTION_"]
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
id: "resend",
|
|
763
|
+
title: "Resend",
|
|
764
|
+
category: "comms",
|
|
765
|
+
twin: planned(NO_TWIN),
|
|
766
|
+
signatures: {
|
|
767
|
+
packages: { node: ["resend"] },
|
|
768
|
+
imports: [`from ["']resend["']`],
|
|
769
|
+
hosts: ["api.resend.com"],
|
|
770
|
+
envPrefixes: ["RESEND_"]
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
];
|
|
774
|
+
for (const [id, title, category, node, python, hosts] of [
|
|
775
|
+
["composio", "Composio", "integration", ["@composio/core", "@composio/client"], ["composio", "composio-core"], ["backend.composio.dev"]],
|
|
776
|
+
["kernel", "Kernel", "browser", ["@onkernel/sdk"], ["kernel"], ["api.onkernel.com"]],
|
|
777
|
+
["e2b", "E2B", "runtime", ["e2b", "@e2b/code-interpreter"], ["e2b", "e2b-code-interpreter"], ["api.e2b.dev"]],
|
|
778
|
+
["mem0", "Mem0", "ai", ["mem0ai"], ["mem0ai"], ["api.mem0.ai"]],
|
|
779
|
+
["ollama", "Ollama", "ai", ["ollama"], ["ollama"], []],
|
|
780
|
+
["onepassword", "1Password", "security", ["@1password/sdk"], ["onepassword-sdk"], []]
|
|
781
|
+
]) PROVIDERS.push({ id, title, category, twin: planned(NO_TWIN), signatures: { packages: { node: [...node], python: [...python] }, imports: [...node, ...python], hosts: [...hosts] } });
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
// src/inventory/project-products.ts
|
|
786
|
+
async function projectProducts(cwd, data, base) {
|
|
787
|
+
const current = new Set(base.tools.map((t) => t.id));
|
|
788
|
+
const known = new Set(PROVIDERS.map((p) => p.title));
|
|
789
|
+
const tools = data.tools.filter((t) => current.has(t.id) || !known.has(t.title) && t.category !== "access" && t.category !== "tooling");
|
|
790
|
+
const ids = new Set(tools.map((t) => t.id));
|
|
791
|
+
const workflows = data.workflows.map((w) => ({ ...w, tools: w.tools.filter((id) => ids.has(id)), steps: w.steps.filter((s) => ids.has(s.tool)) }));
|
|
792
|
+
const google = tools.find((t) => /Google \(Gmail/.test(t.title));
|
|
793
|
+
if (!google) return discoveryTransfer({ ...data, tools, workflows });
|
|
794
|
+
const products = [
|
|
795
|
+
{ key: "gmail", title: "Gmail", pattern: /(?:gmail\.googleapis\.com|\/gmail\/v\d|\bgoogle\.gmail\s*\()/i },
|
|
796
|
+
{ key: "calendar", title: "Google Calendar", pattern: /(?:\/calendar\/v\d|\bgoogle\.calendar\s*\()/i },
|
|
797
|
+
{ key: "drive", title: "Google Drive", pattern: /(?:\/drive\/v\d|\bgoogle\.drive\s*\()/i },
|
|
798
|
+
{ key: "docs", title: "Google Docs", pattern: /(?:docs\.googleapis\.com|\/documents\/|\bgoogle\.docs\s*\()/i },
|
|
799
|
+
{ key: "sheets", title: "Google Sheets", pattern: /(?:sheets\.googleapis\.com|\/spreadsheets\/|\bgoogle\.sheets\s*\()/i }
|
|
800
|
+
];
|
|
801
|
+
const files = await createFileIndex(cwd);
|
|
802
|
+
const paths = (await files.walk()).filter((p) => /\.(?:ts|tsx|js|mjs|py)$/.test(p) && !/(?:^|\/)(?:artifacts|tests?|testbed|docs?|fixtures?|examples?|public|assets)(?:\/|$)|\.(?:test|spec)\.|fixtures?\./i.test(p)).slice(0, 2500);
|
|
803
|
+
const found = /* @__PURE__ */ new Set();
|
|
804
|
+
for (const path of paths) {
|
|
805
|
+
const source = await files.read(path, 2e5);
|
|
806
|
+
if (!source) continue;
|
|
807
|
+
for (const p of products) if (p.pattern.test(source)) found.add(p.key);
|
|
808
|
+
}
|
|
809
|
+
const mapped = workflows.map((w) => {
|
|
810
|
+
if (!w.tools.includes(google.id)) return w;
|
|
811
|
+
const [path, fn] = (w.entrypoint ?? "").split("#");
|
|
812
|
+
return { w, path, fn };
|
|
813
|
+
});
|
|
814
|
+
const finalWorkflows = [];
|
|
815
|
+
let unresolved = false;
|
|
816
|
+
for (const value of mapped) {
|
|
817
|
+
if (!("w" in value)) {
|
|
818
|
+
finalWorkflows.push(value);
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
const { w, path, fn } = value;
|
|
822
|
+
const source = path ? await files.read(path, 2e5) : void 0;
|
|
823
|
+
const entry = source && fn ? exportedEntrypoints(source).find((e) => e.name === fn) : void 0;
|
|
824
|
+
const body2 = source && entry ? source.split("\n").slice(entry.startLine - 1, entry.endLine).join("\n") : fn ? "" : source ?? "";
|
|
825
|
+
const matches = products.filter((p) => found.has(p.key) && p.pattern.test(body2)).map((p) => google.id + ":" + p.key);
|
|
826
|
+
if (!matches.length) {
|
|
827
|
+
unresolved = true;
|
|
828
|
+
finalWorkflows.push(w);
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
finalWorkflows.push({ ...w, tools: [...new Set(w.tools.flatMap((id) => id === google.id ? matches : [id]))], steps: w.steps.flatMap((s) => s.tool === google.id ? matches.map((tool) => ({ ...s, tool })) : [s]) });
|
|
832
|
+
}
|
|
833
|
+
const productTools = products.filter((p) => found.has(p.key)).map((p) => ({ ...google, id: google.id + ":" + p.key, title: p.title }));
|
|
834
|
+
return discoveryTransfer({ ...data, tools: [...tools.filter((t) => t.id !== google.id || unresolved || !productTools.length).map((t) => t.id === google.id ? { ...t, title: "Google API (unspecified)" } : t), ...productTools], workflows: finalWorkflows });
|
|
835
|
+
}
|
|
836
|
+
var init_project_products = __esm({
|
|
837
|
+
"src/inventory/project-products.ts"() {
|
|
838
|
+
"use strict";
|
|
839
|
+
init_files();
|
|
840
|
+
init_entrypoints();
|
|
841
|
+
init_catalog();
|
|
842
|
+
init_dashboard_transfer();
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
|
|
94
846
|
// src/inspect/sanitize.ts
|
|
95
847
|
function stripUrlCredentials(value) {
|
|
96
848
|
return value.replace(URL_CREDENTIALS, "$1");
|
|
97
849
|
}
|
|
98
850
|
function sanitizeOutput(value) {
|
|
99
|
-
return stripUrlCredentials(value).replace(
|
|
851
|
+
return stripUrlCredentials(value).replace(TOKENISH2, (m) => `${m.slice(0, 4)}***`).replace(EMAIL, "$1***$2");
|
|
100
852
|
}
|
|
101
|
-
var URL_CREDENTIALS,
|
|
853
|
+
var URL_CREDENTIALS, TOKENISH2, EMAIL;
|
|
102
854
|
var init_sanitize = __esm({
|
|
103
855
|
"src/inspect/sanitize.ts"() {
|
|
104
856
|
"use strict";
|
|
105
857
|
URL_CREDENTIALS = /(https?:\/\/)([^/@\s:]+)(:[^/@\s]+)?@/gi;
|
|
106
|
-
|
|
858
|
+
TOKENISH2 = /\b(sk|rk|pk|ghp|gho|ghs|glpat|xox[baprs]|AKIA|AIza)[A-Za-z0-9_-]{6,}\b/g;
|
|
107
859
|
EMAIL = /([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*(@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g;
|
|
108
860
|
}
|
|
109
861
|
});
|
|
110
862
|
|
|
111
863
|
// src/commands/dashboard.ts
|
|
112
|
-
import { cp, mkdir, readFile as
|
|
113
|
-
import { basename, join as
|
|
864
|
+
import { cp, mkdir, readFile as readFile5, writeFile } from "fs/promises";
|
|
865
|
+
import { basename, join as join5 } from "path";
|
|
114
866
|
import { fileURLToPath } from "url";
|
|
115
867
|
function dashboardInventory(cwd, inventory) {
|
|
116
868
|
return JSON.parse(sanitizeOutput(JSON.stringify({
|
|
117
869
|
schema: "company-inventory/v1",
|
|
118
870
|
generatedAt: inventory.generatedAt,
|
|
119
871
|
name: basename(cwd),
|
|
120
|
-
tools: inventory.tools.map((t) => ({
|
|
872
|
+
tools: inventory.tools.filter((t) => !t.facts?.length || !t.facts.every((f) => f.evidence.every((e) => /^(?:credential:|command:)/.test(e.ref)))).map((t) => ({
|
|
121
873
|
id: t.id,
|
|
122
874
|
title: t.title,
|
|
123
875
|
kind: t.kind,
|
|
@@ -146,8 +898,8 @@ __export(session_exports, {
|
|
|
146
898
|
writeSessionTask: () => writeSessionTask
|
|
147
899
|
});
|
|
148
900
|
import { spawn as spawn2, spawnSync } from "child_process";
|
|
149
|
-
import { writeFile as writeFile2, readFile as
|
|
150
|
-
import { join as
|
|
901
|
+
import { writeFile as writeFile2, readFile as readFile6 } from "fs/promises";
|
|
902
|
+
import { join as join6 } from "path";
|
|
151
903
|
function sessionTask() {
|
|
152
904
|
return `# Prepare this repository for AgentTwin
|
|
153
905
|
|
|
@@ -172,7 +924,7 @@ function installedOpenCode() {
|
|
|
172
924
|
return spawnSync("opencode", ["--version"], { stdio: "ignore", timeout: 5e3 }).status === 0;
|
|
173
925
|
}
|
|
174
926
|
async function writeSessionTask(cwd) {
|
|
175
|
-
const path =
|
|
927
|
+
const path = join6(cwd, ".agenttwin/AGENT-TASK.md");
|
|
176
928
|
await writeFile2(path, sessionTask(), "utf8");
|
|
177
929
|
return path;
|
|
178
930
|
}
|
|
@@ -189,9 +941,9 @@ async function loadDashboardDiscovery(cwd, inventory) {
|
|
|
189
941
|
const base = discoveryTransfer(dashboardInventory(cwd, inventory));
|
|
190
942
|
let raw;
|
|
191
943
|
try {
|
|
192
|
-
raw = await
|
|
944
|
+
raw = await readFile6(join6(cwd, ".agenttwin/agent-discovery.json"), "utf8");
|
|
193
945
|
} catch (error) {
|
|
194
|
-
if (error.code === "ENOENT") return base;
|
|
946
|
+
if (error.code === "ENOENT") return projectProducts(cwd, base, base);
|
|
195
947
|
throw error;
|
|
196
948
|
}
|
|
197
949
|
if (Buffer.byteLength(raw) > 2 * 1024 * 1024) throw new Error("Agent discovery exceeds 2 MiB");
|
|
@@ -203,11 +955,12 @@ async function loadDashboardDiscovery(cwd, inventory) {
|
|
|
203
955
|
const result = discoveryTransfer({ ...base, tools: [...tools.values()], workflows: [...workflows.values()] });
|
|
204
956
|
const ids = new Set(result.tools.map((t) => t.id));
|
|
205
957
|
if (result.workflows.some((w) => w.tools.some((id) => !ids.has(id)) || w.steps.some((s) => !ids.has(s.tool)))) throw new Error("Agent discovery references unknown tools. Correct .agenttwin/agent-discovery.json before syncing.");
|
|
206
|
-
return result;
|
|
958
|
+
return projectProducts(cwd, result, base);
|
|
207
959
|
}
|
|
208
960
|
var init_session = __esm({
|
|
209
961
|
"src/commands/session.ts"() {
|
|
210
962
|
"use strict";
|
|
963
|
+
init_project_products();
|
|
211
964
|
init_dashboard_transfer();
|
|
212
965
|
init_dashboard();
|
|
213
966
|
}
|
|
@@ -217,8 +970,8 @@ var init_session = __esm({
|
|
|
217
970
|
init_dashboard_transfer();
|
|
218
971
|
import { createServer as createServer2 } from "http";
|
|
219
972
|
import { createHash as createHash2, randomBytes as randomBytes3, randomUUID as randomUUID2, timingSafeEqual } from "crypto";
|
|
220
|
-
import { mkdir as mkdir3, readFile as
|
|
221
|
-
import { resolve as resolve2, join as
|
|
973
|
+
import { mkdir as mkdir3, readFile as readFile8, writeFile as writeFile4, rename, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
974
|
+
import { resolve as resolve2, join as join8, dirname as dirname2, extname } from "path";
|
|
222
975
|
import { tmpdir as tmpdir2 } from "os";
|
|
223
976
|
|
|
224
977
|
// src/runtime/chat.ts
|
|
@@ -329,9 +1082,9 @@ function portableLocalReceipt(value) {
|
|
|
329
1082
|
}
|
|
330
1083
|
|
|
331
1084
|
// src/commands/run.ts
|
|
332
|
-
import { mkdtemp, mkdir as mkdir2, readFile as
|
|
1085
|
+
import { mkdtemp, mkdir as mkdir2, readFile as readFile7, writeFile as writeFile3, realpath, lstat, rm, copyFile } from "fs/promises";
|
|
333
1086
|
import { tmpdir } from "os";
|
|
334
|
-
import { basename as basename2, dirname, isAbsolute, join as
|
|
1087
|
+
import { basename as basename2, dirname, isAbsolute, join as join7, relative as relative2, resolve, sep as sep2 } from "path";
|
|
335
1088
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
336
1089
|
import { createHash, randomUUID } from "crypto";
|
|
337
1090
|
|
|
@@ -741,12 +1494,12 @@ async function stageApplication(root, files, destination) {
|
|
|
741
1494
|
for (const file of files) {
|
|
742
1495
|
if (isAbsolute(file) || file.includes("\\") || file.split("/").some((p) => p === ".." || /^(\.env.*|\.git|\.ssh|\.aws|\.azure|\.config|\.npmrc|\.netrc|credentials.*)$/i.test(p)) || /\.(pem|key|p12|pfx)$/i.test(file)) throw new CliError(`Unsafe application file: ${file}`, { code: EXIT.USAGE });
|
|
743
1496
|
const source = await realpath(resolve(canonicalRoot, file));
|
|
744
|
-
const rel =
|
|
745
|
-
if (rel.startsWith(`..${
|
|
746
|
-
const contents = await
|
|
1497
|
+
const rel = relative2(canonicalRoot, source);
|
|
1498
|
+
if (rel.startsWith(`..${sep2}`) || rel === ".." || isAbsolute(rel) || source !== resolve(canonicalRoot, file) || !(await lstat(source)).isFile()) throw new CliError(`Files must be regular files within the project, without symlinks: ${file}`, { code: EXIT.USAGE });
|
|
1499
|
+
const contents = await readFile7(source);
|
|
747
1500
|
bytes += contents.length;
|
|
748
1501
|
if (bytes > 20 * 1024 * 1024) throw new CliError("Application files exceed 20 MiB; supply a smaller built bundle", { code: EXIT.USAGE });
|
|
749
|
-
const target =
|
|
1502
|
+
const target = join7(destination, file);
|
|
750
1503
|
await mkdir2(dirname(target), { recursive: true });
|
|
751
1504
|
await writeFile3(target, contents, { mode: 420 });
|
|
752
1505
|
digests[file] = createHash("sha256").update(contents).digest("hex");
|
|
@@ -757,14 +1510,14 @@ async function runWorld(cwd, opts) {
|
|
|
757
1510
|
const file = resolve(cwd, opts.file ?? "agenttwin.world.json");
|
|
758
1511
|
let spec;
|
|
759
1512
|
try {
|
|
760
|
-
spec = parseWorld(JSON.parse(await
|
|
1513
|
+
spec = parseWorld(JSON.parse(await readFile7(file, "utf8")));
|
|
761
1514
|
} catch (error) {
|
|
762
1515
|
throw new CliError(String(error), { code: EXIT.USAGE, action: "Create agenttwin.world.json; run agenttwin example --out ./agenttwin-example for a working example." });
|
|
763
1516
|
}
|
|
764
1517
|
if (spec.workflowId) {
|
|
765
1518
|
let inventory;
|
|
766
1519
|
try {
|
|
767
|
-
inventory = JSON.parse(await
|
|
1520
|
+
inventory = JSON.parse(await readFile7(join7(cwd, ".agenttwin/inventory.json"), "utf8"));
|
|
768
1521
|
} catch {
|
|
769
1522
|
throw new CliError("Linked workflow needs discovery inventory; run init first", { code: EXIT.BLOCKED });
|
|
770
1523
|
}
|
|
@@ -777,24 +1530,24 @@ async function runWorld(cwd, opts) {
|
|
|
777
1530
|
const runtime = opts.runtime ?? "local";
|
|
778
1531
|
if (!["local", "e2b", "hosted"].includes(runtime)) throw new CliError("runtime must be local, hosted or e2b", { code: EXIT.USAGE });
|
|
779
1532
|
if (runtime === "e2b" && !process.env.E2B_API_KEY) throw new CliError("E2B_API_KEY is not configured", { code: EXIT.BLOCKED, action: "Configure your E2B key in the shell; it will not be uploaded." });
|
|
780
|
-
const stage = await mkdtemp(
|
|
1533
|
+
const stage = await mkdtemp(join7(tmpdir(), "agenttwin-world-"));
|
|
781
1534
|
try {
|
|
782
|
-
const sourceDigests = await stageApplication(dirname(file), spec.files,
|
|
783
|
-
await writeFile3(
|
|
1535
|
+
const sourceDigests = await stageApplication(dirname(file), spec.files, join7(stage, "app"));
|
|
1536
|
+
await writeFile3(join7(stage, "world.json"), JSON.stringify(spec), { mode: 420 });
|
|
784
1537
|
const git = await captureGit(cwd);
|
|
785
1538
|
let project;
|
|
786
1539
|
try {
|
|
787
|
-
project = JSON.parse(await
|
|
1540
|
+
project = JSON.parse(await readFile7(join7(cwd, ".agenttwin/inventory.json"), "utf8")).name ?? basename2(cwd);
|
|
788
1541
|
} catch {
|
|
789
1542
|
}
|
|
790
1543
|
if (runtime === "local") {
|
|
791
1544
|
opts.log("Testing through the CLI with in-process service twins. No cloud VM or containers.");
|
|
792
|
-
const report2 = await executeLocal(
|
|
1545
|
+
const report2 = await executeLocal(join7(stage, "app"), spec, opts.signal);
|
|
793
1546
|
const runId2 = randomUUID();
|
|
794
1547
|
const receipt2 = { ...report2, runId: runId2, runtime, project, git, createdAt: (/* @__PURE__ */ new Date()).toISOString(), sourceDigests, specDigest: createHash("sha256").update(JSON.stringify(spec)).digest("hex"), provenance: "CLI-reported execution" };
|
|
795
1548
|
const out2 = resolve(cwd, opts.out ?? `.agenttwin/runs/${runId2}`);
|
|
796
1549
|
await mkdir2(out2, { recursive: true });
|
|
797
|
-
const evidencePath =
|
|
1550
|
+
const evidencePath = join7(out2, "run.json"), reportPath = join7(out2, "report.md");
|
|
798
1551
|
await writeFile3(evidencePath, JSON.stringify(receipt2, null, 2) + "\n", { mode: 384 });
|
|
799
1552
|
await writeFile3(reportPath, `# ${spec.name}: ${receipt2.status}
|
|
800
1553
|
|
|
@@ -804,8 +1557,8 @@ Git: ${git?.head ?? "unavailable"}
|
|
|
804
1557
|
` + receipt2.assertions.map((a) => `- ${a.passed ? "PASS" : "FAIL"} ${a.service}${a.path}: expected ${a.count}, observed ${a.actual}`).join("\n") + "\n\nSee run.json for activities, fault and initial/final state.\n");
|
|
805
1558
|
return { ...receipt2, reportPath, evidencePath };
|
|
806
1559
|
}
|
|
807
|
-
const worker =
|
|
808
|
-
await copyFile(worker,
|
|
1560
|
+
const worker = join7(dirname(fileURLToPath2(import.meta.url)), "worker.js");
|
|
1561
|
+
await copyFile(worker, join7(stage, "worker.js"));
|
|
809
1562
|
if (runtime === "hosted") {
|
|
810
1563
|
const receipt2 = await executeRemote(stage, spec, opts.log, { git, project });
|
|
811
1564
|
if (receipt2.schema !== "agenttwin/run-v1" || typeof receipt2.runId !== "string" || !/^[a-f0-9-]{36}$/.test(receipt2.runId) || !["passed", "failed", "error", "incomplete"].includes(String(receipt2.status))) throw new CliError("Invalid hosted receipt");
|
|
@@ -814,8 +1567,8 @@ Git: ${git?.head ?? "unavailable"}
|
|
|
814
1567
|
if (receipt2.specDigest !== expectedSpec || !reportedSources || Object.keys(reportedSources).length !== Object.keys(sourceDigests).length || Object.entries(sourceDigests).some(([path, digest]) => reportedSources[path] !== digest)) throw new CliError("Hosted receipt does not match the submitted source/specification");
|
|
815
1568
|
const out2 = resolve(cwd, opts.out ?? `.agenttwin/runs/${receipt2.runId}`);
|
|
816
1569
|
await mkdir2(out2, { recursive: true });
|
|
817
|
-
const evidencePath =
|
|
818
|
-
const reportPath =
|
|
1570
|
+
const evidencePath = join7(out2, "run.json");
|
|
1571
|
+
const reportPath = join7(out2, "report.md");
|
|
819
1572
|
await writeFile3(evidencePath, JSON.stringify(receipt2, null, 2) + "\n", { mode: 384 });
|
|
820
1573
|
await writeFile3(reportPath, `# ${spec.name}: ${receipt2.status}
|
|
821
1574
|
|
|
@@ -852,11 +1605,11 @@ See run.json for assertions, service state, application output and isolation met
|
|
|
852
1605
|
};
|
|
853
1606
|
const out = resolve(cwd, opts.out ?? `.agenttwin/runs/${runId}`);
|
|
854
1607
|
await mkdir2(out, { recursive: true });
|
|
855
|
-
await writeFile3(
|
|
856
|
-
await writeFile3(
|
|
1608
|
+
await writeFile3(join7(out, "run.json"), JSON.stringify(receipt, null, 2) + "\n", { mode: 384 });
|
|
1609
|
+
await writeFile3(join7(out, "world.json"), JSON.stringify(spec, null, 2) + "\n", { mode: 384 });
|
|
857
1610
|
const summary = [`# ${spec.name}: ${report.status}`, "", `Run: ${runId}`, `Image: ${image}`, "", "## Assertions", "", ...report.assertions.map((a) => `- ${a.passed ? "PASS" : "FAIL"} ${a.service}${a.path}: expected ${a.count}, observed ${a.actual}`), "", "## Infrastructure assertions", "", ...(report.infrastructureAssertions ?? []).map((a) => `- ${a.passed ? "PASS" : "FAIL"} ${a.service}: ${JSON.stringify(a.actual)}`), "", "## Coverage", "", report.coverage, "", "External networking disabled. Only explicitly listed source files were uploaded. New state and tokens are created for each run.", "", "See run.json for application output, faults, events, and initial/final state.", ""].join("\n");
|
|
858
|
-
await writeFile3(
|
|
859
|
-
return { ...receipt, reportPath:
|
|
1611
|
+
await writeFile3(join7(out, "report.md"), summary);
|
|
1612
|
+
return { ...receipt, reportPath: join7(out, "report.md"), evidencePath: join7(out, "run.json") };
|
|
860
1613
|
} finally {
|
|
861
1614
|
await rm(stage, { recursive: true, force: true });
|
|
862
1615
|
}
|
|
@@ -898,10 +1651,10 @@ async function startGateway(opts) {
|
|
|
898
1651
|
const chatControllers = /* @__PURE__ */ new Map();
|
|
899
1652
|
const directory = resolve2(opts.directory);
|
|
900
1653
|
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
901
|
-
const storePath =
|
|
1654
|
+
const storePath = join8(directory, "state.json");
|
|
902
1655
|
let store;
|
|
903
1656
|
try {
|
|
904
|
-
store = JSON.parse(await
|
|
1657
|
+
store = JSON.parse(await readFile8(storePath, "utf8"));
|
|
905
1658
|
if (store.schema !== "agenttwin/gateway-v1") throw new Error("Invalid store");
|
|
906
1659
|
} catch (error) {
|
|
907
1660
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -916,7 +1669,7 @@ async function startGateway(opts) {
|
|
|
916
1669
|
const save = () => {
|
|
917
1670
|
const snapshot = JSON.stringify(store);
|
|
918
1671
|
writes = writes.then(async () => {
|
|
919
|
-
const path =
|
|
1672
|
+
const path = join8(directory, `state-${randomUUID2()}.tmp`);
|
|
920
1673
|
await writeFile4(path, snapshot, { mode: 384 });
|
|
921
1674
|
await rename(path, storePath);
|
|
922
1675
|
}).catch((error) => {
|
|
@@ -951,16 +1704,16 @@ async function startGateway(opts) {
|
|
|
951
1704
|
if (!store.accounts.some((a) => a.id === item.job.accountId && a.active)) throw new Error("Account revoked before execution");
|
|
952
1705
|
item.job.status = "running";
|
|
953
1706
|
await save();
|
|
954
|
-
workspace = await mkdtemp2(
|
|
1707
|
+
workspace = await mkdtemp2(join8(tmpdir2(), "agenttwin-api-run-"));
|
|
955
1708
|
for (const [file, encoded] of Object.entries(item.files)) {
|
|
956
|
-
const destination =
|
|
1709
|
+
const destination = join8(workspace, uploadPath(file));
|
|
957
1710
|
await mkdir3(dirname2(destination), { recursive: true });
|
|
958
1711
|
await writeFile4(destination, Buffer.from(encoded, "base64"), { mode: 384 });
|
|
959
1712
|
}
|
|
960
|
-
await writeFile4(
|
|
1713
|
+
await writeFile4(join8(workspace, "agenttwin.world.json"), JSON.stringify(item.spec));
|
|
961
1714
|
if (item.spec.workflowId) {
|
|
962
|
-
await mkdir3(
|
|
963
|
-
await writeFile4(
|
|
1715
|
+
await mkdir3(join8(workspace, ".agenttwin"));
|
|
1716
|
+
await writeFile4(join8(workspace, ".agenttwin/inventory.json"), JSON.stringify({ workflows: [{ id: item.spec.workflowId }] }));
|
|
964
1717
|
}
|
|
965
1718
|
const receipt = await runWorld(workspace, { runtime: "e2b", signal: controllers.get(item.job.id)?.signal, log: () => {
|
|
966
1719
|
} });
|
|
@@ -998,14 +1751,14 @@ async function startGateway(opts) {
|
|
|
998
1751
|
res.end();
|
|
999
1752
|
return;
|
|
1000
1753
|
}
|
|
1001
|
-
const
|
|
1002
|
-
const root = resolve2(opts.dashboardDirectory), file = resolve2(root,
|
|
1003
|
-
if (!file.startsWith(root + "/") ||
|
|
1754
|
+
const relative3 = decodeURIComponent(path.slice("/dashboard/".length)) || "index.html";
|
|
1755
|
+
const root = resolve2(opts.dashboardDirectory), file = resolve2(root, relative3);
|
|
1756
|
+
if (!file.startsWith(root + "/") || relative3 !== "index.html" && !relative3.startsWith("assets/")) {
|
|
1004
1757
|
send(res, 404, { error: "not_found" });
|
|
1005
1758
|
return;
|
|
1006
1759
|
}
|
|
1007
1760
|
try {
|
|
1008
|
-
const bytes = await
|
|
1761
|
+
const bytes = await readFile8(file);
|
|
1009
1762
|
const types = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".ico": "image/x-icon", ".woff2": "font/woff2" };
|
|
1010
1763
|
res.writeHead(200, { "content-type": types[extname(file)] ?? "application/octet-stream", "cache-control": "no-store", "x-content-type-options": "nosniff", "referrer-policy": "no-referrer" });
|
|
1011
1764
|
res.end(bytes);
|