@astrosheep/keiyaku 4.0.2 → 4.0.4

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 (145) hide show
  1. package/build/integrations/marketplace/plugins/keiyaku/.claude-plugin/plugin.json +1 -1
  2. package/build/integrations/marketplace/plugins/keiyaku/.codex-plugin/plugin.json +1 -1
  3. package/build/integrations/marketplace/plugins/keiyaku/package.json +1 -1
  4. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +1 -1
  5. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +1 -1
  6. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +5 -0
  7. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-task/SKILL.md +3 -1
  8. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +9 -8
  9. package/build/src/akuma/akuma.d.ts +34 -11
  10. package/build/src/akuma/akuma.js +121 -61
  11. package/build/src/akuma/allowed.d.ts +7 -0
  12. package/build/src/akuma/allowed.js +40 -0
  13. package/build/src/akuma/archetype.d.ts +7 -4
  14. package/build/src/akuma/archetype.js +15 -16
  15. package/build/src/akuma/body.d.ts +3 -1
  16. package/build/src/akuma/body.js +19 -8
  17. package/build/src/akuma/heart/facts.d.ts +49 -7
  18. package/build/src/akuma/heart/index.d.ts +4 -8
  19. package/build/src/akuma/heart/index.js +76 -15
  20. package/build/src/akuma/heart/rows.d.ts +7 -5
  21. package/build/src/akuma/heart/rows.js +98 -11
  22. package/build/src/akuma/heart/schema.d.ts +1 -1
  23. package/build/src/akuma/heart/schema.js +15 -10
  24. package/build/src/akuma/heart/soul.js +3 -1
  25. package/build/src/akuma/heart/storage.d.ts +2 -0
  26. package/build/src/akuma/heart/storage.js +20 -0
  27. package/build/src/akuma/heart/timeline.d.ts +1 -5
  28. package/build/src/akuma/heart/timeline.js +8 -17
  29. package/build/src/akuma/index.d.ts +4 -1
  30. package/build/src/akuma/index.js +1 -0
  31. package/build/src/akuma/projection.d.ts +17 -3
  32. package/build/src/akuma/projection.js +40 -3
  33. package/build/src/akuma/provider.d.ts +1 -1
  34. package/build/src/akuma/provider.js +2 -1
  35. package/build/src/akuma/providers/acp/core.d.ts +3 -1
  36. package/build/src/akuma/providers/acp/core.js +4 -4
  37. package/build/src/akuma/providers/acp/events.d.ts +11 -3
  38. package/build/src/akuma/providers/acp/events.js +78 -4
  39. package/build/src/akuma/providers/claude/events.js +62 -3
  40. package/build/src/akuma/providers/codex-app-server/events.js +1 -13
  41. package/build/src/akuma/providers/grok-build/index.d.ts +2 -1
  42. package/build/src/akuma/providers/grok-build/index.js +72 -2
  43. package/build/src/akuma/providers/opencode-sdk/events.js +71 -6
  44. package/build/src/akuma/providers/pi/events.js +17 -3
  45. package/build/src/akuma/providers/unified-patch.d.ts +4 -0
  46. package/build/src/akuma/providers/unified-patch.js +14 -0
  47. package/build/src/akuma/publication.d.ts +1 -1
  48. package/build/src/akuma/publication.js +36 -7
  49. package/build/src/akuma/requests.d.ts +70 -4
  50. package/build/src/akuma/requests.js +288 -77
  51. package/build/src/akuma-body.d.ts +1 -0
  52. package/build/src/akuma-body.js +31 -0
  53. package/build/src/cli/accepted.d.ts +4 -1
  54. package/build/src/cli/accepted.js +26 -4
  55. package/build/src/cli/commands/akuma-invoke.d.ts +8 -7
  56. package/build/src/cli/commands/akuma-invoke.js +18 -10
  57. package/build/src/cli/commands/akuma.d.ts +11 -2
  58. package/build/src/cli/commands/akuma.js +67 -28
  59. package/build/src/cli/commands/amend.d.ts +1 -1
  60. package/build/src/cli/commands/amend.js +1 -1
  61. package/build/src/cli/commands/contract.d.ts +3 -3
  62. package/build/src/cli/commands/contract.js +1 -1
  63. package/build/src/cli/commands/install.js +4 -4
  64. package/build/src/cli/commands/task-query.d.ts +1 -1
  65. package/build/src/cli/commands/task-query.js +11 -5
  66. package/build/src/cli/commands/task.js +4 -4
  67. package/build/src/cli/coordinates.d.ts +1 -0
  68. package/build/src/cli/coordinates.js +5 -1
  69. package/build/src/cli/invoke.d.ts +1 -0
  70. package/build/src/cli/invoke.js +90 -73
  71. package/build/src/cli/main.d.ts +2 -0
  72. package/build/src/cli/main.js +29 -4
  73. package/build/src/cli/parse.d.ts +1 -0
  74. package/build/src/cli/parse.js +8 -4
  75. package/build/src/cli/render/akuma.d.ts +2 -2
  76. package/build/src/cli/render/akuma.js +84 -13
  77. package/build/src/cli/render/audit.js +16 -2
  78. package/build/src/cli/render/contract.d.ts +2 -0
  79. package/build/src/cli/render/contract.js +203 -36
  80. package/build/src/cli/render/kanshi.js +64 -29
  81. package/build/src/cli/render/receipt.d.ts +2 -1
  82. package/build/src/cli/render/receipt.js +9 -1
  83. package/build/src/cli/render/text.js +3 -1
  84. package/build/src/cli/result.d.ts +38 -3
  85. package/build/src/cli/selectors.d.ts +1 -0
  86. package/build/src/cli/selectors.js +9 -6
  87. package/build/src/cli/usage.d.ts +2 -0
  88. package/build/src/cli/usage.js +4 -0
  89. package/build/src/contract-worktree.js +3 -0
  90. package/build/src/dispatch/index.js +50 -39
  91. package/build/src/git/hooks.d.ts +0 -4
  92. package/build/src/git/hooks.js +0 -36
  93. package/build/src/git/integration.d.ts +2 -6
  94. package/build/src/git/integration.js +8 -8
  95. package/build/src/git/read-observation.js +1 -0
  96. package/build/src/git/reconcile.d.ts +6 -1
  97. package/build/src/git/reconcile.js +55 -9
  98. package/build/src/git/tender.d.ts +19 -4
  99. package/build/src/git/tender.js +44 -13
  100. package/build/src/git/terminal-seal.d.ts +14 -0
  101. package/build/src/git/terminal-seal.js +15 -2
  102. package/build/src/git/workspace.d.ts +1 -0
  103. package/build/src/git/workspace.js +12 -4
  104. package/build/src/index.d.ts +1 -1
  105. package/build/src/kanshi/index.d.ts +1 -1
  106. package/build/src/kanshi/index.js +1 -1
  107. package/build/src/kanshi/read.d.ts +9 -1
  108. package/build/src/kanshi/read.js +91 -37
  109. package/build/src/kanshi/report.d.ts +1 -0
  110. package/build/src/library/address.d.ts +5 -14
  111. package/build/src/library/address.js +20 -22
  112. package/build/src/library/akuma-creation.d.ts +4 -2
  113. package/build/src/library/akuma-creation.js +43 -26
  114. package/build/src/library/bind.js +3 -3
  115. package/build/src/library/catalog.d.ts +1 -2
  116. package/build/src/library/catalog.js +11 -6
  117. package/build/src/library/configuration.d.ts +4 -0
  118. package/build/src/library/configuration.js +19 -0
  119. package/build/src/library/contract.d.ts +15 -1
  120. package/build/src/library/contract.js +51 -4
  121. package/build/src/library/fleet.d.ts +47 -9
  122. package/build/src/library/fleet.js +184 -44
  123. package/build/src/library/input.js +1 -0
  124. package/build/src/library/keiyaku.d.ts +4 -4
  125. package/build/src/library/keiyaku.js +1 -2
  126. package/build/src/protocol/bind.d.ts +1 -2
  127. package/build/src/protocol/bind.js +2 -16
  128. package/build/src/protocol/operations.d.ts +4 -1
  129. package/build/src/protocol/operations.js +33 -27
  130. package/build/src/protocol/read/status.d.ts +3 -1
  131. package/build/src/protocol/read/status.js +24 -14
  132. package/build/src/runtime/proc/run.d.ts +1 -0
  133. package/build/src/runtime/proc/run.js +9 -2
  134. package/build/src/settlement/settle.d.ts +1 -0
  135. package/build/src/settlement/settle.js +4 -1
  136. package/build/src/task/board.d.ts +18 -3
  137. package/build/src/task/board.js +73 -26
  138. package/build/src/task/index.d.ts +3 -3
  139. package/build/src/task/index.js +7 -5
  140. package/build/src/task/operations.d.ts +2 -19
  141. package/build/src/task/operations.js +29 -7
  142. package/build/src/task/query.d.ts +9 -2
  143. package/build/src/task/query.js +32 -64
  144. package/build/src/verification/execution.js +2 -1
  145. package/package.json +3 -1
@@ -1,9 +1,11 @@
1
1
  import { readFile, readdir } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
3
4
  import { parseDocument } from "yaml";
4
5
  import { archetypeName } from "./identity.js";
5
6
  import { decodeProviderOptions, decodeProviderRecipe, } from "./provider-recipe.js";
6
7
  import { resolveProviderExecution } from "./providers/index.js";
8
+ import { effectiveAllowedActions } from "./allowed.js";
7
9
  export class AkumaArchetypeError extends Error {
8
10
  archetype;
9
11
  searched;
@@ -17,14 +19,11 @@ export class AkumaArchetypeError extends Error {
17
19
  this.name = "AkumaArchetypeError";
18
20
  }
19
21
  }
20
- function archetypeDirectory(settings) {
21
- const userPath = settings.scopes.user.path;
22
- return userPath === undefined ? null : join(dirname(userPath), "akuma");
22
+ function archetypeDirectory(home) {
23
+ return join(home ?? join(homedir(), ".keiyaku"), "akuma");
23
24
  }
24
- async function archetypePaths(settings) {
25
- const directory = archetypeDirectory(settings);
26
- if (directory === null)
27
- return [];
25
+ async function archetypePaths(home) {
26
+ const directory = archetypeDirectory(home);
28
27
  try {
29
28
  return (await readdir(directory, { withFileTypes: true }))
30
29
  .flatMap((entry) => {
@@ -46,8 +45,8 @@ async function archetypePaths(settings) {
46
45
  throw error;
47
46
  }
48
47
  }
49
- export async function listArchetypes(input) {
50
- return (await archetypePaths(input.settings)).map(({ name }) => name);
48
+ export async function listArchetypes(input = {}) {
49
+ return (await archetypePaths(input.home)).map(({ name }) => name);
51
50
  }
52
51
  function archetypeField(values, key, required = false) {
53
52
  const value = values[key];
@@ -96,6 +95,7 @@ function decodeArchetype(name, path, markdown) {
96
95
  const readonly = archetypeReadonly(values);
97
96
  const network = archetypeEnum(values, "network", ["disabled", "enabled"]);
98
97
  const description = archetypeField(values, "description");
98
+ const allowed = effectiveAllowedActions(values.allowed);
99
99
  const systemPrompt = lines.slice(closing + 1).join("\n");
100
100
  return Object.freeze({
101
101
  name,
@@ -109,12 +109,13 @@ function decodeArchetype(name, path, markdown) {
109
109
  ...(network === undefined ? {} : { network }),
110
110
  ...(systemPrompt.length === 0 ? {} : { systemPrompt }),
111
111
  }),
112
+ allowed,
112
113
  });
113
114
  }
114
- export async function listArchetypeDefinitions(input) {
115
+ export async function listArchetypeDefinitions(input = {}) {
115
116
  // Reads still run concurrently, but the reported failure is the first invalid
116
117
  // definition in catalog byte order, not whichever read happened to finish first.
117
- const settled = await Promise.allSettled((await archetypePaths(input.settings)).map(async ({ name, path }) => {
118
+ const settled = await Promise.allSettled((await archetypePaths(input.home)).map(async ({ name, path }) => {
118
119
  try {
119
120
  const definition = decodeArchetype(name, path, await readFile(path, "utf8"));
120
121
  return Object.freeze({
@@ -209,11 +210,9 @@ function admitArchetype(archetype, settings) {
209
210
  }
210
211
  export async function loadArchetype(input) {
211
212
  const name = archetypeName(input.name);
212
- const directory = archetypeDirectory(input.settings);
213
- const missing = () => new AkumaArchetypeError(name, directory === null ? [] : [join(directory, `${name}.md`)], "was not found", "use `keiyaku ls aku/` to list available Akuma");
214
- if (directory === null)
215
- throw missing();
213
+ const directory = archetypeDirectory(input.home);
216
214
  const path = join(directory, `${name}.md`);
215
+ const missing = () => new AkumaArchetypeError(name, [path], "was not found", "use `keiyaku ls aku/` to list available Akuma");
217
216
  let markdown;
218
217
  try {
219
218
  markdown = await readFile(path, "utf8");
@@ -1,7 +1,7 @@
1
1
  import { type SessionFact, type Soul } from "./heart/index.js";
2
2
  import type { AkumaPaths } from "./identity.js";
3
3
  import { type ProviderAdapter } from "./provider.js";
4
- import { type RequestChildLaunch } from "./requests.js";
4
+ import { type UpstreamExecutionPort, type RequestChildLaunch } from "./requests.js";
5
5
  export declare const CONTROL_RESPONSE_MS = 1000;
6
6
  export type BodyLaunch = Readonly<{
7
7
  paths: AkumaPaths;
@@ -12,7 +12,9 @@ export type BodyLaunch = Readonly<{
12
12
  type BodyRuntime = Readonly<{
13
13
  now(): string;
14
14
  spawnChild?(launch: RequestChildLaunch): Promise<void>;
15
+ upstream?: UpstreamExecutionPort;
15
16
  }>;
16
17
  export declare function driveAkumaBody(launch: BodyLaunch, adapter?: ProviderAdapter, runtime?: BodyRuntime): Promise<void>;
17
18
  export declare function spawnAkumaBody(launch: BodyLaunch): Promise<void>;
19
+ export declare function runAkumaBody(upstreamFor: (launch: BodyLaunch) => UpstreamExecutionPort): Promise<void>;
18
20
  export {};
@@ -185,6 +185,7 @@ async function startTurnDrive(input) {
185
185
  bodySequence: input.bodySequence,
186
186
  now: input.now,
187
187
  spawn: input.runtimeSpawn,
188
+ ...(input.upstream === undefined ? {} : { upstream: input.upstream }),
188
189
  signal: input.supervisor.signal,
189
190
  })
190
191
  : null;
@@ -538,6 +539,7 @@ async function runBodyTurns(input) {
538
539
  body: initial ?? "",
539
540
  ...(initial === undefined ? {} : { call: initial }),
540
541
  launchTells,
542
+ ...(runtime.upstream === undefined ? {} : { upstream: runtime.upstream }),
541
543
  now: runtime.now,
542
544
  });
543
545
  if (result.kind === "stopped") {
@@ -580,8 +582,16 @@ export async function driveAkumaBody(launch, adapter, runtime = defaultRuntime()
580
582
  }
581
583
  }
582
584
  catch (error) {
583
- if (await heartExists(launch.paths))
584
- throw error;
585
+ if (!await heartExists(launch.paths))
586
+ return;
587
+ try {
588
+ await leash.sealIfUnborn(launch.paths, {
589
+ evidence: error instanceof Error ? error.message : String(error),
590
+ at: runtime.now(),
591
+ });
592
+ }
593
+ catch { /* the original Body failure remains authoritative */ }
594
+ throw error;
585
595
  }
586
596
  finally {
587
597
  leash.release();
@@ -593,20 +603,21 @@ export async function spawnAkumaBody(launch) {
593
603
  if (actorId === undefined)
594
604
  throw new Error("Akuma wake has no born soul");
595
605
  const owned = await spawnDetachedProcess({
596
- argv: [process.execPath, ...process.execArgv, fileURLToPath(import.meta.url), encoded],
606
+ argv: [process.execPath, ...process.execArgv, fileURLToPath(new URL("../akuma-body.js", import.meta.url)), encoded],
597
607
  cwd: await launchCwd(launch),
598
608
  env: { ...process.env, KEIYAKU_ACTOR_ID: actorId },
599
609
  log: launch.paths.log,
600
610
  });
601
611
  owned.release();
602
612
  }
603
- async function main() {
613
+ export async function runAkumaBody(upstreamFor) {
604
614
  const encoded = process.argv[2];
605
615
  if (encoded === undefined)
606
616
  throw new TypeError("Akuma body launch payload is missing");
607
617
  const launch = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
608
- await driveAkumaBody(launch);
609
- }
610
- if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
611
- await main();
618
+ await driveAkumaBody(launch, undefined, {
619
+ now: () => new Date().toISOString(),
620
+ spawnChild: spawnAkumaBody,
621
+ upstream: upstreamFor(launch),
622
+ });
612
623
  }
@@ -1,6 +1,7 @@
1
1
  import type { AkuId } from "../identity.js";
2
2
  import type { ResumeCoordinate } from "../coordinate.js";
3
3
  import type { ProviderExecution, ProviderOptions, ReadonlyRestraint } from "../provider-recipe.js";
4
+ import type { AllowedActions } from "../allowed.js";
4
5
  export type { ResumeCoordinate } from "../coordinate.js";
5
6
  export type Confinement = Readonly<{
6
7
  kind: "unconfined";
@@ -29,9 +30,10 @@ export type Soul = Readonly<{
29
30
  cwd: string;
30
31
  origin: AkumaOrigin;
31
32
  confinement: Confinement;
33
+ allowed: AllowedActions;
32
34
  createdAt: string;
33
35
  }>;
34
- export type RequestRecipe = Pick<Soul, "description" | "provider" | "options" | "readonly" | "confinement">;
36
+ export type RequestRecipe = Pick<Soul, "description" | "provider" | "options" | "readonly" | "confinement" | "allowed">;
35
37
  export type BodyEnd = "exited" | "broke-off" | "put-down";
36
38
  export type BodyFact = Readonly<{
37
39
  sequence: number;
@@ -129,26 +131,66 @@ export type TellReceiptInput = Readonly<{
129
131
  }>);
130
132
  export type RequestInput = Readonly<{
131
133
  id: string;
134
+ action: "akuma.call";
132
135
  archetype: string;
133
136
  body: string;
134
137
  cwd?: string;
135
138
  world: string;
136
139
  recipe: RequestRecipe;
140
+ }> | Readonly<{
141
+ id: string;
142
+ action: "akuma.wait";
143
+ targets: readonly AkuId[];
144
+ completion: "any" | "all";
145
+ timeoutMs?: number;
146
+ }> | Readonly<{
147
+ id: string;
148
+ action: "akuma.tell";
149
+ target: AkuId;
150
+ body: string;
151
+ }> | Readonly<{
152
+ id: string;
153
+ action: "akuma.kill";
154
+ targets: readonly AkuId[];
137
155
  }>;
138
- export type RequestFact = RequestInput & Readonly<{
156
+ export type UpstreamRequestService = Readonly<{
157
+ action: "akuma.wait";
158
+ }> | Readonly<{
159
+ action: "akuma.tell";
160
+ target: AkuId;
161
+ tellId: string;
162
+ }> | Readonly<{
163
+ action: "akuma.kill";
164
+ results: readonly Readonly<{
165
+ id: AkuId;
166
+ evidence: KillEvidence;
167
+ }>[];
168
+ }>;
169
+ type AdmittedRequest = RequestInput & Readonly<{
170
+ requester: AkuId;
139
171
  admittedAt: string;
140
- }> & (Readonly<{
172
+ }>;
173
+ export type RequestFact = (AdmittedRequest & Readonly<{
141
174
  state: "admitted";
142
- }> | Readonly<{
175
+ }>) | (Extract<AdmittedRequest, {
176
+ action: "akuma.call";
177
+ }> & Readonly<{
143
178
  state: "reserved";
144
179
  child: AkuId;
145
- }> | Readonly<{
180
+ }>) | (Extract<AdmittedRequest, {
181
+ action: "akuma.call";
182
+ }> & Readonly<{
146
183
  state: "served";
147
184
  child: AkuId;
148
- }> | Readonly<{
185
+ }>) | (Exclude<AdmittedRequest, {
186
+ action: "akuma.call";
187
+ }> & Readonly<{
188
+ state: "served";
189
+ service: UpstreamRequestService;
190
+ }>) | (AdmittedRequest & Readonly<{
149
191
  state: "refused";
150
192
  diagnostic: string;
151
- }> | Readonly<{
193
+ }>) | (AdmittedRequest & Readonly<{
152
194
  state: "voided";
153
195
  evidence: string;
154
196
  }>);
@@ -1,11 +1,11 @@
1
1
  import type { AkuId, AkumaPaths } from "../identity.js";
2
- import type { BodyEnd, BodyFact, ForkPoint, HeartSnapshot, KillFact, RequestFact, RequestInput, SealFact, SessionFact, Soul, TellDeliveryInput, TellFact, TellReceiptInput, TurnEndFact, TurnFact, TurnOutcome, TurnStartFact } from "./facts.js";
2
+ import type { BodyEnd, BodyFact, ForkPoint, HeartSnapshot, KillFact, RequestFact, RequestInput, UpstreamRequestService, SealFact, SessionFact, Soul, TellDeliveryInput, TellFact, TellReceiptInput, TurnEndFact, TurnFact, TurnOutcome, TurnStartFact } from "./facts.js";
3
3
  export type { CallFact } from "./facts.js";
4
4
  import type { ActivityFact } from "./rows.js";
5
5
  import { type ActivityFactSlice } from "./timeline.js";
6
6
  export { HeartAbsentError, HeldAkumaLeash, initializeHeart, isHeartAbsent, probeLeash } from "./storage.js";
7
7
  export { life, lifeAt } from "./facts.js";
8
- export type { AkumaLife, AkumaOrigin, BodyEnd, BodyFact, Confinement, HeartSnapshot, ForkPoint, KillEvidence, KillFact, LeashProbe, PauseFact, RequestFact, RequestInput, RequestRecipe, SealFact, SessionFact, Soul, StopFact, TellFact, TellDelivery, TellDeliveryInput, TellReceiptInput, TurnEndFact, TurnFact, TurnOutcome, TurnStartFact, } from "./facts.js";
8
+ export type { AkumaLife, AkumaOrigin, BodyEnd, BodyFact, Confinement, HeartSnapshot, ForkPoint, KillEvidence, KillFact, LeashProbe, PauseFact, RequestFact, RequestInput, RequestRecipe, UpstreamRequestService, SealFact, SessionFact, Soul, StopFact, TellFact, TellDelivery, TellDeliveryInput, TellReceiptInput, TurnEndFact, TurnFact, TurnOutcome, TurnStartFact, } from "./facts.js";
9
9
  export type { ResumeCoordinate } from "../coordinate.js";
10
10
  export declare function readSoul(paths: AkumaPaths): Promise<Soul | null>;
11
11
  export declare function heartExists(paths: AkumaPaths): Promise<boolean>;
@@ -16,15 +16,10 @@ export declare function appendActivity(paths: AkumaPaths, input: Readonly<{
16
16
  event: unknown;
17
17
  at: string;
18
18
  }>): Promise<number>;
19
- export type ActivitySliceInput = Readonly<{
20
- before?: number;
21
- since?: number;
22
- limit?: number;
23
- }>;
24
19
  export type ActivitySlice = ActivityFactSlice;
25
20
  export type { ActivityFact };
26
21
  export type { TimelineFact } from "./timeline.js";
27
- export declare function activitySlice(paths: AkumaPaths, input?: ActivitySliceInput): Promise<ActivitySlice>;
22
+ export declare function activitySlice(paths: AkumaPaths): Promise<ActivitySlice>;
28
23
  export declare function recordTell(paths: AkumaPaths, tell: Omit<TellFact, "sequence" | "state" | "deliveries">): Promise<Readonly<{
29
24
  kind: "not-born";
30
25
  } | {
@@ -38,6 +33,7 @@ export declare function admitRequest(paths: AkumaPaths, input: RequestInput & Re
38
33
  }>): Promise<RequestFact>;
39
34
  export declare function reserveRequest(paths: AkumaPaths, id: string, child: AkuId): Promise<RequestFact>;
40
35
  export declare function serveRequest(paths: AkumaPaths, id: string, child: AkuId): Promise<RequestFact>;
36
+ export declare function serveUpstreamRequest(paths: AkumaPaths, id: string, service: UpstreamRequestService): Promise<RequestFact>;
41
37
  export declare function refuseRequest(paths: AkumaPaths, id: string, diagnostic: string): Promise<RequestFact>;
42
38
  export declare function voidRequest(paths: AkumaPaths, id: string, evidence: string): Promise<RequestFact>;
43
39
  export declare function readRequest(paths: AkumaPaths, id: string): Promise<RequestFact | null>;
@@ -1,8 +1,9 @@
1
- import { answeredTurnFact, endBodyFact, finishBodyFact, insertActivityFact, insertTurnEndFact, insertTurnStartFact, insertPauseControl, insertRequestFact, insertSessionFact, insertStopControl, killFactForBody, latestBodyFact, latestKillFact, latestSessionFact, lastAnsweredTurnFact, nonterminalRequestFacts, pauseFact, requestFact, sessionFactForCoordinate, stopFact, updateRequestRefused, updateRequestReserved, updateRequestServed, updateRequestVoided, } from "./rows.js";
1
+ import { answeredTurnFact, endBodyFact, finishBodyFact, insertActivityFact, insertTurnEndFact, insertTurnStartFact, insertPauseControl, insertRequestFact, insertSessionFact, insertStopControl, killFactForBody, latestBodyFact, latestKillFact, latestSessionFact, lastAnsweredTurnFact, nonterminalRequestFacts, pauseFact, requestFact, sessionFactForCoordinate, stopFact, updateRequestRefused, updateRequestReserved, updateRequestServed, updateRequestVoided, updateUpstreamRequestServed, } from "./rows.js";
2
2
  import { insertTellDeliveryFact, insertTellFact, insertTellReceiptFact, pendingTellFacts, tellFact, tellIdsForFence, } from "./tells.js";
3
3
  import { activityFactSlice, pruneActivityFacts, } from "./timeline.js";
4
- import { isHeartAbsent, readSealFromLeash, transaction, withHeart } from "./storage.js";
4
+ import { isHeartAbsent, readSealFromLeash, readTransaction, transaction, withHeart } from "./storage.js";
5
5
  import { soulFact } from "./soul.js";
6
+ import { clipAllowedActions } from "../allowed.js";
6
7
  export { HeartAbsentError, HeldAkumaLeash, initializeHeart, isHeartAbsent, probeLeash } from "./storage.js";
7
8
  export { life, lifeAt } from "./facts.js";
8
9
  const ACTIVITY_LIMIT = 5_000;
@@ -46,14 +47,20 @@ export async function appendActivity(paths, input) {
46
47
  return sequence;
47
48
  }));
48
49
  }
49
- export async function activitySlice(paths, input = {}) {
50
- const limit = input.limit ?? ACTIVITY_LIMIT;
51
- return await withHeart(paths, (heart) => activityFactSlice(heart, { ...input, limit }));
50
+ export async function activitySlice(paths) {
51
+ return await withHeart(paths, (heart) => readTransaction(heart, () => activityFactSlice(heart)));
52
52
  }
53
53
  export async function recordTell(paths, tell) {
54
54
  return await withHeart(paths, (heart) => transaction(heart, () => {
55
55
  if (soulFact(heart) === null)
56
56
  return { kind: "not-born" };
57
+ const existing = tellFact(heart, tell.id);
58
+ if (existing !== null) {
59
+ if (existing.body !== tell.body || existing.recordedAt !== tell.recordedAt) {
60
+ throw new Error(`tell ${tell.id} reused different input`);
61
+ }
62
+ return { kind: "recorded", tell: existing };
63
+ }
57
64
  const sequence = insertTellFact(heart, tell);
58
65
  pruneActivityFacts(heart, ACTIVITY_LIMIT);
59
66
  return { kind: "recorded", tell: { sequence, ...tell, state: "pending", deliveries: [] } };
@@ -83,21 +90,55 @@ export async function recordTellReceipt(paths, input) {
83
90
  }));
84
91
  }
85
92
  function sameRequestInput(fact, input) {
86
- return fact.id === input.id
87
- && fact.archetype === input.archetype
88
- && fact.body === input.body
89
- && fact.cwd === input.cwd
90
- && fact.world === input.world
91
- && JSON.stringify(fact.recipe) === JSON.stringify(input.recipe);
93
+ if (fact.id !== input.id || fact.action !== input.action)
94
+ return false;
95
+ if (fact.action === "akuma.call" && input.action === "akuma.call") {
96
+ return fact.archetype === input.archetype
97
+ && fact.body === input.body
98
+ && fact.cwd === input.cwd
99
+ && fact.world === input.world
100
+ && JSON.stringify(fact.recipe) === JSON.stringify(input.recipe);
101
+ }
102
+ if (fact.action === "akuma.wait" && input.action === "akuma.wait") {
103
+ return fact.completion === input.completion
104
+ && fact.timeoutMs === input.timeoutMs
105
+ && JSON.stringify(fact.targets) === JSON.stringify(input.targets);
106
+ }
107
+ if (fact.action === "akuma.tell" && input.action === "akuma.tell") {
108
+ return fact.target === input.target && fact.body === input.body;
109
+ }
110
+ return fact.action === "akuma.kill" && input.action === "akuma.kill"
111
+ && JSON.stringify(fact.targets) === JSON.stringify(input.targets);
92
112
  }
93
113
  export async function admitRequest(paths, input) {
94
114
  return await withHeart(paths, (heart) => transaction(heart, () => {
95
- insertRequestFact(heart, input);
115
+ const soul = soulFact(heart);
116
+ if (soul === null)
117
+ throw new Error("Akuma request admission requires a born Soul");
118
+ const action = input.action;
119
+ const parentAllowed = soul.allowed;
120
+ const permitted = action === "akuma.wait" || parentAllowed.includes(action);
121
+ const normalized = input.action === "akuma.call"
122
+ ? {
123
+ ...input,
124
+ recipe: {
125
+ ...input.recipe,
126
+ allowed: permitted ? clipAllowedActions(input.recipe.allowed, parentAllowed) : input.recipe.allowed,
127
+ },
128
+ }
129
+ : input;
130
+ insertRequestFact(heart, {
131
+ ...normalized,
132
+ requester: soul.id,
133
+ admittedAt: input.admittedAt,
134
+ ...(permitted ? {} : { refusal: `not-allowed: ${action}` }),
135
+ });
96
136
  const fact = requestFact(heart, input.id);
97
137
  if (fact === null)
98
138
  throw new Error(`Akuma request ${input.id} was not admitted`);
99
- if (!sameRequestInput(fact, input))
139
+ if (fact.requester !== soul.id || !sameRequestInput(fact, normalized)) {
100
140
  throw new Error(`Akuma request ${input.id} reused different input`);
141
+ }
101
142
  return fact;
102
143
  }));
103
144
  }
@@ -106,6 +147,8 @@ export async function reserveRequest(paths, id, child) {
106
147
  const before = requestFact(heart, id);
107
148
  if (before === null)
108
149
  throw new Error(`unknown Akuma request ${id}`);
150
+ if (before.action !== "akuma.call")
151
+ throw new Error(`Akuma request ${id} cannot reserve a child`);
109
152
  if (before.state === "admitted")
110
153
  updateRequestReserved(heart, id, child);
111
154
  else if ((before.state !== "reserved" && before.state !== "served") || before.child !== child) {
@@ -119,6 +162,8 @@ export async function serveRequest(paths, id, child) {
119
162
  const before = requestFact(heart, id);
120
163
  if (before === null)
121
164
  throw new Error(`unknown Akuma request ${id}`);
165
+ if (before.action !== "akuma.call")
166
+ throw new Error(`Akuma request ${id} cannot serve a child`);
122
167
  if (before.state === "reserved" && before.child === child)
123
168
  updateRequestServed(heart, id, child);
124
169
  else if (before.state !== "served" || before.child !== child) {
@@ -127,6 +172,22 @@ export async function serveRequest(paths, id, child) {
127
172
  return requestFact(heart, id);
128
173
  }));
129
174
  }
175
+ export async function serveUpstreamRequest(paths, id, service) {
176
+ return await withHeart(paths, (heart) => transaction(heart, () => {
177
+ const before = requestFact(heart, id);
178
+ if (before === null)
179
+ throw new Error(`unknown Akuma request ${id}`);
180
+ if (before.action === "akuma.call" || service.action !== before.action) {
181
+ throw new Error(`Akuma request ${id} has a mismatched service reference`);
182
+ }
183
+ if (before.state === "admitted")
184
+ updateUpstreamRequestServed(heart, id, service);
185
+ else if (before.state !== "served" || JSON.stringify(before.service) !== JSON.stringify(service)) {
186
+ throw new Error(`Akuma request ${id} cannot be served as ${service.action}`);
187
+ }
188
+ return requestFact(heart, id);
189
+ }));
190
+ }
130
191
  export async function refuseRequest(paths, id, diagnostic) {
131
192
  return await withHeart(paths, (heart) => transaction(heart, () => {
132
193
  const before = requestFact(heart, id);
@@ -234,7 +295,7 @@ export async function finishBodyIfIdle(paths, input) {
234
295
  }
235
296
  export async function readHeart(paths) {
236
297
  try {
237
- return await withHeart(paths, (heart) => ({
298
+ return await withHeart(paths, (heart) => readTransaction(heart, () => ({
238
299
  soul: soulFact(heart),
239
300
  latestBody: latestBodyFact(heart),
240
301
  latestSession: latestSessionFact(heart),
@@ -242,7 +303,7 @@ export async function readHeart(paths) {
242
303
  latestKill: latestKillFact(heart),
243
304
  stop: stopFact(heart),
244
305
  pause: pauseFact(heart),
245
- }));
306
+ })));
246
307
  }
247
308
  catch (error) {
248
309
  if (isHeartAbsent(error)) {
@@ -42,14 +42,13 @@ export type CallRow = Readonly<{
42
42
  export type RequestRow = Readonly<{
43
43
  sequence: number;
44
44
  id: string;
45
- archetype: string;
46
- body: string;
47
- cwd: string | null;
48
- world: string;
49
- recipe_json: string;
45
+ requester: string;
46
+ action: string;
47
+ payload_json: string;
50
48
  admitted_at: string;
51
49
  state: RequestFact["state"];
52
50
  child: string | null;
51
+ service_json: string | null;
53
52
  diagnostic: string | null;
54
53
  evidence: string | null;
55
54
  }>;
@@ -102,12 +101,15 @@ export declare function insertActivityFact(database: DatabaseSync, input: Readon
102
101
  at: string;
103
102
  }>): number;
104
103
  export declare function insertRequestFact(database: DatabaseSync, input: RequestInput & Readonly<{
104
+ requester: AkuId;
105
105
  admittedAt: string;
106
+ refusal?: string;
106
107
  }>): void;
107
108
  export declare function requestFact(database: DatabaseSync, id: string): RequestFact | null;
108
109
  export declare function nonterminalRequestFacts(database: DatabaseSync): readonly RequestFact[];
109
110
  export declare function updateRequestReserved(database: DatabaseSync, id: string, child: AkuId): void;
110
111
  export declare function updateRequestServed(database: DatabaseSync, id: string, child: AkuId): void;
112
+ export declare function updateUpstreamRequestServed(database: DatabaseSync, id: string, service: unknown): void;
111
113
  export declare function updateRequestRefused(database: DatabaseSync, id: string, diagnostic: string): void;
112
114
  export declare function updateRequestVoided(database: DatabaseSync, id: string, evidence: string): void;
113
115
  export declare function insertStopControl(database: DatabaseSync, bodySequence: number, at: string): void;
@@ -7,6 +7,66 @@ function parsed(value) {
7
7
  throw new Error("Akuma authority contains non-text JSON");
8
8
  return JSON.parse(value);
9
9
  }
10
+ function object(value) {
11
+ return value !== null && typeof value === "object" && !Array.isArray(value)
12
+ ? value
13
+ : null;
14
+ }
15
+ function exactKeys(value, expected) {
16
+ const actual = Object.keys(value).sort();
17
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
18
+ }
19
+ const KILL_EVIDENCE = [
20
+ "already-killed",
21
+ "already-stopped",
22
+ "hung",
23
+ "killed",
24
+ "untidy",
25
+ "unavailable",
26
+ ];
27
+ function decodeRequestService(value, input) {
28
+ const service = object(value);
29
+ if (service === null || service.action !== input.action) {
30
+ throw new Error("Akuma authority contains a mismatched request service reference");
31
+ }
32
+ if (input.action === "akuma.wait") {
33
+ if (!exactKeys(service, ["action"])) {
34
+ throw new Error("Akuma authority contains an invalid wait service reference");
35
+ }
36
+ return { action: input.action };
37
+ }
38
+ if (input.action === "akuma.tell") {
39
+ if (!exactKeys(service, ["action", "target", "tellId"])
40
+ || service.target !== input.target
41
+ || service.tellId !== input.id) {
42
+ throw new Error("Akuma authority contains an invalid tell service reference");
43
+ }
44
+ return { action: input.action, target: input.target, tellId: input.id };
45
+ }
46
+ if (!exactKeys(service, ["action", "results"]) || !Array.isArray(service.results)) {
47
+ throw new Error("Akuma authority contains an invalid kill service reference");
48
+ }
49
+ let targetIndex = -1;
50
+ const results = service.results.map((value) => {
51
+ const result = object(value);
52
+ if (result === null
53
+ || !exactKeys(result, ["evidence", "id"])
54
+ || typeof result.id !== "string"
55
+ || !KILL_EVIDENCE.includes(result.evidence)) {
56
+ throw new Error("Akuma authority contains an invalid kill service reference");
57
+ }
58
+ const index = input.targets.indexOf(result.id);
59
+ if (index <= targetIndex) {
60
+ throw new Error("Akuma authority contains an invalid kill service reference");
61
+ }
62
+ targetIndex = index;
63
+ return {
64
+ id: result.id,
65
+ evidence: result.evidence,
66
+ };
67
+ });
68
+ return { action: input.action, results };
69
+ }
10
70
  function resumeCoordinate(value) {
11
71
  const coordinate = decodeResumeCoordinate(value);
12
72
  if (coordinate === null)
@@ -67,18 +127,39 @@ export function decodeCallRow(row) {
67
127
  return { kind: "call", sequence: row.sequence, turnSequence: row.turn_sequence, body: row.body, at: row.at };
68
128
  }
69
129
  export function decodeRequestRow(row) {
130
+ if (!["akuma.call", "akuma.wait", "akuma.tell", "akuma.kill"].includes(row.action)) {
131
+ throw new Error(`Akuma authority contains an unknown request action: ${row.action}`);
132
+ }
133
+ const payload = parsed(row.payload_json);
70
134
  const input = {
71
135
  id: row.id,
72
- archetype: row.archetype,
73
- body: row.body,
74
- ...(row.cwd === null ? {} : { cwd: row.cwd }),
75
- world: row.world,
76
- recipe: parsed(row.recipe_json),
136
+ requester: row.requester,
137
+ action: row.action,
138
+ ...payload,
77
139
  admittedAt: row.admitted_at,
78
140
  };
79
- if (row.state === "reserved" || row.state === "served") {
141
+ if (row.state === "reserved") {
142
+ if (input.action !== "akuma.call" || row.child === null) {
143
+ throw new Error("Akuma authority contains an invalid reserved request");
144
+ }
80
145
  return { ...input, state: row.state, child: row.child };
81
146
  }
147
+ if (row.state === "served") {
148
+ if (input.action === "akuma.call") {
149
+ if (row.child === null)
150
+ throw new Error("Akuma authority contains a served call without a child");
151
+ return { ...input, state: row.state, child: row.child };
152
+ }
153
+ if (row.service_json === null) {
154
+ throw new Error("Akuma authority contains a served request without a service reference");
155
+ }
156
+ const service = decodeRequestService(parsed(row.service_json), input);
157
+ return {
158
+ ...input,
159
+ state: row.state,
160
+ service,
161
+ };
162
+ }
82
163
  if (row.state === "refused")
83
164
  return { ...input, state: row.state, diagnostic: row.diagnostic };
84
165
  if (row.state === "voided")
@@ -129,11 +210,13 @@ export function insertActivityFact(database, input) {
129
210
  .run(sequence, input.turnSequence, encodeActivityEvent(input.event), input.at);
130
211
  return sequence;
131
212
  }
132
- const REQUEST_COLUMNS = `sequence, id, archetype, body, cwd, world, recipe_json, admitted_at,
133
- state, child, diagnostic, evidence`;
213
+ const REQUEST_COLUMNS = `sequence, id, requester, action, payload_json, admitted_at,
214
+ state, child, service_json, diagnostic, evidence`;
134
215
  export function insertRequestFact(database, input) {
135
- database.prepare(`INSERT OR IGNORE INTO requests(id, archetype, body, cwd, world, recipe_json, admitted_at, state)
136
- VALUES (?, ?, ?, ?, ?, ?, ?, 'admitted')`).run(input.id, input.archetype, input.body, input.cwd ?? null, input.world, json(input.recipe), input.admittedAt);
216
+ const { id, action, requester, admittedAt, refusal, ...payload } = input;
217
+ database.prepare(`INSERT OR IGNORE INTO requests(
218
+ id, requester, action, payload_json, admitted_at, state, diagnostic
219
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, requester, action, json(payload), admittedAt, refusal === undefined ? "admitted" : "refused", refusal ?? null);
137
220
  }
138
221
  export function requestFact(database, id) {
139
222
  const row = database.prepare(`SELECT ${REQUEST_COLUMNS} FROM requests WHERE id = ?`).get(id);
@@ -152,12 +235,16 @@ export function updateRequestServed(database, id, child) {
152
235
  database.prepare(`UPDATE requests SET state = 'served', child = ?
153
236
  WHERE id = ? AND state = 'reserved'`).run(child, id);
154
237
  }
238
+ export function updateUpstreamRequestServed(database, id, service) {
239
+ database.prepare(`UPDATE requests SET state = 'served', service_json = ?
240
+ WHERE id = ? AND state = 'admitted' AND action != 'akuma.call'`).run(json(service), id);
241
+ }
155
242
  export function updateRequestRefused(database, id, diagnostic) {
156
243
  database.prepare(`UPDATE requests SET state = 'refused', diagnostic = ?
157
244
  WHERE id = ? AND state = 'admitted'`).run(diagnostic, id);
158
245
  }
159
246
  export function updateRequestVoided(database, id, evidence) {
160
- database.prepare(`UPDATE requests SET state = 'voided', child = NULL, evidence = ?
247
+ database.prepare(`UPDATE requests SET state = 'voided', child = NULL, service_json = NULL, evidence = ?
161
248
  WHERE id = ? AND state IN ('admitted', 'reserved')`).run(evidence, id);
162
249
  }
163
250
  export function insertStopControl(database, bodySequence, at) {