@agentshouse/kit 0.1.0-alpha.32 → 0.1.0-alpha.34
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/README.md +52 -10
- package/bin/connect-linux.sh +87 -30
- package/bin/kit.js +1 -0
- package/bootstrap.json +3 -3
- package/package.json +1 -1
- package/src/contract.js +2 -0
- package/src/google/entries.js +4 -1
- package/src/login.js +4 -3
- package/src/sync.js +26 -10
- package/src/telegram-view.js +6 -3
- package/src/telegram.js +44 -4
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ The Skill set Kit installs is Matt Pocock's
|
|
|
12
12
|
On Linux `amd64` or `arm64`, run the published, versioned bootstrap once:
|
|
13
13
|
|
|
14
14
|
```sh
|
|
15
|
-
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
15
|
+
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.34/connect-linux.sh)
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
The bootstrap first establishes Docker Engine from what it observes on the
|
|
@@ -145,6 +145,37 @@ matching directory beneath `/agents/house`, through the resident's lease when
|
|
|
145
145
|
the container runs, and refuses a directory outside the workspace before
|
|
146
146
|
anything reaches House.
|
|
147
147
|
|
|
148
|
+
## Connect a macOS computer
|
|
149
|
+
|
|
150
|
+
On a Mac with Apple silicon or an Intel chip, run the same
|
|
151
|
+
`bash <(curl -fsSL …/connect-linux.sh)` command as
|
|
152
|
+
[a Linux computer](#connect-a-linux-computer). It runs in macOS's own
|
|
153
|
+
`bash` and reads the chip from the hardware, so it selects `arm64` or `amd64`
|
|
154
|
+
correctly even from a terminal running under Rosetta.
|
|
155
|
+
|
|
156
|
+
The bootstrap first establishes Docker Desktop from what it observes. A Docker
|
|
157
|
+
Desktop that already answers is used as it is. Without
|
|
158
|
+
`/Applications/Docker.app` it follows Docker's
|
|
159
|
+
[command-line installation](https://docs.docker.com/desktop/setup/install/mac-install/):
|
|
160
|
+
it downloads `Docker.dmg` for the chip from `desktop.docker.com`, attaches it
|
|
161
|
+
through `sudo hdiutil`, runs its `install --user=<account>` through `sudo`, and
|
|
162
|
+
detaches it. `sudo` asks for the password as it ordinarily does, and the
|
|
163
|
+
installer does not accept Docker's Subscription Service Agreement for you. The
|
|
164
|
+
bootstrap then opens Docker Desktop, including one a restart left stopped, and
|
|
165
|
+
waits up to five minutes for its Linux containers to answer while you accept
|
|
166
|
+
its terms or finish what its window asks. If Docker Desktop is still not ready,
|
|
167
|
+
or the download or the installer fails, the bootstrap names the cause and changes
|
|
168
|
+
nothing further; running it again continues from that state. Docker Desktop
|
|
169
|
+
built for the other chip is refused. The bootstrap reaches `docker` in
|
|
170
|
+
Docker Desktop's app bundle when no terminal has added it to `PATH` yet.
|
|
171
|
+
|
|
172
|
+
Everything after that is the Linux journey above, with `open` as the host
|
|
173
|
+
browser opener. With an HTTPS House, login's container publishes its one
|
|
174
|
+
callback port on the Mac's `127.0.0.1`, so House Login returns from the Mac's
|
|
175
|
+
browser, and no Kit container uses the host network. A loopback House needs
|
|
176
|
+
Docker Desktop's host networking enabled. A `--workspace` root must be a
|
|
177
|
+
directory Docker Desktop shares, such as one under `/Users`.
|
|
178
|
+
|
|
148
179
|
## Building the package
|
|
149
180
|
|
|
150
181
|
The repository checkout is the development source. Build the package that can
|
|
@@ -414,7 +445,8 @@ service to a signed-in account asks only for that service's consent, and each
|
|
|
414
445
|
service keeps its own grant. An address that answers another consent, or a code
|
|
415
446
|
Google refuses to exchange, stores nothing; a declined consent or a ceremony
|
|
416
447
|
that outlives its ten-minute bound cancels the sign-in, and the Kit tries again
|
|
417
|
-
at the next reconciliation.
|
|
448
|
+
at the next reconciliation. A capture that finds the grant revoked removes it,
|
|
449
|
+
reports the entry `signed_out` and opens a new sign-in at once.
|
|
418
450
|
|
|
419
451
|
The resident captures each signed-in entry every 15 minutes, with no command.
|
|
420
452
|
An occurrence never overlaps its entry's previous one, and one missed while the
|
|
@@ -1545,8 +1577,9 @@ kit sync rebootstrap ./replica
|
|
|
1545
1577
|
```
|
|
1546
1578
|
|
|
1547
1579
|
`bootstrap` writes `ROOM.md`, Library text, House-derived projections, and
|
|
1548
|
-
Capture text with
|
|
1549
|
-
|
|
1580
|
+
Capture text, with the provenance House serves on a file's manifest entry as
|
|
1581
|
+
its `_provenance` sidecar, and records one position per selected authority from
|
|
1582
|
+
the bundle, with the log epoch House issued it in, together with
|
|
1550
1583
|
the positive operation scopes House declared for it: `read`, `sync`, `write`,
|
|
1551
1584
|
and `delete`, each a list of paths relative to the authority under which that
|
|
1552
1585
|
operation is admitted. Managed replication needs `read` plus `sync` on a Room;
|
|
@@ -1555,11 +1588,11 @@ with `operation_denied`. Generated `_provenance` files delivered beside authored
|
|
|
1555
1588
|
Library paths are House metadata, not authored Library documents. `pull` presents
|
|
1556
1589
|
each recorded position with its log epoch, applies each enumerated path's whole
|
|
1557
1590
|
content or removal, and advances those positions. A removal, pulled or accepted
|
|
1558
|
-
from `submit`, also removes
|
|
1559
|
-
so a later file can take that path; a
|
|
1560
|
-
local draft, stays. A position older
|
|
1561
|
-
declared retention, or one of a log epoch before House's database
|
|
1562
|
-
is reported as `position_expired`; only `rebootstrap` rebuilds the directory.
|
|
1591
|
+
from `submit`, also removes the file's `_provenance` sidecar and each directory
|
|
1592
|
+
it leaves empty up to the authority, so a later file can take that path; a
|
|
1593
|
+
directory still holding a file, such as a local draft, stays. A position older
|
|
1594
|
+
than House's declared retention, or one of a log epoch before House's database
|
|
1595
|
+
was restored, is reported as `position_expired`; only `rebootstrap` rebuilds the directory.
|
|
1563
1596
|
|
|
1564
1597
|
`submit` sends field, section, and preamble operations with that value's prior
|
|
1565
1598
|
content as base, and whole-file, rename, and removal operations with the file
|
|
@@ -1772,7 +1805,12 @@ another, beside the live drafts described below:
|
|
|
1772
1805
|
the answer is absent or unreadable. File items are pulled back from House,
|
|
1773
1806
|
refused as `file_too_large` above the 10 MiB image ceiling, uploaded as
|
|
1774
1807
|
`sendPhoto` with the caption House composed around the link to the retained
|
|
1775
|
-
original, and reported the same way
|
|
1808
|
+
original, and reported the same way. A caption past Telegram's
|
|
1809
|
+
1,024-character caption limit keeps its head under the image, cut at its last
|
|
1810
|
+
line break that fits or else at the limit, and the rest follows in the topic
|
|
1811
|
+
as ordinary messages; the upload is reported with the image's answer when
|
|
1812
|
+
every part was delivered and otherwise with the answer of the first part that
|
|
1813
|
+
was not. A pull that fails reports
|
|
1776
1814
|
`transfer_failed` and an ambiguous upload answer reports `delivery_unknown`.
|
|
1777
1815
|
Only an image ever becomes a file item, because House offers every other file
|
|
1778
1816
|
as a `conversation_file_link` message this queue delivers like any other. The
|
|
@@ -1783,6 +1821,10 @@ another, beside the live drafts described below:
|
|
|
1783
1821
|
between blocks and inside a block only when that block alone exceeds them,
|
|
1784
1822
|
and reported once, `delivered` with the first message's id when every part
|
|
1785
1823
|
was delivered and otherwise with the answer of the first part that was not.
|
|
1824
|
+
A new plain-text message past Telegram's 4,096-character message limit, such
|
|
1825
|
+
as a `conversation_file_link` text, is sent and reported the same way, cut
|
|
1826
|
+
at the last line break that fits or else at the limit, so the first message
|
|
1827
|
+
keeps its reply and keyboard and a link on its own line stays whole.
|
|
1786
1828
|
Each answer also names the `threads` the bot's topics carry, a
|
|
1787
1829
|
`conversation_id` with its `chat_id` and `topic_id`, which is where live
|
|
1788
1830
|
drafts go.
|
package/bin/connect-linux.sh
CHANGED
|
@@ -15,7 +15,12 @@ FORWARD=()
|
|
|
15
15
|
UBUNTU_RELEASES=(jammy noble resolute)
|
|
16
16
|
DOCKER_PACKAGES=(docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin)
|
|
17
17
|
CONFLICTING_PACKAGES=(docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc)
|
|
18
|
-
|
|
18
|
+
DOCKER_APP=/Applications/Docker.app
|
|
19
|
+
DESKTOP_WAIT_SECONDS=300
|
|
20
|
+
RUNTIME='Docker Engine'
|
|
21
|
+
OPENER=xdg-open
|
|
22
|
+
NETWORK=(--network host)
|
|
23
|
+
PUBLISHED=()
|
|
19
24
|
|
|
20
25
|
refuse() {
|
|
21
26
|
printf 'kit_bootstrap_refused: %s\n' "$1" >&2
|
|
@@ -32,11 +37,11 @@ login_kit() {
|
|
|
32
37
|
"${DOCKER_RUN[@]:0:2}" -i "${DOCKER_RUN[@]:2}" login "$@"
|
|
33
38
|
return
|
|
34
39
|
fi
|
|
35
|
-
command -v
|
|
36
|
-
"${DOCKER_RUN[@]}" login "$@" --host-browser | while IFS= read -r line; do
|
|
40
|
+
command -v "$OPENER" >/dev/null || refuse 'no host browser opener; rerun with --manual'
|
|
41
|
+
"${DOCKER_RUN[@]:0:2}" ${PUBLISHED[@]+"${PUBLISHED[@]}"} "${DOCKER_RUN[@]:2}" login "$@" --host-browser | while IFS= read -r line; do
|
|
37
42
|
if [[ "$line" == 'Open '* ]]; then
|
|
38
43
|
printf 'Opening House Login in the host browser.\n'
|
|
39
|
-
|
|
44
|
+
"$OPENER" "${line#Open }" >/dev/null 2>&1 || refuse 'host browser did not open; rerun with --manual'
|
|
40
45
|
else
|
|
41
46
|
printf '%s\n' "$line"
|
|
42
47
|
fi
|
|
@@ -46,15 +51,7 @@ login_kit() {
|
|
|
46
51
|
start_resident() {
|
|
47
52
|
local network=()
|
|
48
53
|
[[ "$HOUSE" == https://* ]] || network=(--network host)
|
|
49
|
-
docker run -d --name "$NAME" --restart unless-stopped "${network[@]}" --user "$(id -u):$(id -g)" --mount "$KIT_HOME_MOUNT" --mount "$WORKSPACE_MOUNT" --label "agentshouse.house=$HOUSE" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 "$IMAGE" resident >/dev/null
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
docker() {
|
|
53
|
-
if ((DOCKER_GROUP)); then
|
|
54
|
-
sudo -u "$(id -un)" docker "$@"
|
|
55
|
-
else
|
|
56
|
-
command docker "$@"
|
|
57
|
-
fi
|
|
54
|
+
docker run -d --name "$NAME" --restart unless-stopped ${network[@]+"${network[@]}"} --user "$(id -u):$(id -g)" --mount "$KIT_HOME_MOUNT" --mount "$WORKSPACE_MOUNT" --label "agentshouse.house=$HOUSE" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 "$IMAGE" resident >/dev/null
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
elevated() {
|
|
@@ -82,6 +79,41 @@ unanswered() {
|
|
|
82
79
|
printf '; Docker Engine does not answer: %s' "$(docker_cause)"
|
|
83
80
|
}
|
|
84
81
|
|
|
82
|
+
install_docker_desktop() {
|
|
83
|
+
local mount
|
|
84
|
+
DOWNLOAD=$(mktemp -d) || refuse 'no temporary directory for the Docker Desktop download'
|
|
85
|
+
trap 'rm -rf "$DOWNLOAD"' EXIT
|
|
86
|
+
mount="$DOWNLOAD/volume"
|
|
87
|
+
printf 'Downloading Docker Desktop for %s from docker.com.\n' "$1"
|
|
88
|
+
curl -fsSL -o "$DOWNLOAD/Docker.dmg" "https://desktop.docker.com/mac/main/$ARCH/Docker.dmg" || refuse 'Docker Desktop could not be downloaded'
|
|
89
|
+
printf 'Installing Docker Desktop in /Applications; sudo may ask for your password.\n'
|
|
90
|
+
mkdir "$mount" || refuse 'the Docker Desktop disk image has no mount point'
|
|
91
|
+
elevated hdiutil attach -nobrowse -readonly -mountpoint "$mount" "$DOWNLOAD/Docker.dmg" >/dev/null || refuse 'the Docker Desktop disk image could not be opened'
|
|
92
|
+
if ! elevated "$mount/Docker.app/Contents/MacOS/install" "--user=$(id -un)"; then
|
|
93
|
+
elevated hdiutil detach "$mount" >/dev/null 2>&1 || true
|
|
94
|
+
refuse 'the Docker Desktop installer did not finish'
|
|
95
|
+
fi
|
|
96
|
+
elevated hdiutil detach "$mount" >/dev/null || refuse 'the Docker Desktop disk image could not be closed'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
establish_docker_desktop() {
|
|
100
|
+
PATH="$PATH:$DOCKER_APP/Contents/Resources/bin"
|
|
101
|
+
docker_answers && return
|
|
102
|
+
if ((FORWARDING)); then
|
|
103
|
+
[[ -d "$DOCKER_APP" ]] || refuse 'House Kit is not installed; run this bootstrap without arguments first'
|
|
104
|
+
refuse "Docker Desktop does not answer: $(docker_cause); start Docker Desktop, then rerun this command"
|
|
105
|
+
fi
|
|
106
|
+
[[ -d "$DOCKER_APP" ]] || install_docker_desktop "$1"
|
|
107
|
+
printf 'Starting Docker Desktop; accept its terms or finish its setup if its window asks.\n'
|
|
108
|
+
open "$DOCKER_APP" || refuse 'Docker Desktop could not be opened'
|
|
109
|
+
local waited=0
|
|
110
|
+
until docker_answers; do
|
|
111
|
+
((waited < DESKTOP_WAIT_SECONDS)) || refuse "Docker Desktop is not ready: $(docker_cause); finish what its window asks, then rerun this bootstrap"
|
|
112
|
+
sleep 2
|
|
113
|
+
waited=$((waited + 2))
|
|
114
|
+
done
|
|
115
|
+
}
|
|
116
|
+
|
|
85
117
|
installed_packages() {
|
|
86
118
|
{ dpkg-query -W -f='${db:Status-Status} ${Package}\n' "$@" 2>/dev/null || true; } | awk '$1 == "installed" { print $2 }'
|
|
87
119
|
}
|
|
@@ -132,7 +164,7 @@ establish_docker_engine() {
|
|
|
132
164
|
printf 'Adding %s to the docker group; it applies from your next sign-in, so this run reaches Docker Engine through sudo.\n' "$(id -un)"
|
|
133
165
|
elevated usermod -aG docker "$(id -un)" || refuse "$(id -un) could not be added to the docker group"
|
|
134
166
|
fi
|
|
135
|
-
docker_member ||
|
|
167
|
+
docker_member || docker() { sudo -u "$(id -un)" docker "$@"; }
|
|
136
168
|
fi
|
|
137
169
|
docker_answers || refuse "Docker Engine does not answer: $(docker_cause)"
|
|
138
170
|
}
|
|
@@ -172,20 +204,39 @@ while (($#)); do
|
|
|
172
204
|
done
|
|
173
205
|
|
|
174
206
|
[[ "$IMAGE" =~ ^ghcr\.io/agentshouse/kit@sha256:[0-9a-f]{64}$ ]] || refuse 'this bootstrap has not been published with an immutable image digest'
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
207
|
+
case "$(uname -s)" in
|
|
208
|
+
Linux)
|
|
209
|
+
case "$(uname -m)" in
|
|
210
|
+
x86_64|amd64) ARCH=amd64 ;;
|
|
211
|
+
aarch64|arm64) ARCH=arm64 ;;
|
|
212
|
+
*) refuse "unsupported Linux architecture $(uname -m)" ;;
|
|
213
|
+
esac
|
|
214
|
+
;;
|
|
215
|
+
Darwin)
|
|
216
|
+
RUNTIME='Docker Desktop'
|
|
217
|
+
OPENER=open
|
|
218
|
+
if [[ "$(sysctl -n hw.optional.arm64 2>/dev/null)" == 1 ]]; then
|
|
219
|
+
ARCH=arm64
|
|
220
|
+
CHIP='Apple silicon'
|
|
221
|
+
else
|
|
222
|
+
ARCH=amd64
|
|
223
|
+
CHIP='an Intel chip'
|
|
224
|
+
fi
|
|
225
|
+
;;
|
|
226
|
+
*) refuse "unsupported host $(uname -s); this bootstrap is for Linux and macOS" ;;
|
|
180
227
|
esac
|
|
181
228
|
[[ "$WORKSPACE" == /* ]] || refuse 'workspace root must be an absolute path'
|
|
182
|
-
|
|
229
|
+
if [[ "$RUNTIME" == 'Docker Desktop' ]]; then
|
|
230
|
+
establish_docker_desktop "$CHIP"
|
|
231
|
+
else
|
|
232
|
+
establish_docker_engine
|
|
233
|
+
fi
|
|
183
234
|
case "$DOCKER_PLATFORM" in
|
|
184
235
|
linux/x86_64|linux/amd64) DOCKER_ARCH=amd64 ;;
|
|
185
236
|
linux/aarch64|linux/arm64) DOCKER_ARCH=arm64 ;;
|
|
186
|
-
*) refuse "unsupported
|
|
237
|
+
*) refuse "unsupported $RUNTIME platform $DOCKER_PLATFORM" ;;
|
|
187
238
|
esac
|
|
188
|
-
[[ "$DOCKER_ARCH" == "$ARCH" ]] || refuse "host linux/$ARCH does not match
|
|
239
|
+
[[ "$DOCKER_ARCH" == "$ARCH" ]] || refuse "host linux/$ARCH does not match $RUNTIME linux/$DOCKER_ARCH"
|
|
189
240
|
if ((FORWARDING)); then
|
|
190
241
|
docker container inspect "$NAME" >/dev/null 2>&1 || refuse 'House Kit is not installed; run this bootstrap without arguments first'
|
|
191
242
|
WORKSPACE=$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/agents/house"}}{{.Source}}{{end}}{{end}}' "$NAME")
|
|
@@ -197,7 +248,7 @@ for path in "$KIT_HOME" "$WORKSPACE"; do
|
|
|
197
248
|
[[ ! -L "$path" ]] || refuse "$path is a symbolic link"
|
|
198
249
|
mkdir -p -m 700 "$path" || refuse "$path could not be created"
|
|
199
250
|
[[ -d "$path" ]] || refuse "$path is not a directory"
|
|
200
|
-
[[
|
|
251
|
+
[[ -O "$path" ]] || refuse "$path is not owned by this User"
|
|
201
252
|
chmod 700 "$path" || refuse "$path could not be owner-protected"
|
|
202
253
|
done
|
|
203
254
|
KIT_HOME=$(cd "$KIT_HOME" && pwd -P)
|
|
@@ -219,13 +270,19 @@ if ((FORWARDING == 0)); then
|
|
|
219
270
|
if [[ ! -f "$KIT_HOME/kit.json" ]]; then
|
|
220
271
|
printf '{"availability":"intermittent","execution_authority":"allow","migrators":[],"instances":[],"clients":[],"host":null,"skills":%s}\n' "$SKILLS" > "$KIT_HOME/kit.json"
|
|
221
272
|
elif grep -Eq '"skills"[[:space:]]*:' "$KIT_HOME/kit.json"; then
|
|
222
|
-
sed -E -i "s/(\"skills\"[[:space:]]*:[[:space:]]*)(true|false)/\\1$SKILLS/" "$KIT_HOME/kit.json" || refuse 'kit.json could not be updated'
|
|
273
|
+
sed -E -i.old "s/(\"skills\"[[:space:]]*:[[:space:]]*)(true|false)/\\1$SKILLS/" "$KIT_HOME/kit.json" || refuse 'kit.json could not be updated'
|
|
223
274
|
else
|
|
224
|
-
sed -E -i "1s/^([[:space:]]*)\\{/\\1{\"skills\":$SKILLS,/" "$KIT_HOME/kit.json" || refuse 'kit.json could not be updated'
|
|
275
|
+
sed -E -i.old "1s/^([[:space:]]*)\\{/\\1{\"skills\":$SKILLS,/" "$KIT_HOME/kit.json" || refuse 'kit.json could not be updated'
|
|
225
276
|
fi
|
|
277
|
+
rm -f "$KIT_HOME/kit.json.old"
|
|
226
278
|
grep -Eq "\"skills\"[[:space:]]*:[[:space:]]*$SKILLS" "$KIT_HOME/kit.json" || refuse 'kit.json could not be updated'
|
|
227
279
|
fi
|
|
228
|
-
|
|
280
|
+
if [[ "$RUNTIME" == 'Docker Desktop' && "$HOUSE" == https://* ]]; then
|
|
281
|
+
NETWORK=()
|
|
282
|
+
LOGIN_PORT=$((49152 + RANDOM % 16384))
|
|
283
|
+
PUBLISHED=(--publish "127.0.0.1:$LOGIN_PORT:$LOGIN_PORT" --env "HOUSE_KIT_LOGIN_PORT=$LOGIN_PORT")
|
|
284
|
+
fi
|
|
285
|
+
DOCKER_RUN=(docker run --rm ${NETWORK[@]+"${NETWORK[@]}"} --user "$(id -u):$(id -g)" --mount "$KIT_HOME_MOUNT" --mount "$WORKSPACE_MOUNT" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 --env "HOUSE_KIT_HOSTNAME=$(hostname)" "$IMAGE")
|
|
229
286
|
|
|
230
287
|
if docker container inspect "$NAME" >/dev/null 2>&1; then
|
|
231
288
|
INSTALLED_IMAGE=$(docker inspect --format '{{.Config.Image}}' "$NAME")
|
|
@@ -240,7 +297,7 @@ if docker container inspect "$NAME" >/dev/null 2>&1; then
|
|
|
240
297
|
[[ -f "$KIT_HOME/credential" && -f "$KIT_HOME/environment" && -f "$KIT_HOME/house" ]] || refuse 'the installed Kit has no enrolled authority'
|
|
241
298
|
if ((FORWARDING)); then
|
|
242
299
|
if [[ "$FORWARDED" == kit && "${FORWARD[0]:-}" == login ]]; then
|
|
243
|
-
login_kit "${FORWARD[@]:1}"
|
|
300
|
+
login_kit ${FORWARD[@]+"${FORWARD[@]:1}"}
|
|
244
301
|
exit 0
|
|
245
302
|
fi
|
|
246
303
|
PLACED=()
|
|
@@ -259,9 +316,9 @@ if docker container inspect "$NAME" >/dev/null 2>&1; then
|
|
|
259
316
|
stty -echo
|
|
260
317
|
fi
|
|
261
318
|
if [[ "$(docker inspect --format '{{.State.Running}}' "$NAME")" == true ]]; then
|
|
262
|
-
docker exec -i "${TERMINAL[@]}" "${PLACED[@]}" "$NAME" "$FORWARDED" "${FORWARD[@]}"
|
|
319
|
+
docker exec -i ${TERMINAL[@]+"${TERMINAL[@]}"} ${PLACED[@]+"${PLACED[@]}"} "$NAME" "$FORWARDED" ${FORWARD[@]+"${FORWARD[@]}"}
|
|
263
320
|
else
|
|
264
|
-
"${DOCKER_RUN[@]:0:2}" -i "${TERMINAL[@]}" "${PLACED[@]}" "${ENTRY[@]}" "${DOCKER_RUN[@]:2}" "${FORWARD[@]}"
|
|
321
|
+
"${DOCKER_RUN[@]:0:2}" -i ${TERMINAL[@]+"${TERMINAL[@]}"} ${PLACED[@]+"${PLACED[@]}"} ${ENTRY[@]+"${ENTRY[@]}"} "${DOCKER_RUN[@]:2}" ${FORWARD[@]+"${FORWARD[@]}"}
|
|
265
322
|
fi
|
|
266
323
|
exit
|
|
267
324
|
fi
|
|
@@ -294,7 +351,7 @@ else
|
|
|
294
351
|
LOGIN=()
|
|
295
352
|
if ((HOUSE_SELECTED)); then LOGIN+=(--house "$HOUSE"); fi
|
|
296
353
|
if ((MANUAL)); then LOGIN+=(--manual); fi
|
|
297
|
-
login_kit "${LOGIN[@]}"
|
|
354
|
+
login_kit ${LOGIN[@]+"${LOGIN[@]}"}
|
|
298
355
|
fi
|
|
299
356
|
|
|
300
357
|
"${DOCKER_RUN[@]}" apply
|
package/bin/kit.js
CHANGED
|
@@ -119,6 +119,7 @@ async function runLogin(args, directory) {
|
|
|
119
119
|
directory,
|
|
120
120
|
hostBrowser: values["--host-browser"] === true,
|
|
121
121
|
hostname: process.env.HOUSE_KIT_HOSTNAME,
|
|
122
|
+
loginPort: process.env.HOUSE_KIT_LOGIN_PORT,
|
|
122
123
|
...(values["--environment"] === undefined ? {} : { environment: values["--environment"] }),
|
|
123
124
|
});
|
|
124
125
|
}
|
package/bootstrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.34",
|
|
4
4
|
"hosts": {
|
|
5
5
|
"linux": {
|
|
6
|
-
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
6
|
+
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.34/connect-linux.sh)",
|
|
7
7
|
"asset": {
|
|
8
8
|
"name": "connect-linux.sh",
|
|
9
|
-
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
9
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.34/connect-linux.sh"
|
|
10
10
|
},
|
|
11
11
|
"workspace": {
|
|
12
12
|
"argument": "--workspace",
|
package/package.json
CHANGED
package/src/contract.js
CHANGED
|
@@ -61,6 +61,8 @@ export const TELEGRAM_DOWNLOAD_CEILING_BYTES = 20 * 1024 * 1024;
|
|
|
61
61
|
export const TELEGRAM_BOT_API_TIMEOUT_MS = 30_000;
|
|
62
62
|
export const TELEGRAM_GATEWAY_POLL_MS = 2_000;
|
|
63
63
|
export const TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX = 32_768;
|
|
64
|
+
export const TELEGRAM_MESSAGE_CHARACTERS_MAX = 4_096;
|
|
65
|
+
export const TELEGRAM_CAPTION_CHARACTERS_MAX = 1_024;
|
|
64
66
|
export const TELEGRAM_RICH_MESSAGE_BLOCKS_MAX = 500;
|
|
65
67
|
export const TELEGRAM_CONTROL_KEYBOARD_COMMANDS = ["/stop", "/kill"];
|
|
66
68
|
export const TELEGRAM_MEDIA_KINDS = [
|
package/src/google/entries.js
CHANGED
|
@@ -213,7 +213,10 @@ export function sourceEntries(context) {
|
|
|
213
213
|
const task = signIn(occasion(record), entry);
|
|
214
214
|
signingIn = { source: entry.source, task };
|
|
215
215
|
try {
|
|
216
|
-
if (await task)
|
|
216
|
+
if (await task) {
|
|
217
|
+
record.last = null;
|
|
218
|
+
attempted.delete(entry.source);
|
|
219
|
+
}
|
|
217
220
|
} catch (error) {
|
|
218
221
|
if (!record.stop.signal.aborted) throw error;
|
|
219
222
|
} finally {
|
package/src/login.js
CHANGED
|
@@ -216,11 +216,12 @@ export async function login(options, dependencies = {}) {
|
|
|
216
216
|
return { environment: exchanged.environment };
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
export async function loopback() {
|
|
219
|
+
export async function loopback(published) {
|
|
220
220
|
const server = createServer();
|
|
221
221
|
await new Promise((resolve, reject) => {
|
|
222
222
|
server.once("error", reject);
|
|
223
|
-
server.listen(0, "127.0.0.1", resolve);
|
|
223
|
+
if (published === undefined) server.listen(0, "127.0.0.1", resolve);
|
|
224
|
+
else server.listen(Number(published), "0.0.0.0", resolve);
|
|
224
225
|
});
|
|
225
226
|
const address = server.address();
|
|
226
227
|
const code = new Promise((resolve, reject) => {
|
|
@@ -274,7 +275,7 @@ export async function connectHouse(options, dependencies = {}) {
|
|
|
274
275
|
refuseDrained(options.directory);
|
|
275
276
|
await prepareLaunchBase(launchBase(dependencies));
|
|
276
277
|
if (options.presentation === "manual") return login(options, dependencies);
|
|
277
|
-
const callback = await loopback();
|
|
278
|
+
const callback = await loopback(options.loginPort);
|
|
278
279
|
try {
|
|
279
280
|
return await login(
|
|
280
281
|
{ ...options, presentation: "loopback", loopbackUri: callback.uri },
|
package/src/sync.js
CHANGED
|
@@ -131,23 +131,31 @@ function scopeContents(replica, scope) {
|
|
|
131
131
|
return scope.contents;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
function servedFile(scope, path) {
|
|
135
|
+
return scope.manifest.files.find((file) => file?.path === path);
|
|
136
|
+
}
|
|
137
|
+
|
|
134
138
|
async function writeScope(replica, scope, signal) {
|
|
135
|
-
const
|
|
139
|
+
const recorded = new Map();
|
|
136
140
|
for (const entry of scopeContents(replica, scope)) {
|
|
137
|
-
const meta = scope
|
|
141
|
+
const meta = servedFile(scope, entry.path) ?? {};
|
|
138
142
|
await writeReplicaFile(locate(replica, scope.manifest.path, entry.path), entry.content, signal);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
content: entry.content,
|
|
144
|
-
});
|
|
143
|
+
recorded.set(entry.path, enumeratedRecord(entry.path, entry.content, meta));
|
|
144
|
+
if (typeof meta.provenance === "string") {
|
|
145
|
+
await writeSidecar({ replica }, scope.manifest, recorded, entry.path, meta, signal);
|
|
146
|
+
}
|
|
145
147
|
}
|
|
146
|
-
return
|
|
148
|
+
return [...recorded.values()];
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
async function replaceScope(replica, previous, scope, signal) {
|
|
150
|
-
const kept = new Set(
|
|
152
|
+
const kept = new Set(
|
|
153
|
+
scopeContents(replica, scope).flatMap((entry) =>
|
|
154
|
+
typeof servedFile(scope, entry.path)?.provenance === "string"
|
|
155
|
+
? [entry.path, sidecarPath(entry.path)]
|
|
156
|
+
: [entry.path],
|
|
157
|
+
),
|
|
158
|
+
);
|
|
151
159
|
const root = locate(replica, scope.manifest.path);
|
|
152
160
|
for (const path of await listFiles(root)) {
|
|
153
161
|
if (!kept.has(path)) await removeReplicaFile(replica, scope.manifest.path, path);
|
|
@@ -255,6 +263,12 @@ async function writeSidecar(options, authority, recorded, path, change, signal)
|
|
|
255
263
|
recorded.set(sidecar, enumeratedRecord(sidecar, change.provenance, change));
|
|
256
264
|
}
|
|
257
265
|
|
|
266
|
+
async function removeSidecar(options, authority, recorded, path) {
|
|
267
|
+
const sidecar = sidecarPath(path);
|
|
268
|
+
await removeReplicaFile(options.replica, authority.path, sidecar);
|
|
269
|
+
recorded.delete(sidecar);
|
|
270
|
+
}
|
|
271
|
+
|
|
258
272
|
async function applyEnumeration(options, state, enumeration, signal) {
|
|
259
273
|
if (!Array.isArray(enumeration?.authorities)) throw new KitRefusal("kit_sync_rejected", "enumerate");
|
|
260
274
|
for (const view of enumeration.authorities) {
|
|
@@ -267,6 +281,7 @@ async function applyEnumeration(options, state, enumeration, signal) {
|
|
|
267
281
|
if (change.removed === true) {
|
|
268
282
|
await removeReplicaFile(options.replica, authority.path, change.path);
|
|
269
283
|
recorded.delete(change.path);
|
|
284
|
+
await removeSidecar(options, authority, recorded, change.path);
|
|
270
285
|
continue;
|
|
271
286
|
}
|
|
272
287
|
if (typeof change.content === "string") {
|
|
@@ -360,6 +375,7 @@ async function acceptBatch(session, options, state, outcome, signal, pending = n
|
|
|
360
375
|
if (accepted === null) {
|
|
361
376
|
if (!preserve) await removeReplicaFile(options.replica, authority.path, relative);
|
|
362
377
|
recorded.delete(relative);
|
|
378
|
+
await removeSidecar(options, authority, recorded, relative);
|
|
363
379
|
continue;
|
|
364
380
|
}
|
|
365
381
|
if (!preserve) await writeReplicaFile(destination, accepted, signal);
|
package/src/telegram-view.js
CHANGED
|
@@ -25,13 +25,16 @@ function richText(value, key = null) {
|
|
|
25
25
|
return [];
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function slices(text) {
|
|
28
|
+
export function slices(text, limit = TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX) {
|
|
29
29
|
const pieces = [];
|
|
30
30
|
let start = 0;
|
|
31
31
|
while (start < text.length) {
|
|
32
|
-
let end = Math.min(start +
|
|
32
|
+
let end = Math.min(start + limit, text.length);
|
|
33
|
+
const window = text.slice(start, end);
|
|
34
|
+
const line = window.lastIndexOf("\n");
|
|
33
35
|
const last = text.charCodeAt(end - 1);
|
|
34
|
-
if (end < text.length &&
|
|
36
|
+
if (end < text.length && line > 0 && /\S/.test(window.slice(0, line))) end = start + line + 1;
|
|
37
|
+
else if (end < text.length && last >= 0xd800 && last <= 0xdbff) end -= 1;
|
|
35
38
|
pieces.push(text.slice(start, end));
|
|
36
39
|
start = end;
|
|
37
40
|
}
|
package/src/telegram.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
PERSONAL_BOT_OFFSET_RELATIVE_PATH,
|
|
11
11
|
PERSONAL_BOT_TOKEN_RELATIVE_PATH,
|
|
12
12
|
TELEGRAM_BOT_API_TIMEOUT_MS,
|
|
13
|
+
TELEGRAM_CAPTION_CHARACTERS_MAX,
|
|
13
14
|
TELEGRAM_CONTROL_KEYBOARD_COMMANDS,
|
|
14
15
|
TELEGRAM_DELIVERY_BATCH_MAX,
|
|
15
16
|
TELEGRAM_DELIVERY_KINDS,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
TELEGRAM_IMAGE_UPLOAD_CEILING_BYTES,
|
|
19
20
|
TELEGRAM_INGRESS_BATCH_MAX,
|
|
20
21
|
TELEGRAM_MEDIA_KINDS,
|
|
22
|
+
TELEGRAM_MESSAGE_CHARACTERS_MAX,
|
|
21
23
|
TELEGRAM_UNSUPPORTED_MEDIA_FIELDS,
|
|
22
24
|
TELEGRAM_UPDATE_OUTCOMES,
|
|
23
25
|
telegramApiUrl,
|
|
@@ -27,7 +29,7 @@ import { heldControl, houseRequest, maintainControl, openSession } from "./house
|
|
|
27
29
|
import { watchKitWork } from "./kit-work-stream.js";
|
|
28
30
|
import { KitRefusal } from "./refusal.js";
|
|
29
31
|
import { writeText } from "./state.js";
|
|
30
|
-
import { richPieces, turnViews } from "./telegram-view.js";
|
|
32
|
+
import { richPieces, slices, turnViews } from "./telegram-view.js";
|
|
31
33
|
import { kitStreamWriter } from "./turn-frames.js";
|
|
32
34
|
|
|
33
35
|
const TOKEN_PROMPT = "Paste the BotFather token for this Environment: ";
|
|
@@ -285,8 +287,21 @@ export function deliveryCall(delivery) {
|
|
|
285
287
|
return { method: "sendMessage", body: sendBody(delivery) };
|
|
286
288
|
}
|
|
287
289
|
|
|
290
|
+
function topicMessage(chat, topic, text) {
|
|
291
|
+
const body = { chat_id: Number(chat), text };
|
|
292
|
+
if (topic !== null) body.message_thread_id = Number(topic);
|
|
293
|
+
return { method: "sendMessage", body };
|
|
294
|
+
}
|
|
295
|
+
|
|
288
296
|
function deliveryCalls(delivery) {
|
|
289
297
|
const call = deliveryCall(delivery);
|
|
298
|
+
if (call.method === "sendMessage" && call.body.text.length > TELEGRAM_MESSAGE_CHARACTERS_MAX) {
|
|
299
|
+
const [first, ...following] = slices(call.body.text, TELEGRAM_MESSAGE_CHARACTERS_MAX);
|
|
300
|
+
return [
|
|
301
|
+
{ method: call.method, body: { ...call.body, text: first } },
|
|
302
|
+
...following.map((text) => topicMessage(delivery.chat_id, delivery.topic_id, text)),
|
|
303
|
+
];
|
|
304
|
+
}
|
|
290
305
|
if (call.method !== "sendRichMessage") return [call];
|
|
291
306
|
return richPieces(call.body.rich_message).map((rich_message) => ({
|
|
292
307
|
method: call.method,
|
|
@@ -791,6 +806,27 @@ async function sendQueuedMessage(gateway, token, delivery) {
|
|
|
791
806
|
return false;
|
|
792
807
|
}
|
|
793
808
|
|
|
809
|
+
function captionPieces(caption) {
|
|
810
|
+
if (caption === null || caption.length <= TELEGRAM_CAPTION_CHARACTERS_MAX) return [caption];
|
|
811
|
+
const [head] = slices(caption, TELEGRAM_CAPTION_CHARACTERS_MAX);
|
|
812
|
+
return [head, ...slices(caption.slice(head.length), TELEGRAM_MESSAGE_CHARACTERS_MAX)];
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async function sentOverflow(gateway, token, upload, overflow) {
|
|
816
|
+
for (const text of overflow) {
|
|
817
|
+
const call = topicMessage(upload.chat_id, upload.topic_id, text);
|
|
818
|
+
let answered;
|
|
819
|
+
try {
|
|
820
|
+
answered = await telegramPost(gateway, telegramApiUrl(token, call.method), call.body);
|
|
821
|
+
} catch (error) {
|
|
822
|
+
if (cancelled(error)) throw error;
|
|
823
|
+
return { settled: uploadOutcome(null), answered: null };
|
|
824
|
+
}
|
|
825
|
+
if (!botApiAccepted(answered)) return { settled: uploadOutcome(answered), answered };
|
|
826
|
+
}
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
|
|
794
830
|
async function sendQueuedUpload(gateway, token, upload) {
|
|
795
831
|
let bytes;
|
|
796
832
|
try {
|
|
@@ -810,12 +846,13 @@ async function sendQueuedUpload(gateway, token, upload) {
|
|
|
810
846
|
});
|
|
811
847
|
return false;
|
|
812
848
|
}
|
|
849
|
+
const [caption, ...overflow] = captionPieces(upload.caption);
|
|
813
850
|
let answered;
|
|
814
851
|
try {
|
|
815
852
|
answered = await telegramUpload(
|
|
816
853
|
gateway,
|
|
817
854
|
telegramApiUrl(token, "sendPhoto"),
|
|
818
|
-
uploadFields(upload),
|
|
855
|
+
uploadFields({ ...upload, caption }),
|
|
819
856
|
{ field: "photo", name: upload.file_name, bytes },
|
|
820
857
|
);
|
|
821
858
|
} catch (error) {
|
|
@@ -826,8 +863,11 @@ async function sendQueuedUpload(gateway, token, upload) {
|
|
|
826
863
|
});
|
|
827
864
|
return false;
|
|
828
865
|
}
|
|
829
|
-
|
|
830
|
-
const
|
|
866
|
+
const photographed = uploadOutcome(answered);
|
|
867
|
+
const unsent =
|
|
868
|
+
photographed.outcome === "delivered" ? await sentOverflow(gateway, token, upload, overflow) : null;
|
|
869
|
+
await reportUpload(gateway, upload, unsent?.settled ?? photographed);
|
|
870
|
+
const retryAfter = telegramRetryAfter(unsent === null ? answered : unsent.answered);
|
|
831
871
|
if (retryAfter === null) return false;
|
|
832
872
|
gateway.gate.hold(retryAfter);
|
|
833
873
|
return true;
|