@telorun/kernel 0.39.1 → 0.40.1
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/README.md +6 -2
- package/dist/application-env.d.ts +7 -0
- package/dist/application-env.d.ts.map +1 -1
- package/dist/application-env.js +19 -0
- package/dist/application-env.js.map +1 -1
- package/dist/controller-loaders/bundle-builder.d.ts.map +1 -1
- package/dist/controller-loaders/bundle-builder.js +4 -3
- package/dist/controller-loaders/bundle-builder.js.map +1 -1
- package/dist/controller-loaders/napi-loader.d.ts.map +1 -1
- package/dist/controller-loaders/napi-loader.js +4 -2
- package/dist/controller-loaders/napi-loader.js.map +1 -1
- package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
- package/dist/controller-loaders/npm-loader.js +7 -5
- package/dist/controller-loaders/npm-loader.js.map +1 -1
- package/dist/evaluation-context.d.ts +1 -2
- package/dist/evaluation-context.d.ts.map +1 -1
- package/dist/evaluation-context.js +1 -2
- package/dist/evaluation-context.js.map +1 -1
- package/dist/host-env.d.ts +66 -0
- package/dist/host-env.d.ts.map +1 -0
- package/dist/host-env.js +130 -0
- package/dist/host-env.js.map +1 -0
- package/dist/kernel.d.ts +1 -0
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +18 -3
- package/dist/kernel.js.map +1 -1
- package/dist/manifest-sources/local-manifest-cache-source.d.ts.map +1 -1
- package/dist/manifest-sources/local-manifest-cache-source.js +2 -1
- package/dist/manifest-sources/local-manifest-cache-source.js.map +1 -1
- package/dist/module-context.d.ts +1 -2
- package/dist/module-context.d.ts.map +1 -1
- package/dist/module-context.js +1 -30
- package/dist/module-context.js.map +1 -1
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +4 -1
- package/dist/resource-context.js.map +1 -1
- package/package.json +2 -2
- package/src/application-env.ts +19 -0
- package/src/controller-loaders/bundle-builder.ts +4 -3
- package/src/controller-loaders/napi-loader.ts +5 -2
- package/src/controller-loaders/npm-loader.ts +7 -5
- package/src/evaluation-context.ts +1 -2
- package/src/host-env.ts +142 -0
- package/src/kernel.ts +22 -2
- package/src/manifest-sources/local-manifest-cache-source.ts +3 -1
- package/src/module-context.ts +0 -27
- package/src/resource-context.ts +4 -1
|
@@ -794,8 +794,7 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
794
794
|
* trace — `variables`, masked `secrets`, resource `snapshots`, `ports`. Attached
|
|
795
795
|
* to a trace's *root* span so the consumer can inspect what data the execution
|
|
796
796
|
* could reference (beyond its own inputs/outputs). Only a `ModuleContext` owns a
|
|
797
|
-
* root scope; child scopes return undefined.
|
|
798
|
-
* (it is the raw process environment — too broad/sensitive to dump).
|
|
797
|
+
* root scope; child scopes return undefined.
|
|
799
798
|
*/
|
|
800
799
|
protected traceRootScope(): Record<string, unknown> | undefined {
|
|
801
800
|
return undefined;
|
package/src/host-env.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-environment access for the kernel, and the controller `process.env`
|
|
3
|
+
* guardrail.
|
|
4
|
+
*
|
|
5
|
+
* Controllers (and their dependencies) should not bypass a declared binding by
|
|
6
|
+
* reading its raw host env var: host configuration reaches a controller only
|
|
7
|
+
* through `ctx.env` (the snapshot the kernel threads in) or its declared
|
|
8
|
+
* `variables` / `secrets` / `ports`. To enforce that, {@link lockControllerEnv}
|
|
9
|
+
* replaces the global `process.env` with a Proxy that denies reads of exactly
|
|
10
|
+
* the env-var names the manifest binds — every other key (`NODE_ENV`, an SDK's
|
|
11
|
+
* own `AWS_*` / `SMITHY_*` config, `~/.aws` path lookups, …) passes through
|
|
12
|
+
* untouched. The kernel carries no allowlist of vendor env conventions; the
|
|
13
|
+
* denied set is derived from the manifest.
|
|
14
|
+
*
|
|
15
|
+
* This is a **guardrail, not an isolation boundary**: controllers run in the
|
|
16
|
+
* same process, so a determined one can still reach the OS environment by other
|
|
17
|
+
* means (a child process, `/proc/self/environ`, re-`defineProperty`). The Proxy
|
|
18
|
+
* stops casual / accidental bypass of a declared binding and makes it visible.
|
|
19
|
+
*
|
|
20
|
+
* The kernel itself still needs a handful of `TELO_*` / cache env vars. Those
|
|
21
|
+
* reads go through {@link hostEnv}, which returns a reference captured at module
|
|
22
|
+
* import — before any lock — so they keep working once the Proxy is installed.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// The real environment, captured before any lock — and stored on `globalThis`
|
|
26
|
+
// under a process-global symbol so it is shared across *every* `@telorun/kernel`
|
|
27
|
+
// module instance in the process. This matters because a controller can load a
|
|
28
|
+
// second copy of the kernel: the `@telorun/test` suite runner imports `Kernel`
|
|
29
|
+
// to spawn child kernels in-process. That second copy's module body runs *after*
|
|
30
|
+
// the main kernel already locked `process.env`, so a plain `process.env` capture
|
|
31
|
+
// there would grab the (proxied) env. Reading the shared snapshot instead keeps
|
|
32
|
+
// every instance's `hostEnv()` — and the subprocess env it feeds — real.
|
|
33
|
+
//
|
|
34
|
+
// `lockControllerEnv` only rebinds the `process.env` *property*; it never mutates
|
|
35
|
+
// the original object, so this reference keeps yielding real values after a lock.
|
|
36
|
+
const REAL_ENV_KEY = Symbol.for("@telorun/kernel:host-env:real-env");
|
|
37
|
+
const LOCKED_KEY = Symbol.for("@telorun/kernel:host-env:locked");
|
|
38
|
+
const DENIED_KEYS_KEY = Symbol.for("@telorun/kernel:host-env:denied-keys");
|
|
39
|
+
const globals = globalThis as Record<symbol, unknown>;
|
|
40
|
+
const REAL_ENV: NodeJS.ProcessEnv = (globals[REAL_ENV_KEY] ??=
|
|
41
|
+
process.env) as NodeJS.ProcessEnv;
|
|
42
|
+
|
|
43
|
+
// Process-global set of denied env-var names — the union of every booted
|
|
44
|
+
// kernel's declared `variables` / `secrets` / `ports` env keys. Shared on
|
|
45
|
+
// `globalThis` so a second in-process `@telorun/kernel` copy (the test suite
|
|
46
|
+
// runner spawns child kernels) and the already-installed Proxy read the same
|
|
47
|
+
// live set: a kernel that boots *after* the Proxy is installed still contributes
|
|
48
|
+
// its declared keys (see {@link lockControllerEnv}).
|
|
49
|
+
const DENIED_KEYS: Set<string> = (globals[DENIED_KEYS_KEY] ??= new Set<string>()) as Set<string>;
|
|
50
|
+
|
|
51
|
+
/** The real host environment. Kernel-internal use only — never handed to
|
|
52
|
+
* controllers (they get the sanctioned `ctx.env` snapshot instead). */
|
|
53
|
+
export function hostEnv(): NodeJS.ProcessEnv {
|
|
54
|
+
return REAL_ENV;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build the guardrail Proxy over `backing`. A key in `deniedKeys` reads back
|
|
59
|
+
* `undefined` — even when set — and is hidden from `in` / enumeration, with the
|
|
60
|
+
* first read of each such key reported via `warn`. Every other key passes
|
|
61
|
+
* through transparently (real value, no warning, visible to `in` /
|
|
62
|
+
* enumeration). Writes pass through to `backing`.
|
|
63
|
+
*
|
|
64
|
+
* `deniedKeys` is consulted live on every trap, so keys added after the Proxy
|
|
65
|
+
* is installed take effect immediately (a later kernel's bindings).
|
|
66
|
+
*
|
|
67
|
+
* Exported for testing; production installs it via {@link lockControllerEnv}.
|
|
68
|
+
*/
|
|
69
|
+
export function createLockedEnv(
|
|
70
|
+
backing: NodeJS.ProcessEnv,
|
|
71
|
+
deniedKeys: ReadonlySet<string>,
|
|
72
|
+
warn: (key: string) => void,
|
|
73
|
+
): NodeJS.ProcessEnv {
|
|
74
|
+
const warned = new Set<string>();
|
|
75
|
+
const denied = (key: string | symbol): boolean =>
|
|
76
|
+
typeof key === "string" && deniedKeys.has(key);
|
|
77
|
+
|
|
78
|
+
return new Proxy(backing, {
|
|
79
|
+
get(target, key) {
|
|
80
|
+
if (!denied(key)) return Reflect.get(target, key);
|
|
81
|
+
const name = key as string;
|
|
82
|
+
if (!warned.has(name)) {
|
|
83
|
+
warned.add(name);
|
|
84
|
+
warn(name);
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
},
|
|
88
|
+
has(target, key) {
|
|
89
|
+
return !denied(key) && Reflect.has(target, key);
|
|
90
|
+
},
|
|
91
|
+
ownKeys(target) {
|
|
92
|
+
return Reflect.ownKeys(target).filter((key) => !denied(key));
|
|
93
|
+
},
|
|
94
|
+
getOwnPropertyDescriptor(target, key) {
|
|
95
|
+
return denied(key) ? undefined : Reflect.getOwnPropertyDescriptor(target, key);
|
|
96
|
+
},
|
|
97
|
+
set(target, key, value) {
|
|
98
|
+
return Reflect.set(target, key, value);
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Install the {@link createLockedEnv} guardrail over the global `process.env`,
|
|
105
|
+
* denying reads of `deniedKeys` — the booting kernel's declared `variables` /
|
|
106
|
+
* `secrets` / `ports` env-var names.
|
|
107
|
+
*
|
|
108
|
+
* The denied set is process-global and additive: every call unions its keys
|
|
109
|
+
* into the shared {@link DENIED_KEYS} set, even after the Proxy is installed, so
|
|
110
|
+
* each kernel that boots in the process contributes its bindings. Several
|
|
111
|
+
* `Kernel` instances can boot in one process (the test suite runs child kernels
|
|
112
|
+
* in-process), each with its own manifest; the union denies any key declared by
|
|
113
|
+
* any of them — strictly safe, since a controller has no reason to read a
|
|
114
|
+
* sibling app's declared key from the raw env.
|
|
115
|
+
*
|
|
116
|
+
* The Proxy install itself is idempotent — the flag lives on `globalThis`, so a
|
|
117
|
+
* second `@telorun/kernel` instance sees the first instance's lock and does not
|
|
118
|
+
* re-wrap. The property is left non-writable so a casual `process.env = {…}`
|
|
119
|
+
* cannot drop the guardrail (`configurable` stays true so tooling can still
|
|
120
|
+
* manage it).
|
|
121
|
+
*
|
|
122
|
+
* `warn` binds to the first kernel that boots in the process. In the common
|
|
123
|
+
* one-kernel-per-process case that is the right sink; with in-process child
|
|
124
|
+
* kernels a stray read warns to the first kernel's stderr (and dedup is
|
|
125
|
+
* process-global). Acceptable — child kernels receive an explicit env and have
|
|
126
|
+
* no reason to read a declared key from `process.env`.
|
|
127
|
+
*/
|
|
128
|
+
export function lockControllerEnv(
|
|
129
|
+
deniedKeys: Iterable<string>,
|
|
130
|
+
warn: (key: string) => void,
|
|
131
|
+
): void {
|
|
132
|
+
for (const key of deniedKeys) DENIED_KEYS.add(key);
|
|
133
|
+
if (globals[LOCKED_KEY]) return;
|
|
134
|
+
globals[LOCKED_KEY] = true;
|
|
135
|
+
|
|
136
|
+
Object.defineProperty(process, "env", {
|
|
137
|
+
value: createLockedEnv(REAL_ENV, DENIED_KEYS, warn),
|
|
138
|
+
writable: false,
|
|
139
|
+
enumerable: true,
|
|
140
|
+
configurable: true,
|
|
141
|
+
});
|
|
142
|
+
}
|
package/src/kernel.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import { parseArgs } from "util";
|
|
33
33
|
import { ControllerRegistry } from "./controller-registry.js";
|
|
34
34
|
import { EventBus } from "./events.js";
|
|
35
|
+
import { hostEnv, lockControllerEnv } from "./host-env.js";
|
|
35
36
|
import { KernelTracer } from "./tracing.js";
|
|
36
37
|
import { ModuleContext } from "./module-context.js";
|
|
37
38
|
import { ResourceContextImpl } from "./resource-context.js";
|
|
@@ -48,6 +49,7 @@ import {
|
|
|
48
49
|
resolveCacheRoot,
|
|
49
50
|
} from "./manifest-sources/local-manifest-cache-source.js";
|
|
50
51
|
import {
|
|
52
|
+
collectDeclaredEnvKeys,
|
|
51
53
|
precompileApplicationEnvSchemas,
|
|
52
54
|
precompileDefinitionSchemas,
|
|
53
55
|
resolveApplicationEnv,
|
|
@@ -119,6 +121,9 @@ export class Kernel implements IKernel {
|
|
|
119
121
|
* per name. Surfaced via {@link getResolvedPorts} so a host can advertise where
|
|
120
122
|
* the running app is reachable. */
|
|
121
123
|
private _resolvedPorts: Array<{ name: string; port: number; protocol: "tcp" | "udp" }> = [];
|
|
124
|
+
// Host env-var names the root Application binds via `variables`/`secrets`/
|
|
125
|
+
// `ports`; the denied set for the `process.env` guardrail installed at boot.
|
|
126
|
+
private _declaredEnvKeys: string[] = [];
|
|
122
127
|
/** The `.telo` cache root for this load, resolved once in `load()` and
|
|
123
128
|
* threaded to the validator, analysis stamp, and npm install root. */
|
|
124
129
|
private _cacheRoot?: string | null;
|
|
@@ -148,7 +153,7 @@ export class Kernel implements IKernel {
|
|
|
148
153
|
this.stdin = options.stdin ?? process.stdin;
|
|
149
154
|
this.stdout = options.stdout ?? process.stdout;
|
|
150
155
|
this.stderr = options.stderr ?? process.stderr;
|
|
151
|
-
this.env = options.env ??
|
|
156
|
+
this.env = options.env ?? hostEnv();
|
|
152
157
|
this.argv = options.argv ?? [];
|
|
153
158
|
this.registryUrl = options.registryUrl;
|
|
154
159
|
this.loader = new Loader(defaultSources(this.registryUrl), { celHandlers: nodeCelHandlers });
|
|
@@ -338,7 +343,6 @@ export class Kernel implements IKernel {
|
|
|
338
343
|
[],
|
|
339
344
|
this._createInstance.bind(this),
|
|
340
345
|
(event, payload, metadata) => this.eventBus.emit(event, payload, metadata),
|
|
341
|
-
this.env,
|
|
342
346
|
);
|
|
343
347
|
this.rootContext.tracer = this.tracer;
|
|
344
348
|
// Initialize built-in Runtime definitions first
|
|
@@ -551,6 +555,9 @@ export class Kernel implements IKernel {
|
|
|
551
555
|
}
|
|
552
556
|
|
|
553
557
|
if (rootApplicationManifest) {
|
|
558
|
+
this._declaredEnvKeys = collectDeclaredEnvKeys(
|
|
559
|
+
rootApplicationManifest as Record<string, any>,
|
|
560
|
+
);
|
|
554
561
|
const { variables, secrets, ports } = resolveApplicationEnv(
|
|
555
562
|
rootApplicationManifest as Record<string, any>,
|
|
556
563
|
this.env,
|
|
@@ -606,6 +613,19 @@ export class Kernel implements IKernel {
|
|
|
606
613
|
}
|
|
607
614
|
this._bootCalled = true;
|
|
608
615
|
|
|
616
|
+
// Lock the ambient host environment before any controller runs: a key the
|
|
617
|
+
// manifest binds via `variables`/`secrets`/`ports` must be read through
|
|
618
|
+
// `ctx.env` or the declared binding, never the raw `process.env` var. Every
|
|
619
|
+
// other key passes through. Only real runs reach boot() — analyzeOnly loads
|
|
620
|
+
// return earlier — so analysis/editor are unaffected. The denied set is
|
|
621
|
+
// process-global and additive across in-process kernels.
|
|
622
|
+
lockControllerEnv(this._declaredEnvKeys, (key) => {
|
|
623
|
+
this.stderr.write(
|
|
624
|
+
`[telo] controller read process.env.${key} directly — ${key} is a declared ` +
|
|
625
|
+
`binding; read it through ctx.env or its variable/secret, not raw process.env.\n`,
|
|
626
|
+
);
|
|
627
|
+
});
|
|
628
|
+
|
|
609
629
|
// Call register hooks for controllers actually loaded at this point (built-ins).
|
|
610
630
|
// User-module kinds load their controllers during Phase 3 (Telo.Definition.init),
|
|
611
631
|
// and registerController() fires their register hook there.
|
|
@@ -5,6 +5,8 @@ import * as fs from "fs/promises";
|
|
|
5
5
|
import * as path from "path";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
7
7
|
|
|
8
|
+
import { hostEnv } from "../host-env.js";
|
|
9
|
+
|
|
8
10
|
const CACHE_SUBDIR = ".telo/manifests";
|
|
9
11
|
const HTTP_NAMESPACE = "__http";
|
|
10
12
|
const DEFAULT_MANIFEST_FILENAME = "telo.yaml";
|
|
@@ -274,7 +276,7 @@ export function resolveEntryDir(entryPath: string): string | null {
|
|
|
274
276
|
* Consumers append the conventional subdirs: `manifests/`, `manifests/__validators/`,
|
|
275
277
|
* `npm/`. */
|
|
276
278
|
export function resolveCacheRoot(entryPath: string): string | null {
|
|
277
|
-
const override =
|
|
279
|
+
const override = hostEnv().TELO_CACHE_DIR;
|
|
278
280
|
if (override && override.trim()) return path.resolve(override.trim());
|
|
279
281
|
const entryDir = resolveEntryDir(entryPath);
|
|
280
282
|
return entryDir ? path.join(entryDir, ".telo") : null;
|
package/src/module-context.ts
CHANGED
|
@@ -12,31 +12,6 @@ import type {
|
|
|
12
12
|
import type { EmitEvent, InstanceFactory } from "@telorun/sdk";
|
|
13
13
|
import { EvaluationContext } from "./evaluation-context.js";
|
|
14
14
|
|
|
15
|
-
/** Wraps process.env so that missing keys return null instead of throwing in CEL.
|
|
16
|
-
* cel-js uses Object.hasOwn(obj, key) before accessing obj[key], so we must
|
|
17
|
-
* intercept getOwnPropertyDescriptor to report every string key as "own".
|
|
18
|
-
* The `constructor` key is special-cased to return `Object` so cel-js's dyn
|
|
19
|
-
* value-type matcher recognises the proxy as a plain map; Node's process.env
|
|
20
|
-
* has an anonymous-function constructor that cel-js otherwise rejects with
|
|
21
|
-
* "Unsupported type: object". */
|
|
22
|
-
function lenientEnv(env: Record<string, string | undefined>): Record<string, string | null> {
|
|
23
|
-
return new Proxy(env as Record<string, string | null>, {
|
|
24
|
-
get(target, key) {
|
|
25
|
-
if (typeof key !== "string") return (target as any)[key];
|
|
26
|
-
if (key === "constructor") return Object as unknown as string;
|
|
27
|
-
return key in target ? (target[key] ?? null) : null;
|
|
28
|
-
},
|
|
29
|
-
has() {
|
|
30
|
-
return true;
|
|
31
|
-
},
|
|
32
|
-
getOwnPropertyDescriptor(target, key) {
|
|
33
|
-
if (typeof key !== "string") return Object.getOwnPropertyDescriptor(target, key);
|
|
34
|
-
const value = key in target ? (target[key] ?? null) : null;
|
|
35
|
-
return { configurable: true, enumerable: true, writable: true, value };
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
15
|
function collectSecretValues(secrets: Record<string, unknown>): Set<string> {
|
|
41
16
|
const values = new Set<string>();
|
|
42
17
|
for (const value of Object.values(secrets)) {
|
|
@@ -155,7 +130,6 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
155
130
|
private targets: BootTarget[] = [],
|
|
156
131
|
createInstance: InstanceFactory = async () => null,
|
|
157
132
|
emit: EmitEvent,
|
|
158
|
-
private readonly _hostEnv?: Record<string, string | undefined>,
|
|
159
133
|
) {
|
|
160
134
|
super(source, {}, createInstance, new Set(), emit);
|
|
161
135
|
this._variables = variables;
|
|
@@ -461,7 +435,6 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
461
435
|
secrets: this._secrets,
|
|
462
436
|
resources: this._resources,
|
|
463
437
|
ports: this._ports,
|
|
464
|
-
...(this._hostEnv ? { env: lenientEnv(this._hostEnv) } : {}),
|
|
465
438
|
};
|
|
466
439
|
this._secretValues = collectSecretValues(this._secrets);
|
|
467
440
|
}
|
package/src/resource-context.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type TypeRule,
|
|
19
19
|
} from "@telorun/sdk";
|
|
20
20
|
import { isRefSentinel } from "@telorun/templating";
|
|
21
|
+
import { hostEnv } from "./host-env.js";
|
|
21
22
|
import { stripCompiledValues } from "./schema-compiled-values.js";
|
|
22
23
|
import AjvModule from "ajv";
|
|
23
24
|
import addFormats from "ajv-formats";
|
|
@@ -55,7 +56,9 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
55
56
|
args?: ParsedArgs,
|
|
56
57
|
ownerPrefix = "",
|
|
57
58
|
) {
|
|
58
|
-
|
|
59
|
+
// `ctx.env` is the sanctioned host-env channel for controllers — always the
|
|
60
|
+
// real environment (kernel passes its snapshot), never the locked Proxy.
|
|
61
|
+
this.env = env ?? hostEnv();
|
|
59
62
|
this.stdin = stdin ?? process.stdin;
|
|
60
63
|
this.stdout = stdout ?? process.stdout;
|
|
61
64
|
this.stderr = stderr ?? process.stderr;
|