@capybara-hq/kernel 0.1.0-rc.25 → 0.1.0-rc.26
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/dist/admin/outbox-dead.service.d.ts +7 -3
- package/dist/admin/outbox-dead.service.js +17 -6
- package/dist/admin/overview.service.d.ts +6 -6
- package/dist/admin/overview.service.js +7 -7
- package/dist/background/wake-rules.d.ts +5 -0
- package/dist/background/wake-rules.js +16 -5
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/channels/channels.module.d.ts +9 -8
- package/dist/channels/channels.module.js +11 -10
- package/dist/channels/connections.service.d.ts +3 -3
- package/dist/channels/connections.service.js +6 -6
- package/dist/channels/deliver-job.d.ts +10 -3
- package/dist/channels/deliver-job.js +7 -3
- package/dist/channels/jobs.d.ts +11 -4
- package/dist/channels/jobs.js +18 -3
- package/dist/channels/outbound.d.ts +5 -2
- package/dist/channels/outbound.js +9 -2
- package/dist/conversations/conversations.controller.d.ts +4 -2
- package/dist/conversations/conversations.controller.js +4 -2
- package/dist/conversations/conversations.service.d.ts +37 -4
- package/dist/conversations/conversations.service.js +77 -20
- package/dist/conversations/events.d.ts +6 -0
- package/dist/conversations/events.js +6 -0
- package/dist/db/kernel-schema.d.ts +10 -1
- package/dist/devices/devices.service.d.ts +14 -6
- package/dist/devices/devices.service.js +20 -7
- package/dist/evals/evals.module.js +5 -1
- package/dist/evals/jobs.d.ts +14 -6
- package/dist/evals/jobs.js +21 -19
- package/dist/evals/runs.service.d.ts +5 -6
- package/dist/evals/runs.service.js +11 -33
- package/dist/infra/outbox/outbox.service.d.ts +6 -0
- package/dist/infra/outbox/outbox.service.js +51 -17
- package/dist/infra/outbox/registry.d.ts +52 -0
- package/dist/infra/outbox/registry.js +57 -0
- package/dist/local-devices/device-policy.service.d.ts +131 -26
- package/dist/local-devices/device-policy.service.js +341 -145
- package/dist/local-devices/machines.d.ts +17 -2
- package/dist/local-devices/machines.js +66 -5
- package/dist/local-devices/policy-op-jobs.d.ts +15 -0
- package/dist/local-devices/policy-op-jobs.js +12 -0
- package/dist/local-devices/policy-summary.d.ts +36 -3
- package/dist/local-devices/policy-summary.js +47 -1
- package/dist/orgs/orgs.service.js +4 -1
- package/dist/overrides.d.ts +4 -0
- package/dist/overrides.js +2 -0
- package/dist/platform/ops/conversations.ops.js +1 -1
- package/dist/runtime/maintenance.d.ts +6 -3
- package/dist/runtime/maintenance.js +21 -7
- package/dist/runtime/plugin-context.service.d.ts +9 -3
- package/dist/runtime/plugin-context.service.js +17 -5
- package/dist/runtime/run-context.service.d.ts +7 -3
- package/dist/runtime/run-context.service.js +12 -2
- package/dist/runtime/run-finish.d.ts +2 -2
- package/dist/runtime/run-finish.js +11 -6
- package/dist/runtime/run-jobs.d.ts +21 -4
- package/dist/runtime/run-jobs.js +29 -6
- package/dist/runtime/run-orphan-check.job.d.ts +14 -11
- package/dist/runtime/run-orphan-check.job.js +16 -11
- package/dist/runtime/run-queue.d.ts +23 -31
- package/dist/runtime/run-queue.js +62 -64
- package/dist/runtime/run-status.d.ts +12 -0
- package/dist/runtime/run-status.js +21 -0
- package/dist/runtime/run-worker.d.ts +10 -2
- package/dist/runtime/run-worker.js +44 -19
- package/dist/runtime/step-log.d.ts +6 -5
- package/dist/runtime/step-log.js +6 -5
- package/dist/runtime/usage-report.d.ts +3 -1
- package/dist/runtime/usage-report.js +4 -2
- package/dist/runtime/waits.d.ts +16 -6
- package/dist/runtime/waits.js +37 -15
- package/dist/schedules/scan.job.d.ts +6 -6
- package/dist/schedules/scan.job.js +6 -6
- package/dist/skills/catalog.d.ts +7 -3
- package/dist/skills/catalog.js +12 -2
- package/dist/tools/approvals.service.d.ts +96 -19
- package/dist/tools/approvals.service.js +300 -67
- package/dist/tools/builtins/sandbox/task-output.js +21 -6
- package/dist/tools/device-op-resolver.d.ts +40 -0
- package/dist/tools/device-op-resolver.js +121 -0
- package/dist/tools/network-asks.d.ts +1 -1
- package/dist/tools/network-asks.js +4 -3
- package/dist/tools/tools.module.js +4 -0
- package/package.json +2 -2
- package/src/migrations/0125_conversation_tree_stopped_at.sql +5 -0
|
@@ -21,6 +21,29 @@ export const OUTBOX_FAST_PATH = Symbol("OUTBOX_FAST_PATH");
|
|
|
21
21
|
* logging an error (spec §4, designer's call: "throws in tests, logs in production").
|
|
22
22
|
*/
|
|
23
23
|
export const OUTBOX_STRICT_CONFLICTS = Symbol("OUTBOX_STRICT_CONFLICTS");
|
|
24
|
+
/** Jobs per insert statement of {@link OutboxService.addMany} (6 parameters each). */
|
|
25
|
+
const ADD_MANY_CHUNK = 1000;
|
|
26
|
+
/** The row of `job` (throws on a job id with `:`). */
|
|
27
|
+
function rowOf(job) {
|
|
28
|
+
if (job.jobId.includes(":")) {
|
|
29
|
+
throw new Error(`outbox job id "${job.jobId}" must not contain ":" (BullMQ uses it as its key separator); ` +
|
|
30
|
+
'join the id\'s parts with "-" instead');
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
queue: job.queue,
|
|
34
|
+
job_name: job.name,
|
|
35
|
+
job_id: job.jobId,
|
|
36
|
+
data: JSON.stringify(job.data ?? null),
|
|
37
|
+
run_at: job.runAt ?? null,
|
|
38
|
+
org_id: job.orgId ?? null,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Only a job due now goes through the fast path (ruling R20): a delayed one (a backstop, a
|
|
43
|
+
* timeout) is claimed by the relay within its next tick and added with its remaining delay, so the
|
|
44
|
+
* commit's caller spends no claim transaction on work due in seconds or hours.
|
|
45
|
+
*/
|
|
46
|
+
const dueNow = (job) => job.runAt === undefined || job.runAt.getTime() <= Date.now();
|
|
24
47
|
/**
|
|
25
48
|
* The ids each open transaction inserted, keyed by its transaction store: lives only as long as
|
|
26
49
|
* that transaction (a WeakMap entry, no state shared across requests).
|
|
@@ -51,31 +74,16 @@ let OutboxService = class OutboxService {
|
|
|
51
74
|
* conflicts, else logs an error. Undefined data is stored as JSON null.
|
|
52
75
|
*/
|
|
53
76
|
async add(job) {
|
|
54
|
-
if (job.jobId.includes(":")) {
|
|
55
|
-
throw new Error(`outbox job id "${job.jobId}" must not contain ":" (BullMQ uses it as its key separator); ` +
|
|
56
|
-
'join the id\'s parts with "-" instead');
|
|
57
|
-
}
|
|
58
77
|
const json = JSON.stringify(job.data ?? null);
|
|
59
78
|
const row = await this.db
|
|
60
79
|
.insertInto("outbox")
|
|
61
|
-
.values(
|
|
62
|
-
queue: job.queue,
|
|
63
|
-
job_name: job.name,
|
|
64
|
-
job_id: job.jobId,
|
|
65
|
-
data: json,
|
|
66
|
-
run_at: job.runAt ?? null,
|
|
67
|
-
org_id: job.orgId ?? null,
|
|
68
|
-
})
|
|
80
|
+
.values(rowOf(job))
|
|
69
81
|
.onConflict((oc) => oc.columns(["queue", "job_id"]).where("dead_at", "is", null).doNothing())
|
|
70
82
|
.returning("id")
|
|
71
83
|
.executeTakeFirst();
|
|
72
84
|
if (row) {
|
|
73
|
-
|
|
74
|
-
// timeout) is claimed by the relay within its next tick and added with its remaining delay,
|
|
75
|
-
// so the commit's caller spends no claim transaction on work due in seconds or hours
|
|
76
|
-
if (job.runAt === undefined || job.runAt.getTime() <= Date.now()) {
|
|
85
|
+
if (dueNow(job))
|
|
77
86
|
this.deliverAfterCommit(row.id);
|
|
78
|
-
}
|
|
79
87
|
return true;
|
|
80
88
|
}
|
|
81
89
|
const stored = await this.db
|
|
@@ -100,6 +108,29 @@ let OutboxService = class OutboxService {
|
|
|
100
108
|
}
|
|
101
109
|
return false;
|
|
102
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* {@link add} for many jobs in one multi-row insert per {@link ADD_MANY_CHUNK} jobs (an eval
|
|
113
|
+
* run's cases). A job whose (queue, jobId) is already owed is skipped without comparing its data.
|
|
114
|
+
* Returns how many rows it inserted.
|
|
115
|
+
*/
|
|
116
|
+
async addMany(jobs) {
|
|
117
|
+
let inserted = 0;
|
|
118
|
+
for (let i = 0; i < jobs.length; i += ADD_MANY_CHUNK) {
|
|
119
|
+
const chunk = jobs.slice(i, i + ADD_MANY_CHUNK);
|
|
120
|
+
const rows = await this.db
|
|
121
|
+
.insertInto("outbox")
|
|
122
|
+
.values(chunk.map(rowOf))
|
|
123
|
+
.onConflict((oc) => oc.columns(["queue", "job_id"]).where("dead_at", "is", null).doNothing())
|
|
124
|
+
.returning(["id", "run_at"])
|
|
125
|
+
.execute();
|
|
126
|
+
for (const r of rows) {
|
|
127
|
+
if (dueNow({ runAt: r.run_at ?? undefined }))
|
|
128
|
+
this.deliverAfterCommit(r.id);
|
|
129
|
+
}
|
|
130
|
+
inserted += rows.length;
|
|
131
|
+
}
|
|
132
|
+
return inserted;
|
|
133
|
+
}
|
|
103
134
|
/** Collects `id` for its transaction; the first id registers the transaction's one hint. */
|
|
104
135
|
deliverAfterCommit(id) {
|
|
105
136
|
// `mandatory` guarantees a live store here
|
|
@@ -119,6 +150,9 @@ let OutboxService = class OutboxService {
|
|
|
119
150
|
__decorate([
|
|
120
151
|
Transactional({ propagation: "mandatory" })
|
|
121
152
|
], OutboxService.prototype, "add", null);
|
|
153
|
+
__decorate([
|
|
154
|
+
Transactional({ propagation: "mandatory" })
|
|
155
|
+
], OutboxService.prototype, "addMany", null);
|
|
122
156
|
OutboxService = __decorate([
|
|
123
157
|
Injectable(),
|
|
124
158
|
__param(0, Inject(TransactionHost)),
|
|
@@ -145,6 +145,21 @@ export declare const OUTBOX_JOBS: {
|
|
|
145
145
|
readonly removeOnFail: true;
|
|
146
146
|
};
|
|
147
147
|
};
|
|
148
|
+
readonly "channel-resume": {
|
|
149
|
+
readonly queue: "cap-kernel-channels";
|
|
150
|
+
readonly idempotent: "the catch-up re-reads which of the connection's conversations lag their log and adds their delivery jobs, and a delivery resumes from the delivered seq under a per-conversation lock, so a second catch-up sends nothing twice";
|
|
151
|
+
readonly processedIn: "packages/kernel/src/channels/channels.module.ts";
|
|
152
|
+
readonly opts: {
|
|
153
|
+
readonly attempts: 5;
|
|
154
|
+
readonly backoff: {
|
|
155
|
+
readonly type: "exponential";
|
|
156
|
+
readonly delay: 5000;
|
|
157
|
+
};
|
|
158
|
+
readonly priority: 10;
|
|
159
|
+
readonly removeOnComplete: true;
|
|
160
|
+
readonly removeOnFail: true;
|
|
161
|
+
};
|
|
162
|
+
};
|
|
148
163
|
readonly "scope-item": {
|
|
149
164
|
readonly queue: "cap-kernel-plugin-scope";
|
|
150
165
|
readonly idempotent: "the sync converges each (item, org) from the scope, switches and installs it reads under the org's lock, and the installer re-checks under the org row lock, so a second run changes nothing";
|
|
@@ -221,6 +236,43 @@ export declare const OUTBOX_JOBS: {
|
|
|
221
236
|
readonly removeOnFail: true;
|
|
222
237
|
};
|
|
223
238
|
};
|
|
239
|
+
readonly "device-op-resolve": {
|
|
240
|
+
readonly queue: "cap-kernel-maintenance";
|
|
241
|
+
readonly idempotent: "resolving an op whose entry is gone is a no-op (the entry is read under the machine row's lock, and under the network ask's lock for an op that answers one, so the ask is decided at most once); the resolver only reads the device, never sends a change";
|
|
242
|
+
readonly processedIn: "packages/kernel/src/runtime/maintenance.ts";
|
|
243
|
+
readonly opts: {
|
|
244
|
+
readonly attempts: 5;
|
|
245
|
+
readonly backoff: {
|
|
246
|
+
readonly type: "exponential";
|
|
247
|
+
readonly delay: 5000;
|
|
248
|
+
};
|
|
249
|
+
readonly removeOnComplete: true;
|
|
250
|
+
readonly removeOnFail: true;
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
readonly "bg-wake": {
|
|
254
|
+
readonly queue: "cap-kernel-maintenance";
|
|
255
|
+
readonly idempotent: "the wake only calls ensureRun, which decides from the conversation's state under its row lock, so a wake with nothing unconsumed or an active run starts nothing";
|
|
256
|
+
readonly processedIn: "packages/kernel/src/runtime/maintenance.ts";
|
|
257
|
+
readonly opts: {
|
|
258
|
+
readonly removeOnComplete: true;
|
|
259
|
+
readonly removeOnFail: true;
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
readonly "eval-case": {
|
|
263
|
+
readonly queue: "cap-kernel-evals";
|
|
264
|
+
readonly idempotent: "a case job replays only the case's unfinished results (and pending judgements) of a still-active run, each under a fresh claim, so a second job writes no result twice";
|
|
265
|
+
readonly processedIn: "packages/kernel/src/evals/evals.module.ts";
|
|
266
|
+
readonly opts: {
|
|
267
|
+
readonly attempts: 3;
|
|
268
|
+
readonly backoff: {
|
|
269
|
+
readonly type: "exponential";
|
|
270
|
+
readonly delay: 1000;
|
|
271
|
+
};
|
|
272
|
+
readonly removeOnComplete: true;
|
|
273
|
+
readonly removeOnFail: 1000;
|
|
274
|
+
};
|
|
275
|
+
};
|
|
224
276
|
readonly lifecycle: {
|
|
225
277
|
readonly queue: "cap-kernel-lifecycle";
|
|
226
278
|
readonly idempotent: "lifecycle handlers must be idempotent (the SDK contract): they delete what the plugin holds for the removed member, agent or org, and deleting it again is a no-op";
|
|
@@ -130,6 +130,22 @@ export const OUTBOX_JOBS = {
|
|
|
130
130
|
removeOnFail: true,
|
|
131
131
|
},
|
|
132
132
|
},
|
|
133
|
+
// `channels/deliver-job.ts` RESUME_JOB: owed by the update that lets a connection deliver again
|
|
134
|
+
// (re-enabled, or credentials rotated while active), under `resumeJobId` (one per update)
|
|
135
|
+
"channel-resume": {
|
|
136
|
+
queue: "cap-kernel-channels",
|
|
137
|
+
idempotent: "the catch-up re-reads which of the connection's conversations lag their log and adds their delivery jobs, and a delivery resumes from the delivered seq under a per-conversation lock, so a second catch-up sends nothing twice",
|
|
138
|
+
processedIn: "packages/kernel/src/channels/channels.module.ts",
|
|
139
|
+
// retried while PG or Redis is briefly unavailable; behind inbound jobs like a delivery;
|
|
140
|
+
// removed once done
|
|
141
|
+
opts: {
|
|
142
|
+
attempts: 5,
|
|
143
|
+
backoff: { type: "exponential", delay: 5_000 },
|
|
144
|
+
priority: 10,
|
|
145
|
+
removeOnComplete: true,
|
|
146
|
+
removeOnFail: true,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
133
149
|
// `catalog/scope-sync.ts` SCOPE_ITEM_JOB: owed by every transaction that changes an item's
|
|
134
150
|
// scope, disabled flag, list, owner or platform version, under `scopeItemJobId` (one per scope
|
|
135
151
|
// version), and by a sync deferred on a replica that had not loaded the extension yet, under
|
|
@@ -210,6 +226,47 @@ export const OUTBOX_JOBS = {
|
|
|
210
226
|
removeOnFail: true,
|
|
211
227
|
},
|
|
212
228
|
},
|
|
229
|
+
// `local-devices/policy-op-jobs.ts` DEVICE_OP_RESOLVE_JOB: owed by a device policy op's intent
|
|
230
|
+
// transaction under `deviceOpResolveJobId` with `runAt` = when the op can no longer reach the
|
|
231
|
+
// machine, and by a hello that reported the op's outcome under `deviceOpReportedJobId` (spec §7)
|
|
232
|
+
"device-op-resolve": {
|
|
233
|
+
queue: "cap-kernel-maintenance",
|
|
234
|
+
idempotent: "resolving an op whose entry is gone is a no-op (the entry is read under the machine row's lock, and under the network ask's lock for an op that answers one, so the ask is decided at most once); the resolver only reads the device, never sends a change",
|
|
235
|
+
processedIn: "packages/kernel/src/runtime/maintenance.ts",
|
|
236
|
+
// retried while PG or the device is briefly unreachable; removed once done
|
|
237
|
+
opts: {
|
|
238
|
+
attempts: 5,
|
|
239
|
+
backoff: { type: "exponential", delay: 5_000 },
|
|
240
|
+
removeOnComplete: true,
|
|
241
|
+
removeOnFail: true,
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
// `background/wake-rules.ts` BG_WAKE_JOB: owed by the `ensureRun` transaction that finds a
|
|
245
|
+
// conversation's wake inside its wake window, under `bgWakeJobId`, with `runAt` = the window's
|
|
246
|
+
// end (background Bash spec §3)
|
|
247
|
+
"bg-wake": {
|
|
248
|
+
queue: "cap-kernel-maintenance",
|
|
249
|
+
idempotent: "the wake only calls ensureRun, which decides from the conversation's state under its row lock, so a wake with nothing unconsumed or an active run starts nothing",
|
|
250
|
+
processedIn: "packages/kernel/src/runtime/maintenance.ts",
|
|
251
|
+
// the options the direct enqueue had (the queue's default retries): removed once done, so a
|
|
252
|
+
// later window's wake is never deduped away (a job that fired early owes its next retry id)
|
|
253
|
+
opts: { removeOnComplete: true, removeOnFail: true },
|
|
254
|
+
},
|
|
255
|
+
// `evals/jobs.ts` EVAL_CASE_JOB: owed by an eval run's start transaction (the run row and its
|
|
256
|
+
// pending results), one per case, under `evalJobId`
|
|
257
|
+
"eval-case": {
|
|
258
|
+
queue: "cap-kernel-evals",
|
|
259
|
+
idempotent: "a case job replays only the case's unfinished results (and pending judgements) of a still-active run, each under a fresh claim, so a second job writes no result twice",
|
|
260
|
+
processedIn: "packages/kernel/src/evals/evals.module.ts",
|
|
261
|
+
// the options the direct enqueue had: transient failures outside the handler (PG, Redis)
|
|
262
|
+
// retried with backoff; removed once done, failed ones kept (by count)
|
|
263
|
+
opts: {
|
|
264
|
+
attempts: 3,
|
|
265
|
+
backoff: { type: "exponential", delay: 1000 },
|
|
266
|
+
removeOnComplete: true,
|
|
267
|
+
removeOnFail: 1000,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
213
270
|
// `extensions/lifecycle-hooks.ts` LIFECYCLE_JOB: owed by the transaction that removes a member,
|
|
214
271
|
// deletes an agent or deletes an org, once per subscribing plugin, under `lifecycleJobId`
|
|
215
272
|
// (memory plugin spec §6.3)
|
|
@@ -1,11 +1,24 @@
|
|
|
1
|
-
import { type DeviceFeature, type DeviceLocalPolicySummary, type DevicePolicyArgs } from "@capybara-hq/plugin-sdk";
|
|
1
|
+
import { type DeviceFeature, type DeviceLocalPolicySummary, type DevicePolicyArgs, type DevicePolicyStatus } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
2
3
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
3
4
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
5
|
+
import { OutboxService } from "../infra/outbox/outbox.service.js";
|
|
4
6
|
import type { KernelLogger } from "../log.js";
|
|
5
7
|
import { MachinesService } from "./machines.js";
|
|
6
8
|
import { LocalDeviceRelay } from "./relay.js";
|
|
7
9
|
/** How long the machine may take to apply a policy change. */
|
|
8
10
|
export declare const POLICY_DEADLINE_MS = 30000;
|
|
11
|
+
/** How long an op left `unknown` waits for the machine's hello to report it (from its intent). */
|
|
12
|
+
export declare const UNKNOWN_OP_MS: number;
|
|
13
|
+
/** A policy change as callers ask for it: the op id and the source are added here. */
|
|
14
|
+
type PolicyChange = Omit<DevicePolicyArgs, "source" | "opId">;
|
|
15
|
+
/**
|
|
16
|
+
* Test seam (`KernelOverride`): called by {@link DevicePolicyService.send} right after the `policy`
|
|
17
|
+
* op's call returned, before its outcome is recorded (by `apply` or a network ask's decision);
|
|
18
|
+
* throwing stands for a crash between the call and the resolve transaction.
|
|
19
|
+
* The kernel never provides it.
|
|
20
|
+
*/
|
|
21
|
+
export declare const DEVICE_POLICY_AFTER_CALL: unique symbol;
|
|
9
22
|
/** What became of a policy change. */
|
|
10
23
|
export type PolicyChangeOutcome = {
|
|
11
24
|
applied: true;
|
|
@@ -15,55 +28,147 @@ export type PolicyChangeOutcome = {
|
|
|
15
28
|
reason: "offline" | "refused" | "unsupported";
|
|
16
29
|
message: string;
|
|
17
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* An op whose intent is recorded ({@link DevicePolicyService.begin}): the call sends `args` to
|
|
33
|
+
* the device's machine.
|
|
34
|
+
*/
|
|
35
|
+
export interface PolicyIntent {
|
|
36
|
+
orgId: string;
|
|
37
|
+
deviceId: string;
|
|
38
|
+
machineId: string;
|
|
39
|
+
opId: string;
|
|
40
|
+
args: DevicePolicyArgs;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* What became of a sent op, as recorded by {@link DevicePolicyService.settleIn}: `applied` (with
|
|
44
|
+
* the policy the machine wrote, when it said), `refused` (the machine's message), `not_received`
|
|
45
|
+
* (the machine never got it and never will), `unknown` (no answer).
|
|
46
|
+
*/
|
|
47
|
+
export type PolicyOpResult = {
|
|
48
|
+
outcome: "applied";
|
|
49
|
+
policy?: DeviceLocalPolicySummary;
|
|
50
|
+
} | {
|
|
51
|
+
outcome: "refused";
|
|
52
|
+
message: string;
|
|
53
|
+
} | {
|
|
54
|
+
outcome: "not_received";
|
|
55
|
+
} | {
|
|
56
|
+
outcome: "interrupted";
|
|
57
|
+
} | {
|
|
58
|
+
outcome: "unknown";
|
|
59
|
+
};
|
|
60
|
+
/** What a machine says of an op id ({@link DevicePolicyService.outcomeOf}). */
|
|
61
|
+
export type PolicyOpStatus = DevicePolicyStatus | {
|
|
62
|
+
outcome: "offline";
|
|
63
|
+
};
|
|
18
64
|
/**
|
|
19
65
|
* Changes a local device's policy on its machine (network ask-to-allow spec §5.2; sandbox dev
|
|
20
66
|
* tiers spec §7.3: its hosts, its `tier`, and `isolation` back to `sandbox`): the kernel→device
|
|
21
|
-
* `policy` op over the {@link LocalDeviceRelay}. The machine is the authority: it
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* audits as `device.policy_changed`.
|
|
67
|
+
* `policy` op over the {@link LocalDeviceRelay}. The machine is the authority: it refuses
|
|
68
|
+
* widening unless its owner allows remote grants, validates every host and answers with the
|
|
69
|
+
* policy as written, which the kernel then keeps on the active machine's row (§5.4).
|
|
25
70
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
71
|
+
* Intent first (transactional outbox spec §7): {@link begin} writes the intent in a transaction
|
|
72
|
+
* before anything is sent (the entry on the machine row's `pending_policy_ops`, the audit
|
|
73
|
+
* `device.policy_requested` and the `device-op-resolve` job owed for when the op can no longer
|
|
74
|
+
* reach the machine); the call sends the op with its id (no transaction open); {@link settleIn}
|
|
75
|
+
* records the outcome: `device.policy_changed` (applied), `device.policy_failed` (refused, never
|
|
76
|
+
* received) or `device.policy_unknown` (no answer: the entry stays, `unknown`, for the machine's
|
|
77
|
+
* next hello or the resolver). A crash between the call and the resolve transaction leaves the
|
|
78
|
+
* intent and its owed job, whose resolver asks the machine what became of the op (never sends it
|
|
79
|
+
* again). Callers decide who may change a device's policy (the device owner, in a signed-in
|
|
80
|
+
* session); this service checks only that the device is a live device of the org.
|
|
30
81
|
*/
|
|
31
82
|
export declare class DevicePolicyService {
|
|
32
83
|
private readonly db;
|
|
33
84
|
readonly txHost: TransactionHost;
|
|
34
85
|
private readonly relay;
|
|
35
86
|
private readonly machines;
|
|
87
|
+
private readonly outbox;
|
|
88
|
+
private readonly config;
|
|
36
89
|
private readonly log;
|
|
37
|
-
|
|
90
|
+
private readonly afterCall;
|
|
91
|
+
constructor(db: KernelDb, txHost: TransactionHost, relay: Pick<LocalDeviceRelay, "call" | "holderLive">, machines: Pick<MachinesService, "activeFor">, outbox: Pick<OutboxService, "add">, config: Pick<KernelConfigService, "get">, log: Pick<KernelLogger, "warn">, afterCall?: (opId: string) => void);
|
|
38
92
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* the `
|
|
42
|
-
* is `device.not_found`.
|
|
93
|
+
* Changes the policy: {@link begin}, the call, then the outcome recorded in its own transaction
|
|
94
|
+
* (no transaction may be open). Never throws for an offline machine, a refusal or a program
|
|
95
|
+
* without the `policyOpId` feature (nothing is sent to it); a device that is not a live device
|
|
96
|
+
* of `orgId` is `device.not_found`.
|
|
43
97
|
*/
|
|
44
98
|
apply(input: {
|
|
45
99
|
orgId: string;
|
|
46
100
|
deviceId: string;
|
|
47
|
-
change:
|
|
101
|
+
change: PolicyChange;
|
|
48
102
|
source: DevicePolicyArgs["source"];
|
|
49
103
|
actorTokenId?: string;
|
|
50
104
|
}): Promise<PolicyChangeOutcome>;
|
|
105
|
+
/**
|
|
106
|
+
* Checks the device and its machine, then records the intent in its own transaction: the entry
|
|
107
|
+
* (`sent`) on the machine's row, the audit `device.policy_requested` and the `device-op-resolve`
|
|
108
|
+
* job owed for when the op can no longer reach the machine. No transaction may be open (the
|
|
109
|
+
* call follows). Returns the intent, or why nothing will be sent (offline, unsupported).
|
|
110
|
+
*/
|
|
111
|
+
begin(input: {
|
|
112
|
+
orgId: string;
|
|
113
|
+
deviceId: string;
|
|
114
|
+
change: PolicyChange;
|
|
115
|
+
source: DevicePolicyArgs["source"];
|
|
116
|
+
actorTokenId?: string;
|
|
117
|
+
}): Promise<{
|
|
118
|
+
intent: PolicyIntent;
|
|
119
|
+
} | {
|
|
120
|
+
outcome: PolicyChangeOutcome;
|
|
121
|
+
}>;
|
|
122
|
+
/**
|
|
123
|
+
* Sends the intent's `policy` op (no transaction may be open) and classifies the answer:
|
|
124
|
+
* applied; refused (the machine's local policy refused it: its message); anything else is
|
|
125
|
+
* `unknown`, left to the resolver's `policyStatus`: no answer, and an answer the kernel cannot
|
|
126
|
+
* trust to mean "not applied" (a result it cannot read, the machine's own error). `notFound`:
|
|
127
|
+
* the device was deleted meanwhile (recorded as unknown; the caller throws `device.not_found`).
|
|
128
|
+
*/
|
|
129
|
+
send(intent: PolicyIntent): Promise<{
|
|
130
|
+
result: PolicyOpResult;
|
|
131
|
+
outcome: PolicyChangeOutcome;
|
|
132
|
+
notFound?: true;
|
|
133
|
+
}>;
|
|
134
|
+
/** The `policy` op's call and its classification ({@link send}). */
|
|
135
|
+
private call;
|
|
136
|
+
/**
|
|
137
|
+
* What the machine says became of the op `opId` (a read-only `policyStatus`), or `offline` when
|
|
138
|
+
* no replica holds its connection or it does not answer. Never sends the change.
|
|
139
|
+
*/
|
|
140
|
+
outcomeOf(deviceId: string, opId: string): Promise<PolicyOpStatus>;
|
|
141
|
+
/**
|
|
142
|
+
* Records what became of the op `opId` of machine `machineId`, in the caller's transaction (the
|
|
143
|
+
* machine's row locked): `applied` → the policy the machine wrote (when given) kept on the row,
|
|
144
|
+
* the entry removed, `device.policy_changed` (`late` when the op was left `unknown`); `refused`,
|
|
145
|
+
* `not_received` or `interrupted` → the entry removed, `device.policy_failed` (`reason` the
|
|
146
|
+
* outcome); `unknown` → the entry kept, `unknown` until a day after its intent,
|
|
147
|
+
* `device.policy_unknown` (once; ignored once a hello reported the op). Returns the outcome for
|
|
148
|
+
* the caller, or null when the op has no live entry (resolved already, or expired: a no-op).
|
|
149
|
+
*/
|
|
150
|
+
settleIn(ref: {
|
|
151
|
+
machineId: string;
|
|
152
|
+
opId: string;
|
|
153
|
+
}, result: PolicyOpResult): Promise<PolicyChangeOutcome | null>;
|
|
51
154
|
/** The active machine's features and policy summary (null when no machine is connected). */
|
|
52
155
|
summary(deviceId: string): Promise<{
|
|
53
156
|
features: DeviceFeature[];
|
|
54
157
|
policy: DeviceLocalPolicySummary | null;
|
|
55
158
|
} | null>;
|
|
56
|
-
/** Adds `op` to the machine's pending ops, dropping the expired ones. */
|
|
57
|
-
private notePending;
|
|
58
|
-
/** Removes the pending op `opId` from the machine's row. */
|
|
59
|
-
private dropPending;
|
|
60
159
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
|
|
160
|
+
* How long after its intent an op can no longer reach the machine: the call's deadline, the
|
|
161
|
+
* reconnect grace and the request list's margin (the relay's list TTL), plus 5 s. A machine
|
|
162
|
+
* that does not know the op id by then never will.
|
|
163
|
+
*/
|
|
164
|
+
resolveDelayMs(): number;
|
|
165
|
+
/** {@link settleIn} in its own transaction (the {@link apply} path). */
|
|
166
|
+
private settle;
|
|
167
|
+
/**
|
|
168
|
+
* The intent transaction: locks the machine's row (still the device's active machine), appends
|
|
169
|
+
* the entry (dropping the expired ones), audits `device.policy_requested` and owes the
|
|
170
|
+
* `device-op-resolve` job. False when the machine was revoked meanwhile (nothing written).
|
|
65
171
|
*/
|
|
66
|
-
private
|
|
67
|
-
/** The outcome of a failed op: offline, or the machine's refusal; a deleted device is thrown. */
|
|
68
|
-
private notApplied;
|
|
172
|
+
private recordIntent;
|
|
69
173
|
}
|
|
174
|
+
export {};
|