@checkstack/satellite-common 0.6.0 → 0.8.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/CHANGELOG.md CHANGED
@@ -1,5 +1,124 @@
1
1
  # @checkstack/satellite-common
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9dcc848: Layered OS-level script sandbox, secure and fail-closed by default (epic #247).
8
+
9
+ Script and shell health checks and the `run_shell` / `run_script` automation actions now run inside a layered OS-level sandbox by default. The sandbox lives in `core/backend-api/src/script-sandbox/` (the single source of truth) and is enforced inside the shared runners, so it applies wherever a job runs.
10
+
11
+ Layers:
12
+
13
+ - Resource caps (CPU / memory / PID / FD / file-size, via `prlimit` on capable Linux; ESM JS-heap cap via `--max-old-space-size`; portable wall-clock timeout) and an OOM-safe streaming output cap.
14
+ - Privilege drop via a NON-ROOT supervisor model: the shipped images run the supervisor as non-root uid `65532`, so every sandboxed script inherits non-root and can never be host-root; filesystem + network confinement is delivered by ROOTLESS `bwrap`/`nsjail` via unprivileged user namespaces. `enforced.privilege` is truthful (true only when the child cannot run as host-root). Runners no longer pass `uid`/`gid` to `Bun.spawn` (a silent no-op and a forward-compat hazard).
15
+ - Filesystem isolation (`scratch-only` / `scratch-plus-ro`) confining the child to its per-run scratch dir over a read-only base; the interpreter path is RO-bound so the runtime execs, and `TMPDIR` is pinned to the in-namespace tmpfs.
16
+ - Network egress control: `deny` (routeless loopback-only netns), `allowlist` (real plumbed egress via macvlan OR rootless slirp4netns + an in-kernel nftables filter), and an always-on metadata / link-local block (`169.254.0.0/16`, `fe80::/10`, `fc00::/7`). No-blackhole invariant: `enforced.network` is never true when egress is actually severed or unfiltered; unpluggable egress degrades to surfaced host net.
17
+ - Per-run fork-bomb containment via RLIMIT*NPROC inside the fresh per-run user+PID namespace; a centralized forbidden-env denylist (`LD_PRELOAD`, `LD_LIBRARY_PATH`, `DYLD*_`, `NODE*OPTIONS`, `BUN*_`, caller `PATH` overrides).
18
+ - A validated tuned seccomp profile (`deploy/seccomp/checkstack-userns.json`) and a live `clone(CLONE_NEWUSER|CLONE_NEWNET)` capability probe (not the static sysctl), shipped by default in both Dockerfiles, `docker-compose.yml`, and `deploy/k8s/checkstack-sandbox.yaml`.
19
+
20
+ Global policy and operator surface:
21
+
22
+ - The global sandbox policy lives in ONE durable row owned by `script-packages` (its `ConfigService` row in shared `plugin_configs`). A single process-wide provider serves every runner; the two script plugins no longer register competing providers. A dedicated admin-only `script-sandbox.manage` permission gates both reading and writing the policy. New `getSandboxPolicy` / `setSandboxPolicy` endpoints and a Settings -> Script Sandbox admin UI (`enabled`, `onUnavailable`, network/filesystem/privilege modes, allow list, metadata block, resource caps). The startup capability/readiness log is emitted in-process by `script-packages-backend` (no fragile init-order RPC self-loop), and on a host that cannot enforce a layer a one-time startup warning explains the two local-dev paths (Docker, or set the global policy to `degrade`).
23
+ - Satellite relay: the WS protocol carries the resolved policy in the `authenticated` message and a `sandbox_policy` push-on-change; a satellite caches the last relayed policy and resolves every run through it.
24
+
25
+ BREAKING CHANGES (platform in BETA, shipped as minor):
26
+
27
+ - Scripts run sandboxed by default. The shipped global default is FAIL-CLOSED (`onUnavailable: "fail"`): when a requested layer cannot be enforced the run is REFUSED (clean `exitCode: -1`, never an unsandboxed spawn) rather than silently degrading. Deployments on hosts that cannot enforce a layer (no bubblewrap, user namespaces blocked, no `/proc` unmask) must run the official images with the documented runtime flags (the bundled seccomp profile + `systempaths=unconfined`, or k8s `procMount: Unmasked`), or set the global policy to `degrade`. On macOS / restricted containers the strong layers degrade to the portable subset and are surfaced per run.
28
+ - Default network posture is deny-egress (`allowlist` with an empty allow list, which resolves to the routeless `deny` path). Scripts calling external endpoints fail until those destinations are allowlisted in the global default. The always-on metadata / link-local block applies even under looser modes.
29
+ - The per-action / per-check `sandbox` config override and the transport `ScriptRequest.sandbox` field are removed; policy is global-only, so an automation/check author can no longer weaken the sandbox on their own item. Stored configs carrying a stray `sandbox` key are tolerated (stripped on parse).
30
+ - The shared runners' `run()` no longer accepts a `sandbox` option; callers rely on the global policy provider.
31
+ - A satellite fails closed (most restrictive profile) until it receives the first relayed policy; a relay-read failure or an older core keeps it fail-closed. A relay failure can never loosen a satellite's sandbox.
32
+
33
+ State and scale: the global policy is a single durable Postgres row read identically on every pod. Capability detection is per-process, deterministic from the host kernel, and surfaced per run via the `EffectiveSandbox` report (a Linux pod and a macOS satellite may legitimately differ). `CHECKSTACK_SANDBOX_UID/GID` and macvlan addressing are genuinely per-host infrastructure, surfaced per run, not the queryable policy. The satellite's policy cache is satellite-local transport state. No new pod-local current-state.
34
+
35
+ This is a beta minor.
36
+
37
+ - 9dcc848: Align workspace dependency versions and migrate React Router to v7.
38
+
39
+ BREAKING CHANGES (React Router v7): All frontend packages now depend on `react-router-dom@^7.16.0`. Previously the workspace declared four divergent ranges (`^6.20.0`, `^6.22.0`, `^7.1.1`, `^7.14.2`), which resolved both `react-router@6` and `react-router@7` into a single bundle. Everything is now unified on v7. The public imports the app uses (`BrowserRouter`, `Routes`, `Route`, `Link`, `NavLink`, `MemoryRouter`, `useNavigate`, `useParams`, `useSearchParams`, `useLocation`) are unchanged between v6 and v7, so no source rewrites were required - but any out-of-tree plugin still on react-router v6 should upgrade to v7 (see the React Router v6 -> v7 upgrade guide) to share the host's single router instance via the import map.
40
+
41
+ Other unified ranges (no API change): `react` -> `^18.3.1`, the `@orpc/*` family (`contract`, `server`, `client`, `tanstack-query`, `openapi`, `zod`) -> `^1.14.4`, and `better-auth` -> `^1.6.13`.
42
+
43
+ Removed the pre-rename `@orpc/react-query` leftover from `@checkstack/frontend-api`; its `createRouterUtils` / `RouterUtils` / `ProcedureUtils` now come from `@orpc/tanstack-query` (the package already in use).
44
+
45
+ Stale in-range runtime deps pulled up to current published versions: `hono` `^4.12.23`, `@tanstack/react-query` (+devtools) `^5.100.14`, `date-fns` `^4.4.0`, `jose` `^6.2.3`, `tar` `^7.5.16`, `semver` `^7.8.1`, `@xyflow/react` `^12.11.0`.
46
+
47
+ ### Patch Changes
48
+
49
+ - Updated dependencies [9dcc848]
50
+ - Updated dependencies [9dcc848]
51
+ - Updated dependencies [9dcc848]
52
+ - Updated dependencies [9dcc848]
53
+ - Updated dependencies [9dcc848]
54
+ - Updated dependencies [9dcc848]
55
+ - Updated dependencies [9dcc848]
56
+ - Updated dependencies [9dcc848]
57
+ - @checkstack/healthcheck-common@1.5.0
58
+ - @checkstack/common@0.13.0
59
+ - @checkstack/signal-common@0.2.6
60
+
61
+ ## 0.7.0
62
+
63
+ ### Minor Changes
64
+
65
+ - 270ef29: Extend the satellite protocol for script-package distribution: the
66
+ `authenticated` / `config_updated` payloads now carry an optional
67
+ `scriptPackagesLockfileHash` (the durable convergence backstop), a new
68
+ `refresh_script_packages { lockfileHash }` core->satellite control push,
69
+ and a new `script_package_sync_state` satellite->core report message. All
70
+ additions are optional / additive, so existing satellites and protocol
71
+ tests are unaffected.
72
+ - 270ef29: Satellite-side script-package reconciliation over the WS channel.
73
+
74
+ - `satellite-common`: WS request/reply messages for pulling the manifest +
75
+ blobs from core (`request_script_package_manifest` /
76
+ `request_script_package_blob` -> `script_package_manifest` /
77
+ `script_package_blob`).
78
+ - `satellite-backend`: the WS handler answers those requests from the
79
+ script-packages store (satellites pull from core, never the registry).
80
+ - `@checkstack/satellite`: the client gains request/reply plumbing + a
81
+ `SatelliteScriptPackages` manager that reuses the Phase 2 reconciler
82
+ (`reconcileToHash` + `createReconcileFsDeps`) over the WS transport. It
83
+ reconciles on a `refresh_script_packages` push and on the
84
+ assignment-carried hash (startup / reconnect backstop), pulls only missing
85
+ blobs (delta), materializes via `bun install --offline`, atomically flips
86
+ `current`, reports sync state back, and degrades cleanly (error state, no
87
+ stale tree, no registry access) when a blob can't be fetched. Reconciles
88
+ are serialized + coalesced + idempotent.
89
+
90
+ - 270ef29: Secrets platform Phase 3: just-in-time secret delivery to satellites + source-side masking, and central-execution injection for healthcheck collectors.
91
+
92
+ - New satellite WS messages `request_run_secrets` / `run_secrets`: just
93
+ before a satellite runs a collector that declares a `secretEnv`, it asks
94
+ core for that collector's resolved env; core resolves ONLY the secrets the
95
+ collector's OWN persisted assignment declares (least-privilege — the
96
+ satellite cannot choose) and replies with the env map (or a clear error).
97
+ The satellite injects it memory-only for the run and drops it on
98
+ completion. Secrets never ride the persisted assignment and never touch
99
+ disk.
100
+ - Source-side masking: the satellite runs `maskSecrets` over the collector's
101
+ stdout/stderr/result/error using the run's delivered values BEFORE the
102
+ result leaves the satellite (defense in depth).
103
+ - `CollectorStrategy.execute` gains an optional `secretEnv`. The
104
+ inline-script and shell collectors inject it into the runner
105
+ (`process.env` / `$VAR`) and mask the values out of their output.
106
+ - Healthcheck collectors running centrally (the queue executor) also resolve
107
+ - inject `secretEnv` via `secretResolverRef`, closing the gap where a
108
+ centrally-run secretEnv collector got no secrets. A missing required
109
+ secret fails the run clearly in all paths.
110
+
111
+ ### Patch Changes
112
+
113
+ - Updated dependencies [270ef29]
114
+ - Updated dependencies [270ef29]
115
+ - Updated dependencies [270ef29]
116
+ - Updated dependencies [b995afb]
117
+ - Updated dependencies [b995afb]
118
+ - Updated dependencies [270ef29]
119
+ - Updated dependencies [270ef29]
120
+ - @checkstack/healthcheck-common@1.4.0
121
+
3
122
  ## 0.6.0
4
123
 
5
124
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/satellite-common",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -9,16 +9,16 @@
9
9
  }
10
10
  },
11
11
  "dependencies": {
12
- "@checkstack/common": "0.11.0",
13
- "@checkstack/healthcheck-common": "1.2.0",
14
- "@checkstack/signal-common": "0.2.4",
15
- "@orpc/contract": "^1.13.14",
12
+ "@checkstack/common": "0.12.0",
13
+ "@checkstack/healthcheck-common": "1.4.0",
14
+ "@checkstack/signal-common": "0.2.5",
15
+ "@orpc/contract": "^1.14.4",
16
16
  "zod": "^4.2.1"
17
17
  },
18
18
  "devDependencies": {
19
19
  "typescript": "^5.7.2",
20
20
  "@checkstack/tsconfig": "0.0.7",
21
- "@checkstack/scripts": "0.3.3"
21
+ "@checkstack/scripts": "0.3.4"
22
22
  },
23
23
  "scripts": {
24
24
  "typecheck": "tsgo -b",
package/src/index.ts CHANGED
@@ -29,6 +29,14 @@ export {
29
29
  type AuthFailedMessage,
30
30
  type ConfigUpdatedMessage,
31
31
  type ShutdownMessage,
32
+ type ScriptPackageSyncStateMessage,
33
+ type RefreshScriptPackagesMessage,
34
+ type ScriptPackageManifestMessage,
35
+ type ScriptPackageBlobMessage,
36
+ type RequestScriptPackageManifestMessage,
37
+ type RequestScriptPackageBlobMessage,
38
+ type RequestRunSecretsMessage,
39
+ type RunSecretsMessage,
32
40
  } from "./protocol";
33
41
  export {
34
42
  SATELLITE_STATUS_CHANGED,
@@ -1,5 +1,9 @@
1
1
  import { describe, test, expect } from "bun:test";
2
- import { SatelliteAssignmentSchema } from "./protocol";
2
+ import {
3
+ SatelliteAssignmentSchema,
4
+ CoreToSatelliteMessageSchema,
5
+ SatelliteToCoreMessageSchema,
6
+ } from "./protocol";
3
7
 
4
8
  describe("SatelliteAssignmentSchema", () => {
5
9
  const base = {
@@ -28,3 +32,171 @@ describe("SatelliteAssignmentSchema", () => {
28
32
  expect(parsed.systemName).toBeUndefined();
29
33
  });
30
34
  });
35
+
36
+ describe("script-packages protocol extensions", () => {
37
+ test("authenticated carries an optional scriptPackagesLockfileHash", () => {
38
+ const withHash = CoreToSatelliteMessageSchema.parse({
39
+ type: "authenticated",
40
+ satelliteId: "sat-1",
41
+ assignments: [],
42
+ scriptPackagesLockfileHash: "abc123",
43
+ });
44
+ expect(withHash.type).toBe("authenticated");
45
+ if (withHash.type === "authenticated") {
46
+ expect(withHash.scriptPackagesLockfileHash).toBe("abc123");
47
+ }
48
+ });
49
+
50
+ test("authenticated WITHOUT the hash still parses (version-skew safe)", () => {
51
+ const parsed = CoreToSatelliteMessageSchema.parse({
52
+ type: "authenticated",
53
+ satelliteId: "sat-1",
54
+ assignments: [],
55
+ });
56
+ expect(parsed.type).toBe("authenticated");
57
+ if (parsed.type === "authenticated") {
58
+ expect(parsed.scriptPackagesLockfileHash).toBeUndefined();
59
+ }
60
+ });
61
+
62
+ test("config_updated carries the optional hash", () => {
63
+ const parsed = CoreToSatelliteMessageSchema.parse({
64
+ type: "config_updated",
65
+ assignments: [],
66
+ scriptPackagesLockfileHash: null,
67
+ });
68
+ if (parsed.type === "config_updated") {
69
+ expect(parsed.scriptPackagesLockfileHash).toBeNull();
70
+ }
71
+ });
72
+
73
+ test("refresh_script_packages round-trips", () => {
74
+ const parsed = CoreToSatelliteMessageSchema.parse({
75
+ type: "refresh_script_packages",
76
+ lockfileHash: "deadbeef",
77
+ });
78
+ expect(parsed.type).toBe("refresh_script_packages");
79
+ if (parsed.type === "refresh_script_packages") {
80
+ expect(parsed.lockfileHash).toBe("deadbeef");
81
+ }
82
+ });
83
+
84
+ test("script_package_sync_state round-trips (satellite -> core)", () => {
85
+ const parsed = SatelliteToCoreMessageSchema.parse({
86
+ type: "script_package_sync_state",
87
+ lockfileHash: "abc",
88
+ status: "ready",
89
+ });
90
+ expect(parsed.type).toBe("script_package_sync_state");
91
+ if (parsed.type === "script_package_sync_state") {
92
+ expect(parsed.status).toBe("ready");
93
+ expect(parsed.lockfileHash).toBe("abc");
94
+ }
95
+ });
96
+
97
+ test("script_package_sync_state carries an error", () => {
98
+ const parsed = SatelliteToCoreMessageSchema.parse({
99
+ type: "script_package_sync_state",
100
+ lockfileHash: null,
101
+ status: "error",
102
+ errorMessage: "blob fetch failed",
103
+ });
104
+ if (parsed.type === "script_package_sync_state") {
105
+ expect(parsed.status).toBe("error");
106
+ expect(parsed.errorMessage).toBe("blob fetch failed");
107
+ }
108
+ });
109
+ });
110
+
111
+ describe("run-secrets request/reply (Phase 3 JIT delivery)", () => {
112
+ test("parses request_run_secrets", () => {
113
+ const parsed = SatelliteToCoreMessageSchema.parse({
114
+ type: "request_run_secrets",
115
+ requestId: "req-1",
116
+ configId: "config-1",
117
+ collectorId: "inline-script",
118
+ runId: "run-1",
119
+ });
120
+ expect(parsed.type).toBe("request_run_secrets");
121
+ if (parsed.type === "request_run_secrets") {
122
+ expect(parsed.requestId).toBe("req-1");
123
+ expect(parsed.collectorId).toBe("inline-script");
124
+ }
125
+ });
126
+
127
+ test("parses run_secrets reply with env", () => {
128
+ const parsed = CoreToSatelliteMessageSchema.parse({
129
+ type: "run_secrets",
130
+ requestId: "req-1",
131
+ env: { API_TOKEN: "resolved-value" },
132
+ });
133
+ if (parsed.type === "run_secrets") {
134
+ expect(parsed.env).toEqual({ API_TOKEN: "resolved-value" });
135
+ expect(parsed.error).toBeUndefined();
136
+ }
137
+ });
138
+
139
+ test("parses run_secrets reply with error (no env)", () => {
140
+ const parsed = CoreToSatelliteMessageSchema.parse({
141
+ type: "run_secrets",
142
+ requestId: "req-1",
143
+ error: "required secret not available",
144
+ });
145
+ if (parsed.type === "run_secrets") {
146
+ expect(parsed.error).toBe("required secret not available");
147
+ expect(parsed.env).toBeUndefined();
148
+ }
149
+ });
150
+ });
151
+
152
+ describe("sandbox-policy protocol extensions", () => {
153
+ const policy = {
154
+ enabled: true,
155
+ onUnavailable: "degrade" as const,
156
+ resources: { cpuSeconds: 30 },
157
+ filesystem: { mode: "scratch-plus-ro" as const },
158
+ network: {
159
+ mode: "allowlist" as const,
160
+ allow: ["10.0.0.1"],
161
+ denyLinkLocalAndMetadata: true,
162
+ },
163
+ privilege: { mode: "drop-to-uid" as const },
164
+ };
165
+
166
+ test("authenticated carries an optional sandboxPolicy that round-trips", () => {
167
+ const parsed = CoreToSatelliteMessageSchema.parse({
168
+ type: "authenticated",
169
+ satelliteId: "sat-1",
170
+ assignments: [],
171
+ sandboxPolicy: policy,
172
+ });
173
+ if (parsed.type === "authenticated") {
174
+ expect(parsed.sandboxPolicy?.network.mode).toBe("allowlist");
175
+ expect(parsed.sandboxPolicy?.network.allow).toEqual(["10.0.0.1"]);
176
+ expect(parsed.sandboxPolicy?.resources.cpuSeconds).toBe(30);
177
+ }
178
+ });
179
+
180
+ test("authenticated WITHOUT sandboxPolicy parses (version-skew safety)", () => {
181
+ const parsed = CoreToSatelliteMessageSchema.parse({
182
+ type: "authenticated",
183
+ satelliteId: "sat-1",
184
+ assignments: [],
185
+ });
186
+ if (parsed.type === "authenticated") {
187
+ expect(parsed.sandboxPolicy).toBeUndefined();
188
+ }
189
+ });
190
+
191
+ test("sandbox_policy push message round-trips the full policy", () => {
192
+ const parsed = CoreToSatelliteMessageSchema.parse({
193
+ type: "sandbox_policy",
194
+ policy,
195
+ });
196
+ expect(parsed.type).toBe("sandbox_policy");
197
+ if (parsed.type === "sandbox_policy") {
198
+ expect(parsed.policy.network.mode).toBe("allowlist");
199
+ expect(parsed.policy.privilege.mode).toBe("drop-to-uid");
200
+ }
201
+ });
202
+ });
package/src/protocol.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  HealthCheckStatusSchema,
4
4
  HealthCheckRunResultSchema,
5
5
  } from "@checkstack/healthcheck-common";
6
+ import { sandboxPolicySchema } from "@checkstack/common";
6
7
 
7
8
  // =============================================================================
8
9
  // SATELLITE ASSIGNMENT (Core → Satellite configuration payload)
@@ -78,6 +79,64 @@ const StrategyErrorMessageSchema = z.object({
78
79
  message: z.string(),
79
80
  });
80
81
 
82
+ /**
83
+ * Reports the satellite's script-package reconcile state back to the core,
84
+ * which persists it in `script_package_satellite_state` for the admin UI.
85
+ * Sent after a reconcile attempt (success or failure).
86
+ */
87
+ const ScriptPackageSyncStateMessageSchema = z.object({
88
+ type: z.literal("script_package_sync_state"),
89
+ /** Active lockfile hash this satellite has materialized, or null. */
90
+ lockfileHash: z.string().nullable(),
91
+ /** "pending" | "syncing" | "ready" | "error" */
92
+ status: z.enum(["pending", "syncing", "ready", "error"]),
93
+ errorMessage: z.string().optional(),
94
+ });
95
+
96
+ /**
97
+ * Satellite -> core request for the manifest of a lockfile hash, so the
98
+ * satellite can diff against its local cache. The core replies with a
99
+ * `script_package_manifest` message. Delivered over the existing
100
+ * authenticated WS channel (no separate satellite HTTP auth surface).
101
+ */
102
+ const RequestScriptPackageManifestMessageSchema = z.object({
103
+ type: z.literal("request_script_package_manifest"),
104
+ lockfileHash: z.string(),
105
+ });
106
+
107
+ /**
108
+ * Satellite -> core request for one content-addressed blob by integrity.
109
+ * The core replies with a `script_package_blob` message (base64 bytes).
110
+ */
111
+ const RequestScriptPackageBlobMessageSchema = z.object({
112
+ type: z.literal("request_script_package_blob"),
113
+ integrity: z.string(),
114
+ });
115
+
116
+ /**
117
+ * Satellite -> core just-in-time request for a collector run's resolved
118
+ * secret env, sent right before the satellite executes a collector that
119
+ * declares a `secretEnv` mapping. The core resolves ONLY that collector's
120
+ * declared `secretEnv` (read from the persisted assignment — the satellite
121
+ * does not get to choose which secrets), and replies with a `run_secrets`
122
+ * message carrying the env map (or an error).
123
+ *
124
+ * Secrets NEVER ride the persisted assignment payload; they are delivered
125
+ * over this authenticated channel per-run and held in satellite memory only
126
+ * for the lifetime of the run. `requestId` correlates the reply (a config
127
+ * can run repeatedly; `runId` is for logging/audit).
128
+ */
129
+ const RequestRunSecretsMessageSchema = z.object({
130
+ type: z.literal("request_run_secrets"),
131
+ /** Correlation id for the reply. */
132
+ requestId: z.string(),
133
+ /** The assignment/collector whose declared secretEnv to resolve. */
134
+ configId: z.string(),
135
+ collectorId: z.string(),
136
+ /** Opaque per-run id for audit/logging on the core side. */
137
+ runId: z.string(),
138
+ });
139
+
81
140
  /**
82
141
  * Discriminated union of all messages that a satellite can send to the core.
83
142
  */
@@ -86,6 +145,10 @@ export const SatelliteToCoreMessageSchema = z.discriminatedUnion("type", [
86
145
  HeartbeatMessageSchema,
87
146
  ResultMessageSchema,
88
147
  StrategyErrorMessageSchema,
148
+ ScriptPackageSyncStateMessageSchema,
149
+ RequestScriptPackageManifestMessageSchema,
150
+ RequestScriptPackageBlobMessageSchema,
151
+ RequestRunSecretsMessageSchema,
89
152
  ]);
90
153
 
91
154
  export type SatelliteToCoreMessage = z.infer<
@@ -97,6 +160,18 @@ export type AuthenticateMessage = z.infer<typeof AuthenticateMessageSchema>;
97
160
  export type HeartbeatMessage = z.infer<typeof HeartbeatMessageSchema>;
98
161
  export type ResultMessage = z.infer<typeof ResultMessageSchema>;
99
162
  export type StrategyErrorMessage = z.infer<typeof StrategyErrorMessageSchema>;
163
+ export type ScriptPackageSyncStateMessage = z.infer<
164
+ typeof ScriptPackageSyncStateMessageSchema
165
+ >;
166
+ export type RequestScriptPackageManifestMessage = z.infer<
167
+ typeof RequestScriptPackageManifestMessageSchema
168
+ >;
169
+ export type RequestScriptPackageBlobMessage = z.infer<
170
+ typeof RequestScriptPackageBlobMessageSchema
171
+ >;
172
+ export type RequestRunSecretsMessage = z.infer<
173
+ typeof RequestRunSecretsMessageSchema
174
+ >;
100
175
 
101
176
  // =============================================================================
102
177
  // CORE → SATELLITE MESSAGES
@@ -106,6 +181,23 @@ const AuthenticatedMessageSchema = z.object({
106
181
  type: z.literal("authenticated"),
107
182
  satelliteId: z.string(),
108
183
  assignments: z.array(SatelliteAssignmentSchema),
184
+ /**
185
+ * Desired script-package lockfile hash. Carried alongside assignments as
186
+ * the durable convergence backstop: a satellite that booted after (or
187
+ * missed) a `refresh_script_packages` push still converges on connect.
188
+ * Optional for version-skew safety; null means "no packages installed".
189
+ */
190
+ scriptPackagesLockfileHash: z.string().nullable().optional(),
191
+ /**
192
+ * The resolved GLOBAL script-sandbox policy, pushed at auth time so the
193
+ * satellite enforces the operator's cluster-wide policy from its very first
194
+ * script run. The satellite caches it and resolves every run through it;
195
+ * until this arrives it FAILS CLOSED (denies egress) rather than using the
196
+ * permissive shipped default. Optional for version-skew safety: an older
197
+ * core omits it, and the satellite then stays fail-closed until a
198
+ * `sandbox_policy` push or a reconnect against a newer core delivers it.
199
+ */
200
+ sandboxPolicy: sandboxPolicySchema.optional(),
109
201
  });
110
202
 
111
203
  const AuthFailedMessageSchema = z.object({
@@ -116,6 +208,8 @@ const AuthFailedMessageSchema = z.object({
116
208
  const ConfigUpdatedMessageSchema = z.object({
117
209
  type: z.literal("config_updated"),
118
210
  assignments: z.array(SatelliteAssignmentSchema),
211
+ /** See {@link AuthenticatedMessageSchema.scriptPackagesLockfileHash}. */
212
+ scriptPackagesLockfileHash: z.string().nullable().optional(),
119
213
  });
120
214
 
121
215
  const ShutdownMessageSchema = z.object({
@@ -123,6 +217,73 @@ const ShutdownMessageSchema = z.object({
123
217
  reason: z.string(),
124
218
  });
125
219
 
220
+ /**
221
+ * Control push telling the satellite to reconcile its script packages to a
222
+ * new lockfile hash. Sent by each core instance's `script-packages.changed`
223
+ * broadcast handler to its currently-connected satellites. Best-effort
224
+ * liveness; the assignment-carried `scriptPackagesLockfileHash` is the
225
+ * durable backstop.
226
+ */
227
+ const RefreshScriptPackagesMessageSchema = z.object({
228
+ type: z.literal("refresh_script_packages"),
229
+ lockfileHash: z.string(),
230
+ });
231
+
232
+ /**
233
+ * Push the new GLOBAL script-sandbox policy to a connected satellite when an
234
+ * admin changes it (the push-on-change relay). Sent by each core instance's
235
+ * `script-sandbox.policy-changed` broadcast handler to its currently-connected
236
+ * satellites. The satellite replaces its cached policy so subsequent runs
237
+ * enforce it immediately. Best-effort liveness; the policy carried in the
238
+ * `authenticated` message on (re)connect is the durable backstop.
239
+ */
240
+ const SandboxPolicyMessageSchema = z.object({
241
+ type: z.literal("sandbox_policy"),
242
+ policy: sandboxPolicySchema,
243
+ });
244
+
245
+ /** One resolved package in a manifest reply. */
246
+ const ManifestEntryWireSchema = z.object({
247
+ name: z.string(),
248
+ version: z.string(),
249
+ integrity: z.string(),
250
+ });
251
+
252
+ /** Core reply to `request_script_package_manifest`. */
253
+ const ScriptPackageManifestMessageSchema = z.object({
254
+ type: z.literal("script_package_manifest"),
255
+ lockfileHash: z.string(),
256
+ entries: z.array(ManifestEntryWireSchema),
257
+ });
258
+
259
+ /** Core reply to `request_script_package_blob` (base64 compressed bytes). */
260
+ const ScriptPackageBlobMessageSchema = z.object({
261
+ type: z.literal("script_package_blob"),
262
+ integrity: z.string(),
263
+ /** base64-encoded compressed blob bytes, or null if not found. */
264
+ data: z.string().nullable(),
265
+ });
266
+
267
+ /**
268
+ * Core reply to `request_run_secrets`. Carries the resolved env map on
269
+ * success, or an `error` when a required secret could not be resolved /
270
+ * the collector was not found. The satellite injects `env` memory-only for
271
+ * the run and drops it on completion; on `error` it fails the run clearly
272
+ * rather than running without the secret.
273
+ *
274
+ * The env map never persists on the core side and is never written to disk
275
+ * on the satellite.
276
+ */
277
+ const RunSecretsMessageSchema = z.object({
278
+ type: z.literal("run_secrets"),
279
+ /** Correlates with the originating `request_run_secrets`. */
280
+ requestId: z.string(),
281
+ /** Resolved env, present only on success. */
282
+ env: z.record(z.string(), z.string()).optional(),
283
+ /** Set when resolution failed; `env` is then absent. */
284
+ error: z.string().optional(),
285
+ });
286
+
126
287
  /**
127
288
  * Discriminated union of all messages that the core can send to a satellite.
128
289
  */
@@ -131,6 +292,11 @@ export const CoreToSatelliteMessageSchema = z.discriminatedUnion("type", [
131
292
  AuthFailedMessageSchema,
132
293
  ConfigUpdatedMessageSchema,
133
294
  ShutdownMessageSchema,
295
+ RefreshScriptPackagesMessageSchema,
296
+ SandboxPolicyMessageSchema,
297
+ ScriptPackageManifestMessageSchema,
298
+ ScriptPackageBlobMessageSchema,
299
+ RunSecretsMessageSchema,
134
300
  ]);
135
301
 
136
302
  export type CoreToSatelliteMessage = z.infer<
@@ -142,3 +308,14 @@ export type AuthenticatedMessage = z.infer<typeof AuthenticatedMessageSchema>;
142
308
  export type AuthFailedMessage = z.infer<typeof AuthFailedMessageSchema>;
143
309
  export type ConfigUpdatedMessage = z.infer<typeof ConfigUpdatedMessageSchema>;
144
310
  export type ShutdownMessage = z.infer<typeof ShutdownMessageSchema>;
311
+ export type RefreshScriptPackagesMessage = z.infer<
312
+ typeof RefreshScriptPackagesMessageSchema
313
+ >;
314
+ export type SandboxPolicyMessage = z.infer<typeof SandboxPolicyMessageSchema>;
315
+ export type ScriptPackageManifestMessage = z.infer<
316
+ typeof ScriptPackageManifestMessageSchema
317
+ >;
318
+ export type ScriptPackageBlobMessage = z.infer<
319
+ typeof ScriptPackageBlobMessageSchema
320
+ >;
321
+ export type RunSecretsMessage = z.infer<typeof RunSecretsMessageSchema>;