@idleware/ringi 0.5.0 → 0.6.1
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/package.json +1 -1
- package/target/dts/adapters/fs/index.d.ts +2 -0
- package/target/dts/adapters/fs/lock.d.ts +1 -0
- package/target/dts/adapters/fs/write.d.ts +5 -0
- package/target/dts/adapters/host-posix/group-state.d.ts +7 -0
- package/target/dts/adapters/host-posix/groups.d.ts +1 -0
- package/target/dts/adapters/host-posix/index.d.ts +2 -0
- package/target/dts/adapters/host-posix/launch.d.ts +7 -0
- package/target/dts/adapters/host-posix/probes.d.ts +5 -0
- package/target/dts/adapters/host-posix/stats.d.ts +9 -0
- package/target/dts/adapters/keys-file/index.d.ts +2 -0
- package/target/dts/adapters/state-fs/index.d.ts +5 -0
- package/target/dts/adapters/tool-claude/index.d.ts +2 -0
- package/target/dts/adapters/tool-codex/index.d.ts +2 -0
- package/target/dts/adapters/tool-cursor/index.d.ts +2 -0
- package/target/dts/adapters/tool-traces/index.d.ts +3 -0
- package/target/dts/adapters/transport-git/branches.d.ts +43 -0
- package/target/dts/adapters/transport-git/files.d.ts +2 -0
- package/target/dts/adapters/transport-git/index.d.ts +32 -0
- package/target/dts/adapters/transport-git/marks.d.ts +20 -0
- package/target/dts/adapters/transport-git/remote/admit.d.ts +7 -0
- package/target/dts/adapters/transport-git/remote/alarms.d.ts +3 -0
- package/target/dts/adapters/transport-git/remote/bootstrap.d.ts +7 -0
- package/target/dts/adapters/transport-git/remote/branch-commit.d.ts +3 -0
- package/target/dts/adapters/transport-git/remote/branch-control.d.ts +15 -0
- package/target/dts/adapters/transport-git/remote/branch-scan.d.ts +2 -0
- package/target/dts/adapters/transport-git/remote/branch.d.ts +5 -0
- package/target/dts/adapters/transport-git/remote/cache.d.ts +3 -0
- package/target/dts/adapters/transport-git/remote/commits.d.ts +2 -0
- package/target/dts/adapters/transport-git/remote/forget.d.ts +2 -0
- package/target/dts/adapters/transport-git/remote/git.d.ts +26 -0
- package/target/dts/adapters/transport-git/remote/heartbeat-facts.d.ts +1 -0
- package/target/dts/adapters/transport-git/remote/heartbeat-name.d.ts +6 -0
- package/target/dts/adapters/transport-git/remote/heartbeat-object.d.ts +10 -0
- package/target/dts/adapters/transport-git/remote/heartbeat-push.d.ts +2 -0
- package/target/dts/adapters/transport-git/remote/heartbeat.d.ts +7 -0
- package/target/dts/adapters/transport-git/remote/keys.d.ts +13 -0
- package/target/dts/adapters/transport-git/remote/main.d.ts +11 -0
- package/target/dts/adapters/transport-git/remote/process.d.ts +20 -0
- package/target/dts/adapters/transport-git/remote/push-retry.d.ts +2 -0
- package/target/dts/adapters/transport-git/remote/push-validate.d.ts +2 -0
- package/target/dts/adapters/transport-git/remote/push.d.ts +16 -0
- package/target/dts/adapters/transport-git/remote/roster.d.ts +7 -0
- package/target/dts/adapters/transport-git/remote/signals.d.ts +15 -0
- package/target/dts/adapters/transport-git/remote/signature.d.ts +5 -0
- package/target/dts/adapters/transport-git/remote/sync-heartbeat.d.ts +2 -0
- package/target/dts/adapters/transport-git/remote/sync-rooms.d.ts +6 -0
- package/target/dts/adapters/transport-git/remote/sync-tags.d.ts +8 -0
- package/target/dts/adapters/transport-git/remote/sync.d.ts +2 -0
- package/target/dts/adapters/transport-git/remote/types.d.ts +64 -0
- package/target/dts/adapters/transport-git/remote.d.ts +17 -0
- package/target/dts/adapters/transport-git/repo.d.ts +31 -0
- package/target/dts/adapters/transport-git/run.d.ts +21 -0
- package/target/dts/adapters/transport-git/signing.d.ts +4 -0
- package/target/dts/adapters/transport-git/tags.d.ts +15 -0
- package/target/dts/adapters/transport-git/transport.d.ts +7 -0
- package/target/dts/adapters/transport-kafka/index.d.ts +2 -0
- package/target/dts/adapters/transport-temporal/index.d.ts +2 -0
- package/target/dts/cli.d.ts +2 -1
- package/target/dts/{cli/agent.d.ts → clients/cli/agent/index.d.ts} +3 -3
- package/target/dts/{cli → clients/cli/agent}/session.d.ts +2 -2
- package/target/dts/clients/cli/arg-parse.d.ts +2 -0
- package/target/dts/{cli → clients/cli}/args.d.ts +2 -2
- package/target/dts/clients/cli/commands.d.ts +2 -0
- package/target/dts/clients/cli/context.d.ts +4 -0
- package/target/dts/{cli/index.d.ts → clients/cli/help.d.ts} +1 -3
- package/target/dts/clients/cli/index.d.ts +4 -0
- package/target/dts/clients/cli/operator/heartbeat-format.d.ts +5 -0
- package/target/dts/clients/cli/operator/heartbeat.d.ts +3 -0
- package/target/dts/clients/cli/operator/hub-commands.d.ts +6 -0
- package/target/dts/clients/cli/operator/hub.d.ts +2 -0
- package/target/dts/clients/cli/operator/index.d.ts +5 -0
- package/target/dts/clients/cli/operator/room-lifecycle.d.ts +5 -0
- package/target/dts/clients/cli/operator/room-list.d.ts +5 -0
- package/target/dts/clients/cli/operator/room-members.d.ts +4 -0
- package/target/dts/clients/cli/operator/room.d.ts +2 -0
- package/target/dts/clients/cli/operator/sessions.d.ts +5 -0
- package/target/dts/clients/cli/operator/watch.d.ts +2 -0
- package/target/dts/clients/cli/operator/where-format.d.ts +7 -0
- package/target/dts/clients/cli/operator/where.d.ts +2 -0
- package/target/dts/clients/cli/shared/budget.d.ts +7 -0
- package/target/dts/clients/cli/shared/files.d.ts +4 -0
- package/target/dts/clients/cli/shared/index.d.ts +7 -0
- package/target/dts/clients/cli/shared/messages.d.ts +7 -0
- package/target/dts/clients/cli/shared/profile.d.ts +4 -0
- package/target/dts/clients/cli/shared/session.d.ts +6 -0
- package/target/dts/clients/cli/shared/signals.d.ts +3 -0
- package/target/dts/clients/cli/shared/work.d.ts +6 -0
- package/target/dts/clients/cli/skill/index.d.ts +3 -0
- package/target/dts/{cli/skill.d.ts → clients/cli/skill/targets.d.ts} +1 -2
- package/target/dts/composition/index.d.ts +2 -0
- package/target/dts/{heartbeat → core/agents/heartbeat}/index.d.ts +2 -4
- package/target/dts/{app/heartbeat.d.ts → core/agents/heartbeat/pulse.d.ts} +5 -6
- package/target/dts/core/agents/heartbeat/singleton.d.ts +3 -0
- package/target/dts/core/agents/heartbeat/tools.d.ts +6 -0
- package/target/dts/{app → core/agents}/overview.d.ts +12 -6
- package/target/dts/core/agents/ports/host.d.ts +47 -0
- package/target/dts/core/agents/ports/index.d.ts +9 -0
- package/target/dts/core/agents/ports/tools.d.ts +18 -0
- package/target/dts/core/agents/sessions/agent.d.ts +70 -0
- package/target/dts/core/agents/sessions/binding.d.ts +9 -0
- package/target/dts/core/agents/sessions/context.d.ts +8 -0
- package/target/dts/{runtime/session.d.ts → core/agents/sessions/index.d.ts} +2 -9
- package/target/dts/core/agents/sessions/join.d.ts +20 -0
- package/target/dts/core/agents/sessions/resume.d.ts +2 -0
- package/target/dts/core/agents/sessions/session.d.ts +46 -0
- package/target/dts/core/agents/sessions/wait.d.ts +38 -0
- package/target/dts/core/agents/turns/budgets.d.ts +5 -0
- package/target/dts/core/agents/turns/dispatch.d.ts +9 -0
- package/target/dts/core/agents/turns/exhaustion.d.ts +11 -0
- package/target/dts/core/agents/turns/grants.d.ts +4 -0
- package/target/dts/core/agents/turns/index.d.ts +4 -0
- package/target/dts/core/agents/turns/limits.d.ts +2 -0
- package/target/dts/core/agents/turns/messages.d.ts +10 -0
- package/target/dts/core/agents/turns/pings.d.ts +5 -0
- package/target/dts/core/agents/turns/plan.d.ts +3 -0
- package/target/dts/core/agents/turns/poll.d.ts +8 -0
- package/target/dts/core/agents/turns/results.d.ts +3 -0
- package/target/dts/core/agents/turns/seat.d.ts +2 -0
- package/target/dts/core/agents/turns/types.d.ts +59 -0
- package/target/dts/core/hub/decisions/budget.d.ts +15 -0
- package/target/dts/core/hub/decisions/check.d.ts +6 -0
- package/target/dts/core/hub/decisions/index.d.ts +5 -0
- package/target/dts/core/hub/decisions/proposal.d.ts +38 -0
- package/target/dts/core/hub/decisions/usage.d.ts +12 -0
- package/target/dts/{runtime/reader.d.ts → core/hub/history/index.d.ts} +12 -6
- package/target/dts/core/hub/ports/index.d.ts +16 -0
- package/target/dts/core/hub/ports/state.d.ts +12 -0
- package/target/dts/{store/index.d.ts → core/hub/ports/store.d.ts} +48 -18
- package/target/dts/core/hub/protocol/index.d.ts +5 -0
- package/target/dts/{protocol/index.d.ts → core/hub/protocol/model.d.ts} +0 -25
- package/target/dts/core/hub/protocol/order.d.ts +2 -0
- package/target/dts/core/hub/protocol/reading.d.ts +8 -0
- package/target/dts/core/hub/protocol/text.d.ts +11 -0
- package/target/dts/core/hub/protocol/threads.d.ts +5 -0
- package/target/dts/core/hub/rooms/compat.d.ts +5 -0
- package/target/dts/core/hub/rooms/hubs.d.ts +14 -0
- package/target/dts/core/hub/rooms/index.d.ts +53 -0
- package/target/dts/core/hub/rooms/messages.d.ts +8 -0
- package/target/dts/core/hub/rooms/rooms.d.ts +10 -0
- package/target/dts/core/hub/signals/check.d.ts +3 -0
- package/target/dts/core/hub/signals/effective.d.ts +3 -0
- package/target/dts/core/hub/signals/index.d.ts +5 -0
- package/target/dts/core/hub/signals/marks.d.ts +4 -0
- package/target/dts/core/hub/signals/model.d.ts +28 -0
- package/target/dts/core/hub/signals/pins.d.ts +5 -0
- package/target/dts/{hub/signals.d.ts → core/hub/signals/write.d.ts} +1 -1
- package/target/dts/core/hub/usecases/actor.d.ts +6 -0
- package/target/dts/{app → core/hub/usecases}/budget.d.ts +5 -18
- package/target/dts/core/hub/usecases/config.d.ts +6 -0
- package/target/dts/{app → core/hub/usecases}/context.d.ts +3 -8
- package/target/dts/{app → core/hub/usecases}/hubs.d.ts +1 -1
- package/target/dts/core/hub/usecases/propose.d.ts +17 -0
- package/target/dts/{app → core/hub/usecases}/room.d.ts +8 -34
- package/target/dts/{app → core/hub/usecases}/rooms.d.ts +2 -6
- package/target/dts/core/hub/usecases/roster.d.ts +5 -0
- package/target/dts/core/hub/usecases/send.d.ts +21 -0
- package/target/dts/core/hub/usecases/staging.d.ts +8 -0
- package/target/dts/{app → core/hub/usecases}/view.d.ts +4 -4
- package/target/dts/{domain/profiles.d.ts → core/hub/work/index.d.ts} +3 -7
- package/target/dts/core/hub/work/parse.d.ts +2 -0
- package/target/dts/core/hub/work/task.d.ts +6 -0
- package/target/dts/core/signing/index.d.ts +35 -0
- package/target/dts/index.d.ts +8 -8
- package/target/dts/lib/fail.d.ts +8 -0
- package/target/dts/lib/index.d.ts +3 -11
- package/target/dts/lib/json.d.ts +1 -0
- package/target/esm/adapters/fs/index.js +5 -0
- package/target/esm/adapters/fs/lock.js +83 -0
- package/target/esm/adapters/fs/write.js +25 -0
- package/target/esm/adapters/host-posix/group-state.js +24 -0
- package/target/esm/adapters/host-posix/groups.js +43 -0
- package/target/esm/adapters/host-posix/index.js +35 -0
- package/target/esm/adapters/host-posix/launch.js +80 -0
- package/target/esm/adapters/host-posix/probes.js +66 -0
- package/target/esm/adapters/host-posix/stats.js +27 -0
- package/target/esm/adapters/keys-file/index.js +68 -0
- package/target/esm/adapters/state-fs/index.js +79 -0
- package/target/esm/adapters/tool-claude/index.js +32 -0
- package/target/esm/adapters/tool-codex/index.js +38 -0
- package/target/esm/adapters/tool-cursor/index.js +8 -0
- package/target/esm/adapters/tool-traces/index.js +18 -0
- package/target/esm/adapters/transport-git/branches.js +155 -0
- package/target/esm/adapters/transport-git/files.js +48 -0
- package/target/esm/adapters/transport-git/index.js +128 -0
- package/target/esm/adapters/transport-git/marks.js +88 -0
- package/target/esm/adapters/transport-git/remote/admit.js +71 -0
- package/target/esm/adapters/transport-git/remote/alarms.js +30 -0
- package/target/esm/adapters/transport-git/remote/bootstrap.js +47 -0
- package/target/esm/adapters/transport-git/remote/branch-commit.js +36 -0
- package/target/esm/adapters/transport-git/remote/branch-control.js +37 -0
- package/target/esm/adapters/transport-git/remote/branch-scan.js +28 -0
- package/target/esm/adapters/transport-git/remote/branch.js +37 -0
- package/target/esm/adapters/transport-git/remote/cache.js +23 -0
- package/target/esm/adapters/transport-git/remote/commits.js +25 -0
- package/target/esm/adapters/transport-git/remote/forget.js +35 -0
- package/target/esm/adapters/transport-git/remote/git.js +58 -0
- package/target/esm/adapters/transport-git/remote/heartbeat-facts.js +35 -0
- package/target/esm/adapters/transport-git/remote/heartbeat-name.js +19 -0
- package/target/esm/adapters/transport-git/remote/heartbeat-object.js +40 -0
- package/target/esm/adapters/transport-git/remote/heartbeat-push.js +44 -0
- package/target/esm/adapters/transport-git/remote/heartbeat.js +44 -0
- package/target/esm/adapters/transport-git/remote/keys.js +75 -0
- package/target/esm/adapters/transport-git/remote/main.js +41 -0
- package/target/esm/adapters/transport-git/remote/process.js +37 -0
- package/target/esm/adapters/transport-git/remote/push-retry.js +46 -0
- package/target/esm/adapters/transport-git/remote/push-validate.js +39 -0
- package/target/esm/adapters/transport-git/remote/push.js +62 -0
- package/target/esm/adapters/transport-git/remote/roster.js +48 -0
- package/target/esm/adapters/transport-git/remote/signals.js +63 -0
- package/target/esm/adapters/transport-git/remote/signature.js +56 -0
- package/target/esm/adapters/transport-git/remote/sync-heartbeat.js +26 -0
- package/target/esm/adapters/transport-git/remote/sync-rooms.js +38 -0
- package/target/esm/adapters/transport-git/remote/sync-tags.js +48 -0
- package/target/esm/adapters/transport-git/remote/sync.js +38 -0
- package/target/esm/adapters/transport-git/remote/types.js +10 -0
- package/target/esm/adapters/transport-git/remote.js +17 -0
- package/target/esm/adapters/transport-git/repo.js +115 -0
- package/target/esm/adapters/transport-git/run.js +57 -0
- package/target/esm/adapters/transport-git/signing.js +44 -0
- package/target/esm/adapters/transport-git/tags.js +21 -0
- package/target/esm/adapters/transport-git/transport.js +29 -0
- package/target/esm/adapters/transport-kafka/index.js +4 -0
- package/target/esm/adapters/transport-temporal/index.js +4 -0
- package/target/esm/cli.js +5 -4
- package/target/esm/{cli/agent.js → clients/cli/agent/index.js} +8 -7
- package/target/esm/{cli → clients/cli/agent}/session.js +7 -7
- package/target/esm/clients/cli/arg-parse.js +41 -0
- package/target/esm/{cli → clients/cli}/args.js +3 -36
- package/target/esm/clients/cli/commands.js +45 -0
- package/target/esm/clients/cli/context.js +6 -0
- package/target/esm/{cli/index.js → clients/cli/help.js} +4 -77
- package/target/esm/clients/cli/index.js +27 -0
- package/target/esm/clients/cli/operator/heartbeat-format.js +8 -0
- package/target/esm/clients/cli/operator/heartbeat.js +35 -0
- package/target/esm/clients/cli/operator/hub-commands.js +37 -0
- package/target/esm/clients/cli/operator/hub.js +10 -0
- package/target/esm/clients/cli/operator/index.js +8 -0
- package/target/esm/clients/cli/operator/room-lifecycle.js +23 -0
- package/target/esm/clients/cli/operator/room-list.js +27 -0
- package/target/esm/clients/cli/operator/room-members.js +15 -0
- package/target/esm/clients/cli/operator/room.js +15 -0
- package/target/esm/clients/cli/operator/sessions.js +32 -0
- package/target/esm/clients/cli/operator/watch.js +15 -0
- package/target/esm/clients/cli/operator/where-format.js +24 -0
- package/target/esm/clients/cli/operator/where.js +32 -0
- package/target/esm/clients/cli/shared/budget.js +50 -0
- package/target/esm/clients/cli/shared/files.js +26 -0
- package/target/esm/clients/cli/shared/index.js +10 -0
- package/target/esm/clients/cli/shared/messages.js +41 -0
- package/target/esm/clients/cli/shared/profile.js +17 -0
- package/target/esm/clients/cli/shared/session.js +8 -0
- package/target/esm/clients/cli/shared/signals.js +21 -0
- package/target/esm/clients/cli/shared/work.js +26 -0
- package/target/esm/clients/cli/skill/index.js +40 -0
- package/target/esm/clients/cli/skill/targets.js +20 -0
- package/target/esm/composition/index.js +34 -0
- package/target/esm/{heartbeat → core/agents/heartbeat}/index.js +14 -12
- package/target/esm/{app/heartbeat.js → core/agents/heartbeat/pulse.js} +31 -55
- package/target/esm/core/agents/heartbeat/singleton.js +38 -0
- package/target/esm/core/agents/heartbeat/tools.js +24 -0
- package/target/esm/{app → core/agents}/overview.js +15 -9
- package/target/esm/core/agents/ports/host.js +4 -0
- package/target/esm/core/agents/ports/index.js +7 -0
- package/target/esm/core/agents/ports/tools.js +5 -0
- package/target/esm/{app → core/agents/sessions}/agent.js +11 -43
- package/target/esm/core/agents/sessions/binding.js +43 -0
- package/target/esm/core/agents/sessions/context.js +11 -0
- package/target/esm/core/agents/sessions/index.js +59 -0
- package/target/esm/core/agents/sessions/join.js +55 -0
- package/target/esm/core/agents/sessions/resume.js +54 -0
- package/target/esm/core/agents/sessions/session.js +28 -0
- package/target/esm/core/agents/sessions/wait.js +47 -0
- package/target/esm/core/agents/turns/budgets.js +48 -0
- package/target/esm/core/agents/turns/dispatch.js +40 -0
- package/target/esm/core/agents/turns/exhaustion.js +16 -0
- package/target/esm/core/agents/turns/grants.js +36 -0
- package/target/esm/core/agents/turns/index.js +33 -0
- package/target/esm/core/agents/turns/limits.js +11 -0
- package/target/esm/core/agents/turns/messages.js +32 -0
- package/target/esm/core/agents/turns/pings.js +30 -0
- package/target/esm/core/agents/turns/plan.js +29 -0
- package/target/esm/core/agents/turns/poll.js +48 -0
- package/target/esm/core/agents/turns/results.js +40 -0
- package/target/esm/core/agents/turns/seat.js +44 -0
- package/target/esm/core/agents/turns/types.js +3 -0
- package/target/esm/{domain/index.js → core/hub/capabilities.js} +3 -3
- package/target/esm/core/hub/decisions/budget.js +40 -0
- package/target/esm/core/hub/decisions/check.js +43 -0
- package/target/esm/core/hub/decisions/index.js +10 -0
- package/target/esm/core/hub/decisions/proposal.js +73 -0
- package/target/esm/core/hub/decisions/usage.js +39 -0
- package/target/esm/core/hub/history/index.js +90 -0
- package/target/esm/core/hub/ports/index.js +10 -0
- package/target/esm/core/hub/ports/state.js +5 -0
- package/target/esm/core/hub/ports/store.js +10 -0
- package/target/esm/{protocol → core/hub/protocol}/files.js +4 -2
- package/target/esm/core/hub/protocol/index.js +9 -0
- package/target/esm/core/hub/protocol/model.js +15 -0
- package/target/esm/core/hub/protocol/order.js +39 -0
- package/target/esm/core/hub/protocol/reading.js +29 -0
- package/target/esm/core/hub/protocol/text.js +95 -0
- package/target/esm/core/hub/protocol/threads.js +52 -0
- package/target/esm/core/hub/rooms/compat.js +51 -0
- package/target/esm/core/hub/rooms/hubs.js +92 -0
- package/target/esm/core/hub/rooms/index.js +23 -0
- package/target/esm/core/hub/rooms/messages.js +47 -0
- package/target/esm/core/hub/rooms/rooms.js +84 -0
- package/target/esm/core/hub/signals/check.js +33 -0
- package/target/esm/core/hub/signals/effective.js +31 -0
- package/target/esm/core/hub/signals/index.js +23 -0
- package/target/esm/core/hub/signals/marks.js +30 -0
- package/target/esm/core/hub/signals/model.js +5 -0
- package/target/esm/core/hub/signals/pins.js +21 -0
- package/target/esm/{hub/signals.js → core/hub/signals/write.js} +4 -4
- package/target/esm/core/hub/usecases/actor.js +3 -0
- package/target/esm/core/hub/usecases/budget.js +78 -0
- package/target/esm/core/hub/usecases/config.js +5 -0
- package/target/esm/core/hub/usecases/context.js +13 -0
- package/target/esm/core/hub/usecases/hubs.js +95 -0
- package/target/esm/core/hub/usecases/propose.js +39 -0
- package/target/esm/core/hub/usecases/room.js +82 -0
- package/target/esm/{app → core/hub/usecases}/rooms.js +11 -63
- package/target/esm/core/hub/usecases/roster.js +47 -0
- package/target/esm/core/hub/usecases/send.js +78 -0
- package/target/esm/core/hub/usecases/staging.js +60 -0
- package/target/esm/{app → core/hub/usecases}/view.js +33 -33
- package/target/esm/core/hub/work/index.js +13 -0
- package/target/esm/core/hub/work/parse.js +37 -0
- package/target/esm/core/hub/work/task.js +68 -0
- package/target/esm/core/signing/index.js +4 -0
- package/target/esm/index.js +7 -6
- package/target/esm/lib/fail.js +18 -0
- package/target/esm/lib/index.js +20 -195
- package/target/esm/lib/json.js +81 -0
- package/target/dts/app/agent.d.ts +0 -109
- package/target/dts/app/session.d.ts +0 -65
- package/target/dts/cli/context.d.ts +0 -6
- package/target/dts/cli/operator.d.ts +0 -10
- package/target/dts/cli/shared.d.ts +0 -27
- package/target/dts/domain/decisions.d.ts +0 -65
- package/target/dts/domain/signals.d.ts +0 -37
- package/target/dts/heartbeat/platform.d.ts +0 -15
- package/target/dts/heartbeat/tools.d.ts +0 -11
- package/target/dts/home/index.d.ts +0 -21
- package/target/dts/hub/index.d.ts +0 -72
- package/target/dts/runtime/watch.d.ts +0 -17
- package/target/dts/store/git/index.d.ts +0 -92
- package/target/dts/store/git/remote.d.ts +0 -95
- package/target/dts/store/kafka/index.d.ts +0 -2
- package/target/dts/store/temporal/index.d.ts +0 -2
- package/target/esm/app/budget.js +0 -95
- package/target/esm/app/context.js +0 -16
- package/target/esm/app/hubs.js +0 -76
- package/target/esm/app/room.js +0 -181
- package/target/esm/app/session.js +0 -99
- package/target/esm/cli/context.js +0 -8
- package/target/esm/cli/operator.js +0 -185
- package/target/esm/cli/shared.js +0 -159
- package/target/esm/cli/skill.js +0 -54
- package/target/esm/domain/decisions.js +0 -178
- package/target/esm/domain/profiles.js +0 -96
- package/target/esm/domain/signals.js +0 -134
- package/target/esm/heartbeat/platform.js +0 -83
- package/target/esm/heartbeat/tools.js +0 -68
- package/target/esm/home/index.js +0 -88
- package/target/esm/hub/index.js +0 -224
- package/target/esm/protocol/index.js +0 -219
- package/target/esm/runtime/reader.js +0 -65
- package/target/esm/runtime/session.js +0 -116
- package/target/esm/runtime/watch.js +0 -321
- package/target/esm/store/git/index.js +0 -719
- package/target/esm/store/git/remote.js +0 -928
- package/target/esm/store/index.js +0 -15
- package/target/esm/store/kafka/index.js +0 -3
- package/target/esm/store/temporal/index.js +0 -3
- /package/target/dts/{domain/index.d.ts → core/hub/capabilities.d.ts} +0 -0
- /package/target/dts/{protocol → core/hub/protocol}/files.d.ts +0 -0
|
@@ -1,719 +0,0 @@
|
|
|
1
|
-
// git-store: the Store port on git. The store directory holds one bare repository (`store/.git`):
|
|
2
|
-
// refs/heads/main the shared contract, one commit per amendment
|
|
3
|
-
// refs/heads/<room> the room: one linear branch, like a chat. Its first commit is the room's genesis
|
|
4
|
-
// (the root's, on top of main); then every message is a commit by its author, and
|
|
5
|
-
// every control record (Kind: room) a commit by the root
|
|
6
|
-
// refs/tags/<agent>.<signal>.<message12> signals: annotated tags without text on the message's commit
|
|
7
|
-
// refs/archive/<room>@<genesis8> an archived room, read-only
|
|
8
|
-
// refs/ringi/accepted/<genesis>/<reader>/<author> readers' pins of each author's accepted last message
|
|
9
|
-
// A member's log is its own commits in the room's branch, in branch order. The control files at the root of a room's
|
|
10
|
-
// tree (members, profile, signers) change only in the root's control records.
|
|
11
|
-
// A signed hub signs every commit and tag with its author's key and verifies what it fetches (remote.ts).
|
|
12
|
-
import fs from 'node:fs';
|
|
13
|
-
import path from 'node:path';
|
|
14
|
-
import crypto from 'node:crypto';
|
|
15
|
-
import { spawnSync } from 'node:child_process';
|
|
16
|
-
import { cannotWrite, mkdirIn, SANDBOX } from '../../lib/index.js';
|
|
17
|
-
import * as Remote from './remote.js';
|
|
18
|
-
import { fail, locked, note, strictJson, writeJsonAtomic } from '../../lib/index.js';
|
|
19
|
-
import { AGENT_RE, ID_RE, TEXT_MAX, checkOutgoing, compose, parse } from '../../protocol/index.js';
|
|
20
|
-
import { checkCollisions, checkPath } from '../../protocol/files.js';
|
|
21
|
-
const ZERO = '0'.repeat(40);
|
|
22
|
-
const ident = (id) => ({ GIT_AUTHOR_NAME: id, GIT_AUTHOR_EMAIL: `${id}@ringi`, GIT_COMMITTER_NAME: id, GIT_COMMITTER_EMAIL: `${id}@ringi` });
|
|
23
|
-
export const runGit = (args, o = {}) => {
|
|
24
|
-
const r = spawnSync('git', args, { input: o.input, encoding: 'utf8', env: Remote.gitEnv(o.env), maxBuffer: 1 << 30, timeout: Remote.gitTimeout() });
|
|
25
|
-
return check(args, r, o.where).stdout;
|
|
26
|
-
};
|
|
27
|
-
const runGitOk = (args, o = {}) => {
|
|
28
|
-
const r = spawnSync('git', args, { input: o.input, encoding: 'utf8', env: Remote.gitEnv(o.env), maxBuffer: 1 << 30, timeout: Remote.gitTimeout() });
|
|
29
|
-
Remote.checkGitTimeout(r.error);
|
|
30
|
-
return r;
|
|
31
|
-
};
|
|
32
|
-
const runGitRaw = (args, where, input) => {
|
|
33
|
-
const r = spawnSync('git', args, { input, encoding: 'buffer', env: Remote.gitEnv(), maxBuffer: 1 << 30, timeout: Remote.gitTimeout() });
|
|
34
|
-
return check(args, r, where).stdout;
|
|
35
|
-
};
|
|
36
|
-
function check(args, r, where = 'the hub store') {
|
|
37
|
-
Remote.checkGitTimeout(r.error);
|
|
38
|
-
if (r.error)
|
|
39
|
-
throw r.error;
|
|
40
|
-
if (r.status !== 0) {
|
|
41
|
-
const err = String(r.stderr ?? '').trim();
|
|
42
|
-
if (SANDBOX.test(err))
|
|
43
|
-
cannotWrite(where, err.split('\n').at(-1));
|
|
44
|
-
fail(`git ${args.find(a => !a.startsWith('-') && !a.includes('/')) ?? ''}: ${err.split('\n').at(-1)}`);
|
|
45
|
-
}
|
|
46
|
-
return r;
|
|
47
|
-
}
|
|
48
|
-
const splitBuf = (b, sep) => {
|
|
49
|
-
const parts = [];
|
|
50
|
-
let at = 0;
|
|
51
|
-
for (let i = b.indexOf(sep); i >= 0; i = b.indexOf(sep, at)) {
|
|
52
|
-
parts.push(b.subarray(at, i));
|
|
53
|
-
at = i + 1;
|
|
54
|
-
}
|
|
55
|
-
parts.push(b.subarray(at));
|
|
56
|
-
return parts;
|
|
57
|
-
};
|
|
58
|
-
const REGULAR = new Set(['100644', '100755']);
|
|
59
|
-
// The store is a repository whose work tree holds the contract (`main`) as it is now, so Git clients (SourceTree, IDEs)
|
|
60
|
-
// open `store/` without errors, on the contract, with every room's branch beside it; `git status` stays clean.
|
|
61
|
-
const init = (dir, home) => runGit(['init', '-q', '-b', 'main', path.dirname(dir)], { where: home });
|
|
62
|
-
// The root's principal, and the files only its control records may change.
|
|
63
|
-
const ROOT = 'operator';
|
|
64
|
-
const CONTROL_FILES = new Set(['ROOM.md', 'members.json', 'profile.json', 'allowed_signers']);
|
|
65
|
-
export class GitStore {
|
|
66
|
-
type = 'git';
|
|
67
|
-
trust;
|
|
68
|
-
dir;
|
|
69
|
-
d;
|
|
70
|
-
home;
|
|
71
|
-
// The handle remote.ts works on: the repository and what it learned on the last sync.
|
|
72
|
-
h;
|
|
73
|
-
constructor(hubDir, d, home) {
|
|
74
|
-
this.d = d;
|
|
75
|
-
this.home = home;
|
|
76
|
-
this.trust = d.trust;
|
|
77
|
-
this.dir = path.join(hubDir, d.store.path, '.git');
|
|
78
|
-
this.h = { name: d.name, dir: this.dir, d };
|
|
79
|
-
if (!fs.existsSync(this.dir))
|
|
80
|
-
fail(`hub ${d.name}: no git store at ${this.dir}`);
|
|
81
|
-
this.openable();
|
|
82
|
-
}
|
|
83
|
-
// A store made bare before, or a replica fetched bare, gets its work tree once (and loses the `_ringi` branch of an unreleased build).
|
|
84
|
-
openable() {
|
|
85
|
-
let changed = false;
|
|
86
|
-
if (/^\s*bare\s*=\s*true\s*$/m.test(fs.readFileSync(path.join(this.dir, 'config'), 'utf8'))) {
|
|
87
|
-
this.git(['config', 'core.bare', 'false']);
|
|
88
|
-
changed = true;
|
|
89
|
-
}
|
|
90
|
-
if (fs.readFileSync(path.join(this.dir, 'HEAD'), 'utf8') !== 'ref: refs/heads/main\n') {
|
|
91
|
-
this.git(['symbolic-ref', 'HEAD', 'refs/heads/main']);
|
|
92
|
-
changed = true;
|
|
93
|
-
}
|
|
94
|
-
if (fs.existsSync(path.join(this.dir, 'refs', 'heads', '_ringi'))) {
|
|
95
|
-
this.gitOk(['update-ref', '-d', 'refs/heads/_ringi']);
|
|
96
|
-
changed = true;
|
|
97
|
-
}
|
|
98
|
-
if (changed)
|
|
99
|
-
this.checkout();
|
|
100
|
-
}
|
|
101
|
-
// The work tree and the index follow the contract, so a Git client sees the store clean.
|
|
102
|
-
checkout() {
|
|
103
|
-
if (this.rev('refs/heads/main'))
|
|
104
|
-
this.git(['--work-tree', path.dirname(this.dir), 'read-tree', '-u', '--reset', 'refs/heads/main']);
|
|
105
|
-
}
|
|
106
|
-
args(a) { return ['--git-dir', this.dir, ...a]; }
|
|
107
|
-
git(a, o) { return runGit(this.args(a), { ...o, where: this.home }); }
|
|
108
|
-
gitOk(a, o) { return runGitOk(this.args(a), o); }
|
|
109
|
-
rev(ref) {
|
|
110
|
-
const r = this.gitOk(['rev-parse', '-q', '--verify', `${ref}^{commit}`]);
|
|
111
|
-
return r.status === 0 ? r.stdout.trim() : null;
|
|
112
|
-
}
|
|
113
|
-
isAncestor(a, b) { return this.gitOk(['merge-base', '--is-ancestor', a, b]).status === 0; }
|
|
114
|
-
isRef(s) { return /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(s ?? ''); }
|
|
115
|
-
extends(older, newer) { return older === newer || this.isAncestor(older, newer); }
|
|
116
|
-
// --- writing ---
|
|
117
|
-
// A tree from a base tree without the `drop` paths, with entries put on top; nested paths are fine.
|
|
118
|
-
tree(base, entries, drop = []) {
|
|
119
|
-
const idx = path.join(this.home, 'tmp', `index.${process.pid}.${crypto.randomBytes(4).toString('hex')}`);
|
|
120
|
-
mkdirIn(this.home, path.dirname(idx));
|
|
121
|
-
const env = { GIT_INDEX_FILE: idx };
|
|
122
|
-
try {
|
|
123
|
-
this.git(['read-tree', ...(base ? [base] : ['--empty'])], { env });
|
|
124
|
-
if (drop.length)
|
|
125
|
-
this.git(['update-index', '--index-info'], { env, input: drop.map(p => `0 ${ZERO}\t${p}\n`).join('') });
|
|
126
|
-
for (const e of entries) {
|
|
127
|
-
const sha = e.blob ?? this.git(['hash-object', '-w', ...(e.file ? ['--', e.file] : ['--stdin'])], { input: e.content }).trim();
|
|
128
|
-
this.git(['update-index', '--add', '--cacheinfo', `${e.mode},${sha},${e.path}`], { env });
|
|
129
|
-
}
|
|
130
|
-
return this.git(['write-tree'], { env }).trim();
|
|
131
|
-
}
|
|
132
|
-
finally {
|
|
133
|
-
fs.rmSync(idx, { force: true });
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
// Every commit is made here: on a signed hub, signed by its author's key on this machine.
|
|
137
|
-
commit(who, text, tree, parents) {
|
|
138
|
-
return this.git(['commit-tree', ...Remote.sign(this.h, who, this.home), tree, ...parents.flatMap(p => ['-p', p])], { input: text, env: ident(who) }).trim();
|
|
139
|
-
}
|
|
140
|
-
advance(ref, sha, old) {
|
|
141
|
-
if (this.gitOk(['update-ref', ref, sha, old ?? ZERO]).status !== 0)
|
|
142
|
-
return false;
|
|
143
|
-
// A signed hub publishes at once; a rejected push keeps the commit here and alarms (remote.ts).
|
|
144
|
-
return Remote.push(this.h, ref, { behind: !!old && ref !== 'refs/heads/main' });
|
|
145
|
-
}
|
|
146
|
-
static create(hubDir, d, home, contract, text) {
|
|
147
|
-
const dir = path.join(hubDir, d.store.path, '.git');
|
|
148
|
-
// A signed hub's repository already exists: rootFor made it with the root key.
|
|
149
|
-
if (!fs.existsSync(dir)) {
|
|
150
|
-
mkdirIn(home, path.dirname(dir));
|
|
151
|
-
init(dir, home);
|
|
152
|
-
}
|
|
153
|
-
const s = new GitStore(hubDir, d, home);
|
|
154
|
-
const sha = s.commit('operator', text, s.tree(null, Object.entries(contract).map(([p, c]) => ({ path: p, content: c, mode: '100644' }))), []);
|
|
155
|
-
if (d.trust === 'signed')
|
|
156
|
-
d.genesis = sha;
|
|
157
|
-
if (!s.advance('refs/heads/main', sha, null))
|
|
158
|
-
fail(`hub ${d.name}: main already exists`);
|
|
159
|
-
s.checkout();
|
|
160
|
-
return s;
|
|
161
|
-
}
|
|
162
|
-
// A new signed hub: the root key is generated on this machine and pinned in the descriptor.
|
|
163
|
-
static rootFor(hubDir, d, home) {
|
|
164
|
-
const dir = path.join(hubDir, d.store.path, '.git');
|
|
165
|
-
mkdirIn(home, path.dirname(dir));
|
|
166
|
-
init(dir, home);
|
|
167
|
-
return Remote.setupSigned({ name: d.name, dir, d }, home, d.store.remote).root;
|
|
168
|
-
}
|
|
169
|
-
sync() {
|
|
170
|
-
const main = this.rev('refs/heads/main');
|
|
171
|
-
Remote.sync(this.h);
|
|
172
|
-
if (this.rev('refs/heads/main') !== main)
|
|
173
|
-
this.checkout();
|
|
174
|
-
}
|
|
175
|
-
// --- the contract ---
|
|
176
|
-
contractHead() { return this.rev('refs/heads/main') ?? fail(`hub ${this.d.name} has no contract (main)`); }
|
|
177
|
-
contractFile(rev, name) {
|
|
178
|
-
const r = this.gitOk(['show', `${rev}:${name}`]);
|
|
179
|
-
return r.status === 0 ? r.stdout : null;
|
|
180
|
-
}
|
|
181
|
-
contractFiles(rev) { return this.git(['ls-tree', '-r', '--name-only', rev]).split('\n').filter(Boolean); }
|
|
182
|
-
amendContract(who, text, files) {
|
|
183
|
-
const head = this.contractHead();
|
|
184
|
-
const sha = this.commit(who, text, this.tree(`${head}^{tree}`, Object.entries(files).map(([p, c]) => ({ path: p, content: c, mode: '100644' }))), [head]);
|
|
185
|
-
if (!this.advance('refs/heads/main', sha, head))
|
|
186
|
-
fail('the contract moved while amending; retry');
|
|
187
|
-
this.checkout();
|
|
188
|
-
return sha;
|
|
189
|
-
}
|
|
190
|
-
// --- rooms ---
|
|
191
|
-
ref(name) { return name.includes('@') ? `refs/archive/${name}` : `refs/heads/${name}`; }
|
|
192
|
-
transition(name) { return `refs/ringi/migrating/${name}`; }
|
|
193
|
-
tip(name) { return this.rev(this.ref(name)) ?? this.rev(this.transition(name)); }
|
|
194
|
-
// Rooms in ringi 0.1's layout (`<room>/control` and a branch per agent) are listed, and refused when opened.
|
|
195
|
-
roomNames(archived) {
|
|
196
|
-
const prefix = archived ? 'refs/archive/' : 'refs/heads/';
|
|
197
|
-
const names = this.git(['for-each-ref', '--format=%(refname)', prefix]).split('\n').filter(Boolean).map(r => r.slice(prefix.length))
|
|
198
|
-
.map(n => n.replace(/\/control$/, '')).filter(n => ID_RE.test(n.split('@')[0]) && n !== 'main' && n.includes('@') === archived);
|
|
199
|
-
return [...new Set(names)].sort();
|
|
200
|
-
}
|
|
201
|
-
// A control record: the room's genesis, or a root commit of Kind: room.
|
|
202
|
-
isControl(room, sha, author, text) {
|
|
203
|
-
return sha === room.genesis || (author === ROOT && parse(text, x => this.isRef(x)).kind === 'room');
|
|
204
|
-
}
|
|
205
|
-
// The room's branch, oldest first, from its genesis. The last scan of each room is kept and grown by what the tip
|
|
206
|
-
// added on top of it.
|
|
207
|
-
scanned = new Map();
|
|
208
|
-
entries(room, tip) {
|
|
209
|
-
const last = this.scanned.get(room.genesis);
|
|
210
|
-
if (last?.tip === tip)
|
|
211
|
-
return last.entries;
|
|
212
|
-
const added = last ? this.scan(room, tip, last.tip) : [];
|
|
213
|
-
const entries = last && added.length && added[0].parent === last.tip ? [...last.entries, ...added] : this.scan(room, tip);
|
|
214
|
-
const judged = this.judged(room, tip, entries);
|
|
215
|
-
this.scanned.set(room.genesis, { tip, entries: judged });
|
|
216
|
-
return judged;
|
|
217
|
-
}
|
|
218
|
-
// On a signed hub the branch's verification says which commits are control records, and why the others that are no
|
|
219
|
-
// valid message are skipped; a commit off the branch's first parents is never a message (remote.ts).
|
|
220
|
-
judged(room, tip, entries) {
|
|
221
|
-
if (this.trust !== 'signed' || room.archived)
|
|
222
|
-
return entries;
|
|
223
|
-
const b = Remote.branch(this.h, { name: room.name, ref: this.ref(room.name), genesis: room.genesis }, tip);
|
|
224
|
-
const control = new Set(b.control);
|
|
225
|
-
return entries.map(e => ({ ...e, control: control.has(e.sha),
|
|
226
|
-
skipped: control.has(e.sha) || b.authors.has(e.sha) ? undefined : b.skipped.get(e.sha) ?? 'not on the room\'s branch' }));
|
|
227
|
-
}
|
|
228
|
-
scan(room, tip, after) {
|
|
229
|
-
const shas = this.git(['rev-list', '--topo-order', '--reverse', tip, '--not', after ?? `${room.genesis}^@`]).trim().split('\n').filter(Boolean);
|
|
230
|
-
return Remote.readCommits(this.h, shas).map(c => {
|
|
231
|
-
const text = c.body.toString('utf8');
|
|
232
|
-
return { sha: c.sha, parent: c.parents[0], author: c.author, text, control: this.isControl(room, c.sha, c.author, text) };
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
room(name) {
|
|
236
|
-
const ref = this.ref(name);
|
|
237
|
-
const tip = this.tip(name);
|
|
238
|
-
if (!tip && this.rev(`${ref}/control`))
|
|
239
|
-
fail(`room ${name} is in the layout of ringi 0.1; run ringi room migrate ${name} --hub ${this.d.name} to keep its history in one branch`);
|
|
240
|
-
if (!tip)
|
|
241
|
-
return null;
|
|
242
|
-
const genesis = this.git(['rev-list', '--first-parent', '--reverse', tip, '^refs/heads/main']).split('\n')[0]
|
|
243
|
-
|| this.git(['rev-list', '--first-parent', '--reverse', tip]).split('\n').filter(Boolean).at(-1);
|
|
244
|
-
const archived = name.includes('@');
|
|
245
|
-
if (this.trust === 'signed' && !archived && this.h.remoteAlarms?.[ref])
|
|
246
|
-
fail(`ALARM ${this.h.remoteAlarms[ref]}; room ${name} is quarantined`);
|
|
247
|
-
const control = this.entries({ name, genesis, archived }, tip).filter(e => e.control).map(e => ({ rev: e.sha, text: e.text }));
|
|
248
|
-
return { name, archived, head: control.at(-1).rev, genesis, tip, base: this.rev(`${genesis}^`) ?? ZERO, control };
|
|
249
|
-
}
|
|
250
|
-
// Control files are read at a control record, never at the branch's tip.
|
|
251
|
-
controlFile(room, name, rev) {
|
|
252
|
-
const r = this.gitOk(['show', `${rev ?? room.head}:${name}`]);
|
|
253
|
-
return r.status === 0 ? r.stdout : null;
|
|
254
|
-
}
|
|
255
|
-
createRoom(name, who, text, files) {
|
|
256
|
-
const ref = `refs/heads/${name}`;
|
|
257
|
-
if (this.rev(ref))
|
|
258
|
-
fail(`room ${name} exists in hub ${this.d.name}`);
|
|
259
|
-
const sha = this.commit(who, text, this.tree(null, Object.entries(files).map(([p, c]) => ({ path: p, content: c, mode: '100644' }))), [this.contractHead()]);
|
|
260
|
-
if (!this.advance(ref, sha, null))
|
|
261
|
-
fail(`room ${name} was created meanwhile`);
|
|
262
|
-
return sha;
|
|
263
|
-
}
|
|
264
|
-
// Old messages keep their ids and parents. One control record joins the old tips, then all new writes share the
|
|
265
|
-
// room's branch. The transition ref makes the room readable even if the process exits between the two ref
|
|
266
|
-
// transactions (Git cannot create <room> and delete <room>/control in the same transaction).
|
|
267
|
-
migrateRoom(name) {
|
|
268
|
-
if (this.trust !== 'asserted')
|
|
269
|
-
fail('legacy room migration is only supported for local asserted hubs');
|
|
270
|
-
if (!ID_RE.test(name) || name === 'main')
|
|
271
|
-
fail('expected a room id');
|
|
272
|
-
const ref = this.ref(name), pending = this.transition(name);
|
|
273
|
-
const source = this.rev(pending) ?? this.rev(ref) ?? this.rev(`${ref}/control`) ?? fail(`no legacy room ${name}`);
|
|
274
|
-
const genesis = this.git(['rev-list', '--first-parent', '--reverse', source, '^refs/heads/main']).trim().split('\n')[0];
|
|
275
|
-
const readers = path.join(this.home, 'readers', this.d.name, genesis);
|
|
276
|
-
const files = fs.existsSync(readers) ? fs.readdirSync(readers).filter(f => f.endsWith('.json')).sort() : [];
|
|
277
|
-
// Readers pin tag objects while holding these same locks. Keep their state stable across both ref transactions
|
|
278
|
-
// and the explicit conversion of their old accepted objects; no new baseline is supplied.
|
|
279
|
-
const protect = (at) => at === files.length ? migrate() : locked(path.join(this.home, 'locks', `reader-${this.d.name}-${genesis.slice(0, 12)}-${files[at].slice(0, -5)}.lock`), () => protect(at + 1));
|
|
280
|
-
const migrate = () => {
|
|
281
|
-
if (!genesis || this.git(['show', '-s', '--format=%s', genesis]).trim() !== `room ${name}`)
|
|
282
|
-
fail('legacy room genesis does not name this room');
|
|
283
|
-
const finish = (sha) => {
|
|
284
|
-
this.git(['update-ref', '--stdin'], { input: `start\ncreate ${ref} ${sha}\ndelete ${pending} ${sha}\nprepare\ncommit\n` });
|
|
285
|
-
return sha;
|
|
286
|
-
};
|
|
287
|
-
const staged = this.rev(pending);
|
|
288
|
-
const current = this.rev(ref);
|
|
289
|
-
if (staged || current) {
|
|
290
|
-
const tip = staged ?? current;
|
|
291
|
-
const tags = this.migrateSignals(name, genesis, [tip], files.map(f => path.join(readers, f)));
|
|
292
|
-
if (staged)
|
|
293
|
-
finish(staged);
|
|
294
|
-
tags();
|
|
295
|
-
return tip;
|
|
296
|
-
}
|
|
297
|
-
const ctl = `${ref}/control`, head = this.rev(ctl) ?? fail(`no legacy room ${name}`);
|
|
298
|
-
const logs = this.refsUnder(`${ref}/log/`);
|
|
299
|
-
for (const [r, sha] of [[ctl, head], ...logs]) {
|
|
300
|
-
const who = r === ctl ? ROOT : r.slice(`${ref}/log/`.length);
|
|
301
|
-
if (!ID_RE.test(who) || !this.extends(genesis, sha))
|
|
302
|
-
fail(`ALARM ${r} does not descend from its room genesis`);
|
|
303
|
-
const commits = this.git(['rev-list', '--parents', '--reverse', sha, `^${genesis}`]).trim().split('\n').filter(Boolean);
|
|
304
|
-
let parent = genesis;
|
|
305
|
-
for (const row of commits) {
|
|
306
|
-
const [id, ...parents] = row.split(' ');
|
|
307
|
-
if (parents.length !== 1 || parents[0] !== parent)
|
|
308
|
-
fail(`ALARM ${r} is not linear`);
|
|
309
|
-
if (this.git(['show', '-s', '--format=%an', id]).trim() !== who)
|
|
310
|
-
fail(`ALARM ${r} contains another author's message`);
|
|
311
|
-
parent = id;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
// Accepted heads must still be present before any ref moves. Migration never supplies a fresh baseline.
|
|
315
|
-
const byAuthor = new Map(logs.map(([r, sha]) => [r.slice(`${ref}/log/`.length), sha]));
|
|
316
|
-
const checkAccepted = (who, sha) => {
|
|
317
|
-
const tip = byAuthor.get(who);
|
|
318
|
-
if (!tip || !this.extends(sha, tip))
|
|
319
|
-
fail(`ALARM log ${who} lost accepted history; migration stopped`);
|
|
320
|
-
};
|
|
321
|
-
for (const [r, sha] of this.refsUnder(`refs/ringi/accepted/${genesis}/`))
|
|
322
|
-
checkAccepted(r.split('/').at(-1), sha);
|
|
323
|
-
for (const f of files) {
|
|
324
|
-
const st = strictJson(fs.readFileSync(path.join(readers, f), 'utf8'), f);
|
|
325
|
-
if (st.control && !this.extends(st.control, head))
|
|
326
|
-
fail('ALARM room control lost accepted history; migration stopped');
|
|
327
|
-
for (const [who, sha] of Object.entries(st.heads ?? {}))
|
|
328
|
-
checkAccepted(who, sha);
|
|
329
|
-
}
|
|
330
|
-
const tags = this.migrateSignals(name, genesis, [head, ...logs.map(([, id]) => id)], files.map(f => path.join(readers, f)));
|
|
331
|
-
const sha = this.commit(ROOT, compose(`migrate room ${name}`, [['To', '*'], ['Kind', 'room'], ['Ringi-Layout', 'branch/1']]), this.git(['rev-parse', `${head}^{tree}`]).trim(), [...new Set([head, ...logs.map(([, id]) => id)])]);
|
|
332
|
-
const old = [[ctl, head], ...logs];
|
|
333
|
-
const backup = `refs/ringi/legacy/${genesis}/`;
|
|
334
|
-
this.git(['update-ref', '--stdin'], { input: ['start', `create ${pending} ${sha}`,
|
|
335
|
-
...old.flatMap(([r, id]) => [`create ${backup}${r.slice(`${ref}/`.length)} ${id}`, `delete ${r} ${id}`]),
|
|
336
|
-
'prepare', 'commit', ''].join('\n') });
|
|
337
|
-
finish(sha);
|
|
338
|
-
tags();
|
|
339
|
-
return sha;
|
|
340
|
-
};
|
|
341
|
-
mkdirIn(this.home, path.join(this.home, 'locks'));
|
|
342
|
-
return protect(0);
|
|
343
|
-
}
|
|
344
|
-
// An asserted hub can explicitly convert its old tag annotations, keeping their target, tagger and date. The
|
|
345
|
-
// original objects and reader snapshots remain evidence. Ref changes are atomic; a repeat resumes reader writes
|
|
346
|
-
// after a crash without replacing an accepted object that changed or disappeared in either format.
|
|
347
|
-
migrateSignals(room, genesis, tips, readers) {
|
|
348
|
-
const prefix = `refs/tags/ringi/${genesis}/`, backup = `refs/ringi/legacy/${genesis}/signals/`;
|
|
349
|
-
const old = new Map(this.refsUnder(prefix)), saved = new Map(this.refsUnder(backup));
|
|
350
|
-
const liveTags = new Map(this.refsUnder('refs/tags/'));
|
|
351
|
-
const refs = [];
|
|
352
|
-
const changes = new Map();
|
|
353
|
-
const identities = new Map();
|
|
354
|
-
const targets = new Set(this.git(['rev-list', ...tips, `^${genesis}`]).trim().split('\n').filter(Boolean));
|
|
355
|
-
const kept = (sha) => tips.some(tip => this.extends(sha, tip));
|
|
356
|
-
for (const [ref, sha] of this.refsUnder(`refs/ringi/accepted/${genesis}/`)) {
|
|
357
|
-
if (!kept(sha))
|
|
358
|
-
fail(`ALARM ${ref} lost accepted history; migration stopped`);
|
|
359
|
-
}
|
|
360
|
-
for (const suffix of new Set([...old.keys()].map(r => r.slice(prefix.length)).concat([...saved.keys()].map(r => r.slice(backup.length))))) {
|
|
361
|
-
const before = saved.get(backup + suffix) ?? old.get(prefix + suffix);
|
|
362
|
-
const liveOld = old.get(prefix + suffix), recorded = saved.get(backup + suffix);
|
|
363
|
-
if (recorded && liveOld && recorded !== liveOld)
|
|
364
|
-
fail(`ALARM signal ${prefix + suffix} changed after migration; stopped`);
|
|
365
|
-
const m = /^([^/]+)\/((?:[0-9a-f]{40}|[0-9a-f]{64}))\.([a-z][a-z0-9.-]*)$/.exec(suffix);
|
|
366
|
-
if (!m || !AGENT_RE.test(m[1]))
|
|
367
|
-
return fail(`ALARM signal ${prefix + suffix} cannot be migrated to an unambiguous agent id`);
|
|
368
|
-
const [, actor, target, signal] = m;
|
|
369
|
-
if (this.git(['cat-file', '-t', before]).trim() !== 'tag')
|
|
370
|
-
fail(`ALARM signal ${prefix + suffix} is not annotated`);
|
|
371
|
-
const text = this.git(['cat-file', 'tag', before]);
|
|
372
|
-
const split = text.indexOf('\n\n'), header = text.slice(0, split), body = text.slice(split + 2);
|
|
373
|
-
const h = /^object ([0-9a-f]+)\ntype commit\ntag ([^\n]+)\n(tagger ([^<\n]+) <[^\n]*> \d+ [+-]\d{4})$/.exec(header);
|
|
374
|
-
if (!h || h[1] !== target || h[2] !== (prefix + suffix).slice('refs/tags/'.length) || h[4].trim() !== actor || !targets.has(target))
|
|
375
|
-
return fail(`ALARM signal ${prefix + suffix} disagrees with its name, actor or room target`);
|
|
376
|
-
if (body.includes('-----BEGIN'))
|
|
377
|
-
fail('signed legacy signals need their authors to sign the new names; migration stopped');
|
|
378
|
-
const p = parse(body, s => this.isRef(s));
|
|
379
|
-
for (const [k, value] of [['room', room], ['genesis', genesis], ['actor', actor], ['message', target], ['signal', signal]]) {
|
|
380
|
-
if (p.trailers[k]?.length !== 1 || p.trailers[k][0] !== value)
|
|
381
|
-
fail(`ALARM signal ${prefix + suffix} has inconsistent legacy metadata`);
|
|
382
|
-
}
|
|
383
|
-
const key = `${actor}.${signal}.${target.slice(0, 12)}`, ref = `refs/tags/${key}`;
|
|
384
|
-
const after = this.git(['mktag'], { input: `object ${target}\ntype commit\ntag ${key}\n${h[3]}\n\n` }).trim();
|
|
385
|
-
const existing = changes.get(key), live = liveTags.get(ref);
|
|
386
|
-
if ((existing && existing.after !== after) || (live && live !== after))
|
|
387
|
-
fail(`ALARM signal ${key} collides with another tag; migration stopped`);
|
|
388
|
-
if (recorded && !live)
|
|
389
|
-
fail(`ALARM migrated signal ${key} was removed; migration stopped`);
|
|
390
|
-
changes.set(key, { before, after });
|
|
391
|
-
identities.set(before, { key, target, signal });
|
|
392
|
-
identities.set(after, { key, target, signal });
|
|
393
|
-
if (!recorded)
|
|
394
|
-
refs.push(`create ${backup + suffix} ${before}`);
|
|
395
|
-
if (!live)
|
|
396
|
-
refs.push(`create ${ref} ${after}`);
|
|
397
|
-
if (liveOld)
|
|
398
|
-
refs.push(`delete ${prefix + suffix} ${liveOld}`);
|
|
399
|
-
}
|
|
400
|
-
const planned = readers.map(file => {
|
|
401
|
-
const text = fs.readFileSync(file, 'utf8');
|
|
402
|
-
const st = strictJson(text, file);
|
|
403
|
-
if (typeof st.control === 'string' && !kept(st.control))
|
|
404
|
-
fail('ALARM room control lost accepted history; migration stopped');
|
|
405
|
-
for (const sha of Object.values(st.heads ?? {})) {
|
|
406
|
-
if (!kept(sha))
|
|
407
|
-
fail('ALARM room log lost accepted history; migration stopped');
|
|
408
|
-
}
|
|
409
|
-
const next = {};
|
|
410
|
-
for (const [original, object] of Object.entries(st.signals ?? {})) {
|
|
411
|
-
const m = /(?:^|\/)([^/]+)\/((?:[0-9a-f]{40}|[0-9a-f]{64}))\.([a-z][a-z0-9.-]*)$/.exec(original);
|
|
412
|
-
// An early reader shortened a full ref to its last component and lost the actor. Its pinned object still
|
|
413
|
-
// proves the identity: recover it only from an exact, verified old object retained by this migration.
|
|
414
|
-
const bare = /^((?:[0-9a-f]{40}|[0-9a-f]{64}))\.([a-z][a-z0-9.-]*)$/.exec(original);
|
|
415
|
-
const identity = bare ? identities.get(object) : undefined;
|
|
416
|
-
if (bare && (!identity || identity.target !== bare[1] || identity.signal !== bare[2]))
|
|
417
|
-
fail(`ALARM accepted signal ${original} lost its verified identity; migration stopped`);
|
|
418
|
-
const key = m ? `${m[1]}.${m[3]}.${m[2].slice(0, 12)}` : identity?.key ?? original;
|
|
419
|
-
const change = changes.get(key);
|
|
420
|
-
if (m && !change)
|
|
421
|
-
fail(`ALARM accepted signal ${original} is missing; migration stopped`);
|
|
422
|
-
if (!change && /^[a-z0-9]+(?:[_-][a-z0-9]+)*\.[a-z][a-z0-9.-]*\.[0-9a-f]{12}$/.test(key)
|
|
423
|
-
&& liveTags.get(`refs/tags/${key}`) !== object)
|
|
424
|
-
fail(`ALARM accepted signal ${original} is missing or replaced; migration stopped`);
|
|
425
|
-
if (change && object !== change.before && object !== change.after)
|
|
426
|
-
fail(`ALARM accepted signal ${original} was replaced; migration stopped`);
|
|
427
|
-
const value = change?.after ?? object;
|
|
428
|
-
if (next[key] && next[key] !== value)
|
|
429
|
-
fail(`ALARM accepted signal ${key} has conflicting objects; migration stopped`);
|
|
430
|
-
next[key] = value;
|
|
431
|
-
}
|
|
432
|
-
return { file, text, st, next, changed: JSON.stringify(st.signals ?? {}) !== JSON.stringify(next) };
|
|
433
|
-
});
|
|
434
|
-
return () => {
|
|
435
|
-
if (refs.length)
|
|
436
|
-
this.git(['update-ref', '--stdin'], { input: ['start', ...refs, 'prepare', 'commit', ''].join('\n') });
|
|
437
|
-
for (const p of planned) {
|
|
438
|
-
if (!p.changed)
|
|
439
|
-
continue;
|
|
440
|
-
// Separate from the live readers directory so the snapshot is never mistaken for another reader.
|
|
441
|
-
const dir = path.join(this.home, 'migration', this.d.name, genesis, 'readers');
|
|
442
|
-
mkdirIn(this.home, dir);
|
|
443
|
-
const snapshot = path.join(dir, `${path.basename(p.file)}.${crypto.createHash('sha256').update(p.text).digest('hex')}`);
|
|
444
|
-
if (!fs.existsSync(snapshot))
|
|
445
|
-
fs.writeFileSync(snapshot, p.text, { flag: 'wx', mode: 0o600 });
|
|
446
|
-
else if (fs.readFileSync(snapshot, 'utf8') !== p.text)
|
|
447
|
-
fail('ALARM migration reader snapshot changed; migration stopped');
|
|
448
|
-
writeJsonAtomic(p.file, { ...p.st, signals: p.next });
|
|
449
|
-
}
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
// A control record on top of the branch: the control files come from the last control record, never from the tip,
|
|
453
|
-
// and the messages' files are kept.
|
|
454
|
-
updateControl(room, who, text, files) {
|
|
455
|
-
const kept = [...CONTROL_FILES].flatMap(n => { const c = this.controlFile(room, n); return c === null ? [] : [[n, c]]; });
|
|
456
|
-
const all = { ...Object.fromEntries(kept), ...files };
|
|
457
|
-
return this.onTop(room, who, text, Object.entries(all).map(([p, c]) => ({ path: p, content: c, mode: '100644' })), [...CONTROL_FILES]);
|
|
458
|
-
}
|
|
459
|
-
refsUnder(prefix) {
|
|
460
|
-
return this.git(['for-each-ref', '--format=%(refname) %(objectname)', prefix]).split('\n').filter(Boolean).map(l => l.split(' '));
|
|
461
|
-
}
|
|
462
|
-
// The branch moves under the archive; its signals stay where they are, on its commits.
|
|
463
|
-
archiveRoom(room, as) {
|
|
464
|
-
const tip = this.rev(this.ref(room.name)) ?? fail(`room ${room.name} is gone`);
|
|
465
|
-
this.git(['update-ref', `refs/archive/${as}`, tip, ZERO]);
|
|
466
|
-
this.git(['update-ref', '-d', this.ref(room.name), tip]);
|
|
467
|
-
}
|
|
468
|
-
// Every ref of the room in one transaction: its branch (or archive) and a pending migration, every tag on its
|
|
469
|
-
// commits (signals, ringi 0.1's, anyone's), what the store keeps under its genesis (pins, ringi 0.1's refs, evidence)
|
|
470
|
-
// and its unpublished commits; then the migration's backup of its readers. Objects are left to the repository.
|
|
471
|
-
deleteRoom(room) {
|
|
472
|
-
const commits = this.commitsOf(room);
|
|
473
|
-
const tags = this.git(['for-each-ref', '--format=%(refname) %(objectname) %(*objectname)', 'refs/tags/']).split('\n').filter(Boolean)
|
|
474
|
-
.map(l => l.split(' ')).filter(([ref, object, target]) => commits.has(target || object) || ref.startsWith(`refs/tags/ringi/${room.genesis}/`)).map(([ref]) => ref);
|
|
475
|
-
const kept = this.refsUnder('refs/ringi/').map(([ref]) => ref).filter(ref => {
|
|
476
|
-
const [, , kind, key] = ref.split('/');
|
|
477
|
-
return key === room.genesis || (kind === 'unpublished' && commits.has(key));
|
|
478
|
-
});
|
|
479
|
-
const refs = [this.ref(room.name), this.transition(room.name)].filter(r => this.rev(r)).concat(tags, kept);
|
|
480
|
-
this.git(['update-ref', '--stdin'], { input: ['start', ...refs.map(r => `delete ${r}`), 'prepare', 'commit', ''].join('\n') });
|
|
481
|
-
fs.rmSync(path.join(this.home, 'migration', this.d.name, room.genesis), { recursive: true, force: true });
|
|
482
|
-
}
|
|
483
|
-
remote(room) {
|
|
484
|
-
return { name: room.name, ref: this.ref(room.name), head: room.head, genesis: room.genesis, archived: room.archived };
|
|
485
|
-
}
|
|
486
|
-
roster(room) {
|
|
487
|
-
if (this.trust !== 'signed' || room.archived)
|
|
488
|
-
return null;
|
|
489
|
-
const v = Remote.verifyRoom(this.h, this.remote(room));
|
|
490
|
-
return { members: v.members, signers: v.entries };
|
|
491
|
-
}
|
|
492
|
-
// --- logs ---
|
|
493
|
-
// Logs are read at the branch's live tip, as a reader sees the room now.
|
|
494
|
-
live(room) {
|
|
495
|
-
const tip = this.tip(room.name);
|
|
496
|
-
return tip ? this.entries(room, tip).filter(e => !e.control && !e.skipped) : [];
|
|
497
|
-
}
|
|
498
|
-
// What a signed hub skips in the room's branch, as alarms; the room reads on without it.
|
|
499
|
-
alarms(room) {
|
|
500
|
-
const tip = this.tip(room.name);
|
|
501
|
-
return tip ? this.entries(room, tip).filter(e => e.skipped).map(e => `commit ${e.sha.slice(0, 12)} by ${e.author || '(none)'}: ${e.skipped}`) : [];
|
|
502
|
-
}
|
|
503
|
-
logIds(room) { return [...new Set(this.live(room).map(e => e.author))]; }
|
|
504
|
-
logHead(room, id) { return this.live(room).filter(e => e.author === id).at(-1)?.sha ?? null; }
|
|
505
|
-
// One author's messages after the genesis up to `head`, oldest first, read as bytes: the text is the commit message,
|
|
506
|
-
// the files are the explicit Ringi-Files list, or the paths the commit adds or changes for records without it.
|
|
507
|
-
messages(room, id, head) {
|
|
508
|
-
const tip = this.trust === 'signed' ? this.tip(room.name) : null;
|
|
509
|
-
const skipped = new Set(tip ? this.entries(room, tip).filter(e => e.skipped).map(e => e.sha) : []);
|
|
510
|
-
const raw = runGitRaw(this.args(['log', '--topo-order', '--reverse', '--no-renames', '--raw', '--no-abbrev', '--format=%x1e%H%x1f%P%x1f%ct%x1f', head, `^${room.genesis}`]), this.home);
|
|
511
|
-
const records = splitBuf(raw, 0x1e).filter(b => b.length).map(rec => splitBuf(rec, 0x1f));
|
|
512
|
-
const commits = new Map(Remote.readCommits(this.h, records.map(([sha]) => sha.toString())).map(c => [c.sha, c]));
|
|
513
|
-
return records.flatMap(([sha, parent, ct, rest = Buffer.alloc(0)]) => {
|
|
514
|
-
const { author, body } = commits.get(sha.toString());
|
|
515
|
-
if (author !== id || this.isControl(room, sha.toString(), id, body.toString('utf8')) || skipped.has(sha.toString()))
|
|
516
|
-
return [];
|
|
517
|
-
let files = rest.toString('utf8').split('\n').map(l => /^:\d{6} (\d{6}) \S+ \S+ ([A-Z])\d*\t(.*)$/.exec(l)).filter((x) => !!x)
|
|
518
|
-
.filter(([, , status]) => status !== 'D').map(([, mode, , p]) => ({ path: p, mode }));
|
|
519
|
-
const declared = parse(body.toString('utf8'), s => this.isRef(s)).trailers['ringi-files'];
|
|
520
|
-
const refused = [];
|
|
521
|
-
if (declared) {
|
|
522
|
-
let names = [];
|
|
523
|
-
try {
|
|
524
|
-
const paths = JSON.parse(declared[0]);
|
|
525
|
-
if (declared.length !== 1 || !Array.isArray(paths) || !paths.every(p => typeof p === 'string'))
|
|
526
|
-
fail('invalid attachment list');
|
|
527
|
-
names = paths.map(checkPath);
|
|
528
|
-
if (new Set(names).size !== names.length)
|
|
529
|
-
fail('duplicate attachment path');
|
|
530
|
-
checkCollisions(names);
|
|
531
|
-
}
|
|
532
|
-
catch {
|
|
533
|
-
refused.push({ path: 'Ringi-Files', mode: 'invalid', why: 'expected one JSON array of safe, distinct file paths' });
|
|
534
|
-
}
|
|
535
|
-
const wanted = new Set(names);
|
|
536
|
-
files = names.length && !refused.length ? splitBuf(runGitRaw(this.args(['ls-tree', '-r', '-z', sha.toString()]), this.home), 0)
|
|
537
|
-
.map(b => /^(\d+) \w+ [0-9a-f]+\t(.*)$/s.exec(b.toString('utf8'))).filter((x) => !!x)
|
|
538
|
-
.map(([, mode, p]) => ({ path: p, mode })).filter(f => wanted.has(f.path)) : [];
|
|
539
|
-
for (const f of files)
|
|
540
|
-
wanted.delete(f.path);
|
|
541
|
-
if (!refused.length)
|
|
542
|
-
for (const p of wanted)
|
|
543
|
-
files.push({ path: p, mode: 'missing' });
|
|
544
|
-
}
|
|
545
|
-
return [{ id: sha.toString(), parent: parent.toString() || null, time: Number(ct.toString()), text: body,
|
|
546
|
-
files: files.filter(f => REGULAR.has(f.mode)), refused: [...refused, ...files.filter(f => !REGULAR.has(f.mode))] }];
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
blob(file) {
|
|
550
|
-
const id = this.git(['hash-object', '-w', '--', file]).trim();
|
|
551
|
-
return { id, size: Number(this.git(['cat-file', '-s', id]).trim()) };
|
|
552
|
-
}
|
|
553
|
-
// A tree of the staged files under refs/ringi/staged/<genesis>/<who>: git keeps what a ref reaches.
|
|
554
|
-
retain(room, who, entries) {
|
|
555
|
-
const ref = `refs/ringi/staged/${room.genesis}/${who}`;
|
|
556
|
-
if (!entries.length) {
|
|
557
|
-
this.gitOk(['update-ref', '-d', ref]);
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
this.git(['update-ref', ref, this.tree(null, entries)]);
|
|
561
|
-
}
|
|
562
|
-
// One message on top of the room's branch.
|
|
563
|
-
append(room, who, text, entries) {
|
|
564
|
-
if (room.archived)
|
|
565
|
-
fail(`room ${room.name} is archived: read-only`);
|
|
566
|
-
if (checkOutgoing(text, s => this.isRef(s)).trailers['ringi-files'])
|
|
567
|
-
fail('Ringi-Files is store metadata; supply attachments instead');
|
|
568
|
-
const own = entries.find(e => CONTROL_FILES.has(e.path));
|
|
569
|
-
if (own)
|
|
570
|
-
fail(`${own.path} is the room's own file; attach it under another path`);
|
|
571
|
-
if (entries.length)
|
|
572
|
-
text = `${text.trimEnd()}\nRingi-Files: ${JSON.stringify([...new Set(entries.map(e => e.path))])}\n`;
|
|
573
|
-
if (Buffer.byteLength(text) > TEXT_MAX)
|
|
574
|
-
fail('attachment list makes the message exceed 64 KiB; use fewer or shorter paths');
|
|
575
|
-
checkOutgoing(text, s => this.isRef(s));
|
|
576
|
-
return this.onTop(room, who, text, entries);
|
|
577
|
-
}
|
|
578
|
-
// A commit on the branch's tip, compare-and-swap: when another writer got there first, it is made again on the new tip.
|
|
579
|
-
onTop(room, who, text, entries, drop = []) {
|
|
580
|
-
const ref = this.ref(room.name);
|
|
581
|
-
for (let i = 0; i < 8; i++) {
|
|
582
|
-
const tip = this.rev(ref) ?? fail(`room ${room.name} is gone`);
|
|
583
|
-
if (drop.length && this.room(room.name)?.head !== room.head)
|
|
584
|
-
fail('room control moved while writing; retry the operation');
|
|
585
|
-
const tree = entries.length || drop.length ? this.tree(`${tip}^{tree}`, entries, drop) : this.git(['rev-parse', `${tip}^{tree}`]).trim();
|
|
586
|
-
const sha = this.commit(who, text, tree, [tip]);
|
|
587
|
-
if (this.advance(ref, sha, tip))
|
|
588
|
-
return sha;
|
|
589
|
-
}
|
|
590
|
-
return fail(`room ${room.name} kept moving while writing; retry`);
|
|
591
|
-
}
|
|
592
|
-
verifyLog(room, id, head) {
|
|
593
|
-
return this.trust === 'signed' ? Remote.verify(this.h, this.remote(room), id, head) : null;
|
|
594
|
-
}
|
|
595
|
-
pin(room, reader, id, rev) {
|
|
596
|
-
if (!room.archived)
|
|
597
|
-
this.git(['update-ref', `refs/ringi/accepted/${room.genesis}/${reader}/${id}`, rev]);
|
|
598
|
-
}
|
|
599
|
-
unpin(room, reader, id) {
|
|
600
|
-
this.gitOk(['update-ref', '-d', `refs/ringi/accepted/${room.genesis}/${reader}/${id}`]);
|
|
601
|
-
}
|
|
602
|
-
file(rev, p) { return runGitRaw(this.args(['show', `${rev}:${p}`]), this.home); }
|
|
603
|
-
sizes(rev, paths) {
|
|
604
|
-
if (!paths.length)
|
|
605
|
-
return new Map();
|
|
606
|
-
const wanted = new Set(paths);
|
|
607
|
-
return new Map(splitBuf(runGitRaw(this.args(['ls-tree', '-r', '-l', '-z', rev]), this.home), 0)
|
|
608
|
-
.map(b => /^\d+ blob [0-9a-f]+ +(\d+)\t(.*)$/s.exec(b.toString('utf8'))).filter((x) => !!x)
|
|
609
|
-
.filter(([, , p]) => wanted.has(p)).map(([, n, p]) => [p, Number(n)]));
|
|
610
|
-
}
|
|
611
|
-
// --- signals ---
|
|
612
|
-
// The heartbeat of `who` in the room: a tag `<who>.heartbeat.<genesis12>` on the room's last commit here, its text the
|
|
613
|
-
// facts of the beat (heartbeat/). Each beat replaces the object under the same name, compare-and-swap on the one it
|
|
614
|
-
// read, so two writers do not overwrite each other blindly. No reader pins it; its replacement is no alarm.
|
|
615
|
-
// On a signed hub the object is signed by `who` and published with the OID it replaced; a newer beat there wins, and
|
|
616
|
-
// this one is taken back here (remote.ts).
|
|
617
|
-
heartbeat(room, who, target, text) {
|
|
618
|
-
const ref = Remote.heartbeatRef(who, room.genesis), name = ref.slice('refs/tags/'.length);
|
|
619
|
-
const old = this.rev(`${ref}^{tag}`) ? this.git(['rev-parse', ref]).trim() : null;
|
|
620
|
-
const obj = this.trust === 'signed' ? Remote.makeHeartbeat(this.h, this.remote(room), who, target, text, this.home)
|
|
621
|
-
: this.git(['mktag'], { input: `object ${target}\ntype commit\ntag ${name}\ntagger ${who} <${who}@ringi> ${Math.floor(Date.now() / 1000)} +0000\n\n${text}` }).trim();
|
|
622
|
-
if (this.gitOk(['update-ref', ref, obj, old ?? ZERO]).status !== 0)
|
|
623
|
-
fail(`${name} moved meanwhile; the next beat writes it`);
|
|
624
|
-
if (this.trust === 'signed' && !Remote.pushHeartbeat(this.h, this.remote(room), who, obj, old))
|
|
625
|
-
this.gitOk(['update-ref', ...(old ? [ref, old, obj] : ['-d', ref, obj])]);
|
|
626
|
-
return obj;
|
|
627
|
-
}
|
|
628
|
-
// The room's heartbeats as they stand: whose, on which commit, when, and what they say.
|
|
629
|
-
heartbeats(room) {
|
|
630
|
-
const g12 = room.genesis.slice(0, 12);
|
|
631
|
-
const tags = this.git(['for-each-ref', '--format=%(refname) %(objectname) %(objecttype)', 'refs/tags/']).split('\n')
|
|
632
|
-
.map(l => l.split(' ')).filter(([ref, , type]) => type === 'tag' && Remote.heartbeatName(ref ?? '')?.genesis12 === g12);
|
|
633
|
-
const objects = this.tagObjects(tags.map(([, object]) => object));
|
|
634
|
-
// On a signed hub only a verified beat shows.
|
|
635
|
-
const ok = (ref, object) => this.trust !== 'signed' || !Remote.verifyHeartbeat(this.h, this.remote(room), ref, object);
|
|
636
|
-
return tags.flatMap(([ref, object]) => {
|
|
637
|
-
const o = objects.get(object);
|
|
638
|
-
return o && ok(ref, object) ? [{ agent: Remote.heartbeatName(ref).actor, target: o.target, time: o.time, text: o.text }] : [];
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
|
-
// An annotated tag without text in its author's namespace, on the message's commit, signed on a signed hub. The
|
|
642
|
-
// message's id in the name keeps it unique, so writing the same signal twice is a no-op.
|
|
643
|
-
signal(room, who, target, name) {
|
|
644
|
-
const tag = `${who}.${name}`;
|
|
645
|
-
const ref = `refs/tags/${tag}`;
|
|
646
|
-
if (this.gitOk(['rev-parse', '-q', '--verify', ref]).status === 0) {
|
|
647
|
-
Remote.push(this.h, ref);
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
const sign = Remote.sign(this.h, who, this.home);
|
|
651
|
-
const key = sign[0]?.slice(2);
|
|
652
|
-
this.git([...(key ? ['-c', `user.signingkey=${key}`] : []), 'tag', key ? '-s' : '-a', '-F', '-', tag, target], { input: '', env: ident(who) });
|
|
653
|
-
Remote.push(this.h, ref);
|
|
654
|
-
}
|
|
655
|
-
// The tags named `<actor>.<signal>.<message12>`, with the objects they name.
|
|
656
|
-
tags() {
|
|
657
|
-
return this.git(['for-each-ref', '--format=%(refname) %(objectname) %(objecttype)', 'refs/tags/']).split('\n').flatMap(line => {
|
|
658
|
-
const [ref, object, type] = line.split(' ');
|
|
659
|
-
const m = /^refs\/tags\/([a-z0-9]+(?:[_-][a-z0-9]+)*)\.([a-z][a-z0-9.-]*\.[0-9a-f]{12})$/.exec(ref ?? '');
|
|
660
|
-
// A heartbeat is no signal: it is replaced on every beat, and readers judge it apart (heartbeats()).
|
|
661
|
-
return m && !Remote.heartbeatName(ref) ? [{ ref, actor: m[1], name: m[2], object, type }] : [];
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
// Annotated tag objects read whole, size-framed: their target, tagger and text are never split on bytes they hold.
|
|
665
|
-
tagObjects(shas) {
|
|
666
|
-
const out = new Map();
|
|
667
|
-
if (!shas.length)
|
|
668
|
-
return out;
|
|
669
|
-
const raw = runGitRaw(this.args(['cat-file', '--batch']), this.home, Buffer.from(shas.join('\n') + '\n'));
|
|
670
|
-
let at = 0;
|
|
671
|
-
for (const sha of shas) {
|
|
672
|
-
const nl = raw.indexOf(0x0a, at);
|
|
673
|
-
const [got, type, size] = raw.subarray(at, nl).toString('utf8').split(' ');
|
|
674
|
-
const body = raw.subarray(nl + 1, nl + 1 + Number(size));
|
|
675
|
-
at = nl + 1 + Number(size) + 1;
|
|
676
|
-
if (got !== sha || type !== 'tag')
|
|
677
|
-
continue;
|
|
678
|
-
const split = body.indexOf('\n\n');
|
|
679
|
-
const hdr = body.subarray(0, split < 0 ? body.length : split).toString('utf8');
|
|
680
|
-
const text = split < 0 ? '' : body.subarray(split + 2).toString('utf8').split('-----BEGIN SSH SIGNATURE-----')[0];
|
|
681
|
-
const target = /^object ([0-9a-f]+)\ntype commit$/m.exec(hdr)?.[1] ?? '';
|
|
682
|
-
const tagger = /^tagger (.*?) <[^>\n]*> (\d+) [+-]\d{4}$/m.exec(hdr);
|
|
683
|
-
out.set(sha, { target, tagger: tagger?.[1] ?? '', time: Number(tagger?.[2] ?? NaN), text });
|
|
684
|
-
}
|
|
685
|
-
return out;
|
|
686
|
-
}
|
|
687
|
-
// Every commit of the room's branch: a signal belongs to the room whose branch holds its message.
|
|
688
|
-
commitsOf(room) {
|
|
689
|
-
const tip = this.tip(room.name);
|
|
690
|
-
return new Set(tip ? this.entries(room, tip).map(e => e.sha) : []);
|
|
691
|
-
}
|
|
692
|
-
signals(room) {
|
|
693
|
-
const tags = this.tags(), objects = this.tagObjects(tags.filter(t => t.type === 'tag').map(t => t.object));
|
|
694
|
-
const mine = this.commitsOf(room);
|
|
695
|
-
const records = tags.flatMap(t => {
|
|
696
|
-
const o = objects.get(t.object);
|
|
697
|
-
// Only an annotated tag on a commit is a signal; anything else points nowhere.
|
|
698
|
-
if (!mine.has(o?.target || t.object))
|
|
699
|
-
return [];
|
|
700
|
-
return [{ ref: t.ref, object: t.object, name: t.name, actor: t.actor, target: o?.target ?? '', tagger: o?.tagger ?? '', text: o?.text ?? '' }];
|
|
701
|
-
});
|
|
702
|
-
if (this.trust !== 'signed' || room.archived)
|
|
703
|
-
return records;
|
|
704
|
-
return Remote.withVerification(this.h, () => records.filter(s => {
|
|
705
|
-
const why = Remote.verifySignal(this.h, this.remote(room), s.ref, s.object);
|
|
706
|
-
if (why)
|
|
707
|
-
note(`ALARM signal ${s.ref}: ${why}; quarantined and withheld`);
|
|
708
|
-
return !why;
|
|
709
|
-
}));
|
|
710
|
-
}
|
|
711
|
-
// --- signed stores ---
|
|
712
|
-
admit(room, who, key) { return Remote.roomAdmit(this.h, this.remote(room), who, key, this.home); }
|
|
713
|
-
joinKey(room, who) { return Remote.join(this.h, this.remote(room), who, this.home); }
|
|
714
|
-
// A room's transport alarm is the room's, whichever author the operator forgets.
|
|
715
|
-
forgetLog(room, _id) { Remote.forget(this.h, this.remote(room)); }
|
|
716
|
-
rootKey() { return this.d.root ?? fail(`hub ${this.d.name} has no pinned root`); }
|
|
717
|
-
rootRoster() { return Remote.rootRoster(this.h); }
|
|
718
|
-
}
|
|
719
|
-
//# sourceMappingURL=index.js.map
|