@usenaive-sdk/blueprints 0.2.0 → 0.3.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/dist/index.d.ts +140 -9
- package/dist/index.js +171 -31
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -80,6 +80,8 @@ declare const AppDeclSchema: z.ZodObject<{
|
|
|
80
80
|
mcp: z.ZodOptional<z.ZodString>;
|
|
81
81
|
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
82
82
|
from_env: z.ZodString;
|
|
83
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
84
|
+
generate: z.ZodLiteral<true>;
|
|
83
85
|
}, z.core.$strip>]>>>;
|
|
84
86
|
}, z.core.$strip>;
|
|
85
87
|
/**
|
|
@@ -252,6 +254,8 @@ declare const ProjectSchema: z.ZodObject<{
|
|
|
252
254
|
mcp: z.ZodOptional<z.ZodString>;
|
|
253
255
|
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
254
256
|
from_env: z.ZodString;
|
|
257
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
258
|
+
generate: z.ZodLiteral<true>;
|
|
255
259
|
}, z.core.$strip>]>>>;
|
|
256
260
|
}, z.core.$strip>>>;
|
|
257
261
|
agents: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
@@ -391,6 +395,50 @@ declare function collectFiles(source: FileSource): Record<string, string>;
|
|
|
391
395
|
*/
|
|
392
396
|
declare function contentHash(files: Record<string, string>): string;
|
|
393
397
|
|
|
398
|
+
/**
|
|
399
|
+
* The blueprint install (`canonical-spec §29.6`) — one applied declaration per
|
|
400
|
+
* (organization, project), and the drift a `revision_conflict` names.
|
|
401
|
+
*
|
|
402
|
+
* `naive up` needed no server-side state while the only writer was a person with a clone: its
|
|
403
|
+
* working tree was the manifest. The studio applying the same declaration makes two writers on one
|
|
404
|
+
* organization, and neither can see the other. This row is what they share.
|
|
405
|
+
*
|
|
406
|
+
* It is not a second source of truth for what is provisioned — apps, agents, skills, identities and
|
|
407
|
+
* vaults stay keyed by name on their own routes. It records *that* an apply happened, with what, by
|
|
408
|
+
* whom, and against which revision.
|
|
409
|
+
*/
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* What one apply did to one resource. This is the vocabulary `naive up` has printed since it
|
|
413
|
+
* existed; it is written HERE, and imported by `@usenaive-sdk/blueprints`, for the reason
|
|
414
|
+
* `CLAUDE.md §2` gives: the hosted apply publishes the engine's report on the install (below), so
|
|
415
|
+
* it is a wire shape, and a wire shape has exactly one definition. The engine re-exports these two
|
|
416
|
+
* names unchanged, so nothing downstream of it learns a second word for the same thing.
|
|
417
|
+
*/
|
|
418
|
+
declare const ResourceActionSchema: z.ZodEnum<{
|
|
419
|
+
created: "created";
|
|
420
|
+
updated: "updated";
|
|
421
|
+
unchanged: "unchanged";
|
|
422
|
+
deleted: "deleted";
|
|
423
|
+
refused: "refused";
|
|
424
|
+
}>;
|
|
425
|
+
type ResourceAction = z.infer<typeof ResourceActionSchema>;
|
|
426
|
+
/** One line of an apply: what was named, what happened to it, and — for a refusal — why. */
|
|
427
|
+
declare const ResourceReportSchema: z.ZodObject<{
|
|
428
|
+
name: z.ZodString;
|
|
429
|
+
action: z.ZodEnum<{
|
|
430
|
+
created: "created";
|
|
431
|
+
updated: "updated";
|
|
432
|
+
unchanged: "unchanged";
|
|
433
|
+
deleted: "deleted";
|
|
434
|
+
refused: "refused";
|
|
435
|
+
}>;
|
|
436
|
+
id: z.ZodOptional<z.ZodString>;
|
|
437
|
+
url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
438
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
439
|
+
}, z.core.$strip>;
|
|
440
|
+
type ResourceReportLine = z.infer<typeof ResourceReportSchema>;
|
|
441
|
+
|
|
394
442
|
/**
|
|
395
443
|
* The report vocabulary `up` speaks (`workstreams/10-blueprints.md §2` item 4) and the handful of
|
|
396
444
|
* client-side moves every resource kind shares: one line per attempt with refusals caught into
|
|
@@ -398,14 +446,62 @@ declare function contentHash(files: Record<string, string>): string;
|
|
|
398
446
|
* cursor pagination, and the by-name tombstone behind `removed`.
|
|
399
447
|
*/
|
|
400
448
|
|
|
401
|
-
type
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
449
|
+
type ResourceReport = ResourceReportLine;
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Apps in `naive up`: upsert by `GET /v1/apps?name=` (§29.2), `env` written as write-only secrets
|
|
453
|
+
* only when the live `value_hash` differs from the local one (ADR-0354, ADR-0357), and the
|
|
454
|
+
* `deploy_dir` shipped only when its content hash differs from the newest live deployment's
|
|
455
|
+
* (ADR-0357). Live secret names the config does not declare are reported, never deleted
|
|
456
|
+
* (ADR-0352). `mcp` is patched on drift, absent meaning `null` (§29.5); the platform mints the
|
|
457
|
+
* app's `VETTA_MCP_TOKEN` itself, so nothing here ever holds that token.
|
|
458
|
+
*
|
|
459
|
+
* A hosted apply also arrives with a PLATFORM ENVIRONMENT (`PlatformEnv`, §29.7) — the base URL, the
|
|
460
|
+
* identity the install runs as, and a scoped key it will mint for an app that asks. That layer is
|
|
461
|
+
* read before `{ from_env }`'s own environment, and a declaration may ask for a value nobody has
|
|
462
|
+
* with `{ generate: true }`. Both are provisioned ONCE per app, because a value the platform invents
|
|
463
|
+
* has no stable digest and rewriting it every apply would rotate a live credential.
|
|
464
|
+
*
|
|
465
|
+
* An app also records the project that made it (ADR-0375), and that stamp is compared before any
|
|
466
|
+
* write: a name this project does not own is refused rather than adopted, which is what standing
|
|
467
|
+
* up a second blueprint did to the first one's live deployment.
|
|
468
|
+
*/
|
|
469
|
+
|
|
470
|
+
/** The platform environment's variable names (`canonical-spec §29.7`) — fixed, never inferred. */
|
|
471
|
+
declare const PLATFORM_ENV: {
|
|
472
|
+
/** The base URL of the API this apply ran through. */
|
|
473
|
+
readonly api_url: "NAIVE_API_URL";
|
|
474
|
+
/** The identity the install runs as, when the declaration declares exactly one. */
|
|
475
|
+
readonly identity_id: "NAIVE_IDENTITY_ID";
|
|
476
|
+
/** A scoped organization API key minted for the app that asks for it. */
|
|
477
|
+
readonly api_key: "NAIVE_API_KEY";
|
|
478
|
+
};
|
|
479
|
+
/**
|
|
480
|
+
* WHAT THE PLATFORM PROVIDES A HOSTED APPLY, AND THE ONE DISTINCTION THAT MATTERS
|
|
481
|
+
* (`canonical-spec §29.7`).
|
|
482
|
+
*
|
|
483
|
+
* Absent on a laptop apply, which reads the author's shell and nothing else — the hosted path gains
|
|
484
|
+
* a layer, the local path does not lose one.
|
|
485
|
+
*
|
|
486
|
+
* **What the platform KNOWS it always tells the app; what the platform must INVENT it invents only
|
|
487
|
+
* on request, and only once.** That is not a stylistic split. A known value has a stable digest, so
|
|
488
|
+
* the `value_hash` compare that makes `naive up` idempotent (§29.2) can say `unchanged` about it and
|
|
489
|
+
* re-asserting it every apply costs nothing. An invented one never matches its own previous digest,
|
|
490
|
+
* so re-inventing it every apply would rotate a live credential under a build that still holds the
|
|
491
|
+
* old one — and, for a minted key, leave one dead `api_key` row behind per apply.
|
|
492
|
+
*/
|
|
493
|
+
interface PlatformEnv {
|
|
494
|
+
/**
|
|
495
|
+
* Values the platform knows. Written to every app this apply deploys that declares an `env` —
|
|
496
|
+
* whether or not the declaration names them, which is the `VETTA_MCP_TOKEN` precedent (§29.2) —
|
|
497
|
+
* and read FIRST by a `{ from_env }` that does name them.
|
|
498
|
+
*/
|
|
499
|
+
known: Record<string, string>;
|
|
500
|
+
/**
|
|
501
|
+
* Values the platform must invent, by variable name, given the app that asked. Called at most
|
|
502
|
+
* once per app, and only when that app holds no secret of that name.
|
|
503
|
+
*/
|
|
504
|
+
minted?: Record<string, (app: string) => Promise<string>>;
|
|
409
505
|
}
|
|
410
506
|
|
|
411
507
|
/**
|
|
@@ -460,8 +556,43 @@ interface UpOptions {
|
|
|
460
556
|
* (ADR-0375). Off by default — adopting by default is what overwrote a live deployment.
|
|
461
557
|
*/
|
|
462
558
|
adopt?: boolean;
|
|
559
|
+
/**
|
|
560
|
+
* The built trees, by app name, for an apply that runs where there is no checkout: the platform
|
|
561
|
+
* applying a published artifact on an operator's behalf (`canonical-spec §31.1`). The bytes come
|
|
562
|
+
* out of the artifact's object-store entry, in the same file map `deploy_dir` would have been
|
|
563
|
+
* collected into — which is why `collectFiles` takes both and there is one digest (`files.ts`).
|
|
564
|
+
*
|
|
565
|
+
* Present ⇒ the disk is never read for an app, and an app whose tree is not in the map is refused
|
|
566
|
+
* by name rather than deployed empty. Absent ⇒ `dir` + `deploy_dir`, exactly as before.
|
|
567
|
+
*/
|
|
568
|
+
trees?: Record<string, Record<string, string>>;
|
|
569
|
+
/**
|
|
570
|
+
* Why a declared app is absent from `trees`, by app name — an object-store read that failed
|
|
571
|
+
* rather than an object that was never uploaded. Both come out of `builtTree` as one refusal;
|
|
572
|
+
* only the caller that did the reading can tell them apart, so only it can say (`apps.ts`).
|
|
573
|
+
*/
|
|
574
|
+
tree_errors?: Record<string, string>;
|
|
575
|
+
/**
|
|
576
|
+
* What an app's `{ from_env }` reads (`apps.ts`). The process's own environment by default —
|
|
577
|
+
* the author's shell, which is the whole point of `from_env` on a laptop. A hosted apply has no
|
|
578
|
+
* shell and passes the operator's **answers** instead (`canonical-spec §31.2`): the same
|
|
579
|
+
* declaration, resolved against the only environment that apply has.
|
|
580
|
+
*/
|
|
581
|
+
env?: Record<string, string | undefined>;
|
|
582
|
+
/**
|
|
583
|
+
* THE PLATFORM ENVIRONMENT (`canonical-spec §29.7`) — what the platform itself provides an app,
|
|
584
|
+
* read BEFORE `env` and written whether or not the declaration names it.
|
|
585
|
+
*
|
|
586
|
+
* Present only on a hosted apply, and that is the whole of the local/hosted difference: `naive up`
|
|
587
|
+
* from a checkout passes none, reads the author's shell, and behaves exactly as it always has.
|
|
588
|
+
*
|
|
589
|
+
* `NAIVE_IDENTITY_ID` is filled in here rather than by the caller, because the id does not exist
|
|
590
|
+
* until this run has created the identity — identities are upserted before apps for the same
|
|
591
|
+
* reason agents reference them by name.
|
|
592
|
+
*/
|
|
593
|
+
platform?: PlatformEnv;
|
|
463
594
|
}
|
|
464
595
|
/** Plan → apply → report. Every refusal is one line in the report, never a thrown escape. */
|
|
465
596
|
declare function up(config: Readonly<ProjectConfig>, client: VettaClient, opts?: UpOptions): Promise<UpReport>;
|
|
466
597
|
|
|
467
|
-
export { type AgentDecl, type AppDecl, BLUEPRINTS, BlueprintRefusal, CONFIG_CANDIDATES, type ConfigLoad, type CredentialDecl, type DefineInput, type FileSource, type IdentityDecl, type ProjectConfig, type ResourceAction, type ResourceReport, type ScheduleDecl, type SkillDecl, type Template, type TemplateChoice, type TemplateKind, type UpOptions, type UpReport, type VaultDecl, chooseTemplate, collectFiles, contentHash, defineProject, loadConfig, parseProject, up };
|
|
598
|
+
export { type AgentDecl, type AppDecl, BLUEPRINTS, BlueprintRefusal, CONFIG_CANDIDATES, type ConfigLoad, type CredentialDecl, type DefineInput, type FileSource, type IdentityDecl, PLATFORM_ENV, type PlatformEnv, type ProjectConfig, type ResourceAction, type ResourceReport, type ScheduleDecl, type SkillDecl, type Template, type TemplateChoice, type TemplateKind, type UpOptions, type UpReport, type VaultDecl, chooseTemplate, collectFiles, contentHash, defineProject, loadConfig, parseProject, up };
|
package/dist/index.js
CHANGED
|
@@ -93,6 +93,8 @@ var AUDIT_ACTIONS_P3 = [
|
|
|
93
93
|
"app.domain_connected",
|
|
94
94
|
"app.domain_disconnected",
|
|
95
95
|
"app.db_queried",
|
|
96
|
+
/** A ticket was minted that lets one browser past an app's own operator gate (§29.7). */
|
|
97
|
+
"app.entered",
|
|
96
98
|
"blueprint.installed"
|
|
97
99
|
];
|
|
98
100
|
var AUDIT_ACTIONS = [
|
|
@@ -817,6 +819,11 @@ var AppSecretSchema = z11.object({
|
|
|
817
819
|
value_hash: z11.string().nullable(),
|
|
818
820
|
created_at: Timestamp
|
|
819
821
|
});
|
|
822
|
+
var AppEntrySchema = z11.object({
|
|
823
|
+
object: z11.literal("app_entry"),
|
|
824
|
+
url: z11.string(),
|
|
825
|
+
ticket: z11.string()
|
|
826
|
+
});
|
|
820
827
|
var AppDomainStatusSchema = z11.enum(["pending", "connected", "failed"]);
|
|
821
828
|
var AppDomainSchema = z11.object({
|
|
822
829
|
object: z11.literal("app_domain"),
|
|
@@ -832,6 +839,26 @@ var APP_NAME = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
|
832
839
|
// ../core/src/schema/install.ts
|
|
833
840
|
import { z as z12 } from "zod";
|
|
834
841
|
var BlueprintInstallSourceSchema = z12.enum(["studio", "cli"]);
|
|
842
|
+
var ResourceActionSchema = z12.enum(["created", "updated", "unchanged", "deleted", "refused"]);
|
|
843
|
+
var ResourceReportSchema = z12.object({
|
|
844
|
+
name: z12.string().min(1),
|
|
845
|
+
action: ResourceActionSchema,
|
|
846
|
+
/** The `agt_`/`app_`/`ses_`… the line resolved to. Absent on a refusal that never got an id. */
|
|
847
|
+
id: z12.string().optional(),
|
|
848
|
+
/** An app's hostname once its build went live; `null` while it has none. Apps only. */
|
|
849
|
+
url: z12.string().nullable().optional(),
|
|
850
|
+
reason: z12.string().optional()
|
|
851
|
+
});
|
|
852
|
+
var BlueprintReportSchema = z12.object({
|
|
853
|
+
skills: z12.array(ResourceReportSchema),
|
|
854
|
+
identities: z12.array(ResourceReportSchema),
|
|
855
|
+
vaults: z12.array(ResourceReportSchema),
|
|
856
|
+
apps: z12.array(ResourceReportSchema),
|
|
857
|
+
agents: z12.array(ResourceReportSchema),
|
|
858
|
+
schedules: z12.array(ResourceReportSchema),
|
|
859
|
+
intake: z12.array(ResourceReportSchema),
|
|
860
|
+
skipped: z12.array(z12.string())
|
|
861
|
+
});
|
|
835
862
|
var BlueprintInstallSchema = z12.object({
|
|
836
863
|
id: idOf("bpi"),
|
|
837
864
|
object: z12.literal("blueprint_install"),
|
|
@@ -869,6 +896,19 @@ var BlueprintInstallSchema = z12.object({
|
|
|
869
896
|
* has it, so nothing is lost by not saying it twice.
|
|
870
897
|
*/
|
|
871
898
|
source: BlueprintInstallSourceSchema,
|
|
899
|
+
/**
|
|
900
|
+
* WHAT THE APPLY DID, or `null` when it did nothing but record.
|
|
901
|
+
*
|
|
902
|
+
* An apply that sends its own `declaration` has already reconciled it — that is what `naive up`
|
|
903
|
+
* is — so the row it writes is a record and this is `null`. An apply that names a **published**
|
|
904
|
+
* pair has reconciled nothing: it was never given the bytes (§31.1), so the server reads them
|
|
905
|
+
* from the catalog and runs the same engine over them, and this is what that run reported.
|
|
906
|
+
*
|
|
907
|
+
* It is stored rather than only returned because a refusal inside a successful apply — an app
|
|
908
|
+
* whose `env` reads a variable no hosted run has, a session refused for credit — is the one fact
|
|
909
|
+
* a person needs after the screen that showed it is gone.
|
|
910
|
+
*/
|
|
911
|
+
report: BlueprintReportSchema.nullable(),
|
|
872
912
|
applied_at: Timestamp,
|
|
873
913
|
created_at: Timestamp
|
|
874
914
|
});
|
|
@@ -895,6 +935,11 @@ var BlueprintAppSchema = z13.object({
|
|
|
895
935
|
description: z13.string().nullable()
|
|
896
936
|
});
|
|
897
937
|
var ContentHashSchema = z13.string().regex(/^[0-9a-f]{64}$/, "a lowercase sha-256 hex digest");
|
|
938
|
+
var BlueprintTreeSchema = z13.object({
|
|
939
|
+
/** `apps[].name` in the declaration — the same name §31.5 prints and the install ensures by. */
|
|
940
|
+
app: z13.string().min(1),
|
|
941
|
+
content_hash: ContentHashSchema
|
|
942
|
+
});
|
|
898
943
|
var BlueprintArtifactSchema = z13.object({
|
|
899
944
|
id: idOf("bpa"),
|
|
900
945
|
object: z13.literal("blueprint_artifact"),
|
|
@@ -904,7 +949,12 @@ var BlueprintArtifactSchema = z13.object({
|
|
|
904
949
|
template: z13.string().min(1),
|
|
905
950
|
/** The blueprint repository's own released version. Stored, never parsed (ADR-0401). */
|
|
906
951
|
version: z13.string().min(1),
|
|
907
|
-
|
|
952
|
+
/**
|
|
953
|
+
* The built trees, one per deployable app, ordered as the declaration orders its apps. Never
|
|
954
|
+
* empty: an artifact that built nothing is not an artifact, and the column behind it says so
|
|
955
|
+
* (migration 0040) exactly as the single `content_hash` it replaces was `not null`.
|
|
956
|
+
*/
|
|
957
|
+
trees: z13.array(BlueprintTreeSchema).min(1),
|
|
908
958
|
/**
|
|
909
959
|
* The setup questions, asked by the studio **before** provisioning. `QuestionField` (§7.1) is
|
|
910
960
|
* reused rather than restated: a setup question and a question a parked agent asks are the same
|
|
@@ -1065,7 +1115,11 @@ function chooseTemplate(blueprint, template) {
|
|
|
1065
1115
|
var BlueprintRefusal = class extends Error {
|
|
1066
1116
|
name = "BlueprintRefusal";
|
|
1067
1117
|
};
|
|
1068
|
-
var EnvValueSchema = z16.union([
|
|
1118
|
+
var EnvValueSchema = z16.union([
|
|
1119
|
+
z16.string(),
|
|
1120
|
+
z16.object({ from_env: z16.string().min(1) }),
|
|
1121
|
+
z16.object({ generate: z16.literal(true) })
|
|
1122
|
+
]);
|
|
1069
1123
|
var AppDeclSchema = z16.object({
|
|
1070
1124
|
/** Becomes a hostname, so the grammar is the platform's `APP_NAME` (a DNS label). */
|
|
1071
1125
|
name: z16.string().regex(APP_NAME, "a lowercase DNS label, 63 characters or fewer"),
|
|
@@ -1458,7 +1512,7 @@ async function tombstone(name, current, remove, dryRun) {
|
|
|
1458
1512
|
|
|
1459
1513
|
// src/agents.ts
|
|
1460
1514
|
function desiredTools(decl, current, ids) {
|
|
1461
|
-
if (
|
|
1515
|
+
if (ids === void 0) return decl.tools;
|
|
1462
1516
|
const base = decl.tools ?? current?.tools ?? { default_config: { permission: "allow" }, configs: {} };
|
|
1463
1517
|
const appsTool = base.configs["apps"] ?? { enabled: true, permission: base.default_config.permission };
|
|
1464
1518
|
return {
|
|
@@ -1483,14 +1537,14 @@ async function createAgent(client, decl, tools, identityId) {
|
|
|
1483
1537
|
if (identityId !== void 0) await client.identities.attach(created.id, identityId);
|
|
1484
1538
|
return { name: decl.name, action: "created", id: created.id };
|
|
1485
1539
|
}
|
|
1486
|
-
async function upsertAgent(client, decl, existing, ids, dryRun) {
|
|
1540
|
+
async function upsertAgent(client, decl, existing, ids, dryRun, scoped) {
|
|
1487
1541
|
const unresolved = (decl.allowed_apps ?? []).filter((name) => !ids.apps.has(name));
|
|
1488
1542
|
const identityId = decl.identity === void 0 ? void 0 : ids.identities.get(decl.identity);
|
|
1489
1543
|
const missing = unresolved.length > 0 ? `allowed_apps needs app "${unresolved[0]}"` : decl.identity !== void 0 && identityId === void 0 ? `identity needs "${decl.identity}"` : null;
|
|
1490
1544
|
if (missing !== null && !dryRun) {
|
|
1491
1545
|
return { name: decl.name, action: "refused", reason: `${missing}, which was not provisioned in this run` };
|
|
1492
1546
|
}
|
|
1493
|
-
const appIds =
|
|
1547
|
+
const appIds = decl.allowed_apps !== void 0 ? decl.allowed_apps.flatMap((name) => ids.apps.get(name) ?? []) : scoped;
|
|
1494
1548
|
const current = existing.find((agent) => agent.name === decl.name) ?? null;
|
|
1495
1549
|
const tools = desiredTools(decl, current, appIds);
|
|
1496
1550
|
if (!current) return dryRun ? { name: decl.name, action: "created" } : createAgent(client, decl, tools, identityId);
|
|
@@ -1563,7 +1617,7 @@ async function reconcileSchedules(client, agent, declared, live, identityIds, dr
|
|
|
1563
1617
|
}
|
|
1564
1618
|
|
|
1565
1619
|
// src/apps.ts
|
|
1566
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
1620
|
+
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
1567
1621
|
import { resolve as resolve2 } from "node:path";
|
|
1568
1622
|
async function findApp(client, name, skipped) {
|
|
1569
1623
|
const rows = await listAll(client.config, "/v1/apps", AppSchema, skipped, { name });
|
|
@@ -1575,25 +1629,74 @@ function ownershipRefusal(name, project, owner) {
|
|
|
1575
1629
|
return `app "${name}" is owned by ${held}; this project is "${project}". Rename the app in your config, or re-run \`naive up --adopt\` to ${verb} it.`;
|
|
1576
1630
|
}
|
|
1577
1631
|
var ensureApp = (client, body) => client.apps.create(body);
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1632
|
+
var PLATFORM_ENV = {
|
|
1633
|
+
/** The base URL of the API this apply ran through. */
|
|
1634
|
+
api_url: "NAIVE_API_URL",
|
|
1635
|
+
/** The identity the install runs as, when the declaration declares exactly one. */
|
|
1636
|
+
identity_id: "NAIVE_IDENTITY_ID",
|
|
1637
|
+
/** A scoped organization API key minted for the app that asks for it. */
|
|
1638
|
+
api_key: "NAIVE_API_KEY"
|
|
1639
|
+
};
|
|
1640
|
+
function planEnv(decl, opts) {
|
|
1641
|
+
if (decl.env === void 0) return [];
|
|
1642
|
+
const plan = /* @__PURE__ */ new Map();
|
|
1643
|
+
for (const [name, value] of Object.entries(decl.env)) {
|
|
1581
1644
|
if (typeof value === "string") {
|
|
1582
|
-
|
|
1645
|
+
plan.set(name, { name, value });
|
|
1583
1646
|
continue;
|
|
1584
1647
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1648
|
+
if ("generate" in value) {
|
|
1649
|
+
plan.set(name, { name, invent: async () => generated() });
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
const known = opts.platform?.known[value.from_env];
|
|
1653
|
+
const mint = opts.platform?.minted?.[value.from_env];
|
|
1654
|
+
if (known !== void 0) plan.set(name, { name, value: known });
|
|
1655
|
+
else if (mint !== void 0) plan.set(name, { name, invent: () => mint(decl.name) });
|
|
1656
|
+
else {
|
|
1657
|
+
const local = opts.env[value.from_env];
|
|
1658
|
+
if (local === void 0) throw new Error(`env ${name} reads local variable ${value.from_env}, which is not set`);
|
|
1659
|
+
plan.set(name, { name, value: local });
|
|
1660
|
+
}
|
|
1588
1661
|
}
|
|
1589
|
-
|
|
1662
|
+
for (const [name, value] of Object.entries(opts.platform?.known ?? {})) plan.set(name, { name, value });
|
|
1663
|
+
return [...plan.values()];
|
|
1590
1664
|
}
|
|
1665
|
+
async function materialize(plan, held, dryRun) {
|
|
1666
|
+
const values = {};
|
|
1667
|
+
const unmade = [];
|
|
1668
|
+
for (const one of plan) {
|
|
1669
|
+
if (one.value !== void 0) values[one.name] = one.value;
|
|
1670
|
+
else if (held.has(one.name)) continue;
|
|
1671
|
+
else if (dryRun) unmade.push(one.name);
|
|
1672
|
+
else values[one.name] = await invent(one);
|
|
1673
|
+
}
|
|
1674
|
+
return { values, unmade };
|
|
1675
|
+
}
|
|
1676
|
+
async function invent(one) {
|
|
1677
|
+
try {
|
|
1678
|
+
return await one.invent();
|
|
1679
|
+
} catch (cause) {
|
|
1680
|
+
throw new Error(
|
|
1681
|
+
`env ${one.name} is provisioned by the platform, and could not be: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
var generated = () => randomBytes(32).toString("base64url");
|
|
1686
|
+
var EMPTY = /* @__PURE__ */ new Set();
|
|
1591
1687
|
function secretHash(name, value) {
|
|
1592
1688
|
return createHash2("sha256").update(`${name}\0`).update(value).digest("hex");
|
|
1593
1689
|
}
|
|
1594
|
-
function
|
|
1690
|
+
function builtTree(decl, opts) {
|
|
1595
1691
|
if (!decl.deploy_dir) return null;
|
|
1596
|
-
const
|
|
1692
|
+
const hosted = opts.trees;
|
|
1693
|
+
if (hosted !== void 0 && hosted[decl.name] === void 0) {
|
|
1694
|
+
const failed = opts.tree_errors?.[decl.name];
|
|
1695
|
+
throw new Error(
|
|
1696
|
+
`app "${decl.name}" declares deploy_dir "${decl.deploy_dir}", but this apply was handed no built tree for it` + (failed === void 0 ? "" : `: ${failed}`)
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
const files = collectFiles(hosted?.[decl.name] ?? resolve2(opts.dir, decl.deploy_dir));
|
|
1597
1700
|
return { files, hash: contentHash(files) };
|
|
1598
1701
|
}
|
|
1599
1702
|
async function writeApp(client, id, env, tree) {
|
|
@@ -1613,7 +1716,7 @@ async function settleDeploy(client, id) {
|
|
|
1613
1716
|
await new Promise((resolve4) => setTimeout(resolve4, SETTLE_MS));
|
|
1614
1717
|
}
|
|
1615
1718
|
}
|
|
1616
|
-
async function createApp(client, decl, env, opts, appIds) {
|
|
1719
|
+
async function createApp(client, decl, env, tree, opts, appIds) {
|
|
1617
1720
|
const created = await ensureApp(client, {
|
|
1618
1721
|
name: decl.name,
|
|
1619
1722
|
type: decl.type,
|
|
@@ -1622,12 +1725,12 @@ async function createApp(client, decl, env, opts, appIds) {
|
|
|
1622
1725
|
...decl.mcp !== void 0 ? { mcp: decl.mcp } : {}
|
|
1623
1726
|
});
|
|
1624
1727
|
appIds.set(decl.name, created.id);
|
|
1625
|
-
await writeApp(client, created.id, env,
|
|
1728
|
+
await writeApp(client, created.id, env, tree);
|
|
1626
1729
|
return { name: decl.name, action: "created", id: created.id, url: (await client.apps.get(created.id)).url };
|
|
1627
1730
|
}
|
|
1628
1731
|
async function upsertApp(client, decl, opts, appIds) {
|
|
1629
|
-
const
|
|
1630
|
-
const names =
|
|
1732
|
+
const declared = planEnv(decl, opts);
|
|
1733
|
+
const names = declared.map((one) => one.name);
|
|
1631
1734
|
const note = (notes) => notes.length > 0 ? { reason: notes.join("; ") } : {};
|
|
1632
1735
|
const mcpPlan = decl.mcp !== void 0 ? [`will set mcp ${decl.mcp}`] : [];
|
|
1633
1736
|
const current = await findApp(client, decl.name, opts.skipped);
|
|
@@ -1640,7 +1743,8 @@ async function upsertApp(client, decl, opts, appIds) {
|
|
|
1640
1743
|
...note([...willSet, ...mcpPlan, ...decl.deploy_dir ? [`will deploy ${decl.deploy_dir}`] : []])
|
|
1641
1744
|
};
|
|
1642
1745
|
}
|
|
1643
|
-
|
|
1746
|
+
const { values } = await materialize(declared, EMPTY, false);
|
|
1747
|
+
return createApp(client, decl, values, builtTree(decl, opts), opts, appIds);
|
|
1644
1748
|
}
|
|
1645
1749
|
const owned = current.project === opts.project;
|
|
1646
1750
|
if (!owned && !opts.adopt) {
|
|
@@ -1665,19 +1769,21 @@ async function upsertApp(client, decl, opts, appIds) {
|
|
|
1665
1769
|
if (!owned && !opts.dryRun) {
|
|
1666
1770
|
await ensureApp(client, { name: decl.name, type: decl.type, project: opts.project, adopt: true });
|
|
1667
1771
|
}
|
|
1668
|
-
const live =
|
|
1669
|
-
const
|
|
1772
|
+
const live = declared.length === 0 ? [] : (await client.apps.secrets(current.id)).data;
|
|
1773
|
+
const managed = new Set(names);
|
|
1774
|
+
const unmanaged = live.map((secret) => secret.name).filter((name) => !managed.has(name));
|
|
1670
1775
|
const held = new Map(live.map((secret) => [secret.name, secret.value_hash]));
|
|
1776
|
+
const { values: env, unmade } = await materialize(declared, new Set(held.keys()), opts.dryRun);
|
|
1671
1777
|
const staleEnv = Object.fromEntries(
|
|
1672
1778
|
Object.entries(env).filter(([name, value]) => held.get(name) !== secretHash(name, value))
|
|
1673
1779
|
);
|
|
1674
|
-
const staleNames = Object.keys(staleEnv);
|
|
1780
|
+
const staleNames = [...Object.keys(staleEnv), ...unmade];
|
|
1675
1781
|
const patch = {
|
|
1676
1782
|
...decl.description !== void 0 && decl.description !== current.description ? { description: decl.description } : {},
|
|
1677
1783
|
...(decl.mcp ?? null) !== current.mcp ? { mcp: decl.mcp ?? null } : {}
|
|
1678
1784
|
};
|
|
1679
1785
|
const drifted = Object.keys(patch).length > 0;
|
|
1680
|
-
const tree =
|
|
1786
|
+
const tree = builtTree(decl, opts);
|
|
1681
1787
|
const newest = tree ? (await client.apps.deployments(current.id, { limit: 1 })).data[0] : void 0;
|
|
1682
1788
|
const stale = tree !== null && newest?.content_hash !== tree.hash;
|
|
1683
1789
|
const action = !owned || drifted || stale || staleNames.length > 0 ? "updated" : "unchanged";
|
|
@@ -1725,14 +1831,14 @@ async function upsertVault(client, decl, existing, identityIds, dryRun) {
|
|
|
1725
1831
|
}
|
|
1726
1832
|
return { name: decl.name, action: "unchanged", id: current.id };
|
|
1727
1833
|
}
|
|
1728
|
-
async function upsertCredential(client, vault, decl, existing, dryRun) {
|
|
1834
|
+
async function upsertCredential(client, vault, decl, existing, dryRun, from) {
|
|
1729
1835
|
const name = `${vault.name} / ${decl.key}`;
|
|
1730
1836
|
if (vault.action === "refused") throw new Error(`vault "${vault.name}" was not provisioned in this run`);
|
|
1731
1837
|
const current = existing.find(
|
|
1732
1838
|
(row) => row.kind === decl.kind && row.key === decl.key && row.host === (decl.host ?? null) && row.mcp_server_url === (decl.mcp_server_url ?? null)
|
|
1733
1839
|
);
|
|
1734
1840
|
if (current) return { name, action: "unchanged", id: current.id };
|
|
1735
|
-
const value =
|
|
1841
|
+
const value = from[decl.value.from_env];
|
|
1736
1842
|
if (value === void 0) throw new Error(`credential ${decl.key} reads local variable ${decl.value.from_env}, which is not set`);
|
|
1737
1843
|
if (dryRun || vault.id === void 0) return { name, action: "created" };
|
|
1738
1844
|
const created = await client.vaults.credentials.create(vault.id, {
|
|
@@ -1746,7 +1852,17 @@ async function upsertCredential(client, vault, decl, existing, dryRun) {
|
|
|
1746
1852
|
}
|
|
1747
1853
|
|
|
1748
1854
|
// src/up.ts
|
|
1855
|
+
function identityEnv(config, ids) {
|
|
1856
|
+
if (config.identities.length !== 1) return {};
|
|
1857
|
+
const id = ids.get(config.identities[0].name);
|
|
1858
|
+
return id === void 0 ? {} : { [PLATFORM_ENV.identity_id]: id };
|
|
1859
|
+
}
|
|
1749
1860
|
async function upsertSkill(client, decl, opts) {
|
|
1861
|
+
if (opts.hosted) {
|
|
1862
|
+
throw new Error(
|
|
1863
|
+
`skill "${decl.slug}" is declared as the file "${decl.file}", and a hosted apply has no checkout to read it from. Push it with \`naive up\` from a clone, or publish it as part of the blueprint.`
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1750
1866
|
const content = readFileSync3(resolve3(opts.dir, decl.file), "utf8");
|
|
1751
1867
|
const sha256 = createHash3("sha256").update(content).digest("hex");
|
|
1752
1868
|
const current = await absent(client.skills.get(decl.slug));
|
|
@@ -1825,8 +1941,9 @@ async function up(config, client, opts = {}) {
|
|
|
1825
1941
|
const ids = { apps: /* @__PURE__ */ new Map(), identities: /* @__PURE__ */ new Map() };
|
|
1826
1942
|
const report = { dry_run: dryRun, ...applied(config), skills: [], identities: [], vaults: [], apps: [], agents: [], schedules: [], intake: [], skipped: [] };
|
|
1827
1943
|
const owner = { project: config.name, adopt: opts.adopt ?? false, skipped: report.skipped };
|
|
1944
|
+
const hosted = opts.trees !== void 0;
|
|
1828
1945
|
for (const decl of config.skills) {
|
|
1829
|
-
await attempt(report.skills, decl.slug, () => upsertSkill(client, decl, { dryRun, dir }));
|
|
1946
|
+
await attempt(report.skills, decl.slug, () => upsertSkill(client, decl, { dryRun, dir, hosted }));
|
|
1830
1947
|
}
|
|
1831
1948
|
for (const decl of config.identities) {
|
|
1832
1949
|
await attempt(report.identities, decl.name, () => upsertIdentity(client, decl, dryRun, ids.identities));
|
|
@@ -1837,17 +1954,39 @@ async function up(config, client, opts = {}) {
|
|
|
1837
1954
|
const vaultId = line.action === "unchanged" ? line.id : void 0;
|
|
1838
1955
|
const rows = vaultId === void 0 ? [] : await listAll(client.config, `/v1/vaults/${vaultId}/credentials`, VaultCredentialSchema, report.skipped);
|
|
1839
1956
|
for (const cred of decl.credentials) {
|
|
1840
|
-
await attempt(
|
|
1957
|
+
await attempt(
|
|
1958
|
+
report.vaults,
|
|
1959
|
+
`${decl.name} / ${cred.key}`,
|
|
1960
|
+
() => upsertCredential(client, line, cred, rows, dryRun, opts.env ?? process.env)
|
|
1961
|
+
);
|
|
1841
1962
|
}
|
|
1842
1963
|
}
|
|
1964
|
+
const platform = opts.platform && {
|
|
1965
|
+
...opts.platform,
|
|
1966
|
+
known: { ...opts.platform.known, ...identityEnv(config, ids.identities) }
|
|
1967
|
+
};
|
|
1843
1968
|
for (const decl of config.apps) {
|
|
1844
|
-
await attempt(
|
|
1969
|
+
await attempt(
|
|
1970
|
+
report.apps,
|
|
1971
|
+
decl.name,
|
|
1972
|
+
() => upsertApp(
|
|
1973
|
+
client,
|
|
1974
|
+
decl,
|
|
1975
|
+
{ ...owner, dryRun, dir, trees: opts.trees, tree_errors: opts.tree_errors, env: opts.env ?? process.env, platform },
|
|
1976
|
+
ids.apps
|
|
1977
|
+
)
|
|
1978
|
+
);
|
|
1845
1979
|
}
|
|
1846
1980
|
const existing = config.agents.length > 0 || config.removed.agents.length > 0 || config.kept.agents.length > 0 ? await listAll(client.config, "/v1/agents", AgentSchema, report.skipped) : [];
|
|
1847
1981
|
const deployments = config.agents.some((decl) => decl.schedules !== void 0) ? await listAll(client.config, "/v1/deployments", DeploymentSchema, report.skipped) : [];
|
|
1982
|
+
const scoped = config.apps.length > 0 ? [...ids.apps.values()] : void 0;
|
|
1848
1983
|
const briefed = [];
|
|
1849
1984
|
for (const decl of config.agents) {
|
|
1850
|
-
const line = await attempt(
|
|
1985
|
+
const line = await attempt(
|
|
1986
|
+
report.agents,
|
|
1987
|
+
decl.name,
|
|
1988
|
+
() => upsertAgent(client, decl, existing, ids, dryRun, scoped)
|
|
1989
|
+
);
|
|
1851
1990
|
if (decl.schedules !== void 0) {
|
|
1852
1991
|
await reconcileSchedules(client, line, decl.schedules, deployments, ids.identities, dryRun, report.schedules);
|
|
1853
1992
|
}
|
|
@@ -1864,6 +2003,7 @@ export {
|
|
|
1864
2003
|
BLUEPRINTS,
|
|
1865
2004
|
BlueprintRefusal,
|
|
1866
2005
|
CONFIG_CANDIDATES,
|
|
2006
|
+
PLATFORM_ENV,
|
|
1867
2007
|
chooseTemplate,
|
|
1868
2008
|
collectFiles,
|
|
1869
2009
|
contentHash,
|