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 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
+ function codeOnly(source) {
249
+ return source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`/g, (part) => part.replace(/[^\n]/g, " "));
250
+ }
251
+ function exportedEntrypoints(source) {
252
+ const code = codeOnly(source);
253
+ 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;
254
+ const result = [];
255
+ let match;
256
+ while (match = pattern.exec(code)) {
257
+ let depth = 1, end = pattern.lastIndex;
258
+ while (end < code.length && depth) {
259
+ if (code[end] === "{") depth++;
260
+ if (code[end] === "}") depth--;
261
+ end++;
262
+ }
263
+ if (depth) continue;
264
+ result.push({ name: match[1] || match[2], startLine: code.slice(0, match.index).split("\n").length, endLine: code.slice(0, end).split("\n").length });
265
+ pattern.lastIndex = end;
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") : 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(TOKENISH, (m) => `${m.slice(0, 4)}***`).replace(EMAIL, "$1***$2");
851
+ return stripUrlCredentials(value).replace(TOKENISH2, (m) => `${m.slice(0, 4)}***`).replace(EMAIL, "$1***$2");
100
852
  }
101
- var URL_CREDENTIALS, TOKENISH, EMAIL;
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
- TOKENISH = /\b(sk|rk|pk|ghp|gho|ghs|glpat|xox[baprs]|AKIA|AIza)[A-Za-z0-9_-]{6,}\b/g;
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 readFile4, writeFile } from "fs/promises";
113
- import { basename, join as join4 } from "path";
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 readFile5 } from "fs/promises";
150
- import { join as join5 } from "path";
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 = join5(cwd, ".agenttwin/AGENT-TASK.md");
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 readFile5(join5(cwd, ".agenttwin/agent-discovery.json"), "utf8");
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 readFile7, writeFile as writeFile4, rename, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
221
- import { resolve as resolve2, join as join7, dirname as dirname2, extname } from "path";
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 readFile6, writeFile as writeFile3, realpath, lstat, rm, copyFile } from "fs/promises";
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 join6, relative, resolve, sep } from "path";
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 = relative(canonicalRoot, source);
745
- if (rel.startsWith(`..${sep}`) || 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 });
746
- const contents = await readFile6(source);
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 = join6(destination, file);
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 readFile6(file, "utf8")));
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 readFile6(join6(cwd, ".agenttwin/inventory.json"), "utf8"));
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(join6(tmpdir(), "agenttwin-world-"));
1533
+ const stage = await mkdtemp(join7(tmpdir(), "agenttwin-world-"));
781
1534
  try {
782
- const sourceDigests = await stageApplication(dirname(file), spec.files, join6(stage, "app"));
783
- await writeFile3(join6(stage, "world.json"), JSON.stringify(spec), { mode: 420 });
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 readFile6(join6(cwd, ".agenttwin/inventory.json"), "utf8")).name ?? basename2(cwd);
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(join6(stage, "app"), spec, opts.signal);
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 = join6(out2, "run.json"), reportPath = join6(out2, "report.md");
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 = join6(dirname(fileURLToPath2(import.meta.url)), "worker.js");
808
- await copyFile(worker, join6(stage, "worker.js"));
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 = join6(out2, "run.json");
818
- const reportPath = join6(out2, "report.md");
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(join6(out, "run.json"), JSON.stringify(receipt, null, 2) + "\n", { mode: 384 });
856
- await writeFile3(join6(out, "world.json"), JSON.stringify(spec, null, 2) + "\n", { mode: 384 });
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(join6(out, "report.md"), summary);
859
- return { ...receipt, reportPath: join6(out, "report.md"), evidencePath: join6(out, "run.json") };
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 = join7(directory, "state.json");
1654
+ const storePath = join8(directory, "state.json");
902
1655
  let store;
903
1656
  try {
904
- store = JSON.parse(await readFile7(storePath, "utf8"));
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 = join7(directory, `state-${randomUUID2()}.tmp`);
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(join7(tmpdir2(), "agenttwin-api-run-"));
1707
+ workspace = await mkdtemp2(join8(tmpdir2(), "agenttwin-api-run-"));
955
1708
  for (const [file, encoded] of Object.entries(item.files)) {
956
- const destination = join7(workspace, uploadPath(file));
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(join7(workspace, "agenttwin.world.json"), JSON.stringify(item.spec));
1713
+ await writeFile4(join8(workspace, "agenttwin.world.json"), JSON.stringify(item.spec));
961
1714
  if (item.spec.workflowId) {
962
- await mkdir3(join7(workspace, ".agenttwin"));
963
- await writeFile4(join7(workspace, ".agenttwin/inventory.json"), JSON.stringify({ workflows: [{ id: item.spec.workflowId }] }));
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 relative2 = decodeURIComponent(path.slice("/dashboard/".length)) || "index.html";
1002
- const root = resolve2(opts.dashboardDirectory), file = resolve2(root, relative2);
1003
- if (!file.startsWith(root + "/") || relative2 !== "index.html" && !relative2.startsWith("assets/")) {
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 readFile7(file);
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);
@@ -1147,7 +1900,7 @@ async function startGateway(opts) {
1147
1900
  return;
1148
1901
  }
1149
1902
  if (path === "/v1/runs" && req.method === "GET") {
1150
- send(res, 200, { jobs: store.jobs.filter((j) => j.accountId === account.id).slice(-100).reverse().map(({ id, status, createdAt }) => ({ id, status, createdAt })) });
1903
+ send(res, 200, { jobs: store.jobs.filter((j) => j.accountId === account.id).sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt)).slice(0, 100).map(({ id, status, createdAt }) => ({ id, status, createdAt })) });
1151
1904
  return;
1152
1905
  }
1153
1906
  if (path === "/v1/runs" && req.method === "POST") {