@staix/agent-hub 0.12.25 → 0.12.27
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/CHANGELOG.md +13 -0
- package/README.md +2 -2
- package/docs/data-policy.md +2 -1
- package/docs/memory.md +45 -8
- package/docs/operations.md +81 -25
- package/docs/quickstart.md +1 -1
- package/docs/security.md +34 -6
- package/docs/specs/2026-09-19-agent-hub-design.md +77 -28
- package/docs/specs/2026-10-12-office-view-design.md +55 -11
- package/docs/text-projection.md +1 -1
- package/docs/verification/2026-10-14-0.12.26.md +31 -0
- package/docs/verification/2026-10-15-0.12.27.md +31 -0
- package/docs/verification/assets/391-pet-window-spike.png +0 -0
- package/docs/verified.json +8 -8
- package/package.json +3 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +3 -1
- package/src/cli/help.ts +2 -1
- package/src/cli/identity.ts +2 -1
- package/src/cli/main.ts +15 -3
- package/src/cli/memory.ts +16 -6
- package/src/cli/pet-config.ts +1 -1
- package/src/hub/path-policy.ts +64 -5
- package/src/hub/text-projection.ts +3 -1
- package/src/hub/ui.ts +1 -3
- package/src/memory/import.ts +52 -17
- package/src/memory/store.ts +53 -5
- package/src/pet/config.ts +42 -5
- package/src/pet/window.html +114 -0
- package/src/pet/window.jxa.js +122 -0
- package/src/pet/window.ts +154 -0
- package/src/ui/index.html +318 -129
- package/src/ui/office-model.ts +12 -12
|
@@ -29,8 +29,8 @@ asset is copied.
|
|
|
29
29
|
No timer, no `requestAnimationFrame`, no request. Colours come from the
|
|
30
30
|
page's theme tokens through CSS classes; every state also carries a glyph
|
|
31
31
|
and a text label.
|
|
32
|
-
-
|
|
33
|
-
|
|
32
|
+
- The Office view section contains a mode control offering Off (the default),
|
|
33
|
+
Flat and Isometric; the choice persists in the host-scoped `agent-hub-office`
|
|
34
34
|
cookie with the `agent-hub-theme` attributes and try/catch fallback (#246).
|
|
35
35
|
- Layout: stable desk slots (claude, codex, kimi, pi, local, then others by id;
|
|
36
36
|
sticky across snapshots via the previous scene, so a peer keeps its slot when
|
|
@@ -40,7 +40,8 @@ asset is copied.
|
|
|
40
40
|
section width: rooms left to right when wide, stacked when narrow.
|
|
41
41
|
- Interaction: desks and cards are focusable SVG groups with `role="button"`
|
|
42
42
|
and accessible names; activation scrolls to and highlights the matching list
|
|
43
|
-
row
|
|
43
|
+
row; a card also opens the shared task detail dialog (#391). A list row
|
|
44
|
+
highlights its desk or card on focus and hover. The scene
|
|
44
45
|
performs no action and sends no request; the closed action list is unchanged.
|
|
45
46
|
- Room counts equal `taskProgress(tasks).counts` for the same snapshot (pinned
|
|
46
47
|
by a test, including randomized boards).
|
|
@@ -64,7 +65,7 @@ Phase 1 decisions within the spec:
|
|
|
64
65
|
- The primary condition of a desk is the first of offline, paused, waiting on
|
|
65
66
|
approval, busy, idle; the raised-hand count, inbox tray, mode badge and break
|
|
66
67
|
sign are independent marks that combine freely with it.
|
|
67
|
-
- Work-floor cards sit at the owner's desk (at most 3, then "+N more"
|
|
68
|
+
- Work-floor cards sit at the owner's desk (at most 3 in Flat, then an owner-filtered "+N more" control);
|
|
68
69
|
a card whose owner has no visible desk waits in the Work floor room. Tasks in
|
|
69
70
|
an unknown state are counted (`counts.unknown`) but placed nowhere.
|
|
70
71
|
- The busy bubble names the task only when the peer has exactly one
|
|
@@ -142,11 +143,30 @@ Phase 4 decisions within the spec:
|
|
|
142
143
|
parallelogram polygons (the linear image of their rect); cards, text labels,
|
|
143
144
|
bubbles, marks and arc labels stay upright. No WebGL, no library, no new
|
|
144
145
|
route, no CSP change.
|
|
145
|
-
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
- Keyboard targets follow model order independently of visual paint depth. Each
|
|
147
|
+
visible card has one transparent, focusable hit target with its full accessible
|
|
148
|
+
name; a separate `aria-hidden` paint layer is ordered back to front under
|
|
149
|
+
Isometric. Click, Enter and Space open the same task detail. Shared visible
|
|
150
|
+
cards retain their relative tab order across projections and switching back
|
|
151
|
+
restores Flat's model order. Correctly placed nodes are untouched during
|
|
152
|
+
snapshots; actual reorders use state-preserving DOM movement when available
|
|
153
|
+
and restore active focus without scrolling on the insertBefore fallback.
|
|
154
|
+
#394 allows Isometric to omit cards that do not
|
|
155
|
+
fit and expose the omitted count through the existing "+N more" control, so
|
|
156
|
+
its visible target inventory can be smaller than Flat's.
|
|
157
|
+
- #394 fixed-scale amendment (2026-10-14): the SVG width attribute is its actual
|
|
158
|
+
occupied projected viewBox width times 12 px/unit, with `max-width: 100%` and
|
|
159
|
+
automatic height. It remains centered and shrinks only when the panel is
|
|
160
|
+
narrower than this intrinsic width. Flat uses up to four room columns;
|
|
161
|
+
Isometric uses two on wide screens and bounds the occupied tiles rather than
|
|
162
|
+
blank grid corners, keeping the two-peer scene at the same scale at 1280 and
|
|
163
|
+
1920 px. The cos-30/sin-30 projection and desk slots remain unchanged.
|
|
164
|
+
Desk trays use 24 px card bodies with 6 px gaps and padding inside a 20-unit
|
|
165
|
+
desk. Isometric fits upright cards wholly inside the tile (two per desk),
|
|
166
|
+
preserving omitted counts in a keyboard-accessible owner-filtered board link.
|
|
167
|
+
Its room band shifts vertically below desk legs and labels, with a one-unit
|
|
168
|
+
gap before the top-layer room headers; header backdrops associate each count
|
|
169
|
+
with its room.
|
|
150
170
|
- A projection switch repaints every node in place (the projection is part of
|
|
151
171
|
each reconcile signature) and is treated as a settle: positions jump, no
|
|
152
172
|
transform transition storms. Card moves under Isometric animate through the
|
|
@@ -154,7 +174,9 @@ Phase 4 decisions within the spec:
|
|
|
154
174
|
- Size accounting against the issue's 30 KB budget (page base 59,329 bytes at
|
|
155
175
|
phase 1 branch point): phase 1 +11,040, phase 2 +7,340, phase 3 +2,141,
|
|
156
176
|
phase 4 +1,704, art polish +3,559; final 89,113 bytes, +25,784 total,
|
|
157
|
-
inside the budget.
|
|
177
|
+
inside the original phase-4 budget. These are historical #264 figures; #394
|
|
178
|
+
adds a separately reviewed visual refinement. The iso shelf's base skirt was
|
|
179
|
+
dropped to stay under the original budget.
|
|
158
180
|
- Art polish (2026-10-12): two reference concept images (a flat view and an
|
|
159
181
|
isometric view) were generated with Higgsfield (nano_banana_pro) and kept
|
|
160
182
|
outside the repo (scratchpad); they served as drawing references only. Per
|
|
@@ -168,7 +190,24 @@ Phase 4 decisions within the spec:
|
|
|
168
190
|
wireframe iso room boxes with small-caps labels at the base, a Done shelf
|
|
169
191
|
cabinet (flat) and stacked boxes on a slab (iso) with the count kept as
|
|
170
192
|
text, two-column room cards with thin dividers, and arc arrowheads at the
|
|
171
|
-
target end.
|
|
193
|
+
target end. #394 supersedes the wireframe posts, small-caps base labels,
|
|
194
|
+
decorative empty shelf slots and two-column cards:
|
|
195
|
+
- Room names are sentence case, 13 px/600 in `--ink`; count pills are 12 px
|
|
196
|
+
monospace with tabular figures. Desk names are 13 px; card/state/meta text
|
|
197
|
+
is 12 px. Shapes use 1 px non-scaling hairlines; focus is 2 px.
|
|
198
|
+
- Room tiles have a one-unit gap horizontally and vertically. Room cards
|
|
199
|
+
use one 20-unit padded column, 28 px height and 6 px gap, keeping
|
|
200
|
+
the eight-card model cap and public fields. Labels show id plus public
|
|
201
|
+
title. Canvas measures the actual system/monospace fonts before clipping at
|
|
202
|
+
a nearby word boundary; an internal SVG clipPath guarantees the card text
|
|
203
|
+
boundary. The full title remains in SVG title, aria-label and task detail.
|
|
204
|
+
- Room title/count groups paint last above all tiles. Isometric cards remain
|
|
205
|
+
upright, fit wholly inside padded tile bounds and paint by projected depth;
|
|
206
|
+
omitted cards contribute to "+N more". Empty rooms and the empty Done shelf
|
|
207
|
+
show "No tasks"; populated shelf slots correspond to existing done count
|
|
208
|
+
up to the current Flat/Isometric caps.
|
|
209
|
+
- Existing theme tokens, public-view privacy, counts/placement classifications,
|
|
210
|
+
animations, reduced motion, keyboard activation and mode cookie remain.
|
|
172
211
|
|
|
173
212
|
## Verification
|
|
174
213
|
|
|
@@ -186,6 +225,11 @@ Phase 4 decisions within the spec:
|
|
|
186
225
|
timer/request/colour literal in the scene code, reduced-motion and motion CSS
|
|
187
226
|
scans, no DOM write on an unchanged snapshot (counting fake document), focus
|
|
188
227
|
and activation highlighting, "+N more" filtering, `resetView` clearing;
|
|
228
|
+
#394: fixed font/scene scale, 1 px hairlines, top-layer headers, actual shelf
|
|
229
|
+
counts, measured Latin/CJK title clipping and hard SVG bounds, cross-room
|
|
230
|
+
depth order, projection-independent shared-target tab order and Flat restore,
|
|
231
|
+
0/4/12-task two-peer geometry, count-stable placement changes and light/dark
|
|
232
|
+
text token contrast at least 4.5:1;
|
|
189
233
|
phase 2: card moves keep their node and animate by transform, arcs/marks/
|
|
190
234
|
latch draw once with fade timing, departure fade purges on animationend;
|
|
191
235
|
phase 3: the exact bubble, finishing-a-turn line, check-running mark and
|
package/docs/text-projection.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
- Optional machine-local text_projection maps lowercase extensions to argv arrays. Each uses one separate {input} argument. No shell string, automatic converter install or default command.
|
|
4
4
|
- Example: { "text_projection": { ".docx": ["/usr/bin/textutil", "-convert", "txt", "-stdout", "{input}"] } }. Configure the converter explicitly for the machine. Shared/tracked config cannot choose these programs.
|
|
5
5
|
- Facts excludes private paths before conversion. The converter reads an immutable snapshot in an owned temporary directory, from its own scratch directory, with literal read grants for that file and for the program and script files its argv names (relative ones are passed as absolute paths, and the directories above an in-project script can be listed) (nothing else in the project is readable, even under a `local.read_allow` entry that encloses it, so a converter that follows an include or loads a sibling helper module fails), uses the existing macOS no-network/read-only sandbox and has a 5-second deadline, 16 MiB input cap and 256 KiB stdout/stderr cap. A trusted helper owns and stops its process group; converter stderr is never published.
|
|
6
|
-
- Output caches in state by raw content hash, extension and argv/program/script metadata fingerprint. Invalid/oversized cache or failing/unavailable conversion falls back to the existing raw/binary facts behavior, with one bounded notice per extension. A conversion that fails with its full deadline is not tried again for the same bytes and command
|
|
6
|
+
- Output caches in state by raw content hash, extension and argv/program/script metadata fingerprint. Invalid/oversized cache or failing/unavailable conversion falls back to the existing raw/binary facts behavior, with one bounded notice per extension. A conversion that fails with its full deadline is not tried again for the same bytes and command while its entry lives in the failure record, which is in memory, bounded at 1024 entries, and cleared when full or at restart (#382).
|
|
7
7
|
- Raw hashes and git blob identities remain the change identities. Projected text only supplies presentation diffs. A projected read or patch cannot establish raw-file write attribution. Existing text privacy checks and header sanitization still apply.
|
|
8
8
|
- Converter programs may require temporary writes/caches; those fail under this read-only policy. Use a converter that emits stdout. The owner and other native same-user processes remain outside this boundary.
|
|
9
9
|
- Linux retains no-sandbox/no-converter behavior. No native model/account study runs. Shadow repositories and profile override/glob follow-ups remain open in #259.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# 0.12.26 release verification
|
|
2
|
+
|
|
3
|
+
This record covers release preparation and source verification. It does not certify tagging, package publication, global installation or a live upgrade; those receipts are recorded separately.
|
|
4
|
+
|
|
5
|
+
## Candidate
|
|
6
|
+
|
|
7
|
+
0.12.26 carries the hub-collaboration round for issues #380, #385 and #386: PRs #389, #387 and #388. Control protocol remains 16.
|
|
8
|
+
|
|
9
|
+
- #389 (#380): claude-mem imports derive their graph nodes from the observation text without consulting the checkout, so one observation is one content-hashed entry on every machine and sync converges regardless of import order; the import scan filters the project in SQL and resumes from a persisted machine-local cursor (`--restart` re-scans), so unrelated projects cannot exhaust the window; `ahub memory dedupe [--apply]` reconciles pre-fix duplicate groups with append-only supersede operations onto the lexicographically smallest id.
|
|
10
|
+
- #387 (#385): an exact or trailing-`**` data-policy entry that matches a directory resolves that directory's direct-child symlinks and protects their targets, matching what `data/*` already did for a matched link; discovery of the on-disk root is case-compatible, the scan streams inside the shared 4096-entry budget without descending, a dangling link covers its lexical target, an unresolvable link loop aliases nothing, a root whose budget is exhausted contributes no aliases, and an unreadable protected root fails closed. The failure semantics for the new scan are the user's direct ruling of 2026-10-14. The spec paragraph in docs/specs/2026-09-19-agent-hub-design.md was amended in the same PR.
|
|
11
|
+
- #388 (#386): the text-projection converter comment and both docs state the project-scoped read guarantee and the 1024-entry, cleared-when-full in-memory failure record.
|
|
12
|
+
|
|
13
|
+
Every fix carries regression tests that failed before it. Review was adversarial and multi-round on the board (tasks #45-#52): the review of #387 found the case-sensitive exact-root miss (P1) and the unbounded readdir (P2), CI found the Linux EACCES-at-read edge (#51) and two seed guards that needed retargeting after the refactor (#52), each fixed and re-gated before merge.
|
|
14
|
+
|
|
15
|
+
## Document review
|
|
16
|
+
|
|
17
|
+
Every stale document was rechecked against the ten source commits of the three PRs. README, Quickstart and Operations carry the 0.12.26 version and install/upgrade strings; Operations' converter failure-record wording landed with #388. The data-policy contract document (docs/data-policy.md) and docs/memory.md were updated inside their fix PRs. The adapters and daemon area notes were confirmed accurate against the `cliCommandLabel` addition, the tests note against the seed-selector repairs, and Security against the policy-matching change (it makes no per-form matching claim; the contract lives in data-policy.md). Budget, Bus, Benchmarks, Models, Tasks and the local-worker notes were not stale.
|
|
18
|
+
|
|
19
|
+
## Recorded checks
|
|
20
|
+
|
|
21
|
+
- PR #388 head 483ca546: CI Linux, macOS and seeded guards passed; board review approved.
|
|
22
|
+
- PR #389 head d5738472: worktree `scripts/check.sh` 2702 pass, 0 fail, 189163 assertions, `check: OK`, no daemon leaks; CI green; board review approved.
|
|
23
|
+
- PR #387 head 5fbe5d3a: worktree full gate 2714 pass, `check: OK`; CI Linux, macOS and seeded guards passed; board review approved.
|
|
24
|
+
- Release candidate fbf96c08: local `scripts/check.sh` in an isolated worktree — 2728 pass, 0 fail, 189269 assertions, `check: OK`, no daemon leaks. Later commits on this branch change documentation only.
|
|
25
|
+
- Release head: CI, document freshness at the merge commit, and the release workflow are required before tagging is reported complete.
|
|
26
|
+
|
|
27
|
+
Note: `scripts/check.sh` in the main checkout fails 35/68 daemon tests on unmodified main because the live hub daemon serving the project interferes; the isolated worktree gate is the valid signal (recorded in shared memory #194264).
|
|
28
|
+
|
|
29
|
+
## Not verified
|
|
30
|
+
|
|
31
|
+
Native, account and research legs remain open in #273 and #1. The installed runtime and live hubs are not upgraded by this release.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# 0.12.27 release verification
|
|
2
|
+
|
|
3
|
+
This record covers release preparation and source verification. It does not certify tagging, package publication, global installation or a live upgrade; those receipts are recorded separately.
|
|
4
|
+
|
|
5
|
+
## Candidate
|
|
6
|
+
|
|
7
|
+
0.12.27 carries the console-presentation round for issues #391 and #394: PRs #392, #393, #395 and #396. Control protocol remains 16.
|
|
8
|
+
|
|
9
|
+
- #392 (#391 Task B): an opt-in macOS floating pet window with built-in pixels, counts-only local state, a console message pencil, saved position and signed process ownership; terminal pet reads stay available to agent shells.
|
|
10
|
+
- #393 (#391 Task A): the local dashboard split into cookie-backed views, compact task cards with shared dialog/column details and exact-id attention badges; character presentation moved to the standalone pet window.
|
|
11
|
+
- #395 (#391 follow-up): the pet window shrinks to a compact 150x150 panel with the original pixel art in deeper state colors and a configurable base palette (`pet.palette`, or 1/2/3/4 while the window is key, or `ahub pet --palette`; blue, teal, violet, ochre) with a matching rim line; the status pill and message composer reveal only while the pointer is over the character, driven by a 10 Hz global-mouse-position poll against a page-posted hotspot rect because an inactive accessory panel receives no mouse-moved events; the pill shadow is dropped.
|
|
12
|
+
- #396 (#394): the dashboard Office view renders at an intrinsic 12 px/unit scale with 1 px non-scaling hairlines and padded room cards in both themes, and the desk More control is a separately reconciled sibling button in the non-button marks layer so assistive technology can reach it.
|
|
13
|
+
|
|
14
|
+
Review was adversarial and multi-round on the board: tasks #56 and #58 (owner kimi, reviewer claude) for the compact window and its round-3 fixes, #57 and #59 (owner codex, reviewer claude) for the Office refinement and its round-3 fixes. Owner iterations settled the final pet design (original art, teal default palette) and the hover-reveal interaction, each re-gated after the change.
|
|
15
|
+
|
|
16
|
+
## Document review
|
|
17
|
+
|
|
18
|
+
Every stale document was rechecked against the source commits of the four PRs. README, Quickstart and the adapters, daemon, tasks and tests area notes were confirmed accurate (version strings landed with the prepare commit; the new pet palette config write is console-local presentation and does not intersect the settings-registry rule). Operations and Security were amended at 59dd4803: Office room labels follow #396 (To do, Done), the pet window section names `ahub pet --palette`, Security enumerates the #391 view and column-focus cookies, the palette field in pet-view.json and the JXA bridge's palette operation with its validated config.local.json write path. All eight documents were restamped at 0f8a7ba4; `node scripts/check-docs.mjs --require-fresh` reports docs: OK.
|
|
19
|
+
|
|
20
|
+
## Recorded checks
|
|
21
|
+
|
|
22
|
+
- PR #395 head b8498c03: worktree `scripts/check.sh` 2744 pass, 0 fail, `check: OK`; CI Linux and macOS passed, seeded guards pending when the owner merged; the main-branch run of merge commit b2f076fd completed success, seeded guards included.
|
|
23
|
+
- PR #396 head aee84ba1: owner-reported worktree gate 2761 pass, 0 fail, `check: OK`; CI Linux, macOS and seeded guards passed; board review approved (task #59).
|
|
24
|
+
- Release candidate 9d21b1f4: local `scripts/check.sh` in an isolated worktree — 2761 pass, 0 fail, 190249 assertions, `check: OK`, no daemon leaks. The first candidate bundle (e9037e43) embedded `../../node_modules` paths because the worktree had no installed dependencies at build time; the bundle was rebuilt after `bun install` (9d21b1f4) and the freshness check passes. Later commits on this branch change documentation only.
|
|
25
|
+
- Release head: CI, document freshness at the merge commit, and the release workflow are required before tagging is reported complete.
|
|
26
|
+
|
|
27
|
+
Note: `scripts/check.sh` in the main checkout is not a valid signal there because the live hub daemon serving the project interferes (recorded in shared memory #194264); the isolated worktree gate is the valid signal.
|
|
28
|
+
|
|
29
|
+
## Not verified
|
|
30
|
+
|
|
31
|
+
Native, account and research legs remain open in #273 and #1. Native VoiceOver traversal of the pet window and the Office view was not run; accessibility is covered at DOM ancestry and keyboard level by tests. The installed runtime and live hubs are not upgraded by this release.
|
|
Binary file
|
package/docs/verified.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"documents": {
|
|
4
4
|
"README.md": {
|
|
5
|
-
"verifiedAgainst": "
|
|
5
|
+
"verifiedAgainst": "59dd48030a3272d984a738017d22bcecc66217e6",
|
|
6
6
|
"paths": [
|
|
7
7
|
"package.json",
|
|
8
8
|
"src/",
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
]
|
|
12
12
|
},
|
|
13
13
|
"docs/security.md": {
|
|
14
|
-
"verifiedAgainst": "
|
|
14
|
+
"verifiedAgainst": "59dd48030a3272d984a738017d22bcecc66217e6",
|
|
15
15
|
"paths": [
|
|
16
16
|
"src/",
|
|
17
17
|
"templates/"
|
|
18
18
|
]
|
|
19
19
|
},
|
|
20
20
|
"docs/operations.md": {
|
|
21
|
-
"verifiedAgainst": "
|
|
21
|
+
"verifiedAgainst": "59dd48030a3272d984a738017d22bcecc66217e6",
|
|
22
22
|
"paths": [
|
|
23
23
|
"package.json",
|
|
24
24
|
"src/",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
]
|
|
36
36
|
},
|
|
37
37
|
"docs/quickstart.md": {
|
|
38
|
-
"verifiedAgainst": "
|
|
38
|
+
"verifiedAgainst": "59dd48030a3272d984a738017d22bcecc66217e6",
|
|
39
39
|
"paths": [
|
|
40
40
|
"package.json",
|
|
41
41
|
"src/",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
]
|
|
44
44
|
},
|
|
45
45
|
"docs/agent-notes/adapters.md": {
|
|
46
|
-
"verifiedAgainst": "
|
|
46
|
+
"verifiedAgainst": "59dd48030a3272d984a738017d22bcecc66217e6",
|
|
47
47
|
"paths": [
|
|
48
48
|
"src/adapters/",
|
|
49
49
|
"src/pi/",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
]
|
|
99
99
|
},
|
|
100
100
|
"docs/agent-notes/daemon.md": {
|
|
101
|
-
"verifiedAgainst": "
|
|
101
|
+
"verifiedAgainst": "59dd48030a3272d984a738017d22bcecc66217e6",
|
|
102
102
|
"paths": [
|
|
103
103
|
"src/hub/daemon.ts",
|
|
104
104
|
"src/hub/control-client.ts",
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
]
|
|
156
156
|
},
|
|
157
157
|
"docs/agent-notes/tasks.md": {
|
|
158
|
-
"verifiedAgainst": "
|
|
158
|
+
"verifiedAgainst": "59dd48030a3272d984a738017d22bcecc66217e6",
|
|
159
159
|
"paths": [
|
|
160
160
|
"src/hub/tasks.ts",
|
|
161
161
|
"src/hub/board.ts",
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
]
|
|
173
173
|
},
|
|
174
174
|
"docs/agent-notes/tests.md": {
|
|
175
|
-
"verifiedAgainst": "
|
|
175
|
+
"verifiedAgainst": "59dd48030a3272d984a738017d22bcecc66217e6",
|
|
176
176
|
"paths": [
|
|
177
177
|
"test/",
|
|
178
178
|
"scripts/check.sh",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@staix/agent-hub",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.27",
|
|
4
4
|
"description": "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
"files": [
|
|
25
25
|
".claude-plugin",
|
|
26
26
|
"src",
|
|
27
|
+
"src/pet/window.jxa.js",
|
|
28
|
+
"src/pet/window.html",
|
|
27
29
|
"plugins",
|
|
28
30
|
"templates",
|
|
29
31
|
"scripts/check-docs.mjs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-hub",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.27",
|
|
4
4
|
"description": "Channel between Claude Code and the agent-hub daemon: peer messages from Codex, Kimi and the local worker arrive as channel events; hub_send replies.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Young Joon Lee",
|
|
@@ -16157,7 +16157,7 @@ import { join as join4 } from "path";
|
|
|
16157
16157
|
// package.json
|
|
16158
16158
|
var package_default = {
|
|
16159
16159
|
name: "@staix/agent-hub",
|
|
16160
|
-
version: "0.12.
|
|
16160
|
+
version: "0.12.27",
|
|
16161
16161
|
description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
|
|
16162
16162
|
license: "MIT",
|
|
16163
16163
|
type: "module",
|
|
@@ -16181,6 +16181,8 @@ var package_default = {
|
|
|
16181
16181
|
files: [
|
|
16182
16182
|
".claude-plugin",
|
|
16183
16183
|
"src",
|
|
16184
|
+
"src/pet/window.jxa.js",
|
|
16185
|
+
"src/pet/window.html",
|
|
16184
16186
|
"plugins",
|
|
16185
16187
|
"templates",
|
|
16186
16188
|
"scripts/check-docs.mjs",
|
package/src/cli/help.ts
CHANGED
|
@@ -18,6 +18,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
18
18
|
["ahub up [--unattended] [--no-console]", "start the daemon; interactive terminals enter console"],
|
|
19
19
|
["ahub console [--panels] [--color=auto|always|never]", "enter the human console; P cycles built-in pet characters/off; exiting leaves the daemon running"],
|
|
20
20
|
["ahub console --no-pet", "turn off attention for this session; combines with --panels and --color"],
|
|
21
|
+
["ahub pet --window [--close]", "start or close the macOS floating window, from a person's terminal"],
|
|
21
22
|
["ahub pet [--short]", "read attention without connecting; --short prints ahub!N only when action is needed"],
|
|
22
23
|
["ahub sdlc status [task] | explain <task> | export <task> | record <task> <receipt-json>", "read head-bound lifecycle gates and the next actor/action (person only)"],
|
|
23
24
|
["ahub sdlc queue <owner/repo> | release <owner/repo> [live-issue...]", "read-only merge and release evidence (person only)"],
|
|
@@ -110,7 +111,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
110
111
|
["ahub memory search <words> | show <id> | graph <id|node> [--hops 1|2] [--json]", "find entries, read one with its provenance, or list its graph neighbourhood"],
|
|
111
112
|
["ahub memory promote|demote|forget <id> | supersede <old> <new> | rebuild", "a person's changes to memory: move an entry between tiers, forget it, mark it superseded, or rebuild the index from the logs"],
|
|
112
113
|
["ahub memory sync [--remote <private git url>] | machine [--label <name>]", "sync hub memory with your other machines through a private git remote (a person's), or show or name this machine"],
|
|
113
|
-
["ahub memory import claude-mem [--db <path>] [--limit N] | suggestions | confirm|reject <id>", "copy the hub's notes from claude-mem into hub memory (a person's); review the model's suggested edges"],
|
|
114
|
+
["ahub memory import claude-mem [--db <path>] [--limit N] [--restart] | dedupe [--apply] | suggestions | confirm|reject <id>", "copy the hub's notes from claude-mem into hub memory (a person's); review the model's suggested edges"],
|
|
114
115
|
["ahub goal [list] [--all] | show <id> [--json]", "long-term tasks: state, which machine holds each and until when, linked board tasks"],
|
|
115
116
|
["ahub goal new <title> [--milestone <text>]... [--depends <id>]... [--shared] | link <id> <task> | state <id> <state> | done <id> | take <id> [--force]", "a person's changes to long-term tasks; take --force voids other machines' claims"],
|
|
116
117
|
]],
|
package/src/cli/identity.ts
CHANGED
|
@@ -28,6 +28,7 @@ export type PeerCommandAccess = "allowed" | "conductor" | "console";
|
|
|
28
28
|
|
|
29
29
|
/** A closed list: unknown commands cannot acquire console authority through an agent shell. */
|
|
30
30
|
export function classifyPeerCommand(command: string, args: readonly string[]): PeerCommandAccess {
|
|
31
|
+
if (command === "pet" && args.includes("--window")) return "console";
|
|
31
32
|
if (command === "permission" || command === "stop" || command === "watch") return "console";
|
|
32
33
|
if (args.some((arg) => arg === "--as-user" || arg.startsWith("--as-user="))) return "console";
|
|
33
34
|
if (command === "role") return ["list", "show"].includes(args[0] ?? "") ? "allowed" : "console";
|
|
@@ -61,7 +62,7 @@ export function cliCommandLabel(command: string, args: readonly string[]): strin
|
|
|
61
62
|
if (!["upgrade", "restart", "recovery", "recovery-run", "help", "--help", "--version", "version", "projects", "ui", "manager", "setup", "init", "daemon", "up", "console", "claude", "codex", "kimi", "qwen", "jcode", "pi", "local", "stop", "models", "say", "queue", "tail", "watch", "budget", "board", "task", "review", "remember", "ask", "route", "pause", "resume", "permit", "permission", "settings", "roster", "status", "pet", "logs", "export", "report", "research", "memory", "goal", "bench", "sdlc", "role", "facts", "check-path", "turns", "undo", "kill", "reset", "doctor"].includes(command)) return "unknown";
|
|
62
63
|
if (["task", "queue", "budget", "route", "recovery", "models", "projects", "research", "memory", "goal", "bench"].includes(command)) {
|
|
63
64
|
const sub = command === "bench" || command === "memory" || command === "goal" ? args.find((arg) => arg !== "--json") : args[0];
|
|
64
|
-
if (sub && ["propose", "show", "assign", "escalate", "label", "list", "resolve", "resume", "set", "execution", "explain", "status", "abort", "dispose", "setup", "start", "stop", "remove", "export", "backfill", "addons", "run", "report", "compare", "search", "graph", "promote", "demote", "forget", "supersede", "rebuild", "sync", "machine", "import", "suggestions", "confirm", "reject", "new", "link", "state", "done", "take"].includes(sub)) return `${command} ${sub}`;
|
|
65
|
+
if (sub && ["propose", "show", "assign", "escalate", "label", "list", "resolve", "resume", "set", "execution", "explain", "status", "abort", "dispose", "setup", "start", "stop", "remove", "export", "backfill", "addons", "run", "report", "compare", "search", "graph", "promote", "demote", "forget", "supersede", "dedupe", "rebuild", "sync", "machine", "import", "suggestions", "confirm", "reject", "new", "link", "state", "done", "take"].includes(sub)) return `${command} ${sub}`;
|
|
65
66
|
}
|
|
66
67
|
return (/^[a-z][a-z0-9-]*$/.test(command) && !/\s/.test(command)) || ["--help", "--version"].includes(command) ? command : "unknown";
|
|
67
68
|
}
|
package/src/cli/main.ts
CHANGED
|
@@ -5,7 +5,8 @@ import { projectedDiff } from "../hub/projected-diff.ts";
|
|
|
5
5
|
import { TextProjections } from "../hub/text-projection.ts";
|
|
6
6
|
import { terminalText } from "./console-state.ts";
|
|
7
7
|
import { petView, readPet, renderPet } from "./pet.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { startPetWindow, closePetWindow } from "../pet/window.ts";
|
|
9
|
+
import { loadPetConfig, savePetPalette } from "./pet-config.ts";
|
|
9
10
|
import { profileName, projectProfile, resolveProfile } from "../hub/profiles.ts";
|
|
10
11
|
import { catalogAssignments, loadRoleCatalog, selectedRoles } from "../hub/roles.ts";
|
|
11
12
|
import { attachWatch } from "./watch.ts";
|
|
@@ -677,6 +678,8 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
677
678
|
if (unattended) console.error(UNATTENDED_WARNING);
|
|
678
679
|
const status = await startProject(registeredProject(), { unattended });
|
|
679
680
|
console.log(`ahub up (ws://127.0.0.1:${status.controlPort}), state in ${stateDir}`);
|
|
681
|
+
try { if (loadPetConfig(cwd).window) console.log(await startPetWindow(stateDir, cwd)); }
|
|
682
|
+
catch { console.error("Pet window was not started; run ahub pet --window to inspect it."); }
|
|
680
683
|
if (process.stdin.isTTY && process.stdout.isTTY && !args.includes("--no-console")) await runConsole({ client: await connect(), cwd, stateDir });
|
|
681
684
|
},
|
|
682
685
|
|
|
@@ -1359,8 +1362,17 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1359
1362
|
hub.close();
|
|
1360
1363
|
},
|
|
1361
1364
|
|
|
1362
|
-
pet: () => {
|
|
1363
|
-
if (args.
|
|
1365
|
+
pet: async () => {
|
|
1366
|
+
if (args.includes("--window")) {
|
|
1367
|
+
if (args.some(arg => !["--window", "--close"].includes(arg))) return fail("usage: ahub pet --window [--close]");
|
|
1368
|
+
return console.log(await (args.includes("--close") ? closePetWindow(stateDir) : startPetWindow(stateDir, cwd)));
|
|
1369
|
+
}
|
|
1370
|
+
if (args.includes("--palette")) {
|
|
1371
|
+
if (args.length !== 2 || args[0] !== "--palette") return fail("usage: ahub pet [--short] | --window [--close] | --palette <blue|teal|violet|ochre>");
|
|
1372
|
+
try { return console.log(savePetPalette(cwd, args[1]!)); }
|
|
1373
|
+
catch (error) { return fail((error as Error).message); }
|
|
1374
|
+
}
|
|
1375
|
+
if (args.some(arg => arg !== "--short")) return fail("usage: ahub pet [--short] | --window [--close] | --palette <blue|teal|violet|ochre>");
|
|
1364
1376
|
const short = args.includes("--short");
|
|
1365
1377
|
let character: import("../pet/frame.ts").CharacterId | undefined;
|
|
1366
1378
|
try { character = loadPetConfig(cwd).character; } catch { /* explicit structural reads remain available with malformed presentation config */ }
|
package/src/cli/memory.ts
CHANGED
|
@@ -12,7 +12,7 @@ const loadConfigSafe = (cwd: string) => { try { return loadConfig(cwd); } catch
|
|
|
12
12
|
* #258: `ahub memory`, straight on the machine's store (no hub needed). Reading is anyone's; promote, demote, forget,
|
|
13
13
|
* supersede and rebuild are a person's (the identity gate refuses agent shells).
|
|
14
14
|
*/
|
|
15
|
-
export const MEMORY_USAGE = "usage: ahub memory [list] [--tier short|long] [--kind <kind>] [--peer <p>] [--machine <id>] [--since <iso>] [--limit N] | search <words> | show <id> | graph <id|node> [--hops 1|2] | promote|demote|forget <id> | supersede <old> <new> | rebuild | sync [--remote <url>] | machine [--label <name>] | import claude-mem [--db <path>] [--limit N] | suggestions | confirm|reject <suggestion id> [--json]";
|
|
15
|
+
export const MEMORY_USAGE = "usage: ahub memory [list] [--tier short|long] [--kind <kind>] [--peer <p>] [--machine <id>] [--since <iso>] [--limit N] | search <words> | show <id> | graph <id|node> [--hops 1|2] | promote|demote|forget <id> | supersede <old> <new> | rebuild | sync [--remote <url>] | machine [--label <name>] | import claude-mem [--db <path>] [--limit N] [--restart] | dedupe [--apply] | suggestions | confirm|reject <suggestion id> [--json]";
|
|
16
16
|
export const GOAL_USAGE = "usage: ahub goal [list] [--all] | show <id> | new <title> [--milestone <text>]... [--depends <id>]... [--shared] | link <id> <task id> | state <id> open|active|blocked|done|dropped | done <id> | take <id> [--force] [--json]";
|
|
17
17
|
|
|
18
18
|
const fail = (message: string): never => { throw new Error(message); };
|
|
@@ -85,17 +85,27 @@ export async function memoryCommand(args: string[], cwd: string, opts: { store?:
|
|
|
85
85
|
if (!r.ok) fail(`hub memory sync: ${r.error}`);
|
|
86
86
|
return `synced${r.pushed ? " and pushed" : ""}: ${r.entries} long-term entries visible here${r.warning ? `\nnote: ${r.warning}` : ""}`;
|
|
87
87
|
}
|
|
88
|
+
case "dedupe": {
|
|
89
|
+
const apply = rest.includes("--apply");
|
|
90
|
+
if (rest.some(arg => arg !== "--apply")) fail("usage: ahub memory dedupe [--apply] [--json]");
|
|
91
|
+
const result = store.dedupe(project, apply);
|
|
92
|
+
if (json) return JSON.stringify(result, null, 2);
|
|
93
|
+
return [result.groups.length ? `${result.groups.length} duplicate import group(s)${apply ? `; appended ${result.applied} supersede operation(s)` : "; use --apply to reconcile"}` : "no duplicate import groups to reconcile",
|
|
94
|
+
...result.groups.map(g => `${g.members.map(v => v.id.slice(0, 8)).join(", ")} -> ${g.target.slice(0, 8)}: ${g.members[0]!.text}`)].join("\n");
|
|
95
|
+
}
|
|
88
96
|
case "import": {
|
|
89
97
|
const db = flag("--db"), limit = flag("--limit");
|
|
90
|
-
|
|
98
|
+
const restart = rest.includes("--restart");
|
|
99
|
+
if (restart) rest.splice(rest.indexOf("--restart"), 1);
|
|
100
|
+
if (rest[0] !== "claude-mem" || rest.length !== 1) fail("usage: ahub memory import claude-mem [--db <path>] [--limit N] [--restart]");
|
|
91
101
|
if (limit && !(Number(limit) > 0)) fail("--limit takes a positive number");
|
|
92
102
|
const routing = currentRouting(cwd);
|
|
93
103
|
const policy = privatePaths(); // a note about a private path never reaches the synced log
|
|
94
|
-
const key = memoryProjectKey(cwd)
|
|
95
|
-
const nodes = (text: string) => extractNodes(text, { cwd, project: key, repo: key.startsWith("github.com/") ? key.slice("github.com/".length) : key,
|
|
96
|
-
const r = importClaudeMem(store, { project: key, chain: projectChain(cwd), nodes, isPii: (text) => detectSignals({ title: text, detail: "", refs: {} }, routing, cwd).includes("pii") || mentionsPrivatePath(text, [], policy, cwd), ...(db ? { db } : {}), ...(limit ? { limit: Number(limit) } : {}) });
|
|
104
|
+
const key = memoryProjectKey(cwd);
|
|
105
|
+
const nodes = (text: string) => extractNodes(text, { cwd, project: key, repo: key.startsWith("github.com/") ? key.slice("github.com/".length) : key, checkExists: false });
|
|
106
|
+
const r = importClaudeMem(store, { project: key, chain: projectChain(cwd), nodes, restart, isPii: (text) => detectSignals({ title: text, detail: "", refs: {} }, routing, cwd).includes("pii") || mentionsPrivatePath(text, [], policy, cwd), ...(db ? { db } : {}), ...(limit ? { limit: Number(limit) } : {}) });
|
|
97
107
|
if (json) return JSON.stringify(r, null, 2);
|
|
98
|
-
return `imported ${r.added} of ${r.read} hub notes from claude-mem as long-term entries (skipped: ${r.skipped.
|
|
108
|
+
return `imported ${r.added} of ${r.read} hub notes from claude-mem as long-term entries (skipped: ${r.skipped.model} model answers, ${r.skipped.pii} PII, ${r.skipped.secret} secret-looking; other candidates were empty, malformed or already imported)${r.more ? "\nmore remain: run it again" : ""}`;
|
|
99
109
|
}
|
|
100
110
|
case "suggestions": {
|
|
101
111
|
const pending = store.suggestions(project).filter((x) => x.status === "suggested");
|
package/src/cli/pet-config.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { loadPetConfig, type PetConfig } from "../pet/config.ts";
|
|
1
|
+
export { loadPetConfig, savePetPalette, type PetConfig } from "../pet/config.ts";
|
package/src/hub/path-policy.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { lstatSync, opendirSync, statSync } from "node:fs";
|
|
1
|
+
import { accessSync, constants, lstatSync, opendirSync, readlinkSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { realPath } from "./project.ts";
|
|
4
4
|
import { foldSegment } from "./permission-mode.ts";
|
|
@@ -25,6 +25,47 @@ export function policyTargets(cwd: string, paths: string[]): string[] {
|
|
|
25
25
|
const root = realPath(cwd);
|
|
26
26
|
return [...new Set(paths.flatMap(path => { const lexical = resolve(root, path); return [lexical, policyRealPath(lexical)]; }))];
|
|
27
27
|
}
|
|
28
|
+
type DiscoveryBudget = { entries: number };
|
|
29
|
+
/** A dangling direct child names a future lexical target; an ELOOP child opens no file. */
|
|
30
|
+
function directAliasTarget(path: string): string | undefined {
|
|
31
|
+
let target = path;
|
|
32
|
+
// Include the final target resolution after the last bounded redirect.
|
|
33
|
+
for (let hops = 0; hops <= 40; hops++) {
|
|
34
|
+
try { return policyRealPath(target); }
|
|
35
|
+
catch (error) {
|
|
36
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
37
|
+
if (code === "ELOOP") return undefined;
|
|
38
|
+
if (code !== "ENOENT") throw error;
|
|
39
|
+
}
|
|
40
|
+
let ancestor = target;
|
|
41
|
+
while (!existing(ancestor)) {
|
|
42
|
+
const parent = dirname(ancestor);
|
|
43
|
+
if (parent === ancestor) return undefined;
|
|
44
|
+
ancestor = parent;
|
|
45
|
+
}
|
|
46
|
+
if (!lstatSync(ancestor).isSymbolicLink()) return undefined;
|
|
47
|
+
target = resolve(dirname(ancestor), readlinkSync(ancestor), relative(ancestor, target));
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/** Only a completed bounded scan contributes aliases; a truncated root contributes none. */
|
|
52
|
+
function aliasScope(dir: string, lexical: string, actual: string, includeParents: boolean, budget: DiscoveryBudget): boolean {
|
|
53
|
+
// Readability must not depend on reaching readSync after an exhausted-budget return.
|
|
54
|
+
accessSync(dir, constants.R_OK);
|
|
55
|
+
const handle = opendirSync(dir);
|
|
56
|
+
let matched = false;
|
|
57
|
+
try {
|
|
58
|
+
if (budget.entries >= 4096) return false;
|
|
59
|
+
for (let entry = handle.readSync(); entry; entry = handle.readSync()) {
|
|
60
|
+
if (budget.entries >= 4096) return false;
|
|
61
|
+
budget.entries++;
|
|
62
|
+
if (!entry.isSymbolicLink()) continue;
|
|
63
|
+
const target = directAliasTarget(join(dir, entry.name));
|
|
64
|
+
if (target !== undefined && (beneath(lexical, target) || beneath(actual, target) || includeParents && (beneath(target, lexical) || beneath(target, actual)))) matched = true;
|
|
65
|
+
}
|
|
66
|
+
} finally { handle.closeSync(); }
|
|
67
|
+
return matched;
|
|
68
|
+
}
|
|
28
69
|
/**
|
|
29
70
|
* Either spelling protects: the lowercase comparison keeps `?` matching one code point whose fold expands (ß, fi), and
|
|
30
71
|
* the folded one catches NFD/NFC, compatibility and long-s aliases. Adding a match only ever adds protection.
|
|
@@ -69,7 +110,7 @@ function patternScope(pattern: string[], states: Set<number>, path: string, incl
|
|
|
69
110
|
return includeParents && states.size > 0; // this scope can contain a future protected descendant
|
|
70
111
|
}
|
|
71
112
|
/** Resolve wildcard directory aliases without trusting a cached expansion. Bound all filesystem discovery. */
|
|
72
|
-
function globScope(root: string, lexical: string, actual: string, path: string, includeParents: boolean, budget:
|
|
113
|
+
function globScope(root: string, lexical: string, actual: string, path: string, includeParents: boolean, budget: DiscoveryBudget, roots: Set<string>, aliasDiscovery = false): boolean {
|
|
73
114
|
const pattern = path.split("/").filter(p => p && p !== ".");
|
|
74
115
|
const start = closure(pattern, new Set([0]));
|
|
75
116
|
if (patternScope(pattern, start, relative(root, lexical), includeParents) || patternScope(pattern, start, relative(root, actual), includeParents)) return true;
|
|
@@ -77,6 +118,8 @@ function globScope(root: string, lexical: string, actual: string, path: string,
|
|
|
77
118
|
const real = policyRealPath(dir);
|
|
78
119
|
if (states.has(pattern.length)) {
|
|
79
120
|
if (beneath(lexical, real) || beneath(actual, real) || includeParents && (beneath(real, lexical) || beneath(real, actual))) return true;
|
|
121
|
+
// Defer new child aliases until every pre-existing policy match has had its discovery budget.
|
|
122
|
+
if (pattern.at(-1) === "**" && existing(real) && statSync(real).isDirectory()) roots.add(real);
|
|
80
123
|
// An earlier `**` can still match deeper: an alias below this match (`ss/ß -> other` for `**/ß`) is searched too.
|
|
81
124
|
// A trailing `**` only re-matches everything below, so it never walks the subtree (as before, for paths outside it).
|
|
82
125
|
states = new Set([...states].filter(i => i !== pattern.length && !(i === pattern.length - 1 && pattern[i] === "**")));
|
|
@@ -88,11 +131,13 @@ function globScope(root: string, lexical: string, actual: string, path: string,
|
|
|
88
131
|
if (!lowerBeneath(real, root)) throw new Error("policy glob discovery outside project");
|
|
89
132
|
const info = statSync(dir);
|
|
90
133
|
if (!info.isDirectory()) return false;
|
|
134
|
+
if (aliasDiscovery && budget.entries >= 4096) return false;
|
|
91
135
|
if (depth > 32 || ancestors.has(real) || budget.entries > 4096) throw new Error("policy glob discovery unavailable");
|
|
92
136
|
const chain = new Set(ancestors); chain.add(real);
|
|
93
137
|
const handle = opendirSync(dir);
|
|
94
138
|
try {
|
|
95
139
|
for (let entry = handle.readSync(); entry; entry = handle.readSync()) {
|
|
140
|
+
if (aliasDiscovery && budget.entries >= 4096) return false;
|
|
96
141
|
if (++budget.entries > 4096) throw new Error("policy glob discovery unavailable");
|
|
97
142
|
const next = advance(pattern, states, entry.name);
|
|
98
143
|
if (next.size && visit(join(dir, entry.name), next, depth + 1, chain)) return true;
|
|
@@ -102,14 +147,28 @@ function globScope(root: string, lexical: string, actual: string, path: string,
|
|
|
102
147
|
};
|
|
103
148
|
return visit(root, start, 0, new Set());
|
|
104
149
|
}
|
|
105
|
-
/** Compare declared spelling, real targets and
|
|
150
|
+
/** Compare declared spelling, real targets and bounded aliases; legacy discovery errors fail closed. */
|
|
106
151
|
export function matchesPolicy(cwd: string, path: string, paths: string[], includeParents = false): boolean {
|
|
107
152
|
if (!paths.length) return false;
|
|
108
153
|
try {
|
|
109
154
|
if (path.length > 4096) return true;
|
|
110
155
|
const root = realPath(cwd), lexical = resolve(root, path), actual = policyRealPath(lexical);
|
|
111
156
|
includeParents &&= !existing(lexical) || statSync(lexical).isDirectory();
|
|
112
|
-
const budget = { entries: 0 };
|
|
113
|
-
|
|
157
|
+
const budget = { entries: 0 }, roots = new Set<string>(), exact: string[] = [];
|
|
158
|
+
if (paths.some(entry => {
|
|
159
|
+
if (/[?*]/.test(entry)) return globScope(root, lexical, actual, entry, includeParents, budget, roots);
|
|
160
|
+
const targets = policyTargets(root, [entry]);
|
|
161
|
+
if (targets.some(target => beneath(lexical, target) || beneath(actual, target) || includeParents && (beneath(target, lexical) || beneath(target, actual)))) return true;
|
|
162
|
+
const real = targets.at(-1)!;
|
|
163
|
+
if (existing(real) && statSync(real).isDirectory()) roots.add(real);
|
|
164
|
+
exact.push(entry);
|
|
165
|
+
return false;
|
|
166
|
+
})) return true;
|
|
167
|
+
// New exact-root discovery cannot consume the budget before legacy wildcard matches are checked.
|
|
168
|
+
for (const entry of exact) {
|
|
169
|
+
if (globScope(root, lexical, actual, `${entry}/**`, includeParents, budget, roots, true)) return true;
|
|
170
|
+
}
|
|
171
|
+
for (const dir of roots) if (aliasScope(dir, lexical, actual, includeParents, budget)) return true;
|
|
172
|
+
return false;
|
|
114
173
|
} catch { return true; }
|
|
115
174
|
}
|
|
@@ -55,7 +55,9 @@ export class TextProjections {
|
|
|
55
55
|
/** Inputs whose full-budget conversion failed: the same bytes under the same command are not converted again. */
|
|
56
56
|
private readonly failed = new Set<string>();
|
|
57
57
|
constructor(private readonly options: { cwd: string; stateDir: string; config: ProjectionConfig; deny?: string[]; readAllow?: string[]; log?: (line: string) => void }) {
|
|
58
|
-
// Rooted at an empty system directory, not the project
|
|
58
|
+
// Rooted at an empty system directory, not the project. The project guarantee: project file data is denied, then the
|
|
59
|
+
// snapshot, the argv-named program/script files and the directory listings above an in-project script are granted, so
|
|
60
|
+
// no other project file is readable (system, toolchain, home and local.read_allow paths outside the project remain granted).
|
|
59
61
|
// Only where conversion can run: /var/empty is a macOS directory, and a host without the sandbox never converts.
|
|
60
62
|
this.sandbox = Object.keys(options.config).length && sandboxAvailable() ? profile(EMPTY_ROOT, false, options.readAllow, options.deny, ["."]) : "";
|
|
61
63
|
}
|
package/src/hub/ui.ts
CHANGED
|
@@ -6,8 +6,6 @@ import { PEER_START_RETRY_MS } from "./start-mode.ts";
|
|
|
6
6
|
import { taskProgress } from "../ui/task-progress.ts";
|
|
7
7
|
import { officeModel } from "../ui/office-model.ts";
|
|
8
8
|
import { needs } from "../ui/needs.ts";
|
|
9
|
-
import { CHARACTERS } from "../pet/characters.ts";
|
|
10
|
-
import { petPixels } from "../pet/pixels.ts";
|
|
11
9
|
import { createHash, randomBytes } from "node:crypto";
|
|
12
10
|
import { readFileSync } from "node:fs";
|
|
13
11
|
|
|
@@ -41,7 +39,7 @@ export function startDashboard(options: DashboardOptions) {
|
|
|
41
39
|
.replace("/* TASK_PROGRESS_MODEL */", () => `const taskProgress = ${taskProgress.toString()};`)
|
|
42
40
|
.replace("/* PEER_START_RETRY_MS */", () => `const PEER_START_RETRY_MS = ${PEER_START_RETRY_MS};`)
|
|
43
41
|
.replace("/* OFFICE_MODEL */", () => `const officeModel = ${officeModel.toString()};`)
|
|
44
|
-
.replace("/*
|
|
42
|
+
.replace("/* NEEDS_MODEL */", () => `const needs = ${needs.toString()};`);
|
|
45
43
|
const hashes = (tag: string) => [...html.matchAll(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`, "g"))]
|
|
46
44
|
.map((m) => `'sha256-${createHash("sha256").update(m[1]!).digest("base64")}'`).join(" ");
|
|
47
45
|
const headers = {
|