@ingram-cloud/pulumi 1.0.0 → 1.1.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 +29 -0
- package/dist/resources.d.ts +40 -0
- package/dist/resources.js +119 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -88,6 +88,35 @@ const curator = new ic.IcAgent("curator", {
|
|
|
88
88
|
export const curatorAgentId = curator.agentId;
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
**MCP Apps UI templates.** Pass `uiTemplates` to attach interactive HTML bundles
|
|
92
|
+
(MCP Apps, SEP-1865). Each template's bytes upload when they (or the metadata)
|
|
93
|
+
change, and its name + content hash + `tool`/`csp`/`permissions` fold into the
|
|
94
|
+
content signature — so a template edit publishes and rolls out a new version like
|
|
95
|
+
an instruction change. Provide the HTML as an absolute `htmlPath` or inline
|
|
96
|
+
`html`; a `tool` binds it as a typed app-tool. Dropping a template from the array
|
|
97
|
+
deletes it from the draft.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { join } from "node:path";
|
|
101
|
+
|
|
102
|
+
const bookkeeper = new ic.IcAgent("bookkeeper", {
|
|
103
|
+
...conn,
|
|
104
|
+
instructions: BOOKKEEPER_INSTRUCTIONS,
|
|
105
|
+
uiTemplates: [
|
|
106
|
+
{
|
|
107
|
+
name: "cash_chart",
|
|
108
|
+
htmlPath: join(__dirname, "ui/cash-chart.html"),
|
|
109
|
+
tool: {
|
|
110
|
+
description: "Show an interactive cash-flow chart.",
|
|
111
|
+
input_schema: { type: "object", properties: {}, required: [] },
|
|
112
|
+
instruction: "Call get_cash_flow_history, then render_app with template cash_chart.",
|
|
113
|
+
mutating: false,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
91
120
|
> Attaching **existing** smiths to a agent is a one-time fleet backfill —
|
|
92
121
|
> keep it as the app's migration script. New smiths attach at birth in app code.
|
|
93
122
|
> Pulumi does not own per-smith runtime state.
|
package/dist/resources.d.ts
CHANGED
|
@@ -37,6 +37,38 @@ export interface AgentVariable {
|
|
|
37
37
|
description?: string | null;
|
|
38
38
|
required?: boolean;
|
|
39
39
|
}
|
|
40
|
+
/** CSP domain allowlists for the sandboxed iframe (the `_meta.ui.csp` shape). */
|
|
41
|
+
export interface UiCsp {
|
|
42
|
+
connectDomains?: string[];
|
|
43
|
+
resourceDomains?: string[];
|
|
44
|
+
frameDomains?: string[];
|
|
45
|
+
baseUriDomains?: string[];
|
|
46
|
+
}
|
|
47
|
+
/** A typed app-tool bound to a template (Rung 2): the host's model calls it and
|
|
48
|
+
* the template renders the result. */
|
|
49
|
+
export interface UiResourceTool {
|
|
50
|
+
description: string;
|
|
51
|
+
/** JSON Schema for the tool's arguments. */
|
|
52
|
+
input_schema?: Record<string, unknown>;
|
|
53
|
+
/** Scoped instruction the smith runs with when this tool is called. */
|
|
54
|
+
instruction?: string | null;
|
|
55
|
+
/** Writes gate through approval; reads flow freely. */
|
|
56
|
+
mutating?: boolean;
|
|
57
|
+
}
|
|
58
|
+
/** One UI template declared on an `IcAgent`. Provide the HTML via `htmlPath`
|
|
59
|
+
* (read at `pulumi up`, so use an absolute path) or inline `html`. */
|
|
60
|
+
export interface UiTemplate {
|
|
61
|
+
/** Template name (`[a-z0-9][a-z0-9_-]*`, ≤63) — the `ui://…/{name}` handle. */
|
|
62
|
+
name: string;
|
|
63
|
+
/** Absolute path to the HTML bundle. One of `htmlPath` / `html` is required. */
|
|
64
|
+
htmlPath?: string;
|
|
65
|
+
/** Inline HTML, as an alternative to `htmlPath`. */
|
|
66
|
+
html?: string;
|
|
67
|
+
csp?: UiCsp;
|
|
68
|
+
/** Permissions-Policy-style host permissions the template requests. */
|
|
69
|
+
permissions?: Record<string, unknown>;
|
|
70
|
+
tool?: UiResourceTool;
|
|
71
|
+
}
|
|
40
72
|
export interface IcAgentArgs {
|
|
41
73
|
baseUrl: pulumi.Input<string>;
|
|
42
74
|
token: pulumi.Input<string>;
|
|
@@ -57,6 +89,14 @@ export interface IcAgentArgs {
|
|
|
57
89
|
/** Opt into observational-memory consolidation (background, billed). Default off. */
|
|
58
90
|
memoryConsolidation?: pulumi.Input<boolean>;
|
|
59
91
|
variables?: pulumi.Input<AgentVariable[]>;
|
|
92
|
+
/**
|
|
93
|
+
* MCP Apps UI templates attached to the agent. Each template's HTML bundle is
|
|
94
|
+
* uploaded when its bytes or metadata change, and its name + content hash +
|
|
95
|
+
* metadata fold into the content signature — so a template edit publishes and
|
|
96
|
+
* rolls out a new version like an instruction change. Provide the HTML via an
|
|
97
|
+
* absolute `htmlPath` or inline `html`.
|
|
98
|
+
*/
|
|
99
|
+
uiTemplates?: pulumi.Input<UiTemplate[]>;
|
|
60
100
|
/** Release note recorded when a new version is published. */
|
|
61
101
|
publishNote?: pulumi.Input<string>;
|
|
62
102
|
/** Rollout percentage for the published version (default 100). */
|
package/dist/resources.js
CHANGED
|
@@ -145,6 +145,80 @@ function makePutResource(cfg) {
|
|
|
145
145
|
},
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
|
+
// Read each template's HTML bundle (from `htmlPath` or inline `html`), hash the
|
|
149
|
+
// bytes with the same sha256-hex the API stores as `content_hash`, and carry the
|
|
150
|
+
// metadata through. Module-scope + node built-ins required inside so Pulumi's
|
|
151
|
+
// closure serialization captures it (the providers otherwise assume only global
|
|
152
|
+
// `fetch`).
|
|
153
|
+
function resolveUiTemplates(templates) {
|
|
154
|
+
if (!templates?.length)
|
|
155
|
+
return [];
|
|
156
|
+
const { createHash } = require("node:crypto");
|
|
157
|
+
return templates.map((t) => {
|
|
158
|
+
let bytes;
|
|
159
|
+
if (t.html !== undefined)
|
|
160
|
+
bytes = Buffer.from(t.html, "utf8");
|
|
161
|
+
else if (t.htmlPath)
|
|
162
|
+
bytes = require("node:fs").readFileSync(t.htmlPath);
|
|
163
|
+
else
|
|
164
|
+
throw new Error(`uiTemplate "${t.name}": set htmlPath or html`);
|
|
165
|
+
return {
|
|
166
|
+
name: t.name,
|
|
167
|
+
bytes,
|
|
168
|
+
contentHash: createHash("sha256").update(bytes).digest("hex"),
|
|
169
|
+
csp: t.csp,
|
|
170
|
+
permissions: t.permissions,
|
|
171
|
+
tool: t.tool,
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
// Multipart upload of one template to POST /v1/agents/{aid}/ui — the HTML `file`
|
|
176
|
+
// part plus a JSON `metadata` part. Not `icRequest` (that JSON-encodes the body);
|
|
177
|
+
// uses global FormData/File (Node ≥18).
|
|
178
|
+
async function uploadUiTemplate(baseUrl, token, aid, t) {
|
|
179
|
+
const form = new FormData();
|
|
180
|
+
form.set("file", new File([new Uint8Array(t.bytes)], `${t.name}.html`, { type: "text/html" }));
|
|
181
|
+
const meta = { name: t.name };
|
|
182
|
+
if (t.csp)
|
|
183
|
+
meta.csp = t.csp;
|
|
184
|
+
if (t.permissions)
|
|
185
|
+
meta.permissions = t.permissions;
|
|
186
|
+
if (t.tool)
|
|
187
|
+
meta.tool = t.tool;
|
|
188
|
+
form.set("metadata", JSON.stringify(meta));
|
|
189
|
+
const res = await fetch(`${baseUrl}/v1/agents/${aid}/ui`, {
|
|
190
|
+
method: "POST",
|
|
191
|
+
headers: { Authorization: `Bearer ${token}`, "IC-Api-Version": exports.IC_API_VERSION },
|
|
192
|
+
body: form,
|
|
193
|
+
});
|
|
194
|
+
if (!res.ok) {
|
|
195
|
+
const err = new Error(`IC POST /v1/agents/${aid}/ui → ${res.status}: ${(await res.text()).slice(0, 400)}`);
|
|
196
|
+
err.status = res.status;
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Reconcile the agent's UI templates against its draft `current` state: upload any
|
|
201
|
+
// whose bytes or metadata drifted (or are new), delete any the resource no longer
|
|
202
|
+
// declares. The draft this leaves behind is what the publish step snapshots.
|
|
203
|
+
async function syncUiTemplates(i, id, current) {
|
|
204
|
+
const desired = resolveUiTemplates(i.uiTemplates);
|
|
205
|
+
const eq = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
206
|
+
for (const t of desired) {
|
|
207
|
+
const cur = current.find((e) => e?.name === t.name);
|
|
208
|
+
const unchanged = cur &&
|
|
209
|
+
cur.content_hash === t.contentHash &&
|
|
210
|
+
eq(cur.tool, t.tool) &&
|
|
211
|
+
eq(cur.csp, t.csp) &&
|
|
212
|
+
eq(cur.permissions, t.permissions);
|
|
213
|
+
if (!unchanged)
|
|
214
|
+
await uploadUiTemplate(i.baseUrl, i.token, id, t);
|
|
215
|
+
}
|
|
216
|
+
for (const e of current) {
|
|
217
|
+
const name = e?.name;
|
|
218
|
+
if (typeof name === "string" && !desired.some((t) => t.name === name))
|
|
219
|
+
await icRequest(i.baseUrl, i.token, "DELETE", `/v1/agents/${id}/ui/${encodeURIComponent(name)}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
148
222
|
// Lower-case, hyphenate, trim — matches the API's slug derivation so a
|
|
149
223
|
// logical-name-derived slug lines up with what the server would compute.
|
|
150
224
|
function slugify(s) {
|
|
@@ -169,17 +243,40 @@ function agentBody(i) {
|
|
|
169
243
|
variables: i.variables ?? [],
|
|
170
244
|
};
|
|
171
245
|
}
|
|
246
|
+
// Canonical, name-sorted signature of an agent's frozen UI templates: each
|
|
247
|
+
// entry's name + content hash + tool/csp/permissions metadata. Both the resource
|
|
248
|
+
// inputs (hash computed from local bytes) and a published version's snapshot
|
|
249
|
+
// (hash already stored) map onto this shape, so a template edit moves the
|
|
250
|
+
// signature exactly one way.
|
|
251
|
+
function uiResourcesSig(list) {
|
|
252
|
+
return (list ?? [])
|
|
253
|
+
.map((e) => ({
|
|
254
|
+
name: e.name,
|
|
255
|
+
content_hash: e.content_hash,
|
|
256
|
+
tool: e.tool ?? null,
|
|
257
|
+
csp: e.csp ?? null,
|
|
258
|
+
permissions: e.permissions ?? null,
|
|
259
|
+
}))
|
|
260
|
+
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
261
|
+
}
|
|
172
262
|
// Stable signature of the *content* that warrants a new published version
|
|
173
263
|
// (name and rollout% deliberately excluded — a rename/restage is not new content).
|
|
174
264
|
function agentContentSig(s) {
|
|
175
|
-
|
|
265
|
+
const base = {
|
|
176
266
|
instructions: s.instructions ?? null,
|
|
177
267
|
model: s.model ?? null,
|
|
178
268
|
tools: [...(s.enabled_hosted_tools ?? [])].sort(),
|
|
179
269
|
auto_memory: s.auto_memory ?? null,
|
|
180
270
|
memory_consolidation: s.memory_consolidation ?? null,
|
|
181
271
|
variables: s.variables ?? [],
|
|
182
|
-
}
|
|
272
|
+
};
|
|
273
|
+
// Only add the key when templates exist, so agents without any keep a
|
|
274
|
+
// byte-identical signature to pre-uiTemplates releases (no spurious republish
|
|
275
|
+
// on the first `up` after upgrading the library).
|
|
276
|
+
const ui = uiResourcesSig(s.ui_resources);
|
|
277
|
+
if (ui.length)
|
|
278
|
+
base.ui_resources = ui;
|
|
279
|
+
return JSON.stringify(base);
|
|
183
280
|
}
|
|
184
281
|
// Map the camelCase resource inputs onto the canonical snapshot shape.
|
|
185
282
|
function inputsToSnapshot(i) {
|
|
@@ -190,6 +287,15 @@ function inputsToSnapshot(i) {
|
|
|
190
287
|
auto_memory: i.autoMemory,
|
|
191
288
|
memory_consolidation: i.memoryConsolidation,
|
|
192
289
|
variables: i.variables,
|
|
290
|
+
// Resolve each template's bytes and hash so a HTML edit shows up in the
|
|
291
|
+
// signature (the API's `content_hash` uses the same sha256-hex).
|
|
292
|
+
ui_resources: resolveUiTemplates(i.uiTemplates).map((t) => ({
|
|
293
|
+
name: t.name,
|
|
294
|
+
content_hash: t.contentHash,
|
|
295
|
+
tool: t.tool,
|
|
296
|
+
csp: t.csp,
|
|
297
|
+
permissions: t.permissions,
|
|
298
|
+
})),
|
|
193
299
|
};
|
|
194
300
|
}
|
|
195
301
|
async function findAgentBySlug(baseUrl, token, slug) {
|
|
@@ -234,6 +340,9 @@ async function reconcileAgent(i, knownId, priorSig) {
|
|
|
234
340
|
await icRequest(i.baseUrl, i.token, "PATCH", `/v1/agents/${id}`, agentBody(i));
|
|
235
341
|
}
|
|
236
342
|
const bp = await icRequest(i.baseUrl, i.token, "GET", `/v1/agents/${id}`);
|
|
343
|
+
// Reconcile UI templates against the draft before publishing so a template
|
|
344
|
+
// change rides the same publish + rollout as an instruction change.
|
|
345
|
+
await syncUiTemplates(i, id, (bp.draft?.ui_resources ?? []));
|
|
237
346
|
const desiredSig = agentContentSig(inputsToSnapshot(i));
|
|
238
347
|
// On the create path we don't know the prior sig — compare to what's actually
|
|
239
348
|
// published so adopting a script-made agent doesn't churn a needless version.
|
|
@@ -295,8 +404,13 @@ const agentProvider = {
|
|
|
295
404
|
// update (the old and new slugs differ, so create-before-delete is safe).
|
|
296
405
|
if (olds.slug !== news.slug)
|
|
297
406
|
return { changes: true, replaces: ["slug"] };
|
|
298
|
-
|
|
299
|
-
|
|
407
|
+
// Compare the freshly-resolved desired signature to the one STORED at the
|
|
408
|
+
// last apply — not a re-resolve of `olds`. A file-backed template edits the
|
|
409
|
+
// same `htmlPath`, so re-reading `olds` would hash the new bytes and hide
|
|
410
|
+
// the change; the stored `contentSig` froze the prior hash.
|
|
411
|
+
const oldSig = olds.contentSig ??
|
|
412
|
+
agentContentSig(inputsToSnapshot(olds));
|
|
413
|
+
const contentChanged = oldSig !== agentContentSig(inputsToSnapshot(news));
|
|
300
414
|
const nameChanged = (olds.name ?? olds.slug) !== (news.name ?? news.slug);
|
|
301
415
|
const rolloutChanged = (olds.rolloutPercent ?? 100) !== (news.rolloutPercent ?? 100);
|
|
302
416
|
return { changes: contentChanged || nameChanged || rolloutChanged };
|
|
@@ -333,6 +447,7 @@ class IcAgent extends pulumi.dynamic.Resource {
|
|
|
333
447
|
autoMemory: undefined,
|
|
334
448
|
memoryConsolidation: undefined,
|
|
335
449
|
variables: undefined,
|
|
450
|
+
uiTemplates: undefined,
|
|
336
451
|
publishNote: undefined,
|
|
337
452
|
rolloutPercent: undefined,
|
|
338
453
|
agentId: undefined,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ingram-cloud/pulumi",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.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",
|