@integrity-labs/agt-cli 0.27.8-test.9 → 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.
@@ -9,7 +9,7 @@ import {
9
9
  parseDeliveryTarget,
10
10
  registerFramework,
11
11
  wrapScheduledTaskPrompt
12
- } from "./chunk-HT6EETEL.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",
@@ -2322,6 +2356,299 @@ function extractAllowedDomains(input) {
2322
2356
  }
2323
2357
  registerFramework(nemoClawAdapter);
2324
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
+
2325
2652
  // ../../packages/core/dist/provisioning/frameworks/claudecode/identity.js
2326
2653
  function buildMemorySection(hasQmd) {
2327
2654
  const recall = hasQmd ? `### Recall
@@ -2570,15 +2897,15 @@ later tick:
2570
2897
  - For long-form output (>500 chars), lead with a one-line summary,
2571
2898
  then a blank line, then the full content. Channel formatters
2572
2899
  truncate gracefully but the lede always lands.
2573
- - **\`kanban_fail\` with a reason** when something went wrong
2574
- (missing access, credential failure, tool error). The work was
2575
- attempted but failed.
2576
- - **\`kanban_cancel\` with a reason** when you started, then realised
2577
- the work isn't actually needed (precondition no longer holds,
2578
- duplicate of another task, asker changed their mind, data was
2579
- already current). This is distinct from \`kanban_fail\`: nothing
2580
- went wrong; you wisely didn't bother. Use this generously \u2014 it's
2581
- 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.
2582
2909
  - **\`kanban_update\` with notes** if you're blocked but might unblock
2583
2910
  later; leave the row in \`in_progress\` and pick up other work.
2584
2911
 
@@ -2922,6 +3249,16 @@ function formatEmailDomainConfigLines(config) {
2922
3249
  lines.push(` *Advisory: honor this restriction \u2014 it is guidance in your instructions, not a hard block at send time.*`);
2923
3250
  return lines;
2924
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
+ }
2925
3262
  function renderGuardrailBullet(g) {
2926
3263
  const lines = [];
2927
3264
  const header = `- **${g.displayName}** (${g.category}, from ${g.source})`;
@@ -2929,16 +3266,26 @@ function renderGuardrailBullet(g) {
2929
3266
  if (g.description?.trim()) {
2930
3267
  lines.push(` ${g.description.trim()}`);
2931
3268
  }
2932
- lines.push(...g.definitionId === EMAIL_DOMAIN_RESTRICT_DEF ? formatEmailDomainConfigLines(g.config) : 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));
2933
3270
  if (g.overrideApplied && g.overrideReason?.trim()) {
2934
3271
  lines.push(` *Override applied: ${g.overrideReason.trim()}*`);
2935
3272
  }
2936
3273
  return lines.join("\n");
2937
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
+ }
2938
3285
  function buildGuardrailsSection(guardrails) {
2939
3286
  if (!guardrails || guardrails.length === 0)
2940
3287
  return "";
2941
- const active = guardrails.filter((g) => g.enforcement !== "disabled");
3288
+ const active = guardrails.map((g) => ({ ...g, enforcement: effectiveCalendarEnforcement(g) })).filter((g) => g.enforcement !== "disabled");
2942
3289
  if (active.length === 0)
2943
3290
  return "";
2944
3291
  const enforce = active.filter((g) => g.enforcement === "enforce");
@@ -3014,33 +3361,72 @@ acknowledge before you start.
3014
3361
  - **FAST (< 60s):** handle inline. Reply via the channel tool
3015
3362
  (slack.reply / telegram.reply / directchat.reply) and end your turn.
3016
3363
 
3017
- - **SLOW (\u2265 60s):** dispatch.
3364
+ - **SLOW (\u2265 60s):** acknowledge first, then handle inline.
3018
3365
  1. Send a one-line acknowledgement via the channel tool \u2014 short, warm,
3019
3366
  and tell the user you'll come back. Example shape (don't copy verbatim,
3020
3367
  match your voice): "On it \u2014 this'll take a minute or two, I'll ping
3021
3368
  when it's done."
3022
- 2. Invoke the \`channel-message-handler\` subagent (Task tool, with
3023
- \`subagent_type: channel-message-handler\`) with the full original
3024
- message text plus the **channel-specific routing keys** the reply
3025
- tool needs: Slack threads \u2192 \`{ channel_id, thread_ts }\`; Slack
3026
- non-thread DMs \u2192 \`{ channel_id }\`; Telegram \u2192 \`{ chat_id, message_id }\`;
3027
- Direct Chat \u2192 \`{ conversation_id }\`. Pass these verbatim from the
3028
- inbound \`<channel>\` tag \u2014 don't substitute a generic \`message_ts\`,
3029
- since only Slack threads use it. The subagent will do the actual
3030
- work and post the real reply itself.
3031
- 3. End your turn. **Do NOT call slack.reply / telegram.reply with the
3032
- full result yourself** \u2014 that's the subagent's job.
3033
-
3034
- **Why this matters more than any other instruction below:** if you handle
3035
- slow requests inline, you go silent for 60+ seconds while operators send
3036
- follow-up messages that queue behind you. Dispatching keeps you free to
3037
- acknowledge new pings. The kanban-tracking-link convention, work-management
3038
- "create a task" guidance, and Slack reply patterns ALL apply to fast
3039
- inline replies \u2014 they do NOT replace this dispatch decision.
3040
-
3041
- If the work turns out to be unexpectedly slow after you started inline,
3042
- finish the current sub-step, then dispatch the rest. Don't apologise for
3043
- mid-task switching \u2014 operators care about responsiveness, not consistency.
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.
3044
3430
 
3045
3431
  ${activeTasksSection}${personalitySection}## Identity
3046
3432
 
@@ -3050,13 +3436,30 @@ ${activeTasksSection}${personalitySection}## Identity
3050
3436
  - Risk Tier: ${frontmatter.risk_tier}
3051
3437
  - Timezone: ${timezone?.trim() || "UTC"}
3052
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.
3053
3454
  ${resolvedChannels?.includes("slack") ? `
3054
3455
  ## Slack
3055
3456
 
3056
3457
  You have a Slack MCP server connected. **First, see \xA7 FIRST ACTION on
3057
3458
  every channel message: triage** at the top of this document \u2014 decide
3058
- fast vs slow before anything else, and dispatch slow work via
3059
- \`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).
3060
3463
 
3061
3464
  For fast requests, respond directly in the conversation. You can also
3062
3465
  proactively use:
@@ -3214,15 +3617,21 @@ When you receive a request via any channel (Slack, Telegram, direct chat):
3214
3617
  3. **Create the kanban task** with kanban.add. Title should be specific
3215
3618
  enough to be self-explanatory \u2014 "Pull Linear ENG sprint velocity for
3216
3619
  this fortnight" beats "Linear stats".
3217
- 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"}".
3218
3627
  5. Move the task to in_progress with kanban.move
3219
3628
  6. Do the work
3220
3629
  7. Mark done with kanban.done including a result summary
3221
3630
  8. Reply in the channel thread with the result
3222
3631
 
3223
3632
  Everything that isn't exempt gets a task \u2014 but only after the request is
3224
- clear. Don't bury a clarifying question underneath a "tracking here" reply;
3225
- 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
3226
3635
  anything goes on the board.
3227
3636
 
3228
3637
  When asked about existing work, tasks, or what you've been doing \u2014 call kanban.list
@@ -3347,163 +3756,90 @@ The marginal cost of completeness is near zero. Do the whole thing.
3347
3756
  ${frontmatter.environment === "prod" ? "- Production environment: exercise extra caution with all operations.\n" : ""}`;
3348
3757
  }
3349
3758
 
3350
- // ../../packages/core/dist/provisioning/frameworks/claudecode/index.js
3351
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync5, chmodSync as chmodSync4, readdirSync, rmSync, copyFileSync } from "fs";
3352
- import { join as join4, relative, dirname as dirname4 } from "path";
3353
- import { homedir as homedir3 } from "os";
3354
- import { execFile as execFile3 } from "child_process";
3355
-
3356
- // ../../packages/core/dist/provisioning/mcp-config-guards.js
3357
- import { existsSync as existsSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3 } from "fs";
3358
- var REQUIRED_ENV_RULES_BY_SERVER = {
3359
- "cloud-broker": [
3360
- { key: "AGT_HOST", mustBeConcrete: false },
3361
- { key: "AGT_API_KEY", mustBeConcrete: false },
3362
- // ENG-4744: this is the bug shape — writer used to omit this
3363
- // entirely, or render it as a literal `${AGT_AGENT_ID}` instead
3364
- // of the agent's real UUID. The broker has no way to substitute
3365
- // it post-spawn, so a placeholder here = silently broken agent.
3366
- { key: "AGT_AGENT_ID", mustBeConcrete: true }
3367
- ]
3368
- };
3369
- var PLACEHOLDER_RE = /\$\{[^}]+\}/;
3370
- function validateRenderedMcpConfig(config) {
3371
- const errors = [];
3372
- if (typeof config !== "object" || config === null || Array.isArray(config)) {
3373
- return {
3374
- ok: false,
3375
- errors: [
3376
- {
3377
- kind: "invalid_json_shape",
3378
- server: "*",
3379
- message: "config root must be a non-null object"
3380
- }
3381
- ]
3382
- };
3383
- }
3384
- const root = config;
3385
- if (root.mcpServers === void 0) {
3386
- return { ok: true };
3387
- }
3388
- if (typeof root.mcpServers !== "object" || root.mcpServers === null) {
3389
- return {
3390
- ok: false,
3391
- errors: [
3392
- {
3393
- kind: "invalid_json_shape",
3394
- server: "*",
3395
- message: "mcpServers must be an object"
3396
- }
3397
- ]
3398
- };
3399
- }
3400
- if (Array.isArray(root.mcpServers)) {
3401
- return {
3402
- ok: false,
3403
- errors: [
3404
- {
3405
- kind: "invalid_json_shape",
3406
- server: "*",
3407
- message: "mcpServers must be an object"
3408
- }
3409
- ]
3410
- };
3411
- }
3412
- for (const [serverKey, raw] of Object.entries(root.mcpServers)) {
3413
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
3414
- errors.push({
3415
- kind: "invalid_json_shape",
3416
- server: serverKey,
3417
- message: `entry must be an object`
3418
- });
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("="))
3419
3782
  continue;
3420
- }
3421
- const entry = raw;
3422
- const entryRecord = entry;
3423
- if (typeof entryRecord["url"] === "string") {
3424
- const type = entryRecord["type"];
3425
- if (type !== "http" && type !== "sse") {
3426
- errors.push({
3427
- kind: "remote_mcp_missing_type",
3428
- server: serverKey,
3429
- message: `url-based entry must include \`type: "http"\` or \`type: "sse"\` (Claude Code MCP schema requires it)`
3430
- });
3431
- }
3432
- }
3433
- const rules = REQUIRED_ENV_RULES_BY_SERVER[serverKey];
3434
- if (rules) {
3435
- const env = entry.env ?? {};
3436
- for (const rule of rules) {
3437
- const value = env[rule.key];
3438
- if (typeof value !== "string" || value.length === 0) {
3439
- errors.push({
3440
- kind: "missing_required_env",
3441
- server: serverKey,
3442
- message: `missing required env key: ${rule.key}`
3443
- });
3444
- continue;
3445
- }
3446
- if (rule.mustBeConcrete && PLACEHOLDER_RE.test(value)) {
3447
- errors.push({
3448
- kind: "unexpanded_placeholder",
3449
- server: serverKey,
3450
- message: `env.${rule.key} contains an unexpanded \${...} placeholder; expected a concrete value`
3451
- });
3452
- }
3453
- }
3454
- }
3783
+ const eqIdx = line.indexOf("=");
3784
+ out.set(line.slice(0, eqIdx), line.slice(eqIdx + 1));
3455
3785
  }
3456
- return errors.length === 0 ? { ok: true } : { ok: false, errors };
3457
- }
3458
- function formatValidationErrors(errors) {
3459
- return errors.map((e) => `${e.server}:${e.kind}=${e.message}`).join("; ");
3786
+ return out;
3460
3787
  }
3461
- function safeWriteJsonAtomic(path, content, opts = {}) {
3462
- const keepBackup = opts.keepBackup !== false;
3463
- const rename = opts.renamer ?? renameSync3;
3464
- const tmpPath = `${path}.new`;
3465
- const bakPath = `${path}.bak`;
3466
- let movedOriginalToBackup = false;
3467
- try {
3468
- writeFileSync4(tmpPath, content);
3469
- } catch (err) {
3470
- throw err;
3471
- }
3472
- try {
3473
- if (keepBackup && existsSync4(path)) {
3474
- rename(path, bakPath);
3475
- movedOriginalToBackup = true;
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));
3476
3804
  }
3477
- rename(tmpPath, path);
3478
- } catch (err) {
3479
- try {
3480
- if (existsSync4(tmpPath))
3481
- unlinkSync3(tmpPath);
3482
- } catch {
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));
3483
3810
  }
3484
- if (movedOriginalToBackup && !existsSync4(path) && existsSync4(bakPath)) {
3485
- try {
3486
- renameSync3(bakPath, path);
3487
- } catch {
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));
3488
3817
  }
3489
3818
  }
3490
- throw err;
3491
- }
3492
- }
3493
- function safeWriteMcpJson(path, config) {
3494
- const validation = validateRenderedMcpConfig(config);
3495
- if (!validation.ok) {
3496
- return { written: false, errors: validation.errors };
3497
3819
  }
3498
- safeWriteJsonAtomic(path, JSON.stringify(config, null, 2));
3499
- return { written: true, errors: [] };
3820
+ return renderEnvIntegrations(next);
3500
3821
  }
3501
3822
 
3823
+ // ../../packages/core/dist/provisioning/frameworks/claudecode/index.js
3824
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync5, chmodSync as chmodSync5, readdirSync, rmSync, copyFileSync } from "fs";
3825
+ import { join as join4, relative, dirname as dirname4 } from "path";
3826
+ import { homedir as homedir3 } from "os";
3827
+ import { execFile as execFile3 } from "child_process";
3828
+
3502
3829
  // ../../packages/core/dist/provisioning/remote-mcp.js
3503
3830
  function envVarForToken(definitionId) {
3504
3831
  return `${definitionId.replace(/-/g, "_").toUpperCase()}_ACCESS_TOKEN`;
3505
3832
  }
3506
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
+ }
3507
3843
  const provider = OAUTH_PROVIDERS[definitionId];
3508
3844
  if (!provider?.mcpUrl)
3509
3845
  return null;
@@ -3579,8 +3915,95 @@ function resolveTemplate(input, ctx) {
3579
3915
  // ../../packages/core/dist/provisioning/frameworks/claudecode/index.js
3580
3916
  var VALID_CODE_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
3581
3917
  var SECRET_FILE_MODE = 384;
3582
- function shellQuote(value) {
3583
- 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
+ }
3584
4007
  }
3585
4008
  function assertValidCodeName(codeName) {
3586
4009
  if (!VALID_CODE_NAME.test(codeName)) {
@@ -3669,10 +4092,23 @@ function syncMcpToProject(codeName) {
3669
4092
  try {
3670
4093
  const content = readFileSync5(provisionMcpPath, "utf-8");
3671
4094
  mkdirSync4(projectDir, { recursive: true });
3672
- 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
+ }
3673
4108
  } catch {
3674
4109
  }
3675
4110
  renderChannelMessageHandlerForAgent(codeName);
4111
+ renderAugmentedWorkerForAgent(codeName);
3676
4112
  }
3677
4113
  var INTEGRATIONS_SUMMARY_FILE = "integrations-summary.json";
3678
4114
  function integrationsSummaryPath(codeName) {
@@ -3778,7 +4214,15 @@ function deployArtifactsToProject(codeName, provisionDir) {
3778
4214
  continue;
3779
4215
  }
3780
4216
  }
3781
- 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
+ }
3782
4226
  } catch {
3783
4227
  }
3784
4228
  }
@@ -3864,14 +4308,23 @@ function deployArtifactsToProject(codeName, provisionDir) {
3864
4308
  return !(entry && typeof entry["url"] === "string" && entry["url"].startsWith("/"));
3865
4309
  }));
3866
4310
  projectMcp["mcpServers"] = { ...stripRelativeUrls(projectServers), ...stripRelativeUrls(agentServers) };
3867
- 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
+ }
3868
4316
  } catch {
3869
4317
  }
3870
4318
  const agentDir = getAgentDir(codeName);
3871
4319
  for (const envFile of [".env", ".env.integrations"]) {
3872
4320
  try {
3873
4321
  const content = readFileSync5(join4(agentDir, envFile), "utf-8");
3874
- 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
+ }
3875
4328
  } catch {
3876
4329
  }
3877
4330
  }
@@ -3886,7 +4339,7 @@ function deployArtifactsToProject(codeName, provisionDir) {
3886
4339
  const upToDate = existsSync5(hookDest) && readFileSync5(hookDest, "utf-8") === srcContent;
3887
4340
  if (!upToDate)
3888
4341
  writeFileSync5(hookDest, srcContent);
3889
- chmodSync4(hookDest, 493);
4342
+ chmodSync5(hookDest, 493);
3890
4343
  }
3891
4344
  } catch {
3892
4345
  }
@@ -4403,6 +4856,7 @@ function buildChannelMessageHandlerAgent(args) {
4403
4856
  "Glob",
4404
4857
  "Skill",
4405
4858
  "Agent",
4859
+ "ToolSearch",
4406
4860
  ...mcpWildcards
4407
4861
  ].join(", ");
4408
4862
  const integrationsBlock = integrations.length === 0 ? "" : `
@@ -4441,6 +4895,82 @@ Your job:
4441
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.
4442
4896
  ${integrationsBlock}`;
4443
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
+ }
4444
4974
  function buildPostizMcpEntry(integration) {
4445
4975
  const rawBaseUrl = integration.config["base_url"];
4446
4976
  const postizBaseUrl = typeof rawBaseUrl === "string" ? rawBaseUrl.trim() : "";
@@ -4468,7 +4998,13 @@ function buildMcpJson(input) {
4468
4998
  args: [localMcpPath],
4469
4999
  env: {
4470
5000
  AGT_HOST: process.env["AGT_HOST"] ?? "",
4471
- 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}",
4472
5008
  AGT_AGENT_ID: input.agent.agent_id,
4473
5009
  AGT_AGENT_CODE_NAME: input.agent.code_name,
4474
5010
  // ENG-4561: Claude Code substitutes `${VAR}` in .mcp.json env values
@@ -4591,7 +5127,7 @@ function mapScheduledTasks(tasks) {
4591
5127
  };
4592
5128
  });
4593
5129
  }
4594
- function buildUrlMcpServerEntry(url, headers) {
5130
+ function buildUrlMcpServerEntry(url, headers, type = "http") {
4595
5131
  const hasHeaders = !!headers && Object.keys(headers).length > 0;
4596
5132
  if (url.includes("composio.dev") && hasHeaders) {
4597
5133
  return { type: "http", url, headers };
@@ -4614,7 +5150,7 @@ function buildUrlMcpServerEntry(url, headers) {
4614
5150
  };
4615
5151
  }
4616
5152
  if (hasHeaders) {
4617
- return { type: "http", url, headers };
5153
+ return { type, url, headers };
4618
5154
  }
4619
5155
  return { command: "npx", args: ["-y", "mcp-remote", url, "--allow-http"] };
4620
5156
  }
@@ -4695,6 +5231,18 @@ var claudeCodeAdapter = {
4695
5231
  integrations: integrationSummaries
4696
5232
  })
4697
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
+ },
4698
5246
  {
4699
5247
  relativePath: `provision/${INTEGRATIONS_SUMMARY_FILE}`,
4700
5248
  content: JSON.stringify(integrationSummaries, null, 2)
@@ -4812,7 +5360,7 @@ ${sections}`
4812
5360
  if (envLines.length > 1) {
4813
5361
  const envPath = join4(agentDir, ".env");
4814
5362
  writeFileSync5(envPath, envLines.join("\n") + "\n");
4815
- chmodSync4(envPath, SECRET_FILE_MODE);
5363
+ chmodSync5(envPath, SECRET_FILE_MODE);
4816
5364
  }
4817
5365
  },
4818
5366
  // Claude Code has no gateway process — methods intentionally omitted
@@ -4836,20 +5384,14 @@ ${sections}`
4836
5384
  },
4837
5385
  writeChannelCredentials(codeName, channelId, config, options) {
4838
5386
  const tzEnv = options?.agentTimezone && options.agentTimezone.trim() !== "" ? { TZ: options.agentTimezone.trim() } : {};
4839
- const senderPolicyEnv = (() => {
4840
- const sp = options?.senderPolicy;
4841
- if (!sp)
4842
- return {};
4843
- return {
4844
- // Channel-specific var name to match what the consumer MCP reads;
4845
- // spread below selects the right one per channelId.
4846
- ...sp.mode === "team_agents_only" && sp.team_id ? { AGT_TEAM_ID: sp.team_id } : {},
4847
- // sentinel keys consumed by the per-channel spread below
4848
- _SENDER_POLICY_MODE: sp.mode
4849
- };
4850
- })();
4851
- const senderPolicyMode = senderPolicyEnv["_SENDER_POLICY_MODE"];
4852
- delete senderPolicyEnv["_SENDER_POLICY_MODE"];
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 } : {};
4853
5395
  const agentDir = getAgentDir(codeName);
4854
5396
  mkdirSync4(agentDir, { recursive: true });
4855
5397
  const isPersistent = options?.sessionMode === "persistent";
@@ -4862,17 +5404,28 @@ ${sections}`
4862
5404
  const localTelegramChannel = join4(getHomeDir3(), ".augmented", "_mcp", "telegram-channel.js");
4863
5405
  const resolvedAgtHostForTelegram = process.env["AGT_HOST"]?.trim() || "https://api.augmented.team";
4864
5406
  const resolvedAgtApiKeyForTelegram = process.env["AGT_API_KEY"]?.trim();
5407
+ writeEnvIntegrationsForAgent(codeName, {
5408
+ mode: "upsert",
5409
+ updates: { TELEGRAM_BOT_TOKEN: botToken }
5410
+ });
4865
5411
  const telegramEnv = {
4866
- TELEGRAM_BOT_TOKEN: botToken,
5412
+ TELEGRAM_BOT_TOKEN: "${TELEGRAM_BOT_TOKEN}",
4867
5413
  AGT_AGENT_CODE_NAME: codeName,
4868
5414
  AGT_HOST: resolvedAgtHostForTelegram,
4869
- ...resolvedAgtApiKeyForTelegram ? { AGT_API_KEY: resolvedAgtApiKeyForTelegram } : {},
5415
+ ...resolvedAgtApiKeyForTelegram ? { AGT_API_KEY: "${AGT_API_KEY}" } : {},
4870
5416
  ...options?.agentId ? { AGT_AGENT_ID: options.agentId } : {},
4871
5417
  ...tzEnv
4872
5418
  };
4873
5419
  if (allowedChats && allowedChats.length > 0) {
4874
5420
  telegramEnv.TELEGRAM_ALLOWED_CHATS = allowedChats.join(",");
4875
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
+ }
4876
5429
  const rawPeerAgentMode = config["peer_agent_mode"];
4877
5430
  if (rawPeerAgentMode === "listen" || rawPeerAgentMode === "respond") {
4878
5431
  telegramEnv.TELEGRAM_PEER_AGENT_MODE = rawPeerAgentMode;
@@ -4937,6 +5490,7 @@ ${sections}`
4937
5490
  const appToken = config["app_token"];
4938
5491
  const threadAutoFollow = config["thread_auto_follow"];
4939
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()) : [];
4940
5494
  const blockKitEnabled = config["block_kit_enabled"] === true;
4941
5495
  const blockKitAskUserEnabled = config["block_kit_ask_user_enabled"] === true;
4942
5496
  const blockKitDisabled = process.env["SLACK_BLOCK_KIT_DISABLED"] === "true";
@@ -4946,7 +5500,9 @@ ${sections}`
4946
5500
  ...blockKitAskUserEnabled ? { SLACK_BLOCK_KIT_ASK_USER_ENABLED: "true" } : {},
4947
5501
  ...blockKitDisabled ? { SLACK_BLOCK_KIT_DISABLED: "true" } : {},
4948
5502
  ...blockKitAskUserEnabled ? { AGT_HOST: resolvedAgtHost } : {},
4949
- ...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}" } : {},
4950
5506
  ...blockKitAskUserEnabled && options?.agentId ? { AGT_AGENT_ID: options.agentId } : {}
4951
5507
  } : {};
4952
5508
  if (botToken) {
@@ -4975,16 +5531,26 @@ ${sections}`
4975
5531
  const slackResolvedAgtApiKey = process.env["AGT_API_KEY"]?.trim();
4976
5532
  const slackAgtAuthEnv = {
4977
5533
  AGT_HOST: resolvedAgtHost,
4978
- ...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}" } : {},
4979
5538
  ...options?.agentId ? { AGT_AGENT_ID: options.agentId } : {}
4980
5539
  };
5540
+ writeEnvIntegrationsForAgent(codeName, {
5541
+ mode: "upsert",
5542
+ updates: {
5543
+ SLACK_BOT_TOKEN: botToken,
5544
+ ...appToken ? { SLACK_APP_TOKEN: appToken } : {}
5545
+ }
5546
+ });
4981
5547
  const localSlackChannel = join4(getHomeDir3(), ".augmented", "_mcp", "slack-channel.js");
4982
5548
  const slackEntry = {
4983
5549
  command: existsSync5(localSlackChannel) ? "node" : "npx",
4984
5550
  args: existsSync5(localSlackChannel) ? [localSlackChannel] : ["-y", "@augmented/claude-code-channel-slack"],
4985
5551
  env: {
4986
- SLACK_BOT_TOKEN: botToken,
4987
- ...appToken ? { SLACK_APP_TOKEN: appToken } : {},
5552
+ SLACK_BOT_TOKEN: "${SLACK_BOT_TOKEN}",
5553
+ ...appToken ? { SLACK_APP_TOKEN: "${SLACK_APP_TOKEN}" } : {},
4988
5554
  ...threadAutoFollow && threadAutoFollow !== "off" ? { SLACK_THREAD_AUTO_FOLLOW: threadAutoFollow } : {},
4989
5555
  // ENG-4464: only emit when non-default — `mention_only` is the
4990
5556
  // default in slack-response-mode.ts, so omitting keeps the env
@@ -5007,8 +5573,39 @@ ${sections}`
5007
5573
  // 'all' is the absence of the env var — same convention the
5008
5574
  // MCP filter uses on its own end).
5009
5575
  ...senderPolicyMode ? { SLACK_SENDER_POLICY: senderPolicyMode } : {},
5010
- ...senderPolicyEnv
5576
+ ...senderPolicyEnv,
5011
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(",") } : {}
5012
5609
  }
5013
5610
  };
5014
5611
  const provisionMcpPath = join4(agentDir, "provision", ".mcp.json");
@@ -5064,6 +5661,8 @@ ${sections}`
5064
5661
  const slackAutoFollowEnv = slackThreadAutoFollow && slackThreadAutoFollow !== "off" ? { SLACK_THREAD_AUTO_FOLLOW: slackThreadAutoFollow } : {};
5065
5662
  const slackChannelResponseMode = config["channel_response_mode"];
5066
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(",") } : {};
5067
5666
  const oneshotBlockKitEnabled = config["block_kit_enabled"] === true;
5068
5667
  const oneshotBlockKitAskUserEnabled = config["block_kit_ask_user_enabled"] === true;
5069
5668
  const oneshotBlockKitDisabled = process.env["SLACK_BLOCK_KIT_DISABLED"] === "true";
@@ -5073,7 +5672,8 @@ ${sections}`
5073
5672
  ...oneshotBlockKitAskUserEnabled ? { SLACK_BLOCK_KIT_ASK_USER_ENABLED: "true" } : {},
5074
5673
  ...oneshotBlockKitDisabled ? { SLACK_BLOCK_KIT_DISABLED: "true" } : {},
5075
5674
  ...oneshotBlockKitAskUserEnabled ? { AGT_HOST: oneshotResolvedAgtHost } : {},
5076
- ...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}" } : {},
5077
5677
  ...oneshotBlockKitAskUserEnabled && options?.agentId ? { AGT_AGENT_ID: options.agentId } : {}
5078
5678
  } : {};
5079
5679
  const slackPeerEnv = {};
@@ -5106,18 +5706,27 @@ ${sections}`
5106
5706
  const slackAgtAuthEnv = {
5107
5707
  AGT_HOST: process.env["AGT_HOST"]?.trim() || "https://api.augmented.team",
5108
5708
  AGT_AGENT_CODE_NAME: codeName,
5109
- ...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}" } : {},
5110
5711
  ...options?.agentId ? { AGT_AGENT_ID: options.agentId } : {}
5111
5712
  };
5713
+ writeEnvIntegrationsForAgent(codeName, {
5714
+ mode: "upsert",
5715
+ updates: {
5716
+ SLACK_BOT_TOKEN: botToken,
5717
+ ...appToken ? { SLACK_APP_TOKEN: appToken } : {}
5718
+ }
5719
+ });
5112
5720
  if (isPersistent && existsSync5(localSlackChannel)) {
5113
5721
  mcpServers["slack"] = {
5114
5722
  command: "node",
5115
5723
  args: [localSlackChannel],
5116
5724
  env: {
5117
- SLACK_BOT_TOKEN: botToken,
5118
- ...appToken ? { SLACK_APP_TOKEN: appToken } : {},
5725
+ SLACK_BOT_TOKEN: "${SLACK_BOT_TOKEN}",
5726
+ ...appToken ? { SLACK_APP_TOKEN: "${SLACK_APP_TOKEN}" } : {},
5119
5727
  ...slackAutoFollowEnv,
5120
5728
  ...slackResponseModeEnv,
5729
+ ...slackAllowedUsersEnv,
5121
5730
  ...oneshotBlockKitEnv,
5122
5731
  ...slackPeerEnv,
5123
5732
  ...slackAgtAuthEnv,
@@ -5129,10 +5738,11 @@ ${sections}`
5129
5738
  command: "npx",
5130
5739
  args: ["-y", "@augmented/claude-code-channel-slack"],
5131
5740
  env: {
5132
- SLACK_BOT_TOKEN: botToken,
5133
- ...appToken ? { SLACK_APP_TOKEN: appToken } : {},
5741
+ SLACK_BOT_TOKEN: "${SLACK_BOT_TOKEN}",
5742
+ ...appToken ? { SLACK_APP_TOKEN: "${SLACK_APP_TOKEN}" } : {},
5134
5743
  ...slackAutoFollowEnv,
5135
5744
  ...slackResponseModeEnv,
5745
+ ...slackAllowedUsersEnv,
5136
5746
  ...oneshotBlockKitEnv,
5137
5747
  ...slackPeerEnv,
5138
5748
  ...slackAgtAuthEnv,
@@ -5167,12 +5777,17 @@ ${sections}`
5167
5777
  const msteamsAgtAuthEnv = {
5168
5778
  AGT_HOST: process.env["AGT_HOST"]?.trim() || "https://api.augmented.team",
5169
5779
  AGT_AGENT_CODE_NAME: codeName,
5170
- ...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}" } : {},
5171
5782
  ...options?.agentId ? { AGT_AGENT_ID: options.agentId } : {}
5172
5783
  };
5784
+ writeEnvIntegrationsForAgent(codeName, {
5785
+ mode: "upsert",
5786
+ updates: { MSTEAMS_CLIENT_SECRET: clientSecret }
5787
+ });
5173
5788
  const teamsEnv = {
5174
5789
  MSTEAMS_APP_ID: appId,
5175
- MSTEAMS_CLIENT_SECRET: clientSecret,
5790
+ MSTEAMS_CLIENT_SECRET: "${MSTEAMS_CLIENT_SECRET}",
5176
5791
  MSTEAMS_TENANT_ID: tenantId,
5177
5792
  ...botObjectId ? { MSTEAMS_BOT_OBJECT_ID: botObjectId } : {},
5178
5793
  ...allowedTeamIds.length > 0 ? { MSTEAMS_ALLOWED_TEAMS: allowedTeamIds.join(",") } : {},
@@ -5188,8 +5803,24 @@ ${sections}`
5188
5803
  // ENG-5841: MSTEAMS_SENDER_POLICY drives teams-inbound-filter.ts.
5189
5804
  // Mirrors the Slack branch above — only emitted when restrictive.
5190
5805
  ...senderPolicyMode ? { MSTEAMS_SENDER_POLICY: senderPolicyMode } : {},
5191
- ...senderPolicyEnv
5806
+ ...senderPolicyEnv,
5192
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 } : {}
5193
5824
  };
5194
5825
  if (isPersistent && existsSync5(localTeamsChannel)) {
5195
5826
  mcpServers["msteams"] = {
@@ -5243,6 +5874,12 @@ ${sections}`
5243
5874
  });
5244
5875
  return changed;
5245
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
+ },
5246
5883
  seedProfileConfig(codeName) {
5247
5884
  const agentDir = getAgentDir(codeName);
5248
5885
  const projectDir = getProjectDir(codeName);
@@ -5274,19 +5911,19 @@ ${sections}`
5274
5911
  ...integration,
5275
5912
  credentials: decryptIntegrationCredentials(integration.credentials)
5276
5913
  }));
5277
- const envLines = ["# Augmented integrations \u2014 auto-generated, do not edit"];
5914
+ const envUpdates = {};
5278
5915
  for (const integration of decryptedIntegrations) {
5279
5916
  const prefix = integration.definition_id.toUpperCase().replace(/[^A-Z0-9]/g, "_");
5280
5917
  const creds = integration.credentials;
5281
5918
  if (integration.auth_type === "oauth2") {
5282
5919
  const accessToken = creds.access_token;
5283
5920
  if (accessToken) {
5284
- envLines.push(`${prefix}_ACCESS_TOKEN=${shellQuote(accessToken)}`);
5921
+ envUpdates[`${prefix}_ACCESS_TOKEN`] = accessToken;
5285
5922
  }
5286
5923
  } else if (integration.auth_type === "api_key") {
5287
5924
  const apiKey = creds.api_key;
5288
5925
  if (apiKey) {
5289
- envLines.push(`${prefix}_API_KEY=${shellQuote(apiKey)}`);
5926
+ envUpdates[`${prefix}_API_KEY`] = apiKey;
5290
5927
  }
5291
5928
  }
5292
5929
  if (integration.config) {
@@ -5295,16 +5932,26 @@ ${sections}`
5295
5932
  if (typeof value === "string" && value) {
5296
5933
  const upperKey = key.toUpperCase();
5297
5934
  const envKey = upperKey.startsWith(`${prefix}_`) ? upperKey : `${prefix}_${upperKey}`;
5298
- envLines.push(`${envKey}=${shellQuote(value)}`);
5935
+ envUpdates[envKey] = value;
5299
5936
  }
5300
5937
  }
5301
5938
  }
5302
5939
  }
5303
- if (envLines.length > 1) {
5304
- const envPath = join4(agentDir, ".env.integrations");
5305
- writeFileSync5(envPath, envLines.join("\n") + "\n");
5306
- 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
+ }
5307
5950
  }
5951
+ writeEnvIntegrationsForAgent(codeName, {
5952
+ mode: "replace-preserving",
5953
+ updates: envUpdates
5954
+ });
5308
5955
  writeXurlStoreForIntegrations(decryptedIntegrations);
5309
5956
  for (const integration of decryptedIntegrations) {
5310
5957
  const def = INTEGRATION_REGISTRY.find((d) => d.id === integration.definition_id);
@@ -5425,12 +6072,18 @@ ${sections}`
5425
6072
  const envSrc = join4(agentDir2, ".env.integrations");
5426
6073
  try {
5427
6074
  const envContent = readFileSync5(envSrc, "utf-8");
5428
- 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
+ }
5429
6081
  } catch {
5430
6082
  }
5431
6083
  } catch {
5432
6084
  }
5433
6085
  renderChannelMessageHandlerForAgent(codeName);
6086
+ renderAugmentedWorkerForAgent(codeName);
5434
6087
  },
5435
6088
  writeMcpServer(codeName, serverId, config) {
5436
6089
  const agentDir = getAgentDir(codeName);
@@ -5448,7 +6101,27 @@ ${sections}`
5448
6101
  const mcpServers = mcpConfig["mcpServers"];
5449
6102
  let serverEntry;
5450
6103
  if ("url" in config) {
5451
- 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
+ }
5452
6125
  } else {
5453
6126
  serverEntry = { command: config.command };
5454
6127
  if (config.args?.length)
@@ -5501,14 +6174,14 @@ ${sections}`
5501
6174
  mkdirSync4(join4(filePath, ".."), { recursive: true });
5502
6175
  if (isPluginManaged && existsSync5(filePath)) {
5503
6176
  try {
5504
- chmodSync4(filePath, READ_WRITE_MODE);
6177
+ chmodSync5(filePath, READ_WRITE_MODE);
5505
6178
  } catch {
5506
6179
  }
5507
6180
  }
5508
6181
  writeFileSync5(filePath, file.content);
5509
6182
  if (isPluginManaged) {
5510
6183
  try {
5511
- chmodSync4(filePath, READ_ONLY_MODE);
6184
+ chmodSync5(filePath, READ_ONLY_MODE);
5512
6185
  } catch {
5513
6186
  }
5514
6187
  }
@@ -5689,7 +6362,7 @@ ${sections}`
5689
6362
  return;
5690
6363
  const tokenPath = join4(agentDir, ".tokens.json");
5691
6364
  writeFileSync5(tokenPath, JSON.stringify(tokens, null, 2));
5692
- chmodSync4(tokenPath, SECRET_FILE_MODE);
6365
+ chmodSync5(tokenPath, SECRET_FILE_MODE);
5693
6366
  }
5694
6367
  };
5695
6368
  registerFramework(claudeCodeAdapter);
@@ -6267,7 +6940,7 @@ import { homedir as homedir6, userInfo } from "os";
6267
6940
  import { spawn as spawn3 } from "child_process";
6268
6941
 
6269
6942
  // src/lib/watchdog.ts
6270
- 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";
6271
6944
  import { join as join7 } from "path";
6272
6945
  import { spawn as spawn2, execFileSync } from "child_process";
6273
6946
  var DEFAULT_CONFIG_DIR = join7(process.env["HOME"] ?? "/tmp", ".augmented");
@@ -6361,7 +7034,7 @@ function startWatchdog(opts) {
6361
7034
  const { logFile } = getManagerPaths(configDir);
6362
7035
  const logFd = openSync(logFile, "a", 384);
6363
7036
  try {
6364
- chmodSync5(logFile, 384);
7037
+ chmodSync6(logFile, 384);
6365
7038
  } catch {
6366
7039
  }
6367
7040
  const intervalSec = String(Math.max(Math.floor(opts.intervalMs / 1e3), 5));
@@ -6874,9 +7547,12 @@ async function managerUninstallSystemUnitCommand() {
6874
7547
  export {
6875
7548
  getIntegration,
6876
7549
  extractCommandNotFound,
7550
+ LITERAL_SECRET_PATTERNS,
7551
+ safeWriteJsonAtomic,
6877
7552
  INTEGRATIONS_SECTION_START,
6878
7553
  INTEGRATIONS_SECTION_END,
6879
7554
  estimateActiveTasksTokens,
7555
+ CHANNEL_SECRET_ENV_KEYS,
6880
7556
  provisionStopHook,
6881
7557
  provisionIsolationHook,
6882
7558
  provisionOrientHook,
@@ -6910,4 +7586,4 @@ export {
6910
7586
  managerInstallSystemUnitCommand,
6911
7587
  managerUninstallSystemUnitCommand
6912
7588
  };
6913
- //# sourceMappingURL=chunk-NFBRJGNU.js.map
7589
+ //# sourceMappingURL=chunk-N2EBL4PZ.js.map