@integrity-labs/agt-cli 0.27.8 → 0.27.9-test.11

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.
Files changed (28) hide show
  1. package/dist/assets/impersonate-statusline.sh +7 -2
  2. package/dist/bin/agt.js +946 -129
  3. package/dist/bin/agt.js.map +1 -1
  4. package/dist/{chunk-YSBGIXJG.js → chunk-45I2X4M7.js} +361 -26
  5. package/dist/chunk-45I2X4M7.js.map +1 -0
  6. package/dist/{chunk-IB655E5U.js → chunk-N2EBL4PZ.js} +1086 -285
  7. package/dist/chunk-N2EBL4PZ.js.map +1 -0
  8. package/dist/{chunk-GN4XPQWJ.js → chunk-SJPAXXTW.js} +535 -94
  9. package/dist/chunk-SJPAXXTW.js.map +1 -0
  10. package/dist/{claude-pair-runtime-ZBQKBBMT.js → claude-pair-runtime-OUGV74LY.js} +2 -2
  11. package/dist/lib/manager-worker.js +2071 -517
  12. package/dist/lib/manager-worker.js.map +1 -1
  13. package/dist/mcp/direct-chat-channel.js +49 -455
  14. package/dist/mcp/index.js +232 -137
  15. package/dist/mcp/slack-channel.js +1379 -856
  16. package/dist/mcp/teams-channel.js +16018 -0
  17. package/dist/mcp/telegram-channel.js +1264 -618
  18. package/dist/{persistent-session-ICYFLUAM.js → persistent-session-G3MR7JVI.js} +5 -3
  19. package/dist/responsiveness-probe-UHSBETBR.js +74 -0
  20. package/dist/responsiveness-probe-UHSBETBR.js.map +1 -0
  21. package/package.json +2 -2
  22. package/dist/chunk-GN4XPQWJ.js.map +0 -1
  23. package/dist/chunk-IB655E5U.js.map +0 -1
  24. package/dist/chunk-YSBGIXJG.js.map +0 -1
  25. package/dist/responsiveness-probe-WZNQ2762.js +0 -33
  26. package/dist/responsiveness-probe-WZNQ2762.js.map +0 -1
  27. /package/dist/{claude-pair-runtime-ZBQKBBMT.js.map → claude-pair-runtime-OUGV74LY.js.map} +0 -0
  28. /package/dist/{persistent-session-ICYFLUAM.js.map → persistent-session-G3MR7JVI.js.map} +0 -0
@@ -9,7 +9,7 @@ import {
9
9
  parseDeliveryTarget,
10
10
  registerFramework,
11
11
  wrapScheduledTaskPrompt
12
- } from "./chunk-YSBGIXJG.js";
12
+ } from "./chunk-45I2X4M7.js";
13
13
 
14
14
  // ../../packages/core/dist/integrations/registry.js
15
15
  var INTEGRATION_REGISTRY = [
@@ -136,6 +136,40 @@ var INTEGRATION_REGISTRY = [
136
136
  docs_url: "https://docs.granola.ai/docs/api/mcp",
137
137
  beta: true
138
138
  },
139
+ {
140
+ id: "anchor-browser",
141
+ name: "Anchor Browser",
142
+ category: "workspace-productivity",
143
+ description: "Cloud browser for agents \u2014 drive any website that lacks an API (LinkedIn, Sales Navigator, supplier portals) via a hosted, stealth Chromium with persistent-login profiles. Wired as Anchor's HOSTED streamable-HTTP MCP at https://api.anchorbrowser.io/mcp.",
144
+ // ENG-5855: api-key header auth (NOT OAuth, NOT a local stdio package).
145
+ // The manager writes ANCHOR_BROWSER_API_KEY to .env.integrations from the
146
+ // stored api_key credential; the hosted MCP authenticates on the
147
+ // `anchor-api-key` header. The `anchor-session-id` header binds an
148
+ // authenticated profile session — its value is minted per-session by the
149
+ // manager (ENG-5857); until then `envDefaults` seeds it empty so
150
+ // stateless browsing works and no literal `${...}` placeholder ships.
151
+ // Tool surface (25 `anchor_*` tools) is the hosted MCP's, validated in
152
+ // the ENG-5854 spike (docs/spikes/eng-5854-anchor-browser-persistent-login.md).
153
+ supported_auth_types: ["api_key"],
154
+ capabilities: [
155
+ { id: "anchor-browser:browse", name: "Browse & Read", description: "Navigate and read pages \u2014 snapshot, screenshot, page HTML, tabs, console, network requests, wait (anchor_navigate, anchor_snapshot, anchor_take_screenshot, anchor_get_body_html, anchor_tab_list, anchor_console_messages, anchor_network_requests, anchor_wait_for, anchor_navigate_back/forward)", access: "read" },
156
+ { id: "anchor-browser:interact", name: "Interact", description: "Act on pages \u2014 click, type, hover, drag, select options, press keys, handle dialogs, upload files, resize, manage tabs (anchor_click, anchor_type, anchor_hover, anchor_drag, anchor_select_option, anchor_press_key, anchor_handle_dialog, anchor_file_upload, anchor_resize, anchor_tab_new/select/close, anchor_close)", access: "write" },
157
+ { id: "anchor-browser:export", name: "Export & Codegen", description: "Save the current page as PDF and generate Playwright code for a scenario (anchor_pdf_save, anchor_generate_playwright_code)", access: "write" }
158
+ ],
159
+ docs_url: "https://docs.anchorbrowser.io/introduction",
160
+ beta: true,
161
+ remoteMcp: {
162
+ type: "http",
163
+ url: "https://api.anchorbrowser.io/mcp",
164
+ headers: {
165
+ "anchor-api-key": "${ANCHOR_BROWSER_API_KEY}",
166
+ "anchor-session-id": "${ANCHOR_BROWSER_SESSION_ID}"
167
+ },
168
+ // ENG-5857 mints the real session id; default empty so the header
169
+ // resolves cleanly (no profile bound → ephemeral session) until then.
170
+ envDefaults: { ANCHOR_BROWSER_SESSION_ID: "" }
171
+ }
172
+ },
139
173
  {
140
174
  id: "postiz",
141
175
  name: "Postiz",
@@ -174,7 +208,15 @@ var INTEGRATION_REGISTRY = [
174
208
  { id: "qmd:search", name: "Search Memory", description: "Semantic + keyword search over indexed memory files", access: "read" },
175
209
  { id: "qmd:get", name: "Get Memory", description: "Read memory files by path and line range", access: "read" }
176
210
  ],
177
- beta: true
211
+ beta: true,
212
+ // ENG-5815: migrated from buildMcpJson's hardcoded if-block. qmd is
213
+ // the simplest of the four pre-data-driven entries — no env, no
214
+ // conditional logic, just `qmd mcp`. The byte-identical render is
215
+ // pinned by claudecode-qmd-data-driven.test.ts.
216
+ nativeMcp: {
217
+ command: "qmd",
218
+ args: ["mcp"]
219
+ }
178
220
  },
179
221
  {
180
222
  id: "v0",
@@ -310,6 +352,35 @@ var INTEGRATION_REGISTRY = [
310
352
  },
311
353
  docs_url: "https://www.coderabbit.ai/cli"
312
354
  },
355
+ {
356
+ id: "aws",
357
+ name: "AWS",
358
+ category: "infrastructure",
359
+ description: "Amazon Web Services \u2014 query AWS APIs (EC2, S3, IAM, Lambda, etc.) via AWS Labs' official AWS API MCP server",
360
+ supported_auth_types: ["api_key", "managed", "none"],
361
+ capabilities: [
362
+ { id: "aws:read", name: "Read AWS Resources", description: "List and describe AWS resources across services (EC2, S3, IAM, Lambda, \u2026)", access: "read" },
363
+ { id: "aws:write", name: "Write AWS Resources", description: "Create and update AWS resources. Pair with an aws-no-destructive-ops guardrail.", access: "write" }
364
+ ],
365
+ docs_url: "https://github.com/awslabs/mcp/tree/main/src/aws-api-mcp-server",
366
+ beta: true,
367
+ // ENG-5815: first integration shipped purely via the data-driven
368
+ // path — buildMcpJson never grew an `aws` if-block. The AWS Labs
369
+ // AWS API MCP server runs through uvx (Python tooling), which the
370
+ // host bootstrap installs alongside python3. Credentials are
371
+ // resolved via the standard AWS_* env / shared credentials file
372
+ // chain on the host; the spec doesn't override them.
373
+ nativeMcp: {
374
+ command: "uvx",
375
+ args: ["awslabs.aws-api-mcp-server@latest"],
376
+ env: {
377
+ AWS_REGION: "{{empty_if_no_env.AWS_REGION}}",
378
+ AWS_PROFILE: "{{empty_if_no_env.AWS_PROFILE}}",
379
+ PATH: "{{process_env.PATH}}",
380
+ HOME: "{{process_env.HOME}}"
381
+ }
382
+ }
383
+ },
313
384
  {
314
385
  id: "custom",
315
386
  name: "Custom Integration",
@@ -2285,6 +2356,299 @@ function extractAllowedDomains(input) {
2285
2356
  }
2286
2357
  registerFramework(nemoClawAdapter);
2287
2358
 
2359
+ // ../../packages/core/dist/provisioning/mcp-config-guards.js
2360
+ import { chmodSync as chmodSync4, existsSync as existsSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3 } from "fs";
2361
+
2362
+ // ../../packages/core/dist/provisioning/mcp-secret-lint.js
2363
+ var LITERAL_SECRET_PATTERNS = [
2364
+ // Slack bot token — `xoxb-<workspace>-<...>`
2365
+ { name: "slack_bot_token", re: /^xoxb-/ },
2366
+ // Slack app-level token (Socket Mode) — `xapp-<...>`
2367
+ { name: "slack_app_token", re: /^xapp-/ },
2368
+ // AGT host API key — `tlk_<...>` (see claudecode-plugin-augmented README).
2369
+ { name: "agt_host_api_key", re: /^tlk_/ },
2370
+ // Composio / generic api-key prefix — `ak_<...>`
2371
+ { name: "composio_api_key", re: /^ak_/ },
2372
+ // Telegram bot token — `<bot id>:AA<...>` (BotFather format). ENG-5901
2373
+ // PR 4: the original `\d{10}:AAE` (from the issue AC) was too narrow —
2374
+ // live tokens on agt-aws-1 carry `AA` + a varying third character
2375
+ // (don/scout/stirling all had AA-not-E tokens the lint and migration
2376
+ // missed). Bot ids are 8–12 digits; the token part always starts `AA`.
2377
+ { name: "telegram_bot_token", re: /^\d{8,12}:AA[A-Za-z0-9_-]/ },
2378
+ // ENG-5901 extension beyond the original AC's five patterns: a literal
2379
+ // JWT (`eyJ...`) is the shape of a leaked AGT_API_KEY, which the
2380
+ // value-prefix patterns above would otherwise miss. Header values often
2381
+ // carry it behind `Bearer ` (or a copy-pasted `Authorization: Bearer `),
2382
+ // so those prefixes are optionally consumed (CodeRabbit #1731).
2383
+ // Templates (`Bearer ${AGT_API_KEY}`) and concrete non-secret values
2384
+ // (UUIDs, hosts) never put `eyJ` after the prefix, so this stays
2385
+ // false-positive-safe inside .mcp.json.
2386
+ { name: "jwt_agt_api_key", re: /^(?:authorization:\s*)?(?:bearer\s+)?eyJ[A-Za-z0-9_-]+\./i }
2387
+ ];
2388
+ function matchLiteralSecret(value) {
2389
+ for (const { name, re } of LITERAL_SECRET_PATTERNS) {
2390
+ if (re.test(value))
2391
+ return name;
2392
+ }
2393
+ return null;
2394
+ }
2395
+ function scanRecord(server, record, location, findings) {
2396
+ if (!record)
2397
+ return;
2398
+ for (const [field, value] of Object.entries(record)) {
2399
+ if (typeof value !== "string")
2400
+ continue;
2401
+ const pattern = matchLiteralSecret(value);
2402
+ if (pattern)
2403
+ findings.push({ server, field, location, pattern });
2404
+ }
2405
+ }
2406
+ function scanConfigForLiteralSecrets(config) {
2407
+ const findings = [];
2408
+ if (typeof config !== "object" || config === null)
2409
+ return findings;
2410
+ const servers = config.mcpServers;
2411
+ if (typeof servers !== "object" || servers === null)
2412
+ return findings;
2413
+ for (const [server, raw] of Object.entries(servers)) {
2414
+ if (typeof raw !== "object" || raw === null)
2415
+ continue;
2416
+ const entry = raw;
2417
+ scanRecord(server, entry.env, "env", findings);
2418
+ scanRecord(server, entry.headers, "header", findings);
2419
+ }
2420
+ return findings;
2421
+ }
2422
+ function formatLiteralSecretRejection(f) {
2423
+ return `[mcp-write] [literal-secret-rejected] field=${f.field} server=${f.server} location=${f.location} pattern=${f.pattern}`;
2424
+ }
2425
+
2426
+ // ../../packages/core/dist/provisioning/mcp-config-guards.js
2427
+ var MCP_FILE_MODE = 384;
2428
+ var lastRejectionFingerprintByPath = /* @__PURE__ */ new Map();
2429
+ var REQUIRED_ENV_RULES_BY_SERVER = {
2430
+ "cloud-broker": [
2431
+ { key: "AGT_HOST", mustBeConcrete: false },
2432
+ { key: "AGT_API_KEY", mustBeConcrete: false },
2433
+ // ENG-4744: this is the bug shape — writer used to omit this
2434
+ // entirely, or render it as a literal `${AGT_AGENT_ID}` instead
2435
+ // of the agent's real UUID. The broker has no way to substitute
2436
+ // it post-spawn, so a placeholder here = silently broken agent.
2437
+ { key: "AGT_AGENT_ID", mustBeConcrete: true }
2438
+ ]
2439
+ };
2440
+ var PLACEHOLDER_RE = /\$\{[^}]+\}/;
2441
+ function validateRenderedMcpConfig(config) {
2442
+ const errors = [];
2443
+ if (typeof config !== "object" || config === null || Array.isArray(config)) {
2444
+ return {
2445
+ ok: false,
2446
+ errors: [
2447
+ {
2448
+ kind: "invalid_json_shape",
2449
+ server: "*",
2450
+ message: "config root must be a non-null object"
2451
+ }
2452
+ ]
2453
+ };
2454
+ }
2455
+ const root = config;
2456
+ if (root.mcpServers === void 0) {
2457
+ return { ok: true };
2458
+ }
2459
+ if (typeof root.mcpServers !== "object" || root.mcpServers === null) {
2460
+ return {
2461
+ ok: false,
2462
+ errors: [
2463
+ {
2464
+ kind: "invalid_json_shape",
2465
+ server: "*",
2466
+ message: "mcpServers must be an object"
2467
+ }
2468
+ ]
2469
+ };
2470
+ }
2471
+ if (Array.isArray(root.mcpServers)) {
2472
+ return {
2473
+ ok: false,
2474
+ errors: [
2475
+ {
2476
+ kind: "invalid_json_shape",
2477
+ server: "*",
2478
+ message: "mcpServers must be an object"
2479
+ }
2480
+ ]
2481
+ };
2482
+ }
2483
+ for (const [serverKey, raw] of Object.entries(root.mcpServers)) {
2484
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
2485
+ errors.push({
2486
+ kind: "invalid_json_shape",
2487
+ server: serverKey,
2488
+ message: `entry must be an object`
2489
+ });
2490
+ continue;
2491
+ }
2492
+ const entry = raw;
2493
+ const entryRecord = entry;
2494
+ if (typeof entryRecord["url"] === "string") {
2495
+ const type = entryRecord["type"];
2496
+ if (type !== "http" && type !== "sse") {
2497
+ errors.push({
2498
+ kind: "remote_mcp_missing_type",
2499
+ server: serverKey,
2500
+ message: `url-based entry must include \`type: "http"\` or \`type: "sse"\` (Claude Code MCP schema requires it)`
2501
+ });
2502
+ }
2503
+ }
2504
+ const rules = REQUIRED_ENV_RULES_BY_SERVER[serverKey];
2505
+ if (rules) {
2506
+ const env = entry.env ?? {};
2507
+ for (const rule of rules) {
2508
+ const value = env[rule.key];
2509
+ if (typeof value !== "string" || value.length === 0) {
2510
+ errors.push({
2511
+ kind: "missing_required_env",
2512
+ server: serverKey,
2513
+ message: `missing required env key: ${rule.key}`
2514
+ });
2515
+ continue;
2516
+ }
2517
+ if (rule.mustBeConcrete && PLACEHOLDER_RE.test(value)) {
2518
+ errors.push({
2519
+ kind: "unexpanded_placeholder",
2520
+ server: serverKey,
2521
+ message: `env.${rule.key} contains an unexpanded \${...} placeholder; expected a concrete value`
2522
+ });
2523
+ }
2524
+ }
2525
+ }
2526
+ }
2527
+ return errors.length === 0 ? { ok: true } : { ok: false, errors };
2528
+ }
2529
+ function formatValidationErrors(errors) {
2530
+ return errors.map((e) => `${e.server}:${e.kind}=${e.message}`).join("; ");
2531
+ }
2532
+ function safeWriteJsonAtomic(path, content, opts = {}) {
2533
+ const keepBackup = opts.keepBackup !== false;
2534
+ const rename = opts.renamer ?? renameSync3;
2535
+ const tmpPath = `${path}.new`;
2536
+ const bakPath = `${path}.bak`;
2537
+ let movedOriginalToBackup = false;
2538
+ try {
2539
+ writeFileSync4(tmpPath, content);
2540
+ if (opts.mode !== void 0) {
2541
+ chmodSync4(tmpPath, opts.mode);
2542
+ }
2543
+ } catch (err) {
2544
+ try {
2545
+ if (existsSync4(tmpPath))
2546
+ unlinkSync3(tmpPath);
2547
+ } catch {
2548
+ }
2549
+ throw err;
2550
+ }
2551
+ try {
2552
+ if (keepBackup && existsSync4(path)) {
2553
+ rename(path, bakPath);
2554
+ movedOriginalToBackup = true;
2555
+ }
2556
+ rename(tmpPath, path);
2557
+ } catch (err) {
2558
+ try {
2559
+ if (existsSync4(tmpPath))
2560
+ unlinkSync3(tmpPath);
2561
+ } catch {
2562
+ }
2563
+ if (movedOriginalToBackup && !existsSync4(path) && existsSync4(bakPath)) {
2564
+ try {
2565
+ renameSync3(bakPath, path);
2566
+ } catch {
2567
+ }
2568
+ }
2569
+ throw err;
2570
+ }
2571
+ }
2572
+ function safeWriteMcpJson(path, config) {
2573
+ const validation = validateRenderedMcpConfig(config);
2574
+ if (!validation.ok) {
2575
+ return { written: false, errors: validation.errors };
2576
+ }
2577
+ const secretFindings = scanConfigForLiteralSecrets(config);
2578
+ if (secretFindings.length > 0) {
2579
+ const fingerprint = secretFindings.map((f) => `${f.server}.${f.field}.${f.location}`).sort().join("|");
2580
+ if (lastRejectionFingerprintByPath.get(path) !== fingerprint) {
2581
+ lastRejectionFingerprintByPath.set(path, fingerprint);
2582
+ for (const f of secretFindings) {
2583
+ process.stderr.write(`${formatLiteralSecretRejection(f)}
2584
+ `);
2585
+ }
2586
+ }
2587
+ return {
2588
+ written: false,
2589
+ errors: secretFindings.map((f) => ({
2590
+ kind: "literal_secret",
2591
+ server: f.server,
2592
+ message: `literal secret in ${f.location} field '${f.field}' (pattern ${f.pattern}); expected a \${VAR} template`
2593
+ }))
2594
+ };
2595
+ }
2596
+ lastRejectionFingerprintByPath.delete(path);
2597
+ safeWriteJsonAtomic(path, JSON.stringify(config, null, 2), { mode: MCP_FILE_MODE });
2598
+ return { written: true, errors: [] };
2599
+ }
2600
+ var SECRET_FIELD_NAME_RE = /TOKEN|KEY|SECRET|BEARER|PASSWORD/i;
2601
+ function collectSecretFields(config) {
2602
+ const out = /* @__PURE__ */ new Map();
2603
+ if (typeof config !== "object" || config === null)
2604
+ return out;
2605
+ const servers = config.mcpServers;
2606
+ if (typeof servers !== "object" || servers === null)
2607
+ return out;
2608
+ for (const [server, raw] of Object.entries(servers)) {
2609
+ if (typeof raw !== "object" || raw === null)
2610
+ continue;
2611
+ const entry = raw;
2612
+ for (const [block, location] of [
2613
+ [entry.env, "env"],
2614
+ [entry.headers, "header"]
2615
+ ]) {
2616
+ if (!block)
2617
+ continue;
2618
+ for (const [field, value] of Object.entries(block)) {
2619
+ if (typeof value !== "string")
2620
+ continue;
2621
+ if (!SECRET_FIELD_NAME_RE.test(field))
2622
+ continue;
2623
+ out.set(`${server}\0${field}`, { location, value });
2624
+ }
2625
+ }
2626
+ }
2627
+ return out;
2628
+ }
2629
+ function mcpMirrorParityErrors(provision2, project) {
2630
+ const a = collectSecretFields(provision2);
2631
+ const b = collectSecretFields(project);
2632
+ const mismatches = [];
2633
+ const keys = /* @__PURE__ */ new Set([...a.keys(), ...b.keys()]);
2634
+ for (const key of keys) {
2635
+ const [server, field] = key.split("\0");
2636
+ const pv = a.get(key);
2637
+ const pj = b.get(key);
2638
+ if (pv && !pj) {
2639
+ mismatches.push({ server, field, location: pv.location, reason: "missing-in-project" });
2640
+ } else if (!pv && pj) {
2641
+ mismatches.push({ server, field, location: pj.location, reason: "missing-in-provision" });
2642
+ } else if (pv && pj && pv.value !== pj.value) {
2643
+ mismatches.push({ server, field, location: pv.location, reason: "value-diverges" });
2644
+ }
2645
+ }
2646
+ return mismatches;
2647
+ }
2648
+ function formatMirrorMismatch(m) {
2649
+ return `[mcp-mirror] [parity-violation] server=${m.server} field=${m.field} location=${m.location} reason=${m.reason}`;
2650
+ }
2651
+
2288
2652
  // ../../packages/core/dist/provisioning/frameworks/claudecode/identity.js
2289
2653
  function buildMemorySection(hasQmd) {
2290
2654
  const recall = hasQmd ? `### Recall
@@ -2533,15 +2897,15 @@ later tick:
2533
2897
  - For long-form output (>500 chars), lead with a one-line summary,
2534
2898
  then a blank line, then the full content. Channel formatters
2535
2899
  truncate gracefully but the lede always lands.
2536
- - **\`kanban_fail\` with a reason** when something went wrong
2537
- (missing access, credential failure, tool error). The work was
2538
- attempted but failed.
2539
- - **\`kanban_cancel\` with a reason** when you started, then realised
2540
- the work isn't actually needed (precondition no longer holds,
2541
- duplicate of another task, asker changed their mind, data was
2542
- already current). This is distinct from \`kanban_fail\`: nothing
2543
- went wrong; you wisely didn't bother. Use this generously \u2014 it's
2544
- cheaper than running unneeded work to a forced "done".
2900
+ - **\`kanban_move\` with \`status="failed"\`** (pass a \`notes\` reason)
2901
+ when something went wrong \u2014 missing access, credential failure, tool
2902
+ error. The work was attempted but couldn't complete.
2903
+ - **No-longer-needed work**: there is no "cancelled" status, so close it
2904
+ with **\`kanban_done\`** and a \`result\` that says why it wasn't
2905
+ needed (precondition no longer holds, duplicate of another task, asker
2906
+ changed their mind, data was already current). Do this generously \u2014
2907
+ it's cheaper than running unneeded work, and the result line tells the
2908
+ user you consciously stood it down rather than silently dropping it.
2545
2909
  - **\`kanban_update\` with notes** if you're blocked but might unblock
2546
2910
  later; leave the row in \`in_progress\` and pick up other work.
2547
2911
 
@@ -2644,42 +3008,6 @@ If your charter doesn't authorise team-scope skill writes
2644
3008
  refused server-side \u2014 surface that error to the user rather than
2645
3009
  falling back to a local-disk write.
2646
3010
 
2647
- `;
2648
- }
2649
- function buildProgressHeartbeatSection(resolvedChannels) {
2650
- const hasSlack = !!resolvedChannels?.includes("slack");
2651
- const hasDirectChat = !!resolvedChannels?.includes("direct-chat");
2652
- if (!hasSlack && !hasDirectChat)
2653
- return "";
2654
- const bullets = [];
2655
- if (hasSlack) {
2656
- bullets.push("- **Slack threads:** `slack_progress_start` (channel + thread_ts from the inbound `<channel>` tag, label = short task name) \u2192 `slack_progress_update` between steps (pass `step_label`, optional `step_current`/`step_total`) \u2192 `slack_progress_complete` (summary) or `slack_progress_fail` (one-sentence reason).");
2657
- }
2658
- if (hasDirectChat) {
2659
- bullets.push("- **Direct Chat sessions:** `direct_chat_progress_start` (session_id from the inbound tag, label) \u2192 `direct_chat_progress_update` \u2192 `direct_chat_progress_complete` / `_fail`.");
2660
- }
2661
- return `
2662
- ## Progress heartbeats during multi-step work
2663
-
2664
- When a task involves **\u22653 tool calls or is likely to take >5 seconds**
2665
- of tool work, open an in-place progress anchor instead of streaming
2666
- "working on\u2026" / "still working\u2026" / "done" messages. The operator sees
2667
- one message that ticks forward as you advance, not a thread flooded
2668
- with status pings.
2669
-
2670
- ${bullets.join("\n")}
2671
-
2672
- The state machine debounces updates to roughly one API call per second
2673
- per channel, so it's safe to call \`_update\` after every step rather
2674
- than batching. Always end with \`_complete\` or \`_fail\` \u2014 leaving an
2675
- anchor in the "working" state means a future stale-state sweep flips it
2676
- to \u26A0\uFE0F "agent unresponsive" once the heartbeat goes cold.
2677
-
2678
- **Skip the heartbeat for one-shot replies.** A single \`slack.reply\` /
2679
- \`direct_chat.reply\` is still the right pattern when there's no
2680
- multi-step work to surface. Heartbeats are for the case where you'd
2681
- otherwise emit several intermediate messages.
2682
-
2683
3011
  `;
2684
3012
  }
2685
3013
  function buildPersonalitySection(seed) {
@@ -2900,6 +3228,37 @@ function formatConfigLines(config) {
2900
3228
  return ` - ${k}: ${rendered}`;
2901
3229
  });
2902
3230
  }
3231
+ var EMAIL_DOMAIN_RESTRICT_DEF = "email.domain_restrict";
3232
+ function stringList(value) {
3233
+ return Array.isArray(value) ? value.filter((d) => typeof d === "string") : [];
3234
+ }
3235
+ function formatEmailDomainConfigLines(config) {
3236
+ const mode = typeof config.mode === "string" ? config.mode : void 0;
3237
+ const lines = [];
3238
+ if (mode)
3239
+ lines.push(` - mode: ${mode}`);
3240
+ if (mode === "allowlist") {
3241
+ const allowed = stringList(config.allowed_domains);
3242
+ lines.push(` - allowed_domains: ${allowed.length ? allowed.join(", ") : "(none \u2014 no external email permitted)"}`);
3243
+ } else if (mode === "blocklist") {
3244
+ const blocked = stringList(config.blocked_domains);
3245
+ lines.push(` - blocked_domains: ${blocked.length ? blocked.join(", ") : "(none)"}`);
3246
+ } else if (mode === "internal_only") {
3247
+ lines.push(` - only your organization's own email domain is permitted`);
3248
+ }
3249
+ lines.push(` *Advisory: honor this restriction \u2014 it is guidance in your instructions, not a hard block at send time.*`);
3250
+ return lines;
3251
+ }
3252
+ var CALENDAR_CONFIDENTIALITY_DEF = "calendar.confidentiality";
3253
+ function formatCalendarConfidentialityLines(config) {
3254
+ const stage = typeof config.stage === "string" ? config.stage : "shadow";
3255
+ const lines = [` - stage: ${stage}`];
3256
+ lines.push(` *Cross-turn re-query: when answering a different person about your principal's calendar, ALWAYS re-query the calendar tool \u2014 never reuse meeting details remembered from an earlier turn that involved a different recipient. The tool's response is filtered per-recipient at the API layer; relying on memory bypasses the filter.*`);
3257
+ if (stage !== "enforce") {
3258
+ lines.push(` *Currently observe-only \u2014 the API records redaction decisions to guardrail_audit_log but returns calendar responses unchanged. Treat the policy as authoritative anyway; the runtime flip is operator-side.*`);
3259
+ }
3260
+ return lines;
3261
+ }
2903
3262
  function renderGuardrailBullet(g) {
2904
3263
  const lines = [];
2905
3264
  const header = `- **${g.displayName}** (${g.category}, from ${g.source})`;
@@ -2907,16 +3266,26 @@ function renderGuardrailBullet(g) {
2907
3266
  if (g.description?.trim()) {
2908
3267
  lines.push(` ${g.description.trim()}`);
2909
3268
  }
2910
- lines.push(...formatConfigLines(g.config));
3269
+ lines.push(...g.definitionId === EMAIL_DOMAIN_RESTRICT_DEF ? formatEmailDomainConfigLines(g.config) : g.definitionId === CALENDAR_CONFIDENTIALITY_DEF ? formatCalendarConfidentialityLines(g.config) : formatConfigLines(g.config));
2911
3270
  if (g.overrideApplied && g.overrideReason?.trim()) {
2912
3271
  lines.push(` *Override applied: ${g.overrideReason.trim()}*`);
2913
3272
  }
2914
3273
  return lines.join("\n");
2915
3274
  }
3275
+ function effectiveCalendarEnforcement(g) {
3276
+ if (g.definitionId !== CALENDAR_CONFIDENTIALITY_DEF)
3277
+ return g.enforcement;
3278
+ const stage = typeof g.config?.["stage"] === "string" ? g.config["stage"] : "shadow";
3279
+ if (stage === "enforce")
3280
+ return "enforce";
3281
+ if (stage === "warn" && g.enforcement !== "enforce")
3282
+ return "warn";
3283
+ return g.enforcement;
3284
+ }
2916
3285
  function buildGuardrailsSection(guardrails) {
2917
3286
  if (!guardrails || guardrails.length === 0)
2918
3287
  return "";
2919
- const active = guardrails.filter((g) => g.enforcement !== "disabled");
3288
+ const active = guardrails.map((g) => ({ ...g, enforcement: effectiveCalendarEnforcement(g) })).filter((g) => g.enforcement !== "disabled");
2920
3289
  if (active.length === 0)
2921
3290
  return "";
2922
3291
  const enforce = active.filter((g) => g.enforcement === "enforce");
@@ -2992,34 +3361,73 @@ acknowledge before you start.
2992
3361
  - **FAST (< 60s):** handle inline. Reply via the channel tool
2993
3362
  (slack.reply / telegram.reply / directchat.reply) and end your turn.
2994
3363
 
2995
- - **SLOW (\u2265 60s):** dispatch.
3364
+ - **SLOW (\u2265 60s):** acknowledge first, then handle inline.
2996
3365
  1. Send a one-line acknowledgement via the channel tool \u2014 short, warm,
2997
3366
  and tell the user you'll come back. Example shape (don't copy verbatim,
2998
3367
  match your voice): "On it \u2014 this'll take a minute or two, I'll ping
2999
3368
  when it's done."
3000
- 2. Invoke the \`channel-message-handler\` subagent (Task tool, with
3001
- \`subagent_type: channel-message-handler\`) with the full original
3002
- message text plus the **channel-specific routing keys** the reply
3003
- tool needs: Slack threads \u2192 \`{ channel_id, thread_ts }\`; Slack
3004
- non-thread DMs \u2192 \`{ channel_id }\`; Telegram \u2192 \`{ chat_id, message_id }\`;
3005
- Direct Chat \u2192 \`{ conversation_id }\`. Pass these verbatim from the
3006
- inbound \`<channel>\` tag \u2014 don't substitute a generic \`message_ts\`,
3007
- since only Slack threads use it. The subagent will do the actual
3008
- work and post the real reply itself.
3009
- 3. End your turn. **Do NOT call slack.reply / telegram.reply with the
3010
- full result yourself** \u2014 that's the subagent's job.
3011
-
3012
- **Why this matters more than any other instruction below:** if you handle
3013
- slow requests inline, you go silent for 60+ seconds while operators send
3014
- follow-up messages that queue behind you. Dispatching keeps you free to
3015
- acknowledge new pings. The kanban-tracking-link convention, work-management
3016
- "create a task" guidance, and Slack reply patterns ALL apply to fast
3017
- inline replies \u2014 they do NOT replace this dispatch decision.
3018
-
3019
- If the work turns out to be unexpectedly slow after you started inline,
3020
- finish the current sub-step, then dispatch the rest. Don't apologise for
3021
- mid-task switching \u2014 operators care about responsiveness, not consistency.
3022
- ${buildProgressHeartbeatSection(resolvedChannels)}
3369
+ 2. Do the work yourself in this same session. Use whatever tools you
3370
+ need (MCP, skills, file reads, etc.) \u2014 your parent session has the
3371
+ full MCP surface bound.
3372
+ 3. Reply with the result via the channel tool (\`slack.reply\` /
3373
+ \`telegram.reply\` / \`directchat.reply\`), addressing the same thread
3374
+ / chat / conversation you acknowledged in step 1.
3375
+
3376
+ > **Why inline and not sub-agent dispatch right now:** there is an
3377
+ > upstream Claude Code bug
3378
+ > ([anthropics/claude-code#64909](https://github.com/anthropics/claude-code/issues/64909))
3379
+ > where sub-agents dispatched via the Task tool with an explicit
3380
+ > \`tools:\` allowlist (which is the shape \`channel-message-handler\`
3381
+ > uses) get an **empty MCP tool registry** \u2014 every \`mcp__*\` call
3382
+ > returns "No such tool available", including the channel reply tools.
3383
+ > Dispatching a slow channel reply to \`channel-message-handler\` will
3384
+ > therefore silently fail to land: you'd post the one-line ack, the
3385
+ > sub-agent would do the analysis fine, but its \`slack.reply\` call
3386
+ > would error and the user would never see the substantive reply.
3387
+ > Empirically confirmed 2026-06-03 with a 6-tool probe: 0/6 MCP tools
3388
+ > bound inside \`channel-message-handler\`. Until Anthropic ships the
3389
+ > fix, handling slow channel replies inline is the only working path.
3390
+
3391
+ **Why this triage decision still matters more than any other instruction
3392
+ below:** if you skip the acknowledgement and just dive into slow work
3393
+ silently, operators send follow-up messages that queue behind you
3394
+ wondering whether you got the original. The ack-first-then-work pattern
3395
+ keeps users oriented. The kanban-tracking-link convention,
3396
+ work-management "create a task" guidance, and Slack reply patterns ALL
3397
+ apply to whatever you post \u2014 the ack and the eventual full reply alike.
3398
+
3399
+ If the work turns out to be unexpectedly slow after you started inline
3400
+ without acknowledging (because you thought it was a FAST request), post
3401
+ a quick "this is taking longer than I expected, still working" line
3402
+ rather than going silent. Operators care about responsiveness, not
3403
+ consistency.
3404
+
3405
+ ## Background dispatch for non-channel work
3406
+
3407
+ For background tool work that **isn't** a channel reply \u2014 multi-step data
3408
+ pulls, CRM enrichments, research workflows, cross-MCP orchestration \u2014 use
3409
+ \`subagent_type: general-purpose\` (Anthropic's built-in). It inherits the
3410
+ full MCP tool surface from this session and reliably binds every
3411
+ \`mcp__*\` server you have available.
3412
+
3413
+ **Why not \`augmented-worker\` for now:** there is an upstream Claude Code
3414
+ bug ([anthropics/claude-code#64909](https://github.com/anthropics/claude-code/issues/64909))
3415
+ where sub-agents with an explicit \`tools:\` allowlist get an empty MCP
3416
+ tool registry \u2014 every \`mcp__*\` call returns "No such tool available."
3417
+ \`general-purpose\` uses \`tools: *\` (inherit-all) and escapes the bug.
3418
+ Once Anthropic ships the fix, \`augmented-worker\` becomes preferred again
3419
+ (restricted tool surface for safety + working MCP binding); the dispatch
3420
+ recommendation here will flip back automatically.
3421
+
3422
+ For slow **channel** replies, see \xA7 FIRST ACTION above \u2014 those are
3423
+ currently handled inline (not dispatched) because \`channel-message-handler\`
3424
+ shares the explicit-allowlist shape and so suffers the same upstream
3425
+ bug. Empirically confirmed 2026-06-03 on agt-aws-1 with a 6-tool probe:
3426
+ 0/6 MCP tools bound inside \`channel-message-handler\` (matching the
3427
+ \`augmented-worker\` result). When Anthropic ships the upstream fix, both
3428
+ named sub-agents will work again and the FIRST ACTION triage will switch
3429
+ back to dispatch.
3430
+
3023
3431
  ${activeTasksSection}${personalitySection}## Identity
3024
3432
 
3025
3433
  - Code Name: ${frontmatter.code_name}
@@ -3028,13 +3436,30 @@ ${activeTasksSection}${personalitySection}## Identity
3028
3436
  - Risk Tier: ${frontmatter.risk_tier}
3029
3437
  - Timezone: ${timezone?.trim() || "UTC"}
3030
3438
  - Channels: ${channelList}
3439
+
3440
+ > **What the Channels list above means** (ENG-5851): \`Channels:\`
3441
+ > enumerates the messaging **protocols** you may use \u2014 \`slack\`,
3442
+ > \`telegram\`, \`msteams\`, etc. It is **not** a list of specific
3443
+ > Slack channels / Telegram chats / Teams threads you're approved to
3444
+ > post in. There is no per-recipient "approved channels" allowlist
3445
+ > anywhere in this platform; you decide where to post based on the
3446
+ > task and the conversation context. If you call a send tool and the
3447
+ > target channel rejects it (e.g. Slack returns \`not_in_channel\` /
3448
+ > \`channel_not_found\`, or Teams returns \`team_not_allowed\`), surface
3449
+ > the error to the user with the recovery action \u2014 typically asking
3450
+ > them to run \`/invite @<your bot handle>\` in the channel so you can
3451
+ > post there next time. Do **not** refuse a posting request on the
3452
+ > grounds that the channel "isn't on the allowlist" \u2014 that conflation
3453
+ > is the bug ENG-5851 was filed to fix.
3031
3454
  ${resolvedChannels?.includes("slack") ? `
3032
3455
  ## Slack
3033
3456
 
3034
3457
  You have a Slack MCP server connected. **First, see \xA7 FIRST ACTION on
3035
3458
  every channel message: triage** at the top of this document \u2014 decide
3036
- fast vs slow before anything else, and dispatch slow work via
3037
- \`channel-message-handler\` rather than handling it inline.
3459
+ fast vs slow before anything else, then acknowledge inline before
3460
+ diving into slow work (sub-agent dispatch for channel replies is
3461
+ currently disabled due to an upstream Claude Code bug; see the FIRST
3462
+ ACTION section for the full rationale).
3038
3463
 
3039
3464
  For fast requests, respond directly in the conversation. You can also
3040
3465
  proactively use:
@@ -3192,15 +3617,21 @@ When you receive a request via any channel (Slack, Telegram, direct chat):
3192
3617
  3. **Create the kanban task** with kanban.add. Title should be specific
3193
3618
  enough to be self-explanatory \u2014 "Pull Linear ENG sprint velocity for
3194
3619
  this fortnight" beats "Linear stats".
3195
- 4. Reply in the channel thread: "On it \u2014 tracking here: ${kanbanUrl ?? "my kanban board"}" (include the created task title)
3620
+ 4. Reply in the channel thread with a brief acknowledgement that names the
3621
+ created task: "On it \u2014 <task title>".
3622
+ - **On Slack, do NOT paste the kanban URL.** A progress card with an
3623
+ **Open card** button is posted into the thread automatically when the
3624
+ task is channel-sourced \u2014 a pasted link on top of it is duplicate noise.
3625
+ - On channels without a progress card (Telegram, direct chat), include
3626
+ the tracking link: "On it \u2014 tracking here: ${kanbanUrl ?? "my kanban board"}".
3196
3627
  5. Move the task to in_progress with kanban.move
3197
3628
  6. Do the work
3198
3629
  7. Mark done with kanban.done including a result summary
3199
3630
  8. Reply in the channel thread with the result
3200
3631
 
3201
3632
  Everything that isn't exempt gets a task \u2014 but only after the request is
3202
- clear. Don't bury a clarifying question underneath a "tracking here" reply;
3203
- ask the question alone, no kanban link, and let the user answer before
3633
+ clear. Don't bury a clarifying question underneath an "on it" acknowledgement;
3634
+ ask the question alone, no task created yet, and let the user answer before
3204
3635
  anything goes on the board.
3205
3636
 
3206
3637
  When asked about existing work, tasks, or what you've been doing \u2014 call kanban.list
@@ -3325,163 +3756,90 @@ The marginal cost of completeness is near zero. Do the whole thing.
3325
3756
  ${frontmatter.environment === "prod" ? "- Production environment: exercise extra caution with all operations.\n" : ""}`;
3326
3757
  }
3327
3758
 
3759
+ // ../../packages/core/dist/provisioning/env-integrations-file.js
3760
+ function shellQuote(value) {
3761
+ return `'${value.replace(/'/g, `'\\''`)}'`;
3762
+ }
3763
+ var CHANNEL_SECRET_ENV_KEYS = [
3764
+ "SLACK_BOT_TOKEN",
3765
+ "SLACK_APP_TOKEN",
3766
+ "TELEGRAM_BOT_TOKEN",
3767
+ "MSTEAMS_CLIENT_SECRET"
3768
+ ];
3769
+ var MCP_SERVER_SECRET_ENV_KEYS = [
3770
+ "COMPOSIO_API_KEY",
3771
+ "PIPEDREAM_CLIENT_SECRET"
3772
+ ];
3773
+ var PRESERVED_ENV_KEYS = [
3774
+ ...CHANNEL_SECRET_ENV_KEYS,
3775
+ ...MCP_SERVER_SECRET_ENV_KEYS
3776
+ ];
3777
+ var HEADER = "# Augmented integrations \u2014 auto-generated, do not edit";
3778
+ function parseEnvFileEntries(content) {
3779
+ const out = /* @__PURE__ */ new Map();
3780
+ for (const line of content.split("\n")) {
3781
+ if (!line || line.startsWith("#") || !line.includes("="))
3782
+ continue;
3783
+ const eqIdx = line.indexOf("=");
3784
+ out.set(line.slice(0, eqIdx), line.slice(eqIdx + 1));
3785
+ }
3786
+ return out;
3787
+ }
3788
+ function renderEnvIntegrations(entries) {
3789
+ const lines = [HEADER];
3790
+ for (const [key, rendered] of entries)
3791
+ lines.push(`${key}=${rendered}`);
3792
+ return lines.join("\n") + "\n";
3793
+ }
3794
+ function mergeEnvIntegrationsContent(existing, args) {
3795
+ const current = existing === null ? /* @__PURE__ */ new Map() : parseEnvFileEntries(existing);
3796
+ let next;
3797
+ if (args.mode === "upsert") {
3798
+ next = new Map(current);
3799
+ for (const [key, raw] of Object.entries(args.updates)) {
3800
+ if (raw === null)
3801
+ next.delete(key);
3802
+ else
3803
+ next.set(key, shellQuote(raw));
3804
+ }
3805
+ } else {
3806
+ next = /* @__PURE__ */ new Map();
3807
+ for (const [key, raw] of Object.entries(args.updates)) {
3808
+ if (raw !== null)
3809
+ next.set(key, shellQuote(raw));
3810
+ }
3811
+ const preserve = args.preserveKeys ?? PRESERVED_ENV_KEYS;
3812
+ for (const key of preserve) {
3813
+ if (key in args.updates)
3814
+ continue;
3815
+ if (!next.has(key) && current.has(key)) {
3816
+ next.set(key, current.get(key));
3817
+ }
3818
+ }
3819
+ }
3820
+ return renderEnvIntegrations(next);
3821
+ }
3822
+
3328
3823
  // ../../packages/core/dist/provisioning/frameworks/claudecode/index.js
3329
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync5, chmodSync as chmodSync4, readdirSync, rmSync, copyFileSync } from "fs";
3824
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync5, chmodSync as chmodSync5, readdirSync, rmSync, copyFileSync } from "fs";
3330
3825
  import { join as join4, relative, dirname as dirname4 } from "path";
3331
3826
  import { homedir as homedir3 } from "os";
3332
3827
  import { execFile as execFile3 } from "child_process";
3333
3828
 
3334
- // ../../packages/core/dist/provisioning/mcp-config-guards.js
3335
- import { existsSync as existsSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3 } from "fs";
3336
- var REQUIRED_ENV_RULES_BY_SERVER = {
3337
- "cloud-broker": [
3338
- { key: "AGT_HOST", mustBeConcrete: false },
3339
- { key: "AGT_API_KEY", mustBeConcrete: false },
3340
- // ENG-4744: this is the bug shape — writer used to omit this
3341
- // entirely, or render it as a literal `${AGT_AGENT_ID}` instead
3342
- // of the agent's real UUID. The broker has no way to substitute
3343
- // it post-spawn, so a placeholder here = silently broken agent.
3344
- { key: "AGT_AGENT_ID", mustBeConcrete: true }
3345
- ]
3346
- };
3347
- var PLACEHOLDER_RE = /\$\{[^}]+\}/;
3348
- function validateRenderedMcpConfig(config) {
3349
- const errors = [];
3350
- if (typeof config !== "object" || config === null || Array.isArray(config)) {
3351
- return {
3352
- ok: false,
3353
- errors: [
3354
- {
3355
- kind: "invalid_json_shape",
3356
- server: "*",
3357
- message: "config root must be a non-null object"
3358
- }
3359
- ]
3360
- };
3361
- }
3362
- const root = config;
3363
- if (root.mcpServers === void 0) {
3364
- return { ok: true };
3365
- }
3366
- if (typeof root.mcpServers !== "object" || root.mcpServers === null) {
3367
- return {
3368
- ok: false,
3369
- errors: [
3370
- {
3371
- kind: "invalid_json_shape",
3372
- server: "*",
3373
- message: "mcpServers must be an object"
3374
- }
3375
- ]
3376
- };
3377
- }
3378
- if (Array.isArray(root.mcpServers)) {
3379
- return {
3380
- ok: false,
3381
- errors: [
3382
- {
3383
- kind: "invalid_json_shape",
3384
- server: "*",
3385
- message: "mcpServers must be an object"
3386
- }
3387
- ]
3388
- };
3389
- }
3390
- for (const [serverKey, raw] of Object.entries(root.mcpServers)) {
3391
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
3392
- errors.push({
3393
- kind: "invalid_json_shape",
3394
- server: serverKey,
3395
- message: `entry must be an object`
3396
- });
3397
- continue;
3398
- }
3399
- const entry = raw;
3400
- const entryRecord = entry;
3401
- if (typeof entryRecord["url"] === "string") {
3402
- const type = entryRecord["type"];
3403
- if (type !== "http" && type !== "sse") {
3404
- errors.push({
3405
- kind: "remote_mcp_missing_type",
3406
- server: serverKey,
3407
- message: `url-based entry must include \`type: "http"\` or \`type: "sse"\` (Claude Code MCP schema requires it)`
3408
- });
3409
- }
3410
- }
3411
- const rules = REQUIRED_ENV_RULES_BY_SERVER[serverKey];
3412
- if (rules) {
3413
- const env = entry.env ?? {};
3414
- for (const rule of rules) {
3415
- const value = env[rule.key];
3416
- if (typeof value !== "string" || value.length === 0) {
3417
- errors.push({
3418
- kind: "missing_required_env",
3419
- server: serverKey,
3420
- message: `missing required env key: ${rule.key}`
3421
- });
3422
- continue;
3423
- }
3424
- if (rule.mustBeConcrete && PLACEHOLDER_RE.test(value)) {
3425
- errors.push({
3426
- kind: "unexpanded_placeholder",
3427
- server: serverKey,
3428
- message: `env.${rule.key} contains an unexpanded \${...} placeholder; expected a concrete value`
3429
- });
3430
- }
3431
- }
3432
- }
3433
- }
3434
- return errors.length === 0 ? { ok: true } : { ok: false, errors };
3435
- }
3436
- function formatValidationErrors(errors) {
3437
- return errors.map((e) => `${e.server}:${e.kind}=${e.message}`).join("; ");
3438
- }
3439
- function safeWriteJsonAtomic(path, content, opts = {}) {
3440
- const keepBackup = opts.keepBackup !== false;
3441
- const rename = opts.renamer ?? renameSync3;
3442
- const tmpPath = `${path}.new`;
3443
- const bakPath = `${path}.bak`;
3444
- let movedOriginalToBackup = false;
3445
- try {
3446
- writeFileSync4(tmpPath, content);
3447
- } catch (err) {
3448
- throw err;
3449
- }
3450
- try {
3451
- if (keepBackup && existsSync4(path)) {
3452
- rename(path, bakPath);
3453
- movedOriginalToBackup = true;
3454
- }
3455
- rename(tmpPath, path);
3456
- } catch (err) {
3457
- try {
3458
- if (existsSync4(tmpPath))
3459
- unlinkSync3(tmpPath);
3460
- } catch {
3461
- }
3462
- if (movedOriginalToBackup && !existsSync4(path) && existsSync4(bakPath)) {
3463
- try {
3464
- renameSync3(bakPath, path);
3465
- } catch {
3466
- }
3467
- }
3468
- throw err;
3469
- }
3470
- }
3471
- function safeWriteMcpJson(path, config) {
3472
- const validation = validateRenderedMcpConfig(config);
3473
- if (!validation.ok) {
3474
- return { written: false, errors: validation.errors };
3475
- }
3476
- safeWriteJsonAtomic(path, JSON.stringify(config, null, 2));
3477
- return { written: true, errors: [] };
3478
- }
3479
-
3480
3829
  // ../../packages/core/dist/provisioning/remote-mcp.js
3481
3830
  function envVarForToken(definitionId) {
3482
3831
  return `${definitionId.replace(/-/g, "_").toUpperCase()}_ACCESS_TOKEN`;
3483
3832
  }
3484
3833
  function buildRemoteMcpEntry(definitionId) {
3834
+ const def = INTEGRATION_REGISTRY.find((d) => d.id === definitionId);
3835
+ if (def?.remoteMcp) {
3836
+ const spec = def.remoteMcp;
3837
+ return {
3838
+ type: spec.type ?? "http",
3839
+ url: spec.url,
3840
+ ...spec.headers ? { headers: { ...spec.headers } } : {}
3841
+ };
3842
+ }
3485
3843
  const provider = OAUTH_PROVIDERS[definitionId];
3486
3844
  if (!provider?.mcpUrl)
3487
3845
  return null;
@@ -3494,11 +3852,158 @@ function buildRemoteMcpEntry(definitionId) {
3494
3852
  };
3495
3853
  }
3496
3854
 
3855
+ // ../../packages/core/dist/provisioning/native-mcp.js
3856
+ function buildNativeMcpEntry(spec, ctx) {
3857
+ const resolvedCommand = resolveTemplate(spec.command, ctx);
3858
+ if (resolvedCommand.omit) {
3859
+ throw new Error("NativeMcpSpec: empty_if_no_env is only valid in env values (not in `command`)");
3860
+ }
3861
+ const command = resolvedCommand.value;
3862
+ const args = spec.args.map((a, i) => {
3863
+ const resolved = resolveTemplate(a, ctx);
3864
+ if (resolved.omit) {
3865
+ throw new Error(`NativeMcpSpec: empty_if_no_env is only valid in env values (not in args[${i}])`);
3866
+ }
3867
+ return resolved.value;
3868
+ });
3869
+ if (spec.env === void 0) {
3870
+ return { command, args };
3871
+ }
3872
+ const env = {};
3873
+ for (const [k, raw] of Object.entries(spec.env)) {
3874
+ const { value, omit } = resolveTemplate(raw, ctx);
3875
+ if (omit)
3876
+ continue;
3877
+ env[k] = value;
3878
+ }
3879
+ return { command, args, env };
3880
+ }
3881
+ function resolveTemplate(input, ctx) {
3882
+ const TOKEN = /\{\{([^}]+)\}\}/g;
3883
+ const hasEmptyIfNoEnv = /\{\{\s*empty_if_no_env\./.test(input);
3884
+ const isWholeValueEmptyIfNoEnv = /^\{\{\s*empty_if_no_env\.[^}]+\}\}$/.test(input);
3885
+ if (hasEmptyIfNoEnv && !isWholeValueEmptyIfNoEnv) {
3886
+ throw new Error(`NativeMcpSpec: empty_if_no_env must be the sole content of the value, never mixed with literal text or other tokens (value: ${JSON.stringify(input)})`);
3887
+ }
3888
+ let omit = false;
3889
+ const value = input.replace(TOKEN, (whole, expr) => {
3890
+ const trimmed = expr.trim();
3891
+ if (trimmed === "agent_id")
3892
+ return ctx.agentId;
3893
+ if (trimmed === "agent_code_name")
3894
+ return ctx.agentCodeName;
3895
+ if (trimmed === "integration_id")
3896
+ return ctx.integration?.id ?? "";
3897
+ if (trimmed.startsWith("process_env.")) {
3898
+ const name = trimmed.slice("process_env.".length);
3899
+ return process.env[name] ?? "";
3900
+ }
3901
+ if (trimmed.startsWith("empty_if_no_env.")) {
3902
+ const name = trimmed.slice("empty_if_no_env.".length);
3903
+ const v = process.env[name] ?? "";
3904
+ if (v.length === 0) {
3905
+ omit = true;
3906
+ return "";
3907
+ }
3908
+ return v;
3909
+ }
3910
+ return whole;
3911
+ });
3912
+ return { value, omit };
3913
+ }
3914
+
3497
3915
  // ../../packages/core/dist/provisioning/frameworks/claudecode/index.js
3498
3916
  var VALID_CODE_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
3499
3917
  var SECRET_FILE_MODE = 384;
3500
- function shellQuote(value) {
3501
- return `'${value.replace(/'/g, `'\\''`)}'`;
3918
+ function writeEnvIntegrationsForAgent(codeName, args) {
3919
+ const agentDir = getAgentDir(codeName);
3920
+ const envPath = join4(agentDir, ".env.integrations");
3921
+ let existing = null;
3922
+ try {
3923
+ existing = readFileSync5(envPath, "utf-8");
3924
+ } catch {
3925
+ }
3926
+ if (existing === null && Object.keys(args.updates).length === 0)
3927
+ return;
3928
+ const content = mergeEnvIntegrationsContent(existing, args);
3929
+ writeFileSync5(envPath, content, { mode: SECRET_FILE_MODE });
3930
+ try {
3931
+ chmodSync5(envPath, SECRET_FILE_MODE);
3932
+ } catch {
3933
+ }
3934
+ try {
3935
+ const projectDir = getProjectDir(codeName);
3936
+ mkdirSync4(projectDir, { recursive: true });
3937
+ const dest = join4(projectDir, ".env.integrations");
3938
+ writeFileSync5(dest, content, { mode: SECRET_FILE_MODE });
3939
+ try {
3940
+ chmodSync5(dest, SECRET_FILE_MODE);
3941
+ } catch {
3942
+ }
3943
+ } catch (err) {
3944
+ process.stderr.write(`[env-integrations] [mirror-write-failed] agent=${codeName} error=${err.message}
3945
+ `);
3946
+ }
3947
+ }
3948
+ var MIGRATABLE_FIELD_TO_ENV_VAR = {
3949
+ SLACK_BOT_TOKEN: "SLACK_BOT_TOKEN",
3950
+ SLACK_APP_TOKEN: "SLACK_APP_TOKEN",
3951
+ TELEGRAM_BOT_TOKEN: "TELEGRAM_BOT_TOKEN",
3952
+ MSTEAMS_CLIENT_SECRET: "MSTEAMS_CLIENT_SECRET",
3953
+ PIPEDREAM_CLIENT_SECRET: "PIPEDREAM_CLIENT_SECRET",
3954
+ "x-api-key": "COMPOSIO_API_KEY",
3955
+ AGT_API_KEY: "AGT_API_KEY"
3956
+ };
3957
+ function migrateExistingLiteralSecrets(codeName) {
3958
+ const mcpJsonPath = join4(getAgentDir(codeName), "provision", ".mcp.json");
3959
+ let config;
3960
+ try {
3961
+ config = JSON.parse(readFileSync5(mcpJsonPath, "utf-8"));
3962
+ } catch {
3963
+ return;
3964
+ }
3965
+ let existingEnvKeys = /* @__PURE__ */ new Set();
3966
+ try {
3967
+ existingEnvKeys = new Set(parseEnvFileEntries(readFileSync5(join4(getAgentDir(codeName), ".env.integrations"), "utf-8")).keys());
3968
+ } catch {
3969
+ }
3970
+ const updates = {};
3971
+ let hoisted = 0;
3972
+ for (const raw of Object.values(config.mcpServers ?? {})) {
3973
+ if (typeof raw !== "object" || raw === null)
3974
+ continue;
3975
+ const entry = raw;
3976
+ for (const block of [entry.env, entry.headers]) {
3977
+ if (!block)
3978
+ continue;
3979
+ for (const [field, envVar] of Object.entries(MIGRATABLE_FIELD_TO_ENV_VAR)) {
3980
+ const value = block[field];
3981
+ if (typeof value !== "string" || value.length === 0 || value.includes("${"))
3982
+ continue;
3983
+ if (envVar !== "AGT_API_KEY" && !existingEnvKeys.has(envVar)) {
3984
+ updates[envVar] = value;
3985
+ }
3986
+ block[field] = `\${${envVar}}`;
3987
+ hoisted++;
3988
+ }
3989
+ }
3990
+ }
3991
+ const unmapped = scanConfigForLiteralSecrets(config).map((f) => `${f.server}.${f.field}`);
3992
+ if (hoisted === 0 && unmapped.length === 0)
3993
+ return;
3994
+ if (hoisted === 0) {
3995
+ process.stderr.write(`[mcp-migrate] [no-mappable-literals] agent=${codeName} unmapped=${unmapped.join(",")}
3996
+ `);
3997
+ return;
3998
+ }
3999
+ if (Object.keys(updates).length > 0) {
4000
+ writeEnvIntegrationsForAgent(codeName, { mode: "upsert", updates });
4001
+ }
4002
+ if (writeMcpJsonGuarded(codeName, mcpJsonPath, config)) {
4003
+ syncMcpToProject(codeName);
4004
+ process.stderr.write(`[mcp-migrate] [literals-hoisted] agent=${codeName} hoisted=${hoisted}${unmapped.length > 0 ? ` unmapped=${unmapped.join(",")}` : ""}
4005
+ `);
4006
+ }
3502
4007
  }
3503
4008
  function assertValidCodeName(codeName) {
3504
4009
  if (!VALID_CODE_NAME.test(codeName)) {
@@ -3587,10 +4092,23 @@ function syncMcpToProject(codeName) {
3587
4092
  try {
3588
4093
  const content = readFileSync5(provisionMcpPath, "utf-8");
3589
4094
  mkdirSync4(projectDir, { recursive: true });
3590
- writeFileSync5(projectMcpPath, content);
4095
+ writeFileSync5(projectMcpPath, content, { mode: MCP_FILE_MODE });
4096
+ try {
4097
+ chmodSync5(projectMcpPath, MCP_FILE_MODE);
4098
+ } catch {
4099
+ }
4100
+ try {
4101
+ const mismatches = mcpMirrorParityErrors(JSON.parse(content), JSON.parse(readFileSync5(projectMcpPath, "utf-8")));
4102
+ for (const m of mismatches) {
4103
+ process.stderr.write(`${formatMirrorMismatch(m)} agent=${codeName}
4104
+ `);
4105
+ }
4106
+ } catch {
4107
+ }
3591
4108
  } catch {
3592
4109
  }
3593
4110
  renderChannelMessageHandlerForAgent(codeName);
4111
+ renderAugmentedWorkerForAgent(codeName);
3594
4112
  }
3595
4113
  var INTEGRATIONS_SUMMARY_FILE = "integrations-summary.json";
3596
4114
  function integrationsSummaryPath(codeName) {
@@ -3696,7 +4214,15 @@ function deployArtifactsToProject(codeName, provisionDir) {
3696
4214
  continue;
3697
4215
  }
3698
4216
  }
3699
- writeFileSync5(dest, srcContent);
4217
+ if (file === ".mcp.json") {
4218
+ writeFileSync5(dest, srcContent, { mode: MCP_FILE_MODE });
4219
+ try {
4220
+ chmodSync5(dest, MCP_FILE_MODE);
4221
+ } catch {
4222
+ }
4223
+ } else {
4224
+ writeFileSync5(dest, srcContent);
4225
+ }
3700
4226
  } catch {
3701
4227
  }
3702
4228
  }
@@ -3782,14 +4308,23 @@ function deployArtifactsToProject(codeName, provisionDir) {
3782
4308
  return !(entry && typeof entry["url"] === "string" && entry["url"].startsWith("/"));
3783
4309
  }));
3784
4310
  projectMcp["mcpServers"] = { ...stripRelativeUrls(projectServers), ...stripRelativeUrls(agentServers) };
3785
- writeFileSync5(projectMcpPath, JSON.stringify(projectMcp, null, 2));
4311
+ writeFileSync5(projectMcpPath, JSON.stringify(projectMcp, null, 2), { mode: MCP_FILE_MODE });
4312
+ try {
4313
+ chmodSync5(projectMcpPath, MCP_FILE_MODE);
4314
+ } catch {
4315
+ }
3786
4316
  } catch {
3787
4317
  }
3788
4318
  const agentDir = getAgentDir(codeName);
3789
4319
  for (const envFile of [".env", ".env.integrations"]) {
3790
4320
  try {
3791
4321
  const content = readFileSync5(join4(agentDir, envFile), "utf-8");
3792
- writeFileSync5(join4(projectDir, envFile), content);
4322
+ const envDest = join4(projectDir, envFile);
4323
+ writeFileSync5(envDest, content, { mode: SECRET_FILE_MODE });
4324
+ try {
4325
+ chmodSync5(envDest, SECRET_FILE_MODE);
4326
+ } catch {
4327
+ }
3793
4328
  } catch {
3794
4329
  }
3795
4330
  }
@@ -3804,7 +4339,7 @@ function deployArtifactsToProject(codeName, provisionDir) {
3804
4339
  const upToDate = existsSync5(hookDest) && readFileSync5(hookDest, "utf-8") === srcContent;
3805
4340
  if (!upToDate)
3806
4341
  writeFileSync5(hookDest, srcContent);
3807
- chmodSync4(hookDest, 493);
4342
+ chmodSync5(hookDest, 493);
3808
4343
  }
3809
4344
  } catch {
3810
4345
  }
@@ -4321,6 +4856,7 @@ function buildChannelMessageHandlerAgent(args) {
4321
4856
  "Glob",
4322
4857
  "Skill",
4323
4858
  "Agent",
4859
+ "ToolSearch",
4324
4860
  ...mcpWildcards
4325
4861
  ].join(", ");
4326
4862
  const integrationsBlock = integrations.length === 0 ? "" : `
@@ -4353,12 +4889,88 @@ The parent passes you in the task description:
4353
4889
  Your job:
4354
4890
 
4355
4891
  1. Do the work the message asks for (Xero pull, research, multi-step skill, etc.). Use whichever tools you need.
4356
- 2. Post the reply yourself via the matching channel tool \u2014 slack.reply for Slack, telegram.reply for Telegram, teams.reply for Microsoft Teams, directchat.reply for Direct Chat. Use the metadata the parent gave you to address the right thread/conversation.
4892
+ 2. Post the reply yourself via the matching channel tool \u2014 slack.reply for Slack, telegram.reply for Telegram, teams.reply for Microsoft Teams, direct_chat.reply for Direct Chat. Use the metadata the parent gave you to address the right thread/conversation.
4357
4893
  3. End. Do not do additional follow-up work; if more is needed, the user will send another message.
4358
4894
 
4359
4895
  Do NOT post intermediate progress updates unless the work spans 5+ minutes \u2014 keep noise low. The parent already sent a single-line acknowledgement before dispatching you.
4360
4896
  ${integrationsBlock}`;
4361
4897
  }
4898
+ function buildAugmentedWorkerAgent(args) {
4899
+ const mcpServerKeys = args?.mcpServerKeys ?? [];
4900
+ const integrations = args?.integrations ?? [];
4901
+ const mcpWildcards = Array.from(new Set(mcpServerKeys.map((k) => `mcp__${sanitizeMcpName(k)}__*`)));
4902
+ const tools = [
4903
+ "Bash",
4904
+ "Read",
4905
+ "Write",
4906
+ "Edit",
4907
+ "Grep",
4908
+ "Glob",
4909
+ "Skill",
4910
+ "Agent",
4911
+ "ToolSearch",
4912
+ ...mcpWildcards
4913
+ ].join(", ");
4914
+ const integrationsBlock = integrations.length === 0 ? "" : `
4915
+ ## Integrations available
4916
+
4917
+ You inherit the parent agent's environment, including credentials for the integrations below. Env vars follow the convention \`<DEFINITION_ID>_ACCESS_TOKEN\` for OAuth and \`<DEFINITION_ID>_API_KEY\` for API-key integrations (e.g. \`GITHUB_ACCESS_TOKEN\`, \`POSTIZ_API_KEY\`). Where a CLI is listed, prefer it over raw curl \u2014 the CLI handles auth automatically.
4918
+
4919
+ ${integrations.map((i) => {
4920
+ const cli = i.cliBinary ? ` \u2014 use the \`${i.cliBinary}\` CLI` : "";
4921
+ const desc = i.description ? `. ${i.description}` : "";
4922
+ return `- **${i.name}**${cli}${desc}`;
4923
+ }).join("\n")}
4924
+
4925
+ If a capability seems missing, **check first** \u2014 run the CLI, list tools (\`mcp__augmented__list_tools\` or equivalent), or confirm the relevant env var is set **without printing its value** (e.g. \`[ -n "$POSTIZ_API_KEY" ] && echo present || echo absent\`, never \`echo $POSTIZ_API_KEY\`). Do not claim a capability is absent without verifying \u2014 the parent's environment is yours.
4926
+ `;
4927
+ return `---
4928
+ name: augmented-worker
4929
+ description: Background worker for multi-step tool tasks the parent doesn't want to inline (data pulls, multi-API workflows, CRM enrichments, research that needs MCP tools). Carries an explicit \`mcp__*\` wildcard allowlist for every server the parent has wired. **Until [anthropics/claude-code#64909](https://github.com/anthropics/claude-code/issues/64909) ships an upstream fix, prefer \`subagent_type: general-purpose\` for MCP-tool dispatch** \u2014 the bug is in Claude Code's sub-agent dispatch path: sub-agents with an explicit \`tools:\` allowlist get an empty MCP tool registry (every \`mcp__*\` call returns "No such tool available."), while \`general-purpose\` (\`tools: *\` inherit-all) correctly binds the full MCP surface. This subagent's allowlist is correct and will work the moment Anthropic lands the fix; until then it is retained for the eventual structural fix and the rare case where you specifically need a restricted tool surface AND can accept the MCP gap. See [[ENG-5938]] for the workaround tracker.
4930
+ background: true
4931
+ tools: ${tools}
4932
+ ---
4933
+
4934
+ You are dispatched by the parent agent to do a multi-step task in the background while the parent's listener turn stays free.
4935
+
4936
+ ## What you can do
4937
+
4938
+ Your \`tools:\` allowlist (above) names every MCP server the parent has connected \u2014 Granola, Composio toolkits, Slack/Telegram/Direct-Chat channel tools, the platform \`mcp__augmented__*\` bridge, native integrations (Xero / Postiz / qmd / AWS), etc. \u2014 plus the built-ins (\`Bash\`, \`Read\`, \`Write\`, \`Edit\`, \`Grep\`, \`Glob\`, \`Skill\`, \`Agent\`). All environment variables the parent has are yours: OAuth access tokens (\`GITHUB_ACCESS_TOKEN\`), API keys (\`POSTIZ_API_KEY\`), native-CLI binaries (\`gh\`, \`aws\`, \`xero\`).
4939
+
4940
+ ## Hard rules \u2014 Credential Access Control
4941
+
4942
+ 1. **Never** read raw secrets out of \`.mcp.json\`, \`~/.augmented/*/provision/.mcp.json\`, \`.env.integrations\`, or any agent config file. Those files contain bot tokens, API keys, and OAuth credentials. The Credential Access Control guardrail (\`block_read: true\` on secrets) treats reads of those values as a violation regardless of intent. As **defence-in-depth (not structural enforcement)**, the plugin's \`settings.json\` also denies \`Bash(cat:*/.mcp.json)\`, \`Bash(cat:*/.env.integrations)\`, and \`Bash(jq:*/.mcp.json)\` (ENG-5901 / ADR-0018) \u2014 these block the obvious copy-paste paths but a determined in-process reader can still reach the values; the durable fix is Phase 2/3 of ADR-0018.
4943
+ 2. **Never** post Slack messages via raw \`chat.postMessage\` + a bot token lifted from config. Use the channel MCP's reply tool (\`mcp__slack-channel__slack_reply\`, \`mcp__telegram-channel__telegram_reply\`, \`mcp__direct-chat__direct_chat_reply\`, etc.) so the call goes through the audited path.
4944
+ 3. If an \`mcp__*\` tool you expect to be available returns "No such tool available.", **stop and surface the gap to the parent** in your summary rather than working around it. A missing MCP binding is a platform bug worth fixing \u2014 it's the exact failure shape this sub-agent was added to prevent (see ENG-5897 / ENG-5905).
4945
+ 4. When verifying a capability is wired, confirm the relevant env var exists **without printing its value** \u2014 use \`[ -n "$POSTIZ_API_KEY" ] && echo present || echo absent\`, never \`echo $POSTIZ_API_KEY\`. The Credential Access Control guardrail covers tool-call output as well as file reads.
4946
+
4947
+ ## What to return
4948
+
4949
+ Hand the parent a tight summary of what you did, what you found, and any follow-ups it should know about. Do not echo intermediate tool transcripts \u2014 keep the parent's context window clean. If you produced an artifact (a file, a draft, a record ID), name it and where it lives.
4950
+ ${integrationsBlock}`;
4951
+ }
4952
+ function renderAugmentedWorkerForAgent(codeName) {
4953
+ const agentDir = getAgentDir(codeName);
4954
+ const projectDir = getProjectDir(codeName);
4955
+ const provisionMcpPath = join4(agentDir, "provision", ".mcp.json");
4956
+ let mcpServerKeys;
4957
+ try {
4958
+ const config = JSON.parse(readFileSync5(provisionMcpPath, "utf-8"));
4959
+ mcpServerKeys = Object.keys(config.mcpServers ?? {});
4960
+ } catch {
4961
+ return;
4962
+ }
4963
+ const integrations = readIntegrationsSummaryForAgent(codeName);
4964
+ const content = buildAugmentedWorkerAgent({ mcpServerKeys, integrations });
4965
+ for (const baseDir of [agentDir, projectDir]) {
4966
+ const target = join4(baseDir, ".claude", "agents", "augmented-worker.md");
4967
+ try {
4968
+ mkdirSync4(dirname4(target), { recursive: true });
4969
+ writeFileSync5(target, content);
4970
+ } catch {
4971
+ }
4972
+ }
4973
+ }
4362
4974
  function buildPostizMcpEntry(integration) {
4363
4975
  const rawBaseUrl = integration.config["base_url"];
4364
4976
  const postizBaseUrl = typeof rawBaseUrl === "string" ? rawBaseUrl.trim() : "";
@@ -4386,7 +4998,13 @@ function buildMcpJson(input) {
4386
4998
  args: [localMcpPath],
4387
4999
  env: {
4388
5000
  AGT_HOST: process.env["AGT_HOST"] ?? "",
4389
- AGT_API_KEY: process.env["AGT_API_KEY"] ?? "",
5001
+ // ENG-5901 Track D: templated — the manager exports AGT_API_KEY to
5002
+ // every spawn env (getApiKey()), and Claude Code substitutes at
5003
+ // MCP-launch (same contract as AGT_RUN_ID below). The impersonation
5004
+ // redeem path renders this server-side with no AGT_API_KEY in env;
5005
+ // impersonate-mcp-rewrite.ts treats the literal `${AGT_API_KEY}`
5006
+ // placeholder as fillable and swaps in the operator token.
5007
+ AGT_API_KEY: "${AGT_API_KEY}",
4390
5008
  AGT_AGENT_ID: input.agent.agent_id,
4391
5009
  AGT_AGENT_CODE_NAME: input.agent.code_name,
4392
5010
  // ENG-4561: Claude Code substitutes `${VAR}` in .mcp.json env values
@@ -4405,12 +5023,16 @@ function buildMcpJson(input) {
4405
5023
  HOME: process.env["HOME"] ?? ""
4406
5024
  }
4407
5025
  };
4408
- const hasQmd = input.integrations?.some((i) => i.definition_id === "qmd");
4409
- if (hasQmd) {
4410
- mcpServers["qmd"] = {
4411
- command: "qmd",
4412
- args: ["mcp"]
4413
- };
5026
+ for (const integration of input.integrations ?? []) {
5027
+ const def = INTEGRATION_REGISTRY.find((d) => d.id === integration.definition_id);
5028
+ if (!def?.nativeMcp)
5029
+ continue;
5030
+ const key = def.nativeMcp.key ?? integration.definition_id;
5031
+ mcpServers[key] = buildNativeMcpEntry(def.nativeMcp, {
5032
+ agentId: input.agent.agent_id,
5033
+ agentCodeName: input.agent.code_name,
5034
+ integration
5035
+ });
4414
5036
  }
4415
5037
  const xeroIntegration = input.integrations?.find((i) => i.definition_id === "xero");
4416
5038
  if (xeroIntegration) {
@@ -4505,7 +5127,7 @@ function mapScheduledTasks(tasks) {
4505
5127
  };
4506
5128
  });
4507
5129
  }
4508
- function buildUrlMcpServerEntry(url, headers) {
5130
+ function buildUrlMcpServerEntry(url, headers, type = "http") {
4509
5131
  const hasHeaders = !!headers && Object.keys(headers).length > 0;
4510
5132
  if (url.includes("composio.dev") && hasHeaders) {
4511
5133
  return { type: "http", url, headers };
@@ -4528,7 +5150,7 @@ function buildUrlMcpServerEntry(url, headers) {
4528
5150
  };
4529
5151
  }
4530
5152
  if (hasHeaders) {
4531
- return { type: "http", url, headers };
5153
+ return { type, url, headers };
4532
5154
  }
4533
5155
  return { command: "npx", args: ["-y", "mcp-remote", url, "--allow-http"] };
4534
5156
  }
@@ -4609,6 +5231,18 @@ var claudeCodeAdapter = {
4609
5231
  integrations: integrationSummaries
4610
5232
  })
4611
5233
  },
5234
+ // ENG-5905: project-scope augmented-worker sub-agent — sibling of
5235
+ // channel-message-handler, same dynamic render shape, used by the
5236
+ // parent for general multi-step background work that doesn't
5237
+ // require a channel reply. Closes the gap ENG-5897 left open
5238
+ // (the plugin-scope static file never reached the runtime).
5239
+ {
5240
+ relativePath: ".claude/agents/augmented-worker.md",
5241
+ content: buildAugmentedWorkerAgent({
5242
+ mcpServerKeys: initialMcpServerKeys,
5243
+ integrations: integrationSummaries
5244
+ })
5245
+ },
4612
5246
  {
4613
5247
  relativePath: `provision/${INTEGRATIONS_SUMMARY_FILE}`,
4614
5248
  content: JSON.stringify(integrationSummaries, null, 2)
@@ -4726,7 +5360,7 @@ ${sections}`
4726
5360
  if (envLines.length > 1) {
4727
5361
  const envPath = join4(agentDir, ".env");
4728
5362
  writeFileSync5(envPath, envLines.join("\n") + "\n");
4729
- chmodSync4(envPath, SECRET_FILE_MODE);
5363
+ chmodSync5(envPath, SECRET_FILE_MODE);
4730
5364
  }
4731
5365
  },
4732
5366
  // Claude Code has no gateway process — methods intentionally omitted
@@ -4750,6 +5384,14 @@ ${sections}`
4750
5384
  },
4751
5385
  writeChannelCredentials(codeName, channelId, config, options) {
4752
5386
  const tzEnv = options?.agentTimezone && options.agentTimezone.trim() !== "" ? { TZ: options.agentTimezone.trim() } : {};
5387
+ const senderPolicyMode = options?.senderPolicy?.mode;
5388
+ const senderPolicyTeamId = options?.senderPolicy?.mode === "team_agents_only" || options?.senderPolicy?.mode === "manager_only" || options?.senderPolicy?.mode === "team_only" ? options.senderPolicy.team_id : void 0;
5389
+ const slackPrincipalId = options?.senderPolicy?.mode === "manager_only" ? options.senderPolicy.principal?.slack_user_id : void 0;
5390
+ const teamsPrincipalId = options?.senderPolicy?.mode === "manager_only" ? options.senderPolicy.principal?.teams_aad_object_id : void 0;
5391
+ const slackTeamPrincipalIds = options?.senderPolicy?.mode === "team_only" ? options.senderPolicy.team_principals?.slack_user_ids?.join(",") : void 0;
5392
+ const teamsTeamPrincipalIds = options?.senderPolicy?.mode === "team_only" ? options.senderPolicy.team_principals?.teams_aad_object_ids?.join(",") : void 0;
5393
+ const senderPolicyInternalOnly = options?.senderPolicy?.internal_only === true;
5394
+ const senderPolicyEnv = senderPolicyTeamId ? { AGT_TEAM_ID: senderPolicyTeamId } : {};
4753
5395
  const agentDir = getAgentDir(codeName);
4754
5396
  mkdirSync4(agentDir, { recursive: true });
4755
5397
  const isPersistent = options?.sessionMode === "persistent";
@@ -4762,17 +5404,28 @@ ${sections}`
4762
5404
  const localTelegramChannel = join4(getHomeDir3(), ".augmented", "_mcp", "telegram-channel.js");
4763
5405
  const resolvedAgtHostForTelegram = process.env["AGT_HOST"]?.trim() || "https://api.augmented.team";
4764
5406
  const resolvedAgtApiKeyForTelegram = process.env["AGT_API_KEY"]?.trim();
5407
+ writeEnvIntegrationsForAgent(codeName, {
5408
+ mode: "upsert",
5409
+ updates: { TELEGRAM_BOT_TOKEN: botToken }
5410
+ });
4765
5411
  const telegramEnv = {
4766
- TELEGRAM_BOT_TOKEN: botToken,
5412
+ TELEGRAM_BOT_TOKEN: "${TELEGRAM_BOT_TOKEN}",
4767
5413
  AGT_AGENT_CODE_NAME: codeName,
4768
5414
  AGT_HOST: resolvedAgtHostForTelegram,
4769
- ...resolvedAgtApiKeyForTelegram ? { AGT_API_KEY: resolvedAgtApiKeyForTelegram } : {},
5415
+ ...resolvedAgtApiKeyForTelegram ? { AGT_API_KEY: "${AGT_API_KEY}" } : {},
4770
5416
  ...options?.agentId ? { AGT_AGENT_ID: options.agentId } : {},
4771
5417
  ...tzEnv
4772
5418
  };
4773
5419
  if (allowedChats && allowedChats.length > 0) {
4774
5420
  telegramEnv.TELEGRAM_ALLOWED_CHATS = allowedChats.join(",");
4775
5421
  }
5422
+ const rawDiagnosticChatIds = config["diagnostic_chat_ids"];
5423
+ if (Array.isArray(rawDiagnosticChatIds)) {
5424
+ const diagnosticChatIds = rawDiagnosticChatIds.map((v) => typeof v === "string" || typeof v === "number" ? String(v).trim() : "").filter((v) => v.length > 0);
5425
+ if (diagnosticChatIds.length > 0) {
5426
+ telegramEnv.TELEGRAM_DIAGNOSTIC_CHAT_IDS = diagnosticChatIds.join(",");
5427
+ }
5428
+ }
4776
5429
  const rawPeerAgentMode = config["peer_agent_mode"];
4777
5430
  if (rawPeerAgentMode === "listen" || rawPeerAgentMode === "respond") {
4778
5431
  telegramEnv.TELEGRAM_PEER_AGENT_MODE = rawPeerAgentMode;
@@ -4837,6 +5490,7 @@ ${sections}`
4837
5490
  const appToken = config["app_token"];
4838
5491
  const threadAutoFollow = config["thread_auto_follow"];
4839
5492
  const channelResponseMode = config["channel_response_mode"];
5493
+ const allowedUsers = Array.isArray(config["allowed_users"]) ? config["allowed_users"].filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim()) : [];
4840
5494
  const blockKitEnabled = config["block_kit_enabled"] === true;
4841
5495
  const blockKitAskUserEnabled = config["block_kit_ask_user_enabled"] === true;
4842
5496
  const blockKitDisabled = process.env["SLACK_BLOCK_KIT_DISABLED"] === "true";
@@ -4846,7 +5500,9 @@ ${sections}`
4846
5500
  ...blockKitAskUserEnabled ? { SLACK_BLOCK_KIT_ASK_USER_ENABLED: "true" } : {},
4847
5501
  ...blockKitDisabled ? { SLACK_BLOCK_KIT_DISABLED: "true" } : {},
4848
5502
  ...blockKitAskUserEnabled ? { AGT_HOST: resolvedAgtHost } : {},
4849
- ...blockKitAskUserEnabled && process.env["AGT_API_KEY"] ? { AGT_API_KEY: process.env["AGT_API_KEY"] } : {},
5503
+ // ENG-5901 Track D: template (manager exports the real value
5504
+ // to every spawn env); gate unchanged.
5505
+ ...blockKitAskUserEnabled && process.env["AGT_API_KEY"] ? { AGT_API_KEY: "${AGT_API_KEY}" } : {},
4850
5506
  ...blockKitAskUserEnabled && options?.agentId ? { AGT_AGENT_ID: options.agentId } : {}
4851
5507
  } : {};
4852
5508
  if (botToken) {
@@ -4875,16 +5531,26 @@ ${sections}`
4875
5531
  const slackResolvedAgtApiKey = process.env["AGT_API_KEY"]?.trim();
4876
5532
  const slackAgtAuthEnv = {
4877
5533
  AGT_HOST: resolvedAgtHost,
4878
- ...slackResolvedAgtApiKey ? { AGT_API_KEY: slackResolvedAgtApiKey } : {},
5534
+ // ENG-5901 Track D: template manager exports AGT_API_KEY to
5535
+ // every spawn env (getApiKey()); the gate still keys off the
5536
+ // manager actually having one.
5537
+ ...slackResolvedAgtApiKey ? { AGT_API_KEY: "${AGT_API_KEY}" } : {},
4879
5538
  ...options?.agentId ? { AGT_AGENT_ID: options.agentId } : {}
4880
5539
  };
5540
+ writeEnvIntegrationsForAgent(codeName, {
5541
+ mode: "upsert",
5542
+ updates: {
5543
+ SLACK_BOT_TOKEN: botToken,
5544
+ ...appToken ? { SLACK_APP_TOKEN: appToken } : {}
5545
+ }
5546
+ });
4881
5547
  const localSlackChannel = join4(getHomeDir3(), ".augmented", "_mcp", "slack-channel.js");
4882
5548
  const slackEntry = {
4883
5549
  command: existsSync5(localSlackChannel) ? "node" : "npx",
4884
5550
  args: existsSync5(localSlackChannel) ? [localSlackChannel] : ["-y", "@augmented/claude-code-channel-slack"],
4885
5551
  env: {
4886
- SLACK_BOT_TOKEN: botToken,
4887
- ...appToken ? { SLACK_APP_TOKEN: appToken } : {},
5552
+ SLACK_BOT_TOKEN: "${SLACK_BOT_TOKEN}",
5553
+ ...appToken ? { SLACK_APP_TOKEN: "${SLACK_APP_TOKEN}" } : {},
4888
5554
  ...threadAutoFollow && threadAutoFollow !== "off" ? { SLACK_THREAD_AUTO_FOLLOW: threadAutoFollow } : {},
4889
5555
  // ENG-4464: only emit when non-default — `mention_only` is the
4890
5556
  // default in slack-response-mode.ts, so omitting keeps the env
@@ -4901,7 +5567,45 @@ ${sections}`
4901
5567
  // (ENG-4936) honours PEER_DISABLED with identical
4902
5568
  // semantics. Only emit when non-default so the env block
4903
5569
  // stays clean for the common case.
4904
- ...peerDisabledMode !== "off" ? { PEER_DISABLED: peerDisabledMode } : {}
5570
+ ...peerDisabledMode !== "off" ? { PEER_DISABLED: peerDisabledMode } : {},
5571
+ // ENG-5841: SLACK_SENDER_POLICY drives slack-inbound-filter.ts.
5572
+ // Only emitted when the effective mode is restrictive (default
5573
+ // 'all' is the absence of the env var — same convention the
5574
+ // MCP filter uses on its own end).
5575
+ ...senderPolicyMode ? { SLACK_SENDER_POLICY: senderPolicyMode } : {},
5576
+ ...senderPolicyEnv,
5577
+ // AGT_TEAM_ID when team_agents_only
5578
+ // ENG-5842: principal ID for manager_only — Slack user_id from
5579
+ // people.contact_preferences.slack_user_id. Omitted when the
5580
+ // principal has no Slack ID; MCP filter fails closed on the
5581
+ // missing env var by dropping all human inbound.
5582
+ ...slackPrincipalId ? { SLACK_SENDER_POLICY_PRINCIPAL_ID: slackPrincipalId } : {},
5583
+ // ENG-5871: team_only mode injects a comma-separated list of
5584
+ // team-member Slack user_ids. Absent/empty = MCP filter drops
5585
+ // humans on Slack but still admits same-team agents via label.
5586
+ // No additional shape parsing on the MCP side — `.split(',')`
5587
+ // works because Slack user_ids are `[A-Z0-9]+` (no commas).
5588
+ ...slackTeamPrincipalIds ? { SLACK_SENDER_POLICY_TEAM_PRINCIPAL_IDS: slackTeamPrincipalIds } : {},
5589
+ // ENG-5843: org-boundary gate. SLACK_INTERNAL_ONLY signals the
5590
+ // filter to check sender's workspace against SLACK_HOME_TEAM_ID
5591
+ // (sourced from the bot install's team_id, populated by
5592
+ // auth.test at install / first-run). Omitted unless explicitly
5593
+ // enabled — the consumer's env-absent default is "no gate".
5594
+ // When INTERNAL_ONLY is true but home team_id can't be
5595
+ // resolved (config['team_id'] not set on the install), the
5596
+ // MCP boot guard fails closed at startup rather than admitting
5597
+ // every sender as "internal".
5598
+ ...senderPolicyInternalOnly ? { SLACK_INTERNAL_ONLY: "true" } : {},
5599
+ ...senderPolicyInternalOnly && typeof config["team_id"] === "string" && config["team_id"].length > 0 ? { SLACK_HOME_TEAM_ID: config["team_id"] } : {},
5600
+ // ENG-6035: per-agent diagnostic/restart allowlist. Gates
5601
+ // /investigate-<code-name> (fail-closed: command disabled when
5602
+ // unset) and /restart-<code-name> (open when unset). An explicit
5603
+ // env entry here overrides any host-level systemd value, which
5604
+ // is the point — the host-wide drop-in pattern wrongly scoped
5605
+ // the allowlist to every agent on the host. Omitted when empty
5606
+ // so the host fallback (and the fail-closed /investigate
5607
+ // default) still apply to unconfigured agents.
5608
+ ...allowedUsers.length > 0 ? { SLACK_ALLOWED_USERS: allowedUsers.join(",") } : {}
4905
5609
  }
4906
5610
  };
4907
5611
  const provisionMcpPath = join4(agentDir, "provision", ".mcp.json");
@@ -4957,6 +5661,8 @@ ${sections}`
4957
5661
  const slackAutoFollowEnv = slackThreadAutoFollow && slackThreadAutoFollow !== "off" ? { SLACK_THREAD_AUTO_FOLLOW: slackThreadAutoFollow } : {};
4958
5662
  const slackChannelResponseMode = config["channel_response_mode"];
4959
5663
  const slackResponseModeEnv = slackChannelResponseMode && slackChannelResponseMode !== "mention_only" ? { SLACK_CHANNEL_RESPONSE_MODE: slackChannelResponseMode } : {};
5664
+ const slackAllowedUsersList = Array.isArray(config["allowed_users"]) ? config["allowed_users"].filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim()) : [];
5665
+ const slackAllowedUsersEnv = slackAllowedUsersList.length > 0 ? { SLACK_ALLOWED_USERS: slackAllowedUsersList.join(",") } : {};
4960
5666
  const oneshotBlockKitEnabled = config["block_kit_enabled"] === true;
4961
5667
  const oneshotBlockKitAskUserEnabled = config["block_kit_ask_user_enabled"] === true;
4962
5668
  const oneshotBlockKitDisabled = process.env["SLACK_BLOCK_KIT_DISABLED"] === "true";
@@ -4966,7 +5672,8 @@ ${sections}`
4966
5672
  ...oneshotBlockKitAskUserEnabled ? { SLACK_BLOCK_KIT_ASK_USER_ENABLED: "true" } : {},
4967
5673
  ...oneshotBlockKitDisabled ? { SLACK_BLOCK_KIT_DISABLED: "true" } : {},
4968
5674
  ...oneshotBlockKitAskUserEnabled ? { AGT_HOST: oneshotResolvedAgtHost } : {},
4969
- ...oneshotBlockKitAskUserEnabled && process.env["AGT_API_KEY"] ? { AGT_API_KEY: process.env["AGT_API_KEY"] } : {},
5675
+ // ENG-5901 Track D: template; manager spawn env carries the value.
5676
+ ...oneshotBlockKitAskUserEnabled && process.env["AGT_API_KEY"] ? { AGT_API_KEY: "${AGT_API_KEY}" } : {},
4970
5677
  ...oneshotBlockKitAskUserEnabled && options?.agentId ? { AGT_AGENT_ID: options.agentId } : {}
4971
5678
  } : {};
4972
5679
  const slackPeerEnv = {};
@@ -4999,18 +5706,27 @@ ${sections}`
4999
5706
  const slackAgtAuthEnv = {
5000
5707
  AGT_HOST: process.env["AGT_HOST"]?.trim() || "https://api.augmented.team",
5001
5708
  AGT_AGENT_CODE_NAME: codeName,
5002
- ...slackResolvedAgtApiKey ? { AGT_API_KEY: slackResolvedAgtApiKey } : {},
5709
+ // ENG-5901 Track D: template; manager spawn env carries the value.
5710
+ ...slackResolvedAgtApiKey ? { AGT_API_KEY: "${AGT_API_KEY}" } : {},
5003
5711
  ...options?.agentId ? { AGT_AGENT_ID: options.agentId } : {}
5004
5712
  };
5713
+ writeEnvIntegrationsForAgent(codeName, {
5714
+ mode: "upsert",
5715
+ updates: {
5716
+ SLACK_BOT_TOKEN: botToken,
5717
+ ...appToken ? { SLACK_APP_TOKEN: appToken } : {}
5718
+ }
5719
+ });
5005
5720
  if (isPersistent && existsSync5(localSlackChannel)) {
5006
5721
  mcpServers["slack"] = {
5007
5722
  command: "node",
5008
5723
  args: [localSlackChannel],
5009
5724
  env: {
5010
- SLACK_BOT_TOKEN: botToken,
5011
- ...appToken ? { SLACK_APP_TOKEN: appToken } : {},
5725
+ SLACK_BOT_TOKEN: "${SLACK_BOT_TOKEN}",
5726
+ ...appToken ? { SLACK_APP_TOKEN: "${SLACK_APP_TOKEN}" } : {},
5012
5727
  ...slackAutoFollowEnv,
5013
5728
  ...slackResponseModeEnv,
5729
+ ...slackAllowedUsersEnv,
5014
5730
  ...oneshotBlockKitEnv,
5015
5731
  ...slackPeerEnv,
5016
5732
  ...slackAgtAuthEnv,
@@ -5022,10 +5738,11 @@ ${sections}`
5022
5738
  command: "npx",
5023
5739
  args: ["-y", "@augmented/claude-code-channel-slack"],
5024
5740
  env: {
5025
- SLACK_BOT_TOKEN: botToken,
5026
- ...appToken ? { SLACK_APP_TOKEN: appToken } : {},
5741
+ SLACK_BOT_TOKEN: "${SLACK_BOT_TOKEN}",
5742
+ ...appToken ? { SLACK_APP_TOKEN: "${SLACK_APP_TOKEN}" } : {},
5027
5743
  ...slackAutoFollowEnv,
5028
5744
  ...slackResponseModeEnv,
5745
+ ...slackAllowedUsersEnv,
5029
5746
  ...oneshotBlockKitEnv,
5030
5747
  ...slackPeerEnv,
5031
5748
  ...slackAgtAuthEnv,
@@ -5060,12 +5777,17 @@ ${sections}`
5060
5777
  const msteamsAgtAuthEnv = {
5061
5778
  AGT_HOST: process.env["AGT_HOST"]?.trim() || "https://api.augmented.team",
5062
5779
  AGT_AGENT_CODE_NAME: codeName,
5063
- ...msResolvedAgtApiKey ? { AGT_API_KEY: msResolvedAgtApiKey } : {},
5780
+ // ENG-5901 Track D: template; manager spawn env carries the value.
5781
+ ...msResolvedAgtApiKey ? { AGT_API_KEY: "${AGT_API_KEY}" } : {},
5064
5782
  ...options?.agentId ? { AGT_AGENT_ID: options.agentId } : {}
5065
5783
  };
5784
+ writeEnvIntegrationsForAgent(codeName, {
5785
+ mode: "upsert",
5786
+ updates: { MSTEAMS_CLIENT_SECRET: clientSecret }
5787
+ });
5066
5788
  const teamsEnv = {
5067
5789
  MSTEAMS_APP_ID: appId,
5068
- MSTEAMS_CLIENT_SECRET: clientSecret,
5790
+ MSTEAMS_CLIENT_SECRET: "${MSTEAMS_CLIENT_SECRET}",
5069
5791
  MSTEAMS_TENANT_ID: tenantId,
5070
5792
  ...botObjectId ? { MSTEAMS_BOT_OBJECT_ID: botObjectId } : {},
5071
5793
  ...allowedTeamIds.length > 0 ? { MSTEAMS_ALLOWED_TEAMS: allowedTeamIds.join(",") } : {},
@@ -5077,7 +5799,28 @@ ${sections}`
5077
5799
  ...peerTeamIds.length > 0 ? { MSTEAMS_PEER_TEAM_IDS: peerTeamIds.join(",") } : {},
5078
5800
  ...knownPeerBotIds.length > 0 ? { MSTEAMS_KNOWN_PEER_BOT_IDS: knownPeerBotIds.join(",") } : {},
5079
5801
  ...msteamsAgtAuthEnv,
5080
- ...tzEnv
5802
+ ...tzEnv,
5803
+ // ENG-5841: MSTEAMS_SENDER_POLICY drives teams-inbound-filter.ts.
5804
+ // Mirrors the Slack branch above — only emitted when restrictive.
5805
+ ...senderPolicyMode ? { MSTEAMS_SENDER_POLICY: senderPolicyMode } : {},
5806
+ ...senderPolicyEnv,
5807
+ // AGT_TEAM_ID when team_agents_only
5808
+ // ENG-5842: principal ID for manager_only — Teams AAD object id from
5809
+ // people.contact_preferences.teams_aad_object_id. Same fail-closed
5810
+ // contract as the Slack branch above.
5811
+ ...teamsPrincipalId ? { MSTEAMS_SENDER_POLICY_PRINCIPAL_ID: teamsPrincipalId } : {},
5812
+ // ENG-5871: team_only mode injects a comma-separated list of team-
5813
+ // member Teams AAD object IDs. Same fail-closed contract as Slack.
5814
+ // AAD IDs are UUIDs (no commas), so .split(',') is safe.
5815
+ ...teamsTeamPrincipalIds ? { MSTEAMS_SENDER_POLICY_TEAM_PRINCIPAL_IDS: teamsTeamPrincipalIds } : {},
5816
+ // ENG-5843: org-boundary gate. MSTEAMS_INTERNAL_ONLY + MSTEAMS_HOME_TENANT_ID
5817
+ // mirror the Slack pair. Source is the same tenantId the existing
5818
+ // MSTEAMS_TENANT_ID env var already carries — defaulting to "common"
5819
+ // would be wrong here (it'd admit any tenant), so we skip the
5820
+ // SLACK_HOME_TEAM_ID-equivalent emission when the install hasn't
5821
+ // pinned a real tenant. MCP boot guard fails closed.
5822
+ ...senderPolicyInternalOnly ? { MSTEAMS_INTERNAL_ONLY: "true" } : {},
5823
+ ...senderPolicyInternalOnly && tenantId !== "common" ? { MSTEAMS_HOME_TENANT_ID: tenantId } : {}
5081
5824
  };
5082
5825
  if (isPersistent && existsSync5(localTeamsChannel)) {
5083
5826
  mcpServers["msteams"] = {
@@ -5131,6 +5874,12 @@ ${sections}`
5131
5874
  });
5132
5875
  return changed;
5133
5876
  },
5877
+ // ENG-5901 PR 3: hoist pre-Track-D literal secrets out of the on-disk
5878
+ // .mcp.json so the armed lint stops rejecting every incremental write.
5879
+ // See migrateExistingLiteralSecrets for the full story.
5880
+ migrateSecretStorage(codeName) {
5881
+ migrateExistingLiteralSecrets(codeName);
5882
+ },
5134
5883
  seedProfileConfig(codeName) {
5135
5884
  const agentDir = getAgentDir(codeName);
5136
5885
  const projectDir = getProjectDir(codeName);
@@ -5162,19 +5911,19 @@ ${sections}`
5162
5911
  ...integration,
5163
5912
  credentials: decryptIntegrationCredentials(integration.credentials)
5164
5913
  }));
5165
- const envLines = ["# Augmented integrations \u2014 auto-generated, do not edit"];
5914
+ const envUpdates = {};
5166
5915
  for (const integration of decryptedIntegrations) {
5167
5916
  const prefix = integration.definition_id.toUpperCase().replace(/[^A-Z0-9]/g, "_");
5168
5917
  const creds = integration.credentials;
5169
5918
  if (integration.auth_type === "oauth2") {
5170
5919
  const accessToken = creds.access_token;
5171
5920
  if (accessToken) {
5172
- envLines.push(`${prefix}_ACCESS_TOKEN=${shellQuote(accessToken)}`);
5921
+ envUpdates[`${prefix}_ACCESS_TOKEN`] = accessToken;
5173
5922
  }
5174
5923
  } else if (integration.auth_type === "api_key") {
5175
5924
  const apiKey = creds.api_key;
5176
5925
  if (apiKey) {
5177
- envLines.push(`${prefix}_API_KEY=${shellQuote(apiKey)}`);
5926
+ envUpdates[`${prefix}_API_KEY`] = apiKey;
5178
5927
  }
5179
5928
  }
5180
5929
  if (integration.config) {
@@ -5183,20 +5932,37 @@ ${sections}`
5183
5932
  if (typeof value === "string" && value) {
5184
5933
  const upperKey = key.toUpperCase();
5185
5934
  const envKey = upperKey.startsWith(`${prefix}_`) ? upperKey : `${prefix}_${upperKey}`;
5186
- envLines.push(`${envKey}=${shellQuote(value)}`);
5935
+ envUpdates[envKey] = value;
5187
5936
  }
5188
5937
  }
5189
5938
  }
5190
5939
  }
5191
- if (envLines.length > 1) {
5192
- const envPath = join4(agentDir, ".env.integrations");
5193
- writeFileSync5(envPath, envLines.join("\n") + "\n");
5194
- chmodSync4(envPath, SECRET_FILE_MODE);
5940
+ for (const integration of decryptedIntegrations) {
5941
+ const def = INTEGRATION_REGISTRY.find((d) => d.id === integration.definition_id);
5942
+ const defaults = def?.remoteMcp?.envDefaults;
5943
+ if (!defaults)
5944
+ continue;
5945
+ for (const [key, value] of Object.entries(defaults)) {
5946
+ if (key in envUpdates)
5947
+ continue;
5948
+ envUpdates[key] = value;
5949
+ }
5195
5950
  }
5951
+ writeEnvIntegrationsForAgent(codeName, {
5952
+ mode: "replace-preserving",
5953
+ updates: envUpdates
5954
+ });
5196
5955
  writeXurlStoreForIntegrations(decryptedIntegrations);
5197
- const hasQmd = integrations.some((i) => i.definition_id === "qmd");
5198
- if (hasQmd) {
5199
- this.writeMcpServer(codeName, "qmd", { command: "qmd", args: ["mcp"] });
5956
+ for (const integration of decryptedIntegrations) {
5957
+ const def = INTEGRATION_REGISTRY.find((d) => d.id === integration.definition_id);
5958
+ if (!def?.nativeMcp)
5959
+ continue;
5960
+ const key = def.nativeMcp.key ?? integration.definition_id;
5961
+ this.writeMcpServer(codeName, key, buildNativeMcpEntry(def.nativeMcp, {
5962
+ agentId: agentId ?? "",
5963
+ agentCodeName: codeName,
5964
+ integration
5965
+ }));
5200
5966
  }
5201
5967
  const xeroIntegration = integrations.find((i) => i.definition_id === "xero");
5202
5968
  if (xeroIntegration) {
@@ -5255,16 +6021,15 @@ ${sections}`
5255
6021
  }
5256
6022
  }
5257
6023
  if (this.removeMcpServer) {
6024
+ const nativeMcpKeys = INTEGRATION_REGISTRY.filter((d) => d.nativeMcp !== void 0).map((d) => d.nativeMcp.key ?? d.id);
5258
6025
  const integrationDerivedKeys = /* @__PURE__ */ new Set([
5259
- "qmd",
5260
6026
  "xero",
5261
6027
  "postiz",
5262
6028
  "cloud-broker",
6029
+ ...nativeMcpKeys,
5263
6030
  ...Object.entries(OAUTH_PROVIDERS).filter(([, provider]) => Boolean(provider.mcpUrl)).map(([id]) => id)
5264
6031
  ]);
5265
6032
  const expectedKeys = /* @__PURE__ */ new Set();
5266
- if (hasQmd)
5267
- expectedKeys.add("qmd");
5268
6033
  if (xeroIntegration)
5269
6034
  expectedKeys.add("xero");
5270
6035
  if (postizIntegration)
@@ -5272,6 +6037,10 @@ ${sections}`
5272
6037
  if (hasCloudBroker)
5273
6038
  expectedKeys.add("cloud-broker");
5274
6039
  for (const integration of integrations) {
6040
+ const def = INTEGRATION_REGISTRY.find((d) => d.id === integration.definition_id);
6041
+ if (def?.nativeMcp) {
6042
+ expectedKeys.add(def.nativeMcp.key ?? integration.definition_id);
6043
+ }
5275
6044
  if (buildRemoteMcpEntry(integration.definition_id)) {
5276
6045
  expectedKeys.add(integration.definition_id);
5277
6046
  }
@@ -5303,12 +6072,18 @@ ${sections}`
5303
6072
  const envSrc = join4(agentDir2, ".env.integrations");
5304
6073
  try {
5305
6074
  const envContent = readFileSync5(envSrc, "utf-8");
5306
- writeFileSync5(join4(projectDir, ".env.integrations"), envContent);
6075
+ const envDest = join4(projectDir, ".env.integrations");
6076
+ writeFileSync5(envDest, envContent, { mode: SECRET_FILE_MODE });
6077
+ try {
6078
+ chmodSync5(envDest, SECRET_FILE_MODE);
6079
+ } catch {
6080
+ }
5307
6081
  } catch {
5308
6082
  }
5309
6083
  } catch {
5310
6084
  }
5311
6085
  renderChannelMessageHandlerForAgent(codeName);
6086
+ renderAugmentedWorkerForAgent(codeName);
5312
6087
  },
5313
6088
  writeMcpServer(codeName, serverId, config) {
5314
6089
  const agentDir = getAgentDir(codeName);
@@ -5326,7 +6101,27 @@ ${sections}`
5326
6101
  const mcpServers = mcpConfig["mcpServers"];
5327
6102
  let serverEntry;
5328
6103
  if ("url" in config) {
5329
- serverEntry = buildUrlMcpServerEntry(config.url, config.headers);
6104
+ serverEntry = buildUrlMcpServerEntry(config.url, config.headers, config.type);
6105
+ const hoisted = {};
6106
+ const entryHeaders = serverEntry.headers;
6107
+ if (entryHeaders) {
6108
+ const composioKey = entryHeaders["x-api-key"];
6109
+ if (composioKey && !composioKey.includes("${")) {
6110
+ hoisted["COMPOSIO_API_KEY"] = composioKey;
6111
+ entryHeaders["x-api-key"] = "${COMPOSIO_API_KEY}";
6112
+ }
6113
+ }
6114
+ const entryEnv = serverEntry.env;
6115
+ if (entryEnv) {
6116
+ const pdSecret = entryEnv["PIPEDREAM_CLIENT_SECRET"];
6117
+ if (pdSecret && !pdSecret.includes("${")) {
6118
+ hoisted["PIPEDREAM_CLIENT_SECRET"] = pdSecret;
6119
+ entryEnv["PIPEDREAM_CLIENT_SECRET"] = "${PIPEDREAM_CLIENT_SECRET}";
6120
+ }
6121
+ }
6122
+ if (Object.keys(hoisted).length > 0) {
6123
+ writeEnvIntegrationsForAgent(codeName, { mode: "upsert", updates: hoisted });
6124
+ }
5330
6125
  } else {
5331
6126
  serverEntry = { command: config.command };
5332
6127
  if (config.args?.length)
@@ -5379,14 +6174,14 @@ ${sections}`
5379
6174
  mkdirSync4(join4(filePath, ".."), { recursive: true });
5380
6175
  if (isPluginManaged && existsSync5(filePath)) {
5381
6176
  try {
5382
- chmodSync4(filePath, READ_WRITE_MODE);
6177
+ chmodSync5(filePath, READ_WRITE_MODE);
5383
6178
  } catch {
5384
6179
  }
5385
6180
  }
5386
6181
  writeFileSync5(filePath, file.content);
5387
6182
  if (isPluginManaged) {
5388
6183
  try {
5389
- chmodSync4(filePath, READ_ONLY_MODE);
6184
+ chmodSync5(filePath, READ_ONLY_MODE);
5390
6185
  } catch {
5391
6186
  }
5392
6187
  }
@@ -5567,7 +6362,7 @@ ${sections}`
5567
6362
  return;
5568
6363
  const tokenPath = join4(agentDir, ".tokens.json");
5569
6364
  writeFileSync5(tokenPath, JSON.stringify(tokens, null, 2));
5570
- chmodSync4(tokenPath, SECRET_FILE_MODE);
6365
+ chmodSync5(tokenPath, SECRET_FILE_MODE);
5571
6366
  }
5572
6367
  };
5573
6368
  registerFramework(claudeCodeAdapter);
@@ -5933,12 +6728,15 @@ function setActiveTeam(slug) {
5933
6728
  config.active_team = slug;
5934
6729
  saveConfig(config);
5935
6730
  }
5936
- var DEFAULT_AGT_HOST = "https://api.augmented.team";
6731
+ var PROD_AGT_HOST = "https://api.augmented.team";
6732
+ var AGT_HOST_UNSET_MESSAGE = "AGT_HOST is not set. Set it to the API URL for your stage before running this command:\n Production: export AGT_HOST=https://api.augmented.team\n Non-prod stage: export AGT_HOST=https://<stage>.api.staging.augmented.team\n Local development: export AGT_HOST=http://api.agt.localhost:1355\n\nFor first-time host setup, pass --api-host <url> to `agt setup` instead \u2014 the setup command does NOT silently default to prod (ENG-5831).";
5937
6733
  function getHost() {
5938
6734
  const envHost = process.env["AGT_HOST"]?.trim();
5939
- return envHost ? envHost : DEFAULT_AGT_HOST;
6735
+ if (!envHost) {
6736
+ throw new Error(AGT_HOST_UNSET_MESSAGE);
6737
+ }
6738
+ return envHost;
5940
6739
  }
5941
- var AGT_HOST = getHost();
5942
6740
  function requireHost() {
5943
6741
  return getHost();
5944
6742
  }
@@ -6142,7 +6940,7 @@ import { homedir as homedir6, userInfo } from "os";
6142
6940
  import { spawn as spawn3 } from "child_process";
6143
6941
 
6144
6942
  // src/lib/watchdog.ts
6145
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, unlinkSync as unlinkSync4, existsSync as existsSync7, mkdirSync as mkdirSync6, openSync, closeSync, chmodSync as chmodSync5 } from "fs";
6943
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, unlinkSync as unlinkSync4, existsSync as existsSync7, mkdirSync as mkdirSync6, openSync, closeSync, chmodSync as chmodSync6 } from "fs";
6146
6944
  import { join as join7 } from "path";
6147
6945
  import { spawn as spawn2, execFileSync } from "child_process";
6148
6946
  var DEFAULT_CONFIG_DIR = join7(process.env["HOME"] ?? "/tmp", ".augmented");
@@ -6236,7 +7034,7 @@ function startWatchdog(opts) {
6236
7034
  const { logFile } = getManagerPaths(configDir);
6237
7035
  const logFd = openSync(logFile, "a", 384);
6238
7036
  try {
6239
- chmodSync5(logFile, 384);
7037
+ chmodSync6(logFile, 384);
6240
7038
  } catch {
6241
7039
  }
6242
7040
  const intervalSec = String(Math.max(Math.floor(opts.intervalMs / 1e3), 5));
@@ -6749,9 +7547,12 @@ async function managerUninstallSystemUnitCommand() {
6749
7547
  export {
6750
7548
  getIntegration,
6751
7549
  extractCommandNotFound,
7550
+ LITERAL_SECRET_PATTERNS,
7551
+ safeWriteJsonAtomic,
6752
7552
  INTEGRATIONS_SECTION_START,
6753
7553
  INTEGRATIONS_SECTION_END,
6754
7554
  estimateActiveTasksTokens,
7555
+ CHANNEL_SECRET_ENV_KEYS,
6755
7556
  provisionStopHook,
6756
7557
  provisionIsolationHook,
6757
7558
  provisionOrientHook,
@@ -6761,7 +7562,7 @@ export {
6761
7562
  getApiKey,
6762
7563
  getActiveTeam,
6763
7564
  setActiveTeam,
6764
- DEFAULT_AGT_HOST,
7565
+ PROD_AGT_HOST,
6765
7566
  getHost,
6766
7567
  requireHost,
6767
7568
  exchangeApiKey,
@@ -6785,4 +7586,4 @@ export {
6785
7586
  managerInstallSystemUnitCommand,
6786
7587
  managerUninstallSystemUnitCommand
6787
7588
  };
6788
- //# sourceMappingURL=chunk-IB655E5U.js.map
7589
+ //# sourceMappingURL=chunk-N2EBL4PZ.js.map