@biffo/cli 0.308.2 → 0.309.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.
|
@@ -330,6 +330,36 @@
|
|
|
330
330
|
"type": "object",
|
|
331
331
|
"additionalProperties": { "type": "string" },
|
|
332
332
|
"description": "Python package dependencies (including biffo-plugin-sdk)."
|
|
333
|
+
},
|
|
334
|
+
"config": {
|
|
335
|
+
"type": "array",
|
|
336
|
+
"items": {
|
|
337
|
+
"type": "object",
|
|
338
|
+
"required": ["name", "kind", "description"],
|
|
339
|
+
"properties": {
|
|
340
|
+
"name": {
|
|
341
|
+
"type": "string",
|
|
342
|
+
"pattern": "^[a-z][a-z0-9_]*$",
|
|
343
|
+
"description": "Config key, snake_case. Referenced by other manifest fields (e.g. chat_agents[].required_group) and used as the per-plugin, per-name lookup key the host resolves at runtime."
|
|
344
|
+
},
|
|
345
|
+
"kind": {
|
|
346
|
+
"enum": ["secret", "setting"],
|
|
347
|
+
"description": "'secret' — the instance supplies an SSM parameter PATH, never the credential itself. 'setting' — the instance supplies a literal value (a Cognito group name, a feature flag, a numeric limit). Never a value itself; this manifest declares only the NEED (biffo-template#1517)."
|
|
348
|
+
},
|
|
349
|
+
"required": {
|
|
350
|
+
"type": "boolean",
|
|
351
|
+
"default": true,
|
|
352
|
+
"description": "If true, `biffo plugin install` fails loudly when the instance supplies no value for this name, rather than installing an unconfigured plugin."
|
|
353
|
+
},
|
|
354
|
+
"description": {
|
|
355
|
+
"type": "string",
|
|
356
|
+
"minLength": 1,
|
|
357
|
+
"description": "What this need is for, shown to whoever installs the plugin so they know what to supply."
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
"additionalProperties": false
|
|
361
|
+
},
|
|
362
|
+
"description": "Named needs (a credential, a group name, a feature flag) this manifest declares that the INSTANCE must supply at install time — never values, only name/kind/required/description (biffo-template#1517). See ConfigDeclaration in biffo_plugin_sdk.plugin for the full mechanism."
|
|
333
363
|
}
|
|
334
364
|
},
|
|
335
365
|
"additionalProperties": false
|
|
@@ -330,6 +330,36 @@
|
|
|
330
330
|
"type": "object",
|
|
331
331
|
"additionalProperties": { "type": "string" },
|
|
332
332
|
"description": "Python package dependencies (including biffo-plugin-sdk)."
|
|
333
|
+
},
|
|
334
|
+
"config": {
|
|
335
|
+
"type": "array",
|
|
336
|
+
"items": {
|
|
337
|
+
"type": "object",
|
|
338
|
+
"required": ["name", "kind", "description"],
|
|
339
|
+
"properties": {
|
|
340
|
+
"name": {
|
|
341
|
+
"type": "string",
|
|
342
|
+
"pattern": "^[a-z][a-z0-9_]*$",
|
|
343
|
+
"description": "Config key, snake_case. Referenced by other manifest fields (e.g. chat_agents[].required_group) and used as the per-plugin, per-name lookup key the host resolves at runtime."
|
|
344
|
+
},
|
|
345
|
+
"kind": {
|
|
346
|
+
"enum": ["secret", "setting"],
|
|
347
|
+
"description": "'secret' — the instance supplies an SSM parameter PATH, never the credential itself. 'setting' — the instance supplies a literal value (a Cognito group name, a feature flag, a numeric limit). Never a value itself; this manifest declares only the NEED (biffo-template#1517)."
|
|
348
|
+
},
|
|
349
|
+
"required": {
|
|
350
|
+
"type": "boolean",
|
|
351
|
+
"default": true,
|
|
352
|
+
"description": "If true, `biffo plugin install` fails loudly when the instance supplies no value for this name, rather than installing an unconfigured plugin."
|
|
353
|
+
},
|
|
354
|
+
"description": {
|
|
355
|
+
"type": "string",
|
|
356
|
+
"minLength": 1,
|
|
357
|
+
"description": "What this need is for, shown to whoever installs the plugin so they know what to supply."
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
"additionalProperties": false
|
|
361
|
+
},
|
|
362
|
+
"description": "Named needs (a credential, a group name, a feature flag) this manifest declares that the INSTANCE must supply at install time — never values, only name/kind/required/description (biffo-template#1517). See ConfigDeclaration in biffo_plugin_sdk.plugin for the full mechanism."
|
|
333
363
|
}
|
|
334
364
|
},
|
|
335
365
|
"additionalProperties": false
|
package/dist/index.js
CHANGED
|
@@ -8317,6 +8317,12 @@ var ChatAgentDeclarationSchema = z7.object({
|
|
|
8317
8317
|
max_output_tokens: z7.number().int().positive().default(1024),
|
|
8318
8318
|
timeout_seconds: z7.number().positive().default(20)
|
|
8319
8319
|
}).strict();
|
|
8320
|
+
var ConfigDeclarationSchema = z7.object({
|
|
8321
|
+
name: z7.string().regex(/^[a-z][a-z0-9_]*$/, "must be snake_case, e.g. image_provider_api_key"),
|
|
8322
|
+
kind: z7.enum(["secret", "setting"]),
|
|
8323
|
+
required: z7.boolean().default(true),
|
|
8324
|
+
description: z7.string().min(1, "description is required so an installer knows what this need is for")
|
|
8325
|
+
}).strict();
|
|
8320
8326
|
var PluginManifestSchema = z7.object({
|
|
8321
8327
|
name: z7.string().regex(/^[a-z][a-z0-9-]*$/, "must be a lowercase kebab-case slug"),
|
|
8322
8328
|
version: z7.string().regex(/^\d+\.\d+\.\d+$/, "must be a full semver, e.g. 1.2.3"),
|
|
@@ -8350,7 +8356,10 @@ var PluginManifestSchema = z7.object({
|
|
|
8350
8356
|
// The plugin's tenant-scoped baseline-row seed (ADR-0005, biffo-template#1554).
|
|
8351
8357
|
// Optional — a plugin with no baseline data omits this entirely, and
|
|
8352
8358
|
// `biffo plugin install`/`upgrade` vendor nothing for it.
|
|
8353
|
-
seed: SeedDeclarationSchema.optional()
|
|
8359
|
+
seed: SeedDeclarationSchema.optional(),
|
|
8360
|
+
// The needs this plugin declares the instance must supply (biffo-template#1517)
|
|
8361
|
+
// — never values, only name/kind/required/description.
|
|
8362
|
+
config: z7.array(ConfigDeclarationSchema).default([])
|
|
8354
8363
|
}).superRefine((manifest, ctx) => {
|
|
8355
8364
|
const tableNames = new Set(manifest.tables.map((t) => t.name));
|
|
8356
8365
|
for (const route of manifest.api_routes) {
|
|
@@ -8371,6 +8380,27 @@ var PluginManifestSchema = z7.object({
|
|
|
8371
8380
|
}
|
|
8372
8381
|
}
|
|
8373
8382
|
}
|
|
8383
|
+
const configCounts = /* @__PURE__ */ new Map();
|
|
8384
|
+
for (const c of manifest.config) configCounts.set(c.name, (configCounts.get(c.name) ?? 0) + 1);
|
|
8385
|
+
for (const [name, count] of configCounts) {
|
|
8386
|
+
if (count > 1) {
|
|
8387
|
+
ctx.addIssue({
|
|
8388
|
+
code: z7.ZodIssueCode.custom,
|
|
8389
|
+
message: `Duplicate config declaration name '${name}'`
|
|
8390
|
+
});
|
|
8391
|
+
}
|
|
8392
|
+
}
|
|
8393
|
+
const settingNames = new Set(
|
|
8394
|
+
manifest.config.filter((c) => c.kind === "setting").map((c) => c.name)
|
|
8395
|
+
);
|
|
8396
|
+
for (const agent of manifest.chat_agents) {
|
|
8397
|
+
if (!settingNames.has(agent.required_group)) {
|
|
8398
|
+
ctx.addIssue({
|
|
8399
|
+
code: z7.ZodIssueCode.custom,
|
|
8400
|
+
message: `chat_agents[key=${agent.key}].required_group '${agent.required_group}' must name a 'config' declaration with kind: "setting" on this same manifest \u2014 a literal Cognito group name is no longer accepted here (biffo-template#1517). Declared setting config names: ${[...settingNames].sort().join(", ") || "none"}`
|
|
8401
|
+
});
|
|
8402
|
+
}
|
|
8403
|
+
}
|
|
8374
8404
|
});
|
|
8375
8405
|
function validateManifest(raw) {
|
|
8376
8406
|
const result = PluginManifestSchema.safeParse(raw);
|
|
@@ -9046,7 +9076,7 @@ function printScopeSeamEntitlement(name) {
|
|
|
9046
9076
|
}
|
|
9047
9077
|
|
|
9048
9078
|
// src/commands/plugin-install.ts
|
|
9049
|
-
import { cpSync as cpSync5, existsSync as existsSync31, mkdirSync as mkdirSync11, readFileSync as readFileSync23, statSync as statSync7 } from "fs";
|
|
9079
|
+
import { cpSync as cpSync5, existsSync as existsSync31, mkdirSync as mkdirSync11, readFileSync as readFileSync23, statSync as statSync7, writeFileSync as writeFileSync12 } from "fs";
|
|
9050
9080
|
import { join as join33, relative as relative4, resolve as resolve12 } from "path";
|
|
9051
9081
|
import chalk15 from "chalk";
|
|
9052
9082
|
import { Command as Command15 } from "commander";
|
|
@@ -9092,6 +9122,58 @@ var PluginMigrationsAdapter = class {
|
|
|
9092
9122
|
}
|
|
9093
9123
|
};
|
|
9094
9124
|
|
|
9125
|
+
// src/lib/plugin-config-resolution.ts
|
|
9126
|
+
function pluginConfigEnvNames(pluginName, configName) {
|
|
9127
|
+
const prefix = `BIFFO_PLUGIN_${pluginName.toUpperCase().replace(/-/g, "_")}_${configName.toUpperCase()}`;
|
|
9128
|
+
return { literalEnv: prefix, parameterEnv: `${prefix}_PARAMETER` };
|
|
9129
|
+
}
|
|
9130
|
+
function resolvePluginConfigSupply(pluginName, declarations, supplied) {
|
|
9131
|
+
const resolved = [];
|
|
9132
|
+
const missingRequired = [];
|
|
9133
|
+
for (const decl of declarations) {
|
|
9134
|
+
const raw = supplied[decl.name];
|
|
9135
|
+
const value = raw?.trim();
|
|
9136
|
+
if (!value) {
|
|
9137
|
+
if (decl.required) {
|
|
9138
|
+
missingRequired.push({ name: decl.name, kind: decl.kind, description: decl.description });
|
|
9139
|
+
}
|
|
9140
|
+
continue;
|
|
9141
|
+
}
|
|
9142
|
+
if (decl.kind === "secret" && !value.startsWith("/")) {
|
|
9143
|
+
throw new Error(
|
|
9144
|
+
`--config ${decl.name}=... must be an SSM parameter PATH starting with '/' for a 'secret' declaration (e.g. /myproject/dev/${pluginName}/${decl.name}) \u2014 never the credential itself. Store the credential in SSM first, then pass its parameter name here.`
|
|
9145
|
+
);
|
|
9146
|
+
}
|
|
9147
|
+
const { literalEnv, parameterEnv } = pluginConfigEnvNames(pluginName, decl.name);
|
|
9148
|
+
resolved.push({
|
|
9149
|
+
name: decl.name,
|
|
9150
|
+
kind: decl.kind,
|
|
9151
|
+
description: decl.description,
|
|
9152
|
+
value,
|
|
9153
|
+
envName: decl.kind === "secret" ? parameterEnv : literalEnv
|
|
9154
|
+
});
|
|
9155
|
+
}
|
|
9156
|
+
return { resolved, missingRequired };
|
|
9157
|
+
}
|
|
9158
|
+
function parseConfigOptionValues(entries) {
|
|
9159
|
+
const record = {};
|
|
9160
|
+
for (const entry of entries) {
|
|
9161
|
+
const eq = entry.indexOf("=");
|
|
9162
|
+
if (eq <= 0) {
|
|
9163
|
+
throw new Error(`--config value '${entry}' must be of the form name=value`);
|
|
9164
|
+
}
|
|
9165
|
+
record[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
9166
|
+
}
|
|
9167
|
+
return record;
|
|
9168
|
+
}
|
|
9169
|
+
function missingRequiredConfigMessage(pluginName, missing) {
|
|
9170
|
+
const lines = missing.map((m) => ` - ${m.name} (${m.kind}): ${m.description}`);
|
|
9171
|
+
return `Plugin '${pluginName}' declares ${missing.length} required config value(s) with no supplied value:
|
|
9172
|
+
${lines.join("\n")}
|
|
9173
|
+
|
|
9174
|
+
Supply each with --config <name>=<value> (repeatable) and re-run install. For a 'secret', pass the SSM parameter PATH holding the credential (create the parameter first, e.g. \`aws ssm put-parameter --type SecureString ...\`), never the credential itself.`;
|
|
9175
|
+
}
|
|
9176
|
+
|
|
9095
9177
|
// src/lib/plugin-provenance.ts
|
|
9096
9178
|
import { existsSync as existsSync28, readFileSync as readFileSync21, writeFileSync as writeFileSync10 } from "fs";
|
|
9097
9179
|
import { join as join29 } from "path";
|
|
@@ -9387,7 +9469,12 @@ var pluginInstallCommand = new Command15("install").description(
|
|
|
9387
9469
|
).argument("[target]", "Plugin name and minor version, e.g. rbac@1.0 (omit when using --local)").option(
|
|
9388
9470
|
"--local <path>",
|
|
9389
9471
|
"Install from a local, unpublished plugin directory instead of the registry"
|
|
9390
|
-
).option("--dry-run", "Resolve the plugin and print planned changes without modifying the repo").option("--cwd <path>", "Project root to install into (defaults to the current directory)").
|
|
9472
|
+
).option("--dry-run", "Resolve the plugin and print planned changes without modifying the repo").option("--cwd <path>", "Project root to install into (defaults to the current directory)").option(
|
|
9473
|
+
"--config <name=value>",
|
|
9474
|
+
"Supply an instance value for one manifest `config:` declaration (biffo-template#1517) \u2014 repeatable. For a 'secret' declaration, value must be an SSM parameter PATH, never the credential itself.",
|
|
9475
|
+
(value, previous) => [...previous, value],
|
|
9476
|
+
[]
|
|
9477
|
+
).action(
|
|
9391
9478
|
async (target, options) => {
|
|
9392
9479
|
const cwd = options.cwd ? resolve12(options.cwd) : process.cwd();
|
|
9393
9480
|
try {
|
|
@@ -9396,7 +9483,8 @@ var pluginInstallCommand = new Command15("install").description(
|
|
|
9396
9483
|
{
|
|
9397
9484
|
...options.local ? { local: resolve12(options.local) } : {},
|
|
9398
9485
|
dryRun: options.dryRun ?? false,
|
|
9399
|
-
cwd
|
|
9486
|
+
cwd,
|
|
9487
|
+
config: parseConfigOptionValues(options.config)
|
|
9400
9488
|
},
|
|
9401
9489
|
{
|
|
9402
9490
|
registry: new RegistryAdapter(),
|
|
@@ -9501,7 +9589,7 @@ async function runPluginInstall(target, options, deps) {
|
|
|
9501
9589
|
);
|
|
9502
9590
|
}
|
|
9503
9591
|
if (options.dryRun) {
|
|
9504
|
-
printDryRun4(entry, source, relTargetDir, inTreeSource);
|
|
9592
|
+
printDryRun4(entry, source, relTargetDir, inTreeSource, options.config ?? {});
|
|
9505
9593
|
return;
|
|
9506
9594
|
}
|
|
9507
9595
|
const isRepo = await deps.git.isGitRepo(options.cwd);
|
|
@@ -9531,6 +9619,14 @@ async function runPluginInstall(target, options, deps) {
|
|
|
9531
9619
|
if (retiredShapeReasons.length > 0) {
|
|
9532
9620
|
throw new Error(retiredFrontendShapeError(pluginName, retiredShapeReasons));
|
|
9533
9621
|
}
|
|
9622
|
+
const configSupply = resolvePluginConfigSupply(
|
|
9623
|
+
pluginName,
|
|
9624
|
+
manifest.config,
|
|
9625
|
+
options.config ?? {}
|
|
9626
|
+
);
|
|
9627
|
+
if (configSupply.missingRequired.length > 0) {
|
|
9628
|
+
throw new Error(missingRequiredConfigMessage(pluginName, configSupply.missingRequired));
|
|
9629
|
+
}
|
|
9534
9630
|
if (inTreeSource) {
|
|
9535
9631
|
log.info(`${relTargetDir}/ is already in this checkout \u2014 installing in place.`);
|
|
9536
9632
|
} else {
|
|
@@ -9589,6 +9685,31 @@ async function runPluginInstall(target, options, deps) {
|
|
|
9589
9685
|
if (seedResult.vendored) {
|
|
9590
9686
|
stagePaths.push(seedResult.stagedPath);
|
|
9591
9687
|
}
|
|
9688
|
+
if (manifest.config.length > 0) {
|
|
9689
|
+
const configFilePath = join33(targetDir, "biffo.plugin-config.json");
|
|
9690
|
+
writeFileSync12(
|
|
9691
|
+
configFilePath,
|
|
9692
|
+
JSON.stringify(
|
|
9693
|
+
{
|
|
9694
|
+
plugin: pluginName,
|
|
9695
|
+
resolved: configSupply.resolved.map(({ name, kind, envName, value }) => ({
|
|
9696
|
+
name,
|
|
9697
|
+
kind,
|
|
9698
|
+
env: envName,
|
|
9699
|
+
// For a secret this is the SSM parameter PATH, not the credential.
|
|
9700
|
+
value
|
|
9701
|
+
}))
|
|
9702
|
+
},
|
|
9703
|
+
null,
|
|
9704
|
+
2
|
|
9705
|
+
) + "\n",
|
|
9706
|
+
"utf8"
|
|
9707
|
+
);
|
|
9708
|
+
stagePaths.push(relative4(options.cwd, configFilePath));
|
|
9709
|
+
log.success(
|
|
9710
|
+
`Recorded ${configSupply.resolved.length}/${manifest.config.length} declared config value(s) at ${relative4(options.cwd, configFilePath)}`
|
|
9711
|
+
);
|
|
9712
|
+
}
|
|
9592
9713
|
const commitMessage = `feat(plugins): install ${pluginName}@${source.version}`;
|
|
9593
9714
|
await deps.git.add(options.cwd, stagePaths);
|
|
9594
9715
|
await deps.git.commit(options.cwd, commitMessage);
|
|
@@ -9599,10 +9720,28 @@ async function runPluginInstall(target, options, deps) {
|
|
|
9599
9720
|
console.log(chalk15.dim(` git push`));
|
|
9600
9721
|
console.log(chalk15.dim(` biffo deploy <environment> --app-only
|
|
9601
9722
|
`));
|
|
9723
|
+
printConfigWiringInstructions(pluginName, configSupply.resolved);
|
|
9602
9724
|
} finally {
|
|
9603
9725
|
source.cleanup();
|
|
9604
9726
|
}
|
|
9605
9727
|
}
|
|
9728
|
+
function printConfigWiringInstructions(pluginName, resolved) {
|
|
9729
|
+
if (resolved.length === 0) return;
|
|
9730
|
+
console.log(chalk15.bold(` ${pluginName} needs these env vars on the shared plugin host:
|
|
9731
|
+
`));
|
|
9732
|
+
const entries = {};
|
|
9733
|
+
for (const r of resolved) entries[r.envName] = r.value;
|
|
9734
|
+
console.log(chalk15.dim(` ${JSON.stringify(entries, null, 2).split("\n").join("\n ")}`));
|
|
9735
|
+
console.log(
|
|
9736
|
+
`
|
|
9737
|
+
Add them to plugin_host_environment in infra/environments/<env>/plugin_host.auto.tfvars.json.`
|
|
9738
|
+
);
|
|
9739
|
+
if (resolved.some((r) => r.kind === "secret")) {
|
|
9740
|
+
console.log(
|
|
9741
|
+
" At least one is a secret reference \u2014 grant the shared plugin host's execution role ssm:GetParameter (and kms:Decrypt if it's a SecureString on a customer-managed key) scoped to that parameter."
|
|
9742
|
+
);
|
|
9743
|
+
}
|
|
9744
|
+
}
|
|
9606
9745
|
function parseManifestFile(path) {
|
|
9607
9746
|
try {
|
|
9608
9747
|
return JSON.parse(readFileSync23(path, "utf8"));
|
|
@@ -9610,7 +9749,7 @@ function parseManifestFile(path) {
|
|
|
9610
9749
|
throw new Error(`Could not parse ${path} as JSON: ${err.message}`);
|
|
9611
9750
|
}
|
|
9612
9751
|
}
|
|
9613
|
-
function printDryRun4(entry, source, relTargetDir, inTreeSource) {
|
|
9752
|
+
function printDryRun4(entry, source, relTargetDir, inTreeSource, suppliedConfig = {}) {
|
|
9614
9753
|
const name = entry ? entry.name : source.name;
|
|
9615
9754
|
const version = entry ? entry.version : source.version;
|
|
9616
9755
|
console.log(chalk15.bold("\n Dry run \u2014 no changes will be made\n"));
|
|
@@ -9642,6 +9781,18 @@ function printDryRun4(entry, source, relTargetDir, inTreeSource) {
|
|
|
9642
9781
|
` Would vendor seed DDL into: ${pluginSeedImportDir(name)}/ (baseline_tables: ${source.manifest.seed.baseline_tables.join(", ") || "none declared"})`
|
|
9643
9782
|
);
|
|
9644
9783
|
}
|
|
9784
|
+
if (source && source.manifest.config.length > 0) {
|
|
9785
|
+
const { resolved, missingRequired } = resolvePluginConfigSupply(
|
|
9786
|
+
name,
|
|
9787
|
+
source.manifest.config,
|
|
9788
|
+
suppliedConfig
|
|
9789
|
+
);
|
|
9790
|
+
console.log(` Declares ${source.manifest.config.length} config need(s):`);
|
|
9791
|
+
for (const c of source.manifest.config) {
|
|
9792
|
+
const status = missingRequired.some((m) => m.name === c.name) ? "MISSING (would fail install)" : resolved.some((r) => r.name === c.name) ? "supplied" : "not supplied (optional)";
|
|
9793
|
+
console.log(` - ${c.name} (${c.kind}${c.required ? ", required" : ""}): ${status}`);
|
|
9794
|
+
}
|
|
9795
|
+
}
|
|
9645
9796
|
console.log(` Would commit: feat(plugins): install ${name}@${version}
|
|
9646
9797
|
`);
|
|
9647
9798
|
}
|