@ingram-cloud/pulumi 1.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 +201 -0
- package/dist/connection.d.ts +22 -0
- package/dist/connection.js +65 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +25 -0
- package/dist/resources.d.ts +210 -0
- package/dist/resources.js +686 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# @ingram-cloud/pulumi
|
|
2
|
+
|
|
3
|
+
Pulumi dynamic resources for the [Ingram Cloud](https://cloud.ingram.tech) `/v1`
|
|
4
|
+
API. Declare your tenant's IC configuration — agents, MCP servers, integrations,
|
|
5
|
+
webhooks, BYOK model keys — as Pulumi resources instead of imperative
|
|
6
|
+
`register-*.ts` / `ensure-agent.ts` scripts, so it lives in state and a change
|
|
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`).
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add @ingram-cloud/pulumi @pulumi/pulumi
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## How the repos interact
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
cloud.ingram.tech/pulumi/ THIS LIBRARY — knows nothing about any consumer
|
|
23
|
+
(Agent, McpServer, TelegramBot, WhatsAppConfig,
|
|
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
|
+
```
|
|
37
|
+
|
|
38
|
+
The hard rule: **prompts never leave the app repo.** This library carries no
|
|
39
|
+
content; it only carries the machinery.
|
|
40
|
+
|
|
41
|
+
## Connection
|
|
42
|
+
|
|
43
|
+
Every resource takes `baseUrl` + `token` (the tenant-admin bearer; `IcProject` /
|
|
44
|
+
`IcProjectToken` take an **organization** key instead — see below). Resolve them
|
|
45
|
+
once with `connectionFromConfig()` and spread:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import * as ic from "@ingram-cloud/pulumi";
|
|
49
|
+
|
|
50
|
+
const conn = ic.connectionFromConfig(); // ingram-cloud:token (secret) / :baseUrl
|
|
51
|
+
// or env INGRAM_CLOUD_TOKEN|CLOUD_API_KEY
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Set the token as a stack secret:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pulumi config set --secret ingram-cloud:token tha_live_…
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Resources
|
|
61
|
+
|
|
62
|
+
### IcAgent (declare in the APP repo)
|
|
63
|
+
|
|
64
|
+
Create-or-adopt by **`slug`** → publish a new immutable version **only when the
|
|
65
|
+
content changed** → roll it out. Mirrors the old `ensure-agent.ts` idempotency, so
|
|
66
|
+
the first `pulumi up` after switching from a script **adopts** the existing agent
|
|
67
|
+
(matched by slug) rather than recreating it.
|
|
68
|
+
|
|
69
|
+
`slug` is the immutable reconcile key; it defaults to the Pulumi resource name
|
|
70
|
+
(here `"curator"`), so existing stacks keep adopting the same agent. `name` is a
|
|
71
|
+
free display label you can rename without churning the agent. Changing `slug`
|
|
72
|
+
replaces it.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { AGENT_SPECS } from "../src/lib/cloud/agent-spec"; // app-owned
|
|
76
|
+
|
|
77
|
+
const curator = new ic.IcAgent("curator", {
|
|
78
|
+
...conn,
|
|
79
|
+
// slug defaults to "curator"; pass `slug` to decouple the key from the name.
|
|
80
|
+
name: AGENT_SPECS.curator.name, // display label, freely renamable
|
|
81
|
+
instructions: AGENT_SPECS.curator.instructions,
|
|
82
|
+
model: AGENT_SPECS.curator.model,
|
|
83
|
+
autoMemory: AGENT_SPECS.curator.auto_memory,
|
|
84
|
+
// memoryConsolidation: true, // opt into background consolidation (billed)
|
|
85
|
+
// variables: [...], enabledHostedTools: [...], rolloutPercent: 100,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const curatorAgentId = curator.agentId;
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
> Attaching **existing** smiths to a agent is a one-time fleet backfill —
|
|
92
|
+
> keep it as the app's migration script. New smiths attach at birth in app code.
|
|
93
|
+
> Pulumi does not own per-smith runtime state.
|
|
94
|
+
|
|
95
|
+
### IcMcpServer, IcOauthProvider, IcTelegramBot, IcWhatsAppConfig, IcEmail, IcWebhook, IcModelKey (declare in INFRA)
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// Your own server (raw URL).
|
|
99
|
+
new ic.IcMcpServer("thornhill-mcp", {
|
|
100
|
+
...conn, serverName: "thornhill", url: `${APP_URL}/api/mcp`,
|
|
101
|
+
authKind: "static", secret: mcpSecret,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// An `oauth`-kind server forwards each end-user's own stored token; its
|
|
105
|
+
// provider record tells IC how tokens refresh. Webhook-delegation mode (a
|
|
106
|
+
// refreshWebhook and NO clientSecret): IC POSTs { connection_id } to the
|
|
107
|
+
// tenant near expiry and the tenant PATCHes a fresh pair back — the tenant
|
|
108
|
+
// owns the token format entirely.
|
|
109
|
+
new ic.IcMcpServer("thornhill-user-mcp", {
|
|
110
|
+
...conn, serverName: "thornhill-user", url: `${APP_URL}/api/user-mcp`,
|
|
111
|
+
authKind: "oauth", authProvider: "thornhill",
|
|
112
|
+
});
|
|
113
|
+
new ic.IcOauthProvider("thornhill-provider", {
|
|
114
|
+
...conn, provider: "thornhill",
|
|
115
|
+
refreshWebhook: `${APP_URL}/api/ic/refresh`,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// A curated third party: stamp from the catalog, gate which tools run, and
|
|
119
|
+
// require human approval on the scary ones. Per-smith OAuth tokens are NOT
|
|
120
|
+
// declared here — the runtime collects each end-user's identity (the hosted
|
|
121
|
+
// connect flow vaults it). `clientMode: "platform"` uses Ingram's OAuth client.
|
|
122
|
+
new ic.IcMcpServer("stripe", {
|
|
123
|
+
...conn, serverName: "stripe", catalog: "stripe",
|
|
124
|
+
toolAllowlist: ["get_balance", "list_charges", "create_refund"],
|
|
125
|
+
approvalPolicy: [{ match: "create_refund" }],
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
new ic.IcTelegramBot("thornhill-telegram", { ...conn, botToken });
|
|
129
|
+
|
|
130
|
+
// The agent's own email channel (BYO-Cloudflare transport). IC returns the
|
|
131
|
+
// inbound HMAC once — it becomes a secret output you flow into the inbound worker.
|
|
132
|
+
const email = new ic.IcEmail("thornhill-email", {
|
|
133
|
+
...conn, cloudflareAccountId, cloudflareApiToken, fromDomain: "mail.thornhill.app",
|
|
134
|
+
});
|
|
135
|
+
export const inboundEmailSecret = email.inboundSecret; // create-time secret output
|
|
136
|
+
|
|
137
|
+
const hook = new ic.IcWebhook("thornhill-events", {
|
|
138
|
+
...conn, url: `${APP_URL}/api/ic/events`,
|
|
139
|
+
events: ["run.completed", "approval.required", /* … */],
|
|
140
|
+
});
|
|
141
|
+
export const webhookSigningSecret = hook.secret; // whsec_… (create-only secret output)
|
|
142
|
+
|
|
143
|
+
new ic.IcModelKey("anthropic", { ...conn, provider: "anthropic", apiKey });
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`IcWebhook.secret` is the `whsec_…` signing secret IC returns exactly once — it
|
|
147
|
+
becomes a secret Pulumi output, so the infra stack can flow it straight into Vercel
|
|
148
|
+
env with no copy-paste.
|
|
149
|
+
|
|
150
|
+
### IcProject + IcProjectToken (declare in the PLATFORM stack)
|
|
151
|
+
|
|
152
|
+
The tier **above** a project. These take an **organization key**
|
|
153
|
+
(`organization:*`, your account master key) — not a project token. Hand it to a
|
|
154
|
+
platform stack and it provisions projects and mints each one a project-scoped
|
|
155
|
+
`tenant:*` token, which you drop straight into that app's env. The org key reads
|
|
156
|
+
no run, memory, or smith itself; only the project tokens it mints can.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// In the PLATFORM stack, `ingram-cloud:token` is your ORGANIZATION key.
|
|
160
|
+
const project = new ic.IcProject("thornhill", { ...conn }); // adopt-by-name
|
|
161
|
+
const icToken = new ic.IcProjectToken("thornhill-admin", {
|
|
162
|
+
...conn,
|
|
163
|
+
project: project.projectId, // the project id == the tenant
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Flow the minted tenant token straight into the app's runtime env:
|
|
167
|
+
new vercel.ProjectEnvironmentVariable("th-ic-token", {
|
|
168
|
+
projectId: thornhillVercelId,
|
|
169
|
+
key: "INGRAM_CLOUD_TOKEN",
|
|
170
|
+
value: icToken.projectToken, // secret tenant:* token
|
|
171
|
+
targets: ["production"],
|
|
172
|
+
});
|
|
173
|
+
export const thornhillProjectId = project.projectId;
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`IcProject` adopts an existing project by `projectName` (defaults to the resource
|
|
177
|
+
name), so re-runs reconcile. `IcProjectToken.projectToken` is a secret output; it
|
|
178
|
+
re-mints on any change and revokes on destroy.
|
|
179
|
+
|
|
180
|
+
## Importing resources a script already created
|
|
181
|
+
|
|
182
|
+
Agents and webhooks implement `read`, so you can adopt pre-existing ones:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
pulumi import 'ingram-cloud:index:IcAgent' curator agt_123…
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
(Agents also self-adopt by slug on first `create`, so an explicit import is
|
|
189
|
+
usually unnecessary — a plain `pulumi up` will reconcile the live agent.)
|
|
190
|
+
|
|
191
|
+
## Notes
|
|
192
|
+
|
|
193
|
+
- Built as CommonJS to match the Pulumi Node.js runtime and the infra stack's
|
|
194
|
+
`module: commonjs` tsconfig.
|
|
195
|
+
- `token` and every credential input are marked `additionalSecretOutputs`, so they
|
|
196
|
+
are encrypted in Pulumi state.
|
|
197
|
+
- CRUD runs inline in the Pulumi process over the global `fetch` (Node ≥18).
|
|
198
|
+
- `read` treats a server-side **404 as "gone"** (returns an empty id) so
|
|
199
|
+
`pulumi refresh` prunes a resource deleted out-of-band — e.g. after an IC DB
|
|
200
|
+
reset, `pulumi refresh && pulumi up` recreates it, no `pulumi state delete`.
|
|
201
|
+
Non-404 errors still throw, so a transient API blip can't drop it from state.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection helper — resolves the IC `{ baseUrl, token }` pair once so a Pulumi
|
|
3
|
+
* program can spread it into every resource:
|
|
4
|
+
*
|
|
5
|
+
* const conn = connectionFromConfig();
|
|
6
|
+
* new ic.IcAgent("assistant", { ...conn, name: "…", instructions: "…" });
|
|
7
|
+
*
|
|
8
|
+
* Resolution order (first hit wins):
|
|
9
|
+
* baseUrl: `ingram-cloud:baseUrl` config → IC_BASE_URL → CLOUD_BASE_URL → prod
|
|
10
|
+
* token: `ingram-cloud:token` secret config → INGRAM_CLOUD_TOKEN → CLOUD_API_KEY
|
|
11
|
+
*
|
|
12
|
+
* The env fallbacks bridge the two naming conventions already in the fleet
|
|
13
|
+
* (thornhill's INGRAM_CLOUD_TOKEN, integrain's CLOUD_API_KEY). The returned
|
|
14
|
+
* token is always a secret Output so it stays encrypted in state.
|
|
15
|
+
*/
|
|
16
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
17
|
+
export declare const DEFAULT_BASE_URL = "https://api.cloud.ingram.tech";
|
|
18
|
+
export interface IcConnection {
|
|
19
|
+
baseUrl: pulumi.Input<string>;
|
|
20
|
+
token: pulumi.Input<string>;
|
|
21
|
+
}
|
|
22
|
+
export declare function connectionFromConfig(cfg?: pulumi.Config): IcConnection;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULT_BASE_URL = void 0;
|
|
37
|
+
exports.connectionFromConfig = connectionFromConfig;
|
|
38
|
+
/**
|
|
39
|
+
* Connection helper — resolves the IC `{ baseUrl, token }` pair once so a Pulumi
|
|
40
|
+
* program can spread it into every resource:
|
|
41
|
+
*
|
|
42
|
+
* const conn = connectionFromConfig();
|
|
43
|
+
* new ic.IcAgent("assistant", { ...conn, name: "…", instructions: "…" });
|
|
44
|
+
*
|
|
45
|
+
* Resolution order (first hit wins):
|
|
46
|
+
* baseUrl: `ingram-cloud:baseUrl` config → IC_BASE_URL → CLOUD_BASE_URL → prod
|
|
47
|
+
* token: `ingram-cloud:token` secret config → INGRAM_CLOUD_TOKEN → CLOUD_API_KEY
|
|
48
|
+
*
|
|
49
|
+
* The env fallbacks bridge the two naming conventions already in the fleet
|
|
50
|
+
* (thornhill's INGRAM_CLOUD_TOKEN, integrain's CLOUD_API_KEY). The returned
|
|
51
|
+
* token is always a secret Output so it stays encrypted in state.
|
|
52
|
+
*/
|
|
53
|
+
const pulumi = __importStar(require("@pulumi/pulumi"));
|
|
54
|
+
exports.DEFAULT_BASE_URL = "https://api.cloud.ingram.tech";
|
|
55
|
+
function connectionFromConfig(cfg) {
|
|
56
|
+
const c = cfg ?? new pulumi.Config("ingram-cloud");
|
|
57
|
+
const baseUrl = c.get("baseUrl") ??
|
|
58
|
+
process.env.IC_BASE_URL ??
|
|
59
|
+
process.env.CLOUD_BASE_URL ??
|
|
60
|
+
exports.DEFAULT_BASE_URL;
|
|
61
|
+
const tokenFromCfg = c.getSecret("token");
|
|
62
|
+
const token = tokenFromCfg ??
|
|
63
|
+
pulumi.secret(process.env.INGRAM_CLOUD_TOKEN ?? process.env.CLOUD_API_KEY ?? "");
|
|
64
|
+
return { baseUrl, token };
|
|
65
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ingram-cloud/pulumi
|
|
3
|
+
*
|
|
4
|
+
* Pulumi dynamic resources for the Ingram Cloud /v1 API. See README.md for the
|
|
5
|
+
* repo-interaction model (library here, agents declared in app repos,
|
|
6
|
+
* integration/tenant config in the infra repo).
|
|
7
|
+
*/
|
|
8
|
+
export * from "./resources";
|
|
9
|
+
export * from "./connection";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
/**
|
|
18
|
+
* @ingram-cloud/pulumi
|
|
19
|
+
*
|
|
20
|
+
* Pulumi dynamic resources for the Ingram Cloud /v1 API. See README.md for the
|
|
21
|
+
* repo-interaction model (library here, agents declared in app repos,
|
|
22
|
+
* integration/tenant config in the infra repo).
|
|
23
|
+
*/
|
|
24
|
+
__exportStar(require("./resources"), exports);
|
|
25
|
+
__exportStar(require("./connection"), exports);
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingram Cloud /v1 tenant configuration as Pulumi dynamic resources.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the per-app `scripts/register-*.ts` / `ensure-agent.ts` one-shots
|
|
5
|
+
* with declarative resources, so IC wiring is a `pulumi up` and lives in state
|
|
6
|
+
* instead of drifting in someone's shell history.
|
|
7
|
+
*
|
|
8
|
+
* Each resource talks to the IC REST API. `token` is the tenant-admin bearer;
|
|
9
|
+
* keep it (and any credential input) a stack secret — it's marked as an
|
|
10
|
+
* additional secret output so it's encrypted in state.
|
|
11
|
+
*
|
|
12
|
+
* Where each resource is meant to be declared:
|
|
13
|
+
* - **Agents** (`IcAgent`) — the `instructions` are app-owned *content*,
|
|
14
|
+
* so declare these in the APP repo's Pulumi program, sourced from the app's
|
|
15
|
+
* own spec module. Never copy prompts into the infra repo.
|
|
16
|
+
* - **Integrations / tenant config** (`IcMcpServer`, `IcOauthProvider`,
|
|
17
|
+
* `IcTelegramBot`, `IcWhatsAppConfig`, `IcWebhook`, `IcModelKey`) —
|
|
18
|
+
* tenant-level wiring; declare in the infra repo alongside DNS/DB/Vercel.
|
|
19
|
+
*
|
|
20
|
+
* Not modelled here, on purpose:
|
|
21
|
+
* - **Per-end-user actions** — creating users, binding deployments, pushing
|
|
22
|
+
* per-user connection tokens, minting user tokens — happen at app runtime.
|
|
23
|
+
* - **Attaching existing smiths to an agent** — a one-time backfill that
|
|
24
|
+
* mutates the live fleet; keep it as the app's migration script. New
|
|
25
|
+
* smiths attach at birth in app code.
|
|
26
|
+
* - **Slack app factory** — config-token rotation is stateful and app-driven.
|
|
27
|
+
*/
|
|
28
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
29
|
+
/**
|
|
30
|
+
* The /v1 API version this library targets. Bumping the IC API contract is a
|
|
31
|
+
* library version bump — consumers pin behaviour by pinning this package.
|
|
32
|
+
*/
|
|
33
|
+
export declare const IC_API_VERSION = "2026-05-01";
|
|
34
|
+
export interface AgentVariable {
|
|
35
|
+
name: string;
|
|
36
|
+
default?: string | null;
|
|
37
|
+
description?: string | null;
|
|
38
|
+
required?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface IcAgentArgs {
|
|
41
|
+
baseUrl: pulumi.Input<string>;
|
|
42
|
+
token: pulumi.Input<string>;
|
|
43
|
+
/**
|
|
44
|
+
* Immutable, tenant-unique reconcile key (`[a-z0-9-]`). This is what the
|
|
45
|
+
* resource adopts on across runs. Defaults to the Pulumi resource name
|
|
46
|
+
* (slugified); set it explicitly to decouple the key from the resource name.
|
|
47
|
+
* Changing it replaces the agent.
|
|
48
|
+
*/
|
|
49
|
+
slug?: pulumi.Input<string>;
|
|
50
|
+
/** Free display label (mutable, not unique). Defaults to the slug. */
|
|
51
|
+
name?: pulumi.Input<string>;
|
|
52
|
+
/** The persona/instruction template (may contain `{{ variables }}`). */
|
|
53
|
+
instructions?: pulumi.Input<string>;
|
|
54
|
+
model?: pulumi.Input<string>;
|
|
55
|
+
enabledHostedTools?: pulumi.Input<string[]>;
|
|
56
|
+
autoMemory?: pulumi.Input<boolean>;
|
|
57
|
+
/** Opt into observational-memory consolidation (background, billed). Default off. */
|
|
58
|
+
memoryConsolidation?: pulumi.Input<boolean>;
|
|
59
|
+
variables?: pulumi.Input<AgentVariable[]>;
|
|
60
|
+
/** Release note recorded when a new version is published. */
|
|
61
|
+
publishNote?: pulumi.Input<string>;
|
|
62
|
+
/** Rollout percentage for the published version (default 100). */
|
|
63
|
+
rolloutPercent?: pulumi.Input<number>;
|
|
64
|
+
}
|
|
65
|
+
export declare class IcAgent extends pulumi.dynamic.Resource {
|
|
66
|
+
/** The `agt_…` id (same as `.id`, exposed for convenience). */
|
|
67
|
+
readonly agentId: pulumi.Output<string>;
|
|
68
|
+
/** The version number currently rolled out. */
|
|
69
|
+
readonly activeVersion: pulumi.Output<number>;
|
|
70
|
+
constructor(name: string, args: IcAgentArgs, opts?: pulumi.CustomResourceOptions);
|
|
71
|
+
}
|
|
72
|
+
export interface IcMcpServerArgs {
|
|
73
|
+
baseUrl: pulumi.Input<string>;
|
|
74
|
+
token: pulumi.Input<string>;
|
|
75
|
+
/** Registry name — the `{name}` in `/v1/tenant/mcp/{name}`. */
|
|
76
|
+
serverName: pulumi.Input<string>;
|
|
77
|
+
/** Server URL. Optional when `catalog` is set (the preset supplies it). */
|
|
78
|
+
url?: pulumi.Input<string>;
|
|
79
|
+
/** Stamp from a curated catalog preset (e.g. "stripe"); copied down at enable. */
|
|
80
|
+
catalog?: pulumi.Input<string>;
|
|
81
|
+
authKind?: pulumi.Input<string>;
|
|
82
|
+
authProvider?: pulumi.Input<string>;
|
|
83
|
+
/** oauth only: "tenant" (your own OAuth client) | "platform" (Ingram-owned). */
|
|
84
|
+
clientMode?: pulumi.Input<string>;
|
|
85
|
+
secret?: pulumi.Input<string>;
|
|
86
|
+
/** Default-deny: only these tool names reach a run. Omit to expose all discovered. */
|
|
87
|
+
toolAllowlist?: pulumi.Input<string[]>;
|
|
88
|
+
/** IC-side approval gating by tool-name glob, e.g. `[{ match: "create_refund" }]`. */
|
|
89
|
+
approvalPolicy?: pulumi.Input<{
|
|
90
|
+
match: string;
|
|
91
|
+
require?: string;
|
|
92
|
+
}[]>;
|
|
93
|
+
}
|
|
94
|
+
export declare class IcMcpServer extends pulumi.dynamic.Resource {
|
|
95
|
+
readonly toolsDiscovered: pulumi.Output<number>;
|
|
96
|
+
readonly discoveryError: pulumi.Output<string | undefined>;
|
|
97
|
+
constructor(name: string, args: IcMcpServerArgs, opts?: pulumi.CustomResourceOptions);
|
|
98
|
+
}
|
|
99
|
+
export interface IcTelegramBotArgs {
|
|
100
|
+
baseUrl: pulumi.Input<string>;
|
|
101
|
+
token: pulumi.Input<string>;
|
|
102
|
+
botToken: pulumi.Input<string>;
|
|
103
|
+
}
|
|
104
|
+
export declare class IcTelegramBot extends pulumi.dynamic.Resource {
|
|
105
|
+
readonly botUsername: pulumi.Output<string>;
|
|
106
|
+
readonly webhookUrl: pulumi.Output<string>;
|
|
107
|
+
constructor(name: string, args: IcTelegramBotArgs, opts?: pulumi.CustomResourceOptions);
|
|
108
|
+
}
|
|
109
|
+
export interface IcOauthProviderArgs {
|
|
110
|
+
baseUrl: pulumi.Input<string>;
|
|
111
|
+
token: pulumi.Input<string>;
|
|
112
|
+
/** Provider key — the `{provider}` in `/v1/tenant/providers/{provider}`;
|
|
113
|
+
* what an oauth-kind `IcMcpServer` names via `authProvider`. */
|
|
114
|
+
provider: pulumi.Input<string>;
|
|
115
|
+
/** Webhook-delegation mode: IC POSTs `{ connection_id }` here when a stored
|
|
116
|
+
* token nears expiry and the tenant PATCHes a fresh pair back. Set this
|
|
117
|
+
* WITHOUT `clientSecret` (a stored secret switches IC to IC-driven mode). */
|
|
118
|
+
refreshWebhook?: pulumi.Input<string>;
|
|
119
|
+
clientId?: pulumi.Input<string>;
|
|
120
|
+
/** IC-driven refresh mode: IC exchanges the refresh token at `tokenUri`. */
|
|
121
|
+
clientSecret?: pulumi.Input<string>;
|
|
122
|
+
tokenUri?: pulumi.Input<string>;
|
|
123
|
+
scopesAllowed?: pulumi.Input<string[]>;
|
|
124
|
+
}
|
|
125
|
+
export declare class IcOauthProvider extends pulumi.dynamic.Resource {
|
|
126
|
+
constructor(name: string, args: IcOauthProviderArgs, opts?: pulumi.CustomResourceOptions);
|
|
127
|
+
}
|
|
128
|
+
export interface IcWhatsAppConfigArgs {
|
|
129
|
+
baseUrl: pulumi.Input<string>;
|
|
130
|
+
token: pulumi.Input<string>;
|
|
131
|
+
phoneNumberId: pulumi.Input<string>;
|
|
132
|
+
accessToken: pulumi.Input<string>;
|
|
133
|
+
/** The WhatsApp Business Account id — IC subscribes its app to it so inbound flows. */
|
|
134
|
+
wabaId: pulumi.Input<string>;
|
|
135
|
+
}
|
|
136
|
+
export declare class IcWhatsAppConfig extends pulumi.dynamic.Resource {
|
|
137
|
+
readonly displayPhoneNumber: pulumi.Output<string>;
|
|
138
|
+
readonly webhookUrl: pulumi.Output<string>;
|
|
139
|
+
constructor(name: string, args: IcWhatsAppConfigArgs, opts?: pulumi.CustomResourceOptions);
|
|
140
|
+
}
|
|
141
|
+
export interface IcEmailArgs {
|
|
142
|
+
baseUrl: pulumi.Input<string>;
|
|
143
|
+
token: pulumi.Input<string>;
|
|
144
|
+
/** Cloudflare account that owns the sending domain + Email-Sending token. */
|
|
145
|
+
cloudflareAccountId: pulumi.Input<string>;
|
|
146
|
+
/** Email-Sending-scoped Cloudflare API token (secret). */
|
|
147
|
+
cloudflareApiToken: pulumi.Input<string>;
|
|
148
|
+
/** The verified sending domain (e.g. `mail.thornhill.app`). */
|
|
149
|
+
fromDomain: pulumi.Input<string>;
|
|
150
|
+
/** Optional friendly From name. */
|
|
151
|
+
displayName?: pulumi.Input<string>;
|
|
152
|
+
/** Optional HMAC for inbound-worker verification; IC generates one if omitted. */
|
|
153
|
+
inboundSecret?: pulumi.Input<string>;
|
|
154
|
+
}
|
|
155
|
+
export declare class IcEmail extends pulumi.dynamic.Resource {
|
|
156
|
+
readonly inboundSecret: pulumi.Output<string>;
|
|
157
|
+
readonly inboundUrl: pulumi.Output<string>;
|
|
158
|
+
readonly configured: pulumi.Output<boolean>;
|
|
159
|
+
constructor(name: string, args: IcEmailArgs, opts?: pulumi.CustomResourceOptions);
|
|
160
|
+
}
|
|
161
|
+
export interface IcWebhookArgs {
|
|
162
|
+
baseUrl: pulumi.Input<string>;
|
|
163
|
+
token: pulumi.Input<string>;
|
|
164
|
+
url: pulumi.Input<string>;
|
|
165
|
+
events: pulumi.Input<string[]>;
|
|
166
|
+
}
|
|
167
|
+
export declare class IcWebhook extends pulumi.dynamic.Resource {
|
|
168
|
+
/** The signing secret — present only when Pulumi created the webhook. */
|
|
169
|
+
readonly secret: pulumi.Output<string | undefined>;
|
|
170
|
+
constructor(name: string, args: IcWebhookArgs, opts?: pulumi.CustomResourceOptions);
|
|
171
|
+
}
|
|
172
|
+
export interface IcModelKeyArgs {
|
|
173
|
+
baseUrl: pulumi.Input<string>;
|
|
174
|
+
token: pulumi.Input<string>;
|
|
175
|
+
/** Provider id, e.g. `openai`, `anthropic`. */
|
|
176
|
+
provider: pulumi.Input<string>;
|
|
177
|
+
apiKey: pulumi.Input<string>;
|
|
178
|
+
/** Optional override base URL (OpenAI-compatible backends). */
|
|
179
|
+
providerBaseUrl?: pulumi.Input<string>;
|
|
180
|
+
}
|
|
181
|
+
export declare class IcModelKey extends pulumi.dynamic.Resource {
|
|
182
|
+
constructor(name: string, args: IcModelKeyArgs, opts?: pulumi.CustomResourceOptions);
|
|
183
|
+
}
|
|
184
|
+
export interface IcProjectArgs {
|
|
185
|
+
baseUrl: pulumi.Input<string>;
|
|
186
|
+
/** An **organization** key (`organization:*`), NOT a project token. */
|
|
187
|
+
token: pulumi.Input<string>;
|
|
188
|
+
/** Adopt-by-name reconcile key (defaults to the Pulumi resource name). */
|
|
189
|
+
projectName?: pulumi.Input<string>;
|
|
190
|
+
}
|
|
191
|
+
export declare class IcProject extends pulumi.dynamic.Resource {
|
|
192
|
+
/** The project id — this is the TENANT a project token is bound to. */
|
|
193
|
+
readonly projectId: pulumi.Output<string>;
|
|
194
|
+
constructor(name: string, args: IcProjectArgs, opts?: pulumi.CustomResourceOptions);
|
|
195
|
+
}
|
|
196
|
+
export interface IcProjectTokenArgs {
|
|
197
|
+
baseUrl: pulumi.Input<string>;
|
|
198
|
+
/** An **organization** key (`organization:*`), NOT a project token. */
|
|
199
|
+
token: pulumi.Input<string>;
|
|
200
|
+
/** The project id to mint for — typically an `IcProject().projectId`. */
|
|
201
|
+
project: pulumi.Input<string>;
|
|
202
|
+
tokenName?: pulumi.Input<string>;
|
|
203
|
+
ttlSeconds?: pulumi.Input<number>;
|
|
204
|
+
}
|
|
205
|
+
export declare class IcProjectToken extends pulumi.dynamic.Resource {
|
|
206
|
+
/** The minted `tenant:*` token (secret) — feed this into the app's env. */
|
|
207
|
+
readonly projectToken: pulumi.Output<string>;
|
|
208
|
+
readonly tokenId: pulumi.Output<string>;
|
|
209
|
+
constructor(name: string, args: IcProjectTokenArgs, opts?: pulumi.CustomResourceOptions);
|
|
210
|
+
}
|
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
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;
|
|
37
|
+
/**
|
|
38
|
+
* Ingram Cloud /v1 tenant configuration as Pulumi dynamic resources.
|
|
39
|
+
*
|
|
40
|
+
* Replaces the per-app `scripts/register-*.ts` / `ensure-agent.ts` one-shots
|
|
41
|
+
* with declarative resources, so IC wiring is a `pulumi up` and lives in state
|
|
42
|
+
* instead of drifting in someone's shell history.
|
|
43
|
+
*
|
|
44
|
+
* Each resource talks to the IC REST API. `token` is the tenant-admin bearer;
|
|
45
|
+
* keep it (and any credential input) a stack secret — it's marked as an
|
|
46
|
+
* additional secret output so it's encrypted in state.
|
|
47
|
+
*
|
|
48
|
+
* Where each resource is meant to be declared:
|
|
49
|
+
* - **Agents** (`IcAgent`) — the `instructions` are app-owned *content*,
|
|
50
|
+
* so declare these in the APP repo's Pulumi program, sourced from the app's
|
|
51
|
+
* own spec module. Never copy prompts into the infra repo.
|
|
52
|
+
* - **Integrations / tenant config** (`IcMcpServer`, `IcOauthProvider`,
|
|
53
|
+
* `IcTelegramBot`, `IcWhatsAppConfig`, `IcWebhook`, `IcModelKey`) —
|
|
54
|
+
* tenant-level wiring; declare in the infra repo alongside DNS/DB/Vercel.
|
|
55
|
+
*
|
|
56
|
+
* Not modelled here, on purpose:
|
|
57
|
+
* - **Per-end-user actions** — creating users, binding deployments, pushing
|
|
58
|
+
* per-user connection tokens, minting user tokens — happen at app runtime.
|
|
59
|
+
* - **Attaching existing smiths to an agent** — a one-time backfill that
|
|
60
|
+
* mutates the live fleet; keep it as the app's migration script. New
|
|
61
|
+
* smiths attach at birth in app code.
|
|
62
|
+
* - **Slack app factory** — config-token rotation is stateful and app-driven.
|
|
63
|
+
*/
|
|
64
|
+
const pulumi = __importStar(require("@pulumi/pulumi"));
|
|
65
|
+
/**
|
|
66
|
+
* The /v1 API version this library targets. Bumping the IC API contract is a
|
|
67
|
+
* library version bump — consumers pin behaviour by pinning this package.
|
|
68
|
+
*/
|
|
69
|
+
exports.IC_API_VERSION = "2026-05-01";
|
|
70
|
+
// Shared IC REST call. Module-scope so Pulumi's closure serialization captures
|
|
71
|
+
// it into each provider's methods; uses only the global `fetch` (Node ≥18).
|
|
72
|
+
async function icRequest(baseUrl, token, method, path, body, idempotencyKey) {
|
|
73
|
+
const headers = {
|
|
74
|
+
Authorization: `Bearer ${token}`,
|
|
75
|
+
"IC-Api-Version": exports.IC_API_VERSION,
|
|
76
|
+
};
|
|
77
|
+
if (body !== undefined)
|
|
78
|
+
headers["Content-Type"] = "application/json";
|
|
79
|
+
if (idempotencyKey)
|
|
80
|
+
headers["Idempotency-Key"] = idempotencyKey;
|
|
81
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
82
|
+
method,
|
|
83
|
+
headers,
|
|
84
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
85
|
+
});
|
|
86
|
+
const text = await res.text();
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
// Carry the status so `read` can tell "gone" (404) from a transient blip.
|
|
89
|
+
const err = new Error(`IC ${method} ${path} → ${res.status}: ${text.slice(0, 400)}`);
|
|
90
|
+
err.status = res.status;
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
return text ? JSON.parse(text) : {};
|
|
94
|
+
}
|
|
95
|
+
// Shared `diff` for the CRUD-scaffold providers: compare a fixed set of keys and
|
|
96
|
+
// build the Pulumi DiffResult. `replaceKeys` force a replace when changed;
|
|
97
|
+
// `scalarKeys` are in-place by reference equality; `listKeys` are in-place but
|
|
98
|
+
// compared by value (JSON). Each provider keeps its exact key set and its
|
|
99
|
+
// `deleteBeforeReplace` choice — this only removes the hand-rolled boilerplate.
|
|
100
|
+
function simpleDiff(olds, news, opts) {
|
|
101
|
+
const replaces = (opts.replaceKeys ?? []).filter((k) => olds[k] !== news[k]);
|
|
102
|
+
const scalarChanged = (opts.scalarKeys ?? []).some((k) => olds[k] !== news[k]);
|
|
103
|
+
const listChanged = (opts.listKeys ?? []).some((k) => JSON.stringify(olds[k] ?? null) !== JSON.stringify(news[k] ?? null));
|
|
104
|
+
return {
|
|
105
|
+
changes: replaces.length > 0 || scalarChanged || listChanged,
|
|
106
|
+
replaces,
|
|
107
|
+
deleteBeforeReplace: opts.deleteBeforeReplace,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// Shared `read` 404 handling: run a fetch, map a server-side 404 to `{ id: "" }`
|
|
111
|
+
// (so refresh prunes the resource and the next `up` recreates it) and rethrow any
|
|
112
|
+
// other error so a transient blip can't nuke state.
|
|
113
|
+
async function read404(fetchFn, onFound) {
|
|
114
|
+
try {
|
|
115
|
+
return onFound(await fetchFn());
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
if (e?.status === 404)
|
|
119
|
+
return { id: "" };
|
|
120
|
+
throw e;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Factory for the PUT-style providers (mcp, telegram, whatsapp, modelKey): they
|
|
124
|
+
// all reconcile by PUTting the desired body to a resource path and DELETE the
|
|
125
|
+
// same path to tear down. They differ only in (a) the path — a fixed singleton
|
|
126
|
+
// vs one keyed by an input field — (b) the request body, (c) which response
|
|
127
|
+
// fields become outputs, and (d) the diff key sets — all passed in here.
|
|
128
|
+
function makePutResource(cfg) {
|
|
129
|
+
const put = async (i) => {
|
|
130
|
+
const res = await icRequest(i.baseUrl, i.token, "PUT", cfg.path(i), cfg.body(i));
|
|
131
|
+
return { ...i, ...(cfg.outs ? cfg.outs(i, res) : {}) };
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
async create(i) {
|
|
135
|
+
return { id: cfg.id(i), outs: await put(i) };
|
|
136
|
+
},
|
|
137
|
+
async update(_id, _olds, news) {
|
|
138
|
+
return { outs: await put(news) };
|
|
139
|
+
},
|
|
140
|
+
async delete(_id, props) {
|
|
141
|
+
await icRequest(props.baseUrl, props.token, "DELETE", cfg.path(props));
|
|
142
|
+
},
|
|
143
|
+
async diff(_id, olds, news) {
|
|
144
|
+
return simpleDiff(olds, news, cfg.diff);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// Lower-case, hyphenate, trim — matches the API's slug derivation so a
|
|
149
|
+
// logical-name-derived slug lines up with what the server would compute.
|
|
150
|
+
function slugify(s) {
|
|
151
|
+
return (s
|
|
152
|
+
.toLowerCase()
|
|
153
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
154
|
+
.replace(/^-+|-+$/g, "")
|
|
155
|
+
.slice(0, 63) || "agent");
|
|
156
|
+
}
|
|
157
|
+
// The draft body IC's create/patch expect (snake_case wire shape). `slug` is
|
|
158
|
+
// honoured on create and ignored on patch (it's immutable), so the display
|
|
159
|
+
// `name` defaults to the slug when none is given.
|
|
160
|
+
function agentBody(i) {
|
|
161
|
+
return {
|
|
162
|
+
slug: i.slug,
|
|
163
|
+
name: i.name ?? i.slug,
|
|
164
|
+
instructions: i.instructions ?? null,
|
|
165
|
+
model: i.model ?? null,
|
|
166
|
+
enabled_hosted_tools: i.enabledHostedTools ?? [],
|
|
167
|
+
auto_memory: i.autoMemory ?? null,
|
|
168
|
+
memory_consolidation: i.memoryConsolidation ?? null,
|
|
169
|
+
variables: i.variables ?? [],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// Stable signature of the *content* that warrants a new published version
|
|
173
|
+
// (name and rollout% deliberately excluded — a rename/restage is not new content).
|
|
174
|
+
function agentContentSig(s) {
|
|
175
|
+
return JSON.stringify({
|
|
176
|
+
instructions: s.instructions ?? null,
|
|
177
|
+
model: s.model ?? null,
|
|
178
|
+
tools: [...(s.enabled_hosted_tools ?? [])].sort(),
|
|
179
|
+
auto_memory: s.auto_memory ?? null,
|
|
180
|
+
memory_consolidation: s.memory_consolidation ?? null,
|
|
181
|
+
variables: s.variables ?? [],
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// Map the camelCase resource inputs onto the canonical snapshot shape.
|
|
185
|
+
function inputsToSnapshot(i) {
|
|
186
|
+
return {
|
|
187
|
+
instructions: i.instructions,
|
|
188
|
+
model: i.model,
|
|
189
|
+
enabled_hosted_tools: i.enabledHostedTools,
|
|
190
|
+
auto_memory: i.autoMemory,
|
|
191
|
+
memory_consolidation: i.memoryConsolidation,
|
|
192
|
+
variables: i.variables,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async function findAgentBySlug(baseUrl, token, slug) {
|
|
196
|
+
const res = await icRequest(baseUrl, token, "GET", "/v1/agents");
|
|
197
|
+
return (res.data ?? []).find((b) => b.slug === slug) ?? null;
|
|
198
|
+
}
|
|
199
|
+
// Signature of the snapshot currently published as the active version, or null
|
|
200
|
+
// if nothing is published yet.
|
|
201
|
+
async function activeSnapshotSig(baseUrl, token, id, activeVersion) {
|
|
202
|
+
if (!activeVersion)
|
|
203
|
+
return null;
|
|
204
|
+
const res = await icRequest(baseUrl, token, "GET", `/v1/agents/${id}/versions`);
|
|
205
|
+
const v = (res.data ?? []).find((x) => x.version === activeVersion);
|
|
206
|
+
if (!v)
|
|
207
|
+
return null;
|
|
208
|
+
// The version's `snapshot` is already the canonical snake_case shape.
|
|
209
|
+
return agentContentSig(v.snapshot ?? {});
|
|
210
|
+
}
|
|
211
|
+
// Ensure the live agent matches desired: adopt-or-create, publish iff the
|
|
212
|
+
// content drifted from the active version, then roll out at `percent`.
|
|
213
|
+
async function reconcileAgent(i, knownId, priorSig) {
|
|
214
|
+
let id;
|
|
215
|
+
if (!knownId) {
|
|
216
|
+
const existing = await findAgentBySlug(i.baseUrl, i.token, i.slug);
|
|
217
|
+
if (existing) {
|
|
218
|
+
id = existing.id;
|
|
219
|
+
await icRequest(i.baseUrl, i.token, "PATCH", `/v1/agents/${id}`, agentBody(i));
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
// No idempotency key: Pulumi calls create once and tracks the id in
|
|
223
|
+
// state, and the adopt-by-slug branch above covers re-runs and the
|
|
224
|
+
// script→Pulumi migration. Keying on the slug would replay a stale
|
|
225
|
+
// (possibly archived) create after a destroy+recreate within the 24h
|
|
226
|
+
// idempotency window.
|
|
227
|
+
const bp = await icRequest(i.baseUrl, i.token, "POST", "/v1/agents", agentBody(i));
|
|
228
|
+
id = bp.id;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
// Known id (update path): push the draft to the desired shape.
|
|
233
|
+
id = knownId;
|
|
234
|
+
await icRequest(i.baseUrl, i.token, "PATCH", `/v1/agents/${id}`, agentBody(i));
|
|
235
|
+
}
|
|
236
|
+
const bp = await icRequest(i.baseUrl, i.token, "GET", `/v1/agents/${id}`);
|
|
237
|
+
const desiredSig = agentContentSig(inputsToSnapshot(i));
|
|
238
|
+
// On the create path we don't know the prior sig — compare to what's actually
|
|
239
|
+
// published so adopting a script-made agent doesn't churn a needless version.
|
|
240
|
+
const liveSig = priorSig ??
|
|
241
|
+
(await activeSnapshotSig(i.baseUrl, i.token, id, bp.active_version));
|
|
242
|
+
let version = bp.active_version ?? 0;
|
|
243
|
+
if (!bp.active_version || liveSig !== desiredSig) {
|
|
244
|
+
// Publishing snapshots the draft as the next immutable version and returns
|
|
245
|
+
// its number. Only the *first* publish auto-activates; later ones go live
|
|
246
|
+
// only via the rollout below — so take the version from the publish reply,
|
|
247
|
+
// not from active_version (which hasn't advanced yet).
|
|
248
|
+
const pub = await icRequest(i.baseUrl, i.token, "POST", `/v1/agents/${id}/versions`, {
|
|
249
|
+
note: i.publishNote ?? null,
|
|
250
|
+
});
|
|
251
|
+
version = pub.version;
|
|
252
|
+
}
|
|
253
|
+
// Always assert the rollout: it activates a freshly published version and is
|
|
254
|
+
// an idempotent no-op when `version` is already the active one at `percent`.
|
|
255
|
+
const percent = i.rolloutPercent ?? 100;
|
|
256
|
+
await icRequest(i.baseUrl, i.token, "POST", `/v1/agents/${id}/rollout`, {
|
|
257
|
+
version,
|
|
258
|
+
percent,
|
|
259
|
+
});
|
|
260
|
+
return {
|
|
261
|
+
...i,
|
|
262
|
+
agentId: id,
|
|
263
|
+
contentSig: desiredSig,
|
|
264
|
+
activeVersion: version,
|
|
265
|
+
rolloutPercent: percent,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const agentProvider = {
|
|
269
|
+
async create(i) {
|
|
270
|
+
const outs = await reconcileAgent(i);
|
|
271
|
+
return { id: outs.agentId, outs };
|
|
272
|
+
},
|
|
273
|
+
async update(id, olds, news) {
|
|
274
|
+
const outs = await reconcileAgent(news, id, olds.contentSig);
|
|
275
|
+
return { outs };
|
|
276
|
+
},
|
|
277
|
+
async delete(id, props) {
|
|
278
|
+
try {
|
|
279
|
+
await icRequest(props.baseUrl, props.token, "DELETE", `/v1/agents/${id}`);
|
|
280
|
+
}
|
|
281
|
+
catch (e) {
|
|
282
|
+
// A 409 is the API's `agent_in_use` precondition: an agent can't be archived
|
|
283
|
+
// while smiths still run it. Surface that as an actionable message; the API's
|
|
284
|
+
// own error (carrying the smith count) is appended. Any other error propagates
|
|
285
|
+
// unwrapped.
|
|
286
|
+
if (e?.status === 409)
|
|
287
|
+
throw new Error(`Agent ${id} can't be archived while smiths still run it. Delete or ` +
|
|
288
|
+
`re-point those smiths first, or set { retainOnDelete: true } on the ` +
|
|
289
|
+
`resource to leave the agent in place. ${e}`);
|
|
290
|
+
throw e;
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
async diff(_id, olds, news) {
|
|
294
|
+
// slug is the immutable reconcile key — changing it is a replace, not an
|
|
295
|
+
// update (the old and new slugs differ, so create-before-delete is safe).
|
|
296
|
+
if (olds.slug !== news.slug)
|
|
297
|
+
return { changes: true, replaces: ["slug"] };
|
|
298
|
+
const contentChanged = agentContentSig(inputsToSnapshot(olds)) !==
|
|
299
|
+
agentContentSig(inputsToSnapshot(news));
|
|
300
|
+
const nameChanged = (olds.name ?? olds.slug) !== (news.name ?? news.slug);
|
|
301
|
+
const rolloutChanged = (olds.rolloutPercent ?? 100) !== (news.rolloutPercent ?? 100);
|
|
302
|
+
return { changes: contentChanged || nameChanged || rolloutChanged };
|
|
303
|
+
},
|
|
304
|
+
// Adopt a agent a script already created (`pulumi import <id>`).
|
|
305
|
+
async read(id, props) {
|
|
306
|
+
return read404(() => icRequest(props.baseUrl, props.token, "GET", `/v1/agents/${id}`), (bp) => {
|
|
307
|
+
if (!bp || !bp.id)
|
|
308
|
+
return { id: "" };
|
|
309
|
+
const d = bp.draft ?? {};
|
|
310
|
+
return {
|
|
311
|
+
id,
|
|
312
|
+
props: {
|
|
313
|
+
...props,
|
|
314
|
+
slug: bp.slug ?? props.slug,
|
|
315
|
+
name: bp.name,
|
|
316
|
+
instructions: d.instructions ?? undefined,
|
|
317
|
+
model: d.model ?? undefined,
|
|
318
|
+
enabledHostedTools: d.enabled_hosted_tools ?? [],
|
|
319
|
+
autoMemory: d.auto_memory ?? undefined,
|
|
320
|
+
memoryConsolidation: d.memory_consolidation ?? undefined,
|
|
321
|
+
variables: d.variables ?? [],
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
class IcAgent extends pulumi.dynamic.Resource {
|
|
328
|
+
constructor(name, args, opts) {
|
|
329
|
+
super(agentProvider, name, {
|
|
330
|
+
instructions: undefined,
|
|
331
|
+
model: undefined,
|
|
332
|
+
enabledHostedTools: undefined,
|
|
333
|
+
autoMemory: undefined,
|
|
334
|
+
memoryConsolidation: undefined,
|
|
335
|
+
variables: undefined,
|
|
336
|
+
publishNote: undefined,
|
|
337
|
+
rolloutPercent: undefined,
|
|
338
|
+
agentId: undefined,
|
|
339
|
+
activeVersion: undefined,
|
|
340
|
+
contentSig: undefined,
|
|
341
|
+
...args,
|
|
342
|
+
// Default the reconcile key to the (stable) resource name when the
|
|
343
|
+
// caller doesn't pin one — kept after ...args so it always resolves.
|
|
344
|
+
slug: args.slug ?? slugify(name),
|
|
345
|
+
}, { ...opts, additionalSecretOutputs: ["token"] });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
exports.IcAgent = IcAgent;
|
|
349
|
+
function mcpBody(i) {
|
|
350
|
+
const auth = { kind: i.authKind };
|
|
351
|
+
if (i.authProvider)
|
|
352
|
+
auth.provider = i.authProvider;
|
|
353
|
+
if (i.clientMode)
|
|
354
|
+
auth.client_mode = i.clientMode;
|
|
355
|
+
if (i.secret)
|
|
356
|
+
auth.secret = i.secret;
|
|
357
|
+
const body = { auth };
|
|
358
|
+
if (i.url)
|
|
359
|
+
body.url = i.url;
|
|
360
|
+
if (i.catalog)
|
|
361
|
+
body.catalog = i.catalog;
|
|
362
|
+
if (i.toolAllowlist !== undefined)
|
|
363
|
+
body.tool_allowlist = i.toolAllowlist;
|
|
364
|
+
if (i.approvalPolicy !== undefined)
|
|
365
|
+
body.approval_policy = i.approvalPolicy;
|
|
366
|
+
return body;
|
|
367
|
+
}
|
|
368
|
+
const mcpProvider = makePutResource({
|
|
369
|
+
path: (i) => `/v1/tenant/mcp/${encodeURIComponent(i.serverName)}`,
|
|
370
|
+
id: (i) => i.serverName,
|
|
371
|
+
body: mcpBody,
|
|
372
|
+
outs: (_i, res) => ({
|
|
373
|
+
toolsDiscovered: res.tools_discovered ?? 0,
|
|
374
|
+
discoveryError: res.discovery_error ?? null,
|
|
375
|
+
}),
|
|
376
|
+
diff: {
|
|
377
|
+
replaceKeys: ["serverName"],
|
|
378
|
+
scalarKeys: [
|
|
379
|
+
"url",
|
|
380
|
+
"catalog",
|
|
381
|
+
"authKind",
|
|
382
|
+
"authProvider",
|
|
383
|
+
"clientMode",
|
|
384
|
+
"secret",
|
|
385
|
+
"token",
|
|
386
|
+
"baseUrl",
|
|
387
|
+
],
|
|
388
|
+
// Arrays compare by value, not reference.
|
|
389
|
+
listKeys: ["toolAllowlist", "approvalPolicy"],
|
|
390
|
+
deleteBeforeReplace: true,
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
class IcMcpServer extends pulumi.dynamic.Resource {
|
|
394
|
+
constructor(name, args, opts) {
|
|
395
|
+
super(mcpProvider, name, {
|
|
396
|
+
authKind: args.catalog ? undefined : "static",
|
|
397
|
+
url: undefined,
|
|
398
|
+
catalog: undefined,
|
|
399
|
+
authProvider: undefined,
|
|
400
|
+
clientMode: undefined,
|
|
401
|
+
secret: undefined,
|
|
402
|
+
toolAllowlist: undefined,
|
|
403
|
+
approvalPolicy: undefined,
|
|
404
|
+
toolsDiscovered: undefined,
|
|
405
|
+
discoveryError: undefined,
|
|
406
|
+
...args,
|
|
407
|
+
}, { ...opts, additionalSecretOutputs: ["token", "secret"] });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
exports.IcMcpServer = IcMcpServer;
|
|
411
|
+
const telegramProvider = makePutResource({
|
|
412
|
+
path: () => "/v1/tenant/telegram",
|
|
413
|
+
id: () => "telegram",
|
|
414
|
+
body: (i) => ({ bot_token: i.botToken }),
|
|
415
|
+
outs: (_i, res) => ({
|
|
416
|
+
botUsername: res.bot_username,
|
|
417
|
+
botId: res.bot_id,
|
|
418
|
+
webhookUrl: res.webhook_url,
|
|
419
|
+
}),
|
|
420
|
+
diff: { scalarKeys: ["botToken", "token", "baseUrl"] },
|
|
421
|
+
});
|
|
422
|
+
class IcTelegramBot extends pulumi.dynamic.Resource {
|
|
423
|
+
constructor(name, args, opts) {
|
|
424
|
+
super(telegramProvider, name, {
|
|
425
|
+
botUsername: undefined,
|
|
426
|
+
botId: undefined,
|
|
427
|
+
webhookUrl: undefined,
|
|
428
|
+
...args,
|
|
429
|
+
}, { ...opts, additionalSecretOutputs: ["token", "botToken"] });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
exports.IcTelegramBot = IcTelegramBot;
|
|
433
|
+
const oauthProviderResource = makePutResource({
|
|
434
|
+
path: (i) => `/v1/tenant/providers/${encodeURIComponent(i.provider)}`,
|
|
435
|
+
id: (i) => i.provider,
|
|
436
|
+
body: (i) => ({
|
|
437
|
+
refresh_webhook: i.refreshWebhook,
|
|
438
|
+
client_id: i.clientId,
|
|
439
|
+
// Key presence is meaningful on the wire: an omitted `client_secret`
|
|
440
|
+
// keeps the stored one, a sent value (even null) replaces it. Only send
|
|
441
|
+
// the key when the input is set so a webhook-mode provider never
|
|
442
|
+
// clobbers— or phantom-clears — a secret.
|
|
443
|
+
...(i.clientSecret !== undefined ? { client_secret: i.clientSecret } : {}),
|
|
444
|
+
token_uri: i.tokenUri,
|
|
445
|
+
scopes_allowed: i.scopesAllowed,
|
|
446
|
+
}),
|
|
447
|
+
diff: {
|
|
448
|
+
replaceKeys: ["provider"],
|
|
449
|
+
scalarKeys: [
|
|
450
|
+
"refreshWebhook",
|
|
451
|
+
"clientId",
|
|
452
|
+
"clientSecret",
|
|
453
|
+
"tokenUri",
|
|
454
|
+
"token",
|
|
455
|
+
"baseUrl",
|
|
456
|
+
],
|
|
457
|
+
listKeys: ["scopesAllowed"],
|
|
458
|
+
deleteBeforeReplace: true,
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
class IcOauthProvider extends pulumi.dynamic.Resource {
|
|
462
|
+
constructor(name, args, opts) {
|
|
463
|
+
super(oauthProviderResource, name, {
|
|
464
|
+
refreshWebhook: undefined,
|
|
465
|
+
clientId: undefined,
|
|
466
|
+
clientSecret: undefined,
|
|
467
|
+
tokenUri: undefined,
|
|
468
|
+
scopesAllowed: undefined,
|
|
469
|
+
...args,
|
|
470
|
+
}, { ...opts, additionalSecretOutputs: ["token", "clientSecret"] });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
exports.IcOauthProvider = IcOauthProvider;
|
|
474
|
+
const whatsappProvider = makePutResource({
|
|
475
|
+
path: () => "/v1/tenant/whatsapp",
|
|
476
|
+
id: () => "whatsapp",
|
|
477
|
+
body: (i) => ({
|
|
478
|
+
phone_number_id: i.phoneNumberId,
|
|
479
|
+
access_token: i.accessToken,
|
|
480
|
+
waba_id: i.wabaId,
|
|
481
|
+
}),
|
|
482
|
+
outs: (_i, res) => ({
|
|
483
|
+
displayPhoneNumber: res.display_phone_number,
|
|
484
|
+
webhookUrl: res.webhook_url,
|
|
485
|
+
}),
|
|
486
|
+
diff: {
|
|
487
|
+
scalarKeys: ["phoneNumberId", "accessToken", "wabaId", "token", "baseUrl"],
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
class IcWhatsAppConfig extends pulumi.dynamic.Resource {
|
|
491
|
+
constructor(name, args, opts) {
|
|
492
|
+
super(whatsappProvider, name, {
|
|
493
|
+
displayPhoneNumber: undefined,
|
|
494
|
+
webhookUrl: undefined,
|
|
495
|
+
...args,
|
|
496
|
+
}, {
|
|
497
|
+
...opts,
|
|
498
|
+
additionalSecretOutputs: ["token", "accessToken"],
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
exports.IcWhatsAppConfig = IcWhatsAppConfig;
|
|
503
|
+
const emailProvider = makePutResource({
|
|
504
|
+
path: () => "/v1/tenant/email",
|
|
505
|
+
id: () => "email",
|
|
506
|
+
body: (i) => ({
|
|
507
|
+
cloudflare_account_id: i.cloudflareAccountId,
|
|
508
|
+
cloudflare_api_token: i.cloudflareApiToken,
|
|
509
|
+
from_domain: i.fromDomain,
|
|
510
|
+
display_name: i.displayName,
|
|
511
|
+
inbound_secret: i.inboundSecret,
|
|
512
|
+
}),
|
|
513
|
+
outs: (i, res) => ({
|
|
514
|
+
// IC generates inbound_secret when we don't supply one — capture it so it's
|
|
515
|
+
// stable in state (and wireable into the inbound worker).
|
|
516
|
+
inboundSecret: res.inbound_secret ?? i.inboundSecret,
|
|
517
|
+
inboundUrl: res.inbound_url,
|
|
518
|
+
configured: res.configured,
|
|
519
|
+
}),
|
|
520
|
+
// inboundSecret excluded — IC may generate it, so it's an output, not drift.
|
|
521
|
+
diff: {
|
|
522
|
+
scalarKeys: [
|
|
523
|
+
"cloudflareAccountId",
|
|
524
|
+
"cloudflareApiToken",
|
|
525
|
+
"fromDomain",
|
|
526
|
+
"displayName",
|
|
527
|
+
"token",
|
|
528
|
+
"baseUrl",
|
|
529
|
+
],
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
class IcEmail extends pulumi.dynamic.Resource {
|
|
533
|
+
constructor(name, args, opts) {
|
|
534
|
+
super(emailProvider, name, {
|
|
535
|
+
displayName: undefined,
|
|
536
|
+
inboundSecret: undefined,
|
|
537
|
+
inboundUrl: undefined,
|
|
538
|
+
configured: undefined,
|
|
539
|
+
...args,
|
|
540
|
+
}, {
|
|
541
|
+
...opts,
|
|
542
|
+
additionalSecretOutputs: [
|
|
543
|
+
"token",
|
|
544
|
+
"cloudflareApiToken",
|
|
545
|
+
"inboundSecret",
|
|
546
|
+
],
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
exports.IcEmail = IcEmail;
|
|
551
|
+
const webhookProvider = {
|
|
552
|
+
async create(i) {
|
|
553
|
+
const res = await icRequest(i.baseUrl, i.token, "POST", "/v1/tenant/webhooks", {
|
|
554
|
+
url: i.url,
|
|
555
|
+
events: i.events,
|
|
556
|
+
active: true,
|
|
557
|
+
});
|
|
558
|
+
return { id: res.id, outs: { ...i, secret: res.secret } };
|
|
559
|
+
},
|
|
560
|
+
async update(id, olds, news) {
|
|
561
|
+
await icRequest(news.baseUrl, news.token, "PATCH", `/v1/tenant/webhooks/${id}`, {
|
|
562
|
+
events: news.events,
|
|
563
|
+
});
|
|
564
|
+
// The signing secret survives an events PATCH on IC's side; carry the
|
|
565
|
+
// create-time output forward so an update never blanks consumers that
|
|
566
|
+
// flow `secret` into app config.
|
|
567
|
+
return { outs: { ...news, secret: olds.secret } };
|
|
568
|
+
},
|
|
569
|
+
async delete(id, props) {
|
|
570
|
+
await icRequest(props.baseUrl, props.token, "DELETE", `/v1/tenant/webhooks/${id}`);
|
|
571
|
+
},
|
|
572
|
+
async diff(_id, olds, news) {
|
|
573
|
+
const replaces = olds.url !== news.url ? ["url"] : [];
|
|
574
|
+
const eventsChanged = JSON.stringify([...(olds.events ?? [])].sort()) !==
|
|
575
|
+
JSON.stringify([...(news.events ?? [])].sort());
|
|
576
|
+
return { changes: replaces.length > 0 || eventsChanged, replaces };
|
|
577
|
+
},
|
|
578
|
+
// Enables `pulumi import` of a webhook a script already created (id is the
|
|
579
|
+
// import id). The signing secret is intentionally not recoverable.
|
|
580
|
+
async read(id, props) {
|
|
581
|
+
const list = await icRequest(props.baseUrl, props.token, "GET", "/v1/tenant/webhooks");
|
|
582
|
+
const hit = (list.data ?? []).find((w) => w.id === id);
|
|
583
|
+
// Absent from the list → gone server-side; empty id lets refresh prune it.
|
|
584
|
+
if (!hit)
|
|
585
|
+
return { id: "" };
|
|
586
|
+
return { id, props: { ...props, url: hit.url, events: hit.events } };
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
class IcWebhook extends pulumi.dynamic.Resource {
|
|
590
|
+
constructor(name, args, opts) {
|
|
591
|
+
super(webhookProvider, name, { secret: undefined, ...args }, {
|
|
592
|
+
...opts,
|
|
593
|
+
additionalSecretOutputs: ["token", "secret"],
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
exports.IcWebhook = IcWebhook;
|
|
598
|
+
const modelKeyProvider = makePutResource({
|
|
599
|
+
path: (i) => `/v1/tenant/model_keys/${encodeURIComponent(i.provider)}`,
|
|
600
|
+
id: (i) => i.provider,
|
|
601
|
+
body: (i) => ({ api_key: i.apiKey, base_url: i.providerBaseUrl ?? null }),
|
|
602
|
+
diff: {
|
|
603
|
+
replaceKeys: ["provider"],
|
|
604
|
+
scalarKeys: ["apiKey", "providerBaseUrl", "token", "baseUrl"],
|
|
605
|
+
deleteBeforeReplace: true,
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
class IcModelKey extends pulumi.dynamic.Resource {
|
|
609
|
+
constructor(name, args, opts) {
|
|
610
|
+
super(modelKeyProvider, name, { providerBaseUrl: undefined, ...args }, { ...opts, additionalSecretOutputs: ["token", "apiKey"] });
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
exports.IcModelKey = IcModelKey;
|
|
614
|
+
async function findProjectByName(baseUrl, token, name) {
|
|
615
|
+
const res = await icRequest(baseUrl, token, "GET", `/v1/organization/projects?name=${encodeURIComponent(name)}`);
|
|
616
|
+
return (res.data ?? [])[0] ?? null;
|
|
617
|
+
}
|
|
618
|
+
const projectProvider = {
|
|
619
|
+
async create(i) {
|
|
620
|
+
const existing = await findProjectByName(i.baseUrl, i.token, i.projectName);
|
|
621
|
+
const proj = existing ??
|
|
622
|
+
(await icRequest(i.baseUrl, i.token, "POST", "/v1/organization/projects", {
|
|
623
|
+
name: i.projectName,
|
|
624
|
+
}));
|
|
625
|
+
return { id: proj.id, outs: { ...i, projectId: proj.id } };
|
|
626
|
+
},
|
|
627
|
+
async update(id, _olds, news) {
|
|
628
|
+
// Nothing mutable server-side (a name change is a replace); just re-assert.
|
|
629
|
+
return { outs: { ...news, projectId: id } };
|
|
630
|
+
},
|
|
631
|
+
async delete(id, props) {
|
|
632
|
+
await icRequest(props.baseUrl, props.token, "DELETE", `/v1/organization/projects/${encodeURIComponent(id)}`);
|
|
633
|
+
},
|
|
634
|
+
async diff(_id, olds, news) {
|
|
635
|
+
return simpleDiff(olds, news, {
|
|
636
|
+
replaceKeys: ["projectName"],
|
|
637
|
+
scalarKeys: ["token", "baseUrl"],
|
|
638
|
+
deleteBeforeReplace: false,
|
|
639
|
+
});
|
|
640
|
+
},
|
|
641
|
+
async read(id, props) {
|
|
642
|
+
return read404(() => icRequest(props.baseUrl, props.token, "GET", `/v1/organization/projects/${encodeURIComponent(id)}`), (p) => ({
|
|
643
|
+
id,
|
|
644
|
+
props: { ...props, projectName: p.name, projectId: p.id },
|
|
645
|
+
}));
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
class IcProject extends pulumi.dynamic.Resource {
|
|
649
|
+
constructor(name, args, opts) {
|
|
650
|
+
super(projectProvider, name, { ...args, projectName: args.projectName ?? name, projectId: undefined }, { ...opts, additionalSecretOutputs: ["token"] });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
exports.IcProject = IcProject;
|
|
654
|
+
const projectTokenProvider = {
|
|
655
|
+
async create(i) {
|
|
656
|
+
const res = await icRequest(i.baseUrl, i.token, "POST", `/v1/organization/projects/${encodeURIComponent(i.project)}/tokens`, { name: i.tokenName, ttl_seconds: i.ttlSeconds });
|
|
657
|
+
return {
|
|
658
|
+
id: res.id,
|
|
659
|
+
outs: { ...i, tokenId: res.id, projectToken: res.token, sub: res.sub },
|
|
660
|
+
};
|
|
661
|
+
},
|
|
662
|
+
async delete(id, props) {
|
|
663
|
+
await icRequest(props.baseUrl, props.token, "DELETE", `/v1/organization/projects/${encodeURIComponent(props.project)}/tokens/${encodeURIComponent(id)}`);
|
|
664
|
+
},
|
|
665
|
+
async diff(_id, olds, news) {
|
|
666
|
+
// The token value can't be edited in place — any change re-mints. Mint the
|
|
667
|
+
// new one before revoking the old so dependents can switch over first
|
|
668
|
+
// (so every input is a replace key, none in-place).
|
|
669
|
+
return simpleDiff(olds, news, {
|
|
670
|
+
replaceKeys: ["project", "tokenName", "ttlSeconds", "token", "baseUrl"],
|
|
671
|
+
deleteBeforeReplace: false,
|
|
672
|
+
});
|
|
673
|
+
},
|
|
674
|
+
};
|
|
675
|
+
class IcProjectToken extends pulumi.dynamic.Resource {
|
|
676
|
+
constructor(name, args, opts) {
|
|
677
|
+
super(projectTokenProvider, name, {
|
|
678
|
+
...args,
|
|
679
|
+
tokenName: args.tokenName ?? name,
|
|
680
|
+
tokenId: undefined,
|
|
681
|
+
projectToken: undefined,
|
|
682
|
+
sub: undefined,
|
|
683
|
+
}, { ...opts, additionalSecretOutputs: ["token", "projectToken"] });
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
exports.IcProjectToken = IcProjectToken;
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ingram-cloud/pulumi",
|
|
3
|
+
"version": "1.0.0",
|
|
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
|
+
"keywords": [
|
|
8
|
+
"pulumi",
|
|
9
|
+
"ingram-cloud",
|
|
10
|
+
"ingram"
|
|
11
|
+
],
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"type-check": "tsc -p tsconfig.json --noEmit",
|
|
23
|
+
"lint": "oxlint",
|
|
24
|
+
"format": "oxfmt --write .",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@pulumi/pulumi": "^3.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@ingram-tech/oxlint-config": "^0.2.1",
|
|
32
|
+
"@pulumi/pulumi": "^3.0.0",
|
|
33
|
+
"@types/node": "^25.0.0",
|
|
34
|
+
"oxfmt": "^0.54.0",
|
|
35
|
+
"oxlint": "^1.70.0",
|
|
36
|
+
"typescript": "^6.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|