@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 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 ResourceAction = "created" | "updated" | "unchanged" | "deleted" | "refused";
402
- interface ResourceReport {
403
- name: string;
404
- action: ResourceAction;
405
- id?: string;
406
- url?: string | null;
407
- /** Why, for `refused`; what would happen, for a dry-run line that needs explaining. */
408
- reason?: string;
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
- content_hash: ContentHashSchema,
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([z16.string(), z16.object({ from_env: z16.string().min(1) })]);
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 (decl.allowed_apps === void 0) return decl.tools;
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 = (decl.allowed_apps ?? []).flatMap((name) => ids.apps.get(name) ?? []);
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
- function resolveEnv(decl) {
1579
- const env = {};
1580
- for (const [name, value] of Object.entries(decl.env ?? {})) {
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
- env[name] = value;
1645
+ plan.set(name, { name, value });
1583
1646
  continue;
1584
1647
  }
1585
- const local = process.env[value.from_env];
1586
- if (local === void 0) throw new Error(`env ${name} reads local variable ${value.from_env}, which is not set`);
1587
- env[name] = local;
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
- return env;
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 localTree(decl, dir) {
1690
+ function builtTree(decl, opts) {
1595
1691
  if (!decl.deploy_dir) return null;
1596
- const files = collectFiles(resolve2(dir, decl.deploy_dir));
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, localTree(decl, opts.dir));
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 env = resolveEnv(decl);
1630
- const names = Object.keys(env);
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
- return createApp(client, decl, env, opts, appIds);
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 = decl.env === void 0 ? [] : (await client.apps.secrets(current.id)).data;
1669
- const unmanaged = live.map((secret) => secret.name).filter((name) => !(name in env));
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 = localTree(decl, opts.dir);
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 = process.env[decl.value.from_env];
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(report.vaults, `${decl.name} / ${cred.key}`, () => upsertCredential(client, line, cred, rows, dryRun));
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(report.apps, decl.name, () => upsertApp(client, decl, { ...owner, dryRun, dir }, ids.apps));
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(report.agents, decl.name, () => upsertAgent(client, decl, existing, ids, dryRun));
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,