agenttwin 0.3.0 → 0.3.2
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 +802 -49
- package/dist/cli.js +860 -738
- package/dist/dashboard/assets/dashboard/agent-home.js +3 -3
- package/dist/dashboard/assets/dashboard/dashboard.css +2 -0
- package/dist/dashboard/assets/dashboard/dashboard.js +20 -7
- package/dist/dashboard/index.html +2 -2
- package/dist/gateway.js +802 -49
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -48,6 +48,704 @@ var init_git = __esm({
|
|
|
48
48
|
}
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
// src/lib/files.ts
|
|
52
|
+
import { readdir as readdir2, readFile as readFile3, stat } from "fs/promises";
|
|
53
|
+
import { join as join3, relative, sep } from "path";
|
|
54
|
+
function shouldIgnoreDir(name) {
|
|
55
|
+
return IGNORED_DIRS.has(name) || name === ".git";
|
|
56
|
+
}
|
|
57
|
+
async function walkDir(root, maxFiles, budgetMs) {
|
|
58
|
+
const out = [];
|
|
59
|
+
const limits = [];
|
|
60
|
+
const stack = [root];
|
|
61
|
+
const started = Date.now();
|
|
62
|
+
while (stack.length > 0 && out.length < maxFiles && Date.now() - started < budgetMs) {
|
|
63
|
+
const dir = stack.pop();
|
|
64
|
+
let entries;
|
|
65
|
+
try {
|
|
66
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
67
|
+
} catch {
|
|
68
|
+
limits.push(`Directory could not be read: ${relative(root, dir) || "."}`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
const full = join3(dir, entry.name);
|
|
74
|
+
if (entry.isDirectory()) {
|
|
75
|
+
if (!shouldIgnoreDir(entry.name)) stack.push(full);
|
|
76
|
+
} else if (entry.isFile()) {
|
|
77
|
+
out.push(relative(root, full).split(sep).join("/"));
|
|
78
|
+
if (out.length >= maxFiles) break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (out.length >= maxFiles) limits.push(`File scan limit reached (${maxFiles} files); additional files may be unscanned`);
|
|
83
|
+
if (stack.length && Date.now() - started >= budgetMs) limits.push(`File walk time budget reached (${budgetMs}ms); directories remain unscanned`);
|
|
84
|
+
return { files: out.sort(), limits };
|
|
85
|
+
}
|
|
86
|
+
async function createFileIndex(cwd) {
|
|
87
|
+
return new RepoFileIndex(cwd);
|
|
88
|
+
}
|
|
89
|
+
var IGNORED_DIRS, MAX_FILES, MAX_BYTES, WALK_BUDGET_MS, RepoFileIndex;
|
|
90
|
+
var init_files = __esm({
|
|
91
|
+
"src/lib/files.ts"() {
|
|
92
|
+
"use strict";
|
|
93
|
+
IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
94
|
+
".git",
|
|
95
|
+
".agenttwin",
|
|
96
|
+
"node_modules",
|
|
97
|
+
"dist",
|
|
98
|
+
"build",
|
|
99
|
+
"out",
|
|
100
|
+
"coverage",
|
|
101
|
+
".next",
|
|
102
|
+
".nuxt",
|
|
103
|
+
".venv",
|
|
104
|
+
"venv",
|
|
105
|
+
"env",
|
|
106
|
+
"site-packages",
|
|
107
|
+
"__pycache__",
|
|
108
|
+
".pytest_cache",
|
|
109
|
+
".mypy_cache",
|
|
110
|
+
".ruff_cache",
|
|
111
|
+
".tox",
|
|
112
|
+
"vendor",
|
|
113
|
+
"target",
|
|
114
|
+
".turbo",
|
|
115
|
+
".cache",
|
|
116
|
+
".idea",
|
|
117
|
+
".vscode",
|
|
118
|
+
".terraform",
|
|
119
|
+
"models",
|
|
120
|
+
"data",
|
|
121
|
+
"datasets",
|
|
122
|
+
"media",
|
|
123
|
+
"uploads",
|
|
124
|
+
"tmp",
|
|
125
|
+
"logs",
|
|
126
|
+
"gen"
|
|
127
|
+
]);
|
|
128
|
+
MAX_FILES = 2e4;
|
|
129
|
+
MAX_BYTES = 1e6;
|
|
130
|
+
WALK_BUDGET_MS = 8e3;
|
|
131
|
+
RepoFileIndex = class {
|
|
132
|
+
cwd;
|
|
133
|
+
cache = { contents: /* @__PURE__ */ new Map(), limits: [] };
|
|
134
|
+
bounds;
|
|
135
|
+
constructor(cwd, bounds = { maxFiles: MAX_FILES, budgetMs: WALK_BUDGET_MS }) {
|
|
136
|
+
this.cwd = cwd;
|
|
137
|
+
this.bounds = bounds;
|
|
138
|
+
}
|
|
139
|
+
async walk() {
|
|
140
|
+
if (!this.cache.files) {
|
|
141
|
+
const result = await walkDir(this.cwd, this.bounds.maxFiles, this.bounds.budgetMs);
|
|
142
|
+
this.cache.files = result.files;
|
|
143
|
+
this.cache.limits.push(...result.limits);
|
|
144
|
+
}
|
|
145
|
+
return this.cache.files;
|
|
146
|
+
}
|
|
147
|
+
scanLimits() {
|
|
148
|
+
return [...new Set(this.cache.limits)].sort();
|
|
149
|
+
}
|
|
150
|
+
async exists(path) {
|
|
151
|
+
try {
|
|
152
|
+
await stat(join3(this.cwd, path));
|
|
153
|
+
return true;
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async read(path, maxBytes = MAX_BYTES) {
|
|
159
|
+
const cached = this.cache.contents.get(path);
|
|
160
|
+
if (cached !== void 0) return cached;
|
|
161
|
+
const full = join3(this.cwd, path);
|
|
162
|
+
try {
|
|
163
|
+
const info2 = await stat(full);
|
|
164
|
+
if (!info2.isFile()) return void 0;
|
|
165
|
+
if (info2.size > maxBytes) {
|
|
166
|
+
this.cache.limits.push(`File excluded by size limit (${maxBytes} bytes): ${path}`);
|
|
167
|
+
return void 0;
|
|
168
|
+
}
|
|
169
|
+
const text = await readFile3(full, "utf8");
|
|
170
|
+
this.cache.contents.set(path, text);
|
|
171
|
+
return text;
|
|
172
|
+
} catch {
|
|
173
|
+
return void 0;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async byExt(...exts) {
|
|
177
|
+
const wanted = new Set(exts.map((e) => e.replace(/^\./, "").toLowerCase()));
|
|
178
|
+
const files = await this.walk();
|
|
179
|
+
return files.filter((f) => {
|
|
180
|
+
const dot = f.lastIndexOf(".");
|
|
181
|
+
if (dot < 0) return false;
|
|
182
|
+
return wanted.has(f.slice(dot + 1).toLowerCase());
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async byName(pattern) {
|
|
186
|
+
const files = await this.walk();
|
|
187
|
+
return files.filter((f) => pattern.test(f));
|
|
188
|
+
}
|
|
189
|
+
line(ref) {
|
|
190
|
+
const idx = ref.lastIndexOf(":");
|
|
191
|
+
if (idx < 0) return void 0;
|
|
192
|
+
const path = ref.slice(0, idx);
|
|
193
|
+
const num = Number(ref.slice(idx + 1));
|
|
194
|
+
if (!Number.isInteger(num)) return void 0;
|
|
195
|
+
const text = this.cache.contents.get(path);
|
|
196
|
+
if (text === void 0) return void 0;
|
|
197
|
+
const lines = text.split(/\r?\n/);
|
|
198
|
+
return lines[num - 1];
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// src/inventory/entrypoints.ts
|
|
205
|
+
function codeOnly(source) {
|
|
206
|
+
return source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`/g, (part) => part.replace(/[^\n]/g, " "));
|
|
207
|
+
}
|
|
208
|
+
function exportedEntrypoints(source) {
|
|
209
|
+
const code = codeOnly(source);
|
|
210
|
+
const pattern = /\bexport\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)[^{;]*\{|\bexport\s+(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*(?::[^=;]+)?=>\s*\{/g;
|
|
211
|
+
const result = [];
|
|
212
|
+
let match;
|
|
213
|
+
while (match = pattern.exec(code)) {
|
|
214
|
+
let depth = 1, end = pattern.lastIndex;
|
|
215
|
+
while (end < code.length && depth) {
|
|
216
|
+
if (code[end] === "{") depth++;
|
|
217
|
+
if (code[end] === "}") depth--;
|
|
218
|
+
end++;
|
|
219
|
+
}
|
|
220
|
+
if (depth) continue;
|
|
221
|
+
result.push({ name: match[1] || match[2], startLine: code.slice(0, match.index).split("\n").length, endLine: code.slice(0, end).split("\n").length });
|
|
222
|
+
pattern.lastIndex = end;
|
|
223
|
+
}
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
var init_entrypoints = __esm({
|
|
227
|
+
"src/inventory/entrypoints.ts"() {
|
|
228
|
+
"use strict";
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// src/discovery/masking.ts
|
|
233
|
+
function maskSecret(value) {
|
|
234
|
+
return value.replace(TOKENISH, (match) => `${match.slice(0, 4)}***`);
|
|
235
|
+
}
|
|
236
|
+
function maskEmail(value) {
|
|
237
|
+
return value.replace(EMAIL, "$1***$2");
|
|
238
|
+
}
|
|
239
|
+
function maskUrl(value) {
|
|
240
|
+
try {
|
|
241
|
+
const url = new URL(value);
|
|
242
|
+
return `${url.protocol}//${url.host}${url.pathname}`;
|
|
243
|
+
} catch {
|
|
244
|
+
return maskSecret(value);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function redactDetail(value) {
|
|
248
|
+
return maskSecret(maskEmail(value)).slice(0, 200);
|
|
249
|
+
}
|
|
250
|
+
var EMAIL, TOKENISH;
|
|
251
|
+
var init_masking = __esm({
|
|
252
|
+
"src/discovery/masking.ts"() {
|
|
253
|
+
"use strict";
|
|
254
|
+
EMAIL = /([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*(@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g;
|
|
255
|
+
TOKENISH = /\b(sk|rk|pk|ghp|gho|ghs|xox[baprs]|AKIA|AIza)[A-Za-z0-9_-]{6,}\b/g;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// src/discovery/probes/providers/stripe.ts
|
|
260
|
+
var stripeProbe;
|
|
261
|
+
var init_stripe = __esm({
|
|
262
|
+
"src/discovery/probes/providers/stripe.ts"() {
|
|
263
|
+
"use strict";
|
|
264
|
+
init_masking();
|
|
265
|
+
stripeProbe = {
|
|
266
|
+
id: "stripe.readonly",
|
|
267
|
+
providerId: "stripe",
|
|
268
|
+
title: "Enumerate Stripe account configuration (read-only)",
|
|
269
|
+
description: "Reads products, prices and webhook endpoints. Read-only; issues no writes.",
|
|
270
|
+
readScope: {
|
|
271
|
+
endpoints: [
|
|
272
|
+
"GET /v1/products",
|
|
273
|
+
"GET /v1/prices",
|
|
274
|
+
"GET /v1/webhook_endpoints"
|
|
275
|
+
],
|
|
276
|
+
methods: ["GET"]
|
|
277
|
+
},
|
|
278
|
+
credentials: [
|
|
279
|
+
{
|
|
280
|
+
envVar: "STRIPE_SECRET_KEY",
|
|
281
|
+
label: "Stripe restricted key (read scopes)",
|
|
282
|
+
required: true,
|
|
283
|
+
scopes: ["products:read", "prices:read", "webhook_endpoints:read"],
|
|
284
|
+
hint: "Prefer a restricted key (rk_...) with read scopes over a full sk_live_ key."
|
|
285
|
+
}
|
|
286
|
+
],
|
|
287
|
+
async run(ctx) {
|
|
288
|
+
const spec = this.credentials[0];
|
|
289
|
+
const key = ctx.credential(spec);
|
|
290
|
+
if (!key) {
|
|
291
|
+
return {
|
|
292
|
+
providerId: this.providerId,
|
|
293
|
+
ok: false,
|
|
294
|
+
summary: "no credential present",
|
|
295
|
+
resources: [],
|
|
296
|
+
evidence: []
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const get = async (path) => {
|
|
300
|
+
const response = await ctx.fetch(`https://api.stripe.com${path}`, {
|
|
301
|
+
method: "GET",
|
|
302
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
303
|
+
signal: ctx.signal
|
|
304
|
+
});
|
|
305
|
+
if (!response.ok) {
|
|
306
|
+
throw new Error(`Stripe ${path} -> HTTP ${response.status}`);
|
|
307
|
+
}
|
|
308
|
+
return await response.json();
|
|
309
|
+
};
|
|
310
|
+
try {
|
|
311
|
+
const [products, prices, hooks] = await Promise.all([
|
|
312
|
+
get("/v1/products?limit=100"),
|
|
313
|
+
get("/v1/prices?limit=100"),
|
|
314
|
+
get("/v1/webhook_endpoints?limit=100")
|
|
315
|
+
]);
|
|
316
|
+
return {
|
|
317
|
+
providerId: this.providerId,
|
|
318
|
+
ok: true,
|
|
319
|
+
summary: `${products.data.length} products, ${prices.data.length} prices, ${hooks.data.length} webhook endpoints`,
|
|
320
|
+
resources: [
|
|
321
|
+
{
|
|
322
|
+
kind: "products",
|
|
323
|
+
count: products.data.length,
|
|
324
|
+
sample: products.data.slice(0, 5).map((p) => ({
|
|
325
|
+
id: p.id,
|
|
326
|
+
name: p.name,
|
|
327
|
+
active: p.active
|
|
328
|
+
}))
|
|
329
|
+
},
|
|
330
|
+
{ kind: "prices", count: prices.data.length },
|
|
331
|
+
{
|
|
332
|
+
kind: "webhook_endpoints",
|
|
333
|
+
count: hooks.data.length,
|
|
334
|
+
sample: hooks.data.slice(0, 5).map((h2) => ({
|
|
335
|
+
url: typeof h2.url === "string" ? maskUrl(h2.url) : void 0,
|
|
336
|
+
status: h2.status
|
|
337
|
+
}))
|
|
338
|
+
}
|
|
339
|
+
],
|
|
340
|
+
evidence: [
|
|
341
|
+
{
|
|
342
|
+
ref: "probe:stripe",
|
|
343
|
+
pass: "probe",
|
|
344
|
+
signal: "probe",
|
|
345
|
+
detail: "account reachable via read scopes"
|
|
346
|
+
}
|
|
347
|
+
]
|
|
348
|
+
};
|
|
349
|
+
} catch (error) {
|
|
350
|
+
return {
|
|
351
|
+
providerId: this.providerId,
|
|
352
|
+
ok: false,
|
|
353
|
+
error: { message: error instanceof Error ? error.message : String(error) },
|
|
354
|
+
summary: "probe failed",
|
|
355
|
+
resources: [],
|
|
356
|
+
evidence: []
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// src/discovery/probes/providers/github.ts
|
|
365
|
+
var githubProbe;
|
|
366
|
+
var init_github = __esm({
|
|
367
|
+
"src/discovery/probes/providers/github.ts"() {
|
|
368
|
+
"use strict";
|
|
369
|
+
githubProbe = {
|
|
370
|
+
id: "github.readonly",
|
|
371
|
+
providerId: "github",
|
|
372
|
+
title: "Enumerate GitHub account (read-only)",
|
|
373
|
+
description: "Reads the authenticated user and their repositories. Read-only.",
|
|
374
|
+
readScope: {
|
|
375
|
+
endpoints: ["GET /user", "GET /user/repos"],
|
|
376
|
+
methods: ["GET"]
|
|
377
|
+
},
|
|
378
|
+
credentials: [
|
|
379
|
+
{
|
|
380
|
+
envVar: "GITHUB_TOKEN",
|
|
381
|
+
label: "GitHub token (read:user, repo:read)",
|
|
382
|
+
required: true,
|
|
383
|
+
scopes: ["read:user", "public_repo"],
|
|
384
|
+
hint: "A fine-grained token with read-only repository access is sufficient."
|
|
385
|
+
}
|
|
386
|
+
],
|
|
387
|
+
async run(ctx) {
|
|
388
|
+
const key = ctx.credential(this.credentials[0]);
|
|
389
|
+
if (!key) {
|
|
390
|
+
return {
|
|
391
|
+
providerId: this.providerId,
|
|
392
|
+
ok: false,
|
|
393
|
+
summary: "no credential present",
|
|
394
|
+
resources: [],
|
|
395
|
+
evidence: []
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
const headers = {
|
|
399
|
+
Authorization: `Bearer ${key}`,
|
|
400
|
+
Accept: "application/vnd.github+json",
|
|
401
|
+
"User-Agent": "agenttwin-discovery"
|
|
402
|
+
};
|
|
403
|
+
try {
|
|
404
|
+
const userResponse = await ctx.fetch("https://api.github.com/user", {
|
|
405
|
+
method: "GET",
|
|
406
|
+
headers,
|
|
407
|
+
signal: ctx.signal
|
|
408
|
+
});
|
|
409
|
+
if (!userResponse.ok) {
|
|
410
|
+
throw new Error(`GitHub /user -> HTTP ${userResponse.status}`);
|
|
411
|
+
}
|
|
412
|
+
const user = await userResponse.json();
|
|
413
|
+
const reposResponse = await ctx.fetch(
|
|
414
|
+
"https://api.github.com/user/repos?per_page=100&sort=updated",
|
|
415
|
+
{ method: "GET", headers, signal: ctx.signal }
|
|
416
|
+
);
|
|
417
|
+
const repos = reposResponse.ok ? await reposResponse.json() : [];
|
|
418
|
+
return {
|
|
419
|
+
providerId: this.providerId,
|
|
420
|
+
ok: true,
|
|
421
|
+
summary: `${repos.length} repositories readable for @${user.login ?? "unknown"}`,
|
|
422
|
+
resources: [
|
|
423
|
+
{
|
|
424
|
+
kind: "repositories",
|
|
425
|
+
count: repos.length,
|
|
426
|
+
sample: repos.slice(0, 5).map((r) => ({
|
|
427
|
+
name: r.name,
|
|
428
|
+
private: r.private,
|
|
429
|
+
archived: r.archived
|
|
430
|
+
}))
|
|
431
|
+
}
|
|
432
|
+
],
|
|
433
|
+
evidence: [
|
|
434
|
+
{
|
|
435
|
+
ref: "probe:github",
|
|
436
|
+
pass: "probe",
|
|
437
|
+
signal: "probe",
|
|
438
|
+
detail: "account reachable via read scopes"
|
|
439
|
+
}
|
|
440
|
+
]
|
|
441
|
+
};
|
|
442
|
+
} catch (error) {
|
|
443
|
+
return {
|
|
444
|
+
providerId: this.providerId,
|
|
445
|
+
ok: false,
|
|
446
|
+
error: { message: error instanceof Error ? error.message : String(error) },
|
|
447
|
+
summary: "probe failed",
|
|
448
|
+
resources: [],
|
|
449
|
+
evidence: []
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// src/discovery/providers/catalog.ts
|
|
458
|
+
function planned(unknown) {
|
|
459
|
+
return { supported: [], verified: [], unknown, implemented: false };
|
|
460
|
+
}
|
|
461
|
+
var NO_TWIN, PROVIDERS;
|
|
462
|
+
var init_catalog = __esm({
|
|
463
|
+
"src/discovery/providers/catalog.ts"() {
|
|
464
|
+
"use strict";
|
|
465
|
+
init_stripe();
|
|
466
|
+
init_github();
|
|
467
|
+
NO_TWIN = ["all behavior (no twin implemented)"];
|
|
468
|
+
PROVIDERS = [
|
|
469
|
+
{
|
|
470
|
+
id: "stripe",
|
|
471
|
+
title: "Stripe",
|
|
472
|
+
category: "payments",
|
|
473
|
+
twin: planned(["refunds", "payouts", "disputes", "webhook ordering"]),
|
|
474
|
+
probe: stripeProbe,
|
|
475
|
+
signatures: {
|
|
476
|
+
packages: {
|
|
477
|
+
node: ["stripe"],
|
|
478
|
+
python: ["stripe"],
|
|
479
|
+
go: ["stripe-go"],
|
|
480
|
+
java: ["stripe-java"],
|
|
481
|
+
ruby: ["stripe"],
|
|
482
|
+
php: ["stripe/stripe-php"],
|
|
483
|
+
dotnet: ["Stripe.net"]
|
|
484
|
+
},
|
|
485
|
+
imports: ["new Stripe\\(", `from ["']stripe["']`, `require\\(['"]stripe['"]\\)`, "stripe-go"],
|
|
486
|
+
hosts: ["api.stripe.com"],
|
|
487
|
+
envPrefixes: ["STRIPE_"],
|
|
488
|
+
webhookPaths: ["/webhooks/stripe", "/stripe/webhook", "/webhook/stripe"]
|
|
489
|
+
},
|
|
490
|
+
docs: "https://stripe.com/docs/api"
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
id: "slack",
|
|
494
|
+
title: "Slack",
|
|
495
|
+
category: "comms",
|
|
496
|
+
twin: planned(NO_TWIN),
|
|
497
|
+
signatures: {
|
|
498
|
+
packages: {
|
|
499
|
+
node: ["@slack/web-api", "@slack/bolt", "@slack/events-api", "@slack/socket-mode"],
|
|
500
|
+
python: ["slack_sdk", "slack-bolt"],
|
|
501
|
+
go: ["slack-go"],
|
|
502
|
+
java: ["slack-java-sdk"],
|
|
503
|
+
ruby: ["slack-ruby-client"]
|
|
504
|
+
},
|
|
505
|
+
imports: ["@slack/web-api", "WebClient\\(", "slack_sdk"],
|
|
506
|
+
hosts: ["slack.com", "api.slack.com", "hooks.slack.com"],
|
|
507
|
+
envPrefixes: ["SLACK_"]
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
id: "github",
|
|
512
|
+
title: "GitHub",
|
|
513
|
+
category: "vcs",
|
|
514
|
+
twin: planned(NO_TWIN),
|
|
515
|
+
probe: githubProbe,
|
|
516
|
+
signatures: {
|
|
517
|
+
packages: {
|
|
518
|
+
node: ["@octokit/rest", "@octokit/core", "@octokit/auth", "octokit"],
|
|
519
|
+
python: ["PyGithub", "github"],
|
|
520
|
+
go: ["google/go-github"],
|
|
521
|
+
java: ["github-api"],
|
|
522
|
+
ruby: ["octokit"]
|
|
523
|
+
},
|
|
524
|
+
imports: ["Octokit", "@octokit/", "PyGithub"],
|
|
525
|
+
hosts: ["api.github.com"],
|
|
526
|
+
envPrefixes: ["GITHUB_", "GH_"],
|
|
527
|
+
webhookPaths: ["/webhooks/github", "/github/webhook"]
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
id: "aws",
|
|
532
|
+
title: "AWS",
|
|
533
|
+
category: "cloud",
|
|
534
|
+
twin: planned(["service coverage (LocalStack passthrough planned)"]),
|
|
535
|
+
envWeak: true,
|
|
536
|
+
signatures: {
|
|
537
|
+
packages: {
|
|
538
|
+
node: ["@aws-sdk/", "aws-sdk"],
|
|
539
|
+
python: ["boto3", "botocore"],
|
|
540
|
+
go: ["aws-sdk-go", "aws-sdk-go-v2"],
|
|
541
|
+
java: ["software.amazon.awssdk", "aws-java-sdk"],
|
|
542
|
+
ruby: ["aws-sdk-"],
|
|
543
|
+
php: ["aws/aws-sdk-php"],
|
|
544
|
+
dotnet: ["AWSSDK."]
|
|
545
|
+
},
|
|
546
|
+
imports: ["@aws-sdk/", "boto3", "aws-sdk-go"],
|
|
547
|
+
hosts: ["amazonaws.com"],
|
|
548
|
+
envPrefixes: ["AWS_"]
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
id: "google",
|
|
553
|
+
title: "Google (Gmail / Calendar / Workspace)",
|
|
554
|
+
category: "comms",
|
|
555
|
+
twin: planned(NO_TWIN),
|
|
556
|
+
envWeak: true,
|
|
557
|
+
signatures: {
|
|
558
|
+
packages: {
|
|
559
|
+
node: ["googleapis", "google-auth-library"],
|
|
560
|
+
python: ["google-api-python-client", "google-cloud", "google-auth"],
|
|
561
|
+
go: ["google.golang.org/api"],
|
|
562
|
+
java: ["google-api-client"],
|
|
563
|
+
ruby: ["google-apis-"]
|
|
564
|
+
},
|
|
565
|
+
imports: ["googleapis", "google-api-python-client", "google.golang.org/api"],
|
|
566
|
+
hosts: ["googleapis.com", "gmail.googleapis.com", "www.googleapis.com"],
|
|
567
|
+
envPrefixes: ["GOOGLE_", "GCP_", "GMAIL_"]
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
id: "openai",
|
|
572
|
+
title: "OpenAI",
|
|
573
|
+
category: "ai",
|
|
574
|
+
twin: planned(NO_TWIN),
|
|
575
|
+
signatures: {
|
|
576
|
+
packages: {
|
|
577
|
+
node: ["openai"],
|
|
578
|
+
python: ["openai"],
|
|
579
|
+
go: ["go-openai"],
|
|
580
|
+
ruby: ["ruby-openai"]
|
|
581
|
+
},
|
|
582
|
+
imports: ["OpenAI\\(", `from ["']openai["']`, "go-openai"],
|
|
583
|
+
hosts: ["api.openai.com"],
|
|
584
|
+
envPrefixes: ["OPENAI_"]
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
id: "anthropic",
|
|
589
|
+
title: "Anthropic",
|
|
590
|
+
category: "ai",
|
|
591
|
+
twin: planned(NO_TWIN),
|
|
592
|
+
signatures: {
|
|
593
|
+
packages: {
|
|
594
|
+
node: ["@anthropic-ai/sdk"],
|
|
595
|
+
python: ["anthropic"],
|
|
596
|
+
go: ["anthropic-sdk-go"]
|
|
597
|
+
},
|
|
598
|
+
imports: ["@anthropic-ai/sdk", "Anthropic\\("],
|
|
599
|
+
hosts: ["api.anthropic.com"],
|
|
600
|
+
envPrefixes: ["ANTHROPIC_"]
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
id: "shopify",
|
|
605
|
+
title: "Shopify",
|
|
606
|
+
category: "commerce",
|
|
607
|
+
twin: planned(NO_TWIN),
|
|
608
|
+
signatures: {
|
|
609
|
+
packages: {
|
|
610
|
+
node: ["@shopify/", "shopify-api"],
|
|
611
|
+
python: ["ShopifyAPI"],
|
|
612
|
+
ruby: ["shopify_api"]
|
|
613
|
+
},
|
|
614
|
+
imports: ["@shopify/", "ShopifyAPI"],
|
|
615
|
+
hosts: ["myshopify.com", "shopify.com"],
|
|
616
|
+
envPrefixes: ["SHOPIFY_"]
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
id: "twilio",
|
|
621
|
+
title: "Twilio",
|
|
622
|
+
category: "comms",
|
|
623
|
+
twin: planned(NO_TWIN),
|
|
624
|
+
signatures: {
|
|
625
|
+
packages: {
|
|
626
|
+
node: ["twilio"],
|
|
627
|
+
python: ["twilio"],
|
|
628
|
+
java: ["twilio"],
|
|
629
|
+
ruby: ["twilio-ruby"],
|
|
630
|
+
php: ["twilio/sdk"]
|
|
631
|
+
},
|
|
632
|
+
imports: ["twilio"],
|
|
633
|
+
hosts: ["api.twilio.com"],
|
|
634
|
+
envPrefixes: ["TWILIO_"]
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
id: "sendgrid",
|
|
639
|
+
title: "SendGrid",
|
|
640
|
+
category: "comms",
|
|
641
|
+
twin: planned(NO_TWIN),
|
|
642
|
+
signatures: {
|
|
643
|
+
packages: {
|
|
644
|
+
node: ["@sendgrid/mail", "@sendgrid/client"],
|
|
645
|
+
python: ["sendgrid"],
|
|
646
|
+
ruby: ["sendgrid-ruby"]
|
|
647
|
+
},
|
|
648
|
+
imports: ["@sendgrid/", "sendgrid"],
|
|
649
|
+
hosts: ["api.sendgrid.com"],
|
|
650
|
+
envPrefixes: ["SENDGRID_"]
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
id: "hubspot",
|
|
655
|
+
title: "HubSpot",
|
|
656
|
+
category: "crm",
|
|
657
|
+
twin: planned(NO_TWIN),
|
|
658
|
+
signatures: {
|
|
659
|
+
packages: {
|
|
660
|
+
node: ["@hubspot/api-client"],
|
|
661
|
+
python: ["hubspot-api-client"],
|
|
662
|
+
ruby: ["hubspot-api-client"]
|
|
663
|
+
},
|
|
664
|
+
imports: ["@hubspot/api-client", "hubspot-api-client"],
|
|
665
|
+
hosts: ["api.hubapi.com"],
|
|
666
|
+
envPrefixes: ["HUBSPOT_"]
|
|
667
|
+
}
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
id: "salesforce",
|
|
671
|
+
title: "Salesforce",
|
|
672
|
+
category: "crm",
|
|
673
|
+
twin: planned(NO_TWIN),
|
|
674
|
+
signatures: {
|
|
675
|
+
packages: {
|
|
676
|
+
node: ["jsforce", "@salesforce/"],
|
|
677
|
+
python: ["simple-salesforce"],
|
|
678
|
+
java: ["force-wsc"]
|
|
679
|
+
},
|
|
680
|
+
imports: ["jsforce", "simple-salesforce"],
|
|
681
|
+
hosts: ["salesforce.com", "force.com"],
|
|
682
|
+
envPrefixes: ["SALESFORCE_", "SF_"]
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
id: "zendesk",
|
|
687
|
+
title: "Zendesk",
|
|
688
|
+
category: "support",
|
|
689
|
+
twin: planned(NO_TWIN),
|
|
690
|
+
signatures: {
|
|
691
|
+
packages: {
|
|
692
|
+
node: ["node-zendesk"],
|
|
693
|
+
python: ["zendesk", "zenpy"],
|
|
694
|
+
ruby: ["zendesk_api"]
|
|
695
|
+
},
|
|
696
|
+
imports: ["node-zendesk", "zenpy"],
|
|
697
|
+
hosts: ["zendesk.com"],
|
|
698
|
+
envPrefixes: ["ZENDESK_"]
|
|
699
|
+
}
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
id: "linear",
|
|
703
|
+
title: "Linear",
|
|
704
|
+
category: "pm",
|
|
705
|
+
twin: planned(NO_TWIN),
|
|
706
|
+
signatures: {
|
|
707
|
+
packages: { node: ["@linear/sdk"], python: ["linear"] },
|
|
708
|
+
imports: ["@linear/sdk"],
|
|
709
|
+
hosts: ["api.linear.app"],
|
|
710
|
+
envPrefixes: ["LINEAR_"]
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
id: "notion",
|
|
715
|
+
title: "Notion",
|
|
716
|
+
category: "docs",
|
|
717
|
+
twin: planned(NO_TWIN),
|
|
718
|
+
signatures: {
|
|
719
|
+
packages: { node: ["@notionhq/client"], python: ["notion-client"] },
|
|
720
|
+
imports: ["@notionhq/client", "notion-client"],
|
|
721
|
+
hosts: ["api.notion.com"],
|
|
722
|
+
envPrefixes: ["NOTION_"]
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
id: "resend",
|
|
727
|
+
title: "Resend",
|
|
728
|
+
category: "comms",
|
|
729
|
+
twin: planned(NO_TWIN),
|
|
730
|
+
signatures: {
|
|
731
|
+
packages: { node: ["resend"] },
|
|
732
|
+
imports: [`from ["']resend["']`],
|
|
733
|
+
hosts: ["api.resend.com"],
|
|
734
|
+
envPrefixes: ["RESEND_"]
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
];
|
|
738
|
+
for (const [id, title, category, node, python, hosts] of [
|
|
739
|
+
["composio", "Composio", "integration", ["@composio/core", "@composio/client"], ["composio", "composio-core"], ["backend.composio.dev"]],
|
|
740
|
+
["kernel", "Kernel", "browser", ["@onkernel/sdk"], ["kernel"], ["api.onkernel.com"]],
|
|
741
|
+
["e2b", "E2B", "runtime", ["e2b", "@e2b/code-interpreter"], ["e2b", "e2b-code-interpreter"], ["api.e2b.dev"]],
|
|
742
|
+
["mem0", "Mem0", "ai", ["mem0ai"], ["mem0ai"], ["api.mem0.ai"]],
|
|
743
|
+
["ollama", "Ollama", "ai", ["ollama"], ["ollama"], []],
|
|
744
|
+
["onepassword", "1Password", "security", ["@1password/sdk"], ["onepassword-sdk"], []]
|
|
745
|
+
]) PROVIDERS.push({ id, title, category, twin: planned(NO_TWIN), signatures: { packages: { node: [...node], python: [...python] }, imports: [...node, ...python], hosts: [...hosts] } });
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
|
|
51
749
|
// src/inventory/dashboard-transfer.ts
|
|
52
750
|
function discoveryTransfer(value) {
|
|
53
751
|
const input = value;
|
|
@@ -91,37 +789,98 @@ var init_dashboard_transfer = __esm({
|
|
|
91
789
|
}
|
|
92
790
|
});
|
|
93
791
|
|
|
792
|
+
// src/inventory/project-products.ts
|
|
793
|
+
async function projectProducts(cwd, data, base) {
|
|
794
|
+
const current = new Set(base.tools.map((t) => t.id));
|
|
795
|
+
const known = new Set(PROVIDERS.map((p) => p.title));
|
|
796
|
+
const tools = data.tools.filter((t) => current.has(t.id) || !known.has(t.title) && t.category !== "access" && t.category !== "tooling");
|
|
797
|
+
const ids = new Set(tools.map((t) => t.id));
|
|
798
|
+
const workflows = data.workflows.map((w) => ({ ...w, tools: w.tools.filter((id) => ids.has(id)), steps: w.steps.filter((s) => ids.has(s.tool)) }));
|
|
799
|
+
const google = tools.find((t) => /Google \(Gmail/.test(t.title));
|
|
800
|
+
if (!google) return discoveryTransfer({ ...data, tools, workflows });
|
|
801
|
+
const products = [
|
|
802
|
+
{ key: "gmail", title: "Gmail", pattern: /(?:gmail\.googleapis\.com|\/gmail\/v\d|\bgoogle\.gmail\s*\()/i },
|
|
803
|
+
{ key: "calendar", title: "Google Calendar", pattern: /(?:\/calendar\/v\d|\bgoogle\.calendar\s*\()/i },
|
|
804
|
+
{ key: "drive", title: "Google Drive", pattern: /(?:\/drive\/v\d|\bgoogle\.drive\s*\()/i },
|
|
805
|
+
{ key: "docs", title: "Google Docs", pattern: /(?:docs\.googleapis\.com|\/documents\/|\bgoogle\.docs\s*\()/i },
|
|
806
|
+
{ key: "sheets", title: "Google Sheets", pattern: /(?:sheets\.googleapis\.com|\/spreadsheets\/|\bgoogle\.sheets\s*\()/i }
|
|
807
|
+
];
|
|
808
|
+
const files = await createFileIndex(cwd);
|
|
809
|
+
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);
|
|
810
|
+
const found = /* @__PURE__ */ new Set();
|
|
811
|
+
for (const path of paths) {
|
|
812
|
+
const source = await files.read(path, 2e5);
|
|
813
|
+
if (!source) continue;
|
|
814
|
+
for (const p of products) if (p.pattern.test(source)) found.add(p.key);
|
|
815
|
+
}
|
|
816
|
+
const mapped = workflows.map((w) => {
|
|
817
|
+
if (!w.tools.includes(google.id)) return w;
|
|
818
|
+
const [path, fn] = (w.entrypoint ?? "").split("#");
|
|
819
|
+
return { w, path, fn };
|
|
820
|
+
});
|
|
821
|
+
const finalWorkflows = [];
|
|
822
|
+
let unresolved = false;
|
|
823
|
+
for (const value of mapped) {
|
|
824
|
+
if (!("w" in value)) {
|
|
825
|
+
finalWorkflows.push(value);
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
const { w, path, fn } = value;
|
|
829
|
+
const source = path ? await files.read(path, 2e5) : void 0;
|
|
830
|
+
const entry = source && fn ? exportedEntrypoints(source).find((e) => e.name === fn) : void 0;
|
|
831
|
+
const body = source && entry ? source.split("\n").slice(entry.startLine - 1, entry.endLine).join("\n") : source ?? "";
|
|
832
|
+
const matches = products.filter((p) => found.has(p.key) && p.pattern.test(body)).map((p) => google.id + ":" + p.key);
|
|
833
|
+
if (!matches.length) {
|
|
834
|
+
unresolved = true;
|
|
835
|
+
finalWorkflows.push(w);
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
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]) });
|
|
839
|
+
}
|
|
840
|
+
const productTools = products.filter((p) => found.has(p.key)).map((p) => ({ ...google, id: google.id + ":" + p.key, title: p.title }));
|
|
841
|
+
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 });
|
|
842
|
+
}
|
|
843
|
+
var init_project_products = __esm({
|
|
844
|
+
"src/inventory/project-products.ts"() {
|
|
845
|
+
"use strict";
|
|
846
|
+
init_files();
|
|
847
|
+
init_entrypoints();
|
|
848
|
+
init_catalog();
|
|
849
|
+
init_dashboard_transfer();
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
|
|
94
853
|
// src/inspect/sanitize.ts
|
|
95
854
|
function stripUrlCredentials(value) {
|
|
96
855
|
return value.replace(URL_CREDENTIALS, "$1");
|
|
97
856
|
}
|
|
98
857
|
function sanitizeOutput(value) {
|
|
99
|
-
return stripUrlCredentials(value).replace(
|
|
858
|
+
return stripUrlCredentials(value).replace(TOKENISH2, (m) => `${m.slice(0, 4)}***`).replace(EMAIL2, "$1***$2");
|
|
100
859
|
}
|
|
101
860
|
function excerpt(value, max = 200) {
|
|
102
861
|
const clean = sanitizeOutput(value).replace(/\s+/g, " ").trim();
|
|
103
862
|
return clean.length > max ? `${clean.slice(0, max)}\u2026` : clean;
|
|
104
863
|
}
|
|
105
|
-
var URL_CREDENTIALS,
|
|
864
|
+
var URL_CREDENTIALS, TOKENISH2, EMAIL2;
|
|
106
865
|
var init_sanitize = __esm({
|
|
107
866
|
"src/inspect/sanitize.ts"() {
|
|
108
867
|
"use strict";
|
|
109
868
|
URL_CREDENTIALS = /(https?:\/\/)([^/@\s:]+)(:[^/@\s]+)?@/gi;
|
|
110
|
-
|
|
111
|
-
|
|
869
|
+
TOKENISH2 = /\b(sk|rk|pk|ghp|gho|ghs|glpat|xox[baprs]|AKIA|AIza)[A-Za-z0-9_-]{6,}\b/g;
|
|
870
|
+
EMAIL2 = /([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*(@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g;
|
|
112
871
|
}
|
|
113
872
|
});
|
|
114
873
|
|
|
115
874
|
// src/commands/dashboard.ts
|
|
116
|
-
import { cp, mkdir, readFile as
|
|
117
|
-
import { basename, join as
|
|
875
|
+
import { cp, mkdir, readFile as readFile4, writeFile } from "fs/promises";
|
|
876
|
+
import { basename, join as join4 } from "path";
|
|
118
877
|
import { fileURLToPath } from "url";
|
|
119
878
|
function dashboardInventory(cwd, inventory) {
|
|
120
879
|
return JSON.parse(sanitizeOutput(JSON.stringify({
|
|
121
880
|
schema: "company-inventory/v1",
|
|
122
881
|
generatedAt: inventory.generatedAt,
|
|
123
882
|
name: basename(cwd),
|
|
124
|
-
tools: inventory.tools.map((t) => ({
|
|
883
|
+
tools: inventory.tools.filter((t) => !t.facts?.length || !t.facts.every((f) => f.evidence.every((e) => /^(?:credential:|command:)/.test(e.ref)))).map((t) => ({
|
|
125
884
|
id: t.id,
|
|
126
885
|
title: t.title,
|
|
127
886
|
kind: t.kind,
|
|
@@ -133,26 +892,26 @@ function dashboardInventory(cwd, inventory) {
|
|
|
133
892
|
})));
|
|
134
893
|
}
|
|
135
894
|
async function writeDashboard(cwd, inventory) {
|
|
136
|
-
const directory =
|
|
895
|
+
const directory = join4(cwd, ".agenttwin", "dashboard");
|
|
137
896
|
let bundle = fileURLToPath(new URL("./dashboard/", import.meta.url));
|
|
138
897
|
try {
|
|
139
|
-
await
|
|
898
|
+
await readFile4(join4(bundle, "index.html"));
|
|
140
899
|
} catch {
|
|
141
900
|
bundle = fileURLToPath(new URL("../../../../site/", import.meta.url));
|
|
142
901
|
}
|
|
143
|
-
await mkdir(
|
|
144
|
-
const html = await
|
|
902
|
+
await mkdir(join4(directory, "assets", "dashboard"), { recursive: true });
|
|
903
|
+
const html = await readFile4(join4(bundle, bundle.endsWith("/site/") ? "dashboard.html" : "index.html"), "utf8");
|
|
145
904
|
for (const folder of ["dashboard", "icons", "logo", "fonts", "vendor"]) {
|
|
146
|
-
await cp(
|
|
905
|
+
await cp(join4(bundle, "assets", folder), join4(directory, "assets", folder), { recursive: true });
|
|
147
906
|
}
|
|
148
|
-
await cp(
|
|
907
|
+
await cp(join4(bundle, "assets", "brand.css"), join4(directory, "assets", "brand.css"));
|
|
149
908
|
const data = await loadDashboardDiscovery(cwd, inventory);
|
|
150
909
|
const payload = JSON.stringify(data).replace(/</g, "\\u003c");
|
|
151
910
|
const page2 = html.replaceAll('type="module"', "defer").replace('<script src="assets/dashboard/dashboard.js', `<script id="agenttwin-discovery" type="application/json">${payload}</script><script src="assets/dashboard/dashboard.js`).replace(/href="index.html"/g, 'href="../report.html"').replace(/href="docs\/"/g, 'href="../report.html"').replace("Docs \u2197", "Discovery report \u2197").replace("Runtime documentation \u2197", "Coverage report \u2197").replace("Back to website \u2197", "Discovery report \u2197");
|
|
152
|
-
await writeFile(
|
|
153
|
-
await writeFile(
|
|
154
|
-
await writeFile(
|
|
155
|
-
return
|
|
911
|
+
await writeFile(join4(directory, "index.html"), page2, "utf8");
|
|
912
|
+
await writeFile(join4(directory, "assets", "dashboard", "inventory.json"), JSON.stringify(data), "utf8");
|
|
913
|
+
await writeFile(join4(directory, "assets", "dashboard", "runs.json"), '{"runs":[]}', "utf8");
|
|
914
|
+
return join4(directory, "index.html");
|
|
156
915
|
}
|
|
157
916
|
var init_dashboard = __esm({
|
|
158
917
|
"src/commands/dashboard.ts"() {
|
|
@@ -172,8 +931,8 @@ __export(session_exports, {
|
|
|
172
931
|
writeSessionTask: () => writeSessionTask
|
|
173
932
|
});
|
|
174
933
|
import { spawn, spawnSync } from "child_process";
|
|
175
|
-
import { writeFile as writeFile2, readFile as
|
|
176
|
-
import { join as
|
|
934
|
+
import { writeFile as writeFile2, readFile as readFile5 } from "fs/promises";
|
|
935
|
+
import { join as join5 } from "path";
|
|
177
936
|
function sessionTask() {
|
|
178
937
|
return `# Prepare this repository for AgentTwin
|
|
179
938
|
|
|
@@ -198,7 +957,7 @@ function installedOpenCode() {
|
|
|
198
957
|
return spawnSync("opencode", ["--version"], { stdio: "ignore", timeout: 5e3 }).status === 0;
|
|
199
958
|
}
|
|
200
959
|
async function writeSessionTask(cwd) {
|
|
201
|
-
const path =
|
|
960
|
+
const path = join5(cwd, ".agenttwin/AGENT-TASK.md");
|
|
202
961
|
await writeFile2(path, sessionTask(), "utf8");
|
|
203
962
|
return path;
|
|
204
963
|
}
|
|
@@ -215,9 +974,9 @@ async function loadDashboardDiscovery(cwd, inventory) {
|
|
|
215
974
|
const base = discoveryTransfer(dashboardInventory(cwd, inventory));
|
|
216
975
|
let raw;
|
|
217
976
|
try {
|
|
218
|
-
raw = await
|
|
977
|
+
raw = await readFile5(join5(cwd, ".agenttwin/agent-discovery.json"), "utf8");
|
|
219
978
|
} catch (error) {
|
|
220
|
-
if (error.code === "ENOENT") return base;
|
|
979
|
+
if (error.code === "ENOENT") return projectProducts(cwd, base, base);
|
|
221
980
|
throw error;
|
|
222
981
|
}
|
|
223
982
|
if (Buffer.byteLength(raw) > 2 * 1024 * 1024) throw new Error("Agent discovery exceeds 2 MiB");
|
|
@@ -229,11 +988,12 @@ async function loadDashboardDiscovery(cwd, inventory) {
|
|
|
229
988
|
const result = discoveryTransfer({ ...base, tools: [...tools.values()], workflows: [...workflows.values()] });
|
|
230
989
|
const ids = new Set(result.tools.map((t) => t.id));
|
|
231
990
|
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.");
|
|
232
|
-
return result;
|
|
991
|
+
return projectProducts(cwd, result, base);
|
|
233
992
|
}
|
|
234
993
|
var init_session = __esm({
|
|
235
994
|
"src/commands/session.ts"() {
|
|
236
995
|
"use strict";
|
|
996
|
+
init_project_products();
|
|
237
997
|
init_dashboard_transfer();
|
|
238
998
|
init_dashboard();
|
|
239
999
|
}
|
|
@@ -289,8 +1049,8 @@ import { parseArgs } from "util";
|
|
|
289
1049
|
|
|
290
1050
|
// src/commands/sync.ts
|
|
291
1051
|
init_git();
|
|
292
|
-
import { readFile as
|
|
293
|
-
import { join as
|
|
1052
|
+
import { readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
|
|
1053
|
+
import { join as join6 } from "path";
|
|
294
1054
|
|
|
295
1055
|
// src/runtime/receipt-sync.ts
|
|
296
1056
|
init_git();
|
|
@@ -438,11 +1198,11 @@ async function runSync(cwd) {
|
|
|
438
1198
|
const token = process.env.AGENTTWIN_TOKEN;
|
|
439
1199
|
if (!token || !/^at_user_[a-f0-9]{64}$/.test(token)) throw new Error("Set AGENTTWIN_TOKEN to your individual account token.");
|
|
440
1200
|
const base = apiBase(process.env.AGENTTWIN_API_URL || "https://agenttwin-api.212.56.40.125.sslip.io");
|
|
441
|
-
const inventory = JSON.parse(await
|
|
1201
|
+
const inventory = JSON.parse(await readFile6(join6(cwd, ".agenttwin/inventory.json"), "utf8"));
|
|
442
1202
|
const payload = await loadDashboardDiscovery(cwd, inventory);
|
|
443
1203
|
let previousHead;
|
|
444
1204
|
try {
|
|
445
|
-
previousHead = JSON.parse(await
|
|
1205
|
+
previousHead = JSON.parse(await readFile6(join6(cwd, ".agenttwin/git.json"), "utf8")).head;
|
|
446
1206
|
} catch {
|
|
447
1207
|
}
|
|
448
1208
|
const git = await captureGit(cwd, previousHead);
|
|
@@ -450,22 +1210,22 @@ async function runSync(cwd) {
|
|
|
450
1210
|
const response = await fetch(base + "/v1/discovery", { method: "POST", headers: { authorization: "Bearer " + token, "content-type": "application/json" }, body: JSON.stringify(payload), redirect: "error", signal: AbortSignal.timeout(2e4) });
|
|
451
1211
|
const result = await response.json();
|
|
452
1212
|
if (!response.ok) throw new Error(result.error || "Discovery sync failed: " + response.status);
|
|
453
|
-
if (git) await writeFile3(
|
|
1213
|
+
if (git) await writeFile3(join6(cwd, ".agenttwin/git.json"), JSON.stringify(git) + "\n", { mode: 384 });
|
|
454
1214
|
const receipts = await syncReceipts(cwd, base, token);
|
|
455
1215
|
return { receipts, synced: true, tools: payload.tools.length, workflows: payload.workflows.length, dashboard: "https://agenttwin.si/site/dashboard.html", uploaded: "discovery, Git metadata and run evidence; no application source or credentials" };
|
|
456
1216
|
}
|
|
457
1217
|
|
|
458
1218
|
// src/commands/prepare.ts
|
|
459
1219
|
init_git();
|
|
460
|
-
import { mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as
|
|
461
|
-
import { join as
|
|
1220
|
+
import { mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile9, writeFile as writeFile5, rm as rm2 } from "fs/promises";
|
|
1221
|
+
import { join as join9, resolve as resolve2, dirname as dirname2 } from "path";
|
|
462
1222
|
import { tmpdir as tmpdir2 } from "os";
|
|
463
1223
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
464
1224
|
|
|
465
1225
|
// src/commands/run.ts
|
|
466
|
-
import { mkdtemp, mkdir as mkdir2, readFile as
|
|
1226
|
+
import { mkdtemp, mkdir as mkdir2, readFile as readFile8, writeFile as writeFile4, realpath, lstat, rm, copyFile } from "fs/promises";
|
|
467
1227
|
import { tmpdir } from "os";
|
|
468
|
-
import { basename as basename2, dirname, isAbsolute, join as
|
|
1228
|
+
import { basename as basename2, dirname, isAbsolute, join as join8, relative as relative2, resolve, sep as sep2 } from "path";
|
|
469
1229
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
470
1230
|
import { createHash, randomUUID } from "crypto";
|
|
471
1231
|
|
|
@@ -718,17 +1478,17 @@ init_git();
|
|
|
718
1478
|
|
|
719
1479
|
// src/runtime/e2b.ts
|
|
720
1480
|
import { Sandbox } from "e2b";
|
|
721
|
-
import { readFile as
|
|
1481
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
722
1482
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
723
1483
|
import { homedir } from "os";
|
|
724
|
-
import { join as
|
|
1484
|
+
import { join as join7 } from "path";
|
|
725
1485
|
async function executeE2B(stage, spec, log, signal) {
|
|
726
1486
|
signal?.throwIfAborted();
|
|
727
1487
|
const apiKey = process.env.E2B_API_KEY;
|
|
728
1488
|
if (!apiKey) throw new CliError("E2B_API_KEY is not configured", { code: EXIT.BLOCKED, action: "Set E2B_API_KEY in your shell. It remains on the host and is never passed to the application." });
|
|
729
1489
|
let saved = {};
|
|
730
1490
|
try {
|
|
731
|
-
saved = JSON.parse(await
|
|
1491
|
+
saved = JSON.parse(await readFile7(join7(homedir(), ".agenttwin/runtime.json"), "utf8"));
|
|
732
1492
|
} catch {
|
|
733
1493
|
}
|
|
734
1494
|
const template = process.env.E2B_TEMPLATE_ID ?? saved.templateId;
|
|
@@ -747,7 +1507,7 @@ async function executeE2B(stage, spec, log, signal) {
|
|
|
747
1507
|
signal?.throwIfAborted();
|
|
748
1508
|
await sandbox.commands.run("mkdir -p /input/app && chmod 755 /input /input/app", { user: "root" });
|
|
749
1509
|
for (const file of ["world.json", "worker.js", ...spec.files.map((f) => `app/${f}`)]) {
|
|
750
|
-
await sandbox.files.write(`/input/${file}`, new Uint8Array(await
|
|
1510
|
+
await sandbox.files.write(`/input/${file}`, new Uint8Array(await readFile7(join7(stage, file))).buffer, { user: "root" });
|
|
751
1511
|
}
|
|
752
1512
|
await sandbox.commands.run("chmod -R a+rX /input", { user: "root" });
|
|
753
1513
|
const runtimeEnv = {};
|
|
@@ -868,12 +1628,12 @@ async function stageApplication(root, files, destination) {
|
|
|
868
1628
|
for (const file of files) {
|
|
869
1629
|
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 });
|
|
870
1630
|
const source = await realpath(resolve(canonicalRoot, file));
|
|
871
|
-
const rel =
|
|
872
|
-
if (rel.startsWith(`..${
|
|
873
|
-
const contents = await
|
|
1631
|
+
const rel = relative2(canonicalRoot, source);
|
|
1632
|
+
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 });
|
|
1633
|
+
const contents = await readFile8(source);
|
|
874
1634
|
bytes += contents.length;
|
|
875
1635
|
if (bytes > 20 * 1024 * 1024) throw new CliError("Application files exceed 20 MiB; supply a smaller built bundle", { code: EXIT.USAGE });
|
|
876
|
-
const target =
|
|
1636
|
+
const target = join8(destination, file);
|
|
877
1637
|
await mkdir2(dirname(target), { recursive: true });
|
|
878
1638
|
await writeFile4(target, contents, { mode: 420 });
|
|
879
1639
|
digests[file] = createHash("sha256").update(contents).digest("hex");
|
|
@@ -884,14 +1644,14 @@ async function runWorld(cwd, opts) {
|
|
|
884
1644
|
const file = resolve(cwd, opts.file ?? "agenttwin.world.json");
|
|
885
1645
|
let spec;
|
|
886
1646
|
try {
|
|
887
|
-
spec = parseWorld(JSON.parse(await
|
|
1647
|
+
spec = parseWorld(JSON.parse(await readFile8(file, "utf8")));
|
|
888
1648
|
} catch (error) {
|
|
889
1649
|
throw new CliError(String(error), { code: EXIT.USAGE, action: "Create agenttwin.world.json; run agenttwin example --out ./agenttwin-example for a working example." });
|
|
890
1650
|
}
|
|
891
1651
|
if (spec.workflowId) {
|
|
892
1652
|
let inventory;
|
|
893
1653
|
try {
|
|
894
|
-
inventory = JSON.parse(await
|
|
1654
|
+
inventory = JSON.parse(await readFile8(join8(cwd, ".agenttwin/inventory.json"), "utf8"));
|
|
895
1655
|
} catch {
|
|
896
1656
|
throw new CliError("Linked workflow needs discovery inventory; run init first", { code: EXIT.BLOCKED });
|
|
897
1657
|
}
|
|
@@ -904,24 +1664,24 @@ async function runWorld(cwd, opts) {
|
|
|
904
1664
|
const runtime = opts.runtime ?? "local";
|
|
905
1665
|
if (!["local", "e2b", "hosted"].includes(runtime)) throw new CliError("runtime must be local, hosted or e2b", { code: EXIT.USAGE });
|
|
906
1666
|
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." });
|
|
907
|
-
const stage = await mkdtemp(
|
|
1667
|
+
const stage = await mkdtemp(join8(tmpdir(), "agenttwin-world-"));
|
|
908
1668
|
try {
|
|
909
|
-
const sourceDigests = await stageApplication(dirname(file), spec.files,
|
|
910
|
-
await writeFile4(
|
|
1669
|
+
const sourceDigests = await stageApplication(dirname(file), spec.files, join8(stage, "app"));
|
|
1670
|
+
await writeFile4(join8(stage, "world.json"), JSON.stringify(spec), { mode: 420 });
|
|
911
1671
|
const git = await captureGit(cwd);
|
|
912
1672
|
let project;
|
|
913
1673
|
try {
|
|
914
|
-
project = JSON.parse(await
|
|
1674
|
+
project = JSON.parse(await readFile8(join8(cwd, ".agenttwin/inventory.json"), "utf8")).name ?? basename2(cwd);
|
|
915
1675
|
} catch {
|
|
916
1676
|
}
|
|
917
1677
|
if (runtime === "local") {
|
|
918
1678
|
opts.log("Testing through the CLI with in-process service twins. No cloud VM or containers.");
|
|
919
|
-
const report2 = await executeLocal(
|
|
1679
|
+
const report2 = await executeLocal(join8(stage, "app"), spec, opts.signal);
|
|
920
1680
|
const runId2 = randomUUID();
|
|
921
1681
|
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" };
|
|
922
1682
|
const out2 = resolve(cwd, opts.out ?? `.agenttwin/runs/${runId2}`);
|
|
923
1683
|
await mkdir2(out2, { recursive: true });
|
|
924
|
-
const evidencePath =
|
|
1684
|
+
const evidencePath = join8(out2, "run.json"), reportPath = join8(out2, "report.md");
|
|
925
1685
|
await writeFile4(evidencePath, JSON.stringify(receipt2, null, 2) + "\n", { mode: 384 });
|
|
926
1686
|
await writeFile4(reportPath, `# ${spec.name}: ${receipt2.status}
|
|
927
1687
|
|
|
@@ -931,8 +1691,8 @@ Git: ${git?.head ?? "unavailable"}
|
|
|
931
1691
|
` + 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");
|
|
932
1692
|
return { ...receipt2, reportPath, evidencePath };
|
|
933
1693
|
}
|
|
934
|
-
const worker =
|
|
935
|
-
await copyFile(worker,
|
|
1694
|
+
const worker = join8(dirname(fileURLToPath2(import.meta.url)), "worker.js");
|
|
1695
|
+
await copyFile(worker, join8(stage, "worker.js"));
|
|
936
1696
|
if (runtime === "hosted") {
|
|
937
1697
|
const receipt2 = await executeRemote(stage, spec, opts.log, { git, project });
|
|
938
1698
|
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");
|
|
@@ -941,8 +1701,8 @@ Git: ${git?.head ?? "unavailable"}
|
|
|
941
1701
|
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");
|
|
942
1702
|
const out2 = resolve(cwd, opts.out ?? `.agenttwin/runs/${receipt2.runId}`);
|
|
943
1703
|
await mkdir2(out2, { recursive: true });
|
|
944
|
-
const evidencePath =
|
|
945
|
-
const reportPath =
|
|
1704
|
+
const evidencePath = join8(out2, "run.json");
|
|
1705
|
+
const reportPath = join8(out2, "report.md");
|
|
946
1706
|
await writeFile4(evidencePath, JSON.stringify(receipt2, null, 2) + "\n", { mode: 384 });
|
|
947
1707
|
await writeFile4(reportPath, `# ${spec.name}: ${receipt2.status}
|
|
948
1708
|
|
|
@@ -979,11 +1739,11 @@ See run.json for assertions, service state, application output and isolation met
|
|
|
979
1739
|
};
|
|
980
1740
|
const out = resolve(cwd, opts.out ?? `.agenttwin/runs/${runId}`);
|
|
981
1741
|
await mkdir2(out, { recursive: true });
|
|
982
|
-
await writeFile4(
|
|
983
|
-
await writeFile4(
|
|
1742
|
+
await writeFile4(join8(out, "run.json"), JSON.stringify(receipt, null, 2) + "\n", { mode: 384 });
|
|
1743
|
+
await writeFile4(join8(out, "world.json"), JSON.stringify(spec, null, 2) + "\n", { mode: 384 });
|
|
984
1744
|
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");
|
|
985
|
-
await writeFile4(
|
|
986
|
-
return { ...receipt, reportPath:
|
|
1745
|
+
await writeFile4(join8(out, "report.md"), summary);
|
|
1746
|
+
return { ...receipt, reportPath: join8(out, "report.md"), evidencePath: join8(out, "run.json") };
|
|
987
1747
|
} finally {
|
|
988
1748
|
await rm(stage, { recursive: true, force: true });
|
|
989
1749
|
}
|
|
@@ -1037,36 +1797,36 @@ function reviewPreparation(inventory, input) {
|
|
|
1037
1797
|
async function runPrepare(cwd, opts) {
|
|
1038
1798
|
if (!opts.file) throw new CliError("prepare requires --file preparation.json", { code: EXIT.USAGE });
|
|
1039
1799
|
const file = resolve2(cwd, opts.file);
|
|
1040
|
-
const inventory = JSON.parse(await
|
|
1800
|
+
const inventory = JSON.parse(await readFile9(join9(cwd, ".agenttwin/inventory.json"), "utf8"));
|
|
1041
1801
|
const discovered = await loadDashboardDiscovery(cwd, inventory);
|
|
1042
1802
|
const known = new Set(inventory.workflows.map((w) => w.id));
|
|
1043
1803
|
for (const w of discovered.workflows) if (!known.has(w.id)) {
|
|
1044
1804
|
inventory.workflows.push({ ...w, confidence: "low", apps: [], sharedEntities: [], evidence: [{ source: "agent-analysis", ref: ".agenttwin/agent-discovery.json" }], steps: w.steps.map((s) => ({ ...s, app: "", evidence: [] })), risk: "high", rank: 0, supported: false, hypothesis: true, linkingSignals: ["Coding-agent analysis requires user review"], scopeUncertain: true, verified: false });
|
|
1045
1805
|
}
|
|
1046
1806
|
for (const t of discovered.tools) if (!inventory.tools.some((tool) => tool.id === t.id)) throw new Error("Agent-discovered dependencies must be classified in init before preparation. Review the additional tool, then rerun init.");
|
|
1047
|
-
const { spec, workflow, mappings } = reviewPreparation(inventory, JSON.parse(await
|
|
1807
|
+
const { spec, workflow, mappings } = reviewPreparation(inventory, JSON.parse(await readFile9(file, "utf8")));
|
|
1048
1808
|
const directory = resolve2(cwd, opts.out ?? `.agenttwin/prepared/${spec.name}-${randomUUID2().slice(0, 8)}`);
|
|
1049
|
-
const stage = await mkdtemp2(
|
|
1809
|
+
const stage = await mkdtemp2(join9(tmpdir2(), "agenttwin-prepare-"));
|
|
1050
1810
|
try {
|
|
1051
1811
|
const sourceDigests = await stageApplication(dirname2(file), spec.files, stage);
|
|
1052
1812
|
const files = /* @__PURE__ */ Object.create(null);
|
|
1053
|
-
for (const name of spec.files) files[name] = (await
|
|
1813
|
+
for (const name of spec.files) files[name] = (await readFile9(join9(stage, name))).toString("base64");
|
|
1054
1814
|
const manifest = { project: discovered.name, git: await captureGit(cwd), schema: "agenttwin/prepared-v1", workflowId: workflow.id, invocation: spec.command, sourceDigests, specDigest: createHash2("sha256").update(JSON.stringify(spec)).digest("hex"), mappings, provenance: "user-reviewed invocation and operation contracts; discovery does not verify runtime fidelity", createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1055
|
-
await writeFile5(
|
|
1056
|
-
await writeFile5(
|
|
1057
|
-
await writeFile5(
|
|
1058
|
-
await mkdir3(
|
|
1059
|
-
await writeFile5(
|
|
1815
|
+
await writeFile5(join9(stage, "agenttwin.world.json"), JSON.stringify(spec, null, 2) + "\n", { mode: 384 });
|
|
1816
|
+
await writeFile5(join9(stage, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", { mode: 384 });
|
|
1817
|
+
await writeFile5(join9(stage, "agenttwin.bundle.json"), JSON.stringify({ schema: "agenttwin/upload-v1", spec, files, manifest }, null, 2) + "\n", { mode: 384 });
|
|
1818
|
+
await mkdir3(join9(stage, ".agenttwin"), { recursive: true });
|
|
1819
|
+
await writeFile5(join9(stage, ".agenttwin/inventory.json"), JSON.stringify({ name: discovered.name, workflows: [{ id: workflow.id }] }), { mode: 384 });
|
|
1060
1820
|
await mkdir3(dirname2(directory), { recursive: true });
|
|
1061
1821
|
await mkdir3(directory, { mode: 448 });
|
|
1062
1822
|
try {
|
|
1063
1823
|
const { cp: cp2, readdir: readdir3 } = await import("fs/promises");
|
|
1064
|
-
for (const entry of await readdir3(stage)) await cp2(
|
|
1824
|
+
for (const entry of await readdir3(stage)) await cp2(join9(stage, entry), join9(directory, entry), { recursive: true, force: false, errorOnExist: true });
|
|
1065
1825
|
} catch (error) {
|
|
1066
1826
|
await rm2(directory, { recursive: true, force: true });
|
|
1067
1827
|
throw error;
|
|
1068
1828
|
}
|
|
1069
|
-
return { directory, worldPath:
|
|
1829
|
+
return { directory, worldPath: join9(directory, "agenttwin.world.json"), bundlePath: join9(directory, "agenttwin.bundle.json"), manifest };
|
|
1070
1830
|
} finally {
|
|
1071
1831
|
await rm2(stage, { recursive: true, force: true });
|
|
1072
1832
|
}
|
|
@@ -1076,7 +1836,7 @@ async function runPrepare(cwd, opts) {
|
|
|
1076
1836
|
import { Template } from "e2b";
|
|
1077
1837
|
import { mkdir as mkdir4, writeFile as writeFile6 } from "fs/promises";
|
|
1078
1838
|
import { homedir as homedir2 } from "os";
|
|
1079
|
-
import { join as
|
|
1839
|
+
import { join as join10 } from "path";
|
|
1080
1840
|
async function runSetup(log) {
|
|
1081
1841
|
if (!process.env.E2B_API_KEY) throw new CliError("Set E2B_API_KEY before creating your hosted runtime template", { code: EXIT.BLOCKED });
|
|
1082
1842
|
log("Building the AgentTwin template on E2B (Node 22, PostgreSQL 15, Redis). No local containers are used.");
|
|
@@ -1084,16 +1844,16 @@ async function runSetup(log) {
|
|
|
1084
1844
|
const result = await Template.build(template, "agenttwin-node-v2", { apiKey: process.env.E2B_API_KEY, cpuCount: 2, memoryMB: 2048, onBuildLogs: (entry) => {
|
|
1085
1845
|
if (!entry.message.includes("[stdout]") && !entry.message.includes("[stderr]")) log(entry.message);
|
|
1086
1846
|
} });
|
|
1087
|
-
const directory =
|
|
1847
|
+
const directory = join10(homedir2(), ".agenttwin");
|
|
1088
1848
|
await mkdir4(directory, { recursive: true, mode: 448 });
|
|
1089
|
-
const path =
|
|
1849
|
+
const path = join10(directory, "runtime.json");
|
|
1090
1850
|
await writeFile6(path, JSON.stringify({ provider: "e2b", templateId: result.templateId, buildId: result.buildId }, null, 2) + "\n", { mode: 384 });
|
|
1091
1851
|
return { provider: "e2b", templateId: result.templateId, buildId: result.buildId, configPath: path };
|
|
1092
1852
|
}
|
|
1093
1853
|
|
|
1094
1854
|
// src/commands/example.ts
|
|
1095
|
-
import { mkdir as mkdir5, writeFile as writeFile7, stat } from "fs/promises";
|
|
1096
|
-
import { resolve as resolve3, join as
|
|
1855
|
+
import { mkdir as mkdir5, writeFile as writeFile7, stat as stat2 } from "fs/promises";
|
|
1856
|
+
import { resolve as resolve3, join as join11 } from "path";
|
|
1097
1857
|
var exampleSource = `// Replace this file with your own bundled Node workflow.
|
|
1098
1858
|
const safe = process.argv.includes('--safe');
|
|
1099
1859
|
const crm = process.env.AGENTTWIN_CRM_URL;
|
|
@@ -1136,18 +1896,18 @@ function exampleWorld(safe = false) {
|
|
|
1136
1896
|
}
|
|
1137
1897
|
async function runExample(cwd, out) {
|
|
1138
1898
|
const directory = resolve3(cwd, out ?? "agenttwin-example");
|
|
1139
|
-
if (await
|
|
1899
|
+
if (await stat2(directory).then(() => true, () => false)) throw new CliError(`Destination exists: ${directory}`, { code: EXIT.USAGE, action: "Choose a new --out directory." });
|
|
1140
1900
|
await mkdir5(directory, { recursive: true });
|
|
1141
|
-
await writeFile7(
|
|
1142
|
-
await writeFile7(
|
|
1143
|
-
await writeFile7(
|
|
1144
|
-
await writeFile7(
|
|
1901
|
+
await writeFile7(join11(directory, "workflow.mjs"), exampleSource);
|
|
1902
|
+
await writeFile7(join11(directory, "agenttwin.world.json"), JSON.stringify(exampleWorld(), null, 2) + "\n");
|
|
1903
|
+
await writeFile7(join11(directory, "safe.world.json"), JSON.stringify(exampleWorld(true), null, 2) + "\n");
|
|
1904
|
+
await writeFile7(join11(directory, "README.md"), `# Support workflow
|
|
1145
1905
|
|
|
1146
1906
|
Run with agenttwin run --file agenttwin.world.json (expected FAIL), then --file safe.world.json (expected PASS). Runs through the CLI with in-process service twins by default. No VM or containers. Use --runtime hosted or --runtime e2b explicitly only when a cloud machine is needed.
|
|
1147
1907
|
|
|
1148
1908
|
This is actual application code using a synthetic custom ticket API plus the supported Slack JSON subset. It is not a clone of a CRM vendor. Replace workflow.mjs with your own built code and edit the explicit file list, endpoints, seed records and assertions. No npm install runs in the sandbox; dependencies must be bundled into your application.
|
|
1149
1909
|
`);
|
|
1150
|
-
return { directory, worldPath:
|
|
1910
|
+
return { directory, worldPath: join11(directory, "agenttwin.world.json") };
|
|
1151
1911
|
}
|
|
1152
1912
|
|
|
1153
1913
|
// src/commands/init.ts
|
|
@@ -1155,152 +1915,8 @@ import { readFileSync as readFileSync2 } from "fs";
|
|
|
1155
1915
|
import { writeFile as writeFile11 } from "fs/promises";
|
|
1156
1916
|
import { join as join17 } from "path";
|
|
1157
1917
|
|
|
1158
|
-
// src/
|
|
1159
|
-
|
|
1160
|
-
import { join as join11, relative as relative2, sep as sep2 } from "path";
|
|
1161
|
-
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
1162
|
-
".git",
|
|
1163
|
-
".agenttwin",
|
|
1164
|
-
"node_modules",
|
|
1165
|
-
"dist",
|
|
1166
|
-
"build",
|
|
1167
|
-
"out",
|
|
1168
|
-
"coverage",
|
|
1169
|
-
".next",
|
|
1170
|
-
".nuxt",
|
|
1171
|
-
".venv",
|
|
1172
|
-
"venv",
|
|
1173
|
-
"env",
|
|
1174
|
-
"site-packages",
|
|
1175
|
-
"__pycache__",
|
|
1176
|
-
".pytest_cache",
|
|
1177
|
-
".mypy_cache",
|
|
1178
|
-
".ruff_cache",
|
|
1179
|
-
".tox",
|
|
1180
|
-
"vendor",
|
|
1181
|
-
"target",
|
|
1182
|
-
".turbo",
|
|
1183
|
-
".cache",
|
|
1184
|
-
".idea",
|
|
1185
|
-
".vscode",
|
|
1186
|
-
".terraform",
|
|
1187
|
-
"models",
|
|
1188
|
-
"data",
|
|
1189
|
-
"datasets",
|
|
1190
|
-
"media",
|
|
1191
|
-
"uploads",
|
|
1192
|
-
"tmp",
|
|
1193
|
-
"logs",
|
|
1194
|
-
"gen"
|
|
1195
|
-
]);
|
|
1196
|
-
var MAX_FILES = 2e4;
|
|
1197
|
-
var MAX_BYTES = 1e6;
|
|
1198
|
-
var WALK_BUDGET_MS = 8e3;
|
|
1199
|
-
function shouldIgnoreDir(name) {
|
|
1200
|
-
return IGNORED_DIRS.has(name) || name === ".git";
|
|
1201
|
-
}
|
|
1202
|
-
async function walkDir(root, maxFiles, budgetMs) {
|
|
1203
|
-
const out = [];
|
|
1204
|
-
const limits = [];
|
|
1205
|
-
const stack = [root];
|
|
1206
|
-
const started = Date.now();
|
|
1207
|
-
while (stack.length > 0 && out.length < maxFiles && Date.now() - started < budgetMs) {
|
|
1208
|
-
const dir = stack.pop();
|
|
1209
|
-
let entries;
|
|
1210
|
-
try {
|
|
1211
|
-
entries = await readdir2(dir, { withFileTypes: true });
|
|
1212
|
-
} catch {
|
|
1213
|
-
limits.push(`Directory could not be read: ${relative2(root, dir) || "."}`);
|
|
1214
|
-
continue;
|
|
1215
|
-
}
|
|
1216
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1217
|
-
for (const entry of entries) {
|
|
1218
|
-
const full = join11(dir, entry.name);
|
|
1219
|
-
if (entry.isDirectory()) {
|
|
1220
|
-
if (!shouldIgnoreDir(entry.name)) stack.push(full);
|
|
1221
|
-
} else if (entry.isFile()) {
|
|
1222
|
-
out.push(relative2(root, full).split(sep2).join("/"));
|
|
1223
|
-
if (out.length >= maxFiles) break;
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
if (out.length >= maxFiles) limits.push(`File scan limit reached (${maxFiles} files); additional files may be unscanned`);
|
|
1228
|
-
if (stack.length && Date.now() - started >= budgetMs) limits.push(`File walk time budget reached (${budgetMs}ms); directories remain unscanned`);
|
|
1229
|
-
return { files: out.sort(), limits };
|
|
1230
|
-
}
|
|
1231
|
-
var RepoFileIndex = class {
|
|
1232
|
-
cwd;
|
|
1233
|
-
cache = { contents: /* @__PURE__ */ new Map(), limits: [] };
|
|
1234
|
-
bounds;
|
|
1235
|
-
constructor(cwd, bounds = { maxFiles: MAX_FILES, budgetMs: WALK_BUDGET_MS }) {
|
|
1236
|
-
this.cwd = cwd;
|
|
1237
|
-
this.bounds = bounds;
|
|
1238
|
-
}
|
|
1239
|
-
async walk() {
|
|
1240
|
-
if (!this.cache.files) {
|
|
1241
|
-
const result = await walkDir(this.cwd, this.bounds.maxFiles, this.bounds.budgetMs);
|
|
1242
|
-
this.cache.files = result.files;
|
|
1243
|
-
this.cache.limits.push(...result.limits);
|
|
1244
|
-
}
|
|
1245
|
-
return this.cache.files;
|
|
1246
|
-
}
|
|
1247
|
-
scanLimits() {
|
|
1248
|
-
return [...new Set(this.cache.limits)].sort();
|
|
1249
|
-
}
|
|
1250
|
-
async exists(path) {
|
|
1251
|
-
try {
|
|
1252
|
-
await stat2(join11(this.cwd, path));
|
|
1253
|
-
return true;
|
|
1254
|
-
} catch {
|
|
1255
|
-
return false;
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
async read(path, maxBytes = MAX_BYTES) {
|
|
1259
|
-
const cached = this.cache.contents.get(path);
|
|
1260
|
-
if (cached !== void 0) return cached;
|
|
1261
|
-
const full = join11(this.cwd, path);
|
|
1262
|
-
try {
|
|
1263
|
-
const info2 = await stat2(full);
|
|
1264
|
-
if (!info2.isFile()) return void 0;
|
|
1265
|
-
if (info2.size > maxBytes) {
|
|
1266
|
-
this.cache.limits.push(`File excluded by size limit (${maxBytes} bytes): ${path}`);
|
|
1267
|
-
return void 0;
|
|
1268
|
-
}
|
|
1269
|
-
const text = await readFile9(full, "utf8");
|
|
1270
|
-
this.cache.contents.set(path, text);
|
|
1271
|
-
return text;
|
|
1272
|
-
} catch {
|
|
1273
|
-
return void 0;
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
async byExt(...exts) {
|
|
1277
|
-
const wanted = new Set(exts.map((e) => e.replace(/^\./, "").toLowerCase()));
|
|
1278
|
-
const files = await this.walk();
|
|
1279
|
-
return files.filter((f) => {
|
|
1280
|
-
const dot = f.lastIndexOf(".");
|
|
1281
|
-
if (dot < 0) return false;
|
|
1282
|
-
return wanted.has(f.slice(dot + 1).toLowerCase());
|
|
1283
|
-
});
|
|
1284
|
-
}
|
|
1285
|
-
async byName(pattern) {
|
|
1286
|
-
const files = await this.walk();
|
|
1287
|
-
return files.filter((f) => pattern.test(f));
|
|
1288
|
-
}
|
|
1289
|
-
line(ref) {
|
|
1290
|
-
const idx = ref.lastIndexOf(":");
|
|
1291
|
-
if (idx < 0) return void 0;
|
|
1292
|
-
const path = ref.slice(0, idx);
|
|
1293
|
-
const num = Number(ref.slice(idx + 1));
|
|
1294
|
-
if (!Number.isInteger(num)) return void 0;
|
|
1295
|
-
const text = this.cache.contents.get(path);
|
|
1296
|
-
if (text === void 0) return void 0;
|
|
1297
|
-
const lines = text.split(/\r?\n/);
|
|
1298
|
-
return lines[num - 1];
|
|
1299
|
-
}
|
|
1300
|
-
};
|
|
1301
|
-
async function createFileIndex(cwd) {
|
|
1302
|
-
return new RepoFileIndex(cwd);
|
|
1303
|
-
}
|
|
1918
|
+
// src/discovery/engine.ts
|
|
1919
|
+
init_files();
|
|
1304
1920
|
|
|
1305
1921
|
// src/discovery/scoring.ts
|
|
1306
1922
|
var WEIGHTS = {
|
|
@@ -1856,9 +2472,9 @@ var iacDetector = {
|
|
|
1856
2472
|
var SOURCE_EXTS = ["ts", "tsx", "js", "jsx", "mjs", "cjs", "py", "go", "java", "kt", "rb", "php", "cs"];
|
|
1857
2473
|
var MAX_FILES2 = 2500;
|
|
1858
2474
|
var MAX_EVIDENCE = 6;
|
|
1859
|
-
var NON_USAGE_DIR = /(^|\/)(docs?|examples?|demos?|samples?|fixtures?|tests?|__tests__|__mocks__|spec|specs|e2e|storybook|public|static|assets|generated|dist|build|coverage)(\/|$)/i;
|
|
2475
|
+
var NON_USAGE_DIR = /(^|\/)(artifacts|testbed|docs?|examples?|demos?|samples?|fixtures?|tests?|__tests__|__mocks__|spec|specs|e2e|storybook|public|static|assets|generated|dist|build|coverage)(\/|$)/i;
|
|
1860
2476
|
function isNonUsagePath(path) {
|
|
1861
|
-
return NON_USAGE_DIR.test(path);
|
|
2477
|
+
return NON_USAGE_DIR.test(path) || /(?:\.(?:test|spec)\.[^/]+$|(?:demo|test)?fixtures?\.[^/]+$)/i.test(path);
|
|
1862
2478
|
}
|
|
1863
2479
|
function safeRegex(source) {
|
|
1864
2480
|
try {
|
|
@@ -2024,7 +2640,7 @@ var envVarDetector = {
|
|
|
2024
2640
|
};
|
|
2025
2641
|
|
|
2026
2642
|
// src/discovery/detectors/ci.ts
|
|
2027
|
-
var
|
|
2643
|
+
var PROVIDERS2 = [
|
|
2028
2644
|
{ re: /(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/, id: "github-actions", title: "GitHub Actions" },
|
|
2029
2645
|
{ re: /(^|\/)\.gitlab-ci\.ya?ml$/, id: "gitlab-ci", title: "GitLab CI" },
|
|
2030
2646
|
{ re: /(^|\/)\.circleci\/config\.ya?ml$/, id: "circleci", title: "CircleCI" },
|
|
@@ -2040,7 +2656,7 @@ var ciDetector = {
|
|
|
2040
2656
|
const all = await ctx.files.walk();
|
|
2041
2657
|
const findings = [];
|
|
2042
2658
|
const providerEvidence = /* @__PURE__ */ new Map();
|
|
2043
|
-
for (const system of
|
|
2659
|
+
for (const system of PROVIDERS2) {
|
|
2044
2660
|
const matches = all.filter((f) => system.re.test(f));
|
|
2045
2661
|
if (matches.length === 0) continue;
|
|
2046
2662
|
const evidence = matches.slice(0, 3).map((ref) => ({
|
|
@@ -2182,487 +2798,8 @@ function registerDetectors() {
|
|
|
2182
2798
|
];
|
|
2183
2799
|
}
|
|
2184
2800
|
|
|
2185
|
-
// src/discovery/
|
|
2186
|
-
|
|
2187
|
-
var TOKENISH2 = /\b(sk|rk|pk|ghp|gho|ghs|xox[baprs]|AKIA|AIza)[A-Za-z0-9_-]{6,}\b/g;
|
|
2188
|
-
function maskSecret(value) {
|
|
2189
|
-
return value.replace(TOKENISH2, (match) => `${match.slice(0, 4)}***`);
|
|
2190
|
-
}
|
|
2191
|
-
function maskEmail(value) {
|
|
2192
|
-
return value.replace(EMAIL2, "$1***$2");
|
|
2193
|
-
}
|
|
2194
|
-
function maskUrl(value) {
|
|
2195
|
-
try {
|
|
2196
|
-
const url = new URL(value);
|
|
2197
|
-
return `${url.protocol}//${url.host}${url.pathname}`;
|
|
2198
|
-
} catch {
|
|
2199
|
-
return maskSecret(value);
|
|
2200
|
-
}
|
|
2201
|
-
}
|
|
2202
|
-
function redactDetail(value) {
|
|
2203
|
-
return maskSecret(maskEmail(value)).slice(0, 200);
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
// src/discovery/probes/providers/stripe.ts
|
|
2207
|
-
var stripeProbe = {
|
|
2208
|
-
id: "stripe.readonly",
|
|
2209
|
-
providerId: "stripe",
|
|
2210
|
-
title: "Enumerate Stripe account configuration (read-only)",
|
|
2211
|
-
description: "Reads products, prices and webhook endpoints. Read-only; issues no writes.",
|
|
2212
|
-
readScope: {
|
|
2213
|
-
endpoints: [
|
|
2214
|
-
"GET /v1/products",
|
|
2215
|
-
"GET /v1/prices",
|
|
2216
|
-
"GET /v1/webhook_endpoints"
|
|
2217
|
-
],
|
|
2218
|
-
methods: ["GET"]
|
|
2219
|
-
},
|
|
2220
|
-
credentials: [
|
|
2221
|
-
{
|
|
2222
|
-
envVar: "STRIPE_SECRET_KEY",
|
|
2223
|
-
label: "Stripe restricted key (read scopes)",
|
|
2224
|
-
required: true,
|
|
2225
|
-
scopes: ["products:read", "prices:read", "webhook_endpoints:read"],
|
|
2226
|
-
hint: "Prefer a restricted key (rk_...) with read scopes over a full sk_live_ key."
|
|
2227
|
-
}
|
|
2228
|
-
],
|
|
2229
|
-
async run(ctx) {
|
|
2230
|
-
const spec = this.credentials[0];
|
|
2231
|
-
const key = ctx.credential(spec);
|
|
2232
|
-
if (!key) {
|
|
2233
|
-
return {
|
|
2234
|
-
providerId: this.providerId,
|
|
2235
|
-
ok: false,
|
|
2236
|
-
summary: "no credential present",
|
|
2237
|
-
resources: [],
|
|
2238
|
-
evidence: []
|
|
2239
|
-
};
|
|
2240
|
-
}
|
|
2241
|
-
const get = async (path) => {
|
|
2242
|
-
const response = await ctx.fetch(`https://api.stripe.com${path}`, {
|
|
2243
|
-
method: "GET",
|
|
2244
|
-
headers: { Authorization: `Bearer ${key}` },
|
|
2245
|
-
signal: ctx.signal
|
|
2246
|
-
});
|
|
2247
|
-
if (!response.ok) {
|
|
2248
|
-
throw new Error(`Stripe ${path} -> HTTP ${response.status}`);
|
|
2249
|
-
}
|
|
2250
|
-
return await response.json();
|
|
2251
|
-
};
|
|
2252
|
-
try {
|
|
2253
|
-
const [products, prices, hooks] = await Promise.all([
|
|
2254
|
-
get("/v1/products?limit=100"),
|
|
2255
|
-
get("/v1/prices?limit=100"),
|
|
2256
|
-
get("/v1/webhook_endpoints?limit=100")
|
|
2257
|
-
]);
|
|
2258
|
-
return {
|
|
2259
|
-
providerId: this.providerId,
|
|
2260
|
-
ok: true,
|
|
2261
|
-
summary: `${products.data.length} products, ${prices.data.length} prices, ${hooks.data.length} webhook endpoints`,
|
|
2262
|
-
resources: [
|
|
2263
|
-
{
|
|
2264
|
-
kind: "products",
|
|
2265
|
-
count: products.data.length,
|
|
2266
|
-
sample: products.data.slice(0, 5).map((p) => ({
|
|
2267
|
-
id: p.id,
|
|
2268
|
-
name: p.name,
|
|
2269
|
-
active: p.active
|
|
2270
|
-
}))
|
|
2271
|
-
},
|
|
2272
|
-
{ kind: "prices", count: prices.data.length },
|
|
2273
|
-
{
|
|
2274
|
-
kind: "webhook_endpoints",
|
|
2275
|
-
count: hooks.data.length,
|
|
2276
|
-
sample: hooks.data.slice(0, 5).map((h2) => ({
|
|
2277
|
-
url: typeof h2.url === "string" ? maskUrl(h2.url) : void 0,
|
|
2278
|
-
status: h2.status
|
|
2279
|
-
}))
|
|
2280
|
-
}
|
|
2281
|
-
],
|
|
2282
|
-
evidence: [
|
|
2283
|
-
{
|
|
2284
|
-
ref: "probe:stripe",
|
|
2285
|
-
pass: "probe",
|
|
2286
|
-
signal: "probe",
|
|
2287
|
-
detail: "account reachable via read scopes"
|
|
2288
|
-
}
|
|
2289
|
-
]
|
|
2290
|
-
};
|
|
2291
|
-
} catch (error) {
|
|
2292
|
-
return {
|
|
2293
|
-
providerId: this.providerId,
|
|
2294
|
-
ok: false,
|
|
2295
|
-
error: { message: error instanceof Error ? error.message : String(error) },
|
|
2296
|
-
summary: "probe failed",
|
|
2297
|
-
resources: [],
|
|
2298
|
-
evidence: []
|
|
2299
|
-
};
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
};
|
|
2303
|
-
|
|
2304
|
-
// src/discovery/probes/providers/github.ts
|
|
2305
|
-
var githubProbe = {
|
|
2306
|
-
id: "github.readonly",
|
|
2307
|
-
providerId: "github",
|
|
2308
|
-
title: "Enumerate GitHub account (read-only)",
|
|
2309
|
-
description: "Reads the authenticated user and their repositories. Read-only.",
|
|
2310
|
-
readScope: {
|
|
2311
|
-
endpoints: ["GET /user", "GET /user/repos"],
|
|
2312
|
-
methods: ["GET"]
|
|
2313
|
-
},
|
|
2314
|
-
credentials: [
|
|
2315
|
-
{
|
|
2316
|
-
envVar: "GITHUB_TOKEN",
|
|
2317
|
-
label: "GitHub token (read:user, repo:read)",
|
|
2318
|
-
required: true,
|
|
2319
|
-
scopes: ["read:user", "public_repo"],
|
|
2320
|
-
hint: "A fine-grained token with read-only repository access is sufficient."
|
|
2321
|
-
}
|
|
2322
|
-
],
|
|
2323
|
-
async run(ctx) {
|
|
2324
|
-
const key = ctx.credential(this.credentials[0]);
|
|
2325
|
-
if (!key) {
|
|
2326
|
-
return {
|
|
2327
|
-
providerId: this.providerId,
|
|
2328
|
-
ok: false,
|
|
2329
|
-
summary: "no credential present",
|
|
2330
|
-
resources: [],
|
|
2331
|
-
evidence: []
|
|
2332
|
-
};
|
|
2333
|
-
}
|
|
2334
|
-
const headers = {
|
|
2335
|
-
Authorization: `Bearer ${key}`,
|
|
2336
|
-
Accept: "application/vnd.github+json",
|
|
2337
|
-
"User-Agent": "agenttwin-discovery"
|
|
2338
|
-
};
|
|
2339
|
-
try {
|
|
2340
|
-
const userResponse = await ctx.fetch("https://api.github.com/user", {
|
|
2341
|
-
method: "GET",
|
|
2342
|
-
headers,
|
|
2343
|
-
signal: ctx.signal
|
|
2344
|
-
});
|
|
2345
|
-
if (!userResponse.ok) {
|
|
2346
|
-
throw new Error(`GitHub /user -> HTTP ${userResponse.status}`);
|
|
2347
|
-
}
|
|
2348
|
-
const user = await userResponse.json();
|
|
2349
|
-
const reposResponse = await ctx.fetch(
|
|
2350
|
-
"https://api.github.com/user/repos?per_page=100&sort=updated",
|
|
2351
|
-
{ method: "GET", headers, signal: ctx.signal }
|
|
2352
|
-
);
|
|
2353
|
-
const repos = reposResponse.ok ? await reposResponse.json() : [];
|
|
2354
|
-
return {
|
|
2355
|
-
providerId: this.providerId,
|
|
2356
|
-
ok: true,
|
|
2357
|
-
summary: `${repos.length} repositories readable for @${user.login ?? "unknown"}`,
|
|
2358
|
-
resources: [
|
|
2359
|
-
{
|
|
2360
|
-
kind: "repositories",
|
|
2361
|
-
count: repos.length,
|
|
2362
|
-
sample: repos.slice(0, 5).map((r) => ({
|
|
2363
|
-
name: r.name,
|
|
2364
|
-
private: r.private,
|
|
2365
|
-
archived: r.archived
|
|
2366
|
-
}))
|
|
2367
|
-
}
|
|
2368
|
-
],
|
|
2369
|
-
evidence: [
|
|
2370
|
-
{
|
|
2371
|
-
ref: "probe:github",
|
|
2372
|
-
pass: "probe",
|
|
2373
|
-
signal: "probe",
|
|
2374
|
-
detail: "account reachable via read scopes"
|
|
2375
|
-
}
|
|
2376
|
-
]
|
|
2377
|
-
};
|
|
2378
|
-
} catch (error) {
|
|
2379
|
-
return {
|
|
2380
|
-
providerId: this.providerId,
|
|
2381
|
-
ok: false,
|
|
2382
|
-
error: { message: error instanceof Error ? error.message : String(error) },
|
|
2383
|
-
summary: "probe failed",
|
|
2384
|
-
resources: [],
|
|
2385
|
-
evidence: []
|
|
2386
|
-
};
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
};
|
|
2390
|
-
|
|
2391
|
-
// src/discovery/providers/catalog.ts
|
|
2392
|
-
function planned(unknown) {
|
|
2393
|
-
return { supported: [], verified: [], unknown, implemented: false };
|
|
2394
|
-
}
|
|
2395
|
-
var NO_TWIN = ["all behavior (no twin implemented)"];
|
|
2396
|
-
var PROVIDERS2 = [
|
|
2397
|
-
{
|
|
2398
|
-
id: "stripe",
|
|
2399
|
-
title: "Stripe",
|
|
2400
|
-
category: "payments",
|
|
2401
|
-
twin: planned(["refunds", "payouts", "disputes", "webhook ordering"]),
|
|
2402
|
-
probe: stripeProbe,
|
|
2403
|
-
signatures: {
|
|
2404
|
-
packages: {
|
|
2405
|
-
node: ["stripe"],
|
|
2406
|
-
python: ["stripe"],
|
|
2407
|
-
go: ["stripe-go"],
|
|
2408
|
-
java: ["stripe-java"],
|
|
2409
|
-
ruby: ["stripe"],
|
|
2410
|
-
php: ["stripe/stripe-php"],
|
|
2411
|
-
dotnet: ["Stripe.net"]
|
|
2412
|
-
},
|
|
2413
|
-
imports: ["new Stripe\\(", `from ["']stripe["']`, `require\\(['"]stripe['"]\\)`, "stripe-go"],
|
|
2414
|
-
hosts: ["api.stripe.com"],
|
|
2415
|
-
envPrefixes: ["STRIPE_"],
|
|
2416
|
-
webhookPaths: ["/webhooks/stripe", "/stripe/webhook", "/webhook/stripe"]
|
|
2417
|
-
},
|
|
2418
|
-
docs: "https://stripe.com/docs/api"
|
|
2419
|
-
},
|
|
2420
|
-
{
|
|
2421
|
-
id: "slack",
|
|
2422
|
-
title: "Slack",
|
|
2423
|
-
category: "comms",
|
|
2424
|
-
twin: planned(NO_TWIN),
|
|
2425
|
-
signatures: {
|
|
2426
|
-
packages: {
|
|
2427
|
-
node: ["@slack/web-api", "@slack/bolt", "@slack/events-api", "@slack/socket-mode"],
|
|
2428
|
-
python: ["slack_sdk", "slack-bolt"],
|
|
2429
|
-
go: ["slack-go"],
|
|
2430
|
-
java: ["slack-java-sdk"],
|
|
2431
|
-
ruby: ["slack-ruby-client"]
|
|
2432
|
-
},
|
|
2433
|
-
imports: ["@slack/web-api", "WebClient\\(", "slack_sdk"],
|
|
2434
|
-
hosts: ["slack.com", "api.slack.com", "hooks.slack.com"],
|
|
2435
|
-
envPrefixes: ["SLACK_"]
|
|
2436
|
-
}
|
|
2437
|
-
},
|
|
2438
|
-
{
|
|
2439
|
-
id: "github",
|
|
2440
|
-
title: "GitHub",
|
|
2441
|
-
category: "vcs",
|
|
2442
|
-
twin: planned(NO_TWIN),
|
|
2443
|
-
probe: githubProbe,
|
|
2444
|
-
signatures: {
|
|
2445
|
-
packages: {
|
|
2446
|
-
node: ["@octokit/rest", "@octokit/core", "@octokit/auth", "octokit"],
|
|
2447
|
-
python: ["PyGithub", "github"],
|
|
2448
|
-
go: ["google/go-github"],
|
|
2449
|
-
java: ["github-api"],
|
|
2450
|
-
ruby: ["octokit"]
|
|
2451
|
-
},
|
|
2452
|
-
imports: ["Octokit", "@octokit/", "PyGithub"],
|
|
2453
|
-
hosts: ["api.github.com"],
|
|
2454
|
-
envPrefixes: ["GITHUB_", "GH_"],
|
|
2455
|
-
webhookPaths: ["/webhooks/github", "/github/webhook"]
|
|
2456
|
-
}
|
|
2457
|
-
},
|
|
2458
|
-
{
|
|
2459
|
-
id: "aws",
|
|
2460
|
-
title: "AWS",
|
|
2461
|
-
category: "cloud",
|
|
2462
|
-
twin: planned(["service coverage (LocalStack passthrough planned)"]),
|
|
2463
|
-
envWeak: true,
|
|
2464
|
-
signatures: {
|
|
2465
|
-
packages: {
|
|
2466
|
-
node: ["@aws-sdk/", "aws-sdk"],
|
|
2467
|
-
python: ["boto3", "botocore"],
|
|
2468
|
-
go: ["aws-sdk-go", "aws-sdk-go-v2"],
|
|
2469
|
-
java: ["software.amazon.awssdk", "aws-java-sdk"],
|
|
2470
|
-
ruby: ["aws-sdk-"],
|
|
2471
|
-
php: ["aws/aws-sdk-php"],
|
|
2472
|
-
dotnet: ["AWSSDK."]
|
|
2473
|
-
},
|
|
2474
|
-
imports: ["@aws-sdk/", "boto3", "aws-sdk-go"],
|
|
2475
|
-
hosts: ["amazonaws.com"],
|
|
2476
|
-
envPrefixes: ["AWS_"]
|
|
2477
|
-
}
|
|
2478
|
-
},
|
|
2479
|
-
{
|
|
2480
|
-
id: "google",
|
|
2481
|
-
title: "Google (Gmail / Calendar / Workspace)",
|
|
2482
|
-
category: "comms",
|
|
2483
|
-
twin: planned(NO_TWIN),
|
|
2484
|
-
envWeak: true,
|
|
2485
|
-
signatures: {
|
|
2486
|
-
packages: {
|
|
2487
|
-
node: ["googleapis", "google-auth-library"],
|
|
2488
|
-
python: ["google-api-python-client", "google-cloud", "google-auth"],
|
|
2489
|
-
go: ["google.golang.org/api"],
|
|
2490
|
-
java: ["google-api-client"],
|
|
2491
|
-
ruby: ["google-apis-"]
|
|
2492
|
-
},
|
|
2493
|
-
imports: ["googleapis", "google-api-python-client", "google.golang.org/api"],
|
|
2494
|
-
hosts: ["googleapis.com", "gmail.googleapis.com", "www.googleapis.com"],
|
|
2495
|
-
envPrefixes: ["GOOGLE_", "GCP_", "GMAIL_"]
|
|
2496
|
-
}
|
|
2497
|
-
},
|
|
2498
|
-
{
|
|
2499
|
-
id: "openai",
|
|
2500
|
-
title: "OpenAI",
|
|
2501
|
-
category: "ai",
|
|
2502
|
-
twin: planned(NO_TWIN),
|
|
2503
|
-
signatures: {
|
|
2504
|
-
packages: {
|
|
2505
|
-
node: ["openai"],
|
|
2506
|
-
python: ["openai"],
|
|
2507
|
-
go: ["go-openai"],
|
|
2508
|
-
ruby: ["ruby-openai"]
|
|
2509
|
-
},
|
|
2510
|
-
imports: ["OpenAI\\(", `from ["']openai["']`, "go-openai"],
|
|
2511
|
-
hosts: ["api.openai.com"],
|
|
2512
|
-
envPrefixes: ["OPENAI_"]
|
|
2513
|
-
}
|
|
2514
|
-
},
|
|
2515
|
-
{
|
|
2516
|
-
id: "anthropic",
|
|
2517
|
-
title: "Anthropic",
|
|
2518
|
-
category: "ai",
|
|
2519
|
-
twin: planned(NO_TWIN),
|
|
2520
|
-
signatures: {
|
|
2521
|
-
packages: {
|
|
2522
|
-
node: ["@anthropic-ai/sdk"],
|
|
2523
|
-
python: ["anthropic"],
|
|
2524
|
-
go: ["anthropic-sdk-go"]
|
|
2525
|
-
},
|
|
2526
|
-
imports: ["@anthropic-ai/sdk", "Anthropic\\("],
|
|
2527
|
-
hosts: ["api.anthropic.com"],
|
|
2528
|
-
envPrefixes: ["ANTHROPIC_"]
|
|
2529
|
-
}
|
|
2530
|
-
},
|
|
2531
|
-
{
|
|
2532
|
-
id: "shopify",
|
|
2533
|
-
title: "Shopify",
|
|
2534
|
-
category: "commerce",
|
|
2535
|
-
twin: planned(NO_TWIN),
|
|
2536
|
-
signatures: {
|
|
2537
|
-
packages: {
|
|
2538
|
-
node: ["@shopify/", "shopify-api"],
|
|
2539
|
-
python: ["ShopifyAPI"],
|
|
2540
|
-
ruby: ["shopify_api"]
|
|
2541
|
-
},
|
|
2542
|
-
imports: ["@shopify/", "ShopifyAPI"],
|
|
2543
|
-
hosts: ["myshopify.com", "shopify.com"],
|
|
2544
|
-
envPrefixes: ["SHOPIFY_"]
|
|
2545
|
-
}
|
|
2546
|
-
},
|
|
2547
|
-
{
|
|
2548
|
-
id: "twilio",
|
|
2549
|
-
title: "Twilio",
|
|
2550
|
-
category: "comms",
|
|
2551
|
-
twin: planned(NO_TWIN),
|
|
2552
|
-
signatures: {
|
|
2553
|
-
packages: {
|
|
2554
|
-
node: ["twilio"],
|
|
2555
|
-
python: ["twilio"],
|
|
2556
|
-
java: ["twilio"],
|
|
2557
|
-
ruby: ["twilio-ruby"],
|
|
2558
|
-
php: ["twilio/sdk"]
|
|
2559
|
-
},
|
|
2560
|
-
imports: ["twilio"],
|
|
2561
|
-
hosts: ["api.twilio.com"],
|
|
2562
|
-
envPrefixes: ["TWILIO_"]
|
|
2563
|
-
}
|
|
2564
|
-
},
|
|
2565
|
-
{
|
|
2566
|
-
id: "sendgrid",
|
|
2567
|
-
title: "SendGrid",
|
|
2568
|
-
category: "comms",
|
|
2569
|
-
twin: planned(NO_TWIN),
|
|
2570
|
-
signatures: {
|
|
2571
|
-
packages: {
|
|
2572
|
-
node: ["@sendgrid/mail", "@sendgrid/client"],
|
|
2573
|
-
python: ["sendgrid"],
|
|
2574
|
-
ruby: ["sendgrid-ruby"]
|
|
2575
|
-
},
|
|
2576
|
-
imports: ["@sendgrid/", "sendgrid"],
|
|
2577
|
-
hosts: ["api.sendgrid.com"],
|
|
2578
|
-
envPrefixes: ["SENDGRID_"]
|
|
2579
|
-
}
|
|
2580
|
-
},
|
|
2581
|
-
{
|
|
2582
|
-
id: "hubspot",
|
|
2583
|
-
title: "HubSpot",
|
|
2584
|
-
category: "crm",
|
|
2585
|
-
twin: planned(NO_TWIN),
|
|
2586
|
-
signatures: {
|
|
2587
|
-
packages: {
|
|
2588
|
-
node: ["@hubspot/api-client"],
|
|
2589
|
-
python: ["hubspot-api-client"],
|
|
2590
|
-
ruby: ["hubspot-api-client"]
|
|
2591
|
-
},
|
|
2592
|
-
imports: ["@hubspot/api-client", "hubspot-api-client"],
|
|
2593
|
-
hosts: ["api.hubapi.com"],
|
|
2594
|
-
envPrefixes: ["HUBSPOT_"]
|
|
2595
|
-
}
|
|
2596
|
-
},
|
|
2597
|
-
{
|
|
2598
|
-
id: "salesforce",
|
|
2599
|
-
title: "Salesforce",
|
|
2600
|
-
category: "crm",
|
|
2601
|
-
twin: planned(NO_TWIN),
|
|
2602
|
-
signatures: {
|
|
2603
|
-
packages: {
|
|
2604
|
-
node: ["jsforce", "@salesforce/"],
|
|
2605
|
-
python: ["simple-salesforce"],
|
|
2606
|
-
java: ["force-wsc"]
|
|
2607
|
-
},
|
|
2608
|
-
imports: ["jsforce", "simple-salesforce"],
|
|
2609
|
-
hosts: ["salesforce.com", "force.com"],
|
|
2610
|
-
envPrefixes: ["SALESFORCE_", "SF_"]
|
|
2611
|
-
}
|
|
2612
|
-
},
|
|
2613
|
-
{
|
|
2614
|
-
id: "zendesk",
|
|
2615
|
-
title: "Zendesk",
|
|
2616
|
-
category: "support",
|
|
2617
|
-
twin: planned(NO_TWIN),
|
|
2618
|
-
signatures: {
|
|
2619
|
-
packages: {
|
|
2620
|
-
node: ["node-zendesk"],
|
|
2621
|
-
python: ["zendesk", "zenpy"],
|
|
2622
|
-
ruby: ["zendesk_api"]
|
|
2623
|
-
},
|
|
2624
|
-
imports: ["node-zendesk", "zenpy"],
|
|
2625
|
-
hosts: ["zendesk.com"],
|
|
2626
|
-
envPrefixes: ["ZENDESK_"]
|
|
2627
|
-
}
|
|
2628
|
-
},
|
|
2629
|
-
{
|
|
2630
|
-
id: "linear",
|
|
2631
|
-
title: "Linear",
|
|
2632
|
-
category: "pm",
|
|
2633
|
-
twin: planned(NO_TWIN),
|
|
2634
|
-
signatures: {
|
|
2635
|
-
packages: { node: ["@linear/sdk"], python: ["linear"] },
|
|
2636
|
-
imports: ["@linear/sdk"],
|
|
2637
|
-
hosts: ["api.linear.app"],
|
|
2638
|
-
envPrefixes: ["LINEAR_"]
|
|
2639
|
-
}
|
|
2640
|
-
},
|
|
2641
|
-
{
|
|
2642
|
-
id: "notion",
|
|
2643
|
-
title: "Notion",
|
|
2644
|
-
category: "docs",
|
|
2645
|
-
twin: planned(NO_TWIN),
|
|
2646
|
-
signatures: {
|
|
2647
|
-
packages: { node: ["@notionhq/client"], python: ["notion-client"] },
|
|
2648
|
-
imports: ["@notionhq/client", "notion-client"],
|
|
2649
|
-
hosts: ["api.notion.com"],
|
|
2650
|
-
envPrefixes: ["NOTION_"]
|
|
2651
|
-
}
|
|
2652
|
-
},
|
|
2653
|
-
{
|
|
2654
|
-
id: "resend",
|
|
2655
|
-
title: "Resend",
|
|
2656
|
-
category: "comms",
|
|
2657
|
-
twin: planned(NO_TWIN),
|
|
2658
|
-
signatures: {
|
|
2659
|
-
packages: { node: ["resend"] },
|
|
2660
|
-
imports: [`from ["']resend["']`],
|
|
2661
|
-
hosts: ["api.resend.com"],
|
|
2662
|
-
envPrefixes: ["RESEND_"]
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2665
|
-
];
|
|
2801
|
+
// src/discovery/engine.ts
|
|
2802
|
+
init_catalog();
|
|
2666
2803
|
|
|
2667
2804
|
// src/discovery/registry.ts
|
|
2668
2805
|
var SimpleProviderRegistry = class {
|
|
@@ -2883,6 +3020,7 @@ async function runProbes({ probes, timeoutMs = 15e3, log }) {
|
|
|
2883
3020
|
}
|
|
2884
3021
|
|
|
2885
3022
|
// src/discovery/engine.ts
|
|
3023
|
+
init_masking();
|
|
2886
3024
|
function twinStatus(provider) {
|
|
2887
3025
|
if (!provider) return "unknown";
|
|
2888
3026
|
const twin = provider.twin;
|
|
@@ -2893,7 +3031,7 @@ function twinStatus(provider) {
|
|
|
2893
3031
|
var PASS_BUDGET_MS = 2e4;
|
|
2894
3032
|
async function runDiscovery(opts) {
|
|
2895
3033
|
const files = await createFileIndex(opts.cwd);
|
|
2896
|
-
const providers = new SimpleProviderRegistry(
|
|
3034
|
+
const providers = new SimpleProviderRegistry(PROVIDERS);
|
|
2897
3035
|
const detectors = new SimpleDetectorRegistry(registerDetectors());
|
|
2898
3036
|
const collected = [];
|
|
2899
3037
|
const passes = [];
|
|
@@ -3235,31 +3373,9 @@ function singularize(word) {
|
|
|
3235
3373
|
}
|
|
3236
3374
|
|
|
3237
3375
|
// src/inventory/relationships.ts
|
|
3376
|
+
init_entrypoints();
|
|
3238
3377
|
import { createHash as createHash4 } from "crypto";
|
|
3239
3378
|
|
|
3240
|
-
// src/inventory/entrypoints.ts
|
|
3241
|
-
function codeOnly(source) {
|
|
3242
|
-
return source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`/g, (part) => part.replace(/[^\n]/g, " "));
|
|
3243
|
-
}
|
|
3244
|
-
function exportedEntrypoints(source) {
|
|
3245
|
-
const code = codeOnly(source);
|
|
3246
|
-
const pattern = /\bexport\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)[^{;]*\{|\bexport\s+(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*(?::[^=;]+)?=>\s*\{/g;
|
|
3247
|
-
const result = [];
|
|
3248
|
-
let match;
|
|
3249
|
-
while (match = pattern.exec(code)) {
|
|
3250
|
-
let depth = 1, end = pattern.lastIndex;
|
|
3251
|
-
while (end < code.length && depth) {
|
|
3252
|
-
if (code[end] === "{") depth++;
|
|
3253
|
-
if (code[end] === "}") depth--;
|
|
3254
|
-
end++;
|
|
3255
|
-
}
|
|
3256
|
-
if (depth) continue;
|
|
3257
|
-
result.push({ name: match[1] || match[2], startLine: code.slice(0, match.index).split("\n").length, endLine: code.slice(0, end).split("\n").length });
|
|
3258
|
-
pattern.lastIndex = end;
|
|
3259
|
-
}
|
|
3260
|
-
return result;
|
|
3261
|
-
}
|
|
3262
|
-
|
|
3263
3379
|
// src/inventory/operations.ts
|
|
3264
3380
|
var WRITE_VERBS = /* @__PURE__ */ new Set([
|
|
3265
3381
|
"create",
|
|
@@ -3318,9 +3434,10 @@ function scanOperations(text, path) {
|
|
|
3318
3434
|
}
|
|
3319
3435
|
|
|
3320
3436
|
// src/inventory/relationships.ts
|
|
3437
|
+
init_catalog();
|
|
3321
3438
|
var SOURCE_EXTS2 = ["ts", "tsx", "js", "jsx", "mjs", "cjs", "py", "go", "java", "kt", "rb", "php", "cs"];
|
|
3322
3439
|
var MAX_FILES3 = 2500;
|
|
3323
|
-
var NON_USAGE_DIR2 = /(^|\/)(docs?|examples?|demos?|samples?|fixtures?|tests?|__tests__|__mocks__|spec|specs|e2e|storybook|public|static|assets|generated)(\/|$)/i;
|
|
3440
|
+
var NON_USAGE_DIR2 = /(^|\/)(artifacts|testbed|docs?|examples?|demos?|samples?|fixtures?|tests?|__tests__|__mocks__|spec|specs|e2e|storybook|public|static|assets|generated)(\/|$)/i;
|
|
3324
3441
|
var PROVIDER_KINDS = /* @__PURE__ */ new Set([
|
|
3325
3442
|
"payments",
|
|
3326
3443
|
"comms",
|
|
@@ -3362,7 +3479,7 @@ function fileRefs(tool) {
|
|
|
3362
3479
|
return [...refs2];
|
|
3363
3480
|
}
|
|
3364
3481
|
function packagesFor(providerId) {
|
|
3365
|
-
const provider =
|
|
3482
|
+
const provider = PROVIDERS.find((p) => p.id === providerId);
|
|
3366
3483
|
if (!provider) return [];
|
|
3367
3484
|
const set = /* @__PURE__ */ new Set();
|
|
3368
3485
|
for (const list2 of Object.values(provider.signatures.packages ?? {})) {
|
|
@@ -3489,7 +3606,7 @@ async function buildRelationships(apps, tools, files, decisions) {
|
|
|
3489
3606
|
});
|
|
3490
3607
|
}
|
|
3491
3608
|
}
|
|
3492
|
-
const sourceFiles = (await Promise.all(SOURCE_EXTS2.map((ext) => files.byExt(ext)))).flat().filter((f) => !NON_USAGE_DIR2.test(f)).slice(0, MAX_FILES3);
|
|
3609
|
+
const sourceFiles = (await Promise.all(SOURCE_EXTS2.map((ext) => files.byExt(ext)))).flat().filter((f) => !NON_USAGE_DIR2.test(f) && !/(?:\.(?:test|spec)\.[^/]+$|(?:demo|test)?fixtures?\.[^/]+$)/i.test(f)).slice(0, MAX_FILES3);
|
|
3493
3610
|
const records = /* @__PURE__ */ new Map();
|
|
3494
3611
|
const roles = /* @__PURE__ */ new Map();
|
|
3495
3612
|
const fileEntities = /* @__PURE__ */ new Map();
|
|
@@ -3738,6 +3855,8 @@ async function extractScopeHints(files, inspections) {
|
|
|
3738
3855
|
}
|
|
3739
3856
|
|
|
3740
3857
|
// src/inventory/build.ts
|
|
3858
|
+
init_catalog();
|
|
3859
|
+
init_files();
|
|
3741
3860
|
var CATEGORY_KIND = {
|
|
3742
3861
|
payments: "payments",
|
|
3743
3862
|
comms: "comms",
|
|
@@ -3748,7 +3867,7 @@ var CATEGORY_KIND = {
|
|
|
3748
3867
|
ai: "ai"
|
|
3749
3868
|
};
|
|
3750
3869
|
function providerKind(id) {
|
|
3751
|
-
const provider =
|
|
3870
|
+
const provider = PROVIDERS.find((p) => p.id === id);
|
|
3752
3871
|
if (!provider) return "external-api";
|
|
3753
3872
|
return CATEGORY_KIND[provider.category] ?? "external-api";
|
|
3754
3873
|
}
|
|
@@ -3982,7 +4101,7 @@ async function buildInventory({
|
|
|
3982
4101
|
const scanLimits = [.../* @__PURE__ */ new Set([...manifest.scanLimits ?? [], ...files.scanLimits()])];
|
|
3983
4102
|
if (scanLimits.length) sources.push({ id: "files:coverage", tier: "local", status: "limited", findings: 0, detail: scanLimits.join("; ") });
|
|
3984
4103
|
const unclassified = candidates.filter((u) => !classified.has(u.ref));
|
|
3985
|
-
const withTwin = toolList.filter((t) =>
|
|
4104
|
+
const withTwin = toolList.filter((t) => PROVIDERS.some((p) => p.id === t.id && p.twin.implemented)).length;
|
|
3986
4105
|
const coverage = {
|
|
3987
4106
|
sources,
|
|
3988
4107
|
counts: {
|
|
@@ -4364,6 +4483,9 @@ async function runInspections(opts) {
|
|
|
4364
4483
|
return results.sort((a, b) => a.id.localeCompare(b.id));
|
|
4365
4484
|
}
|
|
4366
4485
|
|
|
4486
|
+
// src/commands/inventory.ts
|
|
4487
|
+
init_catalog();
|
|
4488
|
+
|
|
4367
4489
|
// src/inventory/workflows.ts
|
|
4368
4490
|
function permissionsFor(kind, entities) {
|
|
4369
4491
|
const list2 = [];
|