@agentshouse/kit 0.1.0-alpha.26 → 0.1.0-alpha.28

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/src/worker.js CHANGED
@@ -20,7 +20,9 @@ import { localRequest, reconcileInbound } from "./inbound.js";
20
20
  import { localFiles } from "./local-files.js";
21
21
  import { answerText, answerYaml, editRefusal } from "./mcp.js";
22
22
  import { inheritedEnvironment } from "./command.js";
23
+ import { sourceEntries } from "./google/entries.js";
23
24
  import { watchKitWork } from "./kit-work-stream.js";
25
+ import { coalescedTask, residentWake } from "./tasks.js";
24
26
  import {
25
27
  applyWorkingCopies,
26
28
  deliverWorkingCopies,
@@ -38,10 +40,8 @@ import {
38
40
  MCP_REQUEST_BODY_MAX_BYTES,
39
41
  PROVIDER_CONTINUATION_UNAVAILABLE,
40
42
  WORKER_COMMAND_BATCH_MAX,
41
- WORKER_COMMAND_POLL_MS,
42
43
  WORKER_EXIT_GRACE_MS,
43
44
  WORKER_EXIT_POLL_MS,
44
- WORKER_HEARTBEAT_MS,
45
45
  WORKER_INHERITED_ENVIRONMENT,
46
46
  WORKER_SUPERVISION_POLL_MS,
47
47
  WORKING_COPY_INDEX_METHOD,
@@ -74,7 +74,6 @@ import {
74
74
  reportInteractionOutcome,
75
75
  toolMutations,
76
76
  reportRunOutcome,
77
- reportSessionHeartbeat,
78
77
  releaseControl,
79
78
  reportSessions,
80
79
  serveResident,
@@ -99,6 +98,7 @@ import {
99
98
 
100
99
  const LAUNCHER_MAIN = fileURLToPath(new URL("launcher-main.js", import.meta.url));
101
100
  const RESIDENT_INTERVAL_MS = 30_000;
101
+ const PASS_REACHES = ["commands", "runs"];
102
102
 
103
103
  async function dispatchSeal(context, bound) {
104
104
  try {
@@ -132,9 +132,7 @@ async function pendingCommands(context, kinds = ["stop"]) {
132
132
  try {
133
133
  return await acquireCommands(context.session, WORKER_COMMAND_BATCH_MAX, kinds);
134
134
  } catch (refusal) {
135
- if (houseFailed(refusal) || context.session.refused === true) {
136
- return { stops: [], answers: [], interrupts: [] };
137
- }
135
+ if (houseFailed(refusal) || context.session.refused === true) return null;
138
136
  throw refusal;
139
137
  }
140
138
  }
@@ -155,14 +153,6 @@ async function settleInterrupt(context, interrupt, outcome) {
155
153
  }
156
154
  }
157
155
 
158
- async function reportHeartbeat(context, bound) {
159
- try {
160
- await reportSessionHeartbeat(context.session, bound.session);
161
- } catch (refusal) {
162
- if (!(refusal instanceof KitRefusal)) throw refusal;
163
- }
164
- }
165
-
166
156
  function retryFiles(context) {
167
157
  if (context.retrying !== undefined) return;
168
158
  context.retrying = retryConversationFiles(context)
@@ -185,40 +175,32 @@ async function settleFiles(context) {
185
175
  }
186
176
 
187
177
  async function supervise(context, bound, bridge) {
188
- const terminal = bridge.departed;
189
- let live = true;
190
- terminal.then(() => {
191
- live = false;
192
- });
193
- let commanded = Date.now() + WORKER_COMMAND_POLL_MS;
194
- let pulsed = Date.now() + WORKER_HEARTBEAT_MS;
195
- while (live) {
196
- await Promise.race([terminal, delay(WORKER_SUPERVISION_POLL_MS)]);
197
- if (!live) return null;
178
+ const departed = new AbortController();
179
+ bridge.departed.then(() => departed.abort());
180
+ const waiting = AbortSignal.any([context.signal, departed.signal]);
181
+ let owed = false;
182
+ for (;;) {
183
+ const commanded = (await context.commands.wait(WORKER_SUPERVISION_POLL_MS, waiting)) || owed;
184
+ if (departed.signal.aborted) return null;
185
+ if (context.signal.aborted) throw new KitRefusal("kit_command_cancelled");
198
186
  await context.update?.announce();
199
- if (Date.now() >= commanded) {
200
- commanded = Date.now() + WORKER_COMMAND_POLL_MS;
187
+ if (commanded) {
201
188
  const polled = await pendingCommands(context, ["stop", "interrupt", "answer"]);
202
- for (const answer of polled.answers) {
189
+ owed = polled === null && context.session.refused !== true;
190
+ for (const answer of polled?.answers ?? []) {
203
191
  if (answer.session === bound.session) bridge.answered(answer);
204
192
  }
205
- for (const interrupt of polled.interrupts) {
193
+ for (const interrupt of polled?.interrupts ?? []) {
206
194
  if (interrupt.session !== bound.session || interrupt.run !== bound.run) continue;
207
195
  if (!bridge.interrupt(interrupt)) {
208
196
  await settleInterrupt(context, interrupt.interrupt, "nothing_to_interrupt");
209
197
  }
210
198
  }
211
- const stop = polled.stops.find((command) => command.run === bound.run);
199
+ const stop = polled?.stops.find((command) => command.run === bound.run);
212
200
  if (stop !== undefined) return stop;
213
201
  }
214
- if (Date.now() >= pulsed) {
215
- pulsed = Date.now() + WORKER_HEARTBEAT_MS;
216
- retryFiles(context);
217
- await reportHeartbeat(context, bound);
218
- }
219
202
  if (!(await sessionPresent(context.runtime, bound.tmux))) return null;
220
203
  }
221
- return null;
222
204
  }
223
205
 
224
206
  async function grantExitGrace(runtime, handle) {
@@ -430,6 +412,7 @@ async function superviseBound(context, bound, opened, start) {
430
412
  } finally {
431
413
  try {
432
414
  try {
415
+ retryFiles(context);
433
416
  await settleFiles(context);
434
417
  for (const interaction of bridge.unsettledInteractions()) {
435
418
  await settleInteraction(context, interaction, "expired");
@@ -725,7 +708,7 @@ async function reconcile(context) {
725
708
  }
726
709
  const bindings = await readBindings(context.directory);
727
710
  if (bindings.length === 0) return { run: null, stopped, unresolved };
728
- const commands = (await pendingCommands(context)).stops;
711
+ const commands = (await pendingCommands(context))?.stops ?? [];
729
712
  const carried = [];
730
713
  for (const binding of bindings) {
731
714
  if (binding.reported !== null) {
@@ -809,7 +792,7 @@ function unlessDraining(context, act) {
809
792
  return context.update?.requested ? null : act();
810
793
  }
811
794
 
812
- async function workerPass(context, dependencies, baseConfiguration) {
795
+ async function workerPass(context, dependencies, baseConfiguration, reach) {
813
796
  const { session, signal } = context;
814
797
  const reconciled = await reconcile(context);
815
798
  if (reconciled.run !== null || reconciled.unresolved.length > 0 || context.update?.requested) {
@@ -840,6 +823,7 @@ async function workerPass(context, dependencies, baseConfiguration) {
840
823
  }
841
824
  const resumption = await unlessDraining(context, () => acquireOneResumption(session));
842
825
  if (resumption !== null) return { ...reconciled, run: await superviseResumption(context, resumption) };
826
+ if (reach !== "runs") return reconciled;
843
827
  const assignment = await unlessDraining(context, () => acquireOneRun(session));
844
828
  if (assignment === null) return reconciled;
845
829
  return { ...reconciled, run: await superviseAssignment(context, assignment) };
@@ -863,6 +847,7 @@ async function workerContext(options, dependencies, resident = false) {
863
847
  directory: options.directory,
864
848
  home: dependencies.home ?? homedir(),
865
849
  files: { originals: [], failures: [] },
850
+ commands: residentWake(),
866
851
  epochReport: null,
867
852
  agentRefresh: Promise.resolve(),
868
853
  configurationPath: options.configuration,
@@ -907,7 +892,7 @@ function queueAgentRefresh(context, dependencies, signal, configuration, pointer
907
892
  return refreshed;
908
893
  }
909
894
 
910
- async function controlledPass(context, dependencies, configuration, signal) {
895
+ async function controlledPass(context, dependencies, configuration, signal, reach = "runs") {
911
896
  context.signal = signal;
912
897
  context.session.signal = signal;
913
898
  await reconcileSkillSet({
@@ -917,7 +902,7 @@ async function controlledPass(context, dependencies, configuration, signal) {
917
902
  });
918
903
  context.busy = true;
919
904
  try {
920
- return await afterEpochReport(context, () => workerPass(context, dependencies, configuration));
905
+ return await afterEpochReport(context, () => workerPass(context, dependencies, configuration, reach));
921
906
  } finally {
922
907
  context.busy = false;
923
908
  if (context.deferredRemoval) context.onIdle?.();
@@ -928,8 +913,23 @@ export async function runWorkers(options, dependencies = {}) {
928
913
  const { context, tmux } = await workerContext(options, dependencies);
929
914
  const { session, runtime } = context;
930
915
  const supervised = await maintainControl(session, async (signal) => {
931
- await reportedWorkingCopies(applyWorkingCopies, context, signal);
932
- return controlledPass(context, dependencies, context.configuration, signal);
916
+ const watcher = new AbortController();
917
+ const commanded = (subject) => {
918
+ if (subject === null || subject === "commands") context.commands.notify();
919
+ };
920
+ const watching = (dependencies.watchKitWork ?? watchKitWork)(
921
+ session,
922
+ AbortSignal.any([signal, watcher.signal]),
923
+ { available: commanded, changed: () => {} },
924
+ dependencies,
925
+ ).catch((error) => reported(`control stream ended: ${error?.code ?? error?.message ?? error}`));
926
+ try {
927
+ await reportedWorkingCopies(applyWorkingCopies, context, signal);
928
+ return await controlledPass(context, dependencies, context.configuration, signal);
929
+ } finally {
930
+ watcher.abort();
931
+ await watching;
932
+ }
933
933
  }, session.signal);
934
934
  return {
935
935
  tmux,
@@ -950,65 +950,11 @@ function reported(line) {
950
950
  }
951
951
 
952
952
  async function reportedWorkingCopies(act, context, signal) {
953
- for (const refused of (await act(context, signal)).refused) {
953
+ const outcome = await act(context, signal);
954
+ for (const refused of outcome.refused) {
954
955
  reported(`working copy ${refused.path} refused: ${refused.code}`);
955
956
  }
956
- }
957
-
958
- function coalescedTask(signal, fail, work) {
959
- let pending = false;
960
- let task = null;
961
- const trigger = () => {
962
- pending = true;
963
- if (task !== null) return;
964
- task = (async () => {
965
- while (pending && !signal.aborted) {
966
- pending = false;
967
- await work();
968
- }
969
- })()
970
- .catch((error) => {
971
- if (signal.aborted) return;
972
- fail(error);
973
- })
974
- .finally(() => {
975
- task = null;
976
- if (pending && !signal.aborted) trigger();
977
- });
978
- };
979
- return { trigger, idle: () => task === null, settled: () => task };
980
- }
981
-
982
- function residentWake() {
983
- let pending = false;
984
- let release = null;
985
- return {
986
- notify() {
987
- pending = true;
988
- release?.();
989
- },
990
- async wait(milliseconds, signal) {
991
- if (pending) {
992
- pending = false;
993
- return;
994
- }
995
- const pause = new AbortController();
996
- try {
997
- await Promise.race([
998
- new Promise((resolve) => {
999
- release = resolve;
1000
- }),
1001
- delay(milliseconds, undefined, { signal: AbortSignal.any([signal, pause.signal]) }),
1002
- ]);
1003
- } catch (error) {
1004
- if (!signal.aborted) throw error;
1005
- } finally {
1006
- release = null;
1007
- pending = false;
1008
- pause.abort();
1009
- }
1010
- },
1011
- };
957
+ return outcome;
1012
958
  }
1013
959
 
1014
960
  function residentUpdate(context, wake) {
@@ -1078,6 +1024,7 @@ export async function runResident(options, dependencies = {}) {
1078
1024
  const wake = residentWake();
1079
1025
  const update = residentUpdate(context, wake);
1080
1026
  context.update = update;
1027
+ let sources = null;
1081
1028
  const served = await serveResident(
1082
1029
  session,
1083
1030
  context.directory,
@@ -1087,6 +1034,8 @@ export async function runResident(options, dependencies = {}) {
1087
1034
  pushWorkingCopy({ directory: context.directory, session }, request, ownerCaller(session), signal).catch(
1088
1035
  (error) => ({ refused: true, text: formatCommandError(error) }),
1089
1036
  ),
1037
+ (source) =>
1038
+ sources === null ? Promise.reject(new KitRefusal("kit_resident_unavailable")) : sources.capture(source),
1090
1039
  );
1091
1040
  const serve = async (signal) => {
1092
1041
  context.busy = true;
@@ -1143,50 +1092,111 @@ export async function runResident(options, dependencies = {}) {
1143
1092
  context.startLogins(needsLogin);
1144
1093
  });
1145
1094
  const unavailable = (error) => reported(`working copies unavailable: ${error?.message ?? error}`);
1146
- const copies = coalescedTask(signal, unavailable, () =>
1147
- reportedWorkingCopies(applyWorkingCopies, context, signal),
1095
+ const changed = () => {
1096
+ if (!update.requested) deliveries.trigger();
1097
+ };
1098
+ const behind = (outcome) => {
1099
+ if (!outcome.behind) return;
1100
+ controlSleep(interval, signal).then(changed, () => {});
1101
+ };
1102
+ const copies = coalescedTask(signal, unavailable, async () =>
1103
+ behind(await reportedWorkingCopies(applyWorkingCopies, context, signal)),
1148
1104
  );
1149
- const deliveries = coalescedTask(signal, unavailable, () =>
1150
- reportedWorkingCopies(deliverWorkingCopies, context, signal),
1105
+ const deliveries = coalescedTask(signal, unavailable, async () =>
1106
+ behind(await reportedWorkingCopies(deliverWorkingCopies, context, signal)),
1151
1107
  );
1108
+ sources = sourceEntries({
1109
+ session: context.session,
1110
+ directory: context.directory,
1111
+ dependencies,
1112
+ signal: AbortSignal.any([signal, watcher.signal]),
1113
+ now: session.now,
1114
+ draining: () => update.requested,
1115
+ unavailable: (error) => reported(`google sources unavailable: ${error?.message ?? error}`),
1116
+ });
1117
+ await sources.start();
1152
1118
  const pointer = () => {
1153
1119
  if (!update.requested) agentRefresh.trigger();
1154
1120
  };
1121
+ let passOwed = null;
1122
+ let deferred = null;
1123
+ let retrying = null;
1124
+ const higher = (reach, than) =>
1125
+ PASS_REACHES.indexOf(reach) > PASS_REACHES.indexOf(than) ? reach : than;
1126
+ const owe = (reach) => {
1127
+ passOwed = higher(reach, passOwed);
1128
+ wake.notify();
1129
+ };
1130
+ const defer = (reach) => {
1131
+ deferred = higher(reach, deferred);
1132
+ if (retrying !== null) return;
1133
+ retrying = new AbortController();
1134
+ controlSleep(interval, AbortSignal.any([signal, retrying.signal])).then(() => {
1135
+ retrying = null;
1136
+ const owed = deferred;
1137
+ deferred = null;
1138
+ owe(owed);
1139
+ }, () => {});
1140
+ };
1141
+ const settle = (reach) => {
1142
+ retrying?.abort();
1143
+ retrying = null;
1144
+ if (higher(deferred, reach) !== reach) owe(deferred);
1145
+ deferred = null;
1146
+ };
1155
1147
  const available = (subject) => {
1148
+ if (subject === null || subject === "runs") owe("runs");
1149
+ if (subject === null || subject === "commands") {
1150
+ context.commands.notify();
1151
+ owe("commands");
1152
+ }
1156
1153
  if (update.requested) return;
1157
- if (subject !== "working_copy") agentRefresh.trigger();
1158
- if (subject !== "agents") copies.trigger();
1154
+ if (subject === null || subject === "agents") agentRefresh.trigger();
1155
+ if (subject === null || subject === "working_copy") copies.trigger();
1156
+ if (subject === null || subject === "sources") sources.reconcile();
1159
1157
  };
1160
1158
  context.onIdle = pointer;
1161
1159
  context.workingCopiesApplied = copies.settled;
1162
- if (!update.requested) copies.trigger();
1163
1160
  const watching = (dependencies.watchKitWork ?? watchKitWork)(
1164
1161
  context.session,
1165
1162
  AbortSignal.any([signal, watcher.signal]),
1166
- available,
1163
+ { available, changed },
1167
1164
  dependencies,
1168
1165
  ).catch(fail);
1169
1166
  try {
1170
1167
  while (!signal.aborted) {
1171
1168
  if (failure !== null) throw failure;
1172
- if (!session.refused) {
1169
+ if (passOwed !== null && !session.refused) {
1170
+ const reach = passOwed;
1171
+ passOwed = null;
1173
1172
  const configuration = await readConfiguration(options.configuration);
1174
- await controlledPass(context, dependencies, configuration, signal).catch((error) => {
1175
- if (!session.refused) throw error;
1176
- });
1173
+ const passed = await controlledPass(context, dependencies, configuration, signal, reach).catch(
1174
+ (error) => {
1175
+ if (!session.refused) throw error;
1176
+ return null;
1177
+ },
1178
+ );
1179
+ if ((passed?.run ?? null) !== null) owe(reach);
1180
+ if ((passed?.unresolved.length ?? 0) > 0) defer(reach);
1181
+ else if (passed !== null) settle(reach);
1177
1182
  }
1178
- if (session.refused || update.requested) {
1183
+ const draining = session.refused || update.requested;
1184
+ if (draining) {
1179
1185
  const sessions = await heldSessions(context);
1180
1186
  if (sessions.length > 0) update.answer({ ready: false, sessions });
1181
1187
  else if (
1182
1188
  session.refused ||
1183
- (!served.serving() && logins.size === 0 && agentRefresh.idle() && copies.idle() && deliveries.idle())
1189
+ (!served.serving() &&
1190
+ logins.size === 0 &&
1191
+ agentRefresh.idle() &&
1192
+ copies.idle() &&
1193
+ deliveries.idle() &&
1194
+ sources.idle())
1184
1195
  ) {
1185
1196
  return;
1186
1197
  }
1187
1198
  }
1188
- await wake.wait(interval, signal);
1189
- if (!update.requested && !session.refused) deliveries.trigger();
1199
+ if (session.refused || passOwed === null) await wake.wait(draining ? interval : Infinity, signal);
1190
1200
  }
1191
1201
  } finally {
1192
1202
  watcher.abort();
@@ -1194,6 +1204,7 @@ export async function runResident(options, dependencies = {}) {
1194
1204
  await agentRefresh.settled();
1195
1205
  await copies.settled();
1196
1206
  await deliveries.settled();
1207
+ await sources.settled();
1197
1208
  for (const running of logins.values()) running.controller.abort();
1198
1209
  await Promise.all([...logins.values()].map((running) => running.task));
1199
1210
  await context.agentRefresh;
@@ -1219,7 +1230,7 @@ export async function runResident(options, dependencies = {}) {
1219
1230
  await wake.wait(interval, options.signal);
1220
1231
  }
1221
1232
  update.quiesce();
1222
- while (!options.signal.aborted) await wake.wait(interval, options.signal);
1233
+ while (!options.signal.aborted) await wake.wait(Infinity, options.signal);
1223
1234
  throw new KitRefusal("kit_command_cancelled");
1224
1235
  } finally {
1225
1236
  await served.close();
@@ -23,6 +23,7 @@ import { readNote, writeNote, writeText } from "./state.js";
23
23
  import { bootstrapBundle, discoveryOf, door, withSession } from "./sync.js";
24
24
  import { isGeneratedProvenance, sidecarPath, structuredOrReplace } from "./sync-library.js";
25
25
  import { locate, removeReplicaFile, writeReplicaFile } from "./sync-state.js";
26
+ import { exclusively } from "./tasks.js";
26
27
 
27
28
  const COMMITTER_NAME = "House Kit";
28
29
  const COMMITTER_EMAIL = "house-kit@localhost";
@@ -366,21 +367,6 @@ async function heldCopies(directory) {
366
367
  return held.filter((copy) => copy !== null);
367
368
  }
368
369
 
369
- const turns = new Map();
370
-
371
- function exclusively(path, work) {
372
- const turn = (turns.get(path) ?? Promise.resolve()).then(work);
373
- const settled = turn.then(
374
- () => undefined,
375
- () => undefined,
376
- );
377
- turns.set(path, settled);
378
- void settled.then(() => {
379
- if (turns.get(path) === settled) turns.delete(path);
380
- });
381
- return turn;
382
- }
383
-
384
370
  function received(scope, read, parents) {
385
371
  const files = read.files.filter((file) => file.writable);
386
372
  return { message: `House ${housePath(scope)} at position ${read.position}`, parents, files, whole: true };
@@ -511,7 +497,7 @@ async function deliver(context, path, authority, signal) {
511
497
  let copy = await readCopy(path);
512
498
  if (copy?.active && JSON.stringify(copy.received.declared) !== JSON.stringify(declaration(authority))) {
513
499
  await writeCopy(context.directory, await receive(context, copy, scopeOf(copy), signal), signal);
514
- return;
500
+ return false;
515
501
  }
516
502
  while (
517
503
  copy?.active &&
@@ -522,24 +508,26 @@ async function deliver(context, path, authority, signal) {
522
508
  try {
523
509
  view = await enumerated(context, copy, since, signal);
524
510
  } catch (error) {
525
- if (error.code === "replica_behind") return;
511
+ if (error.code === "replica_behind") return true;
526
512
  if (error.code !== "position_expired") throw error;
527
513
  await writeCopy(context.directory, await receive(context, copy, scopeOf(copy), signal), signal);
528
- return;
514
+ return false;
529
515
  }
530
- if (view.position === since.position) return;
516
+ if (view.position === since.position) return false;
531
517
  copy = await incorporated(copy, view, authority, signal);
532
518
  await writeCopy(context.directory, copy, signal);
533
519
  }
520
+ return false;
534
521
  }
535
522
 
536
523
  async function delivering(context, signal) {
537
524
  const copies = (await heldCopies(context.directory)).filter((copy) => copy.active);
538
- if (copies.length === 0) return { refused: [], lost: false };
525
+ if (copies.length === 0) return { refused: [], lost: false, behind: false };
539
526
  const prefixes = copies.flatMap((copy) => selectedPrefixes(scopeOf(copy)));
540
527
  const discovery = discoveryOf(await door(context.session, KIT_DOOR_DISCOVER_PATH, { prefixes }, signal));
541
528
  const refused = [];
542
529
  let lost = false;
530
+ let behind = false;
543
531
  for (const copy of copies) {
544
532
  const at = housePath(scopeOf(copy));
545
533
  const authority = discovery.authorities.find((entry) => entry?.room_ref === copy.room_ref);
@@ -553,18 +541,24 @@ async function delivering(context, signal) {
553
541
  }
554
542
  const path = copyPath(context.directory, copy);
555
543
  try {
556
- await exclusively(path, () => deliver(context, path, authority, signal));
544
+ if (await exclusively(path, () => deliver(context, path, authority, signal))) behind = true;
557
545
  } catch (error) {
558
546
  if (!scopeRefused(error)) throw error;
559
547
  refused.push({ path: at, code: error.code });
560
548
  }
561
549
  }
562
- return { refused, lost };
550
+ return { refused, lost, behind };
551
+ }
552
+
553
+ function deliveredOutcome(refused, behind) {
554
+ return behind ? { refused, behind } : { refused };
563
555
  }
564
556
 
565
557
  export async function deliverWorkingCopies(context, signal) {
566
558
  const delivered = await delivering(context, signal);
567
- return delivered.lost ? applyWorkingCopies(context, signal) : { refused: delivered.refused };
559
+ return delivered.lost
560
+ ? applyWorkingCopies(context, signal)
561
+ : deliveredOutcome(delivered.refused, delivered.behind);
568
562
  }
569
563
 
570
564
  export async function applyWorkingCopies(context, signal) {
@@ -596,7 +590,8 @@ export async function applyWorkingCopies(context, signal) {
596
590
  refused.push({ path: housePath(scope), code: error.code });
597
591
  }
598
592
  }
599
- return { refused: [...refused, ...(await delivering(context, signal)).refused] };
593
+ const delivered = await delivering(context, signal);
594
+ return deliveredOutcome([...refused, ...delivered.refused], delivered.behind);
600
595
  }
601
596
 
602
597
  export async function workingCopyPaths(directory) {
@@ -1,146 +0,0 @@
1
- import { ITEM_BODY_MAX_BYTES } from "../contract.js";
2
- import { KitRefusal } from "../refusal.js";
3
- import { googleJson } from "./gateway.js";
4
-
5
- function valueAt(value, path) {
6
- let current = value;
7
- for (const segment of path.split(".")) {
8
- if (current === null || current === undefined || typeof current !== "object") return undefined;
9
- current = current[segment];
10
- }
11
- return current;
12
- }
13
-
14
- function requiredText(value, declaration) {
15
- if (typeof value !== "string" || value.length === 0) {
16
- throw new KitRefusal("google_provider_response_invalid", declaration);
17
- }
18
- return value;
19
- }
20
-
21
- function optionalText(value, declaration) {
22
- if (value === undefined || value === null) return null;
23
- return requiredText(value, declaration);
24
- }
25
-
26
- function cursorOf(value) {
27
- if (value === null) return { pageToken: null, checkpoint: null };
28
- let cursor;
29
- try {
30
- cursor = JSON.parse(value);
31
- } catch {
32
- throw new KitRefusal("google_checkpoint_invalid", "declared service");
33
- }
34
- if (
35
- typeof cursor !== "object" ||
36
- cursor === null ||
37
- (cursor.pageToken !== null && typeof cursor.pageToken !== "string") ||
38
- (cursor.checkpoint !== null && typeof cursor.checkpoint !== "string")
39
- ) {
40
- throw new KitRefusal("google_checkpoint_invalid", "declared service");
41
- }
42
- return cursor;
43
- }
44
-
45
- function composedBody(item, paths) {
46
- const parts = paths.map((path) => valueAt(item, path));
47
- if (parts.some((part) => !["string", "number", "boolean"].includes(typeof part))) {
48
- throw new KitRefusal("google_provider_response_invalid", "declared body");
49
- }
50
- const body = parts.map((part) => `${part}`).join("\n\n");
51
- if (Buffer.byteLength(body, "utf8") > ITEM_BODY_MAX_BYTES) {
52
- throw new KitRefusal("google_provider_response_invalid", "declared body");
53
- }
54
- return body;
55
- }
56
-
57
- function normalizedItem(service, item) {
58
- const fields = service.declaration.fields;
59
- const observedAt = requiredText(valueAt(item, fields.observedAt), "declared observed time");
60
- if (Number.isNaN(Date.parse(observedAt))) {
61
- throw new KitRefusal("google_provider_response_invalid", "declared observed time");
62
- }
63
- const container = requiredText(valueAt(item, fields.container), "declared container");
64
- const nativeVersion =
65
- fields.nativeVersion === null
66
- ? null
67
- : optionalText(valueAt(item, fields.nativeVersion), "declared native version");
68
- const providerModifiedAt =
69
- fields.providerModifiedAt === null
70
- ? null
71
- : optionalText(
72
- valueAt(item, fields.providerModifiedAt),
73
- "declared provider modification time",
74
- );
75
- if (providerModifiedAt !== null && Number.isNaN(Date.parse(providerModifiedAt))) {
76
- throw new KitRefusal("google_provider_response_invalid", "declared provider modification time");
77
- }
78
- return {
79
- namespace: `google/${service.service}`,
80
- nativeObjectId: requiredText(valueAt(item, fields.nativeId), "declared native id"),
81
- nativeVersion,
82
- providerModifiedAt:
83
- providerModifiedAt === null ? null : new Date(providerModifiedAt).toISOString(),
84
- observedAt: new Date(observedAt).toISOString(),
85
- body: composedBody(item, fields.body),
86
- author:
87
- fields.author === null
88
- ? null
89
- : optionalText(valueAt(item, fields.author), "declared author"),
90
- url:
91
- fields.url === null ? null : optionalText(valueAt(item, fields.url), "declared url"),
92
- attachments: [],
93
- container: `google/${service.service}/${container}`,
94
- };
95
- }
96
-
97
- export async function declaredPage(options, dependencies = {}) {
98
- const declared = options.service.declaration;
99
- const cursor = cursorOf(options.cursor);
100
- const url = new URL(declared.path, `https://${declared.host}`);
101
- for (const [name, value] of Object.entries(declared.query)) url.searchParams.set(name, value);
102
- url.searchParams.set(declared.pageSize, `${options.limit}`);
103
- if (cursor.pageToken !== null) {
104
- url.searchParams.set(declared.pageToken.request, cursor.pageToken);
105
- }
106
- if (cursor.checkpoint !== null) {
107
- url.searchParams.set(declared.pageToken.checkpointRequest, cursor.checkpoint);
108
- }
109
- const answer = await googleJson(
110
- {
111
- directory: options.directory,
112
- instance: options.instance,
113
- client: options.client,
114
- requiredScopes: declared.scopes,
115
- url: url.toString(),
116
- signal: options.signal,
117
- },
118
- dependencies,
119
- );
120
- const entries = valueAt(answer, declared.items);
121
- if (!Array.isArray(entries) || entries.length > options.limit) {
122
- throw new KitRefusal("google_provider_response_invalid", "declared items");
123
- }
124
- const pageToken = valueAt(answer, declared.pageToken.response);
125
- let next;
126
- if (pageToken !== undefined && pageToken !== null) {
127
- next = {
128
- pageToken: requiredText(pageToken, "declared page token"),
129
- checkpoint: cursor.checkpoint,
130
- };
131
- } else {
132
- next = {
133
- pageToken: null,
134
- checkpoint: requiredText(
135
- valueAt(answer, declared.pageToken.checkpointResponse),
136
- "declared checkpoint",
137
- ),
138
- };
139
- }
140
- return {
141
- items: entries.map((entry) => normalizedItem(options.service, entry)),
142
- dropped: [],
143
- cursor: JSON.stringify(next),
144
- more: pageToken !== undefined && pageToken !== null,
145
- };
146
- }
package/src/google/id.js DELETED
@@ -1 +0,0 @@
1
- export const GOOGLE_DRIVE_ID = /^[A-Za-z0-9_-]{1,512}$/;
@@ -1 +0,0 @@
1
- export const GOOGLE_OAUTH_CLIENT = Object.freeze({"id":"911965338518-867gdvuddb7kdkjo3qvpv6gds369alb7.apps.googleusercontent.com","secret":"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"});