@ingram-cloud/pulumi 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -83,6 +83,7 @@ const curator = new ic.IcAgent("curator", {
83
83
  autoMemory: AGENT_SPECS.curator.auto_memory,
84
84
  // memoryConsolidation: true, // opt into background consolidation (billed)
85
85
  // variables: [...], enabledHostedTools: [...], rolloutPercent: 100,
86
+ // mcpServers: ["sheets"], // scope runs to these registered MCP servers (omit = all)
86
87
  });
87
88
 
88
89
  export const curatorAgentId = curator.agentId;
@@ -251,3 +252,9 @@ usually unnecessary — a plain `pulumi up` will reconcile the live agent.)
251
252
  `pulumi refresh` prunes a resource deleted out-of-band — e.g. after an IC DB
252
253
  reset, `pulumi refresh && pulumi up` recreates it, no `pulumi state delete`.
253
254
  Non-404 errors still throw, so a transient API blip can't drop it from state.
255
+ - `IcMcpServer` `read` also round-trips the live `url` / `authKind` /
256
+ `authProvider` (raw registrations only — catalog presets supply these
257
+ server-side), so `pulumi refresh && pulumi up` detects and repairs server-side
258
+ drift such as a lost auth block. Write-only fields (`secret`) can't round-trip
259
+ and stay diff-by-state. Dynamic providers run the code serialized into state,
260
+ so drift detection starts after the first `pulumi up` with this version.
@@ -85,6 +85,14 @@ export interface IcAgentArgs {
85
85
  instructions?: pulumi.Input<string>;
86
86
  model?: pulumi.Input<string>;
87
87
  enabledHostedTools?: pulumi.Input<string[]>;
88
+ /** Scope runs to these registered MCP servers (by name). Omitted = all. */
89
+ mcpServers?: pulumi.Input<string[]>;
90
+ /**
91
+ * Vector store ids (`vs_…`) whose contents this agent can retrieve over. Folds
92
+ * into the content signature like `enabledHostedTools`, so a change publishes and
93
+ * rolls out a new version. Pair with {@link IcVectorStore} to IaC the stores.
94
+ */
95
+ vectorStoreIds?: pulumi.Input<string[]>;
88
96
  autoMemory?: pulumi.Input<boolean>;
89
97
  /** Opt into observational-memory consolidation (background, billed). Default off. */
90
98
  memoryConsolidation?: pulumi.Input<boolean>;
@@ -109,6 +117,35 @@ export declare class IcAgent extends pulumi.dynamic.Resource {
109
117
  readonly activeVersion: pulumi.Output<number>;
110
118
  constructor(name: string, args: IcAgentArgs, opts?: pulumi.CustomResourceOptions);
111
119
  }
120
+ /** One source file on an {@link IcVectorStore}, provided by local path. */
121
+ export interface VectorStoreFile {
122
+ /** Absolute path to the file to ingest (read at `pulumi up`, like `htmlPath`). */
123
+ path: string;
124
+ /** Per-file chunking override (the `chunking_strategy` wire shape); defaults to
125
+ * the server default (`auto`). */
126
+ chunkingStrategy?: Record<string, unknown>;
127
+ }
128
+ export interface IcVectorStoreArgs {
129
+ baseUrl: pulumi.Input<string>;
130
+ token: pulumi.Input<string>;
131
+ /** Display name. */
132
+ name?: pulumi.Input<string>;
133
+ description?: pulumi.Input<string>;
134
+ /** Arbitrary key/value metadata stored on the vector store. */
135
+ metadata?: pulumi.Input<Record<string, unknown>>;
136
+ /**
137
+ * Source files to ingest, each declared by an absolute local `path` (read at
138
+ * `pulumi up`, like a UI template's `htmlPath`). A file's sha256 tracks drift:
139
+ * edit it and it re-uploads + re-attaches, remove it and it detaches. Attaching
140
+ * starts async ingest server-side; this resource does not block on indexing.
141
+ */
142
+ files?: pulumi.Input<VectorStoreFile[]>;
143
+ }
144
+ export declare class IcVectorStore extends pulumi.dynamic.Resource {
145
+ /** The `vs_…` id (same as `.id`, exposed for convenience). */
146
+ readonly vectorStoreId: pulumi.Output<string>;
147
+ constructor(name: string, args: IcVectorStoreArgs, opts?: pulumi.CustomResourceOptions);
148
+ }
112
149
  export interface IcMcpServerArgs {
113
150
  baseUrl: pulumi.Input<string>;
114
151
  token: pulumi.Input<string>;
package/dist/resources.js CHANGED
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.IcWebPage = exports.IcProjectToken = exports.IcProject = exports.IcModelKey = exports.IcWebhook = exports.IcEmail = exports.IcWhatsAppConfig = exports.IcOauthProvider = exports.IcTelegramBot = exports.IcMcpServer = exports.IcAgent = exports.IC_API_VERSION = void 0;
36
+ exports.IcWebPage = exports.IcProjectToken = exports.IcProject = exports.IcModelKey = exports.IcWebhook = exports.IcEmail = exports.IcWhatsAppConfig = exports.IcOauthProvider = exports.IcTelegramBot = exports.IcMcpServer = exports.IcVectorStore = exports.IcAgent = exports.IC_API_VERSION = void 0;
37
37
  /**
38
38
  * Ingram Cloud /v1 tenant configuration as Pulumi dynamic resources.
39
39
  *
@@ -143,6 +143,11 @@ function makePutResource(cfg) {
143
143
  async diff(_id, olds, news) {
144
144
  return simpleDiff(olds, news, cfg.diff);
145
145
  },
146
+ async read(id, props) {
147
+ if (!cfg.readProps || !props?.baseUrl || !props?.token)
148
+ return { id, props };
149
+ return read404(() => icRequest(props.baseUrl, props.token, "GET", cfg.path(props)), (res) => ({ id, props: { ...props, ...cfg.readProps(props, res) } }));
150
+ },
146
151
  };
147
152
  }
148
153
  // Read each template's HTML bundle (from `htmlPath` or inline `html`), hash the
@@ -238,6 +243,8 @@ function agentBody(i) {
238
243
  instructions: i.instructions ?? null,
239
244
  model: i.model ?? null,
240
245
  enabled_hosted_tools: i.enabledHostedTools ?? [],
246
+ mcp_servers: i.mcpServers ?? null,
247
+ vector_store_ids: i.vectorStoreIds ?? [],
241
248
  auto_memory: i.autoMemory ?? null,
242
249
  memory_consolidation: i.memoryConsolidation ?? null,
243
250
  variables: i.variables ?? [],
@@ -276,6 +283,14 @@ function agentContentSig(s) {
276
283
  const ui = uiResourcesSig(s.ui_resources);
277
284
  if (ui.length)
278
285
  base.ui_resources = ui;
286
+ // Same discipline for mcp_servers: unset (= all) stays out of the signature.
287
+ if (s.mcp_servers != null)
288
+ base.mcp_servers = [...s.mcp_servers].sort();
289
+ // And for vector stores: omit when empty so agents without any keep a
290
+ // byte-identical signature to pre-vectorStoreIds releases.
291
+ const vs = [...(s.vector_store_ids ?? [])].sort();
292
+ if (vs.length)
293
+ base.vector_store_ids = vs;
279
294
  return JSON.stringify(base);
280
295
  }
281
296
  // Map the camelCase resource inputs onto the canonical snapshot shape.
@@ -284,6 +299,8 @@ function inputsToSnapshot(i) {
284
299
  instructions: i.instructions,
285
300
  model: i.model,
286
301
  enabled_hosted_tools: i.enabledHostedTools,
302
+ mcp_servers: i.mcpServers,
303
+ vector_store_ids: i.vectorStoreIds,
287
304
  auto_memory: i.autoMemory,
288
305
  memory_consolidation: i.memoryConsolidation,
289
306
  variables: i.variables,
@@ -430,6 +447,7 @@ const agentProvider = {
430
447
  instructions: d.instructions ?? undefined,
431
448
  model: d.model ?? undefined,
432
449
  enabledHostedTools: d.enabled_hosted_tools ?? [],
450
+ vectorStoreIds: d.vector_store_ids ?? [],
433
451
  autoMemory: d.auto_memory ?? undefined,
434
452
  memoryConsolidation: d.memory_consolidation ?? undefined,
435
453
  variables: d.variables ?? [],
@@ -444,6 +462,8 @@ class IcAgent extends pulumi.dynamic.Resource {
444
462
  instructions: undefined,
445
463
  model: undefined,
446
464
  enabledHostedTools: undefined,
465
+ mcpServers: undefined,
466
+ vectorStoreIds: undefined,
447
467
  autoMemory: undefined,
448
468
  memoryConsolidation: undefined,
449
469
  variables: undefined,
@@ -461,6 +481,168 @@ class IcAgent extends pulumi.dynamic.Resource {
461
481
  }
462
482
  }
463
483
  exports.IcAgent = IcAgent;
484
+ // Read each declared file's bytes and hash them (same sha256-hex the API keys
485
+ // files by) so an edit shows up in the file signature. Module-scope + node
486
+ // built-ins required inside for Pulumi's closure serialization.
487
+ function resolveVsFiles(files) {
488
+ if (!files?.length)
489
+ return [];
490
+ const { createHash } = require("node:crypto");
491
+ return files.map((f) => {
492
+ if (!f.path)
493
+ throw new Error("vectorStore file: `path` is required");
494
+ const bytes = require("node:fs").readFileSync(f.path);
495
+ return {
496
+ path: f.path,
497
+ bytes,
498
+ contentHash: createHash("sha256").update(bytes).digest("hex"),
499
+ chunkingStrategy: f.chunkingStrategy,
500
+ };
501
+ });
502
+ }
503
+ // Multipart upload of one file to POST /v1/files (purpose `assistants`, the
504
+ // vector-store source purpose) → the new `file_…` id. Not `icRequest` (that
505
+ // JSON-encodes the body); uses global FormData/File (Node ≥18).
506
+ async function uploadFile(baseUrl, token, bytes, filename) {
507
+ const form = new FormData();
508
+ form.set("file", new File([new Uint8Array(bytes)], filename, { type: "application/octet-stream" }));
509
+ form.set("purpose", "assistants");
510
+ const res = await fetch(`${baseUrl}/v1/files`, {
511
+ method: "POST",
512
+ headers: { Authorization: `Bearer ${token}`, "IC-Api-Version": exports.IC_API_VERSION },
513
+ body: form,
514
+ });
515
+ if (!res.ok) {
516
+ const err = new Error(`IC POST /v1/files → ${res.status}: ${(await res.text()).slice(0, 400)}`);
517
+ err.status = res.status;
518
+ throw err;
519
+ }
520
+ return (await res.json()).id;
521
+ }
522
+ const vsBasename = (p) => p.split(/[\\/]/).pop() || p;
523
+ // The store's mutable fields (POST /v1/vector_stores on create, POST
524
+ // /v1/vector_stores/{id} to modify). Only send keys the caller set.
525
+ function vsBody(i) {
526
+ const body = {};
527
+ if (i.name !== undefined)
528
+ body.name = i.name;
529
+ if (i.description !== undefined)
530
+ body.description = i.description;
531
+ if (i.metadata !== undefined)
532
+ body.metadata = i.metadata;
533
+ return body;
534
+ }
535
+ // Upload one file's bytes and attach the resulting id to the store.
536
+ async function attachVsFile(i, id, f) {
537
+ const fileId = await uploadFile(i.baseUrl, i.token, f.bytes, vsBasename(f.path));
538
+ await icRequest(i.baseUrl, i.token, "POST", `/v1/vector_stores/${id}/files`, {
539
+ file_id: fileId,
540
+ ...(f.chunkingStrategy ? { chunking_strategy: f.chunkingStrategy } : {}),
541
+ });
542
+ return { path: f.path, contentHash: f.contentHash, fileId };
543
+ }
544
+ // Reconcile attachments against the declared set: (re)upload+attach any new-or-
545
+ // changed file, detach any whose path was dropped or whose bytes changed. Returns
546
+ // the new file-state list to store as an output. The store-file id IS the
547
+ // `file_…` id, so detach targets `/files/{fileId}`.
548
+ async function syncVsFiles(i, id, prior) {
549
+ const desired = resolveVsFiles(i.files);
550
+ const byPath = new Map(prior.map((p) => [p.path, p]));
551
+ const next = [];
552
+ for (const f of desired) {
553
+ const cur = byPath.get(f.path);
554
+ if (cur && cur.contentHash === f.contentHash) {
555
+ next.push(cur); // unchanged bytes — keep the existing attachment
556
+ continue;
557
+ }
558
+ if (cur)
559
+ // bytes changed → detach the stale file before re-uploading
560
+ await icRequest(i.baseUrl, i.token, "DELETE", `/v1/vector_stores/${id}/files/${cur.fileId}`);
561
+ next.push(await attachVsFile(i, id, f));
562
+ }
563
+ const keep = new Set(desired.map((f) => f.path));
564
+ for (const p of prior)
565
+ if (!keep.has(p.path))
566
+ await icRequest(i.baseUrl, i.token, "DELETE", `/v1/vector_stores/${id}/files/${p.fileId}`);
567
+ return next;
568
+ }
569
+ // Canonical, path-sorted signature of the declared files (path + content hash +
570
+ // chunking), so a file edit/add/remove is exactly one diff. Empty ⇒ "[]".
571
+ function vsFilesSig(files) {
572
+ return JSON.stringify(resolveVsFiles(files)
573
+ .map((f) => ({
574
+ path: f.path,
575
+ content_hash: f.contentHash,
576
+ chunking: f.chunkingStrategy ?? null,
577
+ }))
578
+ .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)));
579
+ }
580
+ const vectorStoreProvider = {
581
+ async create(i) {
582
+ const res = await icRequest(i.baseUrl, i.token, "POST", "/v1/vector_stores", vsBody(i));
583
+ const fileState = await syncVsFiles(i, res.id, []);
584
+ return {
585
+ id: res.id,
586
+ outs: { ...i, vectorStoreId: res.id, fileState, filesSig: vsFilesSig(i.files) },
587
+ };
588
+ },
589
+ async update(id, olds, news) {
590
+ const body = vsBody(news);
591
+ if (Object.keys(body).length)
592
+ await icRequest(news.baseUrl, news.token, "POST", `/v1/vector_stores/${id}`, body);
593
+ const fileState = await syncVsFiles(news, id, olds.fileState ?? []);
594
+ return {
595
+ outs: { ...news, vectorStoreId: id, fileState, filesSig: vsFilesSig(news.files) },
596
+ };
597
+ },
598
+ async delete(id, props) {
599
+ await icRequest(props.baseUrl, props.token, "DELETE", `/v1/vector_stores/${id}`);
600
+ },
601
+ async diff(_id, olds, news) {
602
+ const scalarChanged = (olds.name ?? undefined) !== (news.name ?? undefined) ||
603
+ (olds.description ?? undefined) !== (news.description ?? undefined);
604
+ const metaChanged = JSON.stringify(olds.metadata ?? null) !== JSON.stringify(news.metadata ?? null);
605
+ // Compare against the sig STORED at the last apply (not a re-resolve of olds):
606
+ // a file-backed path edits the same bytes, so re-reading olds would hash the
607
+ // new bytes and hide the change (same reasoning as the agent diff).
608
+ const oldSig = olds.filesSig ?? vsFilesSig(olds.files);
609
+ const filesChanged = oldSig !== vsFilesSig(news.files);
610
+ return { changes: scalarChanged || metaChanged || filesChanged };
611
+ },
612
+ // Adopt a store a script already created (`pulumi import <id>`). Local file
613
+ // attachments can't be reconstructed from the server (no local paths), so this
614
+ // recovers only the store's own fields; declare `files` to (re)assert them.
615
+ async read(id, props) {
616
+ return read404(() => icRequest(props.baseUrl, props.token, "GET", `/v1/vector_stores/${id}`), (vs) => {
617
+ if (!vs || !vs.id)
618
+ return { id: "" };
619
+ return {
620
+ id,
621
+ props: {
622
+ ...props,
623
+ name: vs.name ?? undefined,
624
+ description: vs.description ?? undefined,
625
+ metadata: vs.metadata ?? undefined,
626
+ },
627
+ };
628
+ });
629
+ },
630
+ };
631
+ class IcVectorStore extends pulumi.dynamic.Resource {
632
+ constructor(name, args, opts) {
633
+ super(vectorStoreProvider, name, {
634
+ name: undefined,
635
+ description: undefined,
636
+ metadata: undefined,
637
+ files: undefined,
638
+ vectorStoreId: undefined,
639
+ fileState: undefined,
640
+ filesSig: undefined,
641
+ ...args,
642
+ }, { ...opts, additionalSecretOutputs: ["token"] });
643
+ }
644
+ }
645
+ exports.IcVectorStore = IcVectorStore;
464
646
  function mcpBody(i) {
465
647
  const auth = { kind: i.authKind };
466
648
  if (i.authProvider)
@@ -488,6 +670,19 @@ const mcpProvider = makePutResource({
488
670
  toolsDiscovered: res.tools_discovered ?? 0,
489
671
  discoveryError: res.discovery_error ?? null,
490
672
  }),
673
+ // `pulumi refresh` round-trips the security-relevant fields, so a live auth
674
+ // downgrade (e.g. oauth silently reverting to none) shows up as a diff and the
675
+ // next `up` re-pushes the declared config. Catalog presets supply url/auth
676
+ // server-side — omitted inputs are correct there, so nothing is overlaid.
677
+ readProps: (i, res) => {
678
+ if (i.catalog)
679
+ return {};
680
+ return {
681
+ url: res.url ?? i.url,
682
+ authKind: res.auth?.kind ?? i.authKind,
683
+ authProvider: res.auth?.provider ?? undefined,
684
+ };
685
+ },
491
686
  diff: {
492
687
  replaceKeys: ["serverName"],
493
688
  scalarKeys: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingram-cloud/pulumi",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Pulumi dynamic resources for the Ingram Cloud /v1 API — projects, project tokens, agents, MCP servers, channels, webhooks, model keys.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://cloud.ingram.tech",