@capybara-hq/kernel 0.1.0-rc.24 → 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/admin.module.js +2 -0
- package/dist/admin/health.service.d.ts +2 -2
- package/dist/admin/health.service.js +6 -2
- 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/agents/agents.module.js +3 -0
- package/dist/agents/agents.service.d.ts +6 -1
- package/dist/agents/agents.service.js +11 -2
- package/dist/background/wake-rules.d.ts +5 -0
- package/dist/background/wake-rules.js +16 -5
- package/dist/catalog/catalog-import.module.js +0 -4
- package/dist/catalog/catalog-scope.service.d.ts +63 -26
- package/dist/catalog/catalog-scope.service.js +146 -71
- package/dist/catalog/catalog.module.js +4 -3
- package/dist/catalog/catalog.service.d.ts +11 -4
- package/dist/catalog/catalog.service.js +21 -5
- package/dist/catalog/cli.d.ts +4 -3
- package/dist/catalog/cli.js +8 -13
- package/dist/catalog/job-ids.d.ts +8 -20
- package/dist/catalog/job-ids.js +11 -16
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/catalog/org-plugin-uploads.d.ts +11 -4
- package/dist/catalog/org-plugin-uploads.js +21 -6
- package/dist/catalog/owned-items.d.ts +6 -13
- package/dist/catalog/owned-items.js +11 -29
- package/dist/catalog/owned-plugin-cleanup.d.ts +8 -17
- package/dist/catalog/owned-plugin-cleanup.js +12 -66
- package/dist/catalog/platform-components.d.ts +56 -34
- package/dist/catalog/platform-components.js +68 -78
- package/dist/catalog/plugin-import.d.ts +28 -25
- package/dist/catalog/plugin-import.js +90 -76
- package/dist/catalog/plugin-upgrade.d.ts +4 -4
- package/dist/catalog/plugin-upgrade.js +7 -19
- package/dist/catalog/scope-jobs.d.ts +59 -0
- package/dist/catalog/scope-jobs.js +65 -0
- package/dist/catalog/scope-sync-workers.d.ts +22 -0
- package/dist/catalog/scope-sync-workers.js +58 -2
- package/dist/catalog/scope-sync.d.ts +40 -65
- package/dist/catalog/scope-sync.js +114 -122
- 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/inbound.js +2 -2
- 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/channels/threads.d.ts +4 -3
- package/dist/channels/threads.js +4 -3
- package/dist/conversations/conversations.controller.d.ts +4 -2
- package/dist/conversations/conversations.controller.js +8 -4
- package/dist/conversations/conversations.service.d.ts +54 -9
- package/dist/conversations/conversations.service.js +94 -25
- package/dist/conversations/events.d.ts +27 -0
- package/dist/conversations/events.js +30 -1
- package/dist/conversations/source.d.ts +10 -0
- package/dist/conversations/source.js +17 -0
- package/dist/conversations/tree.d.ts +10 -0
- package/dist/conversations/tree.js +35 -0
- package/dist/db/kernel-schema.d.ts +19 -15
- package/dist/devices/devices.service.d.ts +25 -14
- package/dist/devices/devices.service.js +36 -30
- 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/run-replay.d.ts +13 -2
- package/dist/evals/run-replay.js +28 -1
- package/dist/evals/runs.service.d.ts +5 -6
- package/dist/evals/runs.service.js +11 -33
- package/dist/evals/snapshot.d.ts +7 -1
- package/dist/evals/snapshot.js +2 -1
- package/dist/extensions/hooks.d.ts +22 -3
- package/dist/extensions/hooks.js +15 -1
- package/dist/extensions/lifecycle-hooks.d.ts +99 -0
- package/dist/extensions/lifecycle-hooks.js +273 -0
- package/dist/extensions/org-config.d.ts +13 -1
- package/dist/extensions/org-config.js +19 -2
- package/dist/extensions/permission-view.d.ts +9 -5
- package/dist/extensions/permission-view.js +9 -4
- package/dist/extensions/point-defaults.d.ts +28 -1
- package/dist/extensions/point-defaults.js +44 -13
- package/dist/inbox/inbox.service.d.ts +6 -20
- package/dist/inbox/inbox.service.js +0 -3
- package/dist/inbox/org-notices.d.ts +1 -1
- package/dist/inbox/platform-inbox.service.d.ts +3 -32
- package/dist/inbox/platform-inbox.service.js +5 -54
- 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 +176 -0
- package/dist/infra/outbox/registry.js +183 -0
- package/dist/infra/queue/queue-names.d.ts +1 -1
- package/dist/infra/queue/queue-names.js +1 -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/directory-sync.js +1 -1
- package/dist/orgs/members.service.js +1 -1
- package/dist/orgs/offboarding.d.ts +50 -57
- package/dist/orgs/offboarding.js +117 -162
- package/dist/orgs/orgs.module.js +3 -0
- package/dist/orgs/orgs.service.d.ts +11 -5
- package/dist/orgs/orgs.service.js +19 -7
- package/dist/overrides.d.ts +7 -0
- package/dist/overrides.js +4 -0
- package/dist/platform/ops/conversations.ops.js +1 -1
- package/dist/platform/ops/inbox.ops.js +4 -20
- package/dist/platform/ops/platform.ops.d.ts +1 -0
- package/dist/platform/ops/platform.ops.js +6 -3
- package/dist/roles/worker-role.d.ts +10 -5
- package/dist/roles/worker-role.js +19 -9
- package/dist/roles/worker.module.js +2 -4
- package/dist/runtime/context/build.d.ts +3 -0
- package/dist/runtime/context/build.js +15 -0
- package/dist/runtime/context/plugin-context.d.ts +20 -0
- package/dist/runtime/context/plugin-context.js +30 -0
- package/dist/runtime/langgraph-loop.js +4 -0
- package/dist/runtime/loop.d.ts +8 -0
- package/dist/runtime/maintenance.d.ts +9 -4
- package/dist/runtime/maintenance.js +52 -13
- package/dist/runtime/model-call.d.ts +6 -1
- package/dist/runtime/model-call.js +1 -0
- package/dist/runtime/plugin-context.service.d.ts +75 -0
- package/dist/runtime/plugin-context.service.js +220 -0
- package/dist/runtime/run-context.service.d.ts +7 -3
- package/dist/runtime/run-context.service.js +12 -2
- package/dist/runtime/run-control.d.ts +6 -1
- package/dist/runtime/run-control.js +13 -0
- 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 +29 -24
- package/dist/runtime/run-queue.js +118 -57
- package/dist/runtime/run-status.d.ts +12 -0
- package/dist/runtime/run-status.js +21 -0
- package/dist/runtime/run-worker.d.ts +32 -3
- package/dist/runtime/run-worker.js +143 -21
- package/dist/runtime/runtime.module.js +2 -0
- package/dist/runtime/step-log.d.ts +6 -5
- package/dist/runtime/step-log.js +6 -5
- package/dist/runtime/subagents/dispatch.js +1 -0
- package/dist/runtime/subagents/skill-fork.js +3 -0
- 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/sandbox/jobs.d.ts +20 -10
- package/dist/sandbox/jobs.js +23 -17
- package/dist/schedules/fire.d.ts +25 -4
- package/dist/schedules/fire.js +87 -13
- package/dist/schedules/job-ids.d.ts +66 -17
- package/dist/schedules/job-ids.js +81 -13
- package/dist/schedules/run-tracker.js +2 -1
- package/dist/schedules/scan.job.d.ts +16 -35
- package/dist/schedules/scan.job.js +38 -147
- package/dist/schedules/scheduled-tasks.service.d.ts +6 -6
- package/dist/schedules/scheduled-tasks.service.js +17 -14
- package/dist/schedules/schedules.module.js +0 -4
- package/dist/skills/catalog.d.ts +7 -3
- package/dist/skills/catalog.js +12 -2
- package/dist/skills/changes.controller.d.ts +1 -1
- package/dist/skills/changes.controller.js +1 -1
- package/dist/tools/approvals.service.d.ts +111 -23
- package/dist/tools/approvals.service.js +326 -77
- 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/pipeline.d.ts +11 -2
- package/dist/tools/pipeline.js +5 -0
- package/dist/tools/questions.service.d.ts +15 -4
- package/dist/tools/questions.service.js +21 -19
- package/dist/tools/registry.d.ts +8 -1
- package/dist/tools/registry.js +9 -0
- package/dist/tools/tools.module.js +4 -0
- package/package.json +3 -3
- package/src/migrations/0121_drop_platform_migration_runs.sql +12 -0
- package/src/migrations/0122_drop_scheduled_task_runs_rechecked_at.sql +5 -0
- package/src/migrations/0123_pgvector.sql +27 -0
- package/src/migrations/0124_conversation_source.sql +36 -0
- package/src/migrations/0125_conversation_tree_stopped_at.sql +5 -0
- package/dist/catalog/platform-migration.d.ts +0 -115
- package/dist/catalog/platform-migration.js +0 -583
- package/dist/infra/queue/add-job-once.d.ts +0 -14
- package/dist/infra/queue/add-job-once.js +0 -16
|
@@ -8,25 +8,29 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { isDeepStrictEqual } from "node:util";
|
|
11
12
|
import { connectorToolKey, parseToolKey, validateAllowHost, } from "@capybara-hq/plugin-sdk";
|
|
12
13
|
import { Inject, Injectable } from "@nestjs/common";
|
|
13
14
|
import { sql } from "kysely";
|
|
14
15
|
import { EventLog } from "../conversations/event-log.js";
|
|
15
16
|
import { NETWORK_ASK_MAX_HOSTS, NETWORK_HOST_MAX_CHARS, } from "../conversations/events.js";
|
|
17
|
+
import { conversationSourceOf } from "../conversations/source.js";
|
|
16
18
|
import { ApiError } from "../errors.js";
|
|
17
19
|
import { audit } from "../identity/audit.js";
|
|
18
20
|
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
21
|
+
import { afterCommit } from "../infra/database/after-commit.js";
|
|
19
22
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
20
23
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
21
24
|
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
22
25
|
import { KernelEvents } from "../infra/events/kernel-events.js";
|
|
23
26
|
import { LockService } from "../infra/lock/lock.service.js";
|
|
24
27
|
import { WithLock } from "../infra/lock/with-lock.decorator.js";
|
|
25
|
-
import { DevicePolicyService } from "../local-devices/device-policy.service.js";
|
|
28
|
+
import { DevicePolicyService, } from "../local-devices/device-policy.service.js";
|
|
29
|
+
import { pendingPolicyOpsOf } from "../local-devices/policy-summary.js";
|
|
26
30
|
import { ORG_ROLES, roleAtLeast } from "../orgs/roles.js";
|
|
27
31
|
import { RunResume } from "../runtime/run-jobs.js";
|
|
28
32
|
import { rootConversationOf } from "../runtime/subagents/child.js";
|
|
29
|
-
import { SandboxService } from "../sandbox/sandbox.service.js";
|
|
33
|
+
import { deviceNotFound, SandboxService } from "../sandbox/sandbox.service.js";
|
|
30
34
|
import { readScheduleSettings } from "../schedules/effective-settings.js";
|
|
31
35
|
import { grantRefusalOf, NETWORK_GRANT_PREFIX, NETWORK_GRANT_SPEC_HASH, networkGrantRefusal, } from "../schedules/grant-rules.js";
|
|
32
36
|
import { ScheduledTaskStore } from "../schedules/store.js";
|
|
@@ -54,6 +58,18 @@ export const NETWORK_ASK_TOOL_KEY = "Bash";
|
|
|
54
58
|
/** The spec hash of a network ask (no tool definition is approved). */
|
|
55
59
|
const NETWORK_ASK_SPEC_HASH = "network";
|
|
56
60
|
const notFound = () => new ApiError("approval.not_found", "Approval not found");
|
|
61
|
+
/**
|
|
62
|
+
* A decision refused before it wrote anything (the ask closed, expired or is not the account's to
|
|
63
|
+
* decide): the device op's outcome is still recorded in the same transaction.
|
|
64
|
+
*/
|
|
65
|
+
const refusedBeforeWrite = (e) => e instanceof ApiError &&
|
|
66
|
+
(e.code === "approval.already_decided" || e.code === "approval.not_found");
|
|
67
|
+
/** A pending network ask's reason when its device change got no answer (`detail.attempt`). */
|
|
68
|
+
const UNCONFIRMED_ATTEMPT = "The device did not confirm the change to its network rules; its next connection tells whether it took effect.";
|
|
69
|
+
/** A pending network ask's reason when the device applied it but its chooser cannot decide. */
|
|
70
|
+
const APPLIED_UNDECIDED_ATTEMPT = "The device allowed it, but the person who chose this can no longer decide this request.";
|
|
71
|
+
/** A pending network ask's reason when its device change failed without a message. */
|
|
72
|
+
const NOT_APPLIED_ATTEMPT = "The device did not apply the change to its network rules.";
|
|
57
73
|
const iso = (v) => new Date(v).toISOString();
|
|
58
74
|
const isRole = (v) => v !== null && ORG_ROLES.includes(v);
|
|
59
75
|
/** The scopes of an approval without stored ones: a role escalation is approved only once. */
|
|
@@ -125,7 +141,10 @@ let ApprovalsService = class ApprovalsService {
|
|
|
125
141
|
constructor(db, config, eventLog, events, txHost, schedules,
|
|
126
142
|
/** A trial approval is kept only for a tool the task may hold as a grant (its agent's tools). */
|
|
127
143
|
registry,
|
|
128
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* 「这台设备一直允许」 records the device change's intent, sends the `policy` op, then records
|
|
146
|
+
* its outcome with the decision.
|
|
147
|
+
*/
|
|
129
148
|
policies,
|
|
130
149
|
/** One decision of a network ask at a time, across replicas (the op is sent before the transaction). */
|
|
131
150
|
lockService,
|
|
@@ -390,12 +409,13 @@ let ApprovalsService = class ApprovalsService {
|
|
|
390
409
|
* A network ask's decision (network ask-to-allow spec §5.2), one at a time per ask across
|
|
391
410
|
* replicas: only the device owner (`requested_for`) decides, in a signed-in session (a personal
|
|
392
411
|
* access token is refused; the route already refuses service and embed tokens); the allowed
|
|
393
|
-
* hosts are a subset of those asked. 「这台设备一直允许」
|
|
394
|
-
*
|
|
395
|
-
* as a run grant with the reason, and when the device refuses (its owner takes no
|
|
396
|
-
* as `always` with outcome `refused` and nothing granted
|
|
397
|
-
*
|
|
398
|
-
*
|
|
412
|
+
* hosts are a subset of those asked. 「这台设备一直允许」 goes through the device change's intent
|
|
413
|
+
* ({@link decideAlways}): when the device cannot be reached or lacks the feature the decision is
|
|
414
|
+
* recorded as a run grant with the reason, and when the device refuses (its owner takes no
|
|
415
|
+
* remote grants) as `always` with outcome `refused` and nothing granted; the decision and the
|
|
416
|
+
* op's outcome are one transaction. Every decision resumes the run in its transaction and is
|
|
417
|
+
* announced after the commit; a change the device applied after the ask closed is audited
|
|
418
|
+
* (`late`) and answered with a 409 that says so.
|
|
399
419
|
*/
|
|
400
420
|
async decideNetwork(orgId, id, accountId, body, actorTokenId, via) {
|
|
401
421
|
const row = await this.db
|
|
@@ -457,18 +477,11 @@ let ApprovalsService = class ApprovalsService {
|
|
|
457
477
|
}
|
|
458
478
|
network = { wanted: granted, granted, outcome: "once", via };
|
|
459
479
|
if (scope === "always") {
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
network =
|
|
466
|
-
applied === null
|
|
467
|
-
? { wanted: granted, granted, outcome: "always", via }
|
|
468
|
-
: applied === "refused"
|
|
469
|
-
? // the device's owner takes no remote grants: nothing is allowed, not even this run
|
|
470
|
-
{ wanted: granted, granted: [], outcome: "refused", fallback: applied, via }
|
|
471
|
-
: { wanted: granted, granted, outcome: "once_fallback", fallback: applied, via };
|
|
480
|
+
const decided = await this.decideAlways(orgId, ask, accountId, body, granted, via);
|
|
481
|
+
// null: the resolver decided it as this owner meanwhile (announced by its commit)
|
|
482
|
+
if (decided.toSeq !== null)
|
|
483
|
+
await this.announceDecided(decided.record, decided.toSeq);
|
|
484
|
+
return decided.record;
|
|
472
485
|
}
|
|
473
486
|
}
|
|
474
487
|
else {
|
|
@@ -479,46 +492,267 @@ let ApprovalsService = class ApprovalsService {
|
|
|
479
492
|
}
|
|
480
493
|
network = { wanted: [], granted: [], outcome: null, via };
|
|
481
494
|
}
|
|
482
|
-
|
|
495
|
+
const decided = await this.decideIn(orgId, id, accountId, body, actorTokenId, network);
|
|
496
|
+
await this.announceDecided(decided.record, decided.toSeq);
|
|
497
|
+
return decided.record;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* 「这台设备一直允许」 (transactional outbox spec §7): the intent of the device change (naming the
|
|
501
|
+
* ask) is recorded, the `policy` op sent (no transaction open), then {@link settleNetworkOp}
|
|
502
|
+
* decides the ask and records the op's outcome in one transaction. When nothing could be sent
|
|
503
|
+
* (the device is offline, deleted, or its program lacks the feature) the decision is a run
|
|
504
|
+
* grant with the reason. The device's answer decides: applied → `always`; refused → `always`
|
|
505
|
+
* with outcome `refused`, nothing granted; no answer → a run grant (`once_fallback`, `offline`)
|
|
506
|
+
* while the op stays `unknown` for the machine's next hello or the resolver. A device that
|
|
507
|
+
* reconnects during the call and reports the op applied lets the resolver approve the ask as
|
|
508
|
+
* this owner first: that decision is returned (`toSeq` null: already announced), never a 409;
|
|
509
|
+
* when it granted other hosts than this call's (another of the owner's changes) and this call's
|
|
510
|
+
* change was not confirmed, `approval.already_decided` says so. A device deleted during the
|
|
511
|
+
* call is `device.not_found` (the op recorded unknown).
|
|
512
|
+
*/
|
|
513
|
+
async decideAlways(orgId, ask, accountId, body, granted, via) {
|
|
514
|
+
const detail = ask.detail;
|
|
515
|
+
const accountName = (await this.displayNameOf(accountId)) ?? accountId;
|
|
516
|
+
let begun;
|
|
483
517
|
try {
|
|
484
|
-
|
|
518
|
+
begun = await this.policies.begin({
|
|
519
|
+
orgId,
|
|
520
|
+
deviceId: detail.deviceId,
|
|
521
|
+
change: { add: granted },
|
|
522
|
+
source: { approvalId: ask.id, accountId, accountName, via },
|
|
523
|
+
});
|
|
485
524
|
}
|
|
486
525
|
catch (e) {
|
|
487
|
-
// the device
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
526
|
+
// the device was deleted meanwhile: nothing to write to, the run cannot go on there either
|
|
527
|
+
if (!(e instanceof ApiError && e.code === "device.not_found"))
|
|
528
|
+
throw e;
|
|
529
|
+
begun = { outcome: { applied: false, reason: "offline", message: e.message } };
|
|
530
|
+
}
|
|
531
|
+
if ("outcome" in begun) {
|
|
532
|
+
// nothing was sent: a run grant with the reason
|
|
533
|
+
const fallback = begun.outcome.applied ? "offline" : begun.outcome.reason;
|
|
534
|
+
return this.decideIn(orgId, ask.id, accountId, body, undefined, {
|
|
535
|
+
wanted: granted,
|
|
536
|
+
granted,
|
|
537
|
+
outcome: "once_fallback",
|
|
538
|
+
fallback,
|
|
539
|
+
via,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
const { intent } = begun;
|
|
543
|
+
const sent = await this.policies.send(intent);
|
|
544
|
+
if (sent.notFound) {
|
|
545
|
+
// deleted during the call: nothing to grant on, the run cannot go on there either
|
|
546
|
+
await this.settleOpOnly({ machineId: intent.machineId, opId: intent.opId }, sent.result);
|
|
547
|
+
throw deviceNotFound();
|
|
548
|
+
}
|
|
549
|
+
const network = sent.result.outcome === "applied"
|
|
550
|
+
? { wanted: granted, granted, outcome: "always", via }
|
|
551
|
+
: sent.result.outcome === "refused"
|
|
552
|
+
? // the device's owner takes no remote grants: nothing is allowed, not even this run
|
|
553
|
+
{ wanted: granted, granted: [], outcome: "refused", fallback: "refused", via }
|
|
554
|
+
: { wanted: granted, granted, outcome: "once_fallback", fallback: "offline", via };
|
|
555
|
+
const settled = await this.settleNetworkOp(orgId, ask.id, accountId, body, network, {
|
|
556
|
+
machineId: intent.machineId,
|
|
557
|
+
opId: intent.opId,
|
|
558
|
+
result: sent.result,
|
|
559
|
+
});
|
|
560
|
+
if ("decided" in settled)
|
|
561
|
+
return settled.decided;
|
|
562
|
+
if (settled.closed.code === "approval.already_decided" && !settled.late) {
|
|
563
|
+
// the machine reconnected mid-call and reported the op: the resolver approved it as this
|
|
564
|
+
// owner, with this change; it is their decision, not a conflict
|
|
565
|
+
const now = await this.db
|
|
566
|
+
.selectFrom("approvals")
|
|
567
|
+
.selectAll()
|
|
568
|
+
.where("id", "=", ask.id)
|
|
569
|
+
.executeTakeFirst();
|
|
570
|
+
const record = now === undefined ? null : toRecord(now);
|
|
571
|
+
if (record?.status === "approved" &&
|
|
572
|
+
record.detail?.outcome === "always" &&
|
|
573
|
+
record.decidedBy === accountId) {
|
|
574
|
+
const stored = [...(record.detail.grantedHosts ?? [])].sort();
|
|
575
|
+
if (isDeepStrictEqual(stored, [...granted].sort()))
|
|
576
|
+
return { record, toSeq: null };
|
|
577
|
+
// approved by another of this owner's changes (other hosts), and this call's change was
|
|
578
|
+
// not confirmed by the device: never report it as done
|
|
579
|
+
throw new ApiError("approval.already_decided", `The request was already approved to always allow ${stored.join(", ") || "no hosts"}; ` +
|
|
580
|
+
`the device did not confirm allowing ${granted.join(", ")}.`);
|
|
494
581
|
}
|
|
495
|
-
throw e;
|
|
496
582
|
}
|
|
497
|
-
|
|
498
|
-
|
|
583
|
+
// the device took the hosts, but a reply, the expiry or a stop closed the ask meanwhile: the
|
|
584
|
+
// change stands (the owner chose it), audited late in the settle transaction, and said
|
|
585
|
+
if (settled.late && settled.closed.code === "approval.already_decided") {
|
|
586
|
+
throw new ApiError("approval.already_decided", `The device now allows ${granted.join(", ")}; the request had already closed.`);
|
|
587
|
+
}
|
|
588
|
+
throw settled.closed;
|
|
499
589
|
}
|
|
500
|
-
/**
|
|
501
|
-
async
|
|
590
|
+
/** Records only the op's outcome, in its own transaction (the ask is left as it is). */
|
|
591
|
+
async settleOpOnly(op, result) {
|
|
592
|
+
await this.policies.settleIn(op, result);
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* The resolve transaction of a network ask's device change: the ask decided as `network` says
|
|
596
|
+
* ({@link decideTx}: lock order conversation → approval → run), then the op's outcome recorded
|
|
597
|
+
* (`DevicePolicyService.settleIn`: the machine's row). When the ask closed meanwhile (a reply,
|
|
598
|
+
* the expiry, a stop) only the op's outcome is recorded, and a change the device applied is
|
|
599
|
+
* audited `device.network_allowed` with `late: true` in the same transaction.
|
|
600
|
+
*/
|
|
601
|
+
async settleNetworkOp(orgId, id, accountId, body, network, op) {
|
|
602
|
+
let decided;
|
|
603
|
+
try {
|
|
604
|
+
decided = await this.decideTx(orgId, id, accountId, body, undefined, network);
|
|
605
|
+
}
|
|
606
|
+
catch (e) {
|
|
607
|
+
// refused before any write (closed, expired, gone): the op's outcome is recorded anyway
|
|
608
|
+
if (!refusedBeforeWrite(e))
|
|
609
|
+
throw e;
|
|
610
|
+
const late = await this.settleClosedIn(id, op, network.via);
|
|
611
|
+
return { closed: e, late };
|
|
612
|
+
}
|
|
613
|
+
await this.policies.settleIn(op, op.result);
|
|
614
|
+
return { decided };
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* The op's outcome for an ask that is no longer pending (in the caller's transaction): recorded,
|
|
618
|
+
* and a change the device applied audited `device.network_allowed` `late` (the change stands:
|
|
619
|
+
* the owner chose it; no inverse change is sent). True when it was audited late. `undecided`:
|
|
620
|
+
* the ask is still pending but the op's account can no longer decide it; the allow is audited
|
|
621
|
+
* without `late` (it is not after the ask) and the caller gives the ask the reason.
|
|
622
|
+
*/
|
|
623
|
+
async settleClosedIn(approvalId, op, via, undecided = false) {
|
|
624
|
+
const pendingOp = await this.pendingOpOf(op.machineId, op.opId);
|
|
625
|
+
const out = await this.policies.settleIn(op, op.result);
|
|
626
|
+
if (out === null || pendingOp === null || op.result.outcome !== "applied")
|
|
627
|
+
return false;
|
|
628
|
+
const row = await this.db
|
|
629
|
+
.selectFrom("approvals")
|
|
630
|
+
.selectAll()
|
|
631
|
+
.where("id", "=", approvalId)
|
|
632
|
+
.executeTakeFirst();
|
|
633
|
+
if (!row)
|
|
634
|
+
return false;
|
|
635
|
+
const ask = toRecord(row);
|
|
502
636
|
if (ask.detail === null)
|
|
503
|
-
return;
|
|
637
|
+
return false;
|
|
504
638
|
await audit(this.db.db, {
|
|
505
|
-
actorAccountId:
|
|
506
|
-
orgId,
|
|
639
|
+
actorAccountId: pendingOp.source.accountId,
|
|
640
|
+
orgId: ask.orgId,
|
|
507
641
|
action: "device.network_allowed",
|
|
508
642
|
target: ask.detail.deviceId,
|
|
509
643
|
detail: {
|
|
510
644
|
approvalId: ask.id,
|
|
511
645
|
deviceId: ask.detail.deviceId,
|
|
512
|
-
hosts:
|
|
646
|
+
hosts: pendingOp.add,
|
|
513
647
|
scope: "always",
|
|
514
648
|
outcome: "always",
|
|
515
|
-
late: true,
|
|
516
|
-
by:
|
|
517
|
-
via
|
|
649
|
+
...(undecided ? { undecided: true } : { late: true }),
|
|
650
|
+
by: pendingOp.source.accountId,
|
|
651
|
+
via,
|
|
518
652
|
conversationId: ask.conversationId,
|
|
519
653
|
runId: ask.runId,
|
|
520
654
|
},
|
|
521
655
|
});
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
/** The live intent `opId` on machine `machineId` (null: settled already, or expired). */
|
|
659
|
+
async pendingOpOf(machineId, opId) {
|
|
660
|
+
const r = await this.db
|
|
661
|
+
.selectFrom("device_machines")
|
|
662
|
+
.select("pending_policy_ops")
|
|
663
|
+
.where("id", "=", machineId)
|
|
664
|
+
.executeTakeFirst();
|
|
665
|
+
const op = pendingPolicyOpsOf(r?.pending_policy_ops).find((o) => o.id === opId);
|
|
666
|
+
return op === undefined || op.until <= Date.now() ? null : op;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* The `device-op-resolve` job's outcome of an op that answers network ask `op.source.approvalId`
|
|
670
|
+
* (transactional outbox spec §7.4), in the resolver's transaction: under the ask's locks
|
|
671
|
+
* (conversation, then the approval) the op is read again (settled meanwhile: nothing to do).
|
|
672
|
+
* Applied, and the ask still pending → approved `always` by the op's account (`via` as it
|
|
673
|
+
* asked), the run resumed in this transaction as every decision does; the ask closed → audited
|
|
674
|
+
* `late`; still pending but the op's account can no longer decide it → the allow audited (not
|
|
675
|
+
* late) and the ask left pending with `applied_undecided`. Not applied (refused, never received,
|
|
676
|
+
* interrupted) or still unknown → the op's outcome recorded and the ask left pending with the
|
|
677
|
+
* reason (`detail.attempt`) and an `approval_updated` event so the card shows it: no person is
|
|
678
|
+
* there to fall back to a run grant (D2).
|
|
679
|
+
*/
|
|
680
|
+
async settleFromResolver(ref, result) {
|
|
681
|
+
const t = this.db.db;
|
|
682
|
+
const op = { machineId: ref.machineId, opId: ref.opId, result };
|
|
683
|
+
if (!(await this.lockConversationOf(ref.approvalId))) {
|
|
684
|
+
await this.policies.settleIn(op, result);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const row = await t
|
|
688
|
+
.selectFrom("approvals")
|
|
689
|
+
.selectAll()
|
|
690
|
+
.select(sql `expires_at <= now()`.as("expired"))
|
|
691
|
+
.where("id", "=", ref.approvalId)
|
|
692
|
+
.forUpdate()
|
|
693
|
+
.executeTakeFirst();
|
|
694
|
+
const pendingOp = await this.pendingOpOf(ref.machineId, ref.opId);
|
|
695
|
+
if (row === undefined || pendingOp === null) {
|
|
696
|
+
await this.policies.settleIn(op, result);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const ask = toRecord(row);
|
|
700
|
+
const open = ask.status === "pending" && !row.expired && ask.kind === "network";
|
|
701
|
+
const via = pendingOp.source.via;
|
|
702
|
+
if (result.outcome === "applied") {
|
|
703
|
+
if (!open) {
|
|
704
|
+
await this.settleClosedIn(ask.id, op, via);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
let decided;
|
|
708
|
+
try {
|
|
709
|
+
decided = await this.decideTx(ask.orgId, ask.id, pendingOp.source.accountId, { decision: "approve", scope: "always" }, undefined, { wanted: pendingOp.add, granted: pendingOp.add, outcome: "always", via });
|
|
710
|
+
}
|
|
711
|
+
catch (e) {
|
|
712
|
+
// the op's account may no longer decide it: the change stands, the ask waits with why
|
|
713
|
+
if (!refusedBeforeWrite(e))
|
|
714
|
+
throw e;
|
|
715
|
+
if (await this.settleClosedIn(ask.id, op, via, true)) {
|
|
716
|
+
await this.attemptIn(ask, ref.opId, {
|
|
717
|
+
outcome: "applied_undecided",
|
|
718
|
+
message: APPLIED_UNDECIDED_ATTEMPT,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
await this.policies.settleIn(op, result);
|
|
724
|
+
afterCommit(() => this.announceDecided(decided.record, decided.toSeq));
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const out = await this.policies.settleIn(op, result);
|
|
728
|
+
if (out === null || !open || ask.detail === null)
|
|
729
|
+
return;
|
|
730
|
+
await this.attemptIn(ask, ref.opId, result.outcome === "unknown"
|
|
731
|
+
? { outcome: "unknown", message: UNCONFIRMED_ATTEMPT }
|
|
732
|
+
: { outcome: "failed", message: out.applied ? NOT_APPLIED_ATTEMPT : out.message });
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* A pending network ask's reason (`detail.attempt`) and its `approval_updated` event (one per
|
|
736
|
+
* op and outcome), in the caller's transaction with the ask locked; notified after the commit.
|
|
737
|
+
*/
|
|
738
|
+
async attemptIn(ask, opId, attempt) {
|
|
739
|
+
if (ask.detail === null)
|
|
740
|
+
return;
|
|
741
|
+
await this.db.db
|
|
742
|
+
.updateTable("approvals")
|
|
743
|
+
.set({ detail: JSON.stringify({ ...ask.detail, attempt }) })
|
|
744
|
+
.where("id", "=", ask.id)
|
|
745
|
+
.execute();
|
|
746
|
+
const written = await this.eventLog.appendIn(ask.conversationId, [
|
|
747
|
+
{
|
|
748
|
+
type: "approval_updated",
|
|
749
|
+
runId: ask.runId,
|
|
750
|
+
idemKey: `approval_updated:${ask.id}:${opId}:${attempt.outcome}`,
|
|
751
|
+
payload: { approvalId: ask.id, callId: ask.callId },
|
|
752
|
+
},
|
|
753
|
+
]);
|
|
754
|
+
const toSeq = Math.max(...written.map((e) => e.seq));
|
|
755
|
+
afterCommit(() => this.eventLog.notify(ask.conversationId, toSeq));
|
|
522
756
|
}
|
|
523
757
|
/** The owner of a live device of the org (null: none, deleted, or of another org). */
|
|
524
758
|
async deviceOwnerOf(orgId, deviceId) {
|
|
@@ -531,28 +765,6 @@ let ApprovalsService = class ApprovalsService {
|
|
|
531
765
|
.executeTakeFirst();
|
|
532
766
|
return d?.owner_account_id ?? null;
|
|
533
767
|
}
|
|
534
|
-
/**
|
|
535
|
-
* Adds the hosts to the device's local policy (the `policy` op; never inside a transaction).
|
|
536
|
-
* Null when it was applied, else why not: the decision falls back to a run grant.
|
|
537
|
-
*/
|
|
538
|
-
async applyAlways(orgId, deviceId, hosts, source) {
|
|
539
|
-
const accountName = (await this.displayNameOf(source.accountId)) ?? source.accountId;
|
|
540
|
-
try {
|
|
541
|
-
const out = await this.policies.apply({
|
|
542
|
-
orgId,
|
|
543
|
-
deviceId,
|
|
544
|
-
change: { add: hosts },
|
|
545
|
-
source: { ...source, accountName },
|
|
546
|
-
});
|
|
547
|
-
return out.applied ? null : out.reason;
|
|
548
|
-
}
|
|
549
|
-
catch (e) {
|
|
550
|
-
// the device was deleted meanwhile: nothing to write to, the run cannot go on there either
|
|
551
|
-
if (e instanceof ApiError && e.code === "device.not_found")
|
|
552
|
-
return "offline";
|
|
553
|
-
throw e;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
768
|
/** After the commit: the decision's events reach the page, the run's waits and IM delivery. */
|
|
557
769
|
async announceDecided(record, toSeq) {
|
|
558
770
|
await this.eventLog.notify(record.conversationId, toSeq);
|
|
@@ -576,7 +788,14 @@ let ApprovalsService = class ApprovalsService {
|
|
|
576
788
|
}
|
|
577
789
|
// its own transaction (as the legacy one was): `approval.decided` is emitted only after the
|
|
578
790
|
// decision committed, even when a caller has a transaction open
|
|
579
|
-
|
|
791
|
+
decideIn(orgId, id, accountId, body, actorTokenId, network) {
|
|
792
|
+
return this.decideTx(orgId, id, accountId, body, actorTokenId, network);
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* A decision in the caller's transaction: lock order conversation → approval → run (the resume).
|
|
796
|
+
* Refuses (`approval.already_decided`, `approval.not_found`, …) before any write.
|
|
797
|
+
*/
|
|
798
|
+
async decideTx(orgId, id, accountId, body, actorTokenId, network) {
|
|
580
799
|
const t = this.db.db;
|
|
581
800
|
// lock order: the conversation, the approval, then the run (as a stop of the waiting run)
|
|
582
801
|
if (!(await this.lockConversationOf(id, orgId)))
|
|
@@ -611,7 +830,7 @@ let ApprovalsService = class ApprovalsService {
|
|
|
611
830
|
if (locked.expired) {
|
|
612
831
|
throw new ApiError("approval.already_decided", "Approval has expired");
|
|
613
832
|
}
|
|
614
|
-
// a network ask is decided only
|
|
833
|
+
// a network ask is decided only with what took effect (decideNetwork, the op's resolver)
|
|
615
834
|
if ((before.kind === "network") !== (network !== undefined)) {
|
|
616
835
|
throw new Error(`approval ${id} is of kind ${before.kind}`);
|
|
617
836
|
}
|
|
@@ -814,6 +1033,8 @@ let ApprovalsService = class ApprovalsService {
|
|
|
814
1033
|
disabledTools: def?.disabledTools ?? [],
|
|
815
1034
|
subagentTools: def?.subagentTools,
|
|
816
1035
|
subagents: def?.subagents,
|
|
1036
|
+
// the tools of the trial run's conversation, where the approval was asked
|
|
1037
|
+
source: await conversationSourceOf(t, a.conversationId),
|
|
817
1038
|
});
|
|
818
1039
|
const tool = tools.find((x) => x.key === a.toolKey);
|
|
819
1040
|
const { forbiddenTools } = await readScheduleSettings(t, this.config, a.orgId);
|
|
@@ -975,21 +1196,34 @@ let ApprovalsService = class ApprovalsService {
|
|
|
975
1196
|
return out;
|
|
976
1197
|
}
|
|
977
1198
|
/**
|
|
978
|
-
*
|
|
979
|
-
*
|
|
980
|
-
*
|
|
1199
|
+
* The conversations of `orgId` holding a pending approval `accountId` is asked to decide
|
|
1200
|
+
* (`requested_for`), read without locks: offboarding locks them, by tree root first
|
|
1201
|
+
* (`lockConversationTrees`), before {@link cancelPendingFor}.
|
|
981
1202
|
*/
|
|
982
|
-
async
|
|
983
|
-
const
|
|
984
|
-
// the conversations first, in id order (lock order: conversation, then approval rows)
|
|
985
|
-
const convs = await this.lockConversations(t
|
|
1203
|
+
async pendingConversationsFor(orgId, accountId) {
|
|
1204
|
+
const rows = await this.db
|
|
986
1205
|
.selectFrom("approvals")
|
|
987
1206
|
.select("conversation_id")
|
|
1207
|
+
.distinct()
|
|
988
1208
|
.where("org_id", "=", orgId)
|
|
989
1209
|
.where("requested_for", "=", accountId)
|
|
990
|
-
.where("status", "=", "pending")
|
|
991
|
-
|
|
992
|
-
|
|
1210
|
+
.where("status", "=", "pending")
|
|
1211
|
+
.execute();
|
|
1212
|
+
return rows.map((r) => r.conversation_id);
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* In the caller's transaction (`mandatory`; offboarding): cancels every pending approval of
|
|
1216
|
+
* `orgId` that `accountId` is asked to decide (`requested_for`) in `conversationIds`, the
|
|
1217
|
+
* conversations the caller already locked (by tree root first, `lockConversationTrees`; lock
|
|
1218
|
+
* order: conversations, then approval rows), along the same path as {@link cancelPendingIn}:
|
|
1219
|
+
* `cancelled`, `approval_decided{cancelled}` with no decider. An approval asked meanwhile in
|
|
1220
|
+
* another conversation is left to its run's stop, so no conversation is locked after a row.
|
|
1221
|
+
* Returns the cancelled records and the highest seq written per conversation; the caller
|
|
1222
|
+
* publishes after the commit.
|
|
1223
|
+
*/
|
|
1224
|
+
async cancelPendingFor(orgId, accountId, conversationIds) {
|
|
1225
|
+
const t = this.db.db;
|
|
1226
|
+
const convs = [...conversationIds];
|
|
993
1227
|
if (convs.length === 0)
|
|
994
1228
|
return { records: [], seqs: new Map() };
|
|
995
1229
|
const rows = await t
|
|
@@ -1293,10 +1527,25 @@ __decorate([
|
|
|
1293
1527
|
], ApprovalsService.prototype, "decideNetwork", null);
|
|
1294
1528
|
__decorate([
|
|
1295
1529
|
Transactional()
|
|
1296
|
-
], ApprovalsService.prototype, "
|
|
1530
|
+
], ApprovalsService.prototype, "settleOpOnly", null);
|
|
1531
|
+
__decorate([
|
|
1532
|
+
Transactional()
|
|
1533
|
+
], ApprovalsService.prototype, "settleNetworkOp", null);
|
|
1534
|
+
__decorate([
|
|
1535
|
+
Transactional({ propagation: "mandatory" })
|
|
1536
|
+
], ApprovalsService.prototype, "settleClosedIn", null);
|
|
1537
|
+
__decorate([
|
|
1538
|
+
Transactional({ propagation: "mandatory" })
|
|
1539
|
+
], ApprovalsService.prototype, "settleFromResolver", null);
|
|
1540
|
+
__decorate([
|
|
1541
|
+
Transactional({ propagation: "mandatory" })
|
|
1542
|
+
], ApprovalsService.prototype, "attemptIn", null);
|
|
1297
1543
|
__decorate([
|
|
1298
1544
|
Transactional({ propagation: "requires_new" })
|
|
1299
1545
|
], ApprovalsService.prototype, "decideIn", null);
|
|
1546
|
+
__decorate([
|
|
1547
|
+
Transactional({ propagation: "mandatory" })
|
|
1548
|
+
], ApprovalsService.prototype, "decideTx", null);
|
|
1300
1549
|
__decorate([
|
|
1301
1550
|
Transactional({ propagation: "mandatory" })
|
|
1302
1551
|
], ApprovalsService.prototype, "keepTrialGrant", null);
|
|
@@ -77,14 +77,18 @@ const notFound = (id) => new ToolError(`No background task ${clip(id, 40)} in th
|
|
|
77
77
|
*/
|
|
78
78
|
export const subagentNotTaskText = (id) => `${clip(id, 40)} is a sub-agent, not a background task. Its report arrives in this conversation as a notification when it hands back or finishes. Don't poll or sleep for it: keep working or end your turn. Use SendMessage to give it more instructions.`;
|
|
79
79
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
80
|
-
/**
|
|
81
|
-
async function
|
|
80
|
+
/** Whether `id` is the agentId of a sub-agent in the caller's tree (not a task id). */
|
|
81
|
+
async function isSubagentId(env, ctx, id) {
|
|
82
82
|
const trimmed = id.trim();
|
|
83
|
-
|
|
83
|
+
return (!TASK_ID_RE.test(id) &&
|
|
84
|
+
env.subagents !== undefined &&
|
|
84
85
|
UUID_RE.test(trimmed) &&
|
|
85
|
-
(await env.subagents.inTree(sandboxConversationOf(ctx), trimmed.toLowerCase())))
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
(await env.subagents.inTree(sandboxConversationOf(ctx), trimmed.toLowerCase())));
|
|
87
|
+
}
|
|
88
|
+
/** {@link notFound}, or {@link subagentNotTaskText} when `id` is a sub-agent of the caller's tree. */
|
|
89
|
+
async function missingTask(env, ctx, id) {
|
|
90
|
+
if (await isSubagentId(env, ctx, id))
|
|
91
|
+
return new ToolError(subagentNotTaskText(id.trim()));
|
|
88
92
|
return notFound(id);
|
|
89
93
|
}
|
|
90
94
|
/**
|
|
@@ -286,6 +290,17 @@ export function taskOutputTool(deps) {
|
|
|
286
290
|
async run(input, ctx) {
|
|
287
291
|
const { task_id, block = true, timeout } = input;
|
|
288
292
|
const env = await taskEnv(deps, ctx);
|
|
293
|
+
// A sub-agent's agentId is answered, not refused: where its report arrives is the answer
|
|
294
|
+
// (acceptance 2026-11-13: guidance the model is expected to follow is not a failed call).
|
|
295
|
+
// TaskStop keeps the error: it cannot stop a sub-agent.
|
|
296
|
+
if (await isSubagentId(env, ctx, task_id)) {
|
|
297
|
+
const id = clip(task_id.trim(), 40);
|
|
298
|
+
return {
|
|
299
|
+
content: [{ type: "text", text: subagentNotTaskText(task_id.trim()) }],
|
|
300
|
+
isError: false,
|
|
301
|
+
display: display(`查看子智能体:${id}`, `Checked sub-agent: ${id}`),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
289
304
|
let task = await ownedTask(env, ctx, task_id);
|
|
290
305
|
await recheckDevice(env, ctx);
|
|
291
306
|
if (block && task.status === "running") {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
2
|
+
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
3
|
+
import { DevicePolicyService } from "../local-devices/device-policy.service.js";
|
|
4
|
+
import type { DeviceOpResolveData } from "../local-devices/policy-op-jobs.js";
|
|
5
|
+
import { ApprovalsService } from "./approvals.service.js";
|
|
6
|
+
/**
|
|
7
|
+
* The `device-op-resolve` job (transactional outbox spec §7): records what became of a device
|
|
8
|
+
* policy op whose outcome its caller did not record (a crash between the call and the resolve
|
|
9
|
+
* transaction, an answer that never came). Owed by the op's intent for when the op can no longer
|
|
10
|
+
* reach the machine, and by a hello that reported the op (`device-op-reported-<opId>`).
|
|
11
|
+
*
|
|
12
|
+
* An op without an entry any more is resolved already: a no-op. Otherwise the outcome is the one
|
|
13
|
+
* the hello reported, else the machine's answer to a read-only `policyStatus`: `applied` →
|
|
14
|
+
* recorded applied; `refused`, never received (`unknown` to the machine, or `absent` from its
|
|
15
|
+
* hello, once the op can no longer reach it) → failed; `started` (the machine stopped between
|
|
16
|
+
* its log and its answer), once the op can no longer reach it → applied when the policy it
|
|
17
|
+
* reports holds the change, else failed (`interrupted`); no answer (`offline`) → `unknown`, left
|
|
18
|
+
* for the machine's next hello. It never sends the change
|
|
19
|
+
* again (a widening after its ask closed is not allowed). An op that answers a network ask
|
|
20
|
+
* decides it when applied and the ask is still pending, else leaves it pending with the reason
|
|
21
|
+
* (`ApprovalsService.settleFromResolver`).
|
|
22
|
+
*/
|
|
23
|
+
export declare class DeviceOpResolver {
|
|
24
|
+
private readonly db;
|
|
25
|
+
readonly txHost: TransactionHost;
|
|
26
|
+
private readonly policies;
|
|
27
|
+
private readonly approvals;
|
|
28
|
+
constructor(db: KernelDb, txHost: TransactionHost, policies: Pick<DevicePolicyService, "outcomeOf" | "settleIn">, approvals: Pick<ApprovalsService, "settleFromResolver">);
|
|
29
|
+
resolve(data: DeviceOpResolveData): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* The outcome a hello reported; a refusal's message (a hello carries none) is the machine's
|
|
32
|
+
* answer to `policyStatus` when it is connected.
|
|
33
|
+
*/
|
|
34
|
+
private reportedResult;
|
|
35
|
+
/**
|
|
36
|
+
* The resolve transaction: the op's outcome, and the network ask it answers decided or given
|
|
37
|
+
* the reason in the same transaction (`ApprovalsService.settleFromResolver`).
|
|
38
|
+
*/
|
|
39
|
+
private settle;
|
|
40
|
+
}
|