@ingram-cloud/pulumi 2.1.1 → 3.0.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 +161 -166
- package/dist/resources.d.ts +18 -7
- package/dist/resources.js +53 -52
- package/package.json +20 -16
package/README.md
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
# @ingram-cloud/pulumi
|
|
2
2
|
|
|
3
3
|
Pulumi dynamic resources for the [Ingram Cloud](https://cloud.ingram.tech) `/v1`
|
|
4
|
-
API
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
is a `pulumi up`.
|
|
8
|
-
|
|
9
|
-
The library is published alongside the Ingram Cloud `/v1` API so the wrapper
|
|
10
|
-
tracks the surface it wraps. The API version it targets is pinned per library
|
|
11
|
-
version (`IC_API_VERSION`).
|
|
4
|
+
API: agents, MCP servers, integrations, webhooks, BYOK model keys and projects,
|
|
5
|
+
declared as Pulumi resources and applied with `pulumi up`. Each library version
|
|
6
|
+
pins the API version it targets (`IC_API_VERSION`).
|
|
12
7
|
|
|
13
8
|
## Install
|
|
14
9
|
|
|
@@ -16,39 +11,35 @@ version (`IC_API_VERSION`).
|
|
|
16
11
|
bun add @ingram-cloud/pulumi @pulumi/pulumi
|
|
17
12
|
```
|
|
18
13
|
|
|
19
|
-
##
|
|
14
|
+
## Where each resource belongs
|
|
15
|
+
|
|
16
|
+
This library carries no prompts or agent content; `instructions` is an input you
|
|
17
|
+
pass. A typical split:
|
|
20
18
|
|
|
21
19
|
```
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Webhook, ModelKey, Secret)
|
|
25
|
-
|
|
26
|
-
app repos (thornhill, …) declare their AGENTS — the `instructions` are
|
|
27
|
-
app-owned content (prompts), so they stay in the app
|
|
28
|
-
repo, sourced from the app's own spec module. The app's
|
|
29
|
-
Pulumi program imports this library + those specs.
|
|
30
|
-
|
|
31
|
-
infra repo (~/src/infra) declares tenant INTEGRATION config (MCP, Telegram, WhatsApp,
|
|
32
|
-
webhook, model keys) alongside DNS/DB/Vercel, and can
|
|
33
|
-
read an app stack's outputs via pulumi.StackReference to
|
|
34
|
-
push IC-derived values (agent ids, webhook secret)
|
|
35
|
-
into Vercel env. No prompts ever live here.
|
|
36
|
-
```
|
|
20
|
+
app stack IcAgent, IcWebPage, IcSkill — instructions come from the app's
|
|
21
|
+
own source.
|
|
37
22
|
|
|
38
|
-
|
|
39
|
-
|
|
23
|
+
infra stack IcMcpServer, IcOauthProvider, IcTelegramBot, IcWhatsAppNumber,
|
|
24
|
+
IcEmail, IcWebhook, IcModelKey — tenant integration config. It can
|
|
25
|
+
read the app stack's outputs (agent ids, webhook secret) through
|
|
26
|
+
pulumi.StackReference and write them to the app's env.
|
|
27
|
+
|
|
28
|
+
platform stack IcProject, IcProjectToken — provisions projects and mints their
|
|
29
|
+
tokens with an organization key.
|
|
30
|
+
```
|
|
40
31
|
|
|
41
32
|
## Connection
|
|
42
33
|
|
|
43
34
|
Every resource takes `baseUrl` + `token` (the tenant-admin bearer; `IcProject` /
|
|
44
|
-
`IcProjectToken` take an
|
|
45
|
-
|
|
35
|
+
`IcProjectToken` take an organization key instead, see below). Resolve them once
|
|
36
|
+
with `connectionFromConfig()` and spread:
|
|
46
37
|
|
|
47
38
|
```ts
|
|
48
39
|
import * as ic from "@ingram-cloud/pulumi";
|
|
49
40
|
|
|
50
41
|
const conn = ic.connectionFromConfig(); // ingram-cloud:token (secret) / :baseUrl
|
|
51
|
-
|
|
42
|
+
// or env INGRAM_CLOUD_TOKEN|CLOUD_API_KEY
|
|
52
43
|
```
|
|
53
44
|
|
|
54
45
|
Set the token as a stack secret:
|
|
@@ -59,132 +50,132 @@ pulumi config set --secret ingram-cloud:token tha_live_…
|
|
|
59
50
|
|
|
60
51
|
## Resources
|
|
61
52
|
|
|
62
|
-
### IcAgent (
|
|
53
|
+
### IcAgent (app stack)
|
|
63
54
|
|
|
64
|
-
|
|
65
|
-
content changed
|
|
66
|
-
|
|
67
|
-
(matched by slug) rather than recreating it.
|
|
55
|
+
Creates or adopts an agent by `slug`, publishes a new immutable version only when
|
|
56
|
+
the content changed, then rolls it out. The first `pulumi up` adopts an existing
|
|
57
|
+
agent with a matching slug rather than recreating it.
|
|
68
58
|
|
|
69
|
-
`slug` is the
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
replaces it.
|
|
59
|
+
`slug` is the reconcile key and defaults to the Pulumi resource name. `name` is a
|
|
60
|
+
display label you can change without churning the agent. Changing `slug` replaces
|
|
61
|
+
the agent.
|
|
73
62
|
|
|
74
63
|
```ts
|
|
75
64
|
import { AGENT_SPECS } from "../src/lib/cloud/agent-spec"; // app-owned
|
|
76
65
|
|
|
77
66
|
const curator = new ic.IcAgent("curator", {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
67
|
+
...conn,
|
|
68
|
+
// slug defaults to "curator"; pass `slug` to decouple the key from the name.
|
|
69
|
+
name: AGENT_SPECS.curator.name, // display label, freely renamable
|
|
70
|
+
instructions: AGENT_SPECS.curator.instructions,
|
|
71
|
+
model: AGENT_SPECS.curator.model,
|
|
72
|
+
autoMemory: AGENT_SPECS.curator.auto_memory,
|
|
73
|
+
// memoryConsolidation: true, // opt into background consolidation (billed)
|
|
74
|
+
// variables: [...], enabledHostedTools: [...], rolloutPercent: 100,
|
|
75
|
+
// mcpServers: ["sheets"], // scope runs to these registered MCP servers (omit = all)
|
|
87
76
|
});
|
|
88
77
|
|
|
89
78
|
export const curatorAgentId = curator.agentId;
|
|
90
79
|
```
|
|
91
80
|
|
|
92
81
|
**MCP Apps UI templates.** Pass `uiTemplates` to attach interactive HTML bundles
|
|
93
|
-
(MCP Apps, SEP-1865).
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
`html`;
|
|
98
|
-
|
|
82
|
+
(MCP Apps, SEP-1865). A template's bytes upload when they or its metadata change.
|
|
83
|
+
Its name, content hash and metadata (`tool`, `csp`, `permissions`,
|
|
84
|
+
`prefersBorder`, `domain`) are part of the content signature, so editing a
|
|
85
|
+
template publishes and rolls out a new version. Provide the HTML as an absolute
|
|
86
|
+
`htmlPath` or inline `html`; `tool` binds it as a typed app-tool, and
|
|
87
|
+
`tool.visibility: ["app"]` hides that tool from the host's model so only the
|
|
88
|
+
rendered panel can call it. Removing a template from the array deletes it from
|
|
89
|
+
the draft.
|
|
99
90
|
|
|
100
91
|
```ts
|
|
101
92
|
import { join } from "node:path";
|
|
102
93
|
|
|
103
94
|
const bookkeeper = new ic.IcAgent("bookkeeper", {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
95
|
+
...conn,
|
|
96
|
+
instructions: BOOKKEEPER_INSTRUCTIONS,
|
|
97
|
+
uiTemplates: [
|
|
98
|
+
{
|
|
99
|
+
name: "cash_chart",
|
|
100
|
+
htmlPath: join(__dirname, "ui/cash-chart.html"),
|
|
101
|
+
tool: {
|
|
102
|
+
description: "Show an interactive cash-flow chart.",
|
|
103
|
+
input_schema: { type: "object", properties: {}, required: [] },
|
|
104
|
+
instruction:
|
|
105
|
+
"Call get_cash_flow_history, then render_app with template cash_chart.",
|
|
106
|
+
mutating: false,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
118
110
|
});
|
|
119
111
|
```
|
|
120
112
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
> Pulumi does not own per-smith runtime state.
|
|
113
|
+
Pulumi does not own per-smith runtime state. New smiths attach to an agent in
|
|
114
|
+
app code; attaching existing smiths is a one-time migration, not a resource.
|
|
124
115
|
|
|
125
|
-
### IcWebPage (
|
|
116
|
+
### IcWebPage (app stack)
|
|
126
117
|
|
|
127
|
-
A
|
|
128
|
-
deployment). Each visitor gets their own smith
|
|
129
|
-
the
|
|
130
|
-
`IcAgent` and read back `pageUrl
|
|
118
|
+
A hosted page: a private, shareable web page that runs an agent (a `web`
|
|
119
|
+
deployment). Each visitor gets their own smith. The 128-bit `dep_` id in the URL
|
|
120
|
+
is the access control, with an optional shared `password`. Point it at an
|
|
121
|
+
`IcAgent` and read back `pageUrl`.
|
|
131
122
|
|
|
132
123
|
```ts
|
|
133
124
|
const page = new ic.IcWebPage("docs-page", {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
125
|
+
...conn,
|
|
126
|
+
agentId: curator.agentId,
|
|
127
|
+
title: "Ingram Cloud docs assistant",
|
|
128
|
+
greeting: "Ask me anything about Ingram Cloud.",
|
|
129
|
+
// password: "optional-shared-secret",
|
|
139
130
|
});
|
|
140
131
|
|
|
141
132
|
export const pageUrl = page.pageUrl; // https://cloud.ingram.tech/hosted/dep_…
|
|
142
133
|
```
|
|
143
134
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
135
|
+
The API has no update path for a web deployment's title, greeting or password,
|
|
136
|
+
so changing any of those (or `agentId`) replaces the page and mints a new
|
|
137
|
+
`pageUrl`. Instructions change on the `IcAgent`.
|
|
147
138
|
|
|
148
|
-
### IcSkill (
|
|
139
|
+
### IcSkill (app stack)
|
|
149
140
|
|
|
150
141
|
An [Agent Skills](https://cloud.ingram.tech/docs/skills) bundle: a folder
|
|
151
|
-
anchored by `SKILL.md`, uploaded whole and versioned server-side.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
bytes are read at `pulumi up`; any edit inside the folder publishes a new
|
|
155
|
-
immutable version and moves `default_version` to it, because a version is
|
|
156
|
-
immutable and nothing resolving by default would otherwise see the change.
|
|
142
|
+
anchored by `SKILL.md`, uploaded whole and versioned server-side. Declare it by
|
|
143
|
+
local directory. The bytes are read at `pulumi up`; any edit inside the folder
|
|
144
|
+
publishes a new immutable version and moves `default_version` to it.
|
|
157
145
|
|
|
158
146
|
```ts
|
|
159
147
|
// The folder is the unit. Its basename is the skill's name, and any edit
|
|
160
148
|
// inside it publishes a new immutable version and moves the default.
|
|
161
149
|
const vatFiling = new ic.IcSkill("vat-filing", {
|
|
162
|
-
|
|
163
|
-
|
|
150
|
+
...conn,
|
|
151
|
+
path: join(__dirname, "skills/vat-filing"),
|
|
164
152
|
});
|
|
165
153
|
|
|
166
|
-
//
|
|
154
|
+
// Attach it to an agent. Order matters: a run that fills its search index
|
|
167
155
|
// keeps the earlier entries and drops the rest.
|
|
168
156
|
const biller = new ic.IcAgent("biller", {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
157
|
+
...conn,
|
|
158
|
+
instructions: AGENT_SPECS.biller.instructions,
|
|
159
|
+
skills: [{ skill_id: vatFiling.skillId }],
|
|
160
|
+
// A skill's scripts/ only run through the run_command hosted tool.
|
|
161
|
+
enabledHostedTools: ["run_command"],
|
|
174
162
|
});
|
|
175
163
|
```
|
|
176
164
|
|
|
177
|
-
### IcMcpServer, IcOauthProvider, IcTelegramBot,
|
|
165
|
+
### IcMcpServer, IcOauthProvider, IcTelegramBot, IcWhatsAppNumber, IcEmail, IcWebhook, IcModelKey (infra stack)
|
|
178
166
|
|
|
179
167
|
```ts
|
|
180
168
|
// Your own server (raw URL).
|
|
181
|
-
new ic.IcMcpServer("
|
|
182
|
-
|
|
183
|
-
|
|
169
|
+
new ic.IcMcpServer("acme-mcp", {
|
|
170
|
+
...conn,
|
|
171
|
+
serverName: "acme",
|
|
172
|
+
url: `${APP_URL}/api/mcp`,
|
|
173
|
+
authKind: "bearer",
|
|
174
|
+
secret: mcpSecret,
|
|
184
175
|
});
|
|
185
176
|
|
|
186
|
-
// The tool list is discovered at create, not on every `pulumi up
|
|
187
|
-
// this stack changes when your
|
|
177
|
+
// The tool list is discovered at create, not on every `pulumi up`: nothing in
|
|
178
|
+
// this stack changes when your server's tools do, so `toolsDiscovered` is what
|
|
188
179
|
// was true at registration. Ingram Cloud re-discovers on the run path once a
|
|
189
180
|
// manifest is an hour old; call `POST /v1/tenant/mcp/{serverName}/refresh` at the
|
|
190
181
|
// end of a deploy that changed your tools if an hour is too long to wait.
|
|
@@ -192,104 +183,108 @@ new ic.IcMcpServer("thornhill-mcp", {
|
|
|
192
183
|
// An `oauth`-kind server forwards each end-user's own stored token; its
|
|
193
184
|
// provider record tells IC how tokens refresh. Webhook-delegation mode (a
|
|
194
185
|
// refreshWebhook and NO clientSecret): IC POSTs { connection_id } to the
|
|
195
|
-
// tenant near expiry and the tenant PATCHes a fresh pair back
|
|
196
|
-
// owns the token format
|
|
197
|
-
new ic.IcMcpServer("
|
|
198
|
-
|
|
199
|
-
|
|
186
|
+
// tenant near expiry and the tenant PATCHes a fresh pair back, so the tenant
|
|
187
|
+
// owns the token format.
|
|
188
|
+
new ic.IcMcpServer("acme-user-mcp", {
|
|
189
|
+
...conn,
|
|
190
|
+
serverName: "acme-user",
|
|
191
|
+
url: `${APP_URL}/api/user-mcp`,
|
|
192
|
+
authKind: "oauth",
|
|
193
|
+
authProvider: "acme",
|
|
200
194
|
});
|
|
201
|
-
new ic.IcOauthProvider("
|
|
202
|
-
|
|
203
|
-
|
|
195
|
+
new ic.IcOauthProvider("acme-provider", {
|
|
196
|
+
...conn,
|
|
197
|
+
provider: "acme",
|
|
198
|
+
refreshWebhook: `${APP_URL}/api/ic/refresh`,
|
|
204
199
|
});
|
|
205
200
|
|
|
206
|
-
// A
|
|
207
|
-
// require human approval on the
|
|
208
|
-
// declared here
|
|
201
|
+
// A catalog third party: stamp from the catalog, gate which tools run, and
|
|
202
|
+
// require human approval on the destructive ones. Per-smith OAuth tokens are
|
|
203
|
+
// NOT declared here; the runtime collects each end-user's identity (the hosted
|
|
209
204
|
// connect flow vaults it). `clientMode: "platform"` uses Ingram's OAuth client.
|
|
210
205
|
new ic.IcMcpServer("stripe", {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
206
|
+
...conn,
|
|
207
|
+
serverName: "stripe",
|
|
208
|
+
catalog: "stripe",
|
|
209
|
+
toolAllowlist: ["get_balance", "list_charges", "create_refund"],
|
|
210
|
+
approvalPolicy: [{ match: "create_refund" }],
|
|
214
211
|
});
|
|
215
212
|
|
|
216
|
-
new ic.IcTelegramBot("
|
|
213
|
+
new ic.IcTelegramBot("acme-telegram", { ...conn, botToken });
|
|
217
214
|
|
|
218
215
|
// The agent's own email channel (BYO-Cloudflare transport). IC returns the
|
|
219
|
-
// inbound HMAC once
|
|
220
|
-
const email = new ic.IcEmail("
|
|
221
|
-
|
|
216
|
+
// inbound HMAC once; it becomes a secret output you flow into the inbound worker.
|
|
217
|
+
const email = new ic.IcEmail("acme-email", {
|
|
218
|
+
...conn,
|
|
219
|
+
cloudflareAccountId,
|
|
220
|
+
cloudflareApiToken,
|
|
221
|
+
fromDomain: "mail.acme.example",
|
|
222
222
|
});
|
|
223
223
|
export const inboundEmailSecret = email.inboundSecret; // create-time secret output
|
|
224
224
|
|
|
225
|
-
const hook = new ic.IcWebhook("
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
const hook = new ic.IcWebhook("acme-events", {
|
|
226
|
+
...conn,
|
|
227
|
+
url: `${APP_URL}/api/ic/events`,
|
|
228
|
+
events: ["run.completed", "approval.required" /* … */],
|
|
228
229
|
});
|
|
229
230
|
export const webhookSigningSecret = hook.secret; // whsec_… (create-only secret output)
|
|
230
231
|
|
|
231
232
|
new ic.IcModelKey("anthropic", { ...conn, provider: "anthropic", apiKey });
|
|
232
233
|
```
|
|
233
234
|
|
|
234
|
-
`IcWebhook.secret` is the `whsec_…` signing secret
|
|
235
|
-
|
|
236
|
-
env with no copy-paste.
|
|
235
|
+
`IcWebhook.secret` is the `whsec_…` signing secret the API returns once. It is a
|
|
236
|
+
secret Pulumi output.
|
|
237
237
|
|
|
238
|
-
### IcProject + IcProjectToken (
|
|
238
|
+
### IcProject + IcProjectToken (platform stack)
|
|
239
239
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
no run, memory, or smith itself; only the project tokens it mints can.
|
|
240
|
+
These take an organization key (`organization:*`, your account master key), not a
|
|
241
|
+
project token. `IcProject` provisions a project; `IcProjectToken` mints it a
|
|
242
|
+
project-scoped `tenant:*` token for the app's env. The organization key cannot
|
|
243
|
+
read runs, memory or smiths; only the project tokens it mints can.
|
|
245
244
|
|
|
246
245
|
```ts
|
|
247
246
|
// In the PLATFORM stack, `ingram-cloud:token` is your ORGANIZATION key.
|
|
248
|
-
const project = new ic.IcProject("
|
|
249
|
-
const icToken = new ic.IcProjectToken("
|
|
250
|
-
|
|
251
|
-
|
|
247
|
+
const project = new ic.IcProject("acme", { ...conn }); // adopt-by-name
|
|
248
|
+
const icToken = new ic.IcProjectToken("acme-admin", {
|
|
249
|
+
...conn,
|
|
250
|
+
project: project.projectId, // the project id == the tenant
|
|
252
251
|
});
|
|
253
252
|
|
|
254
|
-
// Flow the minted tenant token
|
|
255
|
-
new vercel.ProjectEnvironmentVariable("
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
253
|
+
// Flow the minted tenant token into the app's runtime env:
|
|
254
|
+
new vercel.ProjectEnvironmentVariable("acme-ic-token", {
|
|
255
|
+
projectId: acmeVercelId,
|
|
256
|
+
key: "INGRAM_CLOUD_TOKEN",
|
|
257
|
+
value: icToken.projectToken, // secret tenant:* token
|
|
258
|
+
targets: ["production"],
|
|
260
259
|
});
|
|
261
|
-
export const
|
|
260
|
+
export const acmeProjectId = project.projectId;
|
|
262
261
|
```
|
|
263
262
|
|
|
264
263
|
`IcProject` adopts an existing project by `projectName` (defaults to the resource
|
|
265
|
-
name)
|
|
266
|
-
|
|
264
|
+
name). `IcProjectToken.projectToken` is a secret output; it re-mints on any change
|
|
265
|
+
and revokes on destroy.
|
|
267
266
|
|
|
268
|
-
## Importing resources
|
|
267
|
+
## Importing existing resources
|
|
269
268
|
|
|
270
|
-
Agents and webhooks implement `read`, so you can
|
|
269
|
+
Agents and webhooks implement `read`, so you can import them:
|
|
271
270
|
|
|
272
271
|
```bash
|
|
273
272
|
pulumi import 'ingram-cloud:index:IcAgent' curator agt_123…
|
|
274
273
|
```
|
|
275
274
|
|
|
276
|
-
(Agents also self-adopt by slug on first `create`, so an explicit import is
|
|
277
|
-
usually unnecessary — a plain `pulumi up` will reconcile the live agent.)
|
|
278
|
-
|
|
279
275
|
## Notes
|
|
280
276
|
|
|
281
|
-
- Built as CommonJS
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
are encrypted in Pulumi state.
|
|
277
|
+
- Built as CommonJS for the Pulumi Node.js runtime.
|
|
278
|
+
- `token` and every credential input are `additionalSecretOutputs`, encrypted in
|
|
279
|
+
Pulumi state.
|
|
285
280
|
- CRUD runs inline in the Pulumi process over the global `fetch` (Node ≥18).
|
|
286
|
-
- `read` treats a server-side
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
- `IcMcpServer` `read`
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
281
|
+
- `read` treats a server-side 404 as gone (empty id), so `pulumi refresh` prunes a
|
|
282
|
+
resource deleted out-of-band and `pulumi up` recreates it without
|
|
283
|
+
`pulumi state delete`. Other errors throw, so a transient API error cannot drop
|
|
284
|
+
a resource from state.
|
|
285
|
+
- `IcMcpServer` `read` round-trips the live `url` / `authKind` / `authProvider`
|
|
286
|
+
(raw registrations only; catalog presets supply these server-side), so
|
|
287
|
+
`pulumi refresh && pulumi up` repairs server-side drift such as a lost auth
|
|
288
|
+
block. Write-only fields (`secret`) cannot round-trip and diff by state.
|
|
289
|
+
Dynamic providers run the code serialized into state, so drift detection starts
|
|
290
|
+
after the first `pulumi up` with this version.
|
package/dist/resources.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* so declare these in the APP repo's Pulumi program, sourced from the app's
|
|
15
15
|
* own spec module. Never copy prompts into the infra repo.
|
|
16
16
|
* - **Integrations / tenant config** (`IcMcpServer`, `IcOauthProvider`,
|
|
17
|
-
* `IcTelegramBot`, `
|
|
17
|
+
* `IcTelegramBot`, `IcWhatsAppNumber`, `IcWebhook`, `IcModelKey`) —
|
|
18
18
|
* tenant-level wiring; declare in the infra repo alongside DNS/DB/Vercel.
|
|
19
19
|
*
|
|
20
20
|
* Not modelled here, on purpose:
|
|
@@ -51,8 +51,8 @@ export interface UiCsp {
|
|
|
51
51
|
frameDomains?: string[];
|
|
52
52
|
baseUriDomains?: string[];
|
|
53
53
|
}
|
|
54
|
-
/** A typed app-tool bound to a template
|
|
55
|
-
* the template renders the result. */
|
|
54
|
+
/** A typed app-tool bound to a template: the host's model (or the rendered
|
|
55
|
+
* panel) calls it and the template renders the result. */
|
|
56
56
|
export interface UiResourceTool {
|
|
57
57
|
description: string;
|
|
58
58
|
/** JSON Schema for the tool's arguments. */
|
|
@@ -61,6 +61,9 @@ export interface UiResourceTool {
|
|
|
61
61
|
instruction?: string | null;
|
|
62
62
|
/** Writes gate through approval; reads flow freely. */
|
|
63
63
|
mutating?: boolean;
|
|
64
|
+
/** MCP Apps `_meta.ui.visibility`: `["app"]` hides the tool from the host's
|
|
65
|
+
* model so only the rendered panel can call it. Omitted means both. */
|
|
66
|
+
visibility?: ("model" | "app")[];
|
|
64
67
|
}
|
|
65
68
|
/** One UI template declared on an `IcAgent`. Provide the HTML via `htmlPath`
|
|
66
69
|
* (read at `pulumi up`, so use an absolute path) or inline `html`. */
|
|
@@ -74,6 +77,12 @@ export interface UiTemplate {
|
|
|
74
77
|
csp?: UiCsp;
|
|
75
78
|
/** Permissions-Policy-style host permissions the template requests. */
|
|
76
79
|
permissions?: Record<string, unknown>;
|
|
80
|
+
/** Whether the host draws its own border around the panel; omitted leaves it
|
|
81
|
+
* to the host. */
|
|
82
|
+
prefersBorder?: boolean;
|
|
83
|
+
/** A stable sandbox origin for the panel, in the host's own format (Claude:
|
|
84
|
+
* `{sha256(connector URL)[:32]}.claudemcpcontent.com`). */
|
|
85
|
+
domain?: string;
|
|
77
86
|
tool?: UiResourceTool;
|
|
78
87
|
}
|
|
79
88
|
export interface IcAgentArgs {
|
|
@@ -239,7 +248,7 @@ export interface IcOauthProviderArgs {
|
|
|
239
248
|
export declare class IcOauthProvider extends pulumi.dynamic.Resource {
|
|
240
249
|
constructor(name: string, args: IcOauthProviderArgs, opts?: pulumi.CustomResourceOptions);
|
|
241
250
|
}
|
|
242
|
-
export interface
|
|
251
|
+
export interface IcWhatsAppNumberArgs {
|
|
243
252
|
baseUrl: pulumi.Input<string>;
|
|
244
253
|
token: pulumi.Input<string>;
|
|
245
254
|
phoneNumberId: pulumi.Input<string>;
|
|
@@ -247,10 +256,12 @@ export interface IcWhatsAppConfigArgs {
|
|
|
247
256
|
/** The WhatsApp Business Account id — IC subscribes its app to it so inbound flows. */
|
|
248
257
|
wabaId: pulumi.Input<string>;
|
|
249
258
|
}
|
|
250
|
-
|
|
251
|
-
|
|
259
|
+
/** One of the tenant's WhatsApp numbers. A tenant may register several; bind one to
|
|
260
|
+
* an agent with a `whatsapp` deployment whose `address` is its `phoneNumberId`. */
|
|
261
|
+
export declare class IcWhatsAppNumber extends pulumi.dynamic.Resource {
|
|
262
|
+
readonly displayPhoneNumber: pulumi.Output<string | null>;
|
|
252
263
|
readonly webhookUrl: pulumi.Output<string>;
|
|
253
|
-
constructor(name: string, args:
|
|
264
|
+
constructor(name: string, args: IcWhatsAppNumberArgs, opts?: pulumi.CustomResourceOptions);
|
|
254
265
|
}
|
|
255
266
|
export interface IcEmailArgs {
|
|
256
267
|
baseUrl: 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.
|
|
36
|
+
exports.IcWebPage = exports.IcProjectToken = exports.IcProject = exports.IcModelKey = exports.IcWebhook = exports.IcEmail = exports.IcWhatsAppNumber = exports.IcOauthProvider = exports.IcTelegramBot = exports.IcMcpServer = exports.IcVectorStore = exports.IcSkill = exports.IcAgent = exports.IC_API_VERSION = void 0;
|
|
37
37
|
/**
|
|
38
38
|
* Ingram Cloud /v1 tenant configuration as Pulumi dynamic resources.
|
|
39
39
|
*
|
|
@@ -50,7 +50,7 @@ exports.IcWebPage = exports.IcProjectToken = exports.IcProject = exports.IcModel
|
|
|
50
50
|
* so declare these in the APP repo's Pulumi program, sourced from the app's
|
|
51
51
|
* own spec module. Never copy prompts into the infra repo.
|
|
52
52
|
* - **Integrations / tenant config** (`IcMcpServer`, `IcOauthProvider`,
|
|
53
|
-
* `IcTelegramBot`, `
|
|
53
|
+
* `IcTelegramBot`, `IcWhatsAppNumber`, `IcWebhook`, `IcModelKey`) —
|
|
54
54
|
* tenant-level wiring; declare in the infra repo alongside DNS/DB/Vercel.
|
|
55
55
|
*
|
|
56
56
|
* Not modelled here, on purpose:
|
|
@@ -150,6 +150,23 @@ function makePutResource(cfg) {
|
|
|
150
150
|
},
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
|
+
const UI_META_KEYS = [
|
|
154
|
+
"csp",
|
|
155
|
+
"permissions",
|
|
156
|
+
"prefers_border",
|
|
157
|
+
"domain",
|
|
158
|
+
"tool",
|
|
159
|
+
];
|
|
160
|
+
/** The declared metadata keys of `obj`, in canonical order, absent ones dropped —
|
|
161
|
+
* so a resource input and a stored entry compare byte-equal under `eq`. */
|
|
162
|
+
function pickUiMeta(obj) {
|
|
163
|
+
const src = obj;
|
|
164
|
+
const out = {};
|
|
165
|
+
for (const k of UI_META_KEYS)
|
|
166
|
+
if (src[k] != null)
|
|
167
|
+
out[k] = src[k];
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
153
170
|
// Read each template's HTML bundle (from `htmlPath` or inline `html`), hash the
|
|
154
171
|
// bytes with the same sha256-hex the API stores as `content_hash`, and carry the
|
|
155
172
|
// metadata through. Module-scope + node built-ins required inside so Pulumi's
|
|
@@ -171,9 +188,13 @@ function resolveUiTemplates(templates) {
|
|
|
171
188
|
name: t.name,
|
|
172
189
|
bytes,
|
|
173
190
|
contentHash: createHash("sha256").update(bytes).digest("hex"),
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
191
|
+
meta: pickUiMeta({
|
|
192
|
+
csp: t.csp,
|
|
193
|
+
permissions: t.permissions,
|
|
194
|
+
prefers_border: t.prefersBorder,
|
|
195
|
+
domain: t.domain,
|
|
196
|
+
tool: t.tool,
|
|
197
|
+
}),
|
|
177
198
|
};
|
|
178
199
|
});
|
|
179
200
|
}
|
|
@@ -183,14 +204,7 @@ function resolveUiTemplates(templates) {
|
|
|
183
204
|
async function uploadUiTemplate(baseUrl, token, aid, t) {
|
|
184
205
|
const form = new FormData();
|
|
185
206
|
form.set("file", new File([new Uint8Array(t.bytes)], `${t.name}.html`, { type: "text/html" }));
|
|
186
|
-
|
|
187
|
-
if (t.csp)
|
|
188
|
-
meta.csp = t.csp;
|
|
189
|
-
if (t.permissions)
|
|
190
|
-
meta.permissions = t.permissions;
|
|
191
|
-
if (t.tool)
|
|
192
|
-
meta.tool = t.tool;
|
|
193
|
-
form.set("metadata", JSON.stringify(meta));
|
|
207
|
+
form.set("metadata", JSON.stringify({ name: t.name, ...t.meta }));
|
|
194
208
|
const res = await fetch(`${baseUrl}/v1/agents/${aid}/ui`, {
|
|
195
209
|
method: "POST",
|
|
196
210
|
headers: { Authorization: `Bearer ${token}`, "IC-Api-Version": exports.IC_API_VERSION },
|
|
@@ -205,16 +219,12 @@ async function uploadUiTemplate(baseUrl, token, aid, t) {
|
|
|
205
219
|
// Reconcile the agent's UI templates against its draft `current` state: upload any
|
|
206
220
|
// whose bytes or metadata drifted (or are new), delete any the resource no longer
|
|
207
221
|
// declares. The draft this leaves behind is what the publish step snapshots.
|
|
222
|
+
const eq = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
208
223
|
async function syncUiTemplates(i, id, current) {
|
|
209
224
|
const desired = resolveUiTemplates(i.uiTemplates);
|
|
210
|
-
const eq = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
211
225
|
for (const t of desired) {
|
|
212
226
|
const cur = current.find((e) => e?.name === t.name);
|
|
213
|
-
const unchanged = cur &&
|
|
214
|
-
cur.content_hash === t.contentHash &&
|
|
215
|
-
eq(cur.tool, t.tool) &&
|
|
216
|
-
eq(cur.csp, t.csp) &&
|
|
217
|
-
eq(cur.permissions, t.permissions);
|
|
227
|
+
const unchanged = cur && cur.content_hash === t.contentHash && eq(pickUiMeta(cur), t.meta);
|
|
218
228
|
if (!unchanged)
|
|
219
229
|
await uploadUiTemplate(i.baseUrl, i.token, id, t);
|
|
220
230
|
}
|
|
@@ -258,20 +268,14 @@ function agentBody(i) {
|
|
|
258
268
|
};
|
|
259
269
|
}
|
|
260
270
|
// Canonical, name-sorted signature of an agent's frozen UI templates: each
|
|
261
|
-
// entry's name + content hash +
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
271
|
+
// entry's name + content hash + declared metadata. Both the resource inputs
|
|
272
|
+
// (hash computed from local bytes) and a published version's snapshot (hash
|
|
273
|
+
// already stored) map onto this shape, so a template edit moves the signature
|
|
274
|
+
// exactly one way.
|
|
265
275
|
function uiResourcesSig(list) {
|
|
266
276
|
return (list ?? [])
|
|
267
|
-
.map((e) => ({
|
|
268
|
-
name:
|
|
269
|
-
content_hash: e.content_hash,
|
|
270
|
-
tool: e.tool ?? null,
|
|
271
|
-
csp: e.csp ?? null,
|
|
272
|
-
permissions: e.permissions ?? null,
|
|
273
|
-
}))
|
|
274
|
-
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
277
|
+
.map((e) => ({ name: e.name, content_hash: e.content_hash, ...pickUiMeta(e) }))
|
|
278
|
+
.toSorted((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
275
279
|
}
|
|
276
280
|
// Stable signature of the *content* that warrants a new published version
|
|
277
281
|
// (name and rollout% deliberately excluded — a rename/restage is not new content).
|
|
@@ -279,7 +283,7 @@ function agentContentSig(s) {
|
|
|
279
283
|
const base = {
|
|
280
284
|
instructions: s.instructions ?? null,
|
|
281
285
|
model: s.model ?? null,
|
|
282
|
-
tools:
|
|
286
|
+
tools: (s.enabled_hosted_tools ?? []).toSorted(),
|
|
283
287
|
auto_memory: s.auto_memory ?? null,
|
|
284
288
|
memory_consolidation: s.memory_consolidation ?? null,
|
|
285
289
|
variables: s.variables ?? [],
|
|
@@ -292,10 +296,10 @@ function agentContentSig(s) {
|
|
|
292
296
|
base.ui_resources = ui;
|
|
293
297
|
// Same discipline for mcp_servers: unset (= all) stays out of the signature.
|
|
294
298
|
if (s.mcp_servers != null)
|
|
295
|
-
base.mcp_servers =
|
|
299
|
+
base.mcp_servers = s.mcp_servers.toSorted();
|
|
296
300
|
// And for vector stores: omit when empty so agents without any keep a
|
|
297
301
|
// byte-identical signature to pre-vectorStoreIds releases.
|
|
298
|
-
const vs =
|
|
302
|
+
const vs = (s.vector_store_ids ?? []).toSorted();
|
|
299
303
|
if (vs.length)
|
|
300
304
|
base.vector_store_ids = vs;
|
|
301
305
|
// Skills, deliberately NOT sorted: reference order is the run's index-ceiling
|
|
@@ -329,9 +333,7 @@ function inputsToSnapshot(i) {
|
|
|
329
333
|
ui_resources: resolveUiTemplates(i.uiTemplates).map((t) => ({
|
|
330
334
|
name: t.name,
|
|
331
335
|
content_hash: t.contentHash,
|
|
332
|
-
|
|
333
|
-
csp: t.csp,
|
|
334
|
-
permissions: t.permissions,
|
|
336
|
+
...t.meta,
|
|
335
337
|
})),
|
|
336
338
|
};
|
|
337
339
|
}
|
|
@@ -432,7 +434,7 @@ const agentProvider = {
|
|
|
432
434
|
if (e?.status === 409)
|
|
433
435
|
throw new Error(`Agent ${id} can't be archived while smiths still run it. Delete or ` +
|
|
434
436
|
`re-point those smiths first, or set { retainOnDelete: true } on the ` +
|
|
435
|
-
`resource to leave the agent in place. ${e}
|
|
437
|
+
`resource to leave the agent in place. ${e}`, { cause: e });
|
|
436
438
|
throw e;
|
|
437
439
|
}
|
|
438
440
|
},
|
|
@@ -531,7 +533,7 @@ function resolveSkillFiles(dir) {
|
|
|
531
533
|
walk(nodePath.resolve(dir), "");
|
|
532
534
|
if (!out.some((f) => f.rel === `${root}/SKILL.md`))
|
|
533
535
|
throw new Error(`skill: ${dir} has no SKILL.md`);
|
|
534
|
-
return out.
|
|
536
|
+
return out.toSorted((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
|
|
535
537
|
}
|
|
536
538
|
/** A content signature over the whole bundle — what `diff` compares. */
|
|
537
539
|
function skillSig(dir) {
|
|
@@ -724,7 +726,7 @@ function vsFilesSig(files) {
|
|
|
724
726
|
content_hash: f.contentHash,
|
|
725
727
|
chunking: f.chunkingStrategy ?? null,
|
|
726
728
|
}))
|
|
727
|
-
.
|
|
729
|
+
.toSorted((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)));
|
|
728
730
|
}
|
|
729
731
|
const vectorStoreProvider = {
|
|
730
732
|
async create(i) {
|
|
@@ -945,22 +947,21 @@ class IcOauthProvider extends pulumi.dynamic.Resource {
|
|
|
945
947
|
}
|
|
946
948
|
exports.IcOauthProvider = IcOauthProvider;
|
|
947
949
|
const whatsappProvider = makePutResource({
|
|
948
|
-
path: () =>
|
|
949
|
-
id: () =>
|
|
950
|
-
body: (i) => ({
|
|
951
|
-
phone_number_id: i.phoneNumberId,
|
|
952
|
-
access_token: i.accessToken,
|
|
953
|
-
waba_id: i.wabaId,
|
|
954
|
-
}),
|
|
950
|
+
path: (i) => `/v1/tenant/whatsapp/${encodeURIComponent(i.phoneNumberId)}`,
|
|
951
|
+
id: (i) => i.phoneNumberId,
|
|
952
|
+
body: (i) => ({ access_token: i.accessToken, waba_id: i.wabaId }),
|
|
955
953
|
outs: (_i, res) => ({
|
|
956
954
|
displayPhoneNumber: res.display_phone_number,
|
|
957
955
|
webhookUrl: res.webhook_url,
|
|
958
956
|
}),
|
|
959
957
|
diff: {
|
|
960
|
-
|
|
958
|
+
replaceKeys: ["phoneNumberId"],
|
|
959
|
+
scalarKeys: ["accessToken", "wabaId", "token", "baseUrl"],
|
|
961
960
|
},
|
|
962
961
|
});
|
|
963
|
-
|
|
962
|
+
/** One of the tenant's WhatsApp numbers. A tenant may register several; bind one to
|
|
963
|
+
* an agent with a `whatsapp` deployment whose `address` is its `phoneNumberId`. */
|
|
964
|
+
class IcWhatsAppNumber extends pulumi.dynamic.Resource {
|
|
964
965
|
constructor(name, args, opts) {
|
|
965
966
|
super(whatsappProvider, name, {
|
|
966
967
|
displayPhoneNumber: undefined,
|
|
@@ -972,7 +973,7 @@ class IcWhatsAppConfig extends pulumi.dynamic.Resource {
|
|
|
972
973
|
});
|
|
973
974
|
}
|
|
974
975
|
}
|
|
975
|
-
exports.
|
|
976
|
+
exports.IcWhatsAppNumber = IcWhatsAppNumber;
|
|
976
977
|
const emailProvider = makePutResource({
|
|
977
978
|
path: () => "/v1/tenant/email",
|
|
978
979
|
id: () => "email",
|
|
@@ -1044,8 +1045,8 @@ const webhookProvider = {
|
|
|
1044
1045
|
},
|
|
1045
1046
|
async diff(_id, olds, news) {
|
|
1046
1047
|
const replaces = olds.url !== news.url ? ["url"] : [];
|
|
1047
|
-
const eventsChanged = JSON.stringify(
|
|
1048
|
-
JSON.stringify(
|
|
1048
|
+
const eventsChanged = JSON.stringify((olds.events ?? []).toSorted()) !==
|
|
1049
|
+
JSON.stringify((news.events ?? []).toSorted());
|
|
1049
1050
|
return { changes: replaces.length > 0 || eventsChanged, replaces };
|
|
1050
1051
|
},
|
|
1051
1052
|
// Enables `pulumi import` of a webhook a script already created (id is the
|
package/package.json
CHANGED
|
@@ -1,38 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ingram-cloud/pulumi",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Pulumi dynamic resources for the Ingram Cloud /v1 API — projects, project tokens, agents, MCP servers, channels, webhooks, model keys.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"homepage": "https://cloud.ingram.tech",
|
|
7
5
|
"keywords": [
|
|
8
|
-
"
|
|
6
|
+
"ingram",
|
|
9
7
|
"ingram-cloud",
|
|
10
|
-
"
|
|
8
|
+
"pulumi"
|
|
11
9
|
],
|
|
12
|
-
"
|
|
13
|
-
"
|
|
10
|
+
"homepage": "https://github.com/ingram-technologies/ingram-cloud-sdks/tree/main/packages/pulumi#readme",
|
|
11
|
+
"bugs": "https://github.com/ingram-technologies/ingram-cloud-sdks/issues",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/ingram-technologies/ingram-cloud-sdks.git",
|
|
16
|
+
"directory": "packages/pulumi"
|
|
17
|
+
},
|
|
14
18
|
"files": [
|
|
15
19
|
"dist"
|
|
16
20
|
],
|
|
21
|
+
"main": "dist/index.js",
|
|
22
|
+
"types": "dist/index.d.ts",
|
|
17
23
|
"publishConfig": {
|
|
18
24
|
"access": "public"
|
|
19
25
|
},
|
|
20
26
|
"scripts": {
|
|
21
27
|
"build": "tsc -p tsconfig.json",
|
|
22
|
-
"
|
|
28
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
23
29
|
"lint": "oxlint",
|
|
24
30
|
"format": "oxfmt --write .",
|
|
25
|
-
"
|
|
31
|
+
"prepack": "bun run build"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@pulumi/pulumi": "^3.260.0"
|
|
26
35
|
},
|
|
27
36
|
"peerDependencies": {
|
|
28
37
|
"@pulumi/pulumi": "^3.0.0"
|
|
29
38
|
},
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"@pulumi/pulumi": "^3.255.0",
|
|
33
|
-
"@types/node": "^26.1.2",
|
|
34
|
-
"oxfmt": "0.62.0",
|
|
35
|
-
"oxlint": "^1.77.0",
|
|
36
|
-
"typescript": "^7.0.2"
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20"
|
|
37
41
|
}
|
|
38
42
|
}
|