@usenaive-sdk/blueprints 0.1.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 +199 -9
- package/dist/index.js +504 -163
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -92,7 +92,10 @@ var AUDIT_ACTIONS_P3 = [
|
|
|
92
92
|
"app.secret_deleted",
|
|
93
93
|
"app.domain_connected",
|
|
94
94
|
"app.domain_disconnected",
|
|
95
|
-
"app.db_queried"
|
|
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",
|
|
98
|
+
"blueprint.installed"
|
|
96
99
|
];
|
|
97
100
|
var AUDIT_ACTIONS = [
|
|
98
101
|
...AUDIT_ACTIONS_SHIPPED,
|
|
@@ -239,7 +242,16 @@ var StopReasonSchema = z4.enum([
|
|
|
239
242
|
* Distinct from `awaiting_input`: nobody is being asked for anything, so no human can unblock it —
|
|
240
243
|
* the child's completion does. Reporting `idle`/`end_turn` here would claim the task was finished.
|
|
241
244
|
*/
|
|
242
|
-
"awaiting_delegation"
|
|
245
|
+
"awaiting_delegation",
|
|
246
|
+
/**
|
|
247
|
+
* The turn yielded because the agent asked the operator a question and cannot proceed without
|
|
248
|
+
* the answer (§7.1). Distinct from all three neighbours, and none can stand in: `awaiting_input`
|
|
249
|
+
* says only that there is nothing left to do, so a client reading it renders a bare composer
|
|
250
|
+
* where a specific answer is owed; `awaiting_approval` says the block is a tool call to allow or
|
|
251
|
+
* deny, and its route carries a verb and no payload; `awaiting_delegation` says no human can
|
|
252
|
+
* unblock it at all. What is being asked is the parked `pending_actions` row.
|
|
253
|
+
*/
|
|
254
|
+
"awaiting_answer"
|
|
243
255
|
]);
|
|
244
256
|
var ThreadRoleSchema = z4.enum(["coordinator", "member"]);
|
|
245
257
|
var MessageSchema = z4.object({
|
|
@@ -252,10 +264,57 @@ var ToolResultSchema = z4.object({
|
|
|
252
264
|
is_error: z4.boolean().optional(),
|
|
253
265
|
content: z4.string().optional()
|
|
254
266
|
});
|
|
267
|
+
var QuestionFieldBase = {
|
|
268
|
+
key: z4.string().min(1).max(64),
|
|
269
|
+
label: z4.string().min(1).max(200),
|
|
270
|
+
help: z4.string().max(500).optional()
|
|
271
|
+
};
|
|
272
|
+
var QuestionFieldSchema = z4.discriminatedUnion("type", [
|
|
273
|
+
z4.object({
|
|
274
|
+
...QuestionFieldBase,
|
|
275
|
+
type: z4.literal("text"),
|
|
276
|
+
placeholder: z4.string().max(200).optional()
|
|
277
|
+
}),
|
|
278
|
+
z4.object({
|
|
279
|
+
...QuestionFieldBase,
|
|
280
|
+
type: z4.literal("choice"),
|
|
281
|
+
/**
|
|
282
|
+
* Plain strings: the option is both the value and the label, because the answer is read back by
|
|
283
|
+
* a model, which wants the words and not an id. Fewer than two is not a choice, and more than
|
|
284
|
+
* twelve is a list — which belongs in the prompt.
|
|
285
|
+
*/
|
|
286
|
+
options: z4.array(z4.string().min(1).max(200)).min(2).max(12),
|
|
287
|
+
multiple: z4.boolean().default(false),
|
|
288
|
+
/**
|
|
289
|
+
* The free-text escape, and it defaults to **true** on purpose: the agent wrote the options and
|
|
290
|
+
* the agent is the one that does not know the answer, so an operator must always be able to say
|
|
291
|
+
* something it did not think of. A question that truly admits only its list opts out explicitly.
|
|
292
|
+
*/
|
|
293
|
+
other: z4.boolean().default(true)
|
|
294
|
+
})
|
|
295
|
+
]);
|
|
296
|
+
var QuestionSchema = z4.object({
|
|
297
|
+
prompt: z4.string().min(1).max(1e3),
|
|
298
|
+
fields: z4.array(QuestionFieldSchema).min(1).max(3)
|
|
299
|
+
});
|
|
300
|
+
var AnswersSchema = z4.record(
|
|
301
|
+
z4.string().min(1).max(64),
|
|
302
|
+
z4.union([z4.string().max(2e3), z4.array(z4.string().max(2e3)).min(1).max(12)])
|
|
303
|
+
).refine((answers) => Object.keys(answers).length <= 3, {
|
|
304
|
+
message: "a question has at most 3 fields, so an answer has at most 3 keys"
|
|
305
|
+
});
|
|
255
306
|
var PendingActionSchema = z4.object({
|
|
307
|
+
kind: z4.enum(["tool", "question"]).default("tool"),
|
|
256
308
|
tool_call_id: z4.string(),
|
|
257
309
|
name: z4.string(),
|
|
258
|
-
args: z4.record(z4.string(), z4.unknown())
|
|
310
|
+
args: z4.record(z4.string(), z4.unknown()),
|
|
311
|
+
/**
|
|
312
|
+
* Present iff `kind` is `"question"`, and it is the parse of `args` rather than a second source
|
|
313
|
+
* of truth: `args` is what the model called and therefore what replays byte-identically, while
|
|
314
|
+
* `question` is what a renderer reads without having to know that `ask_operator`'s arguments
|
|
315
|
+
* happen to be the question. Same content, two jobs.
|
|
316
|
+
*/
|
|
317
|
+
question: QuestionSchema.optional()
|
|
259
318
|
});
|
|
260
319
|
var SessionSchema = z4.object({
|
|
261
320
|
id: idOf("ses"),
|
|
@@ -314,6 +373,10 @@ var ToolConfirmationSchema = z4.object({
|
|
|
314
373
|
decision: z4.enum(["allow", "deny"]),
|
|
315
374
|
reason: z4.string().optional()
|
|
316
375
|
});
|
|
376
|
+
var SessionAnswerSchema = z4.object({
|
|
377
|
+
tool_call_id: z4.string(),
|
|
378
|
+
answers: z4.record(z4.string(), z4.union([z4.string(), z4.array(z4.string())]))
|
|
379
|
+
});
|
|
317
380
|
|
|
318
381
|
// ../core/src/schema/event.ts
|
|
319
382
|
import { z as z5 } from "zod";
|
|
@@ -756,6 +819,11 @@ var AppSecretSchema = z11.object({
|
|
|
756
819
|
value_hash: z11.string().nullable(),
|
|
757
820
|
created_at: Timestamp
|
|
758
821
|
});
|
|
822
|
+
var AppEntrySchema = z11.object({
|
|
823
|
+
object: z11.literal("app_entry"),
|
|
824
|
+
url: z11.string(),
|
|
825
|
+
ticket: z11.string()
|
|
826
|
+
});
|
|
759
827
|
var AppDomainStatusSchema = z11.enum(["pending", "connected", "failed"]);
|
|
760
828
|
var AppDomainSchema = z11.object({
|
|
761
829
|
object: z11.literal("app_domain"),
|
|
@@ -768,36 +836,168 @@ var AppDomainSchema = z11.object({
|
|
|
768
836
|
var ENV_NAME = /^[A-Z_][A-Z0-9_]*$/;
|
|
769
837
|
var APP_NAME = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
770
838
|
|
|
771
|
-
// ../core/src/schema/
|
|
839
|
+
// ../core/src/schema/install.ts
|
|
772
840
|
import { z as z12 } from "zod";
|
|
773
|
-
var
|
|
774
|
-
var
|
|
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
|
+
});
|
|
862
|
+
var BlueprintInstallSchema = z12.object({
|
|
863
|
+
id: idOf("bpi"),
|
|
864
|
+
object: z12.literal("blueprint_install"),
|
|
865
|
+
/** The customer's own `defineProject({ name })` — the same word `app.project` stamps (§29.2). */
|
|
866
|
+
project: z12.string().min(1),
|
|
867
|
+
/** The pair the declaration resolved to; `null` for a declaration that names no blueprint. */
|
|
868
|
+
blueprint: z12.string().nullable(),
|
|
869
|
+
template: z12.string().nullable(),
|
|
870
|
+
/**
|
|
871
|
+
* Applies, not versions of anything: 1 on the first apply, +1 on each one after, whichever writer
|
|
872
|
+
* made it. It is not an agent version and it never rolls back.
|
|
873
|
+
*/
|
|
874
|
+
revision: z12.number().int().positive(),
|
|
875
|
+
/**
|
|
876
|
+
* The blueprint artifact version this came from, or `null` when it was applied from a working
|
|
877
|
+
* tree — an operator editing a clone has no version to name.
|
|
878
|
+
*/
|
|
879
|
+
artifact_version: z12.string().nullable(),
|
|
880
|
+
/**
|
|
881
|
+
* The setup answers this project was configured with (§7.1 fields, keyed by their `key`), so a
|
|
882
|
+
* re-apply does not re-ask and both writers configure from the same answers. Configuration only:
|
|
883
|
+
* a secret is a vault credential (§23) or an app secret (§29.2), and neither is ever an answer.
|
|
884
|
+
*/
|
|
885
|
+
answers: z12.record(z12.string(), z12.unknown()),
|
|
886
|
+
/**
|
|
887
|
+
* NO `declaration` ON THE WIRE, and that is the reconciliation of §29.6 with §31.1.
|
|
888
|
+
*
|
|
889
|
+
* The applied bytes are stored — a `revision_conflict` names the paths two writers disagree at,
|
|
890
|
+
* and it can only do that by holding what was applied. They are not published. An apply that
|
|
891
|
+
* names a published `blueprint` and `template` sends no declaration: the server reads the
|
|
892
|
+
* catalog's, which §31.1 says never leaves it. Echoing it back on the 201 (and again on every
|
|
893
|
+
* later list) would hand any organization that can sign up the whole private config of a
|
|
894
|
+
* blueprint it has installed nothing of — every agent's system prompt, its `mcp_servers`, its
|
|
895
|
+
* vault credential declarations and its app `env` keys. A caller that sent a declaration already
|
|
896
|
+
* has it, so nothing is lost by not saying it twice.
|
|
897
|
+
*/
|
|
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(),
|
|
912
|
+
applied_at: Timestamp,
|
|
913
|
+
created_at: Timestamp
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
// ../core/src/schema/blueprint.ts
|
|
917
|
+
import { z as z13 } from "zod";
|
|
918
|
+
var BlueprintScheduleSchema = z13.object({
|
|
919
|
+
agent: z13.string().min(1),
|
|
920
|
+
cron: z13.string().min(1),
|
|
921
|
+
timezone: z13.string().nullable(),
|
|
922
|
+
/** The message each fire opens its session with — the sentence worth printing. */
|
|
923
|
+
input: z13.string().min(1),
|
|
924
|
+
budget_micro_usd: MicroUsdField.nonnegative()
|
|
925
|
+
});
|
|
926
|
+
var BlueprintCrewSchema = z13.object({
|
|
927
|
+
name: z13.string().min(1),
|
|
928
|
+
description: z13.string().nullable(),
|
|
929
|
+
model: z13.string().min(1),
|
|
930
|
+
briefed: z13.boolean()
|
|
931
|
+
});
|
|
932
|
+
var BlueprintAppSchema = z13.object({
|
|
933
|
+
name: z13.string().min(1),
|
|
934
|
+
type: AppTypeSchema,
|
|
935
|
+
description: z13.string().nullable()
|
|
936
|
+
});
|
|
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
|
+
});
|
|
943
|
+
var BlueprintArtifactSchema = z13.object({
|
|
944
|
+
id: idOf("bpa"),
|
|
945
|
+
object: z13.literal("blueprint_artifact"),
|
|
946
|
+
/** The machine — its repository, screens and `/api/*`. */
|
|
947
|
+
blueprint: z13.string().min(1),
|
|
948
|
+
/** Which of that blueprint's templates provides the crew. */
|
|
949
|
+
template: z13.string().min(1),
|
|
950
|
+
/** The blueprint repository's own released version. Stored, never parsed (ADR-0401). */
|
|
951
|
+
version: z13.string().min(1),
|
|
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),
|
|
958
|
+
/**
|
|
959
|
+
* The setup questions, asked by the studio **before** provisioning. `QuestionField` (§7.1) is
|
|
960
|
+
* reused rather than restated: a setup question and a question a parked agent asks are the same
|
|
961
|
+
* object at different times, so one component renders both.
|
|
962
|
+
*/
|
|
963
|
+
questions: z13.array(QuestionFieldSchema),
|
|
964
|
+
schedules: z13.array(BlueprintScheduleSchema),
|
|
965
|
+
/** Who the crew is, and what it deploys — §31.5. Derived from `declaration`, never stored twice. */
|
|
966
|
+
agents: z13.array(BlueprintCrewSchema),
|
|
967
|
+
apps: z13.array(BlueprintAppSchema),
|
|
968
|
+
published_at: Timestamp
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
// ../core/src/schema/vault.ts
|
|
972
|
+
import { z as z14 } from "zod";
|
|
973
|
+
var VaultCredentialKindSchema = z14.enum(["env_var", "static_bearer", "mcp_oauth"]);
|
|
974
|
+
var VaultSchema = z14.object({
|
|
775
975
|
id: idOf("vlt"),
|
|
776
|
-
object:
|
|
976
|
+
object: z14.literal("vault"),
|
|
777
977
|
identity_id: idOf("idn").nullable(),
|
|
778
|
-
display_name:
|
|
779
|
-
credential_count:
|
|
978
|
+
display_name: z14.string().min(1).max(128),
|
|
979
|
+
credential_count: z14.number().int().nonnegative(),
|
|
780
980
|
created_at: Timestamp
|
|
781
981
|
});
|
|
782
|
-
var VaultCredentialSchema =
|
|
982
|
+
var VaultCredentialSchema = z14.object({
|
|
783
983
|
id: idOf("vcr"),
|
|
784
|
-
object:
|
|
984
|
+
object: z14.literal("vault_credential"),
|
|
785
985
|
vault_id: idOf("vlt"),
|
|
786
986
|
kind: VaultCredentialKindSchema,
|
|
787
987
|
/** For `env_var`, the environment variable name. For the MCP kinds, a display label. */
|
|
788
|
-
key:
|
|
988
|
+
key: z14.string().min(1).max(128),
|
|
789
989
|
/** The exact destination host an `env_var` credential is bound to. Never a wildcard. */
|
|
790
|
-
host:
|
|
990
|
+
host: z14.string().nullable(),
|
|
791
991
|
/** The exact MCP server URL an `mcp_*` token is matched to. */
|
|
792
|
-
mcp_server_url:
|
|
793
|
-
placeholder_ref:
|
|
992
|
+
mcp_server_url: z14.url().nullable(),
|
|
993
|
+
placeholder_ref: z14.string(),
|
|
794
994
|
expires_at: Timestamp.nullable(),
|
|
795
995
|
last_injected_at: Timestamp.nullable(),
|
|
796
996
|
created_at: Timestamp
|
|
797
997
|
});
|
|
798
998
|
|
|
799
999
|
// ../core/src/schema/social.ts
|
|
800
|
-
import { z as
|
|
1000
|
+
import { z as z15 } from "zod";
|
|
801
1001
|
var SOCIAL_PLATFORMS = [
|
|
802
1002
|
"bluesky",
|
|
803
1003
|
"facebook",
|
|
@@ -819,19 +1019,19 @@ var SOCIAL_TEXT_PLATFORMS = [
|
|
|
819
1019
|
"threads",
|
|
820
1020
|
"x"
|
|
821
1021
|
];
|
|
822
|
-
var SocialPlatformSchema =
|
|
823
|
-
var SocialTextPlatformSchema =
|
|
824
|
-
var SocialAccountSchema =
|
|
825
|
-
id:
|
|
826
|
-
object:
|
|
1022
|
+
var SocialPlatformSchema = z15.enum(SOCIAL_PLATFORMS);
|
|
1023
|
+
var SocialTextPlatformSchema = z15.enum(SOCIAL_TEXT_PLATFORMS);
|
|
1024
|
+
var SocialAccountSchema = z15.object({
|
|
1025
|
+
id: z15.string().min(1),
|
|
1026
|
+
object: z15.literal("social_account"),
|
|
827
1027
|
identity_id: idOf("idn"),
|
|
828
1028
|
platform: SocialPlatformSchema,
|
|
829
|
-
username:
|
|
830
|
-
display_name:
|
|
831
|
-
avatar_url:
|
|
1029
|
+
username: z15.string().nullable(),
|
|
1030
|
+
display_name: z15.string().nullable(),
|
|
1031
|
+
avatar_url: z15.url().nullable(),
|
|
832
1032
|
connected_at: Timestamp.nullable()
|
|
833
1033
|
});
|
|
834
|
-
var SocialPostStatusSchema =
|
|
1034
|
+
var SocialPostStatusSchema = z15.enum([
|
|
835
1035
|
"draft",
|
|
836
1036
|
"scheduled",
|
|
837
1037
|
"processing",
|
|
@@ -839,44 +1039,44 @@ var SocialPostStatusSchema = z13.enum([
|
|
|
839
1039
|
"failed",
|
|
840
1040
|
"deleted"
|
|
841
1041
|
]);
|
|
842
|
-
var SocialPostSchema =
|
|
843
|
-
id:
|
|
844
|
-
object:
|
|
1042
|
+
var SocialPostSchema = z15.object({
|
|
1043
|
+
id: z15.string().min(1),
|
|
1044
|
+
object: z15.literal("social_post"),
|
|
845
1045
|
identity_id: idOf("idn"),
|
|
846
|
-
title:
|
|
847
|
-
platforms:
|
|
1046
|
+
title: z15.string(),
|
|
1047
|
+
platforms: z15.array(SocialTextPlatformSchema),
|
|
848
1048
|
status: SocialPostStatusSchema,
|
|
849
1049
|
scheduled_at: Timestamp.nullable(),
|
|
850
1050
|
posted_at: Timestamp.nullable(),
|
|
851
|
-
error:
|
|
1051
|
+
error: z15.string().nullable()
|
|
852
1052
|
});
|
|
853
|
-
var SocialUploadSchema =
|
|
854
|
-
id:
|
|
855
|
-
object:
|
|
1053
|
+
var SocialUploadSchema = z15.object({
|
|
1054
|
+
id: z15.string().min(1),
|
|
1055
|
+
object: z15.literal("social_upload"),
|
|
856
1056
|
identity_id: idOf("idn"),
|
|
857
|
-
type:
|
|
858
|
-
url:
|
|
1057
|
+
type: z15.enum(["image", "video", "document"]),
|
|
1058
|
+
url: z15.url().nullable()
|
|
859
1059
|
});
|
|
860
|
-
var SocialAnalyticsSchema =
|
|
861
|
-
object:
|
|
1060
|
+
var SocialAnalyticsSchema = z15.object({
|
|
1061
|
+
object: z15.literal("social_analytics"),
|
|
862
1062
|
identity_id: idOf("idn"),
|
|
863
|
-
resource_id:
|
|
1063
|
+
resource_id: z15.string().min(1),
|
|
864
1064
|
platform: SocialPlatformSchema.nullable(),
|
|
865
|
-
data:
|
|
1065
|
+
data: z15.unknown()
|
|
866
1066
|
});
|
|
867
|
-
var SocialCommentSchema =
|
|
868
|
-
id:
|
|
869
|
-
object:
|
|
1067
|
+
var SocialCommentSchema = z15.object({
|
|
1068
|
+
id: z15.string().min(1),
|
|
1069
|
+
object: z15.literal("social_comment"),
|
|
870
1070
|
identity_id: idOf("idn"),
|
|
871
|
-
post_id:
|
|
872
|
-
platforms:
|
|
1071
|
+
post_id: z15.string().min(1),
|
|
1072
|
+
platforms: z15.array(SocialPlatformSchema),
|
|
873
1073
|
status: SocialPostStatusSchema,
|
|
874
1074
|
posted_at: Timestamp.nullable(),
|
|
875
|
-
data:
|
|
1075
|
+
data: z15.unknown()
|
|
876
1076
|
});
|
|
877
1077
|
|
|
878
1078
|
// src/define.ts
|
|
879
|
-
import { z as
|
|
1079
|
+
import { z as z16 } from "zod";
|
|
880
1080
|
|
|
881
1081
|
// src/template.ts
|
|
882
1082
|
var BLUEPRINTS = {
|
|
@@ -915,84 +1115,113 @@ function chooseTemplate(blueprint, template) {
|
|
|
915
1115
|
var BlueprintRefusal = class extends Error {
|
|
916
1116
|
name = "BlueprintRefusal";
|
|
917
1117
|
};
|
|
918
|
-
var EnvValueSchema =
|
|
919
|
-
|
|
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
|
+
]);
|
|
1123
|
+
var AppDeclSchema = z16.object({
|
|
920
1124
|
/** Becomes a hostname, so the grammar is the platform's `APP_NAME` (a DNS label). */
|
|
921
|
-
name:
|
|
1125
|
+
name: z16.string().regex(APP_NAME, "a lowercase DNS label, 63 characters or fewer"),
|
|
922
1126
|
type: AppTypeSchema,
|
|
923
|
-
description:
|
|
1127
|
+
description: z16.string().optional(),
|
|
924
1128
|
/** A directory whose files are collected and shipped as the app's file-map deployment. */
|
|
925
|
-
deploy_dir:
|
|
1129
|
+
deploy_dir: z16.string().min(1).optional(),
|
|
926
1130
|
/** The path the app itself serves MCP on (§29.5); `fullstack` only. Absent means none. */
|
|
927
1131
|
mcp: AppMcpPathSchema.optional(),
|
|
928
1132
|
/** Written as the app's write-only secrets (§29.2) on every apply; names follow the platform's `ENV_NAME`. */
|
|
929
|
-
env:
|
|
1133
|
+
env: z16.record(z16.string().regex(ENV_NAME), EnvValueSchema, { error: "keyed by environment variable name (A-Z, 0-9, _)" }).optional()
|
|
930
1134
|
});
|
|
931
|
-
var ScheduleDeclSchema =
|
|
932
|
-
cron:
|
|
933
|
-
timezone:
|
|
1135
|
+
var ScheduleDeclSchema = z16.object({
|
|
1136
|
+
cron: z16.string().min(1),
|
|
1137
|
+
timezone: z16.string().min(1).optional(),
|
|
934
1138
|
/** The message every fire opens its session with (`input_template` on the wire). */
|
|
935
|
-
input:
|
|
1139
|
+
input: z16.string().min(1),
|
|
936
1140
|
budget_micro_usd: MicroUsdField.nonnegative(),
|
|
937
|
-
identity:
|
|
938
|
-
enabled:
|
|
1141
|
+
identity: z16.string().min(1).optional(),
|
|
1142
|
+
enabled: z16.boolean().optional()
|
|
939
1143
|
});
|
|
940
|
-
var AgentDeclSchema =
|
|
941
|
-
name:
|
|
942
|
-
model:
|
|
1144
|
+
var AgentDeclSchema = z16.object({
|
|
1145
|
+
name: z16.string().min(1),
|
|
1146
|
+
model: z16.string().min(1),
|
|
943
1147
|
budget: BudgetSchema,
|
|
944
|
-
description:
|
|
945
|
-
system:
|
|
1148
|
+
description: z16.string().optional(),
|
|
1149
|
+
system: z16.string().optional(),
|
|
946
1150
|
tools: ToolsetSchema.optional(),
|
|
947
|
-
skills:
|
|
948
|
-
mcp_servers:
|
|
1151
|
+
skills: z16.array(SkillRefSchema).optional(),
|
|
1152
|
+
mcp_servers: z16.array(McpServerSchema).optional(),
|
|
949
1153
|
/** Declared app *names*; `up` resolves them to `app_` ids in `tools.configs.apps` (§29.3). */
|
|
950
|
-
allowed_apps:
|
|
1154
|
+
allowed_apps: z16.array(z16.string()).optional(),
|
|
951
1155
|
/** A declared identity *name*; `up` attaches it after the agent upsert (§22). */
|
|
952
|
-
identity:
|
|
1156
|
+
identity: z16.string().min(1).optional(),
|
|
953
1157
|
/** The agent's deployments as a complete set: declared → owned, and the rest are pruned (ADR-0355). */
|
|
954
|
-
schedules:
|
|
1158
|
+
schedules: z16.array(ScheduleDeclSchema).optional(),
|
|
1159
|
+
/**
|
|
1160
|
+
* The first thing this agent reads, once, on the apply that **creates** it (`canonical-spec
|
|
1161
|
+
* §31.4`). It is what turns a provisioned crew into a crew that has started working.
|
|
1162
|
+
*
|
|
1163
|
+
* Neither of its neighbours can carry it. A `schedule` fires again and again; an intake happens
|
|
1164
|
+
* exactly once, at creation, and never on a later apply of the same agent. `system` is standing
|
|
1165
|
+
* instruction re-read every turn; an intake is a message the agent is sent.
|
|
1166
|
+
*
|
|
1167
|
+
* `budget_micro_usd` is optional here and required on a schedule, because a cron spends
|
|
1168
|
+
* repeatedly and unattended while an intake spends once, at an apply a person is watching. Absent
|
|
1169
|
+
* means the agent's own `budget` governs.
|
|
1170
|
+
*/
|
|
1171
|
+
intake: z16.object({ message: z16.string().min(1), budget_micro_usd: MicroUsdField.nonnegative().optional() }).optional()
|
|
955
1172
|
});
|
|
956
|
-
var CredentialDeclSchema =
|
|
1173
|
+
var CredentialDeclSchema = z16.object({
|
|
957
1174
|
kind: VaultCredentialKindSchema,
|
|
958
|
-
key:
|
|
1175
|
+
key: z16.string().min(1).max(128),
|
|
959
1176
|
/** The one exact destination host an `env_var` is bound to. */
|
|
960
|
-
host:
|
|
1177
|
+
host: z16.string().min(1).optional(),
|
|
961
1178
|
/** The MCP server URL a `static_bearer` / `mcp_oauth` token is matched to. */
|
|
962
|
-
mcp_server_url:
|
|
963
|
-
value:
|
|
1179
|
+
mcp_server_url: z16.url().optional(),
|
|
1180
|
+
value: z16.object({ from_env: z16.string().min(1) })
|
|
964
1181
|
});
|
|
965
|
-
var VaultDeclSchema =
|
|
1182
|
+
var VaultDeclSchema = z16.object({
|
|
966
1183
|
name: VaultSchema.shape.display_name,
|
|
967
|
-
identity:
|
|
968
|
-
credentials:
|
|
1184
|
+
identity: z16.string().min(1).optional(),
|
|
1185
|
+
credentials: z16.array(CredentialDeclSchema)
|
|
969
1186
|
});
|
|
970
|
-
var SkillDeclSchema =
|
|
971
|
-
slug:
|
|
972
|
-
file:
|
|
1187
|
+
var SkillDeclSchema = z16.object({
|
|
1188
|
+
slug: z16.string().regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters, digits and dashes"),
|
|
1189
|
+
file: z16.string().min(1)
|
|
973
1190
|
});
|
|
974
1191
|
var IdentityDeclSchema = IdentitySchema.pick({ name: true, description: true }).partial({
|
|
975
1192
|
description: true
|
|
976
1193
|
});
|
|
977
|
-
var RemovedSchema =
|
|
978
|
-
apps:
|
|
979
|
-
agents:
|
|
980
|
-
skills:
|
|
981
|
-
identities:
|
|
982
|
-
vaults:
|
|
1194
|
+
var RemovedSchema = z16.object({
|
|
1195
|
+
apps: z16.array(z16.string().min(1)).default([]),
|
|
1196
|
+
agents: z16.array(z16.string().min(1)).default([]),
|
|
1197
|
+
skills: z16.array(z16.string().min(1)).default([]),
|
|
1198
|
+
identities: z16.array(z16.string().min(1)).default([]),
|
|
1199
|
+
vaults: z16.array(z16.string().min(1)).default([])
|
|
983
1200
|
});
|
|
984
|
-
var KeptSchema =
|
|
985
|
-
var ProjectSchema =
|
|
986
|
-
name:
|
|
1201
|
+
var KeptSchema = z16.object({ agents: z16.array(z16.string().min(1)).default([]) });
|
|
1202
|
+
var ProjectSchema = z16.object({
|
|
1203
|
+
name: z16.string().min(1),
|
|
987
1204
|
/** The machine this project runs: its repo, its screens, its `/api/*` (`template.ts`). */
|
|
988
|
-
blueprint:
|
|
1205
|
+
blueprint: z16.string().min(1).optional(),
|
|
989
1206
|
/** Which of that blueprint's templates provides the crew; a blueprint with a default may omit it. */
|
|
990
|
-
template:
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1207
|
+
template: z16.string().min(1).optional(),
|
|
1208
|
+
/**
|
|
1209
|
+
* What a person is asked BEFORE the crew is provisioned (`canonical-spec §31.2`).
|
|
1210
|
+
*
|
|
1211
|
+
* `QuestionField` is reused rather than restated: a setup question and a question a parked agent
|
|
1212
|
+
* asks mid-run are the same object asked at different times, so a blueprint author writes one
|
|
1213
|
+
* shape and one component renders both. These are published on the artifact — they are the whole
|
|
1214
|
+
* reason the catalog has a `questions` column — and the answers a person gives go on the install
|
|
1215
|
+
* (§29.6), never into the tree, because the tree is identical for everyone.
|
|
1216
|
+
*
|
|
1217
|
+
* **Configuration only.** A secret is a vault credential or an app secret; neither is an answer.
|
|
1218
|
+
*/
|
|
1219
|
+
questions: z16.array(QuestionFieldSchema).default([]),
|
|
1220
|
+
skills: z16.array(SkillDeclSchema).default([]),
|
|
1221
|
+
identities: z16.array(IdentityDeclSchema).default([]),
|
|
1222
|
+
vaults: z16.array(VaultDeclSchema).default([]),
|
|
1223
|
+
apps: z16.array(AppDeclSchema).default([]),
|
|
1224
|
+
agents: z16.array(AgentDeclSchema).default([]),
|
|
996
1225
|
removed: RemovedSchema.default({ apps: [], agents: [], skills: [], identities: [], vaults: [] }),
|
|
997
1226
|
kept: KeptSchema.default({ agents: [] })
|
|
998
1227
|
});
|
|
@@ -1168,6 +1397,48 @@ async function loadConfig(dir) {
|
|
|
1168
1397
|
return validate(path, raw);
|
|
1169
1398
|
}
|
|
1170
1399
|
|
|
1400
|
+
// src/files.ts
|
|
1401
|
+
import { createHash } from "node:crypto";
|
|
1402
|
+
import { readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
|
|
1403
|
+
import { join as join2, relative, sep } from "node:path";
|
|
1404
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
1405
|
+
var MAX_FILE_BYTES = 8 * 1024 * 1024;
|
|
1406
|
+
function collectFiles(source) {
|
|
1407
|
+
if (typeof source !== "string") return fromMap(source);
|
|
1408
|
+
const root = source;
|
|
1409
|
+
const files = {};
|
|
1410
|
+
const walk = (dir) => {
|
|
1411
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1412
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".env")) continue;
|
|
1413
|
+
const full = join2(dir, entry.name);
|
|
1414
|
+
if (entry.isDirectory()) walk(full);
|
|
1415
|
+
else if (entry.isFile() && statSync(full).size <= MAX_FILE_BYTES) {
|
|
1416
|
+
files[relative(root, full).split(sep).join("/")] = readFileSync2(full).toString("base64");
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
walk(root);
|
|
1421
|
+
return files;
|
|
1422
|
+
}
|
|
1423
|
+
function fromMap(map) {
|
|
1424
|
+
const files = {};
|
|
1425
|
+
for (const [path, base64] of Object.entries(map)) {
|
|
1426
|
+
const segments = path.split("/");
|
|
1427
|
+
if (segments.some((segment) => SKIP_DIRS.has(segment) || segment.startsWith(".env"))) continue;
|
|
1428
|
+
if (Buffer.from(base64, "base64").length > MAX_FILE_BYTES) continue;
|
|
1429
|
+
files[path] = base64;
|
|
1430
|
+
}
|
|
1431
|
+
return files;
|
|
1432
|
+
}
|
|
1433
|
+
function contentHash(files) {
|
|
1434
|
+
const hash = createHash("sha256");
|
|
1435
|
+
for (const [path, base64] of Object.entries(files).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
1436
|
+
const bytes = Buffer.from(base64, "base64");
|
|
1437
|
+
hash.update(`${path}\0${bytes.length}\0`).update(bytes);
|
|
1438
|
+
}
|
|
1439
|
+
return hash.digest("hex");
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1171
1442
|
// src/up.ts
|
|
1172
1443
|
import { createHash as createHash3 } from "node:crypto";
|
|
1173
1444
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
@@ -1175,7 +1446,7 @@ import { resolve as resolve3 } from "node:path";
|
|
|
1175
1446
|
|
|
1176
1447
|
// src/report.ts
|
|
1177
1448
|
import { ApiError, send } from "@usenaive-sdk/vetta";
|
|
1178
|
-
import { z as
|
|
1449
|
+
import { z as z17 } from "zod";
|
|
1179
1450
|
function message2(error) {
|
|
1180
1451
|
return error instanceof Error ? error.message : String(error);
|
|
1181
1452
|
}
|
|
@@ -1205,10 +1476,10 @@ async function absent(work) {
|
|
|
1205
1476
|
throw error;
|
|
1206
1477
|
}
|
|
1207
1478
|
}
|
|
1208
|
-
var PageShape =
|
|
1209
|
-
data:
|
|
1210
|
-
has_more:
|
|
1211
|
-
next_cursor:
|
|
1479
|
+
var PageShape = z17.object({
|
|
1480
|
+
data: z17.array(z17.unknown()),
|
|
1481
|
+
has_more: z17.boolean(),
|
|
1482
|
+
next_cursor: z17.string().nullable()
|
|
1212
1483
|
});
|
|
1213
1484
|
function rowLabel(row) {
|
|
1214
1485
|
const { object, id } = row ?? {};
|
|
@@ -1241,7 +1512,7 @@ async function tombstone(name, current, remove, dryRun) {
|
|
|
1241
1512
|
|
|
1242
1513
|
// src/agents.ts
|
|
1243
1514
|
function desiredTools(decl, current, ids) {
|
|
1244
|
-
if (
|
|
1515
|
+
if (ids === void 0) return decl.tools;
|
|
1245
1516
|
const base = decl.tools ?? current?.tools ?? { default_config: { permission: "allow" }, configs: {} };
|
|
1246
1517
|
const appsTool = base.configs["apps"] ?? { enabled: true, permission: base.default_config.permission };
|
|
1247
1518
|
return {
|
|
@@ -1266,14 +1537,14 @@ async function createAgent(client, decl, tools, identityId) {
|
|
|
1266
1537
|
if (identityId !== void 0) await client.identities.attach(created.id, identityId);
|
|
1267
1538
|
return { name: decl.name, action: "created", id: created.id };
|
|
1268
1539
|
}
|
|
1269
|
-
async function upsertAgent(client, decl, existing, ids, dryRun) {
|
|
1540
|
+
async function upsertAgent(client, decl, existing, ids, dryRun, scoped) {
|
|
1270
1541
|
const unresolved = (decl.allowed_apps ?? []).filter((name) => !ids.apps.has(name));
|
|
1271
1542
|
const identityId = decl.identity === void 0 ? void 0 : ids.identities.get(decl.identity);
|
|
1272
1543
|
const missing = unresolved.length > 0 ? `allowed_apps needs app "${unresolved[0]}"` : decl.identity !== void 0 && identityId === void 0 ? `identity needs "${decl.identity}"` : null;
|
|
1273
1544
|
if (missing !== null && !dryRun) {
|
|
1274
1545
|
return { name: decl.name, action: "refused", reason: `${missing}, which was not provisioned in this run` };
|
|
1275
1546
|
}
|
|
1276
|
-
const appIds =
|
|
1547
|
+
const appIds = decl.allowed_apps !== void 0 ? decl.allowed_apps.flatMap((name) => ids.apps.get(name) ?? []) : scoped;
|
|
1277
1548
|
const current = existing.find((agent) => agent.name === decl.name) ?? null;
|
|
1278
1549
|
const tools = desiredTools(decl, current, appIds);
|
|
1279
1550
|
if (!current) return dryRun ? { name: decl.name, action: "created" } : createAgent(client, decl, tools, identityId);
|
|
@@ -1295,6 +1566,15 @@ async function upsertAgent(client, decl, existing, ids, dryRun) {
|
|
|
1295
1566
|
}
|
|
1296
1567
|
return { name: decl.name, action: "updated", id: current.id };
|
|
1297
1568
|
}
|
|
1569
|
+
async function openIntake(client, agent, decl, dryRun) {
|
|
1570
|
+
if (dryRun || agent.id === void 0) return { name: agent.name, action: "created" };
|
|
1571
|
+
const started = await client.sessions.create({
|
|
1572
|
+
agent_id: agent.id,
|
|
1573
|
+
message: decl.message,
|
|
1574
|
+
...decl.budget_micro_usd !== void 0 ? { budget_micro_usd: decl.budget_micro_usd } : {}
|
|
1575
|
+
});
|
|
1576
|
+
return { name: agent.name, action: "created", id: started.id };
|
|
1577
|
+
}
|
|
1298
1578
|
async function upsertSchedule(client, agent, decl, current, identityIds, dryRun) {
|
|
1299
1579
|
const name = `${agent.name} @ ${decl.cron}`;
|
|
1300
1580
|
if (agent.action === "refused") throw new Error(`agent "${agent.name}" was not provisioned in this run`);
|
|
@@ -1337,40 +1617,8 @@ async function reconcileSchedules(client, agent, declared, live, identityIds, dr
|
|
|
1337
1617
|
}
|
|
1338
1618
|
|
|
1339
1619
|
// src/apps.ts
|
|
1340
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
1620
|
+
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
1341
1621
|
import { resolve as resolve2 } from "node:path";
|
|
1342
|
-
|
|
1343
|
-
// src/files.ts
|
|
1344
|
-
import { createHash } from "node:crypto";
|
|
1345
|
-
import { readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
|
|
1346
|
-
import { join as join2, relative, sep } from "node:path";
|
|
1347
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
1348
|
-
var MAX_FILE_BYTES = 8 * 1024 * 1024;
|
|
1349
|
-
function collectFiles(root) {
|
|
1350
|
-
const files = {};
|
|
1351
|
-
const walk = (dir) => {
|
|
1352
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1353
|
-
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".env")) continue;
|
|
1354
|
-
const full = join2(dir, entry.name);
|
|
1355
|
-
if (entry.isDirectory()) walk(full);
|
|
1356
|
-
else if (entry.isFile() && statSync(full).size <= MAX_FILE_BYTES) {
|
|
1357
|
-
files[relative(root, full).split(sep).join("/")] = readFileSync2(full).toString("base64");
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
};
|
|
1361
|
-
walk(root);
|
|
1362
|
-
return files;
|
|
1363
|
-
}
|
|
1364
|
-
function contentHash(files) {
|
|
1365
|
-
const hash = createHash("sha256");
|
|
1366
|
-
for (const [path, base64] of Object.entries(files).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
1367
|
-
const bytes = Buffer.from(base64, "base64");
|
|
1368
|
-
hash.update(`${path}\0${bytes.length}\0`).update(bytes);
|
|
1369
|
-
}
|
|
1370
|
-
return hash.digest("hex");
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
// src/apps.ts
|
|
1374
1622
|
async function findApp(client, name, skipped) {
|
|
1375
1623
|
const rows = await listAll(client.config, "/v1/apps", AppSchema, skipped, { name });
|
|
1376
1624
|
return rows.find((app) => app.name === name) ?? null;
|
|
@@ -1381,25 +1629,74 @@ function ownershipRefusal(name, project, owner) {
|
|
|
1381
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.`;
|
|
1382
1630
|
}
|
|
1383
1631
|
var ensureApp = (client, body) => client.apps.create(body);
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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)) {
|
|
1387
1644
|
if (typeof value === "string") {
|
|
1388
|
-
|
|
1645
|
+
plan.set(name, { name, value });
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
if ("generate" in value) {
|
|
1649
|
+
plan.set(name, { name, invent: async () => generated() });
|
|
1389
1650
|
continue;
|
|
1390
1651
|
}
|
|
1391
|
-
const
|
|
1392
|
-
|
|
1393
|
-
|
|
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
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
for (const [name, value] of Object.entries(opts.platform?.known ?? {})) plan.set(name, { name, value });
|
|
1663
|
+
return [...plan.values()];
|
|
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);
|
|
1394
1673
|
}
|
|
1395
|
-
return
|
|
1674
|
+
return { values, unmade };
|
|
1396
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();
|
|
1397
1687
|
function secretHash(name, value) {
|
|
1398
1688
|
return createHash2("sha256").update(`${name}\0`).update(value).digest("hex");
|
|
1399
1689
|
}
|
|
1400
|
-
function
|
|
1690
|
+
function builtTree(decl, opts) {
|
|
1401
1691
|
if (!decl.deploy_dir) return null;
|
|
1402
|
-
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));
|
|
1403
1700
|
return { files, hash: contentHash(files) };
|
|
1404
1701
|
}
|
|
1405
1702
|
async function writeApp(client, id, env, tree) {
|
|
@@ -1419,7 +1716,7 @@ async function settleDeploy(client, id) {
|
|
|
1419
1716
|
await new Promise((resolve4) => setTimeout(resolve4, SETTLE_MS));
|
|
1420
1717
|
}
|
|
1421
1718
|
}
|
|
1422
|
-
async function createApp(client, decl, env, opts, appIds) {
|
|
1719
|
+
async function createApp(client, decl, env, tree, opts, appIds) {
|
|
1423
1720
|
const created = await ensureApp(client, {
|
|
1424
1721
|
name: decl.name,
|
|
1425
1722
|
type: decl.type,
|
|
@@ -1428,12 +1725,12 @@ async function createApp(client, decl, env, opts, appIds) {
|
|
|
1428
1725
|
...decl.mcp !== void 0 ? { mcp: decl.mcp } : {}
|
|
1429
1726
|
});
|
|
1430
1727
|
appIds.set(decl.name, created.id);
|
|
1431
|
-
await writeApp(client, created.id, env,
|
|
1728
|
+
await writeApp(client, created.id, env, tree);
|
|
1432
1729
|
return { name: decl.name, action: "created", id: created.id, url: (await client.apps.get(created.id)).url };
|
|
1433
1730
|
}
|
|
1434
1731
|
async function upsertApp(client, decl, opts, appIds) {
|
|
1435
|
-
const
|
|
1436
|
-
const names =
|
|
1732
|
+
const declared = planEnv(decl, opts);
|
|
1733
|
+
const names = declared.map((one) => one.name);
|
|
1437
1734
|
const note = (notes) => notes.length > 0 ? { reason: notes.join("; ") } : {};
|
|
1438
1735
|
const mcpPlan = decl.mcp !== void 0 ? [`will set mcp ${decl.mcp}`] : [];
|
|
1439
1736
|
const current = await findApp(client, decl.name, opts.skipped);
|
|
@@ -1446,7 +1743,8 @@ async function upsertApp(client, decl, opts, appIds) {
|
|
|
1446
1743
|
...note([...willSet, ...mcpPlan, ...decl.deploy_dir ? [`will deploy ${decl.deploy_dir}`] : []])
|
|
1447
1744
|
};
|
|
1448
1745
|
}
|
|
1449
|
-
|
|
1746
|
+
const { values } = await materialize(declared, EMPTY, false);
|
|
1747
|
+
return createApp(client, decl, values, builtTree(decl, opts), opts, appIds);
|
|
1450
1748
|
}
|
|
1451
1749
|
const owned = current.project === opts.project;
|
|
1452
1750
|
if (!owned && !opts.adopt) {
|
|
@@ -1471,19 +1769,21 @@ async function upsertApp(client, decl, opts, appIds) {
|
|
|
1471
1769
|
if (!owned && !opts.dryRun) {
|
|
1472
1770
|
await ensureApp(client, { name: decl.name, type: decl.type, project: opts.project, adopt: true });
|
|
1473
1771
|
}
|
|
1474
|
-
const live =
|
|
1475
|
-
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));
|
|
1476
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);
|
|
1477
1777
|
const staleEnv = Object.fromEntries(
|
|
1478
1778
|
Object.entries(env).filter(([name, value]) => held.get(name) !== secretHash(name, value))
|
|
1479
1779
|
);
|
|
1480
|
-
const staleNames = Object.keys(staleEnv);
|
|
1780
|
+
const staleNames = [...Object.keys(staleEnv), ...unmade];
|
|
1481
1781
|
const patch = {
|
|
1482
1782
|
...decl.description !== void 0 && decl.description !== current.description ? { description: decl.description } : {},
|
|
1483
1783
|
...(decl.mcp ?? null) !== current.mcp ? { mcp: decl.mcp ?? null } : {}
|
|
1484
1784
|
};
|
|
1485
1785
|
const drifted = Object.keys(patch).length > 0;
|
|
1486
|
-
const tree =
|
|
1786
|
+
const tree = builtTree(decl, opts);
|
|
1487
1787
|
const newest = tree ? (await client.apps.deployments(current.id, { limit: 1 })).data[0] : void 0;
|
|
1488
1788
|
const stale = tree !== null && newest?.content_hash !== tree.hash;
|
|
1489
1789
|
const action = !owned || drifted || stale || staleNames.length > 0 ? "updated" : "unchanged";
|
|
@@ -1531,14 +1831,14 @@ async function upsertVault(client, decl, existing, identityIds, dryRun) {
|
|
|
1531
1831
|
}
|
|
1532
1832
|
return { name: decl.name, action: "unchanged", id: current.id };
|
|
1533
1833
|
}
|
|
1534
|
-
async function upsertCredential(client, vault, decl, existing, dryRun) {
|
|
1834
|
+
async function upsertCredential(client, vault, decl, existing, dryRun, from) {
|
|
1535
1835
|
const name = `${vault.name} / ${decl.key}`;
|
|
1536
1836
|
if (vault.action === "refused") throw new Error(`vault "${vault.name}" was not provisioned in this run`);
|
|
1537
1837
|
const current = existing.find(
|
|
1538
1838
|
(row) => row.kind === decl.kind && row.key === decl.key && row.host === (decl.host ?? null) && row.mcp_server_url === (decl.mcp_server_url ?? null)
|
|
1539
1839
|
);
|
|
1540
1840
|
if (current) return { name, action: "unchanged", id: current.id };
|
|
1541
|
-
const value =
|
|
1841
|
+
const value = from[decl.value.from_env];
|
|
1542
1842
|
if (value === void 0) throw new Error(`credential ${decl.key} reads local variable ${decl.value.from_env}, which is not set`);
|
|
1543
1843
|
if (dryRun || vault.id === void 0) return { name, action: "created" };
|
|
1544
1844
|
const created = await client.vaults.credentials.create(vault.id, {
|
|
@@ -1552,7 +1852,17 @@ async function upsertCredential(client, vault, decl, existing, dryRun) {
|
|
|
1552
1852
|
}
|
|
1553
1853
|
|
|
1554
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
|
+
}
|
|
1555
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
|
+
}
|
|
1556
1866
|
const content = readFileSync3(resolve3(opts.dir, decl.file), "utf8");
|
|
1557
1867
|
const sha256 = createHash3("sha256").update(content).digest("hex");
|
|
1558
1868
|
const current = await absent(client.skills.get(decl.slug));
|
|
@@ -1629,10 +1939,11 @@ async function up(config, client, opts = {}) {
|
|
|
1629
1939
|
const dryRun = opts.dry_run ?? false;
|
|
1630
1940
|
const dir = opts.dir ?? process.cwd();
|
|
1631
1941
|
const ids = { apps: /* @__PURE__ */ new Map(), identities: /* @__PURE__ */ new Map() };
|
|
1632
|
-
const report = { dry_run: dryRun, ...applied(config), skills: [], identities: [], vaults: [], apps: [], agents: [], schedules: [], skipped: [] };
|
|
1942
|
+
const report = { dry_run: dryRun, ...applied(config), skills: [], identities: [], vaults: [], apps: [], agents: [], schedules: [], intake: [], skipped: [] };
|
|
1633
1943
|
const owner = { project: config.name, adopt: opts.adopt ?? false, skipped: report.skipped };
|
|
1944
|
+
const hosted = opts.trees !== void 0;
|
|
1634
1945
|
for (const decl of config.skills) {
|
|
1635
|
-
await attempt(report.skills, decl.slug, () => upsertSkill(client, decl, { dryRun, dir }));
|
|
1946
|
+
await attempt(report.skills, decl.slug, () => upsertSkill(client, decl, { dryRun, dir, hosted }));
|
|
1636
1947
|
}
|
|
1637
1948
|
for (const decl of config.identities) {
|
|
1638
1949
|
await attempt(report.identities, decl.name, () => upsertIdentity(client, decl, dryRun, ids.identities));
|
|
@@ -1643,29 +1954,59 @@ async function up(config, client, opts = {}) {
|
|
|
1643
1954
|
const vaultId = line.action === "unchanged" ? line.id : void 0;
|
|
1644
1955
|
const rows = vaultId === void 0 ? [] : await listAll(client.config, `/v1/vaults/${vaultId}/credentials`, VaultCredentialSchema, report.skipped);
|
|
1645
1956
|
for (const cred of decl.credentials) {
|
|
1646
|
-
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
|
+
);
|
|
1647
1962
|
}
|
|
1648
1963
|
}
|
|
1964
|
+
const platform = opts.platform && {
|
|
1965
|
+
...opts.platform,
|
|
1966
|
+
known: { ...opts.platform.known, ...identityEnv(config, ids.identities) }
|
|
1967
|
+
};
|
|
1649
1968
|
for (const decl of config.apps) {
|
|
1650
|
-
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
|
+
);
|
|
1651
1979
|
}
|
|
1652
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) : [];
|
|
1653
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;
|
|
1983
|
+
const briefed = [];
|
|
1654
1984
|
for (const decl of config.agents) {
|
|
1655
|
-
const line = await attempt(
|
|
1985
|
+
const line = await attempt(
|
|
1986
|
+
report.agents,
|
|
1987
|
+
decl.name,
|
|
1988
|
+
() => upsertAgent(client, decl, existing, ids, dryRun, scoped)
|
|
1989
|
+
);
|
|
1656
1990
|
if (decl.schedules !== void 0) {
|
|
1657
1991
|
await reconcileSchedules(client, line, decl.schedules, deployments, ids.identities, dryRun, report.schedules);
|
|
1658
1992
|
}
|
|
1993
|
+
if (decl.intake !== void 0 && line.action === "created") briefed.push([line, decl.intake]);
|
|
1659
1994
|
}
|
|
1660
1995
|
reportKept(config, existing, report.agents);
|
|
1661
1996
|
await applyRemoved(client, config.removed, { agents: existing, vaults }, owner, report);
|
|
1997
|
+
for (const [line, decl] of briefed) {
|
|
1998
|
+
await attempt(report.intake, line.name, () => openIntake(client, line, decl, dryRun));
|
|
1999
|
+
}
|
|
1662
2000
|
return report;
|
|
1663
2001
|
}
|
|
1664
2002
|
export {
|
|
1665
2003
|
BLUEPRINTS,
|
|
1666
2004
|
BlueprintRefusal,
|
|
1667
2005
|
CONFIG_CANDIDATES,
|
|
2006
|
+
PLATFORM_ENV,
|
|
1668
2007
|
chooseTemplate,
|
|
2008
|
+
collectFiles,
|
|
2009
|
+
contentHash,
|
|
1669
2010
|
defineProject,
|
|
1670
2011
|
loadConfig,
|
|
1671
2012
|
parseProject,
|