@poncho-ai/harness 0.34.1 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1450,6 +1450,112 @@ Available memory tools:
1450
1450
  - \`memory_main_write\` \u2014 overwrite the entire memory document
1451
1451
  - \`memory_main_edit\` \u2014 edit memory via exact string replacement (\`old_str\` / \`new_str\`)
1452
1452
  - \`conversation_recall\` \u2014 search past conversations
1453
+
1454
+ ## Multi-Tenancy
1455
+
1456
+ Deploy one agent and let many users (tenants) use it, each with fully isolated data. Multi-tenancy is opt-in \u2014 it activates automatically when a valid tenant JWT is received. Existing single-user deployments work unchanged.
1457
+
1458
+ ### How it works
1459
+
1460
+ 1. **Builder creates a JWT** for each tenant, signed with \`PONCHO_AUTH_TOKEN\` (HS256).
1461
+ 2. **Tenant accesses the agent** using \`?token=<jwt>\` in the web UI URL, or \`Authorization: Bearer <jwt>\` for API calls.
1462
+ 3. **All data is scoped** \u2014 conversations, memory, reminders, and todos are isolated per tenant.
1463
+
1464
+ ### Creating tenant tokens
1465
+
1466
+ Using the CLI (for development/testing):
1467
+
1468
+ \`\`\`bash
1469
+ poncho auth create-token --tenant acme-corp --ttl 24h
1470
+ \`\`\`
1471
+
1472
+ Using the client SDK (for your backend):
1473
+
1474
+ \`\`\`typescript
1475
+ import { createTenantToken } from "@poncho-ai/client";
1476
+
1477
+ const token = await createTenantToken({
1478
+ signingKey: process.env.PONCHO_AUTH_TOKEN,
1479
+ tenantId: "acme-corp",
1480
+ expiresIn: "1h",
1481
+ metadata: { plan: "pro" }, // optional, stored in JWT \`meta\` claim
1482
+ });
1483
+
1484
+ // Give this URL to the tenant:
1485
+ const url = \`https://my-agent.example.com/?token=\${token}\`;
1486
+ \`\`\`
1487
+
1488
+ Or use any HS256 JWT library in any language \u2014 set \`sub\` to the tenant ID.
1489
+
1490
+ ### Tenant-scoped API access
1491
+
1492
+ \`\`\`typescript
1493
+ import { AgentClient, createTenantToken } from "@poncho-ai/client";
1494
+
1495
+ const token = await createTenantToken({
1496
+ signingKey: process.env.PONCHO_AUTH_TOKEN,
1497
+ tenantId: "acme-corp",
1498
+ expiresIn: "1h",
1499
+ });
1500
+
1501
+ const client = new AgentClient({
1502
+ url: "https://my-agent.example.com",
1503
+ token, // tenant-scoped access
1504
+ });
1505
+
1506
+ const conversations = await client.listConversations(); // only acme-corp's
1507
+ \`\`\`
1508
+
1509
+ ### Per-tenant secrets
1510
+
1511
+ Tenants can provide their own API keys for MCP integrations. Declare which env vars are tenant-managed in \`poncho.config.js\`:
1512
+
1513
+ \`\`\`javascript
1514
+ export default {
1515
+ tenantSecrets: {
1516
+ LINEAR_API_KEY: "Linear API Key",
1517
+ GITHUB_TOKEN: "GitHub Token",
1518
+ },
1519
+ mcp: [
1520
+ { url: "https://mcp.linear.app/sse", auth: { type: "bearer", tokenEnv: "LINEAR_API_KEY" } },
1521
+ ],
1522
+ };
1523
+ \`\`\`
1524
+
1525
+ When a tenant sets their \`LINEAR_API_KEY\` through the web UI settings panel (or API), MCP tool calls for that tenant use their key. If the tenant hasn't set it, the agent falls back to \`process.env.LINEAR_API_KEY\`.
1526
+
1527
+ Builders can also set secrets for tenants via CLI:
1528
+
1529
+ \`\`\`bash
1530
+ poncho secrets set --tenant acme-corp LINEAR_API_KEY lk_acme_123
1531
+ poncho secrets list --tenant acme-corp
1532
+ poncho secrets delete --tenant acme-corp LINEAR_API_KEY
1533
+ \`\`\`
1534
+
1535
+ Secrets are encrypted at rest (AES-256-GCM) using a key derived from \`PONCHO_AUTH_TOKEN\`.
1536
+
1537
+ ### Auth model
1538
+
1539
+ | Access type | Token | Scope |
1540
+ |---|---|---|
1541
+ | **Builder** | \`Authorization: Bearer <PONCHO_AUTH_TOKEN>\` | Full admin access |
1542
+ | **Tenant** | \`Authorization: Bearer <JWT>\` or \`?token=<JWT>\` | Scoped to one tenant |
1543
+ | **Anonymous** | No token (when \`auth.required\` is false) | Legacy single-user mode |
1544
+
1545
+ ### What's isolated per tenant
1546
+
1547
+ - Conversations and message history
1548
+ - Persistent memory
1549
+ - Reminders
1550
+ - Todos
1551
+ - Secrets (MCP auth tokens)
1552
+ - Subagent conversations
1553
+
1554
+ ### Limitations
1555
+
1556
+ - **No tenant registry**: builders can't enumerate all tenants. Cron jobs and reminders run in agent scope, not per-tenant.
1557
+ - **No token revocation**: use short TTLs (1h recommended). Stateless JWTs can't be individually revoked.
1558
+ - **\`PONCHO_AUTH_TOKEN\` is immutable**: rotating it invalidates all tenant JWTs and encrypted secrets.
1453
1559
  `,
1454
1560
  "configuration": `# Configuration & Security
1455
1561
 
@@ -1532,6 +1638,14 @@ export default {
1532
1638
  // When auth.required is true:
1533
1639
  // - Web UI: users enter the passphrase (value of PONCHO_AUTH_TOKEN env var)
1534
1640
  // - API: clients include Authorization: Bearer <token> header
1641
+ // - Tenants: use JWT tokens (see Multi-Tenancy in docs/features.md)
1642
+
1643
+ // Per-tenant secrets (optional). Tenants can set their own values for these
1644
+ // env vars via the web UI settings panel or API. Used for MCP auth tokens.
1645
+ // tenantSecrets: {
1646
+ // LINEAR_API_KEY: 'Linear API Key',
1647
+ // GITHUB_TOKEN: 'GitHub Token',
1648
+ // },
1535
1649
 
1536
1650
  // Model provider API key env var overrides (optional)
1537
1651
  providers: {
@@ -2102,8 +2216,8 @@ var ponchoDocsTool = defineTool({
2102
2216
 
2103
2217
  // src/harness.ts
2104
2218
  import { randomUUID as randomUUID3 } from "crypto";
2105
- import { readFile as readFile10 } from "fs/promises";
2106
- import { resolve as resolve12 } from "path";
2219
+ import { readFile as readFile11 } from "fs/promises";
2220
+ import { resolve as resolve13 } from "path";
2107
2221
  import { defineTool as defineTool8, getTextContent as getTextContent2 } from "@poncho-ai/sdk";
2108
2222
 
2109
2223
  // src/upload-store.ts
@@ -2616,20 +2730,25 @@ var InMemoryMemoryStore = class {
2616
2730
  var FileMainMemoryStore = class {
2617
2731
  workingDir;
2618
2732
  filePath = "";
2733
+ customRelPath;
2619
2734
  ttlMs;
2620
2735
  loaded = false;
2621
2736
  writing = Promise.resolve();
2622
2737
  mainMemory = { ...DEFAULT_MAIN_MEMORY };
2623
- constructor(workingDir, ttlSeconds) {
2738
+ constructor(workingDir, ttlSeconds, customRelPath) {
2624
2739
  this.workingDir = workingDir;
2625
2740
  this.ttlMs = typeof ttlSeconds === "number" ? ttlSeconds * 1e3 : void 0;
2741
+ this.customRelPath = customRelPath;
2626
2742
  }
2627
2743
  async ensureFilePath() {
2628
2744
  if (this.filePath) {
2629
2745
  return;
2630
2746
  }
2631
2747
  const identity = await ensureAgentIdentity(this.workingDir);
2632
- this.filePath = resolve6(getAgentStoreDirectory(identity), LOCAL_MEMORY_FILE);
2748
+ this.filePath = resolve6(
2749
+ getAgentStoreDirectory(identity),
2750
+ this.customRelPath ?? LOCAL_MEMORY_FILE
2751
+ );
2633
2752
  }
2634
2753
  isExpired(updatedAt) {
2635
2754
  return typeof this.ttlMs === "number" && Date.now() - updatedAt > this.ttlMs;
@@ -2725,7 +2844,15 @@ var createMemoryStore = (agentId, config, options) => {
2725
2844
  const provider = config?.provider ?? "local";
2726
2845
  const ttl = config?.ttl;
2727
2846
  const workingDir = options?.workingDir ?? process.cwd();
2847
+ const tenantId = options?.tenantId;
2728
2848
  if (provider === "local") {
2849
+ if (tenantId) {
2850
+ return new FileMainMemoryStore(
2851
+ workingDir,
2852
+ ttl,
2853
+ `tenants/${slugifyStorageComponent(tenantId)}/${LOCAL_MEMORY_FILE}`
2854
+ );
2855
+ }
2729
2856
  return new FileMainMemoryStore(workingDir, ttl);
2730
2857
  }
2731
2858
  if (provider === "memory") {
@@ -2733,7 +2860,8 @@ var createMemoryStore = (agentId, config, options) => {
2733
2860
  }
2734
2861
  const kv = createRawKVStore(config);
2735
2862
  if (kv) {
2736
- const storageKey = `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(agentId)}:memory:main`;
2863
+ const base = `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(agentId)}`;
2864
+ const storageKey = tenantId ? `${base}:t:${slugifyStorageComponent(tenantId)}:memory:main` : `${base}:memory:main`;
2737
2865
  return new KVBackedMemoryStore(kv, storageKey, ttl);
2738
2866
  }
2739
2867
  return new InMemoryMemoryStore(ttl);
@@ -2768,6 +2896,7 @@ var buildRecallSnippet = (content, query, maxChars = 360) => {
2768
2896
  return content.slice(start, end);
2769
2897
  };
2770
2898
  var createMemoryTools = (store, options) => {
2899
+ const resolveStore = typeof store === "function" ? store : () => store;
2771
2900
  const maxRecallConversations = Math.max(1, options?.maxRecallConversations ?? 20);
2772
2901
  return [
2773
2902
  defineTool2({
@@ -2778,8 +2907,8 @@ var createMemoryTools = (store, options) => {
2778
2907
  properties: {},
2779
2908
  additionalProperties: false
2780
2909
  },
2781
- handler: async () => {
2782
- const memory = await store.getMainMemory();
2910
+ handler: async (_input, context) => {
2911
+ const memory = await resolveStore(context).getMainMemory();
2783
2912
  return { memory };
2784
2913
  }
2785
2914
  }),
@@ -2797,12 +2926,12 @@ var createMemoryTools = (store, options) => {
2797
2926
  required: ["content"],
2798
2927
  additionalProperties: false
2799
2928
  },
2800
- handler: async (input) => {
2929
+ handler: async (input, context) => {
2801
2930
  const content = typeof input.content === "string" ? input.content.trim() : "";
2802
2931
  if (!content) {
2803
2932
  throw new Error("content is required");
2804
2933
  }
2805
- const memory = await store.updateMainMemory({ content });
2934
+ const memory = await resolveStore(context).updateMainMemory({ content });
2806
2935
  return { ok: true, memory };
2807
2936
  }
2808
2937
  }),
@@ -2824,13 +2953,13 @@ var createMemoryTools = (store, options) => {
2824
2953
  required: ["old_str", "new_str"],
2825
2954
  additionalProperties: false
2826
2955
  },
2827
- handler: async (input) => {
2956
+ handler: async (input, context) => {
2828
2957
  const oldStr = typeof input.old_str === "string" ? input.old_str : "";
2829
2958
  const newStr = typeof input.new_str === "string" ? input.new_str : "";
2830
2959
  if (!oldStr) {
2831
2960
  throw new Error("old_str must not be empty.");
2832
2961
  }
2833
- const current = await store.getMainMemory();
2962
+ const current = await resolveStore(context).getMainMemory();
2834
2963
  const content = current.content;
2835
2964
  const first = content.indexOf(oldStr);
2836
2965
  if (first === -1) {
@@ -2845,7 +2974,7 @@ var createMemoryTools = (store, options) => {
2845
2974
  );
2846
2975
  }
2847
2976
  const newContent = content.slice(0, first) + newStr + content.slice(first + oldStr.length);
2848
- const memory = await store.updateMainMemory({ content: newContent });
2977
+ const memory = await resolveStore(context).updateMainMemory({ content: newContent });
2849
2978
  return { ok: true, memory };
2850
2979
  }
2851
2980
  }),
@@ -3208,7 +3337,8 @@ var InMemoryReminderStore = class {
3208
3337
  status: "pending",
3209
3338
  createdAt: Date.now(),
3210
3339
  conversationId: input.conversationId,
3211
- ownerId: input.ownerId
3340
+ ownerId: input.ownerId,
3341
+ tenantId: input.tenantId
3212
3342
  };
3213
3343
  this.reminders = pruneStale(this.reminders);
3214
3344
  this.reminders.push(reminder);
@@ -3267,7 +3397,8 @@ var FileReminderStore = class {
3267
3397
  status: "pending",
3268
3398
  createdAt: Date.now(),
3269
3399
  conversationId: input.conversationId,
3270
- ownerId: input.ownerId
3400
+ ownerId: input.ownerId,
3401
+ tenantId: input.tenantId
3271
3402
  };
3272
3403
  let reminders = await this.readAll();
3273
3404
  reminders = pruneStale(reminders);
@@ -3393,6 +3524,171 @@ var createReminderStore = (agentId, config, options) => {
3393
3524
  return new InMemoryReminderStore();
3394
3525
  };
3395
3526
 
3527
+ // src/secrets-store.ts
3528
+ import { createCipheriv, createDecipheriv, randomBytes, createHash as createHash3 } from "crypto";
3529
+ import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
3530
+ import { dirname as dirname5, resolve as resolve9 } from "path";
3531
+ function deriveKey(signingKey) {
3532
+ return createHash3("sha256").update("poncho-secrets-v1:" + signingKey).digest();
3533
+ }
3534
+ function encrypt(plaintext, key) {
3535
+ const iv = randomBytes(12);
3536
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
3537
+ const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
3538
+ const tag = cipher.getAuthTag();
3539
+ return {
3540
+ iv: iv.toString("base64"),
3541
+ ct: ct.toString("base64"),
3542
+ tag: tag.toString("base64")
3543
+ };
3544
+ }
3545
+ function decrypt(blob, key) {
3546
+ const decipher = createDecipheriv(
3547
+ "aes-256-gcm",
3548
+ key,
3549
+ Buffer.from(blob.iv, "base64")
3550
+ );
3551
+ decipher.setAuthTag(Buffer.from(blob.tag, "base64"));
3552
+ return Buffer.concat([
3553
+ decipher.update(Buffer.from(blob.ct, "base64")),
3554
+ decipher.final()
3555
+ ]).toString("utf8");
3556
+ }
3557
+ var FileSecretsStore = class {
3558
+ workingDir;
3559
+ encKey;
3560
+ constructor(workingDir, signingKey) {
3561
+ this.workingDir = workingDir;
3562
+ this.encKey = deriveKey(signingKey);
3563
+ }
3564
+ async filePath(tenantId) {
3565
+ const identity = await ensureAgentIdentity(this.workingDir);
3566
+ const dir = resolve9(
3567
+ getAgentStoreDirectory(identity),
3568
+ "tenants",
3569
+ slugifyStorageComponent(tenantId)
3570
+ );
3571
+ return resolve9(dir, "secrets.json");
3572
+ }
3573
+ async readAll(tenantId) {
3574
+ try {
3575
+ const raw = await readFile8(await this.filePath(tenantId), "utf8");
3576
+ return JSON.parse(raw);
3577
+ } catch {
3578
+ return {};
3579
+ }
3580
+ }
3581
+ async writeAll(tenantId, data) {
3582
+ const fp = await this.filePath(tenantId);
3583
+ await mkdir6(dirname5(fp), { recursive: true });
3584
+ await writeFile7(fp, JSON.stringify(data, null, 2), "utf8");
3585
+ }
3586
+ async get(tenantId) {
3587
+ const data = await this.readAll(tenantId);
3588
+ const result = {};
3589
+ for (const [k, blob] of Object.entries(data)) {
3590
+ try {
3591
+ result[k] = decrypt(blob, this.encKey);
3592
+ } catch {
3593
+ }
3594
+ }
3595
+ return result;
3596
+ }
3597
+ async set(tenantId, key, value) {
3598
+ const data = await this.readAll(tenantId);
3599
+ data[key] = encrypt(value, this.encKey);
3600
+ await this.writeAll(tenantId, data);
3601
+ }
3602
+ async delete(tenantId, key) {
3603
+ const data = await this.readAll(tenantId);
3604
+ delete data[key];
3605
+ await this.writeAll(tenantId, data);
3606
+ }
3607
+ async list(tenantId) {
3608
+ const data = await this.readAll(tenantId);
3609
+ return Object.keys(data);
3610
+ }
3611
+ };
3612
+ var KVSecretsStore = class {
3613
+ kv;
3614
+ baseKey;
3615
+ encKey;
3616
+ ttl;
3617
+ constructor(kv, baseKey, signingKey, ttl) {
3618
+ this.kv = kv;
3619
+ this.baseKey = baseKey;
3620
+ this.encKey = deriveKey(signingKey);
3621
+ this.ttl = ttl;
3622
+ }
3623
+ kvKey(tenantId) {
3624
+ return `${this.baseKey}:t:${slugifyStorageComponent(tenantId)}:secrets`;
3625
+ }
3626
+ async readAll(tenantId) {
3627
+ try {
3628
+ const raw = await this.kv.get(this.kvKey(tenantId));
3629
+ if (!raw) return {};
3630
+ return JSON.parse(raw);
3631
+ } catch {
3632
+ return {};
3633
+ }
3634
+ }
3635
+ async writeAll(tenantId, data) {
3636
+ const key = this.kvKey(tenantId);
3637
+ const value = JSON.stringify(data);
3638
+ if (this.ttl) {
3639
+ await this.kv.setWithTtl(key, value, this.ttl);
3640
+ } else {
3641
+ await this.kv.set(key, value);
3642
+ }
3643
+ }
3644
+ async get(tenantId) {
3645
+ const data = await this.readAll(tenantId);
3646
+ const result = {};
3647
+ for (const [k, blob] of Object.entries(data)) {
3648
+ try {
3649
+ result[k] = decrypt(blob, this.encKey);
3650
+ } catch {
3651
+ }
3652
+ }
3653
+ return result;
3654
+ }
3655
+ async set(tenantId, key, value) {
3656
+ const data = await this.readAll(tenantId);
3657
+ data[key] = encrypt(value, this.encKey);
3658
+ await this.writeAll(tenantId, data);
3659
+ }
3660
+ async delete(tenantId, key) {
3661
+ const data = await this.readAll(tenantId);
3662
+ delete data[key];
3663
+ await this.writeAll(tenantId, data);
3664
+ }
3665
+ async list(tenantId) {
3666
+ const data = await this.readAll(tenantId);
3667
+ return Object.keys(data);
3668
+ }
3669
+ };
3670
+ var createSecretsStore = (agentId, signingKey, config, options) => {
3671
+ const provider = config?.provider ?? "local";
3672
+ const ttl = typeof config?.ttl === "number" ? config.ttl : void 0;
3673
+ const workingDir = options?.workingDir ?? process.cwd();
3674
+ if (provider === "local" || provider === "memory") {
3675
+ return new FileSecretsStore(workingDir, signingKey);
3676
+ }
3677
+ const kv = createRawKVStore(config);
3678
+ if (kv) {
3679
+ const baseKey = `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(agentId)}`;
3680
+ return new KVSecretsStore(kv, baseKey, signingKey, ttl);
3681
+ }
3682
+ return new FileSecretsStore(workingDir, signingKey);
3683
+ };
3684
+ async function resolveEnv(secretsStore, tenantId, envName) {
3685
+ if (tenantId && secretsStore) {
3686
+ const secrets = await secretsStore.get(tenantId);
3687
+ if (secrets[envName]) return secrets[envName];
3688
+ }
3689
+ return process.env[envName];
3690
+ }
3691
+
3396
3692
  // src/reminder-tools.ts
3397
3693
  import { defineTool as defineTool4 } from "@poncho-ai/sdk";
3398
3694
  var VALID_STATUSES2 = ["pending", "cancelled"];
@@ -3465,7 +3761,8 @@ var createReminderTools = (store) => [
3465
3761
  task,
3466
3762
  scheduledAt,
3467
3763
  timezone,
3468
- conversationId
3764
+ conversationId,
3765
+ tenantId: context.tenantId
3469
3766
  });
3470
3767
  return {
3471
3768
  ok: true,
@@ -3493,8 +3790,11 @@ var createReminderTools = (store) => [
3493
3790
  },
3494
3791
  additionalProperties: false
3495
3792
  },
3496
- handler: async (input) => {
3793
+ handler: async (input, context) => {
3497
3794
  let reminders = await store.list();
3795
+ if (context.tenantId) {
3796
+ reminders = reminders.filter((r) => r.tenantId === context.tenantId);
3797
+ }
3498
3798
  const status = typeof input.status === "string" ? input.status : void 0;
3499
3799
  if (status && VALID_STATUSES2.includes(status)) {
3500
3800
  reminders = reminders.filter((r) => r.status === status);
@@ -3526,9 +3826,16 @@ var createReminderTools = (store) => [
3526
3826
  required: ["id"],
3527
3827
  additionalProperties: false
3528
3828
  },
3529
- handler: async (input) => {
3829
+ handler: async (input, context) => {
3530
3830
  const id = typeof input.id === "string" ? input.id.trim() : "";
3531
3831
  if (!id) throw new Error("id is required");
3832
+ if (context.tenantId) {
3833
+ const all = await store.list();
3834
+ const target = all.find((r) => r.id === id);
3835
+ if (target && target.tenantId !== context.tenantId) {
3836
+ throw new Error("Reminder not found");
3837
+ }
3838
+ }
3532
3839
  const cancelled = await store.cancel(id);
3533
3840
  return {
3534
3841
  ok: true,
@@ -3751,6 +4058,14 @@ var LocalMcpBridge = class {
3751
4058
  toolCatalog = /* @__PURE__ */ new Map();
3752
4059
  unavailableServers = /* @__PURE__ */ new Map();
3753
4060
  authFailedServers = /* @__PURE__ */ new Set();
4061
+ envResolver;
4062
+ /**
4063
+ * Set a resolver for per-tenant env vars (e.g. MCP auth tokens).
4064
+ * Called by the harness after creating the secrets store.
4065
+ */
4066
+ setEnvResolver(resolver) {
4067
+ this.envResolver = resolver;
4068
+ }
3754
4069
  constructor(config) {
3755
4070
  this.remoteServers = (config?.mcp ?? []).filter(
3756
4071
  (entry) => typeof entry.url === "string"
@@ -3790,8 +4105,11 @@ var LocalMcpBridge = class {
3790
4105
  }
3791
4106
  console.info(`[poncho][mcp] ${line}`);
3792
4107
  }
4108
+ /** Set of servers where discovery was deferred (no default token, has env resolver). */
4109
+ deferredDiscoveryServers = /* @__PURE__ */ new Set();
3793
4110
  async discoverTools() {
3794
4111
  this.toolCatalog.clear();
4112
+ this.deferredDiscoveryServers.clear();
3795
4113
  for (const remoteServer of this.remoteServers) {
3796
4114
  const name = this.getServerName(remoteServer);
3797
4115
  if (this.unavailableServers.has(name)) {
@@ -3812,6 +4130,11 @@ var LocalMcpBridge = class {
3812
4130
  } catch (error) {
3813
4131
  const message = error instanceof Error ? error.message : String(error);
3814
4132
  if (error instanceof McpHttpError && error.status === 401) {
4133
+ if (this.envResolver && remoteServer.auth?.tokenEnv) {
4134
+ this.deferredDiscoveryServers.add(name);
4135
+ this.log("info", "catalog.deferred", { server: name, reason: "auth_deferred_to_tenant" });
4136
+ continue;
4137
+ }
3815
4138
  this.authFailedServers.add(name);
3816
4139
  this.log("warn", "auth.failed", {
3817
4140
  server: name,
@@ -3824,6 +4147,57 @@ var LocalMcpBridge = class {
3824
4147
  }
3825
4148
  }
3826
4149
  }
4150
+ /**
4151
+ * Run deferred discovery and return ToolDefinitions for all newly discovered tools.
4152
+ * Call this during run() so the tools are available to the model immediately.
4153
+ */
4154
+ async discoverAndLoadDeferred(tenantId) {
4155
+ if (this.deferredDiscoveryServers.size === 0) return [];
4156
+ for (const server of this.remoteServers) {
4157
+ await this.tryDeferredDiscovery(server, tenantId);
4158
+ }
4159
+ const tools = [];
4160
+ for (const server of this.remoteServers) {
4161
+ const name = this.getServerName(server);
4162
+ if (!server.auth?.tokenEnv) continue;
4163
+ const discovered = this.toolCatalog.get(name);
4164
+ if (!discovered || discovered.length === 0) continue;
4165
+ const client = this.rpcClients.get(name);
4166
+ if (!client) continue;
4167
+ tools.push(...this.toToolDefinitions(name, discovered, client, server));
4168
+ }
4169
+ return tools;
4170
+ }
4171
+ async tryDeferredDiscovery(server, tenantId) {
4172
+ const name = this.getServerName(server);
4173
+ if (!this.deferredDiscoveryServers.has(name)) return;
4174
+ if (this.toolCatalog.has(name)) return;
4175
+ const tokenEnv = server.auth?.tokenEnv;
4176
+ if (!tokenEnv || !this.envResolver) return;
4177
+ const token = await this.envResolver(tenantId, tokenEnv);
4178
+ if (!token) return;
4179
+ try {
4180
+ const probeClient = new StreamableHttpMcpRpcClient(
4181
+ server.url,
4182
+ server.timeoutMs ?? 1e4,
4183
+ token,
4184
+ server.headers
4185
+ );
4186
+ const discovered = await probeClient.listTools();
4187
+ this.toolCatalog.set(name, discovered);
4188
+ this.deferredDiscoveryServers.delete(name);
4189
+ this.log("info", "catalog.loaded", {
4190
+ server: name,
4191
+ discoveredCount: discovered.length,
4192
+ via: "deferred_tenant_discovery"
4193
+ });
4194
+ } catch (error) {
4195
+ this.log("warn", "catalog.deferred_failed", {
4196
+ server: name,
4197
+ error: error instanceof Error ? error.message : String(error)
4198
+ });
4199
+ }
4200
+ }
3827
4201
  async startLocalServers() {
3828
4202
  this.unavailableServers.clear();
3829
4203
  for (const server of this.remoteServers) {
@@ -3832,15 +4206,21 @@ var LocalMcpBridge = class {
3832
4206
  if (tokenEnv) {
3833
4207
  const token = process.env[tokenEnv];
3834
4208
  if (!token || token.trim().length === 0) {
3835
- this.unavailableServers.set(
3836
- name,
3837
- `Missing bearer token value from env var ${tokenEnv}`
3838
- );
3839
- this.log("warn", "auth.token_missing", {
4209
+ if (!this.envResolver) {
4210
+ this.unavailableServers.set(
4211
+ name,
4212
+ `Missing bearer token value from env var ${tokenEnv}`
4213
+ );
4214
+ this.log("warn", "auth.token_missing", {
4215
+ server: name,
4216
+ tokenEnv
4217
+ });
4218
+ continue;
4219
+ }
4220
+ this.log("info", "auth.token_deferred", {
3840
4221
  server: name,
3841
4222
  tokenEnv
3842
4223
  });
3843
- continue;
3844
4224
  }
3845
4225
  }
3846
4226
  this.rpcClients.set(
@@ -3904,6 +4284,29 @@ var LocalMcpBridge = class {
3904
4284
  }
3905
4285
  return output.sort();
3906
4286
  }
4287
+ hasDeferredServers() {
4288
+ return this.deferredDiscoveryServers.size > 0;
4289
+ }
4290
+ /**
4291
+ * Return ToolDefinitions for catalog tools not already registered in the dispatcher.
4292
+ */
4293
+ getUnregisteredTools(registeredNames) {
4294
+ const tools = [];
4295
+ for (const server of this.remoteServers) {
4296
+ const name = this.getServerName(server);
4297
+ const discovered = this.toolCatalog.get(name);
4298
+ if (!discovered || discovered.length === 0) continue;
4299
+ const client = this.rpcClients.get(name);
4300
+ if (!client) continue;
4301
+ const unregistered = discovered.filter(
4302
+ (d) => !registeredNames.has(`${name}/${d.name}`)
4303
+ );
4304
+ if (unregistered.length > 0) {
4305
+ tools.push(...this.toToolDefinitions(name, unregistered, client, server));
4306
+ }
4307
+ }
4308
+ return tools;
4309
+ }
3907
4310
  async loadTools(requestedPatterns) {
3908
4311
  for (const [index, pattern] of requestedPatterns.entries()) {
3909
4312
  validateMcpPattern(pattern, `requestedPatterns[${index}]`);
@@ -3935,7 +4338,7 @@ var LocalMcpBridge = class {
3935
4338
  const selectedDescriptors = discovered.filter(
3936
4339
  (descriptor) => selectedRawNames.has(descriptor.name)
3937
4340
  );
3938
- tools.push(...this.toToolDefinitions(serverName, selectedDescriptors, client));
4341
+ tools.push(...this.toToolDefinitions(serverName, selectedDescriptors, client, server));
3939
4342
  }
3940
4343
  this.log("info", "tools.selected", {
3941
4344
  requestedPatternCount: requestedPatterns.length,
@@ -3945,7 +4348,7 @@ var LocalMcpBridge = class {
3945
4348
  });
3946
4349
  return tools;
3947
4350
  }
3948
- toToolDefinitions(serverName, tools, client) {
4351
+ toToolDefinitions(serverName, tools, client, server) {
3949
4352
  return tools.map((tool) => ({
3950
4353
  name: `${serverName}/${tool.name}`,
3951
4354
  description: tool.description ?? `MCP tool ${tool.name} from ${serverName}`,
@@ -3953,9 +4356,23 @@ var LocalMcpBridge = class {
3953
4356
  type: "object",
3954
4357
  properties: {}
3955
4358
  },
3956
- handler: async (input) => {
4359
+ handler: async (input, context) => {
3957
4360
  try {
3958
- return await client.callTool(tool.name, input);
4361
+ const tokenEnv = server?.auth?.tokenEnv;
4362
+ let callClient = client;
4363
+ if (tokenEnv && this.envResolver && context?.tenantId) {
4364
+ const tenantToken = await this.envResolver(context.tenantId, tokenEnv);
4365
+ const defaultToken = process.env[tokenEnv];
4366
+ if (tenantToken && tenantToken !== defaultToken) {
4367
+ callClient = new StreamableHttpMcpRpcClient(
4368
+ server.url,
4369
+ server.timeoutMs ?? 1e4,
4370
+ tenantToken,
4371
+ server.headers
4372
+ );
4373
+ }
4374
+ }
4375
+ return await callClient.callTool(tool.name, input);
3959
4376
  } catch (error) {
3960
4377
  if (error instanceof McpHttpError && error.status === 401) {
3961
4378
  this.authFailedServers.add(serverName);
@@ -3986,8 +4403,8 @@ import { createAnthropic } from "@ai-sdk/anthropic";
3986
4403
 
3987
4404
  // src/openai-codex-auth.ts
3988
4405
  import { homedir as homedir3 } from "os";
3989
- import { dirname as dirname5, resolve as resolve9 } from "path";
3990
- import { mkdir as mkdir6, readFile as readFile8, chmod, writeFile as writeFile7, rm as rm3 } from "fs/promises";
4406
+ import { dirname as dirname6, resolve as resolve10 } from "path";
4407
+ import { mkdir as mkdir7, readFile as readFile9, chmod, writeFile as writeFile8, rm as rm3 } from "fs/promises";
3991
4408
  var OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
3992
4409
  var OPENAI_AUTH_ISSUER = "https://auth.openai.com";
3993
4410
  var REFRESH_TOKEN_GRACE_MS = 5 * 60 * 1e3;
@@ -4019,14 +4436,14 @@ var getOpenAICodexAuthFilePath = (config) => {
4019
4436
  const env = defaultedConfig(config);
4020
4437
  const fromEnv = process.env[env.authFilePathEnv];
4021
4438
  if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
4022
- return resolve9(fromEnv);
4439
+ return resolve10(fromEnv);
4023
4440
  }
4024
- return resolve9(homedir3(), ".poncho", "auth", "openai-codex.json");
4441
+ return resolve10(homedir3(), ".poncho", "auth", "openai-codex.json");
4025
4442
  };
4026
4443
  var readOpenAICodexSession = async (config) => {
4027
4444
  const filePath = getOpenAICodexAuthFilePath(config);
4028
4445
  try {
4029
- const content = await readFile8(filePath, "utf8");
4446
+ const content = await readFile9(filePath, "utf8");
4030
4447
  const parsed = JSON.parse(content);
4031
4448
  if (typeof parsed.refreshToken !== "string" || parsed.refreshToken.length === 0) {
4032
4449
  return void 0;
@@ -4043,12 +4460,12 @@ var readOpenAICodexSession = async (config) => {
4043
4460
  };
4044
4461
  var writeOpenAICodexSession = async (session, config) => {
4045
4462
  const filePath = getOpenAICodexAuthFilePath(config);
4046
- await mkdir6(dirname5(filePath), { recursive: true });
4463
+ await mkdir7(dirname6(filePath), { recursive: true });
4047
4464
  const payload = {
4048
4465
  ...session,
4049
4466
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4050
4467
  };
4051
- await writeFile7(filePath, `${JSON.stringify(payload, null, 2)}
4468
+ await writeFile8(filePath, `${JSON.stringify(payload, null, 2)}
4052
4469
  `, "utf8");
4053
4470
  await chmod(filePath, 384);
4054
4471
  };
@@ -4361,8 +4778,8 @@ var createModelProvider = (provider, config) => {
4361
4778
  };
4362
4779
 
4363
4780
  // src/skill-context.ts
4364
- import { readFile as readFile9, readdir as readdir2, stat } from "fs/promises";
4365
- import { dirname as dirname6, resolve as resolve10, normalize } from "path";
4781
+ import { readFile as readFile10, readdir as readdir2, stat } from "fs/promises";
4782
+ import { dirname as dirname7, resolve as resolve11, normalize } from "path";
4366
4783
  import YAML3 from "yaml";
4367
4784
  var DEFAULT_SKILL_DIRS = ["skills"];
4368
4785
  var resolveSkillDirs = (workingDir, extraPaths) => {
@@ -4374,7 +4791,7 @@ var resolveSkillDirs = (workingDir, extraPaths) => {
4374
4791
  }
4375
4792
  }
4376
4793
  }
4377
- return dirs.map((d) => resolve10(workingDir, d));
4794
+ return dirs.map((d) => resolve11(workingDir, d));
4378
4795
  };
4379
4796
  var FRONTMATTER_PATTERN3 = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/;
4380
4797
  var asRecord2 = (value) => typeof value === "object" && value !== null ? value : {};
@@ -4443,7 +4860,7 @@ var collectSkillManifests = async (directory) => {
4443
4860
  const entries = await readdir2(directory, { withFileTypes: true });
4444
4861
  const files = [];
4445
4862
  for (const entry of entries) {
4446
- const fullPath = resolve10(directory, entry.name);
4863
+ const fullPath = resolve11(directory, entry.name);
4447
4864
  let isDir = entry.isDirectory();
4448
4865
  let isFile = entry.isFile();
4449
4866
  if (entry.isSymbolicLink()) {
@@ -4478,13 +4895,13 @@ var loadSkillMetadata = async (workingDir, extraSkillPaths) => {
4478
4895
  const seen = /* @__PURE__ */ new Set();
4479
4896
  for (const manifest of allManifests) {
4480
4897
  try {
4481
- const content = await readFile9(manifest, "utf8");
4898
+ const content = await readFile10(manifest, "utf8");
4482
4899
  const parsed = parseSkillFrontmatter(content);
4483
4900
  if (parsed && !seen.has(parsed.name)) {
4484
4901
  seen.add(parsed.name);
4485
4902
  skills.push({
4486
4903
  ...parsed,
4487
- skillDir: dirname6(manifest),
4904
+ skillDir: dirname7(manifest),
4488
4905
  skillPath: manifest
4489
4906
  });
4490
4907
  }
@@ -4523,7 +4940,7 @@ ${xmlSkills}
4523
4940
  };
4524
4941
  var escapeXml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
4525
4942
  var loadSkillInstructions = async (skill) => {
4526
- const content = await readFile9(skill.skillPath, "utf8");
4943
+ const content = await readFile10(skill.skillPath, "utf8");
4527
4944
  const match = content.match(FRONTMATTER_PATTERN3);
4528
4945
  return match ? match[2].trim() : content.trim();
4529
4946
  };
@@ -4532,11 +4949,11 @@ var readSkillResource = async (skill, relativePath) => {
4532
4949
  if (normalized.startsWith("..") || normalized.startsWith("/")) {
4533
4950
  throw new Error("Path must be relative and within the skill directory");
4534
4951
  }
4535
- const fullPath = resolve10(skill.skillDir, normalized);
4952
+ const fullPath = resolve11(skill.skillDir, normalized);
4536
4953
  if (!fullPath.startsWith(skill.skillDir)) {
4537
4954
  throw new Error("Path escapes the skill directory");
4538
4955
  }
4539
- return await readFile9(fullPath, "utf8");
4956
+ return await readFile10(fullPath, "utf8");
4540
4957
  };
4541
4958
  var MAX_INSTRUCTIONS_PER_SKILL = 1200;
4542
4959
  var loadSkillContext = async (workingDir) => {
@@ -4655,7 +5072,7 @@ function convertSchema(schema) {
4655
5072
  // src/skill-tools.ts
4656
5073
  import { defineTool as defineTool5 } from "@poncho-ai/sdk";
4657
5074
  import { access as access2, readdir as readdir3, stat as stat2 } from "fs/promises";
4658
- import { extname, normalize as normalize2, resolve as resolve11, sep as sep2 } from "path";
5075
+ import { extname, normalize as normalize2, resolve as resolve12, sep as sep2 } from "path";
4659
5076
  import { pathToFileURL } from "url";
4660
5077
  import { createJiti as createJiti2 } from "jiti";
4661
5078
  var createSkillTools = (skills, options) => {
@@ -4913,7 +5330,7 @@ var collectScriptFiles = async (directory) => {
4913
5330
  if (entry.name === "node_modules") {
4914
5331
  continue;
4915
5332
  }
4916
- const fullPath = resolve11(directory, entry.name);
5333
+ const fullPath = resolve12(directory, entry.name);
4917
5334
  let isDir = entry.isDirectory();
4918
5335
  let isFile = entry.isFile();
4919
5336
  if (entry.isSymbolicLink()) {
@@ -4952,8 +5369,8 @@ var normalizeScriptPolicyPath = (relativePath) => {
4952
5369
  };
4953
5370
  var resolveScriptPath = (baseDir, relativePath, containmentDir) => {
4954
5371
  const normalized = normalizeScriptPolicyPath(relativePath);
4955
- const fullPath = resolve11(baseDir, normalized);
4956
- const boundary = resolve11(containmentDir ?? baseDir);
5372
+ const fullPath = resolve12(baseDir, normalized);
5373
+ const boundary = resolve12(containmentDir ?? baseDir);
4957
5374
  if (!fullPath.startsWith(`${boundary}${sep2}`) && fullPath !== boundary) {
4958
5375
  throw new Error("Script path must stay inside the allowed directory");
4959
5376
  }
@@ -5059,8 +5476,8 @@ function applyRateLimitCooldown(retryAfterHeader) {
5059
5476
  async function runWithSearchThrottle(fn) {
5060
5477
  const previous = searchQueue;
5061
5478
  let release;
5062
- searchQueue = new Promise((resolve14) => {
5063
- release = resolve14;
5479
+ searchQueue = new Promise((resolve15) => {
5480
+ release = resolve15;
5064
5481
  });
5065
5482
  await previous.catch(() => {
5066
5483
  });
@@ -5266,7 +5683,8 @@ var createSubagentTools = (manager) => [
5266
5683
  const { subagentId } = await manager.spawn({
5267
5684
  task: task.trim(),
5268
5685
  parentConversationId: conversationId,
5269
- ownerId
5686
+ ownerId,
5687
+ tenantId: context.tenantId
5270
5688
  });
5271
5689
  return { subagentId, status: "running" };
5272
5690
  }
@@ -5351,8 +5769,12 @@ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
5351
5769
 
5352
5770
  // src/telemetry.ts
5353
5771
  var MAX_FIELD_LENGTH = 200;
5772
+ var OMIT_FROM_LOG = /* @__PURE__ */ new Set(["continuationMessages", "_harnessMessages", "messages", "compactedHistory"]);
5354
5773
  function sanitizeEventForLog(event) {
5355
- return JSON.stringify(event, (_key, value) => {
5774
+ return JSON.stringify(event, (key, value) => {
5775
+ if (OMIT_FROM_LOG.has(key) && Array.isArray(value)) {
5776
+ return `[${value.length} messages]`;
5777
+ }
5356
5778
  if (typeof value === "string" && value.length > MAX_FIELD_LENGTH) {
5357
5779
  return `${value.slice(0, 80)}...[${value.length} chars]`;
5358
5780
  }
@@ -6002,8 +6424,11 @@ var AgentHarness = class _AgentHarness {
6002
6424
  uploadStore;
6003
6425
  skillContextWindow = "";
6004
6426
  memoryStore;
6427
+ tenantMemoryStores = /* @__PURE__ */ new Map();
6428
+ memoryConfig;
6005
6429
  todoStore;
6006
6430
  reminderStore;
6431
+ secretsStore;
6007
6432
  loadedConfig;
6008
6433
  loadedSkills = [];
6009
6434
  skillFingerprint = "";
@@ -6288,6 +6713,28 @@ var AgentHarness = class _AgentHarness {
6288
6713
  if (!this.todoStore) return [];
6289
6714
  return this.todoStore.get(conversationId);
6290
6715
  }
6716
+ /**
6717
+ * Get a memory store, optionally scoped to a tenant.
6718
+ * Returns the default (agent-wide) store when tenantId is null/undefined.
6719
+ */
6720
+ getMemoryStore(tenantId) {
6721
+ if (!this.memoryConfig?.enabled) return void 0;
6722
+ if (!tenantId) return this.memoryStore;
6723
+ let store = this.tenantMemoryStores.get(tenantId);
6724
+ if (!store) {
6725
+ const agentId = this.parsedAgent?.frontmatter.id ?? this.parsedAgent?.frontmatter.name ?? "unknown";
6726
+ store = createMemoryStore(agentId, this.memoryConfig, {
6727
+ workingDir: this.workingDir,
6728
+ tenantId
6729
+ });
6730
+ this.tenantMemoryStores.set(tenantId, store);
6731
+ if (this.tenantMemoryStores.size > 100) {
6732
+ const oldest = this.tenantMemoryStores.keys().next().value;
6733
+ if (oldest) this.tenantMemoryStores.delete(oldest);
6734
+ }
6735
+ }
6736
+ return store;
6737
+ }
6291
6738
  listActiveSkills() {
6292
6739
  return [...this.activeSkillNames].sort();
6293
6740
  }
@@ -6518,8 +6965,8 @@ var AgentHarness = class _AgentHarness {
6518
6965
  return false;
6519
6966
  }
6520
6967
  try {
6521
- const agentFilePath = resolve12(this.workingDir, "AGENT.md");
6522
- const rawContent = await readFile10(agentFilePath, "utf8");
6968
+ const agentFilePath = resolve13(this.workingDir, "AGENT.md");
6969
+ const rawContent = await readFile11(agentFilePath, "utf8");
6523
6970
  if (rawContent === this.agentFileFingerprint) {
6524
6971
  return false;
6525
6972
  }
@@ -6588,8 +7035,8 @@ var AgentHarness = class _AgentHarness {
6588
7035
  }
6589
7036
  }
6590
7037
  async initialize() {
6591
- const agentFilePath = resolve12(this.workingDir, "AGENT.md");
6592
- const agentRawContent = await readFile10(agentFilePath, "utf8");
7038
+ const agentFilePath = resolve13(this.workingDir, "AGENT.md");
7039
+ const agentRawContent = await readFile11(agentFilePath, "utf8");
6593
7040
  this.parsedAgent = parseAgentMarkdown(agentRawContent);
6594
7041
  this.agentFileFingerprint = agentRawContent;
6595
7042
  const identity = await ensureAgentIdentity(this.workingDir);
@@ -6613,6 +7060,7 @@ var AgentHarness = class _AgentHarness {
6613
7060
  this.skillFingerprint = this.buildSkillFingerprint(skillMetadata);
6614
7061
  this.registerSkillTools(skillMetadata);
6615
7062
  const agentId = this.parsedAgent.frontmatter.id ?? this.parsedAgent.frontmatter.name;
7063
+ this.memoryConfig = memoryConfig ?? void 0;
6616
7064
  if (memoryConfig?.enabled) {
6617
7065
  this.memoryStore = createMemoryStore(
6618
7066
  agentId,
@@ -6620,9 +7068,10 @@ var AgentHarness = class _AgentHarness {
6620
7068
  { workingDir: this.workingDir }
6621
7069
  );
6622
7070
  this.dispatcher.registerMany(
6623
- createMemoryTools(this.memoryStore, {
6624
- maxRecallConversations: memoryConfig.maxRecallConversations
6625
- })
7071
+ createMemoryTools(
7072
+ (ctx) => this.getMemoryStore(ctx.tenantId) ?? this.memoryStore,
7073
+ { maxRecallConversations: memoryConfig.maxRecallConversations }
7074
+ )
6626
7075
  );
6627
7076
  }
6628
7077
  const stateConfig = resolveStateConfig(config);
@@ -6647,6 +7096,14 @@ var AgentHarness = class _AgentHarness {
6647
7096
  );
6648
7097
  });
6649
7098
  }
7099
+ const authTokenEnv = config?.auth?.tokenEnv ?? "PONCHO_AUTH_TOKEN";
7100
+ const authToken = process.env[authTokenEnv];
7101
+ if (authToken) {
7102
+ this.secretsStore = createSecretsStore(agentId, authToken, stateConfig, { workingDir: this.workingDir });
7103
+ bridge.setEnvResolver(async (tenantId, envName) => {
7104
+ return resolveEnv(this.secretsStore, tenantId, envName);
7105
+ });
7106
+ }
6650
7107
  await bridge.startLocalServers();
6651
7108
  await bridge.discoverTools();
6652
7109
  await this.refreshMcpTools("initialize");
@@ -6680,14 +7137,14 @@ var AgentHarness = class _AgentHarness {
6680
7137
  const filePath = pathResolve(stateDir, `${sessionId}.json`);
6681
7138
  return {
6682
7139
  async save(json) {
6683
- const { mkdir: mkdir8, writeFile: writeFile9 } = await import("fs/promises");
6684
- await mkdir8(stateDir, { recursive: true });
6685
- await writeFile9(filePath, json, "utf8");
7140
+ const { mkdir: mkdir9, writeFile: writeFile10 } = await import("fs/promises");
7141
+ await mkdir9(stateDir, { recursive: true });
7142
+ await writeFile10(filePath, json, "utf8");
6686
7143
  },
6687
7144
  async load() {
6688
- const { readFile: readFile12 } = await import("fs/promises");
7145
+ const { readFile: readFile13 } = await import("fs/promises");
6689
7146
  try {
6690
- return await readFile12(filePath, "utf8");
7147
+ return await readFile13(filePath, "utf8");
6691
7148
  } catch {
6692
7149
  return void 0;
6693
7150
  }
@@ -6753,7 +7210,7 @@ var AgentHarness = class _AgentHarness {
6753
7210
  let browserMod;
6754
7211
  try {
6755
7212
  const { existsSync } = await import("fs");
6756
- const { join, dirname: dirname8 } = await import("path");
7213
+ const { join, dirname: dirname9 } = await import("path");
6757
7214
  const { pathToFileURL: pathToFileURL2 } = await import("url");
6758
7215
  let searchDir = this.workingDir;
6759
7216
  let entryPath;
@@ -6763,7 +7220,7 @@ var AgentHarness = class _AgentHarness {
6763
7220
  entryPath = candidate;
6764
7221
  break;
6765
7222
  }
6766
- const parent = dirname8(searchDir);
7223
+ const parent = dirname9(searchDir);
6767
7224
  if (parent === searchDir) break;
6768
7225
  searchDir = parent;
6769
7226
  }
@@ -6839,7 +7296,8 @@ var AgentHarness = class _AgentHarness {
6839
7296
  kind: SpanKind.INTERNAL,
6840
7297
  attributes: {
6841
7298
  "gen_ai.operation.name": "invoke_agent",
6842
- ...input.conversationId ? { "gen_ai.conversation.id": input.conversationId } : {}
7299
+ ...input.conversationId ? { "gen_ai.conversation.id": input.conversationId } : {},
7300
+ ...input.tenantId ? { "tenant.id": input.tenantId } : {}
6843
7301
  }
6844
7302
  });
6845
7303
  const spanContext = trace.setSpan(otelContext.active(), rootSpan);
@@ -6885,10 +7343,18 @@ var AgentHarness = class _AgentHarness {
6885
7343
  if (!this.parsedAgent) {
6886
7344
  await this.initialize();
6887
7345
  }
6888
- const memoryPromise = this.memoryStore ? this.memoryStore.getMainMemory() : void 0;
7346
+ const activeMemoryStore = this.getMemoryStore(input.tenantId);
7347
+ const memoryPromise = activeMemoryStore ? activeMemoryStore.getMainMemory() : void 0;
6889
7348
  const todosPromise = this.todoStore ? this.todoStore.get(input.conversationId ?? "__default__") : void 0;
6890
7349
  await this.refreshAgentIfChanged();
6891
7350
  await this.refreshSkillsIfChanged();
7351
+ if (input.tenantId && this.mcpBridge?.hasDeferredServers()) {
7352
+ const newTools = await this.mcpBridge.discoverAndLoadDeferred(input.tenantId);
7353
+ for (const tool of newTools) {
7354
+ this.dispatcher.register(tool);
7355
+ this.registeredMcpToolNames.add(tool.name);
7356
+ }
7357
+ }
6892
7358
  let agent = this.parsedAgent;
6893
7359
  const runId = `run_${randomUUID3()}`;
6894
7360
  const start = now();
@@ -7445,8 +7911,8 @@ ${textContent}` };
7445
7911
  let timer;
7446
7912
  nextPart = await Promise.race([
7447
7913
  fullStreamIterator.next(),
7448
- new Promise((resolve14) => {
7449
- timer = setTimeout(() => resolve14(null), effectiveTimeout);
7914
+ new Promise((resolve15) => {
7915
+ timer = setTimeout(() => resolve15(null), effectiveTimeout);
7450
7916
  })
7451
7917
  ]);
7452
7918
  clearTimeout(timer);
@@ -7664,7 +8130,8 @@ ${textContent}` };
7664
8130
  workingDir: this.workingDir,
7665
8131
  parameters: input.parameters ?? {},
7666
8132
  abortSignal: input.abortSignal,
7667
- conversationId: input.conversationId
8133
+ conversationId: input.conversationId,
8134
+ tenantId: input.tenantId
7668
8135
  };
7669
8136
  const toolResultsForModel = [];
7670
8137
  const richToolResults = [];
@@ -7766,7 +8233,7 @@ ${textContent}` };
7766
8233
  const raced = await Promise.race([
7767
8234
  this.dispatcher.executeBatch(approvedCalls, toolContext),
7768
8235
  new Promise(
7769
- (resolve14) => setTimeout(() => resolve14(TOOL_DEADLINE_SENTINEL), toolDeadlineRemainingMs)
8236
+ (resolve15) => setTimeout(() => resolve15(TOOL_DEADLINE_SENTINEL), toolDeadlineRemainingMs)
7770
8237
  )
7771
8238
  ]);
7772
8239
  if (raced === TOOL_DEADLINE_SENTINEL) {
@@ -8139,8 +8606,8 @@ ${this.skillFingerprint}`;
8139
8606
 
8140
8607
  // src/state.ts
8141
8608
  import { randomUUID as randomUUID4 } from "crypto";
8142
- import { mkdir as mkdir7, readFile as readFile11, readdir as readdir4, rename as rename4, rm as rm4, writeFile as writeFile8 } from "fs/promises";
8143
- import { dirname as dirname7, resolve as resolve13 } from "path";
8609
+ import { mkdir as mkdir8, readFile as readFile12, readdir as readdir4, rename as rename4, rm as rm4, writeFile as writeFile9 } from "fs/promises";
8610
+ import { dirname as dirname8, resolve as resolve14 } from "path";
8144
8611
  var DEFAULT_OWNER = "local-owner";
8145
8612
  var LOCAL_STATE_FILE = "state.json";
8146
8613
  var CONVERSATIONS_DIRECTORY = "conversations";
@@ -8156,9 +8623,9 @@ var toStoreIdentity = async ({
8156
8623
  return { name: ensured.name, id: agentId };
8157
8624
  };
8158
8625
  var writeJsonAtomic4 = async (filePath, payload) => {
8159
- await mkdir7(dirname7(filePath), { recursive: true });
8626
+ await mkdir8(dirname8(filePath), { recursive: true });
8160
8627
  const tmpPath = `${filePath}.tmp`;
8161
- await writeFile8(tmpPath, JSON.stringify(payload, null, 2), "utf8");
8628
+ await writeFile9(tmpPath, JSON.stringify(payload, null, 2), "utf8");
8162
8629
  await rename4(tmpPath, filePath);
8163
8630
  };
8164
8631
  var formatUtcTimestamp = (value) => new Date(value).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
@@ -8254,18 +8721,19 @@ var InMemoryConversationStore = class {
8254
8721
  }
8255
8722
  }
8256
8723
  }
8257
- async list(ownerId) {
8724
+ async list(ownerId, tenantId) {
8258
8725
  this.purgeExpired();
8259
- return Array.from(this.conversations.values()).filter((conversation) => !ownerId || conversation.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt);
8726
+ return Array.from(this.conversations.values()).filter((conversation) => !ownerId || conversation.ownerId === ownerId).filter((c) => tenantId === void 0 || c.tenantId === tenantId).sort((a, b) => b.updatedAt - a.updatedAt);
8260
8727
  }
8261
- async listSummaries(ownerId) {
8728
+ async listSummaries(ownerId, tenantId) {
8262
8729
  this.purgeExpired();
8263
- return Array.from(this.conversations.values()).filter((c) => !ownerId || c.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt).map((c) => ({
8730
+ return Array.from(this.conversations.values()).filter((c) => !ownerId || c.ownerId === ownerId).filter((c) => tenantId === void 0 || c.tenantId === tenantId).sort((a, b) => b.updatedAt - a.updatedAt).map((c) => ({
8264
8731
  conversationId: c.conversationId,
8265
8732
  title: c.title,
8266
8733
  updatedAt: c.updatedAt,
8267
8734
  createdAt: c.createdAt,
8268
8735
  ownerId: c.ownerId,
8736
+ tenantId: c.tenantId,
8269
8737
  parentConversationId: c.parentConversationId,
8270
8738
  messageCount: c.messages.length,
8271
8739
  hasPendingApprovals: Array.isArray(c.pendingApprovals) && c.pendingApprovals.length > 0,
@@ -8276,14 +8744,14 @@ var InMemoryConversationStore = class {
8276
8744
  this.purgeExpired();
8277
8745
  return this.conversations.get(conversationId);
8278
8746
  }
8279
- async create(ownerId = DEFAULT_OWNER, title) {
8747
+ async create(ownerId = DEFAULT_OWNER, title, tenantId = null) {
8280
8748
  const now2 = Date.now();
8281
8749
  const conversation = {
8282
8750
  conversationId: globalThis.crypto?.randomUUID?.() ?? `${now2}-${Math.random()}`,
8283
8751
  title: normalizeTitle(title),
8284
8752
  messages: [],
8285
8753
  ownerId,
8286
- tenantId: null,
8754
+ tenantId,
8287
8755
  createdAt: now2,
8288
8756
  updatedAt: now2
8289
8757
  };
@@ -8347,8 +8815,8 @@ var FileConversationStore = class {
8347
8815
  agentId: this.agentId
8348
8816
  });
8349
8817
  const agentDir = getAgentStoreDirectory(identity);
8350
- const conversationsDir = resolve13(agentDir, CONVERSATIONS_DIRECTORY);
8351
- const indexPath = resolve13(conversationsDir, LOCAL_CONVERSATION_INDEX_FILE);
8818
+ const conversationsDir = resolve14(agentDir, CONVERSATIONS_DIRECTORY);
8819
+ const indexPath = resolve14(conversationsDir, LOCAL_CONVERSATION_INDEX_FILE);
8352
8820
  this.paths = { conversationsDir, indexPath };
8353
8821
  return this.paths;
8354
8822
  }
@@ -8362,9 +8830,9 @@ var FileConversationStore = class {
8362
8830
  }
8363
8831
  async readConversationFile(fileName) {
8364
8832
  const { conversationsDir } = await this.resolvePaths();
8365
- const filePath = resolve13(conversationsDir, fileName);
8833
+ const filePath = resolve14(conversationsDir, fileName);
8366
8834
  try {
8367
- const raw = await readFile11(filePath, "utf8");
8835
+ const raw = await readFile12(filePath, "utf8");
8368
8836
  return JSON.parse(raw);
8369
8837
  } catch {
8370
8838
  return void 0;
@@ -8389,6 +8857,7 @@ var FileConversationStore = class {
8389
8857
  updatedAt: conversation.updatedAt,
8390
8858
  createdAt: conversation.createdAt,
8391
8859
  ownerId: conversation.ownerId,
8860
+ tenantId: conversation.tenantId,
8392
8861
  fileName: entry.name,
8393
8862
  parentConversationId: conversation.parentConversationId,
8394
8863
  messageCount: conversation.messages.length,
@@ -8410,7 +8879,7 @@ var FileConversationStore = class {
8410
8879
  this.loaded = true;
8411
8880
  const { indexPath } = await this.resolvePaths();
8412
8881
  try {
8413
- const raw = await readFile11(indexPath, "utf8");
8882
+ const raw = await readFile12(indexPath, "utf8");
8414
8883
  const parsed = JSON.parse(raw);
8415
8884
  for (const conversation of parsed.conversations ?? []) {
8416
8885
  this.conversations.set(conversation.conversationId, conversation);
@@ -8433,7 +8902,7 @@ var FileConversationStore = class {
8433
8902
  const { conversationsDir } = await this.resolvePaths();
8434
8903
  const existing = this.conversations.get(conversation.conversationId);
8435
8904
  const fileName = existing?.fileName ?? this.resolveConversationFileName(conversation);
8436
- const filePath = resolve13(conversationsDir, fileName);
8905
+ const filePath = resolve14(conversationsDir, fileName);
8437
8906
  this.writing = this.writing.then(async () => {
8438
8907
  await writeJsonAtomic4(filePath, conversation);
8439
8908
  this.conversations.set(conversation.conversationId, {
@@ -8442,6 +8911,7 @@ var FileConversationStore = class {
8442
8911
  updatedAt: conversation.updatedAt,
8443
8912
  createdAt: conversation.createdAt,
8444
8913
  ownerId: conversation.ownerId,
8914
+ tenantId: conversation.tenantId,
8445
8915
  fileName,
8446
8916
  parentConversationId: conversation.parentConversationId,
8447
8917
  messageCount: conversation.messages.length,
@@ -8452,9 +8922,9 @@ var FileConversationStore = class {
8452
8922
  });
8453
8923
  await this.writing;
8454
8924
  }
8455
- async list(ownerId) {
8925
+ async list(ownerId, tenantId) {
8456
8926
  await this.ensureLoaded();
8457
- const summaries = Array.from(this.conversations.values()).filter((conversation) => !ownerId || conversation.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt);
8927
+ const summaries = Array.from(this.conversations.values()).filter((conversation) => !ownerId || conversation.ownerId === ownerId).filter((c) => tenantId === void 0 || (c.tenantId ?? null) === tenantId).sort((a, b) => b.updatedAt - a.updatedAt);
8458
8928
  const conversations = [];
8459
8929
  for (const summary of summaries) {
8460
8930
  const loaded = await this.readConversationFile(summary.fileName);
@@ -8464,14 +8934,15 @@ var FileConversationStore = class {
8464
8934
  }
8465
8935
  return conversations;
8466
8936
  }
8467
- async listSummaries(ownerId) {
8937
+ async listSummaries(ownerId, tenantId) {
8468
8938
  await this.ensureLoaded();
8469
- return Array.from(this.conversations.values()).filter((c) => !ownerId || c.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt).map((c) => ({
8939
+ return Array.from(this.conversations.values()).filter((c) => !ownerId || c.ownerId === ownerId).filter((c) => tenantId === void 0 || (c.tenantId ?? null) === tenantId).sort((a, b) => b.updatedAt - a.updatedAt).map((c) => ({
8470
8940
  conversationId: c.conversationId,
8471
8941
  title: c.title,
8472
8942
  updatedAt: c.updatedAt,
8473
8943
  createdAt: c.createdAt,
8474
8944
  ownerId: c.ownerId,
8945
+ tenantId: c.tenantId,
8475
8946
  parentConversationId: c.parentConversationId,
8476
8947
  messageCount: c.messageCount,
8477
8948
  hasPendingApprovals: c.hasPendingApprovals,
@@ -8486,7 +8957,7 @@ var FileConversationStore = class {
8486
8957
  }
8487
8958
  return await this.readConversationFile(summary.fileName);
8488
8959
  }
8489
- async create(ownerId = DEFAULT_OWNER, title) {
8960
+ async create(ownerId = DEFAULT_OWNER, title, tenantId = null) {
8490
8961
  await this.ensureLoaded();
8491
8962
  const now2 = Date.now();
8492
8963
  const conversation = {
@@ -8494,7 +8965,7 @@ var FileConversationStore = class {
8494
8965
  title: normalizeTitle(title),
8495
8966
  messages: [],
8496
8967
  ownerId,
8497
- tenantId: null,
8968
+ tenantId,
8498
8969
  createdAt: now2,
8499
8970
  updatedAt: now2
8500
8971
  };
@@ -8531,7 +9002,7 @@ var FileConversationStore = class {
8531
9002
  if (removed) {
8532
9003
  this.writing = this.writing.then(async () => {
8533
9004
  if (existing) {
8534
- await rm4(resolve13(conversationsDir, existing.fileName), { force: true });
9005
+ await rm4(resolve14(conversationsDir, existing.fileName), { force: true });
8535
9006
  }
8536
9007
  await this.writeIndex();
8537
9008
  });
@@ -8553,7 +9024,7 @@ var FileConversationStore = class {
8553
9024
  const summary = this.conversations.get(conversationId);
8554
9025
  if (!summary) return void 0;
8555
9026
  const { conversationsDir } = await this.resolvePaths();
8556
- const filePath = resolve13(conversationsDir, summary.fileName);
9027
+ const filePath = resolve14(conversationsDir, summary.fileName);
8557
9028
  let result;
8558
9029
  this.writing = this.writing.then(async () => {
8559
9030
  const conv = await this.readConversationFile(summary.fileName);
@@ -8592,7 +9063,7 @@ var FileStateStore = class {
8592
9063
  workingDir: this.workingDir,
8593
9064
  agentId: this.agentId
8594
9065
  });
8595
- this.filePath = resolve13(getAgentStoreDirectory(identity), LOCAL_STATE_FILE);
9066
+ this.filePath = resolve14(getAgentStoreDirectory(identity), LOCAL_STATE_FILE);
8596
9067
  }
8597
9068
  isExpired(state) {
8598
9069
  return typeof this.ttlMs === "number" && Date.now() - state.updatedAt > this.ttlMs;
@@ -8604,7 +9075,7 @@ var FileStateStore = class {
8604
9075
  }
8605
9076
  this.loaded = true;
8606
9077
  try {
8607
- const raw = await readFile11(this.filePath, "utf8");
9078
+ const raw = await readFile12(this.filePath, "utf8");
8608
9079
  const parsed = JSON.parse(raw);
8609
9080
  for (const state of parsed.states ?? []) {
8610
9081
  this.states.set(state.runId, state);
@@ -8737,10 +9208,10 @@ var KeyValueConversationStoreBase = class {
8737
9208
  return void 0;
8738
9209
  }
8739
9210
  }
8740
- async list(ownerId) {
9211
+ async list(ownerId, tenantId) {
8741
9212
  const kv = await this.client();
8742
9213
  if (!kv) {
8743
- return await this.memoryFallback.list(ownerId);
9214
+ return await this.memoryFallback.list(ownerId, tenantId);
8744
9215
  }
8745
9216
  if (!ownerId) {
8746
9217
  return [];
@@ -8753,16 +9224,19 @@ var KeyValueConversationStoreBase = class {
8753
9224
  for (const raw of rawValues) {
8754
9225
  if (!raw) continue;
8755
9226
  try {
8756
- conversations.push(JSON.parse(raw));
9227
+ const conv = JSON.parse(raw);
9228
+ if (tenantId === void 0 || conv.tenantId === tenantId) {
9229
+ conversations.push(conv);
9230
+ }
8757
9231
  } catch {
8758
9232
  }
8759
9233
  }
8760
9234
  return conversations.sort((a, b) => b.updatedAt - a.updatedAt);
8761
9235
  }
8762
- async listSummaries(ownerId) {
9236
+ async listSummaries(ownerId, tenantId) {
8763
9237
  const kv = await this.client();
8764
9238
  if (!kv) {
8765
- return await this.memoryFallback.listSummaries(ownerId);
9239
+ return await this.memoryFallback.listSummaries(ownerId, tenantId);
8766
9240
  }
8767
9241
  if (!ownerId) {
8768
9242
  return [];
@@ -8776,13 +9250,14 @@ var KeyValueConversationStoreBase = class {
8776
9250
  if (!raw) continue;
8777
9251
  try {
8778
9252
  const meta = JSON.parse(raw);
8779
- if (meta.ownerId === ownerId) {
9253
+ if (meta.ownerId === ownerId && (tenantId === void 0 || (meta.tenantId ?? null) === tenantId)) {
8780
9254
  summaries.push({
8781
9255
  conversationId: meta.conversationId,
8782
9256
  title: meta.title,
8783
9257
  updatedAt: meta.updatedAt,
8784
9258
  createdAt: meta.createdAt,
8785
9259
  ownerId: meta.ownerId,
9260
+ tenantId: meta.tenantId,
8786
9261
  parentConversationId: meta.parentConversationId,
8787
9262
  messageCount: meta.messageCount,
8788
9263
  hasPendingApprovals: meta.hasPendingApprovals,
@@ -8809,14 +9284,14 @@ var KeyValueConversationStoreBase = class {
8809
9284
  return void 0;
8810
9285
  }
8811
9286
  }
8812
- async create(ownerId = DEFAULT_OWNER, title) {
9287
+ async create(ownerId = DEFAULT_OWNER, title, tenantId = null) {
8813
9288
  const now2 = Date.now();
8814
9289
  const conversation = {
8815
9290
  conversationId: globalThis.crypto?.randomUUID?.() ?? `${now2}-${Math.random()}`,
8816
9291
  title: normalizeTitle(title),
8817
9292
  messages: [],
8818
9293
  ownerId,
8819
- tenantId: null,
9294
+ tenantId,
8820
9295
  createdAt: now2,
8821
9296
  updatedAt: now2
8822
9297
  };
@@ -8845,6 +9320,7 @@ var KeyValueConversationStoreBase = class {
8845
9320
  updatedAt: nextConversation.updatedAt,
8846
9321
  createdAt: nextConversation.createdAt,
8847
9322
  ownerId: nextConversation.ownerId,
9323
+ tenantId: nextConversation.tenantId,
8848
9324
  parentConversationId: nextConversation.parentConversationId,
8849
9325
  messageCount: nextConversation.messages.length,
8850
9326
  hasPendingApprovals: Array.isArray(nextConversation.pendingApprovals) && nextConversation.pendingApprovals.length > 0,
@@ -9304,6 +9780,32 @@ var createConversationStore = (config, options) => {
9304
9780
  return new InMemoryConversationStore(ttl);
9305
9781
  };
9306
9782
 
9783
+ // src/tenant-token.ts
9784
+ import { jwtVerify } from "jose";
9785
+ async function verifyTenantToken(signingKey, token) {
9786
+ try {
9787
+ const secret = new TextEncoder().encode(signingKey);
9788
+ const { payload } = await jwtVerify(token, secret, {
9789
+ algorithms: ["HS256"]
9790
+ });
9791
+ const tenantId = payload.sub;
9792
+ if (!tenantId || typeof tenantId !== "string") {
9793
+ return void 0;
9794
+ }
9795
+ const metadata = extractMetadata(payload);
9796
+ return { tenantId, metadata };
9797
+ } catch {
9798
+ return void 0;
9799
+ }
9800
+ }
9801
+ function extractMetadata(payload) {
9802
+ const meta = payload.meta;
9803
+ if (meta && typeof meta === "object" && !Array.isArray(meta)) {
9804
+ return meta;
9805
+ }
9806
+ return void 0;
9807
+ }
9808
+
9307
9809
  // src/index.ts
9308
9810
  import { defineTool as defineTool9 } from "@poncho-ai/sdk";
9309
9811
  export {
@@ -9334,6 +9836,7 @@ export {
9334
9836
  createReminderStore,
9335
9837
  createReminderTools,
9336
9838
  createSearchTools,
9839
+ createSecretsStore,
9337
9840
  createSkillTools,
9338
9841
  createStateStore,
9339
9842
  createSubagentTools,
@@ -9368,10 +9871,12 @@ export {
9368
9871
  renderAgentPrompt,
9369
9872
  resolveAgentIdentity,
9370
9873
  resolveCompactionConfig,
9874
+ resolveEnv,
9371
9875
  resolveMemoryConfig,
9372
9876
  resolveSkillDirs,
9373
9877
  resolveStateConfig,
9374
9878
  slugifyStorageComponent,
9375
9879
  startOpenAICodexDeviceAuth,
9880
+ verifyTenantToken,
9376
9881
  writeOpenAICodexSession
9377
9882
  };