@treeseed/sdk 0.13.0-rc.27 → 0.13.0-rc.29

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.
@@ -8,13 +8,57 @@ const localAlias = z.string().regex(
8
8
  "Host aliases must use the .localhost namespace."
9
9
  );
10
10
  const binding = z.string().regex(/^(?:\[[0-9a-f:]+\]|[^:]+):[1-9][0-9]{0,4}$/iu);
11
+ const httpsUrl = z.string().url().refine((value) => new URL(value).protocol === "https:", "Remote TreeSeed connections require HTTPS.");
11
12
  const deploymentTrackSchema = z.enum(["stable", "development"]);
13
+ const hostRoleSchema = z.union([
14
+ z.enum(["integrated", "capacity-provider", "control-plane", "knowledge", "ai-gpu", "edge"]),
15
+ identifier
16
+ ]);
17
+ const connectionHealthGateSchema = z.object({
18
+ protocol: z.enum(["http", "https", "tcp"]),
19
+ path: z.string().startsWith("/").optional(),
20
+ timeoutSeconds: z.number().int().positive().max(600)
21
+ }).strict();
22
+ const remoteTlsSchema = z.object({
23
+ trust: z.enum(["system", "secret", "spki"]),
24
+ serverName: z.string().min(1).optional(),
25
+ caSecretRef: identifier.optional(),
26
+ spkiSha256: digest.optional()
27
+ }).strict().superRefine((tls, context) => {
28
+ if (tls.trust === "secret" && !tls.caSecretRef) context.addIssue({ code: z.ZodIssueCode.custom, path: ["caSecretRef"], message: "Secret TLS trust requires a CA secret reference." });
29
+ if (tls.trust === "spki" && !tls.spkiSha256) context.addIssue({ code: z.ZodIssueCode.custom, path: ["spkiSha256"], message: "SPKI TLS trust requires an exact SHA-256 pin." });
30
+ });
31
+ const hostConnectionBindingSchema = z.union([
32
+ z.object({
33
+ kind: z.literal("local"),
34
+ componentId: identifier,
35
+ serviceId: identifier,
36
+ endpointId: identifier
37
+ }).strict(),
38
+ z.object({
39
+ kind: z.literal("remote"),
40
+ url: httpsUrl,
41
+ audience: z.string().min(1),
42
+ tls: remoteTlsSchema,
43
+ authentication: z.object({ mode: z.enum(["none", "application", "bearer", "mtls"]), secretRef: identifier.optional() }).strict(),
44
+ healthGate: connectionHealthGateSchema
45
+ }).strict()
46
+ ]).superRefine((connection, context) => {
47
+ if (connection.kind === "remote" && connection.authentication.mode !== "none" && !connection.authentication.secretRef) context.addIssue({ code: z.ZodIssueCode.custom, path: ["authentication", "secretRef"], message: "Authenticated remote connections require a secret reference." });
48
+ });
12
49
  const hostComponentSchema = z.object({
13
50
  enabled: z.boolean(),
14
51
  track: deploymentTrackSchema,
15
52
  profile: identifier.optional(),
16
53
  aliases: z.record(z.string().min(1), localAlias).default({}),
17
- configuration: z.record(z.unknown()).default({})
54
+ configuration: z.record(z.unknown()).default({}),
55
+ resources: z.object({
56
+ cpuCores: z.number().positive().optional(),
57
+ memoryBytes: z.number().int().positive().optional(),
58
+ storageBytes: z.number().int().positive().optional(),
59
+ gpuDevices: z.array(z.string().min(1)).default([])
60
+ }).strict().default({ gpuDevices: [] }),
61
+ connections: z.record(identifier, hostConnectionBindingSchema).default({})
18
62
  }).strict();
19
63
  const hostConfigurationSchema = z.object({
20
64
  schemaVersion: z.literal("treeseed.host/v1"),
@@ -22,7 +66,7 @@ const hostConfigurationSchema = z.object({
22
66
  generation: z.number().int().positive(),
23
67
  host: z.object({
24
68
  id: identifier,
25
- role: identifier,
69
+ role: hostRoleSchema,
26
70
  architecture: z.literal("amd64")
27
71
  }).strict(),
28
72
  runtime: z.object({ management: z.enum(["managed", "external"]) }).strict(),
@@ -42,6 +86,16 @@ const hostConfigurationSchema = z.object({
42
86
  network: z.object({
43
87
  manager: z.object({ binding, aliases: z.array(localAlias).min(1), sans: z.array(z.string().min(1)), trustedLanCidrs: z.array(z.string().min(1)) }).strict()
44
88
  }).strict(),
89
+ fleet: z.object({
90
+ rolloutGroup: identifier,
91
+ receiptReporting: z.object({
92
+ enabled: z.boolean(),
93
+ connection: hostConnectionBindingSchema.optional(),
94
+ intervalSeconds: z.number().int().min(60).max(86400)
95
+ }).strict()
96
+ }).strict().superRefine((fleet, context) => {
97
+ if (fleet.receiptReporting.enabled && fleet.receiptReporting.connection?.kind !== "remote") context.addIssue({ code: z.ZodIssueCode.custom, path: ["receiptReporting", "connection"], message: "Enabled fleet receipt reporting requires an explicit remote connection." });
98
+ }),
45
99
  secrets: z.record(identifier, z.object({
46
100
  provider: z.enum(["file", "systemd-credential", "vault", "aws-secrets-manager"]),
47
101
  reference: z.string().min(1)
@@ -66,11 +120,20 @@ const packageRuntimeSchema = z.object({
66
120
  schemaVersion: z.literal("treeseed.package-runtime/v1"),
67
121
  componentId: identifier,
68
122
  version: packageVersion,
69
- compose: z.object({ projectName: identifier, files: z.array(z.string().min(1)).min(1) }).strict(),
123
+ compose: z.object({
124
+ projectName: identifier,
125
+ files: z.array(z.object({ path: z.string().min(1), digest }).strict()).min(1)
126
+ }).strict(),
70
127
  services: z.array(z.object({ id: identifier, composeService: identifier, endpoints: z.array(packageEndpointSchema) }).strict()).min(1),
71
128
  stateVolumes: z.array(z.object({ id: identifier, volume: z.string().min(1), backup: z.enum(["required", "optional", "none"]) }).strict()),
72
129
  migrations: z.array(z.object({ id: identifier, order: z.number().int().nonnegative(), backupRequired: z.boolean() }).strict()),
73
- requiredCapabilities: z.array(identifier)
130
+ requiredCapabilities: z.array(identifier),
131
+ dependencies: z.array(z.object({
132
+ id: identifier,
133
+ capability: identifier,
134
+ locality: z.enum(["local", "remote", "either"]),
135
+ optional: z.boolean().default(false)
136
+ }).strict()).default([])
74
137
  }).strict().superRefine((runtime, context) => {
75
138
  const endpointIds = runtime.services.flatMap((service) => service.endpoints.map((endpoint) => `${service.id}.${endpoint.id}`));
76
139
  if (new Set(endpointIds).size !== endpointIds.length) context.addIssue({ code: z.ZodIssueCode.custom, path: ["services"], message: "Endpoint identities must be unique within a component." });
@@ -81,6 +144,8 @@ const componentReleaseSchema = z.object({
81
144
  schemaVersion: z.literal("treeseed.component-release/v1"),
82
145
  componentId: identifier,
83
146
  release: packageVersion,
147
+ applicationVersion: packageVersion,
148
+ revision: z.number().int().positive(),
84
149
  track: deploymentTrackSchema,
85
150
  source: z.object({ repository: z.string().regex(/^treeseed-ai\/[a-z0-9-]+$/u), commit: gitCommit }).strict(),
86
151
  stableBase: z.object({ releaseRange: z.string().min(1), compatibilityId: identifier, catalogDigest: digest.nullable() }).strict().nullable(),
@@ -95,6 +160,26 @@ const componentReleaseSchema = z.object({
95
160
  if (release.track === "development" && !release.stableBase) context.addIssue({ code: z.ZodIssueCode.custom, path: ["stableBase"], message: "Development releases require an exact stable-base binding." });
96
161
  if (release.track === "stable" && release.stableBase) context.addIssue({ code: z.ZodIssueCode.custom, path: ["stableBase"], message: "Stable releases cannot be overlays." });
97
162
  });
163
+ const lockedArtifactSchema = z.object({ url: z.string().url(), sha256: z.string().regex(/^[a-f0-9]{64}$/u) }).strict();
164
+ const integrationReleaseSchema = z.object({
165
+ schemaVersion: z.literal("treeseed.integration-release/v1"),
166
+ release: packageVersion,
167
+ track: deploymentTrackSchema,
168
+ compatibilityId: identifier,
169
+ platform: z.object({ repository: z.literal("treeseed-ai/platform"), commit: gitCommit }).strict(),
170
+ deployment: z.object({ repository: z.literal("treeseed-ai/deployment"), commit: gitCommit, tag: z.string().min(1) }).strict(),
171
+ hostPayloads: z.array(z.object({ id: identifier, packageName: z.string().min(1), version: packageVersion, artifact: lockedArtifactSchema }).strict()),
172
+ components: z.array(z.object({
173
+ componentId: identifier,
174
+ release: packageVersion,
175
+ manifest: lockedArtifactSchema,
176
+ files: z.array(z.object({ path: z.string().min(1), artifact: lockedArtifactSchema }).strict()).min(1)
177
+ }).strict()).min(1),
178
+ createdAt: z.string().datetime()
179
+ }).strict().superRefine((release, context) => {
180
+ const components = release.components.map((component) => component.componentId);
181
+ if (new Set(components).size !== components.length) context.addIssue({ code: z.ZodIssueCode.custom, path: ["components"], message: "Integration releases cannot select a component more than once." });
182
+ });
98
183
  const releaseCatalogSchema = z.object({
99
184
  schemaVersion: z.literal("treeseed.release-catalog/v1"),
100
185
  release: packageVersion,
@@ -132,7 +217,7 @@ const hostUpdateSchema = z.object({
132
217
  activation: z.object({ policy: z.enum(["maintenance-window", "continuous"]), eligibleAt: z.string().datetime(), jitterSeconds: z.number().int().nonnegative() }).strict(),
133
218
  state: z.enum(["planned", "draining", "applying", "healthy", "failed", "rolled-back"])
134
219
  }).strict();
135
- const hostReceiptSchema = z.object({ schemaVersion: z.literal("treeseed.host-receipt/v1"), receiptId: identifier, planId: identifier, state: z.enum(["known-good", "degraded", "rolled-back"]), configurationDigest: digest, catalogDigest: digest, packages: z.array(catalogPackageSchema), images: z.array(catalogImageSchema), completedAt: z.string().datetime() }).strict();
220
+ const hostReceiptSchema = z.object({ schemaVersion: z.literal("treeseed.host-receipt/v1"), receiptId: identifier, planId: identifier, state: z.enum(["known-good", "degraded", "rolled-back"]), hostId: identifier, role: hostRoleSchema, rolloutGroup: identifier, configurationDigest: digest, catalogDigest: digest, packages: z.array(catalogPackageSchema), images: z.array(catalogImageSchema), runtimes: z.array(z.object({ componentId: identifier, release: packageVersion, runtimeDigest: digest }).strict()), completedAt: z.string().datetime() }).strict();
136
221
  const hostBackupSchema = z.object({
137
222
  schemaVersion: z.literal("treeseed.host-backup/v1"),
138
223
  backupId: identifier,
@@ -174,11 +259,14 @@ export {
174
259
  hostBootstrapSchema,
175
260
  hostComponentSchema,
176
261
  hostConfigurationSchema,
262
+ hostConnectionBindingSchema,
177
263
  hostMigrationSchema,
178
264
  hostPlanSchema,
179
265
  hostReceiptSchema,
180
266
  hostRecoverySchema,
267
+ hostRoleSchema,
181
268
  hostUpdateSchema,
269
+ integrationReleaseSchema,
182
270
  packageEndpointSchema,
183
271
  packageRuntimeSchema,
184
272
  releaseCatalogSchema
@@ -0,0 +1,9 @@
1
+ import type { ComponentRelease, HostConfiguration } from './schemas.js';
2
+ export interface TopologyBlocker {
3
+ code: 'missing-connection' | 'invalid-locality' | 'missing-local-component' | 'missing-local-endpoint';
4
+ componentId: string;
5
+ dependencyId: string;
6
+ message: string;
7
+ }
8
+ export declare function collectTopologyBlockers(host: HostConfiguration, releases: ComponentRelease[]): TopologyBlocker[];
9
+ export declare function hostNeedsEdge(host: HostConfiguration, releases: ComponentRelease[]): boolean;
@@ -0,0 +1,35 @@
1
+ function collectTopologyBlockers(host, releases) {
2
+ const selected = new Map(releases.map((release) => [release.componentId, release]));
3
+ const blockers = [];
4
+ for (const release of releases) {
5
+ const selection = host.components[release.componentId];
6
+ if (!selection?.enabled) continue;
7
+ for (const dependency of release.runtime.dependencies) {
8
+ const connection = selection.connections[dependency.id];
9
+ if (!connection) {
10
+ if (!dependency.optional) blockers.push({ code: "missing-connection", componentId: release.componentId, dependencyId: dependency.id, message: `${release.componentId} requires an explicit ${dependency.id} connection.` });
11
+ continue;
12
+ }
13
+ if (dependency.locality !== "either" && connection.kind !== dependency.locality) {
14
+ blockers.push({ code: "invalid-locality", componentId: release.componentId, dependencyId: dependency.id, message: `${release.componentId}.${dependency.id} requires a ${dependency.locality} connection.` });
15
+ continue;
16
+ }
17
+ if (connection.kind !== "local") continue;
18
+ const target = selected.get(connection.componentId);
19
+ if (!target || !host.components[connection.componentId]?.enabled) {
20
+ blockers.push({ code: "missing-local-component", componentId: release.componentId, dependencyId: dependency.id, message: `${release.componentId}.${dependency.id} references disabled or unselected component ${connection.componentId}.` });
21
+ continue;
22
+ }
23
+ const service = target.runtime.services.find((candidate) => candidate.id === connection.serviceId);
24
+ if (!service?.endpoints.some((endpoint) => endpoint.id === connection.endpointId)) blockers.push({ code: "missing-local-endpoint", componentId: release.componentId, dependencyId: dependency.id, message: `${release.componentId}.${dependency.id} references unknown endpoint ${connection.componentId}.${connection.serviceId}.${connection.endpointId}.` });
25
+ }
26
+ }
27
+ return blockers.sort((left, right) => `${left.componentId}.${left.dependencyId}`.localeCompare(`${right.componentId}.${right.dependencyId}`));
28
+ }
29
+ function hostNeedsEdge(host, releases) {
30
+ return releases.some((release) => host.components[release.componentId]?.enabled && release.runtime.services.some((service) => service.endpoints.some((endpoint) => endpoint.visibility === "host")));
31
+ }
32
+ export {
33
+ collectTopologyBlockers,
34
+ hostNeedsEdge
35
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.13.0-rc.27",
3
+ "version": "0.13.0-rc.29",
4
4
  "description": "Portable TreeSeed contracts, standards, and typed remote control-plane clients.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {