@ingram-cloud/pulumi 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/dist/resources.d.ts +33 -0
- package/dist/resources.js +142 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -145,6 +145,35 @@ export const pageUrl = page.pageUrl; // https://cloud.ingram.tech/hosted/dep_…
|
|
|
145
145
|
> change to any of those (or to `agentId`) **replaces** the page and mints a fresh
|
|
146
146
|
> `pageUrl`. The agent's instructions change on the `IcAgent`, not here.
|
|
147
147
|
|
|
148
|
+
### IcSkill (declare in the APP repo)
|
|
149
|
+
|
|
150
|
+
An [Agent Skills](https://cloud.ingram.tech/docs/skills) bundle: a folder
|
|
151
|
+
anchored by `SKILL.md`, uploaded whole and versioned server-side. Declared by
|
|
152
|
+
**local directory** — the folder is the unit — like `IcAgent`'s `uiTemplates`
|
|
153
|
+
and the vector-store-shaped file attachments elsewhere in this pattern. The
|
|
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.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// The folder is the unit. Its basename is the skill's name, and any edit
|
|
160
|
+
// inside it publishes a new immutable version and moves the default.
|
|
161
|
+
const vatFiling = new ic.IcSkill("vat-filing", {
|
|
162
|
+
...conn,
|
|
163
|
+
path: join(__dirname, "skills/vat-filing"),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Put it in front of a fleet. Order matters: a run that fills its search index
|
|
167
|
+
// keeps the earlier entries and drops the rest.
|
|
168
|
+
const biller = new ic.IcAgent("biller", {
|
|
169
|
+
...conn,
|
|
170
|
+
instructions: AGENT_SPECS.biller.instructions,
|
|
171
|
+
skills: [{ skill_id: vatFiling.skillId }],
|
|
172
|
+
// A skill's scripts/ only run through the run_command hosted tool.
|
|
173
|
+
enabledHostedTools: ["run_command"],
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
148
177
|
### IcMcpServer, IcOauthProvider, IcTelegramBot, IcWhatsAppConfig, IcEmail, IcWebhook, IcModelKey (declare in INFRA)
|
|
149
178
|
|
|
150
179
|
```ts
|
|
@@ -154,6 +183,12 @@ new ic.IcMcpServer("thornhill-mcp", {
|
|
|
154
183
|
authKind: "bearer", secret: mcpSecret,
|
|
155
184
|
});
|
|
156
185
|
|
|
186
|
+
// The tool list is discovered at create, not on every `pulumi up` — nothing in
|
|
187
|
+
// this stack changes when your *server's* tools do, so `toolsDiscovered` is what
|
|
188
|
+
// was true at registration. Ingram Cloud re-discovers on the run path once a
|
|
189
|
+
// manifest is an hour old; call `POST /v1/tenant/mcp/{serverName}/refresh` at the
|
|
190
|
+
// end of a deploy that changed your tools if an hour is too long to wait.
|
|
191
|
+
|
|
157
192
|
// An `oauth`-kind server forwards each end-user's own stored token; its
|
|
158
193
|
// provider record tells IC how tokens refresh. Webhook-delegation mode (a
|
|
159
194
|
// refreshWebhook and NO clientSecret): IC POSTs { connection_id } to the
|
package/dist/resources.d.ts
CHANGED
|
@@ -37,6 +37,13 @@ export interface AgentVariable {
|
|
|
37
37
|
description?: string | null;
|
|
38
38
|
required?: boolean;
|
|
39
39
|
}
|
|
40
|
+
/** One skill reference in an agent's `skills` array. Omitted `version` follows
|
|
41
|
+
* the skill's `default_version` and is frozen at publish. Pair with
|
|
42
|
+
* {@link IcSkill}'s `skillId` output. */
|
|
43
|
+
export interface AgentSkillRef {
|
|
44
|
+
skillId: string;
|
|
45
|
+
version?: number;
|
|
46
|
+
}
|
|
40
47
|
/** CSP domain allowlists for the sandboxed iframe (the `_meta.ui.csp` shape). */
|
|
41
48
|
export interface UiCsp {
|
|
42
49
|
connectDomains?: string[];
|
|
@@ -97,6 +104,15 @@ export interface IcAgentArgs {
|
|
|
97
104
|
/** Opt into observational-memory consolidation (background, billed). Default off. */
|
|
98
105
|
memoryConsolidation?: pulumi.Input<boolean>;
|
|
99
106
|
variables?: pulumi.Input<AgentVariable[]>;
|
|
107
|
+
/**
|
|
108
|
+
* Skills (`skl_…`) attached to the agent, in order. Order is not cosmetic: a
|
|
109
|
+
* run whose skills together exceed the search-index ceiling keeps them in
|
|
110
|
+
* this order and drops the rest, so it is the survival priority. Folds into
|
|
111
|
+
* the content signature unsorted, unlike `enabledHostedTools`/`vectorStoreIds`
|
|
112
|
+
* — reordering is itself a content change. Pair with {@link IcSkill}'s
|
|
113
|
+
* `skillId` output.
|
|
114
|
+
*/
|
|
115
|
+
skills?: pulumi.Input<AgentSkillRef[]>;
|
|
100
116
|
/**
|
|
101
117
|
* MCP Apps UI templates attached to the agent. Each template's HTML bundle is
|
|
102
118
|
* uploaded when its bytes or metadata change, and its name + content hash +
|
|
@@ -117,6 +133,23 @@ export declare class IcAgent extends pulumi.dynamic.Resource {
|
|
|
117
133
|
readonly activeVersion: pulumi.Output<number>;
|
|
118
134
|
constructor(name: string, args: IcAgentArgs, opts?: pulumi.CustomResourceOptions);
|
|
119
135
|
}
|
|
136
|
+
export interface IcSkillArgs {
|
|
137
|
+
baseUrl: pulumi.Input<string>;
|
|
138
|
+
token: pulumi.Input<string>;
|
|
139
|
+
/**
|
|
140
|
+
* Absolute path to the skill's directory — the folder anchored by `SKILL.md`,
|
|
141
|
+
* whose basename is the skill's name. Read at `pulumi up`; edit any file in it
|
|
142
|
+
* and a new immutable version is published and made the default.
|
|
143
|
+
*/
|
|
144
|
+
path: pulumi.Input<string>;
|
|
145
|
+
}
|
|
146
|
+
export declare class IcSkill extends pulumi.dynamic.Resource {
|
|
147
|
+
/** The `skl_…` id (same as `.id`, exposed for convenience). */
|
|
148
|
+
readonly skillId: pulumi.Output<string>;
|
|
149
|
+
/** The version this stack last published, and the current `default_version`. */
|
|
150
|
+
readonly version: pulumi.Output<number>;
|
|
151
|
+
constructor(name: string, args: IcSkillArgs, opts?: pulumi.CustomResourceOptions);
|
|
152
|
+
}
|
|
120
153
|
/** One source file on an {@link IcVectorStore}, provided by local path. */
|
|
121
154
|
export interface VectorStoreFile {
|
|
122
155
|
/** Absolute path to the file to ingest (read at `pulumi up`, like `htmlPath`). */
|
package/dist/resources.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.IcWebPage = exports.IcProjectToken = exports.IcProject = exports.IcModelKey = exports.IcWebhook = exports.IcEmail = exports.IcWhatsAppConfig = exports.IcOauthProvider = exports.IcTelegramBot = exports.IcMcpServer = exports.IcVectorStore = exports.IcAgent = exports.IC_API_VERSION = void 0;
|
|
36
|
+
exports.IcWebPage = exports.IcProjectToken = exports.IcProject = exports.IcModelKey = exports.IcWebhook = exports.IcEmail = exports.IcWhatsAppConfig = exports.IcOauthProvider = exports.IcTelegramBot = exports.IcMcpServer = exports.IcVectorStore = exports.IcSkill = exports.IcAgent = exports.IC_API_VERSION = void 0;
|
|
37
37
|
/**
|
|
38
38
|
* Ingram Cloud /v1 tenant configuration as Pulumi dynamic resources.
|
|
39
39
|
*
|
|
@@ -248,6 +248,10 @@ function agentBody(i) {
|
|
|
248
248
|
auto_memory: i.autoMemory ?? null,
|
|
249
249
|
memory_consolidation: i.memoryConsolidation ?? null,
|
|
250
250
|
variables: i.variables ?? [],
|
|
251
|
+
skills: (i.skills ?? []).map((s) => ({
|
|
252
|
+
skill_id: s.skillId,
|
|
253
|
+
...(s.version != null ? { version: s.version } : {}),
|
|
254
|
+
})),
|
|
251
255
|
};
|
|
252
256
|
}
|
|
253
257
|
// Canonical, name-sorted signature of an agent's frozen UI templates: each
|
|
@@ -291,6 +295,12 @@ function agentContentSig(s) {
|
|
|
291
295
|
const vs = [...(s.vector_store_ids ?? [])].sort();
|
|
292
296
|
if (vs.length)
|
|
293
297
|
base.vector_store_ids = vs;
|
|
298
|
+
// Skills, deliberately NOT sorted: reference order is the run's index-ceiling
|
|
299
|
+
// drop priority, so reordering the array is itself a content change worth a
|
|
300
|
+
// new version, not a no-op like reordering hosted tools or vector stores is.
|
|
301
|
+
const skills = s.skills ?? [];
|
|
302
|
+
if (skills.length)
|
|
303
|
+
base.skills = skills;
|
|
294
304
|
return JSON.stringify(base);
|
|
295
305
|
}
|
|
296
306
|
// Map the camelCase resource inputs onto the canonical snapshot shape.
|
|
@@ -304,6 +314,10 @@ function inputsToSnapshot(i) {
|
|
|
304
314
|
auto_memory: i.autoMemory,
|
|
305
315
|
memory_consolidation: i.memoryConsolidation,
|
|
306
316
|
variables: i.variables,
|
|
317
|
+
skills: (i.skills ?? []).map((s) => ({
|
|
318
|
+
skill_id: s.skillId,
|
|
319
|
+
...(s.version != null ? { version: s.version } : {}),
|
|
320
|
+
})),
|
|
307
321
|
// Resolve each template's bytes and hash so a HTML edit shows up in the
|
|
308
322
|
// signature (the API's `content_hash` uses the same sha256-hex).
|
|
309
323
|
ui_resources: resolveUiTemplates(i.uiTemplates).map((t) => ({
|
|
@@ -451,6 +465,10 @@ const agentProvider = {
|
|
|
451
465
|
autoMemory: d.auto_memory ?? undefined,
|
|
452
466
|
memoryConsolidation: d.memory_consolidation ?? undefined,
|
|
453
467
|
variables: d.variables ?? [],
|
|
468
|
+
skills: (d.skills ?? []).map((s) => ({
|
|
469
|
+
skillId: s.skill_id,
|
|
470
|
+
version: s.version,
|
|
471
|
+
})),
|
|
454
472
|
},
|
|
455
473
|
};
|
|
456
474
|
});
|
|
@@ -467,6 +485,7 @@ class IcAgent extends pulumi.dynamic.Resource {
|
|
|
467
485
|
autoMemory: undefined,
|
|
468
486
|
memoryConsolidation: undefined,
|
|
469
487
|
variables: undefined,
|
|
488
|
+
skills: undefined,
|
|
470
489
|
uiTemplates: undefined,
|
|
471
490
|
publishNote: undefined,
|
|
472
491
|
rolloutPercent: undefined,
|
|
@@ -481,6 +500,128 @@ class IcAgent extends pulumi.dynamic.Resource {
|
|
|
481
500
|
}
|
|
482
501
|
}
|
|
483
502
|
exports.IcAgent = IcAgent;
|
|
503
|
+
/** Walk a skill directory into the parts the upload sends.
|
|
504
|
+
*
|
|
505
|
+
* `require` inline rather than a top-level import: Pulumi serializes a dynamic
|
|
506
|
+
* provider's closure, and this is the idiom the vector-store resource next door
|
|
507
|
+
* already uses to read local files. */
|
|
508
|
+
function resolveSkillFiles(dir) {
|
|
509
|
+
if (!dir)
|
|
510
|
+
throw new Error("skill: `path` is required");
|
|
511
|
+
const fs = require("node:fs");
|
|
512
|
+
const nodePath = require("node:path");
|
|
513
|
+
const root = nodePath.basename(nodePath.resolve(dir));
|
|
514
|
+
const out = [];
|
|
515
|
+
const walk = (at, prefix) => {
|
|
516
|
+
for (const entry of fs.readdirSync(at, { withFileTypes: true })) {
|
|
517
|
+
const full = nodePath.join(at, entry.name);
|
|
518
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
519
|
+
if (entry.isDirectory())
|
|
520
|
+
walk(full, rel);
|
|
521
|
+
else if (entry.isFile())
|
|
522
|
+
out.push({ rel: `${root}/${rel}`, bytes: fs.readFileSync(full) });
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
walk(nodePath.resolve(dir), "");
|
|
526
|
+
if (!out.some((f) => f.rel === `${root}/SKILL.md`))
|
|
527
|
+
throw new Error(`skill: ${dir} has no SKILL.md`);
|
|
528
|
+
return out.sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
|
|
529
|
+
}
|
|
530
|
+
/** A content signature over the whole bundle — what `diff` compares. */
|
|
531
|
+
function skillSig(dir) {
|
|
532
|
+
if (!dir)
|
|
533
|
+
return "";
|
|
534
|
+
const { createHash } = require("node:crypto");
|
|
535
|
+
const hash = createHash("sha256");
|
|
536
|
+
for (const file of resolveSkillFiles(dir)) {
|
|
537
|
+
hash.update(file.rel);
|
|
538
|
+
hash.update(file.bytes);
|
|
539
|
+
}
|
|
540
|
+
return hash.digest("hex");
|
|
541
|
+
}
|
|
542
|
+
/** Upload a bundle as repeated `files[]` parts. Not `icRequest` (that
|
|
543
|
+
* JSON-encodes); uses global FormData/File (Node ≥18), like `uploadFile`. */
|
|
544
|
+
async function uploadSkillBundle(baseUrl, token, path, dir) {
|
|
545
|
+
const form = new FormData();
|
|
546
|
+
for (const file of resolveSkillFiles(dir))
|
|
547
|
+
form.append("files[]", new File([file.bytes], file.rel));
|
|
548
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
549
|
+
method: "POST",
|
|
550
|
+
headers: {
|
|
551
|
+
Authorization: `Bearer ${token}`,
|
|
552
|
+
"IC-Api-Version": exports.IC_API_VERSION,
|
|
553
|
+
},
|
|
554
|
+
body: form,
|
|
555
|
+
});
|
|
556
|
+
const text = await res.text();
|
|
557
|
+
if (!res.ok) {
|
|
558
|
+
const err = new Error(`IC POST ${path} → ${res.status}: ${text.slice(0, 400)}`);
|
|
559
|
+
err.status = res.status;
|
|
560
|
+
throw err;
|
|
561
|
+
}
|
|
562
|
+
return text ? JSON.parse(text) : {};
|
|
563
|
+
}
|
|
564
|
+
const skillProvider = {
|
|
565
|
+
async create(i) {
|
|
566
|
+
const skill = await uploadSkillBundle(i.baseUrl, i.token, "/v1/skills", i.path);
|
|
567
|
+
return {
|
|
568
|
+
id: skill.id,
|
|
569
|
+
outs: {
|
|
570
|
+
...i,
|
|
571
|
+
skillId: skill.id,
|
|
572
|
+
version: skill.default_version,
|
|
573
|
+
contentSig: skillSig(i.path),
|
|
574
|
+
},
|
|
575
|
+
};
|
|
576
|
+
},
|
|
577
|
+
async update(id, _olds, news) {
|
|
578
|
+
// A version is immutable, so an edited bundle is a new one — and the
|
|
579
|
+
// default has to move, or nothing that resolves by default would see it.
|
|
580
|
+
const published = await uploadSkillBundle(news.baseUrl, news.token, `/v1/skills/${id}/versions`, news.path);
|
|
581
|
+
await icRequest(news.baseUrl, news.token, "POST", `/v1/skills/${id}`, {
|
|
582
|
+
default_version: published.version,
|
|
583
|
+
});
|
|
584
|
+
return {
|
|
585
|
+
outs: {
|
|
586
|
+
...news,
|
|
587
|
+
skillId: id,
|
|
588
|
+
version: published.version,
|
|
589
|
+
contentSig: skillSig(news.path),
|
|
590
|
+
},
|
|
591
|
+
};
|
|
592
|
+
},
|
|
593
|
+
async delete(id, props) {
|
|
594
|
+
await icRequest(props.baseUrl, props.token, "DELETE", `/v1/skills/${id}`);
|
|
595
|
+
},
|
|
596
|
+
async diff(_id, olds, news) {
|
|
597
|
+
// Compare against the sig STORED at the last apply, not a re-resolve of
|
|
598
|
+
// olds: olds.path points at the same folder, whose bytes have already
|
|
599
|
+
// changed, so re-reading it would hide the change (same reasoning as the
|
|
600
|
+
// agent and vector-store diffs).
|
|
601
|
+
const changed = (olds.contentSig ?? "") !== skillSig(news.path);
|
|
602
|
+
return { changes: changed };
|
|
603
|
+
},
|
|
604
|
+
async read(id, props) {
|
|
605
|
+
return read404(() => icRequest(props.baseUrl, props.token, "GET", `/v1/skills/${id}`), (skill) => {
|
|
606
|
+
if (!skill || !skill.id)
|
|
607
|
+
return { id: "" };
|
|
608
|
+
return {
|
|
609
|
+
id,
|
|
610
|
+
props: {
|
|
611
|
+
...props,
|
|
612
|
+
skillId: skill.id,
|
|
613
|
+
version: skill.default_version,
|
|
614
|
+
},
|
|
615
|
+
};
|
|
616
|
+
});
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
class IcSkill extends pulumi.dynamic.Resource {
|
|
620
|
+
constructor(name, args, opts) {
|
|
621
|
+
super(skillProvider, name, { skillId: undefined, version: undefined, contentSig: undefined, ...args }, { ...opts, additionalSecretOutputs: ["token"] });
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
exports.IcSkill = IcSkill;
|
|
484
625
|
// Read each declared file's bytes and hash them (same sha256-hex the API keys
|
|
485
626
|
// files by) so an edit shows up in the file signature. Module-scope + node
|
|
486
627
|
// built-ins required inside for Pulumi's closure serialization.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ingram-cloud/pulumi",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Pulumi dynamic resources for the Ingram Cloud /v1 API — projects, project tokens, agents, MCP servers, channels, webhooks, model keys.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://cloud.ingram.tech",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@ingram-tech/oxlint-config": "^0.2.2",
|
|
32
|
-
"@pulumi/pulumi": "^3.
|
|
33
|
-
"@types/node": "^26.1.
|
|
34
|
-
"oxfmt": "
|
|
35
|
-
"oxlint": "^1.
|
|
32
|
+
"@pulumi/pulumi": "^3.255.0",
|
|
33
|
+
"@types/node": "^26.1.2",
|
|
34
|
+
"oxfmt": "0.62.0",
|
|
35
|
+
"oxlint": "^1.77.0",
|
|
36
36
|
"typescript": "^7.0.2"
|
|
37
37
|
}
|
|
38
38
|
}
|