ai-runtime-engine 1.2.0 → 2.7.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.
Files changed (153) hide show
  1. package/CHANGELOG.md +602 -0
  2. package/dist/agents/admit.d.ts +69 -0
  3. package/dist/agents/admit.js +129 -0
  4. package/dist/agents/definition.d.ts +36 -0
  5. package/dist/agents/definition.js +9 -0
  6. package/dist/agents/envelope.d.ts +53 -0
  7. package/dist/agents/envelope.js +68 -0
  8. package/dist/agents/finding.d.ts +79 -0
  9. package/dist/agents/finding.js +80 -0
  10. package/dist/agents/task.d.ts +60 -0
  11. package/dist/agents/task.js +32 -0
  12. package/dist/agents/worker.d.ts +68 -0
  13. package/dist/agents/worker.js +256 -0
  14. package/dist/capabilities/capability.d.ts +117 -0
  15. package/dist/capabilities/capability.js +66 -0
  16. package/dist/capabilities/registry.d.ts +139 -0
  17. package/dist/capabilities/registry.js +413 -0
  18. package/dist/capabilities/vocabulary.d.ts +32 -0
  19. package/dist/capabilities/vocabulary.js +34 -0
  20. package/dist/cli/cli.js +56 -4
  21. package/dist/cli/commands/cleanup.js +29 -27
  22. package/dist/cli/commands/doctor.d.ts +14 -0
  23. package/dist/cli/commands/doctor.js +38 -8
  24. package/dist/cli/commands/executions.js +34 -25
  25. package/dist/cli/commands/info.d.ts +1 -0
  26. package/dist/cli/commands/info.js +11 -9
  27. package/dist/cli/commands/init.js +19 -0
  28. package/dist/cli/commands/inspect.d.ts +40 -1
  29. package/dist/cli/commands/inspect.js +157 -2
  30. package/dist/cli/commands/mcp.d.ts +45 -0
  31. package/dist/cli/commands/mcp.js +148 -0
  32. package/dist/cli/commands/route.js +21 -0
  33. package/dist/cli/commands/run.d.ts +2 -0
  34. package/dist/cli/commands/run.js +36 -4
  35. package/dist/cli/commands/skills.d.ts +2 -0
  36. package/dist/cli/commands/skills.js +29 -7
  37. package/dist/cli/interactive/ansi.d.ts +41 -0
  38. package/dist/cli/interactive/ansi.js +43 -0
  39. package/dist/cli/interactive/complete.d.ts +10 -0
  40. package/dist/cli/interactive/complete.js +19 -0
  41. package/dist/cli/interactive/repl.d.ts +3 -0
  42. package/dist/cli/interactive/repl.js +105 -16
  43. package/dist/cli/interactive/session.d.ts +12 -1
  44. package/dist/cli/interactive/session.js +83 -5
  45. package/dist/cli/render.d.ts +13 -0
  46. package/dist/cli/render.js +18 -0
  47. package/dist/cli/runtimeSession.d.ts +11 -0
  48. package/dist/cli/runtimeSession.js +17 -0
  49. package/dist/config/defaults.d.ts +3 -1
  50. package/dist/config/defaults.js +2 -0
  51. package/dist/config/schema.d.ts +1 -0
  52. package/dist/config/schema.js +2 -2
  53. package/dist/context/lossVerifier.d.ts +24 -0
  54. package/dist/context/lossVerifier.js +45 -0
  55. package/dist/context/summarize.d.ts +19 -0
  56. package/dist/context/summarize.js +53 -0
  57. package/dist/core/fallback/fallback.d.ts +8 -0
  58. package/dist/core/fallback/fallback.js +3 -1
  59. package/dist/core/router/executor.d.ts +6 -1
  60. package/dist/core/router/executor.js +9 -2
  61. package/dist/core/router/normalize.d.ts +2 -0
  62. package/dist/core/router/request.js +2 -0
  63. package/dist/core/router/router.d.ts +3 -0
  64. package/dist/core/router/router.js +7 -0
  65. package/dist/executions/execution.d.ts +13 -2
  66. package/dist/generation/generateAdapter.d.ts +14 -0
  67. package/dist/generation/generateAdapter.js +38 -0
  68. package/dist/generation/generateSkill.d.ts +26 -0
  69. package/dist/generation/generateSkill.js +51 -0
  70. package/dist/index.d.ts +44 -5
  71. package/dist/index.js +26 -2
  72. package/dist/mcp/client.d.ts +70 -0
  73. package/dist/mcp/client.js +221 -0
  74. package/dist/mcp/manager.d.ts +151 -0
  75. package/dist/mcp/manager.js +493 -0
  76. package/dist/mcp/protocol.d.ts +216 -0
  77. package/dist/mcp/protocol.js +149 -0
  78. package/dist/mcp/toolAdapter.d.ts +44 -0
  79. package/dist/mcp/toolAdapter.js +94 -0
  80. package/dist/mcp/transport.d.ts +109 -0
  81. package/dist/mcp/transport.js +383 -0
  82. package/dist/memory/embedders/hash.d.ts +12 -0
  83. package/dist/memory/embedders/hash.js +31 -0
  84. package/dist/memory/embedders/http.d.ts +25 -0
  85. package/dist/memory/embedders/http.js +48 -0
  86. package/dist/memory/memory.d.ts +19 -2
  87. package/dist/memory/memory.js +75 -11
  88. package/dist/memory/semantic.d.ts +17 -0
  89. package/dist/memory/semantic.js +29 -0
  90. package/dist/orchestration/budget.d.ts +30 -0
  91. package/dist/orchestration/budget.js +40 -0
  92. package/dist/orchestration/executor.d.ts +39 -1
  93. package/dist/orchestration/executor.js +64 -4
  94. package/dist/orchestration/orchestrator.d.ts +29 -1
  95. package/dist/orchestration/orchestrator.js +89 -8
  96. package/dist/orchestration/plan.d.ts +15 -1
  97. package/dist/orchestration/plan.js +23 -4
  98. package/dist/orchestration/planner.d.ts +19 -1
  99. package/dist/orchestration/planner.js +25 -5
  100. package/dist/plugin/ai.d.ts +4 -0
  101. package/dist/plugin/ai.js +9 -0
  102. package/dist/providers/httpClient.d.ts +25 -1
  103. package/dist/providers/httpClient.js +93 -0
  104. package/dist/providers/httpProvider.d.ts +1 -0
  105. package/dist/providers/httpProvider.js +67 -1
  106. package/dist/providers/mock/mockProvider.d.ts +3 -0
  107. package/dist/providers/mock/mockProvider.js +54 -0
  108. package/dist/providers/mock/scenarios.d.ts +7 -0
  109. package/dist/providers/provider.d.ts +6 -0
  110. package/dist/providers/wire/anthropicWire.js +34 -0
  111. package/dist/providers/wire/openaiWire.js +30 -0
  112. package/dist/providers/wire/types.d.ts +16 -0
  113. package/dist/runtime/config.js +50 -6
  114. package/dist/runtime/intent/aiClassifier.d.ts +19 -0
  115. package/dist/runtime/intent/aiClassifier.js +74 -0
  116. package/dist/runtime/models/modelProfile.d.ts +61 -0
  117. package/dist/runtime/models/modelProfile.js +139 -0
  118. package/dist/runtime/planning/deriveCapabilities.d.ts +95 -0
  119. package/dist/runtime/planning/deriveCapabilities.js +146 -0
  120. package/dist/runtime/policy.d.ts +10 -0
  121. package/dist/runtime/policy.js +9 -2
  122. package/dist/runtime/runtime.d.ts +173 -0
  123. package/dist/runtime/runtime.js +723 -50
  124. package/dist/runtime/types.d.ts +94 -2
  125. package/dist/skills/manifest.d.ts +3 -0
  126. package/dist/skills/manifest.js +24 -0
  127. package/dist/skills/registry.d.ts +16 -1
  128. package/dist/skills/registry.js +21 -1
  129. package/dist/skills/skill.d.ts +6 -1
  130. package/dist/store/area.d.ts +15 -1
  131. package/dist/store/area.js +19 -8
  132. package/dist/store/crypto.d.ts +21 -0
  133. package/dist/store/crypto.js +49 -0
  134. package/dist/store/paths.d.ts +5 -1
  135. package/dist/store/paths.js +6 -0
  136. package/dist/store/store.d.ts +15 -3
  137. package/dist/store/store.js +28 -7
  138. package/dist/telemetry/sinks/otlp.d.ts +31 -0
  139. package/dist/telemetry/sinks/otlp.js +76 -0
  140. package/dist/tools/builtins/filesystem.js +1 -0
  141. package/dist/tools/builtins/git.js +1 -0
  142. package/dist/tools/builtins/shell.js +1 -0
  143. package/dist/tools/permissions.d.ts +28 -0
  144. package/dist/tools/permissions.js +72 -0
  145. package/dist/tools/registry.d.ts +18 -2
  146. package/dist/tools/registry.js +22 -2
  147. package/dist/tools/tool.d.ts +4 -0
  148. package/dist/types.d.ts +11 -1
  149. package/dist/util/flatten.d.ts +11 -0
  150. package/dist/util/flatten.js +18 -0
  151. package/dist/util/semaphore.d.ts +19 -0
  152. package/dist/util/semaphore.js +60 -0
  153. package/package.json +24 -9
@@ -7,17 +7,21 @@ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync }
7
7
  import { join } from 'node:path';
8
8
  import { systemClock } from '../util/clock.js';
9
9
  import { FileArea, NullArea } from './area.js';
10
- import { projectId as computeProjectId, repositoryId as computeRepositoryId, resolveHome } from './paths.js';
10
+ import { projectId as computeProjectId, repositoryId as computeRepositoryId, organizationId as computeOrganizationId, resolveHome } from './paths.js';
11
11
  export const STORE_VERSION = 1;
12
12
  export class RuntimeStore {
13
13
  enabled;
14
14
  projectId;
15
15
  repositoryId;
16
+ /** Present only when an organization is configured — the availability signal for the `organization` scope. */
17
+ organizationId;
16
18
  clock;
17
19
  home;
18
20
  projectDir;
19
21
  repoDir;
20
22
  userDir;
23
+ orgDir;
24
+ cipher;
21
25
  areas = new Map();
22
26
  metaStamped = false;
23
27
  constructor(options) {
@@ -30,26 +34,38 @@ export class RuntimeStore {
30
34
  this.projectDir = join(this.home, 'projects', this.projectId);
31
35
  this.repoDir = join(this.home, 'repos', this.repositoryId);
32
36
  this.userDir = join(this.home, 'user');
37
+ if (options.organization && options.organization.trim()) {
38
+ this.organizationId = computeOrganizationId(options.organization);
39
+ this.orgDir = join(this.home, 'organizations', this.organizationId);
40
+ }
41
+ if (options.cipher)
42
+ this.cipher = options.cipher;
33
43
  }
34
44
  paths() {
35
- return { home: this.home, project: this.projectDir, repository: this.repoDir, user: this.userDir };
45
+ return { home: this.home, project: this.projectDir, repository: this.repoDir, user: this.userDir, ...(this.orgDir ? { organization: this.orgDir } : {}) };
36
46
  }
37
47
  area(dir) {
38
48
  if (!this.enabled)
39
- return new NullArea();
49
+ return new NullArea(); // stateless areas are never wrapped
40
50
  let a = this.areas.get(dir);
41
51
  if (!a) {
42
- a = new FileArea(dir);
52
+ a = new FileArea(dir, this.cipher); // cipher (when configured) encrypts every file area transparently
43
53
  this.areas.set(dir, a);
44
54
  }
45
55
  return a;
46
56
  }
47
- /** Memory area for a physical scope. */
57
+ /** Memory area for a physical scope. `organization` requires a configured org (guarded by callers). */
48
58
  memory(scope) {
49
59
  if (scope === 'user')
50
60
  return this.area(join(this.userDir, 'memory'));
51
61
  if (scope === 'repository')
52
62
  return this.area(join(this.repoDir, 'memory'));
63
+ if (scope === 'organization') {
64
+ // Callers only traverse this scope when `organizationId` is set; a stray call with no org is a no-op.
65
+ if (!this.orgDir)
66
+ return new NullArea();
67
+ return this.area(join(this.orgDir, 'memory'));
68
+ }
53
69
  return this.area(join(this.projectDir, 'memory'));
54
70
  }
55
71
  conversations() {
@@ -58,6 +74,10 @@ export class RuntimeStore {
58
74
  indexes() {
59
75
  return this.area(join(this.projectDir, 'indexes'));
60
76
  }
77
+ /** MCP state: dynamic servers, enable/disable overrides, and per-server tool caches (Phase 3.2). */
78
+ mcp() {
79
+ return this.area(join(this.projectDir, 'mcp'));
80
+ }
61
81
  cache() {
62
82
  return this.area(join(this.projectDir, 'cache'));
63
83
  }
@@ -131,10 +151,11 @@ export class RuntimeStore {
131
151
  rmSync(lockPath);
132
152
  }
133
153
  }
134
- /** Aggregate integrity check across the project areas. */
154
+ /** Aggregate integrity check across the project areas (plus the org area when configured). */
135
155
  check() {
136
156
  if (!this.enabled)
137
157
  return [];
138
- return [...this.memory('project').check(), ...this.memory('user').check(), ...this.memory('repository').check(), ...this.conversations().check(), ...this.indexes().check()];
158
+ const org = this.orgDir ? this.memory('organization').check() : [];
159
+ return [...this.memory('project').check(), ...this.memory('user').check(), ...this.memory('repository').check(), ...org, ...this.conversations().check(), ...this.indexes().check()];
139
160
  }
140
161
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * OTLP-shaped telemetry exporter (Phase 19). Maps the four frozen `TelemetryEvent` members to an
3
+ * OpenTelemetry-JSON-ish payload (resource + a scope with logRecords) and POSTs them, batched, to an
4
+ * OTLP/HTTP-style `endpoint`. Metadata ONLY — the events are already routing/perf metadata and are passed
5
+ * through `redact()` before they leave the process; prompt text / inputs / keys never appear. Like every
6
+ * sink, ALL IO is wrapped and swallowed: telemetry must never fail a run.
7
+ *
8
+ * An optional auth header value is read from the env var NAMED by `headersEnv` (never a value in config).
9
+ */
10
+ import type { TelemetrySink } from '../telemetry.js';
11
+ import type { TelemetryEvent } from '../../types.js';
12
+ import type { FetchLike } from '../../providers/httpClient.js';
13
+ export interface OtlpSinkOptions {
14
+ endpoint: string;
15
+ fetchImpl?: FetchLike;
16
+ /** Resolved auth header value (from an env-var NAME upstream), sent as `authorization` if present. */
17
+ authHeader?: string;
18
+ /** Flush when this many events are buffered (default 20). */
19
+ batchSize?: number;
20
+ }
21
+ export declare class OtlpSink implements TelemetrySink {
22
+ private readonly endpoint;
23
+ private readonly fetchImpl;
24
+ private readonly authHeader?;
25
+ private readonly batchSize;
26
+ private buffer;
27
+ constructor(opts: OtlpSinkOptions);
28
+ emit(event: TelemetryEvent): void;
29
+ /** POST the buffered batch. Fire-and-forget; every error is swallowed. */
30
+ flush(): void;
31
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * OTLP-shaped telemetry exporter (Phase 19). Maps the four frozen `TelemetryEvent` members to an
3
+ * OpenTelemetry-JSON-ish payload (resource + a scope with logRecords) and POSTs them, batched, to an
4
+ * OTLP/HTTP-style `endpoint`. Metadata ONLY — the events are already routing/perf metadata and are passed
5
+ * through `redact()` before they leave the process; prompt text / inputs / keys never appear. Like every
6
+ * sink, ALL IO is wrapped and swallowed: telemetry must never fail a run.
7
+ *
8
+ * An optional auth header value is read from the env var NAMED by `headersEnv` (never a value in config).
9
+ */
10
+ import { redact } from '../../security/redact.js';
11
+ /** Severity mapping for the four event types (INFO by default; a failed result is WARN). */
12
+ function toLogRecord(event) {
13
+ const attributes = Object.entries(event)
14
+ .filter(([k]) => k !== 'ts' && k !== 'type')
15
+ .map(([k, v]) => ({ key: k, value: otlpAnyValue(v) }));
16
+ const severe = event.type === 'route.result' && event.ok === false;
17
+ return {
18
+ timeUnixNano: String(BigInt(Math.trunc(event.ts)) * 1000000n), // ms → ns
19
+ severityText: severe ? 'WARN' : 'INFO',
20
+ body: { stringValue: event.type },
21
+ attributes,
22
+ };
23
+ }
24
+ function otlpAnyValue(v) {
25
+ if (typeof v === 'string')
26
+ return { stringValue: v };
27
+ if (typeof v === 'boolean')
28
+ return { boolValue: v };
29
+ if (typeof v === 'number')
30
+ return Number.isInteger(v) ? { intValue: String(v) } : { doubleValue: v };
31
+ return { stringValue: JSON.stringify(v) };
32
+ }
33
+ export class OtlpSink {
34
+ endpoint;
35
+ fetchImpl;
36
+ authHeader;
37
+ batchSize;
38
+ buffer = [];
39
+ constructor(opts) {
40
+ this.endpoint = opts.endpoint;
41
+ this.fetchImpl = opts.fetchImpl ?? fetch;
42
+ if (opts.authHeader)
43
+ this.authHeader = opts.authHeader;
44
+ this.batchSize = opts.batchSize && opts.batchSize > 0 ? opts.batchSize : 20;
45
+ }
46
+ emit(event) {
47
+ this.buffer.push(redact(event));
48
+ if (this.buffer.length >= this.batchSize)
49
+ this.flush();
50
+ }
51
+ /** POST the buffered batch. Fire-and-forget; every error is swallowed. */
52
+ flush() {
53
+ if (this.buffer.length === 0)
54
+ return;
55
+ const batch = this.buffer;
56
+ this.buffer = [];
57
+ const payload = {
58
+ resourceLogs: [
59
+ {
60
+ resource: { attributes: [{ key: 'service.name', value: { stringValue: 'ai-runtime' } }] },
61
+ scopeLogs: [{ scope: { name: 'ai-runtime.router' }, logRecords: batch.map(toLogRecord) }],
62
+ },
63
+ ],
64
+ };
65
+ const headers = { 'content-type': 'application/json' };
66
+ if (this.authHeader)
67
+ headers.authorization = this.authHeader;
68
+ try {
69
+ // Fire-and-forget: never await, never let a rejection escape (telemetry must not fail a run).
70
+ void Promise.resolve(this.fetchImpl(this.endpoint, { method: 'POST', headers, body: JSON.stringify(payload) })).catch(() => { });
71
+ }
72
+ catch {
73
+ /* a telemetry export must never fail a run */
74
+ }
75
+ }
76
+ }
@@ -10,6 +10,7 @@ import { resolveInJail, JailError } from '../jail.js';
10
10
  export const filesystemTool = {
11
11
  id: 'filesystem',
12
12
  description: 'Read, write, list, and check files within the workspace (jailed; no escape).',
13
+ provides: [{ id: 'read_file' }, { id: 'write_file' }],
13
14
  parameters: { op: 'read | write | list | exists', path: 'string (relative to workspace)', content: 'string (write only)' },
14
15
  async execute(input, ctx) {
15
16
  const { op, path, content } = (input ?? {});
@@ -19,6 +19,7 @@ export function createGitTool(runner = defaultRunner) {
19
19
  id: 'git',
20
20
  description: 'Read git state (status/diff/log); commit/push are permission-gated and off by default.',
21
21
  parameters: { op: 'status | diff | log | commit | push', message: 'string (commit)', args: 'string[] (refs only)' },
22
+ provides: [{ id: 'read_repository' }, { id: 'commit_changes' }],
22
23
  async execute(input, ctx) {
23
24
  const { op, message, args = [] } = (input ?? {});
24
25
  if (!op)
@@ -56,6 +56,7 @@ export function createShellTool(runner = defaultRunner) {
56
56
  id: 'shell',
57
57
  description: 'Run an allowlisted shell command in the workspace (destructive commands need approval).',
58
58
  parameters: { command: 'string (argv[0])', args: 'string[]' },
59
+ provides: [{ id: 'run_command' }, { id: 'run_tests' }],
59
60
  async execute(input, ctx) {
60
61
  const { command, args = [] } = (input ?? {});
61
62
  if (typeof command !== 'string' || command === '')
@@ -5,6 +5,8 @@
5
5
  * allowlist fields win over the flat Phase-1 fields when both are present.
6
6
  */
7
7
  import type { PermissionPolicy } from '../runtime/policy.js';
8
+ /** Per-server MCP grant. `off` denies before any prompt; `read` admits read-only tools; `full` admits all. */
9
+ export type McpServerGrant = 'off' | 'read' | 'full';
8
10
  export interface ResolvedPermissions {
9
11
  filesystem: {
10
12
  read: boolean;
@@ -21,5 +23,31 @@ export interface ResolvedPermissions {
21
23
  network: {
22
24
  enabled: boolean;
23
25
  };
26
+ /**
27
+ * MCP execution (Phase 3.2). This is the SOLE authority for MCP tool execution: adding or connecting a
28
+ * server grants nothing, and `network`/`shell` are tool-local concerns that do not gate MCP transports
29
+ * (recorded as a decision — one dimension answers one question, so deny-by-default cannot fail open).
30
+ */
31
+ mcp: {
32
+ servers: Record<string, McpServerGrant>;
33
+ };
24
34
  }
35
+ /**
36
+ * Combine two MCP permission maps by MINIMUM per server — the only nested permission dimension, so its
37
+ * merge rule is explicit: an override can never widen a grant, and a server absent from the override
38
+ * keeps the base grant. Every agent/skill/tool override path must go through this.
39
+ */
40
+ export declare function clampMcpPermissions(base: Record<string, McpServerGrant>, override: Record<string, McpServerGrant | boolean> | undefined): Record<string, McpServerGrant>;
41
+ /**
42
+ * Combine two permission policies by MINIMUM — the permission half of an agent envelope.
43
+ *
44
+ * The output is FULLY EXPLICIT on every dimension (it is resolved through `resolvePermissions` first),
45
+ * and that is what makes it safe to hand to `Runtime.resolvedPermissions` as an override: that merge is
46
+ * a shallow spread, so an override with ABSENT fields would silently fall back to defaults — and
47
+ * `fsRead` defaults to TRUE. Emitting explicit values means the spread can only ever narrow.
48
+ *
49
+ * Booleans combine by AND, `shellAllowedCommands` by INTERSECTION, and `mcp.servers` by
50
+ * `clampMcpPermissions` (per-server minimum). An override can never widen anything.
51
+ */
52
+ export declare function clampPermissions(base: PermissionPolicy, override?: PermissionPolicy): PermissionPolicy;
25
53
  export declare function resolvePermissions(policy?: PermissionPolicy): ResolvedPermissions;
@@ -4,6 +4,74 @@
4
4
  * the workspace), writes/shell/git-mutations/network are OFF unless explicitly granted. The structured
5
5
  * allowlist fields win over the flat Phase-1 fields when both are present.
6
6
  */
7
+ const GRANT_RANK = { off: 0, read: 1, full: 2 };
8
+ /**
9
+ * Normalize the config-facing value (`true` ≡ full, `false` ≡ off) into a grant. Anything that is not
10
+ * exactly one of the three grant names collapses to `off`: the value comes from a YAML file, so
11
+ * `read: yes` (a string), a number, or a typo must never be treated as "some grant".
12
+ */
13
+ function toGrant(v) {
14
+ if (v === true)
15
+ return 'full';
16
+ if (v === 'off' || v === 'read' || v === 'full')
17
+ return v;
18
+ return 'off';
19
+ }
20
+ /**
21
+ * Combine two MCP permission maps by MINIMUM per server — the only nested permission dimension, so its
22
+ * merge rule is explicit: an override can never widen a grant, and a server absent from the override
23
+ * keeps the base grant. Every agent/skill/tool override path must go through this.
24
+ */
25
+ export function clampMcpPermissions(base, override) {
26
+ const out = Object.create(null);
27
+ for (const [k, v] of Object.entries(base))
28
+ out[k] = toGrant(v);
29
+ if (!override)
30
+ return out;
31
+ for (const [server, raw] of Object.entries(override)) {
32
+ const want = toGrant(raw);
33
+ const have = Object.prototype.hasOwnProperty.call(out, server) ? out[server] : 'off';
34
+ out[server] = GRANT_RANK[want] < GRANT_RANK[have] ? want : have;
35
+ }
36
+ return out;
37
+ }
38
+ /** Build a prototype-free grant map, so an unconfigured key can only ever answer `undefined`. */
39
+ function grantMap(raw) {
40
+ const out = Object.create(null);
41
+ for (const [k, v] of Object.entries(raw ?? {}))
42
+ out[k] = toGrant(v);
43
+ return out;
44
+ }
45
+ /**
46
+ * Combine two permission policies by MINIMUM — the permission half of an agent envelope.
47
+ *
48
+ * The output is FULLY EXPLICIT on every dimension (it is resolved through `resolvePermissions` first),
49
+ * and that is what makes it safe to hand to `Runtime.resolvedPermissions` as an override: that merge is
50
+ * a shallow spread, so an override with ABSENT fields would silently fall back to defaults — and
51
+ * `fsRead` defaults to TRUE. Emitting explicit values means the spread can only ever narrow.
52
+ *
53
+ * Booleans combine by AND, `shellAllowedCommands` by INTERSECTION, and `mcp.servers` by
54
+ * `clampMcpPermissions` (per-server minimum). An override can never widen anything.
55
+ */
56
+ export function clampPermissions(base, override) {
57
+ const b = resolvePermissions(base);
58
+ const and = (left, right) => left && (right ?? true);
59
+ const allowed = override?.shellAllowedCommands
60
+ ? b.shell.allowedCommands.filter((c) => override.shellAllowedCommands.includes(c))
61
+ : [...b.shell.allowedCommands];
62
+ return {
63
+ fsRead: and(b.filesystem.read, override?.fsRead),
64
+ fsWrite: and(b.filesystem.write, override?.fsWrite),
65
+ shell: and(b.shell.enabled, override?.shell),
66
+ shellAllowedCommands: allowed,
67
+ // `gitWrite` is the Phase-1 umbrella; emitting the two explicit grants instead means the resolved
68
+ // view cannot be widened by an absent umbrella on a later merge.
69
+ gitCommit: and(b.git.commit, override?.gitCommit ?? override?.gitWrite),
70
+ gitPush: and(b.git.push, override?.gitPush ?? override?.gitWrite),
71
+ network: and(b.network.enabled, override?.network),
72
+ mcp: { servers: { ...clampMcpPermissions(b.mcp.servers, override?.mcp?.servers) } },
73
+ };
74
+ }
7
75
  export function resolvePermissions(policy = {}) {
8
76
  const gitDefault = policy.gitWrite ?? false;
9
77
  return {
@@ -20,5 +88,9 @@ export function resolvePermissions(policy = {}) {
20
88
  push: policy.gitPush ?? gitDefault,
21
89
  },
22
90
  network: { enabled: policy.network ?? false },
91
+ // A NULL-PROTOTYPE map: `Object.fromEntries` inherits Object.prototype, so `servers['constructor']`
92
+ // would answer with a function — and a lookup of `?? 'off'` would then see a truthy non-'off' value.
93
+ // Deny-by-default has to hold for every possible key, not just the configured ones.
94
+ mcp: { servers: grantMap(policy.mcp?.servers) },
23
95
  };
24
96
  }
@@ -1,8 +1,24 @@
1
- /** A registry of tools, keyed by id. Registration is idempotent-by-id (last wins, like tasks). */
1
+ /**
2
+ * A registry of tools, keyed by id. Registration is idempotent-by-id (last wins, like tasks).
3
+ *
4
+ * An optional change listener (Phase 3.1) lets the Runtime ingest ACTION-capability declarations at the
5
+ * one real registration choke point — builtins, user `registerTool`, and future sources all pass through
6
+ * `register()`. The zero-arg constructor is preserved (this class is public API).
7
+ */
2
8
  import type { Tool } from './tool.js';
9
+ export interface ToolRegistryListener {
10
+ /** Called after a tool is registered. `trusted` marks in-tree builtins (see the claiming rule). */
11
+ onRegister?: (tool: Tool, trusted: boolean) => void;
12
+ onUnregister?: (id: string) => void;
13
+ }
3
14
  export declare class ToolRegistry {
4
15
  private readonly tools;
5
- register(tool: Tool): this;
16
+ private listener;
17
+ /** Attach the change listener (called once by the Runtime before builtin registration). */
18
+ attach(listener: ToolRegistryListener): this;
19
+ /** Register a tool. `trusted` is set only for in-tree builtins registered by the Runtime itself. */
20
+ register(tool: Tool, trusted?: boolean): this;
21
+ unregister(id: string): boolean;
6
22
  get(id: string): Tool | undefined;
7
23
  has(id: string): boolean;
8
24
  list(): Tool[];
@@ -1,10 +1,30 @@
1
- /** A registry of tools, keyed by id. Registration is idempotent-by-id (last wins, like tasks). */
1
+ /**
2
+ * A registry of tools, keyed by id. Registration is idempotent-by-id (last wins, like tasks).
3
+ *
4
+ * An optional change listener (Phase 3.1) lets the Runtime ingest ACTION-capability declarations at the
5
+ * one real registration choke point — builtins, user `registerTool`, and future sources all pass through
6
+ * `register()`. The zero-arg constructor is preserved (this class is public API).
7
+ */
2
8
  export class ToolRegistry {
3
9
  tools = new Map();
4
- register(tool) {
10
+ listener = {};
11
+ /** Attach the change listener (called once by the Runtime before builtin registration). */
12
+ attach(listener) {
13
+ this.listener = listener ?? {};
14
+ return this;
15
+ }
16
+ /** Register a tool. `trusted` is set only for in-tree builtins registered by the Runtime itself. */
17
+ register(tool, trusted = false) {
5
18
  this.tools.set(tool.id, tool);
19
+ this.listener.onRegister?.(tool, trusted);
6
20
  return this;
7
21
  }
22
+ unregister(id) {
23
+ const had = this.tools.delete(id);
24
+ if (had)
25
+ this.listener.onUnregister?.(id);
26
+ return had;
27
+ }
8
28
  get(id) {
9
29
  return this.tools.get(id);
10
30
  }
@@ -9,6 +9,7 @@ import type { ArtifactRef } from '../runtime/types.js';
9
9
  import type { ApprovalProvider } from '../runtime/host.js';
10
10
  import type { Clock } from '../util/clock.js';
11
11
  import type { ResolvedPermissions } from './permissions.js';
12
+ import type { ActionCapabilityDecl } from '../capabilities/capability.js';
12
13
  export interface ToolContext {
13
14
  workspaceRoot: string;
14
15
  permissions: ResolvedPermissions;
@@ -35,6 +36,9 @@ export interface Tool {
35
36
  description: string;
36
37
  /** JSON-schema-ish parameter description (advisory; validated by the tool's own execute). */
37
38
  parameters?: unknown;
39
+ /** ACTION capabilities this tool provides (Phase 3.1). Advisory metadata, NEVER authorization — this
40
+ * tool's own permission checks remain the only gate. Absent ⇒ one inferred `tool:<id>` entry. */
41
+ provides?: ActionCapabilityDecl[];
38
42
  execute(input: unknown, ctx: ToolContext): Promise<ToolResult>;
39
43
  }
40
44
  /** Helper: a denied result with a consistent shape. */
package/dist/types.d.ts CHANGED
@@ -80,6 +80,8 @@ export interface AIRequest {
80
80
  signal?: AbortSignal;
81
81
  sensitivity: Sensitivity;
82
82
  metadata?: Record<string, unknown>;
83
+ /** Request token-by-token streaming (text output only). A provider without `executeStream` ignores it. */
84
+ stream?: boolean;
83
85
  }
84
86
  export type FinishReason = 'stop' | 'length' | 'tool_calls' | 'content_filter' | 'error';
85
87
  /** What every provider adapter returns — normalized so callers never parse vendor JSON. */
@@ -335,6 +337,10 @@ export interface RunRequest {
335
337
  verification?: boolean;
336
338
  /** Attach tools from registered MCP sources to this run (requires a tool-calling model). */
337
339
  mcp?: boolean;
340
+ /** Stream the answer token-by-token (text output only; ignored for JSON output). Needs `onDelta`. */
341
+ stream?: boolean;
342
+ /** Called with each text chunk as it streams. The final `AIResponse.text` is still the full aggregate. */
343
+ onDelta?: (chunk: string) => void;
338
344
  }
339
345
  /**
340
346
  * User exclude/prefer routing (all optional). EXCLUDE is a HARD filter — an excluded candidate is never
@@ -375,9 +381,13 @@ export interface ProviderConfig {
375
381
  }
376
382
  export interface TelemetryConfig {
377
383
  enabled: boolean;
378
- sink: 'memory' | 'file';
384
+ sink: 'memory' | 'file' | 'otlp';
379
385
  storePrompts: false;
380
386
  path?: string;
387
+ /** OTLP sink: the OTLP/HTTP endpoint to POST batched, redacted, metadata-only events to. */
388
+ endpoint?: string;
389
+ /** OTLP sink: env-var NAME (never a value) whose value is sent as the `authorization` header. */
390
+ headersEnv?: string;
381
391
  }
382
392
  export interface RouterConfig {
383
393
  providers: ProviderConfig[];
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Flatten and clamp a source-, server- or MODEL-controlled string before it crosses a rendering
3
+ * boundary. ONE implementation behind three named entry points: `promptSafe` (model prompts,
4
+ * src/runtime/runtime.ts), `displaySafe` (the terminal, src/cli/render.ts), and the capability deriver's
5
+ * candidate labels. Internal — deliberately not exported from src/index.ts.
6
+ *
7
+ * The two properties that matter: no control characters or newlines survive (so a value can never forge
8
+ * an extra row in a rendered list), and the length is bounded (so it cannot dominate a prompt or a
9
+ * terminal line).
10
+ */
11
+ export declare function flattenClamp(raw: string, max?: number): string;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Flatten and clamp a source-, server- or MODEL-controlled string before it crosses a rendering
3
+ * boundary. ONE implementation behind three named entry points: `promptSafe` (model prompts,
4
+ * src/runtime/runtime.ts), `displaySafe` (the terminal, src/cli/render.ts), and the capability deriver's
5
+ * candidate labels. Internal — deliberately not exported from src/index.ts.
6
+ *
7
+ * The two properties that matter: no control characters or newlines survive (so a value can never forge
8
+ * an extra row in a rendered list), and the length is bounded (so it cannot dominate a prompt or a
9
+ * terminal line).
10
+ */
11
+ export function flattenClamp(raw, max = 80) {
12
+ const flat = String(raw ?? '')
13
+ // eslint-disable-next-line no-control-regex
14
+ .replace(/[\u0000-\u001f\u007f]+/g, ' ')
15
+ .replace(/\s+/g, ' ')
16
+ .trim();
17
+ return flat.length > max ? `${flat.slice(0, max)}…` : flat;
18
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * A tiny keyed async semaphore (Phase 19). Each key has an independent concurrency limit; `run(key, fn)`
3
+ * waits until a slot for that key is free, runs `fn`, and releases the slot even if `fn` throws. Waiters are
4
+ * served FIFO. A key with no configured limit (or a limit ≤ 0) is UNLIMITED — `run` calls `fn` immediately,
5
+ * so an empty limits map is a zero-overhead pass-through (the "no concurrency config" default).
6
+ */
7
+ export declare class KeyedSemaphore {
8
+ private readonly limits;
9
+ private readonly inFlight;
10
+ private readonly waiters;
11
+ constructor(limits?: Record<string, number>);
12
+ /** Whether this semaphore constrains anything at all (empty/limitless → no-op fast path). */
13
+ get active(): boolean;
14
+ private limitFor;
15
+ acquire(key: string): Promise<void>;
16
+ release(key: string): void;
17
+ /** Acquire a slot for `key`, run `fn`, and always release — even on throw. */
18
+ run<T>(key: string, fn: () => Promise<T>): Promise<T>;
19
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * A tiny keyed async semaphore (Phase 19). Each key has an independent concurrency limit; `run(key, fn)`
3
+ * waits until a slot for that key is free, runs `fn`, and releases the slot even if `fn` throws. Waiters are
4
+ * served FIFO. A key with no configured limit (or a limit ≤ 0) is UNLIMITED — `run` calls `fn` immediately,
5
+ * so an empty limits map is a zero-overhead pass-through (the "no concurrency config" default).
6
+ */
7
+ export class KeyedSemaphore {
8
+ limits;
9
+ inFlight = new Map();
10
+ waiters = new Map();
11
+ constructor(limits = {}) {
12
+ this.limits = limits;
13
+ }
14
+ /** Whether this semaphore constrains anything at all (empty/limitless → no-op fast path). */
15
+ get active() {
16
+ return Object.values(this.limits).some((n) => n > 0);
17
+ }
18
+ limitFor(key) {
19
+ const n = this.limits[key];
20
+ return n !== undefined && n > 0 ? n : undefined;
21
+ }
22
+ async acquire(key) {
23
+ const limit = this.limitFor(key);
24
+ if (limit === undefined)
25
+ return; // unlimited
26
+ const cur = this.inFlight.get(key) ?? 0;
27
+ if (cur < limit) {
28
+ this.inFlight.set(key, cur + 1);
29
+ return;
30
+ }
31
+ await new Promise((resolve) => {
32
+ const q = this.waiters.get(key) ?? [];
33
+ q.push(resolve);
34
+ this.waiters.set(key, q);
35
+ });
36
+ // Resumed by release(), which handed its slot directly to us — the in-flight count is unchanged.
37
+ }
38
+ release(key) {
39
+ const limit = this.limitFor(key);
40
+ if (limit === undefined)
41
+ return; // unlimited — nothing was counted
42
+ const q = this.waiters.get(key);
43
+ if (q && q.length) {
44
+ const next = q.shift();
45
+ next(); // hand the slot to the next waiter; count stays at the limit
46
+ return;
47
+ }
48
+ this.inFlight.set(key, Math.max(0, (this.inFlight.get(key) ?? 1) - 1));
49
+ }
50
+ /** Acquire a slot for `key`, run `fn`, and always release — even on throw. */
51
+ async run(key, fn) {
52
+ await this.acquire(key);
53
+ try {
54
+ return await fn();
55
+ }
56
+ finally {
57
+ this.release(key);
58
+ }
59
+ }
60
+ }
package/package.json CHANGED
@@ -1,13 +1,28 @@
1
1
  {
2
2
  "name": "ai-runtime-engine",
3
- "version": "1.2.0",
4
- "description": "AI Runtime a provider-agnostic AI runtime and orchestration platform. Point it at whatever AI providers you have; it routes each task to the best available model. Ships the `ai-runtime` CLI and the `Runtime`/`AI` library API.",
3
+ "version": "2.7.0",
4
+ "description": "AI Runtime \u2014 a provider-agnostic AI runtime and orchestration platform. Point it at whatever AI providers you have; it routes each task to the best available model. Ships the `ai-runtime` CLI and the `Runtime`/`AI` library API.",
5
5
  "type": "module",
6
6
  "license": "ISC",
7
7
  "author": "pavan",
8
8
  "keywords": [
9
- "ai", "llm", "ai-runtime", "runtime", "orchestration", "router", "provider-agnostic", "agent",
10
- "openai", "gemini", "groq", "anthropic", "ollama", "openrouter", "mcp", "cli", "typescript"
9
+ "ai",
10
+ "llm",
11
+ "ai-runtime",
12
+ "runtime",
13
+ "orchestration",
14
+ "router",
15
+ "provider-agnostic",
16
+ "agent",
17
+ "openai",
18
+ "gemini",
19
+ "groq",
20
+ "anthropic",
21
+ "ollama",
22
+ "openrouter",
23
+ "mcp",
24
+ "cli",
25
+ "typescript"
11
26
  ],
12
27
  "engines": {
13
28
  "node": ">=22"
@@ -43,11 +58,11 @@
43
58
  "scripts": {
44
59
  "build": "tsc -p tsconfig.build.json",
45
60
  "typecheck": "tsc --noEmit",
46
- "test": "node --import tsx --test \"tests/**/*.test.ts\"",
47
- "test:unit": "node --import tsx --test \"tests/unit/**/*.test.ts\"",
48
- "test:integration": "node --import tsx --test \"tests/integration/**/*.test.ts\"",
49
- "test:contract": "node --import tsx --test \"tests/contract/**/*.test.ts\"",
50
- "test:chaos": "node --import tsx --test \"tests/chaos/**/*.test.ts\"",
61
+ "test": "node --import tsx --test --test-timeout=120000 \"tests/**/*.test.ts\"",
62
+ "test:unit": "node --import tsx --test --test-timeout=120000 \"tests/unit/**/*.test.ts\"",
63
+ "test:integration": "node --import tsx --test --test-timeout=120000 \"tests/integration/**/*.test.ts\"",
64
+ "test:contract": "node --import tsx --test --test-timeout=120000 \"tests/contract/**/*.test.ts\"",
65
+ "test:chaos": "node --import tsx --test --test-timeout=120000 \"tests/chaos/**/*.test.ts\"",
51
66
  "cli": "node --import tsx src/cli/cli.ts",
52
67
  "example:fallback": "node --import tsx examples/fallback.ts",
53
68
  "example:minimal": "node --import tsx examples/minimal.ts",