@scp-dev/broker 0.1.3 → 0.1.5

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/server.ts CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  stripControl,
27
27
  } from "@scp-dev/core"
28
28
  import { FederatedCore } from "@scp-dev/hub"
29
- import { type Bridge, type BridgeConfig, remoteSend, startBridge } from "./bridge"
29
+ import { type Bridge, type BridgeConfig, type BridgeHooks, remoteSend, startBridge } from "./bridge"
30
30
  import { type Deliver, type JudgeClientFactory, JudgeGate } from "./judge-gate"
31
31
  import { releaseBrokerLock, takeBrokerLock } from "./lock"
32
32
  import { encodeLine, LineReader, LineWriter, MAX_LINE } from "./ndjson"
@@ -60,6 +60,8 @@ export interface BrokerOptions {
60
60
  bridge?: BridgeConfig | undefined
61
61
  /** reads the bridge configuration again for bridge_relink; one that appeared or names another hub or machine restarts the broker */
62
62
  bridgeConfig?: (() => Promise<BridgeConfig | undefined>) | undefined
63
+ /** experimental: another link to other machines in place of the hub's, started only when `bridge` is unset */
64
+ link?: LinkFactory | undefined
63
65
  /** how often presence that decays without a request (a hook-only endpoint) is checked for changes (default 60 s) */
64
66
  presenceEveryMs?: number | undefined
65
67
  /** where a judge model's key_env is read (default the broker's environment) */
@@ -68,6 +70,15 @@ export interface BrokerOptions {
68
70
  judgeClient?: JudgeClientFactory | undefined
69
71
  }
70
72
 
73
+ /** what a link gets from the broker: its core, the remote hooks a bridge gets, and a send as a local endpoint or a mirrored one through the judge */
74
+ export interface LinkContext {
75
+ core: FederatedCore
76
+ hooks: BridgeHooks
77
+ send: Deliver
78
+ }
79
+
80
+ export type LinkFactory = (context: LinkContext) => Bridge
81
+
71
82
  export interface Broker {
72
83
  socket: string
73
84
  core: FederatedCore
@@ -88,6 +99,8 @@ interface Connection {
88
99
  tty?: boolean
89
100
  /** the connection holds the human inbox */
90
101
  human?: boolean
102
+ /** a human hello bound to the agent of this shell's lineage said a terminal was attached: the human at that agent's shell */
103
+ terminal?: boolean
91
104
  /**
92
105
  * the lineage endpoint of an observer, or of a hello bound to it: the connection sees and acts with that endpoint's view,
93
106
  * never widening unless `own`, and never counts as connected
@@ -135,7 +148,19 @@ const CHANGES_SCOPE = new Set(["rules_set", "rules_widen", "rules_lift", "group"
135
148
  const ASK_RECHECK_MS = 1000
136
149
  /** how often a waiting stop rechecks its workers, since a worker's exit or lapse wakes no one */
137
150
  const STOP_RECHECK_MS = 1000
138
- const TRANSIENT_METHODS = new Set(["event", "status", "whoami", "inject", "injected", "peek_turn", "stop_wait", "prompted", "title_infer"])
151
+ const TRANSIENT_METHODS = new Set([
152
+ "event",
153
+ "status",
154
+ "whoami",
155
+ "inject",
156
+ "injected",
157
+ "peek_turn",
158
+ "stop_wait",
159
+ "prompted",
160
+ "title_infer",
161
+ "subagent_title",
162
+ "wake",
163
+ ])
139
164
  const GRANT_UNBOUND = `a spawn-env grant binds only a new harness process: run the harness with it, or name the child (${CLI_NAME} spawn-env --name / --as)`
140
165
 
141
166
  /** the workers' messages and handbacks a stop's push would show, and those of theirs left unread past it */
@@ -269,21 +294,17 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
269
294
  }
270
295
  for (const id of known.keys()) if (!live.has(id)) known.delete(id)
271
296
  }, options.presenceEveryMs ?? PRESENCE_MS)
272
- if (options.bridge) {
273
- bridge = startBridge({
274
- ...options.bridge,
275
- core,
276
- onRemote: {
277
- wake,
278
- fleet: notifyFleet,
279
- policyError: () => {
280
- loadPolicy()
281
- return policyError
282
- },
283
- log,
284
- },
285
- })
297
+ const remoteHooks: BridgeHooks = {
298
+ wake,
299
+ fleet: notifyFleet,
300
+ policyError: () => {
301
+ loadPolicy()
302
+ return policyError
303
+ },
304
+ log,
286
305
  }
306
+ if (options.bridge) bridge = startBridge({ ...options.bridge, core, onRemote: remoteHooks })
307
+ else if (options.link) bridge = options.link({ core, hooks: remoteHooks, send: (from, params, origin) => judge.send(from, params, origin, deliver) })
287
308
 
288
309
  /** links the bridge again with the credential on disk; a configuration this broker cannot take without a restart stops it */
289
310
  async function relink(): Promise<object> {
@@ -697,18 +718,24 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
697
718
  }
698
719
  const join = params.join
699
720
  if (join !== undefined && (typeof join !== "object" || join === null || Array.isArray(join)))
700
- throw new RelayError("invalid_params", "join is {handle?}")
721
+ throw new RelayError("invalid_params", "join is {handle?, under?}")
722
+ const under = (join as { under?: unknown } | undefined)?.under
701
723
  const before = new Map(core.list({ all: true }).map((view) => [view.id, view]))
702
724
  const { endpoint, subagent, created } =
703
725
  join === undefined
704
726
  ? core.claim((params.claim ?? {}) as Claim, { transient: true })
705
- : { endpoint: core.joinSession((params.claim ?? {}) as Claim, (join as { handle?: unknown }).handle), subagent: undefined, created: undefined }
727
+ : {
728
+ endpoint: core.joinSession((params.claim ?? {}) as Claim, (join as { handle?: unknown }).handle, under),
729
+ subagent: undefined,
730
+ created: undefined,
731
+ }
706
732
  s.data.endpoint = endpoint.id
707
733
  s.data.transient = true
708
734
  const key = (params.claim as Claim | undefined)?.process
709
735
  if (typeof key === "string") s.data.process = key
710
736
  const was = before.get(endpoint.id)?.address
711
737
  if (join !== undefined) membership(endpoint, core.sessionOf(endpoint.id), true, was?.slice(was.lastIndexOf("/") + 1))
738
+ if (under !== undefined && before.get(endpoint.id)?.parent !== endpoint.parent) bridge?.resync()
712
739
  else if ((before.get(endpoint.id)?.presence ?? "offline") !== endpoint.presence) presenceChanged(endpoint)
713
740
  return {
714
741
  endpoint,
@@ -741,7 +768,10 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
741
768
  const claim = params.claim as Claim | undefined
742
769
  if (params.human === true && params.resume === undefined && params.lineage !== undefined) {
743
770
  const placed = core.placeLineage(params.lineage)
744
- if (placed) return bindLineage(s, placed, params)
771
+ if (placed) {
772
+ s.data.terminal = params.tty === true
773
+ return bindLineage(s, placed, params)
774
+ }
745
775
  } else if (params.human !== true && params.resume === undefined && claim?.process === undefined && typeof claim?.handle === "string") {
746
776
  const placed = claim.lineage === undefined ? null : core.placeLineage(claim.lineage, claim.handle)
747
777
  if (placed && (claim.grant === undefined || core.grantBinds(claim.grant, placed))) {
@@ -853,11 +883,37 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
853
883
  if (changed) bridge?.changed(me)
854
884
  return { changed }
855
885
  }
886
+ case "subagent_title": {
887
+ const changed = core.subagentTitle(me, params, hookOf(s))
888
+ if (changed) bridge?.changed(changed)
889
+ return { changed: changed !== null }
890
+ }
891
+ case "wake": {
892
+ if (s.data.lineage && !s.data.own) throw new RelayError("unauthorized", "only the endpoint's own session says how it wakes")
893
+ const changed = core.setWake(me, params.via, hookOf(s))
894
+ if (changed) bridge?.changed(me)
895
+ return { changed }
896
+ }
856
897
  case "rename": {
857
898
  const renamed = core.rename(actorOf(s), { ...(params.endpoint !== undefined ? { endpoint: params.endpoint } : {}), handle: params.handle })
858
899
  membership(renamed.endpoint, renamed.session, true, renamed.from)
859
900
  return renamed.endpoint
860
901
  }
902
+ case "adopt": {
903
+ const actor = actorOf(s)
904
+ const adopted = core.adopt(
905
+ actor,
906
+ { endpoint: params.endpoint, ...(params.parent !== undefined ? { parent: params.parent } : {}) },
907
+ { tty: actor.kind === "endpoint" && s.data.terminal === true },
908
+ )
909
+ bridge?.resync()
910
+ return adopted
911
+ }
912
+ case "unlink": {
913
+ const unlinked = core.unlink(actorOf(s), params.endpoint !== undefined ? { endpoint: params.endpoint } : {})
914
+ bridge?.resync()
915
+ return unlinked
916
+ }
861
917
  case "leave": {
862
918
  const left = core.leaveSession(actorOf(s), params.endpoint !== undefined ? { endpoint: params.endpoint } : {})
863
919
  for (const c of connections) if (c.data.endpoint === left.endpoint.id && !c.data.transient && !c.data.lineage && c !== s) c.end()