@capybara-hq/kernel 0.1.0-rc.19 → 0.1.0-rc.20

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 (141) hide show
  1. package/bootstrap.json +25 -25
  2. package/dist/admin/admin-agents.controller.d.ts +28 -0
  3. package/dist/admin/admin-agents.controller.js +79 -0
  4. package/dist/admin/admin-connectors.controller.d.ts +29 -0
  5. package/dist/admin/admin-connectors.controller.js +79 -0
  6. package/dist/admin/admin-items.schemas.d.ts +42 -0
  7. package/dist/admin/admin-items.schemas.js +48 -0
  8. package/dist/admin/admin.controller.d.ts +11 -20
  9. package/dist/admin/admin.controller.js +28 -34
  10. package/dist/admin/admin.module.d.ts +4 -1
  11. package/dist/admin/admin.module.js +50 -4
  12. package/dist/admin/admin.schemas.d.ts +4 -0
  13. package/dist/admin/admin.schemas.js +4 -1
  14. package/dist/admin/audit.controller.d.ts +14 -0
  15. package/dist/admin/audit.controller.js +42 -0
  16. package/dist/admin/audit.schemas.d.ts +8 -0
  17. package/dist/admin/audit.schemas.js +17 -0
  18. package/dist/admin/health.service.d.ts +104 -0
  19. package/dist/admin/health.service.js +246 -0
  20. package/dist/admin/overview.controller.d.ts +31 -0
  21. package/dist/admin/overview.controller.js +78 -0
  22. package/dist/admin/overview.schemas.d.ts +8 -0
  23. package/dist/admin/overview.schemas.js +13 -0
  24. package/dist/admin/overview.service.d.ts +112 -0
  25. package/dist/admin/overview.service.js +223 -0
  26. package/dist/admin/platform-audit.service.d.ts +41 -0
  27. package/dist/admin/platform-audit.service.js +70 -0
  28. package/dist/admin/usage.service.d.ts +149 -0
  29. package/dist/admin/usage.service.js +321 -0
  30. package/dist/agents/admin-agents.d.ts +92 -0
  31. package/dist/agents/admin-agents.js +170 -0
  32. package/dist/agents/agents.module.d.ts +4 -3
  33. package/dist/agents/agents.module.js +7 -3
  34. package/dist/agents/agents.service.d.ts +41 -3
  35. package/dist/agents/agents.service.js +113 -27
  36. package/dist/agents/definition.d.ts +16 -0
  37. package/dist/agents/definition.js +32 -2
  38. package/dist/agents/org-templates.js +4 -0
  39. package/dist/background/launch.d.ts +1 -1
  40. package/dist/background/launch.js +3 -3
  41. package/dist/background/tasks.service.d.ts +35 -3
  42. package/dist/background/tasks.service.js +91 -36
  43. package/dist/catalog/catalog.module.js +8 -2
  44. package/dist/channels/channels.module.js +11 -2
  45. package/dist/channels/supervisor.d.ts +2 -0
  46. package/dist/channels/supervisor.js +4 -0
  47. package/dist/conversations/events.js +1 -1
  48. package/dist/conversations/titles.js +4 -8
  49. package/dist/db/kernel-schema.d.ts +8 -1
  50. package/dist/devices/devices.module.js +5 -2
  51. package/dist/errors.d.ts +22 -2
  52. package/dist/errors.js +22 -2
  53. package/dist/evals/evals.module.js +11 -2
  54. package/dist/extensions/permission-view.js +3 -3
  55. package/dist/http/api.module.js +10 -1
  56. package/dist/identity/settings.js +3 -2
  57. package/dist/identity/write-actor.d.ts +11 -0
  58. package/dist/identity/write-actor.js +7 -0
  59. package/dist/inbox/inbox.service.d.ts +2 -1
  60. package/dist/inbox/platform-inbox.service.d.ts +7 -1
  61. package/dist/inbox/platform-inbox.service.js +20 -2
  62. package/dist/infra/presence/presence.module.d.ts +6 -0
  63. package/dist/infra/presence/presence.module.js +22 -0
  64. package/dist/infra/presence/replica-presence.d.ts +87 -0
  65. package/dist/infra/presence/replica-presence.js +189 -0
  66. package/dist/kernel.module.js +3 -0
  67. package/dist/lifecycle/kernel-lifecycle.d.ts +3 -1
  68. package/dist/lifecycle/kernel-lifecycle.js +17 -3
  69. package/dist/local-devices/local-device-gateway.d.ts +6 -0
  70. package/dist/local-devices/local-device-gateway.js +12 -0
  71. package/dist/overrides.d.ts +7 -1
  72. package/dist/overrides.js +4 -0
  73. package/dist/platform/exclusions.js +15 -0
  74. package/dist/platform/operation.d.ts +1 -1
  75. package/dist/platform/operation.js +1 -1
  76. package/dist/platform/ops/admin-items.ops.d.ts +37 -0
  77. package/dist/platform/ops/admin-items.ops.js +278 -0
  78. package/dist/platform/ops/admin.ops.d.ts +10 -10
  79. package/dist/platform/ops/admin.ops.js +111 -42
  80. package/dist/platform/ops/agent-skills.ops.d.ts +29 -3
  81. package/dist/platform/ops/agent-skills.ops.js +290 -7
  82. package/dist/platform/ops/inbox.ops.js +7 -4
  83. package/dist/platform/ops/platform.ops.js +1 -1
  84. package/dist/platform/ops/skills.ops.js +1 -57
  85. package/dist/platform/platform-ops.module.d.ts +2 -1
  86. package/dist/platform/platform-ops.module.js +5 -0
  87. package/dist/platform/route-schemas.js +8 -0
  88. package/dist/roles/gateway-role.d.ts +4 -0
  89. package/dist/roles/gateway-role.js +8 -0
  90. package/dist/runtime/run-jobs.js +4 -2
  91. package/dist/sandbox/sandbox-file-reader.d.ts +35 -0
  92. package/dist/sandbox/sandbox-file-reader.js +98 -0
  93. package/dist/skills/agent-skills.controller.d.ts +48 -0
  94. package/dist/skills/agent-skills.controller.js +131 -0
  95. package/dist/skills/agent-skills.service.d.ts +262 -0
  96. package/dist/skills/agent-skills.service.js +996 -0
  97. package/dist/skills/catalog.d.ts +8 -6
  98. package/dist/skills/catalog.js +8 -4
  99. package/dist/skills/changes.controller.d.ts +5 -22
  100. package/dist/skills/changes.controller.js +7 -55
  101. package/dist/skills/changes.service.d.ts +69 -115
  102. package/dist/skills/changes.service.js +249 -599
  103. package/dist/skills/conversation-catalog.d.ts +53 -0
  104. package/dist/skills/conversation-catalog.js +149 -0
  105. package/dist/skills/invocation-keys.d.ts +1 -1
  106. package/dist/skills/invocation-keys.js +1 -1
  107. package/dist/skills/package.d.ts +0 -5
  108. package/dist/skills/package.js +0 -14
  109. package/dist/skills/skills.controller.js +1 -10
  110. package/dist/skills/skills.module.js +15 -2
  111. package/dist/skills/skills.schemas.d.ts +11 -15
  112. package/dist/skills/skills.schemas.js +27 -20
  113. package/dist/skills/skills.service.d.ts +37 -30
  114. package/dist/skills/skills.service.js +107 -59
  115. package/dist/skills/usable.d.ts +4 -3
  116. package/dist/skills/usable.js +13 -4
  117. package/dist/tools/admin-connectors.d.ts +69 -0
  118. package/dist/tools/admin-connectors.js +135 -0
  119. package/dist/tools/builtin-names.d.ts +9 -14
  120. package/dist/tools/builtin-names.js +13 -22
  121. package/dist/tools/builtins/builtin-tools.d.ts +2 -3
  122. package/dist/tools/builtins/builtin-tools.js +21 -26
  123. package/dist/tools/builtins/sandbox/bash.js +2 -2
  124. package/dist/tools/builtins/skill-load.js +1 -0
  125. package/dist/tools/connectors.controller.d.ts +5 -1
  126. package/dist/tools/connectors.controller.js +17 -3
  127. package/dist/tools/connectors.service.d.ts +20 -1
  128. package/dist/tools/connectors.service.js +61 -0
  129. package/dist/tools/legacy-keys.js +0 -1
  130. package/dist/tools/org-tool-switches.d.ts +3 -2
  131. package/dist/tools/org-tool-switches.js +4 -4
  132. package/dist/tools/policy.d.ts +7 -9
  133. package/dist/tools/policy.js +11 -22
  134. package/dist/tools/tools.module.js +3 -0
  135. package/package.json +1 -1
  136. package/src/migrations/0109_audit_platform_idx.sql +5 -0
  137. package/src/migrations/0110_usage_time_idx.sql +8 -0
  138. package/src/migrations/0111_agent_skill_sharing.sql +22 -0
  139. package/src/migrations/0112_skill_share_requests.sql +4 -0
  140. package/dist/tools/builtins/skill-write.d.ts +0 -59
  141. package/dist/tools/builtins/skill-write.js +0 -244
@@ -0,0 +1,22 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Global, Module } from "@nestjs/common";
8
+ import { ReplicaPresence } from "./replica-presence.js";
9
+ /**
10
+ * Global: {@link ReplicaPresence}, on every role (each process reports itself; `KernelLifecycle`
11
+ * starts and stops it), read by the admin health overview.
12
+ */
13
+ let PresenceModule = class PresenceModule {
14
+ };
15
+ PresenceModule = __decorate([
16
+ Global(),
17
+ Module({
18
+ providers: [ReplicaPresence],
19
+ exports: [ReplicaPresence],
20
+ })
21
+ ], PresenceModule);
22
+ export { PresenceModule };
@@ -0,0 +1,87 @@
1
+ import type { Redis } from "ioredis";
2
+ import type { KernelLogger } from "../../log.js";
3
+ import { KernelConfigService } from "../config/kernel-config.service.js";
4
+ /** How often each process writes its presence (admin overview spec §5.1). */
5
+ export declare const PRESENCE_DEFAULT_INTERVAL_MS = 15000;
6
+ /** Optional provider: the presence interval (ms) instead of {@link PRESENCE_DEFAULT_INTERVAL_MS} (tests). */
7
+ export declare const PRESENCE_INTERVAL_MS: unique symbol;
8
+ /** The ZSET of live replicas: member = replica id, score = last beat (Redis server clock, ms). */
9
+ export declare const REPLICAS_KEY = "capk:replicas";
10
+ /** One replica's facts: `capk:replica:<replicaId>`. */
11
+ export declare const replicaKey: (replicaId: string) => string;
12
+ /** A kernel role (`roles` in the config). */
13
+ export type ReplicaRole = "api" | "worker" | "gateway";
14
+ /** What a live replica last reported about itself. */
15
+ export interface LiveReplica {
16
+ /** The process id (`ReplicaStatusStore.replicaId`, the gateway heartbeat's member). */
17
+ id: string;
18
+ roles: ReplicaRole[];
19
+ /** `KERNEL_VERSION` of the process. */
20
+ version: string;
21
+ /** When the process's start sequence was done, before it listens (ISO 8601). */
22
+ startedAt: string;
23
+ /** The configured `http.replicaId` (the `x-capybara-replica` header), null when unset. */
24
+ httpReplicaId: string | null;
25
+ /** Channel long connections this process holds (0 without the gateway role). */
26
+ channelsHeld: number;
27
+ /** Device sockets open on this process (0 without the gateway role). */
28
+ devicesConnected: number;
29
+ }
30
+ /** The per-process facts the presence reports, read at every beat. */
31
+ export interface PresenceSources {
32
+ /** This process's replica id (`ReplicaStatusStore.replicaId`). */
33
+ replicaId: string;
34
+ channelsHeld(): number;
35
+ devicesConnected(): number;
36
+ }
37
+ /**
38
+ * The replicas with a beat newer than `windowMs` (Redis server clock): one script for the members,
39
+ * then one pipelined HGETALL each. A member whose facts already expired is left out. Sorted by
40
+ * start time, then id.
41
+ */
42
+ export declare function readLiveReplicas(redis: Pick<Redis, "eval" | "pipeline">, windowMs: number): Promise<LiveReplica[]>;
43
+ /**
44
+ * Replica presence (admin overview spec §5.1): every process, whatever its roles, says every
45
+ * {@link PRESENCE_DEFAULT_INTERVAL_MS} that it is alive, with its roles, version, start time and
46
+ * the connections it holds, so the admin console can tell which replicas run in which role.
47
+ *
48
+ * Multi-replica: the state lives only in Redis, with a TTL of three intervals on both keys; the
49
+ * score is the Redis server clock (replica clocks may drift), as in the gateway heartbeat. Each
50
+ * process writes only its own entry and holds nothing other processes need: no lock, no
51
+ * transaction, no shared job. So it is not a `@LockedInterval` (that runs once across replicas)
52
+ * but a `setTimeout` chain of this process (`unref`), started by `KernelLifecycle` at the end of
53
+ * the start (after the start gate opened) and stopped first thing in the stop, which removes the
54
+ * entry at once; a crashed process ages out of {@link live} within the window. A failing beat is
55
+ * logged at warn and never thrown.
56
+ */
57
+ export declare class ReplicaPresence {
58
+ private readonly redis;
59
+ private readonly config;
60
+ private readonly log;
61
+ /** The beat interval; the window (and the TTL) is three of them. */
62
+ readonly intervalMs: number;
63
+ private sources;
64
+ private startedAt;
65
+ private timer;
66
+ private running;
67
+ private halted;
68
+ constructor(redis: Redis, config: Pick<KernelConfigService, "get">, log: Pick<KernelLogger, "warn">, intervalMs?: number);
69
+ /** How long an entry stays live without a beat (ms). */
70
+ get windowMs(): number;
71
+ /** Starts beating (the first beat at once). Called once, at the end of the start. */
72
+ start(sources: PresenceSources): void;
73
+ private schedule;
74
+ private beat;
75
+ /**
76
+ * Stops beating and waits for a beat in flight, leaving the entry to age out (a crash looks like
77
+ * this; the tests use it as one). Idempotent.
78
+ */
79
+ halt(): Promise<void>;
80
+ /** The stop: {@link halt}, then removes this process's entry at once (best effort). Idempotent. */
81
+ stop(): Promise<void>;
82
+ /**
83
+ * The live replicas (a beat within the window, by the Redis server clock), sorted by start time.
84
+ * Throws when Redis does not answer.
85
+ */
86
+ live(): Promise<LiveReplica[]>;
87
+ }
@@ -0,0 +1,189 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { Inject, Injectable, Optional } from "@nestjs/common";
11
+ import { KERNEL_VERSION } from "../../version.js";
12
+ import { KernelConfigService } from "../config/kernel-config.service.js";
13
+ import { KERNEL_LOGGER, REDIS } from "../tokens.js";
14
+ /** How often each process writes its presence (admin overview spec §5.1). */
15
+ export const PRESENCE_DEFAULT_INTERVAL_MS = 15_000;
16
+ /** Optional provider: the presence interval (ms) instead of {@link PRESENCE_DEFAULT_INTERVAL_MS} (tests). */
17
+ export const PRESENCE_INTERVAL_MS = Symbol("PRESENCE_INTERVAL_MS");
18
+ /** The ZSET of live replicas: member = replica id, score = last beat (Redis server clock, ms). */
19
+ export const REPLICAS_KEY = "capk:replicas";
20
+ /** One replica's facts: `capk:replica:<replicaId>`. */
21
+ export const replicaKey = (replicaId) => `capk:replica:${replicaId}`;
22
+ /**
23
+ * One beat: ZADD the member at the server clock, write the facts, give both keys a TTL of the
24
+ * window and trim members older than it. KEYS: the ZSET, the replica's hash. ARGV: the replica id,
25
+ * the window (ms), then the hash's field / value pairs.
26
+ */
27
+ const BEAT = `
28
+ local t = redis.call('TIME')
29
+ local now = tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)
30
+ local ttl = tonumber(ARGV[2])
31
+ redis.call('ZADD', KEYS[1], now, ARGV[1])
32
+ redis.call('HSET', KEYS[2], unpack(ARGV, 3))
33
+ redis.call('PEXPIRE', KEYS[2], ttl)
34
+ redis.call('PEXPIRE', KEYS[1], ttl)
35
+ redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', now - ttl)
36
+ return now
37
+ `;
38
+ /** The members newer than the window (ARGV[1], ms) by the Redis server clock. */
39
+ const LIVE = `
40
+ local t = redis.call('TIME')
41
+ local now = tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)
42
+ return redis.call('ZRANGEBYSCORE', KEYS[1], '(' .. (now - tonumber(ARGV[1])), '+inf')
43
+ `;
44
+ const ROLES = ["api", "worker", "gateway"];
45
+ const count = (v) => {
46
+ const n = Number(v);
47
+ return Number.isInteger(n) && n >= 0 ? n : 0;
48
+ };
49
+ /**
50
+ * The replicas with a beat newer than `windowMs` (Redis server clock): one script for the members,
51
+ * then one pipelined HGETALL each. A member whose facts already expired is left out. Sorted by
52
+ * start time, then id.
53
+ */
54
+ export async function readLiveReplicas(redis, windowMs) {
55
+ const ids = (await redis.eval(LIVE, 1, REPLICAS_KEY, windowMs));
56
+ if (ids.length === 0)
57
+ return [];
58
+ const pipe = redis.pipeline();
59
+ for (const id of ids)
60
+ pipe.hgetall(replicaKey(id));
61
+ const res = (await pipe.exec()) ?? [];
62
+ const out = [];
63
+ ids.forEach((id, i) => {
64
+ const [err, h] = res[i] ?? [null, null];
65
+ if (err)
66
+ throw err;
67
+ const f = h;
68
+ if (f === null || f.version === undefined)
69
+ return;
70
+ out.push({
71
+ id,
72
+ roles: (f.roles ?? "")
73
+ .split(",")
74
+ .filter((r) => ROLES.includes(r)),
75
+ version: f.version,
76
+ startedAt: f.startedAt ?? "",
77
+ httpReplicaId: f.httpReplicaId ? f.httpReplicaId : null,
78
+ channelsHeld: count(f.channelsHeld),
79
+ devicesConnected: count(f.devicesConnected),
80
+ });
81
+ });
82
+ return out.sort((a, b) => a.startedAt.localeCompare(b.startedAt) || a.id.localeCompare(b.id));
83
+ }
84
+ /**
85
+ * Replica presence (admin overview spec §5.1): every process, whatever its roles, says every
86
+ * {@link PRESENCE_DEFAULT_INTERVAL_MS} that it is alive, with its roles, version, start time and
87
+ * the connections it holds, so the admin console can tell which replicas run in which role.
88
+ *
89
+ * Multi-replica: the state lives only in Redis, with a TTL of three intervals on both keys; the
90
+ * score is the Redis server clock (replica clocks may drift), as in the gateway heartbeat. Each
91
+ * process writes only its own entry and holds nothing other processes need: no lock, no
92
+ * transaction, no shared job. So it is not a `@LockedInterval` (that runs once across replicas)
93
+ * but a `setTimeout` chain of this process (`unref`), started by `KernelLifecycle` at the end of
94
+ * the start (after the start gate opened) and stopped first thing in the stop, which removes the
95
+ * entry at once; a crashed process ages out of {@link live} within the window. A failing beat is
96
+ * logged at warn and never thrown.
97
+ */
98
+ let ReplicaPresence = class ReplicaPresence {
99
+ redis;
100
+ config;
101
+ log;
102
+ /** The beat interval; the window (and the TTL) is three of them. */
103
+ intervalMs;
104
+ sources;
105
+ startedAt = "";
106
+ timer;
107
+ running;
108
+ halted = false;
109
+ constructor(redis, config, log, intervalMs) {
110
+ this.redis = redis;
111
+ this.config = config;
112
+ this.log = log;
113
+ this.intervalMs = intervalMs ?? PRESENCE_DEFAULT_INTERVAL_MS;
114
+ }
115
+ /** How long an entry stays live without a beat (ms). */
116
+ get windowMs() {
117
+ return this.intervalMs * 3;
118
+ }
119
+ /** Starts beating (the first beat at once). Called once, at the end of the start. */
120
+ start(sources) {
121
+ if (this.sources !== undefined || this.halted)
122
+ return;
123
+ this.sources = sources;
124
+ this.startedAt = new Date().toISOString();
125
+ this.schedule(0);
126
+ }
127
+ schedule(ms) {
128
+ if (this.halted)
129
+ return;
130
+ this.timer = setTimeout(() => {
131
+ this.running = this.beat().finally(() => {
132
+ this.running = undefined;
133
+ this.schedule(this.intervalMs);
134
+ });
135
+ }, ms);
136
+ this.timer.unref?.();
137
+ }
138
+ async beat() {
139
+ const s = this.sources;
140
+ if (s === undefined || this.halted)
141
+ return;
142
+ try {
143
+ const http = this.config.get().http.replicaId;
144
+ await this.redis.eval(BEAT, 2, REPLICAS_KEY, replicaKey(s.replicaId), s.replicaId, this.windowMs, "roles", this.config.get().roles.join(","), "version", KERNEL_VERSION, "startedAt", this.startedAt, "httpReplicaId", http === undefined ? "" : String(http), "channelsHeld", s.channelsHeld(), "devicesConnected", s.devicesConnected());
145
+ }
146
+ catch (err) {
147
+ this.log.warn("replica presence beat failed", { err });
148
+ }
149
+ }
150
+ /**
151
+ * Stops beating and waits for a beat in flight, leaving the entry to age out (a crash looks like
152
+ * this; the tests use it as one). Idempotent.
153
+ */
154
+ async halt() {
155
+ this.halted = true;
156
+ clearTimeout(this.timer);
157
+ await this.running;
158
+ }
159
+ /** The stop: {@link halt}, then removes this process's entry at once (best effort). Idempotent. */
160
+ async stop() {
161
+ await this.halt();
162
+ const s = this.sources;
163
+ if (s === undefined)
164
+ return;
165
+ this.sources = undefined;
166
+ try {
167
+ await this.redis.multi().zrem(REPLICAS_KEY, s.replicaId).del(replicaKey(s.replicaId)).exec();
168
+ }
169
+ catch (err) {
170
+ this.log.warn("cannot remove replica presence", { err });
171
+ }
172
+ }
173
+ /**
174
+ * The live replicas (a beat within the window, by the Redis server clock), sorted by start time.
175
+ * Throws when Redis does not answer.
176
+ */
177
+ live() {
178
+ return readLiveReplicas(this.redis, this.windowMs);
179
+ }
180
+ };
181
+ ReplicaPresence = __decorate([
182
+ Injectable(),
183
+ __param(0, Inject(REDIS)),
184
+ __param(1, Inject(KernelConfigService)),
185
+ __param(2, Inject(KERNEL_LOGGER)),
186
+ __param(3, Optional()),
187
+ __param(3, Inject(PRESENCE_INTERVAL_MS))
188
+ ], ReplicaPresence);
189
+ export { ReplicaPresence };
@@ -25,6 +25,7 @@ import { databaseCheck, HEALTH_CHECKS, redisCheck } from "./infra/health/health.
25
25
  import { GracefulShutdownModule } from "./infra/lifecycle/graceful-shutdown.module.js";
26
26
  import { LockModule } from "./infra/lock/lock.module.js";
27
27
  import { LoggingModule } from "./infra/logging/logging.module.js";
28
+ import { PresenceModule } from "./infra/presence/presence.module.js";
28
29
  import { QueueModule } from "./infra/queue/queue.module.js";
29
30
  import { RedisModule } from "./infra/redis/redis.module.js";
30
31
  import { KernelThrottlerModule } from "./infra/throttler/throttler.module.js";
@@ -101,6 +102,8 @@ let KernelModule = KernelModule_1 = class KernelModule {
101
102
  QueueModule.forRoot(),
102
103
  KernelThrottlerModule,
103
104
  HealthModule,
105
+ // every process's presence beat (admin overview spec §5.1), whatever its roles
106
+ PresenceModule,
104
107
  ValidationModule,
105
108
  ErrorFilterModule,
106
109
  KernelAppOptionsModule.forRoot({ ...options, logger }),
@@ -21,6 +21,7 @@ import { AccountsService } from "../identity/accounts.js";
21
21
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
22
22
  import { KernelDb } from "../infra/database/kernel-db.js";
23
23
  import { GracefulShutdown } from "../infra/lifecycle/graceful-shutdown.service.js";
24
+ import { ReplicaPresence } from "../infra/presence/replica-presence.js";
24
25
  import type { KernelLogger } from "../log.js";
25
26
  import { GatewayRole } from "../roles/gateway-role.js";
26
27
  import { WorkerRole } from "../roles/worker-role.js";
@@ -83,6 +84,7 @@ export declare class KernelLifecycle implements OnApplicationBootstrap, BeforeAp
83
84
  private readonly titles;
84
85
  private readonly channelJobs;
85
86
  private readonly channelOutbound;
87
+ private readonly presence;
86
88
  private readonly worker?;
87
89
  private readonly gateway?;
88
90
  /** The startup config (hot fields are read through the config service). */
@@ -97,7 +99,7 @@ export declare class KernelLifecycle implements OnApplicationBootstrap, BeforeAp
97
99
  private unwatchConfig;
98
100
  /** What the first bootstrap pass did, carried into the second one. */
99
101
  private bootstrapCarry;
100
- constructor(accounts: AccountsService, configService: KernelConfigService, log: KernelLogger, options: KernelAppOptions, pool: Kysely<any>, redis: Redis, kdb: KernelDb, shutdown: GracefulShutdown, host: ExtensionHost, registry: ExtensionRegistry, installs: ExtensionInstalls, blobs: ExtensionBlobs, extensionLifecycle: ExtensionLifecycle, replicaStatus: ReplicaStatusStore, bootstrap: BootstrapService, marketplaces: MarketplacesService, pluginImport: PluginImport, bus: Pick<ExtensionContext["bus"], "publish" | "subscribe">, usageReport: UsageReporter, titles: TitleQueue, channelJobs: ChannelJobs, channelOutbound: ChannelOutbound, worker?: WorkerRole | undefined, gateway?: GatewayRole | undefined);
102
+ constructor(accounts: AccountsService, configService: KernelConfigService, log: KernelLogger, options: KernelAppOptions, pool: Kysely<any>, redis: Redis, kdb: KernelDb, shutdown: GracefulShutdown, host: ExtensionHost, registry: ExtensionRegistry, installs: ExtensionInstalls, blobs: ExtensionBlobs, extensionLifecycle: ExtensionLifecycle, replicaStatus: ReplicaStatusStore, bootstrap: BootstrapService, marketplaces: MarketplacesService, pluginImport: PluginImport, bus: Pick<ExtensionContext["bus"], "publish" | "subscribe">, usageReport: UsageReporter, titles: TitleQueue, channelJobs: ChannelJobs, channelOutbound: ChannelOutbound, presence: ReplicaPresence, worker?: WorkerRole | undefined, gateway?: GatewayRole | undefined);
101
103
  /** The kernel HTTP application (set up by `buildKernelApp` before `init()`). */
102
104
  get http(): KernelHttpApp;
103
105
  attachHttp(http: KernelHttpApp): void;
@@ -31,6 +31,7 @@ import { waitWithin } from "../infra/close-within.js";
31
31
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
32
32
  import { KernelDb } from "../infra/database/kernel-db.js";
33
33
  import { GracefulShutdown } from "../infra/lifecycle/graceful-shutdown.service.js";
34
+ import { ReplicaPresence } from "../infra/presence/replica-presence.js";
34
35
  import { KERNEL_BUS, KERNEL_LOGGER, KERNEL_POOL, REDIS } from "../infra/tokens.js";
35
36
  import { KERNEL_MIGRATIONS_DIR, KERNEL_SCHEMA } from "../kernel.js";
36
37
  import { GatewayRole } from "../roles/gateway-role.js";
@@ -104,6 +105,7 @@ let KernelLifecycle = class KernelLifecycle {
104
105
  titles;
105
106
  channelJobs;
106
107
  channelOutbound;
108
+ presence;
107
109
  worker;
108
110
  gateway;
109
111
  /** The startup config (hot fields are read through the config service). */
@@ -123,6 +125,8 @@ let KernelLifecycle = class KernelLifecycle {
123
125
  pool, redis, kdb, shutdown, host, registry, installs, blobs, extensionLifecycle, replicaStatus, bootstrap, marketplaces, pluginImport, bus,
124
126
  // the producers closed on every role (their consumers are the worker role's)
125
127
  usageReport, titles, channelJobs, channelOutbound,
128
+ // this process's presence beat, on every role (admin overview spec §5.1)
129
+ presence,
126
130
  // The role modules' parts: a role module is imported only when `roles` names it, so each is
127
131
  // present exactly when its role is (checked at the start)
128
132
  worker, gateway) {
@@ -148,6 +152,7 @@ let KernelLifecycle = class KernelLifecycle {
148
152
  this.titles = titles;
149
153
  this.channelJobs = channelJobs;
150
154
  this.channelOutbound = channelOutbound;
155
+ this.presence = presence;
151
156
  this.worker = worker;
152
157
  this.gateway = gateway;
153
158
  this.config = configService.get();
@@ -267,6 +272,12 @@ let KernelLifecycle = class KernelLifecycle {
267
272
  // 热更新:重新校验;只应用 hot 字段,其余变化 warn 并列出需重启的键;校验失败保持原配置
268
273
  this.unwatchConfig = opts.source.watch((raw) => this.configService.apply(raw), (err) => this.log.error("config watch error", { error: err.message }));
269
274
  }
275
+ // Last: the process says it is alive (in its roles) only once its start went through
276
+ this.presence.start({
277
+ replicaId: this.replicaStatus.replicaId,
278
+ channelsHeld: () => gateway?.channelsHeld() ?? 0,
279
+ devicesConnected: () => gateway?.devicesConnected() ?? 0,
280
+ });
270
281
  }
271
282
  /**
272
283
  * Listens on `port` (0.0.0.0; every role listens: worker-only serves only `/healthz` and
@@ -379,6 +390,8 @@ let KernelLifecycle = class KernelLifecycle {
379
390
  // Extension requests in progress (SSE and other long requests end on req.signal) are aborted
380
391
  // first, otherwise they would use up the http phase
381
392
  this.httpApp?.abortPluginRequests();
393
+ // Leave the replica presence first: the admin console stops counting this process at once
394
+ await within("presence", this.presence.stop(), 1_000);
382
395
  // Scheduled jobs (worker role): no new tick, the runs in progress get the stop signal; awaited
383
396
  // in the runs phase (before the extension host closes: a job may call an extension)
384
397
  const { worker, gateway } = this;
@@ -457,9 +470,10 @@ KernelLifecycle = __decorate([
457
470
  __param(19, Inject(TitleQueue)),
458
471
  __param(20, Inject(ChannelJobs)),
459
472
  __param(21, Inject(ChannelOutbound)),
460
- __param(22, Optional()),
461
- __param(22, Inject(WorkerRole)),
473
+ __param(22, Inject(ReplicaPresence)),
462
474
  __param(23, Optional()),
463
- __param(23, Inject(GatewayRole))
475
+ __param(23, Inject(WorkerRole)),
476
+ __param(24, Optional()),
477
+ __param(24, Inject(GatewayRole))
464
478
  ], KernelLifecycle);
465
479
  export { KernelLifecycle };
@@ -120,6 +120,12 @@ export declare class LocalDeviceGateway {
120
120
  authenticate(credential: string): Promise<MachineIdentity | null>;
121
121
  /** Serves one upgraded socket of an authenticated machine until it closes. */
122
122
  accept(socket: WebSocket, identity: MachineIdentity, credential: string): void;
123
+ /**
124
+ * How many devices this process holds the connection of (replica presence reports it): only
125
+ * sockets past the hello that hold their device's lease, so a socket still claiming or being
126
+ * replaced by a newer one does not count a device twice.
127
+ */
128
+ connectionCount(): number;
123
129
  /** @internal a connection that ended. */
124
130
  forget(conn: DeviceConnection): void;
125
131
  /** Closes every connection (1001) and hands their leases back. */
@@ -508,6 +508,18 @@ let LocalDeviceGateway = class LocalDeviceGateway {
508
508
  this.connections.add(conn);
509
509
  conn.start();
510
510
  }
511
+ /**
512
+ * How many devices this process holds the connection of (replica presence reports it): only
513
+ * sockets past the hello that hold their device's lease, so a socket still claiming or being
514
+ * replaced by a newer one does not count a device twice.
515
+ */
516
+ connectionCount() {
517
+ let n = 0;
518
+ for (const c of this.connections)
519
+ if (c.state === "holding")
520
+ n++;
521
+ return n;
522
+ }
511
523
  /** @internal a connection that ended. */
512
524
  forget(conn) {
513
525
  this.connections.delete(conn);
@@ -1,10 +1,12 @@
1
1
  import type { NestInterceptor } from "@nestjs/common";
2
2
  import { APP_INTERCEPTOR } from "@nestjs/core";
3
+ import { ADMIN_USAGE_TIMINGS, type AdminUsageTimings } from "./admin/usage.service.js";
3
4
  import { CHANNEL_WEBHOOK_TIMINGS, type ChannelWebhookTimings } from "./channels/webhook.js";
4
5
  import type { EvalReplayHooks } from "./evals/run-replay.js";
5
6
  import { EVAL_REPLAY_HOOKS } from "./evals/worker.js";
6
7
  import { BOOTSTRAP_LOCK_TTL } from "./extensions/bootstrap.js";
7
8
  import { FILE_PREVIEW_TIMEOUT } from "./files/previewers.js";
9
+ import { PRESENCE_INTERVAL_MS } from "./infra/presence/replica-presence.js";
8
10
  import { LIST_MODELS_TIMEOUT } from "./runtime/providers.js";
9
11
  import { RUN_CONTEXT_CLOCK } from "./runtime/run-context.service.js";
10
12
  import { AGENT_LOOP_FACTORY, type AgentLoopFactory } from "./runtime/run-steps.js";
@@ -25,7 +27,9 @@ import { EMBED_TOKEN_CLOCK } from "./ui/embed-token.js";
25
27
  * - `TOOL_PIPELINE_HOOKS`: the tool pipeline's hooks and policy decision;
26
28
  * - `SUBAGENT_MESSAGES_HOOKS`: the sub-agent message tools' hooks (a send raced by a run's end);
27
29
  * - `BOOTSTRAP_LOCK_TTL`, `FILE_PREVIEW_TIMEOUT`, `LIST_MODELS_TIMEOUT`, `USAGE_REPORT_RETRY`,
28
- * `SANDBOX_LEASE_TIMINGS`, `CHANNEL_WEBHOOK_TIMINGS`, `SETUP_CHECK_TIMEOUT`: shorter timings;
30
+ * `SANDBOX_LEASE_TIMINGS`, `CHANNEL_WEBHOOK_TIMINGS`, `SETUP_CHECK_TIMEOUT`,
31
+ * `PRESENCE_INTERVAL_MS`: shorter timings;
32
+ * - `ADMIN_USAGE_TIMINGS`: the usage aggregate's statement timeout and clock;
29
33
  * - `EMBED_TOKEN_CLOCK`: the clock embed tokens are issued and checked with;
30
34
  * - `RUN_CONTEXT_CLOCK`: the clock run start reads the run context's date from, and the
31
35
  * `CurrentTime` builtin the time;
@@ -44,6 +48,8 @@ interface KernelSeams {
44
48
  [SANDBOX_LEASE_TIMINGS]: Partial<SandboxLeaseTimings>;
45
49
  [CHANNEL_WEBHOOK_TIMINGS]: Partial<ChannelWebhookTimings>;
46
50
  [SETUP_CHECK_TIMEOUT]: number;
51
+ [PRESENCE_INTERVAL_MS]: number;
52
+ [ADMIN_USAGE_TIMINGS]: Partial<AdminUsageTimings>;
47
53
  [EMBED_TOKEN_CLOCK]: () => number;
48
54
  [RUN_CONTEXT_CLOCK]: () => number;
49
55
  [APP_INTERCEPTOR]: NestInterceptor;
package/dist/overrides.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { APP_INTERCEPTOR } from "@nestjs/core";
2
+ import { ADMIN_USAGE_TIMINGS } from "./admin/usage.service.js";
2
3
  import { CHANNEL_WEBHOOK_TIMINGS } from "./channels/webhook.js";
3
4
  import { EVAL_REPLAY_HOOKS } from "./evals/worker.js";
4
5
  import { BOOTSTRAP_LOCK_TTL } from "./extensions/bootstrap.js";
5
6
  import { FILE_PREVIEW_TIMEOUT } from "./files/previewers.js";
7
+ import { PRESENCE_INTERVAL_MS } from "./infra/presence/replica-presence.js";
6
8
  import { LIST_MODELS_TIMEOUT } from "./runtime/providers.js";
7
9
  import { RUN_CONTEXT_CLOCK } from "./runtime/run-context.service.js";
8
10
  import { AGENT_LOOP_FACTORY } from "./runtime/run-steps.js";
@@ -26,6 +28,8 @@ const SEAM_TOKENS = [
26
28
  SANDBOX_LEASE_TIMINGS,
27
29
  CHANNEL_WEBHOOK_TIMINGS,
28
30
  SETUP_CHECK_TIMEOUT,
31
+ PRESENCE_INTERVAL_MS,
32
+ ADMIN_USAGE_TIMINGS,
29
33
  EMBED_TOKEN_CLOCK,
30
34
  RUN_CONTEXT_CLOCK,
31
35
  APP_INTERCEPTOR,
@@ -73,6 +73,11 @@ export const PLATFORM_EXCLUSIONS = [
73
73
  category: "human-only",
74
74
  reason: "Transfers ownership of an agent to another person",
75
75
  },
76
+ {
77
+ route: "POST /api/v1/admin/agents/:agentId/owner",
78
+ category: "human-only",
79
+ reason: "Transfers ownership of an org's agent to another person on the platform's behalf, as the org's own transfer route",
80
+ },
76
81
  // background
77
82
  {
78
83
  route: "GET /api/v1/orgs/:orgId/conversations/:id/background-tasks/:taskId/output",
@@ -541,6 +546,16 @@ export const PLATFORM_EXCLUSIONS = [
541
546
  category: "transport",
542
547
  reason: "Uploads a skill archive as multipart bytes",
543
548
  },
549
+ {
550
+ route: "POST /api/v1/orgs/:orgId/skills",
551
+ category: "transport",
552
+ reason: "Creates an org skill from an uploaded archive; uploads are multipart bytes",
553
+ },
554
+ {
555
+ route: "POST /api/v1/orgs/:orgId/agents/:agentId/skills/uploads",
556
+ category: "transport",
557
+ reason: "Uploads a skill archive as multipart bytes",
558
+ },
544
559
  // approvals
545
560
  {
546
561
  route: "POST /api/v1/orgs/:orgId/approvals/:id/decision",
@@ -91,7 +91,7 @@ export interface PlatformOperation<I = unknown> {
91
91
  mirrors: readonly string[];
92
92
  /**
93
93
  * Fields of the mirrored route (body, query, path params but `:orgId`) the input leaves out, e.g.
94
- * `uploadId` of `skills.create` (spec §14.5 item 3; the parity test in `tools/checks` holds it).
94
+ * an `uploadId` (spec §14.5 item 3; the parity test in `tools/checks` holds it).
95
95
  */
96
96
  omits?: readonly string[];
97
97
  /** Input fields the mirrored route lacks, e.g. `cursor` of a paged list. */
@@ -57,7 +57,7 @@ export const PLATFORM_DOMAINS = [
57
57
  },
58
58
  {
59
59
  id: "admin",
60
- description: "The admin console (super admin): dashboard totals and deployment settings.",
60
+ description: "The admin console (super admin): dashboard totals, deployment settings, the org's members, and the agents and connectors of the org (read, delete, disable or enable; other orgs' only in the console).",
61
61
  },
62
62
  {
63
63
  id: "models",
@@ -0,0 +1,37 @@
1
+ import { type OnModuleInit } from "@nestjs/common";
2
+ import { AdminAgents } from "../../agents/admin-agents.js";
3
+ import { AgentsService } from "../../agents/agents.service.js";
4
+ import { KernelDb } from "../../infra/database/kernel-db.js";
5
+ import { MembersService } from "../../orgs/members.service.js";
6
+ import { AdminConnectors } from "../../tools/admin-connectors.js";
7
+ import { ConnectorsService } from "../../tools/connectors.service.js";
8
+ import { PlatformOperations } from "../registry.js";
9
+ /**
10
+ * Refusal of an item another org owns: the tool's results enter this org's event log, where
11
+ * another org's items do not belong (Platform tool spec §6.1, §14.5 item 1).
12
+ */
13
+ export declare const OTHER_ORG_ITEM = "This belongs to another organization; the Platform tool acts only on this conversation's organization. Use the admin console.";
14
+ /**
15
+ * The super admin's agent and connector operations of the `Platform` tool (admin agents and
16
+ * connectors spec §4.5): the admin console's views and writes, run through the same services as
17
+ * the routes. Unlike the console they cover only the conversation's org: lists filter to it, and
18
+ * reads and writes of another org's item are refused (Platform tool spec §6.1). The owner transfer
19
+ * is excluded (human-only, as the org's own transfer route).
20
+ */
21
+ export declare class AdminItemsOperations implements OnModuleInit {
22
+ private readonly registry;
23
+ private readonly adminAgents;
24
+ private readonly agents;
25
+ private readonly adminConnectors;
26
+ private readonly connectors;
27
+ private readonly members;
28
+ private readonly kdb;
29
+ constructor(registry: PlatformOperations, adminAgents: AdminAgents, agents: AgentsService, adminConnectors: AdminConnectors, connectors: ConnectorsService, members: MembersService, kdb: KernelDb);
30
+ /** The agent's admin detail when it is the conversation org's; else {@link OTHER_ORG_ITEM}. */
31
+ private agentOf;
32
+ /** The connector's admin detail when it is the conversation org's; else {@link OTHER_ORG_ITEM}. */
33
+ private connectorOf;
34
+ /** A precheck from a read: its `ApiError` code, or `notFound` for another org's item. */
35
+ private precheck;
36
+ onModuleInit(): void;
37
+ }