instar 1.3.1612 → 1.3.1613
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/dashboard/console/console.js +14 -2
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +31 -7
- package/dist/commands/server.js.map +1 -1
- package/dist/core/AppConversationStore.d.ts +2 -0
- package/dist/core/AppConversationStore.d.ts.map +1 -1
- package/dist/core/AppConversationStore.js +8 -1
- package/dist/core/AppConversationStore.js.map +1 -1
- package/dist/core/AppRecordSignatures.d.ts +23 -2
- package/dist/core/AppRecordSignatures.d.ts.map +1 -1
- package/dist/core/AppRecordSignatures.js +32 -5
- package/dist/core/AppRecordSignatures.js.map +1 -1
- package/dist/core/LocalAttentionItems.d.ts +49 -0
- package/dist/core/LocalAttentionItems.d.ts.map +1 -0
- package/dist/core/LocalAttentionItems.js +107 -0
- package/dist/core/LocalAttentionItems.js.map +1 -0
- package/dist/core/MacDesktopHost.d.ts +3 -0
- package/dist/core/MacDesktopHost.d.ts.map +1 -1
- package/dist/core/MacDesktopHost.js +10 -0
- package/dist/core/MacDesktopHost.js.map +1 -1
- package/dist/core/MachineLockStateReader.d.ts.map +1 -1
- package/dist/core/MachineLockStateReader.js +6 -1
- package/dist/core/MachineLockStateReader.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +6 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +33 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/types.d.ts +12 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +7 -8
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/messaging/shared/MessageLogger.d.ts +2 -0
- package/dist/messaging/shared/MessageLogger.d.ts.map +1 -1
- package/dist/messaging/shared/MessageLogger.js.map +1 -1
- package/dist/messaging/slack/SlackAdapter.d.ts +19 -0
- package/dist/messaging/slack/SlackAdapter.d.ts.map +1 -1
- package/dist/messaging/slack/SlackAdapter.js +21 -0
- package/dist/messaging/slack/SlackAdapter.js.map +1 -1
- package/dist/messaging/telegram-origin/OriginForwardToHolder.d.ts +13 -0
- package/dist/messaging/telegram-origin/OriginForwardToHolder.d.ts.map +1 -1
- package/dist/messaging/telegram-origin/OriginForwardToHolder.js +16 -0
- package/dist/messaging/telegram-origin/OriginForwardToHolder.js.map +1 -1
- package/dist/messaging/telegram-origin/OriginLegacy.d.ts +8 -0
- package/dist/messaging/telegram-origin/OriginLegacy.d.ts.map +1 -1
- package/dist/messaging/telegram-origin/OriginLegacy.js +34 -6
- package/dist/messaging/telegram-origin/OriginLegacy.js.map +1 -1
- package/dist/messaging/telegram-origin/TelegramOriginBoot.d.ts +3 -0
- package/dist/messaging/telegram-origin/TelegramOriginBoot.d.ts.map +1 -1
- package/dist/messaging/telegram-origin/TelegramOriginBoot.js +54 -10
- package/dist/messaging/telegram-origin/TelegramOriginBoot.js.map +1 -1
- package/dist/messaging/telegram-origin/types.d.ts +11 -0
- package/dist/messaging/telegram-origin/types.d.ts.map +1 -1
- package/dist/messaging/telegram-origin/types.js +15 -0
- package/dist/messaging/telegram-origin/types.js.map +1 -1
- package/dist/monitoring/promptRelaySettle.d.ts +35 -0
- package/dist/monitoring/promptRelaySettle.d.ts.map +1 -0
- package/dist/monitoring/promptRelaySettle.js +58 -0
- package/dist/monitoring/promptRelaySettle.js.map +1 -0
- package/dist/native/instar-desktop-host +0 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +3 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +15 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +78 -20
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.js +2 -2
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/mobileConversationRoutes.d.ts +16 -3
- package/dist/server/mobileConversationRoutes.d.ts.map +1 -1
- package/dist/server/mobileConversationRoutes.js +36 -8
- package/dist/server/mobileConversationRoutes.js.map +1 -1
- package/dist/server/mobilePairingRoutes.d.ts +1 -1
- package/dist/server/mobilePairingRoutes.d.ts.map +1 -1
- package/dist/server/mobilePairingRoutes.js +5 -2
- package/dist/server/mobilePairingRoutes.js.map +1 -1
- package/dist/server/mobileSlackRoutes.d.ts +162 -0
- package/dist/server/mobileSlackRoutes.d.ts.map +1 -0
- package/dist/server/mobileSlackRoutes.js +552 -0
- package/dist/server/mobileSlackRoutes.js.map +1 -0
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +59 -3
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/build-locked-proof.mjs +125 -0
- package/scripts/lib/locked-proof-build.mjs +71 -0
- package/scripts/locked-proof/configure-proof-agent.mjs +68 -0
- package/scripts/locked-proof/create-proof-user.sh +89 -0
- package/scripts/locked-proof/driver.mjs +457 -0
- package/scripts/locked-proof/lib.mjs +267 -0
- package/scripts/locked-proof/pty-feed.py +59 -0
- package/scripts/locked-proof/remove-proof-user.sh +18 -0
- package/scripts/locked-proof/reset-attempts.py +52 -0
- package/scripts/locked-proof/stage-proof-agents.sh +68 -0
- package/src/data/builtin-manifest.json +64 -64
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/src/scaffold/templates.ts +3 -1
- package/upgrades/1.3.1613.md +108 -0
- package/upgrades/side-effects/console-partial-survives-reload.eli16.md +5 -0
- package/upgrades/side-effects/console-partial-survives-reload.md +34 -0
- package/upgrades/side-effects/desktop-tests-stop-touching-tcc.eli16.md +13 -0
- package/upgrades/side-effects/desktop-tests-stop-touching-tcc.md +65 -0
- package/upgrades/side-effects/held-expiry-one-row-per-topic.eli16.md +5 -0
- package/upgrades/side-effects/held-expiry-one-row-per-topic.md +37 -0
- package/upgrades/side-effects/locked-proof-driver.md +72 -0
- package/upgrades/side-effects/locked-proof-test-build.md +64 -0
- package/upgrades/side-effects/locked-security-item-no-telegram.md +78 -0
- package/upgrades/side-effects/mobile-native-conversations-inc4.md +115 -0
- package/upgrades/side-effects/origin-config-unknown-not-missing.eli16.md +16 -0
- package/upgrades/side-effects/origin-config-unknown-not-missing.md +38 -0
- package/upgrades/side-effects/prompt-gate-settle-before-relay.eli16.md +11 -0
- package/upgrades/side-effects/prompt-gate-settle-before-relay.md +57 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
- The dashboard's home page keeps a slow machine's items for up to two minutes even across a page reload or reopening the app, instead of only while the page stays open.
|
|
9
|
+
|
|
10
|
+
Some desktop tests started the real packaged Mac desktop helper. macOS records a new Screen Recording / Accessibility entry for every path that runs it, so each test run from a checkout or a Local Verify clone left another "instar-desktop-host" row on the Mac running the tests, and one test asked macOS for permission, which showed real pop-ups.
|
|
11
|
+
|
|
12
|
+
- `MacDesktopHost` and the lock-state reader now refuse to start the real packaged helper inside a test runner (`VITEST`) unless `INSTAR_REAL_DESKTOP_HELPER_TESTS=1` is set. Fake test helpers have other names and still run.
|
|
13
|
+
- The tests that need the real helper run only with that opt-in.
|
|
14
|
+
|
|
15
|
+
Nothing changes outside a test runner.
|
|
16
|
+
|
|
17
|
+
- When held replies to one topic expire, they now add to a single alert for that topic (with a count) instead of creating one alert per message.
|
|
18
|
+
|
|
19
|
+
- New maintainer scripts in `scripts/locked-proof/` for the locked-Mac proof run (P12 crash tests and P15 Stop tests in `docs/specs/desktop-locked-screen.md`). Nothing runs against any Mac unless called with `--execute`; none of it ships as runtime behaviour.
|
|
20
|
+
- `driver.mjs`: the case table (27 P12 cases, 11 P15 cases), `expected` prints the expected-results table to commit before any run, `calibrate` measures the typing pace with one normal unlock, `run` drives each case through the real viewer page and records evidence plus a verdict to `results.jsonl`. The agent types the throwaway test user's password itself through the remote-desktop path, reading it once from its vault into memory; it is never printed, logged, written to results or put on a command line. The person is asked only for what software cannot do (local unlocks, a user switch, a login).
|
|
21
|
+
- `create-proof-user.sh`: generates the throwaway password, stores it in the vault first (`locked_proof_user_password`, plus Bitwarden; it refuses to run if the agent's `credential-store.mjs` still puts the encoded Bitwarden item on `bw`'s command line instead of stdin), creates the standard user `instarproof` by answering `sysadminctl`'s prompt through a pseudo-terminal (`pty-feed.py`), clears the user's account policies so macOS does not force a password change at first login, and checks the password authenticates.
|
|
22
|
+
- `stage-proof-agents.sh`, `configure-proof-agent.mjs`: install the `-lockedproof` test build in the user's own prefix and set up two proof agents (auto-update off, no start-on-login job, remote desktop on, locked access left off).
|
|
23
|
+
- Every signal is owner-checked (only the proof user's processes), a case that throws still resumes everything it stopped and relocks the test account, and an expectation that is not yet measured fails as "not measured" instead of passing.
|
|
24
|
+
- `reset-attempts.py`: resets the helper's attempt counter between batches under the helper's own lock, refused while a relock duty is open, without `--execute`, or for any store but the proof user's; every reset is logged.
|
|
25
|
+
- `remove-proof-user.sh`: tears the user down.
|
|
26
|
+
|
|
27
|
+
- New maintainer script `scripts/build-locked-proof.mjs`: the locked-screen test build the spec calls for (`docs/specs/desktop-locked-screen.md`, phase 3). It copies one committed revision into a scratch directory outside the checkout, adds a single `PROOF-RUN-IN-PROGRESS` row for one macOS major to that copy's proven-versions table, versions it `<release>-lockedproof.<n>`, builds it (the Swift helper is unchanged, so its source hash matches the release) and packs it, writing a manifest with the commit, macOS major, helper source hash and package checksum. The committed table is never touched and nothing is published. `--dry-run` shows the row and builds nothing.
|
|
28
|
+
- New test: the committed `PROVEN_MACOS_VERSIONS` table fails the unit suite if any row lacks an existing proof record under `docs/proofs/desktop-locked-screen/` for P12 or P15, or carries the test-build marker.
|
|
29
|
+
|
|
30
|
+
- An agent with no Telegram now has an attention list. Before, the remote-desktop security items ("A Mac may still be unlocked", "Remote desktop through the lock was turned off") had nowhere to go without Telegram: the server printed a warning and the owner was never told. They are now stored on disk (`state/local-attention-items.json`, one row per condition, bounded to 200 rows, open rows never dropped) and pushed to paired phones.
|
|
31
|
+
- `GET /attention`, `GET /attention/:id` and `PATCH /attention/:id` serve that local list when there is no Telegram (the list carries `surface: "local"`) instead of answering 503. The iPhone app's notification lookup reads it too. With Telegram, nothing changes.
|
|
32
|
+
- The relock record is only marked "reported" after the item is actually stored, so a failed write is retried by the next sweep.
|
|
33
|
+
- CLAUDE.md (new and existing agents): one bullet saying an agent without Telegram still has an attention list and where to read it.
|
|
34
|
+
|
|
35
|
+
- Slack conversations from the Instar iPhone app (native conversations increment 4). Where Slack is set up, the app's conversation list also shows Slack channels and threads, the phone can read their history, and a message typed in the app reaches the agent's Slack session exactly like a Slack message from the operator.
|
|
36
|
+
- Each message sent from the app into Slack is saved first, with the phone's signature and this machine's signed receipt (a new receipt version that also names the Slack conversation), so history can show whether it really came from the paired phone.
|
|
37
|
+
- A copy "📱 <owner> (from the app): …" is posted into the Slack channel or thread, with anything that looks like a password or key hidden.
|
|
38
|
+
- The agent's own Slack replies are now kept in the Slack message log, so the app shows both sides of the conversation.
|
|
39
|
+
- New optional setting `mobilePairing.slackOwnerUserId`: the operator's Slack user id. Until it is set, the app shows no Slack conversations. The agent proposes it and sets it only after the operator agrees.
|
|
40
|
+
- Existing agents get a new CLAUDE.md section about this on update.
|
|
41
|
+
|
|
42
|
+
- When a machine briefly cannot read its own messaging config (seen on a busy standby whose server stalled for tens of seconds), a message to a bug-group topic was refused with "this process has no messaging config issuesChatId", although the config holds it. The refusal now says what happened: the config could not be read. It is a retryable `503 origin-config-unavailable` with the reason (for example `origin-config-reader-timeout`).
|
|
43
|
+
- Messages refused this way, and the older "issues-chat-not-configured" and bare "origin-display-authority-unavailable" refusals already in the queue, were later marked "may already be delivered" and never sent. Both refusals happen before anything is sent, so recovery now replays them once config reads again, through the normal tone review, duplicate check and 6-hour deadline.
|
|
44
|
+
- The server log now records why a config refresh failed (`[telegram-origin] config snapshot unavailable: <reason>`) and when it recovered. Before, the reason was discarded.
|
|
45
|
+
|
|
46
|
+
Prompt Gate relayed every session prompt to Telegram the moment it saw one, even prompts that something else answered seconds later (an account swap after a usage limit, the permission-prompt floor, a respawn). The first relay in a topic also sent a separate "Prompt Gate is now active" message.
|
|
47
|
+
|
|
48
|
+
- A detected prompt now waits a short settle window (`promptGate.relaySettleMs`, default 45000 ms), then the live pane is read again. The prompt is relayed only if it is still on screen. A session that is gone, or a pane that no longer shows the prompt, means no message. An unreadable pane still relays.
|
|
49
|
+
- The one-time privacy note now rides on the first relayed prompt as a single line instead of arriving as its own message.
|
|
50
|
+
- `relaySettleMs: 0` restores the old immediate relay.
|
|
51
|
+
|
|
52
|
+
## What to Tell Your User
|
|
53
|
+
|
|
54
|
+
If one of your machines is slow when you reopen the app, its items no longer disappear from "what needs you".
|
|
55
|
+
|
|
56
|
+
If your Mac's Accessibility or Screen Recording list filled up with many "instar-desktop-host" entries, they came from automated test runs on that Mac and stop being added after this update. Old ones can be removed with the minus button; keep the one at ~/.instar/desktop-helper/instar-desktop-host switched on.
|
|
57
|
+
|
|
58
|
+
If replies to a conversation ever fail to go out, you'll see one alert for that conversation with a count, not dozens of copies, and the dashboard loads faster.
|
|
59
|
+
|
|
60
|
+
If you run me without Telegram and I ever can't confirm that a Mac relocked after a remote-desktop session, you'll now see it: it shows up in my attention list, in the Instar app, and as a phone notification. Before, that warning had nowhere to go.
|
|
61
|
+
|
|
62
|
+
You can now continue your Slack conversations from the Instar app. First I need to know which Slack account is yours: ask me to "link my Slack to the app" and I will propose your Slack user id for you to confirm. After that, your Slack channels show up in the app's conversation list, and what you type there reaches me as if you had typed it in Slack, with a copy posted in the channel so others see it. The app itself needs its next update to show the Slack rows.
|
|
63
|
+
|
|
64
|
+
If one of my machines is briefly too busy to read its own settings, messages I send from it to a bug topic now wait and arrive shortly after, instead of failing with a misleading "not configured" error and going missing.
|
|
65
|
+
|
|
66
|
+
You'll stop getting "your agent needs confirmation" messages for prompts that were already handled automatically. A prompt that really needs you arrives about 45 seconds after it appears.
|
|
67
|
+
|
|
68
|
+
## Summary of New Capabilities
|
|
69
|
+
|
|
70
|
+
None.
|
|
71
|
+
|
|
72
|
+
None. This removes a side effect of the test suite.
|
|
73
|
+
|
|
74
|
+
None.
|
|
75
|
+
|
|
76
|
+
- Attention list without Telegram: `GET /attention` / `PATCH /attention/:id` work on an agent with no Telegram, holding security items such as "A Mac may still be unlocked".
|
|
77
|
+
|
|
78
|
+
- Continue Slack conversations from the Instar app (list, history, send), with signed records and a copy in the channel. Dev-gated with native app conversations (`mobilePairing.native.enabled`); needs `mobilePairing.slackOwnerUserId`.
|
|
79
|
+
|
|
80
|
+
None. This fixes a misleading refusal and the lost messages behind it.
|
|
81
|
+
|
|
82
|
+
None. One new optional setting: `promptGate.relaySettleMs`.
|
|
83
|
+
|
|
84
|
+
## Evidence
|
|
85
|
+
|
|
86
|
+
- Live check: reloading Home while one machine's answer was missing dropped its row before this change and kept it after (preview).
|
|
87
|
+
|
|
88
|
+
Unit tests for the guard (both sides), plus the fake-helper suites passing with and without a built helper. Independent second-pass review concurred.
|
|
89
|
+
|
|
90
|
+
- On a live agent about 750 open "held reply expired" alerts (109 for one topic) made each pooled alert read 742 KB and the first read after a restart take 25 seconds. Unit test covers the shared per-topic condition and distinct per-message ids.
|
|
91
|
+
|
|
92
|
+
- Unit tests (no real user, lock screen or desktop helper is touched): case table coverage and selection; password shape and use of the random source; attempt reset refusal on an open duty; proof-target check; the runner against a fake Mac both passing and failing (Mac left unlocked) and never putting the password in evidence; every verdict reason; the user-creation script against stand-in commands (vault before user creation, password only via the prompt, policies cleared, verified, never printed); `pty-feed.py` both ways; `reset-attempts.py` both ways; dry runs do nothing.
|
|
93
|
+
|
|
94
|
+
- Unit tests: version naming, macOS major parsing, the rewrite against the real committed source (one row added, the rest of the file byte-identical), refusal of an already-proven version and of a missing declaration, the dry run leaving the committed table's bytes unchanged, refusal of an output directory inside the checkout, and the committed-table check on both sides (empty table passes; marker, stray path and missing record each fail).
|
|
95
|
+
|
|
96
|
+
- Unit: the store keeps one row per id, reopens on a re-raise, reports false when it cannot write, stays bounded without dropping open rows, and survives a corrupt file.
|
|
97
|
+
- Integration: the three attention routes serve, filter and resolve the local list without Telegram, and still answer 503 with neither Telegram nor a local list.
|
|
98
|
+
- E2E (production boot, no Telegram, remote desktop off so no real Mac helper starts): `GET /attention` answers 200, and the same method the locked-screen controller calls puts the security item on it.
|
|
99
|
+
|
|
100
|
+
- Unit tests: a Slack record verifies with the new receipt and fails when it is moved to another conversation, carries an old-style receipt, or was signed for another path; the store keeps the new receipt fields; the Slack bridge reads one channel or thread from the log and the in-memory history, and dispatches as the operator in the right thread; the CLAUDE.md section is added once.
|
|
101
|
+
- Integration tests (stub Slack adapter, real registry, request store and device signatures): list with and without the owner id, history merging Slack rows and app-sent rows, a send stored first and read back "verified", retry replay, refusals before anything is saved, a failed dispatch, a failed mirror, an emergency stop over the rate limit, invite removal, and recovery after the Slack log rotates.
|
|
102
|
+
- End-to-end test: a server built on the production path with a stub Slack adapter pairs a phone on the dashboard, lists the Slack channel, sends into it, the server's Slack dispatch receives it as the operator tagged signed, the mirror is posted, the agent's reply is logged, and the phone's history shows both.
|
|
103
|
+
|
|
104
|
+
- Mama PC (standby, 2026-11-02): sends to issue topic 10000000001 were refused at about 17:59, 20:03, 20:20 and 20:37 UTC, and the queued copies became `delivered-ambiguous` without being sent. Twenty fresh config reads there all succeeded in about 150 ms with `issuesChatId` present. The same log shows the server's event loop blocked (25 s drift bursts, "event-loop BLOCK 95% CPU") and the lifeline restarting it for being unresponsive, which is when a 2 s config read times out.
|
|
105
|
+
- Integration test: after boot, the config becomes unreadable; an issues-topic send is refused with `OriginConfigUnavailableError` (reason `origin-config-unparseable`), never "issues-chat-not-configured", and routes to the issues group again once the config reads.
|
|
106
|
+
- Unit test: both refusal bodies replay exactly once; a generic 500 and a relayed body stay uncertain.
|
|
107
|
+
|
|
108
|
+
Unit tests cover both sides of the decision (still on screen → relay; cleared, session gone → no relay; unreadable pane, nothing to match → relay; 0 disables the wait) plus wiring checks on the server relay path. Existing Prompt Gate tests pass.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Reopening the app no longer drops a slow machine's items
|
|
2
|
+
|
|
3
|
+
The dashboard's home page gathers "what needs you" from every one of the agent's machines. If one machine is slow to answer, the page keeps showing that machine's items from the last complete answer for up to two minutes, instead of dropping them or saying part of the page could not be checked. That memory only lived inside the open page, though. Reloading the page, or reopening the iPhone app (which reloads it), wiped it. If a machine happened to be slow at that moment, its items vanished from the list.
|
|
4
|
+
|
|
5
|
+
Now the last complete answer is also kept in the browser's session storage, which survives a reload of the same tab. After a reload, the page uses it under the same two-minute rule. If the browser does not allow storage, or it is full, the page behaves exactly as before. Nothing new is shown, and nothing older than two minutes is used.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Side-effects review: the last complete pooled answer survives a page reload
|
|
2
|
+
|
|
3
|
+
## Change
|
|
4
|
+
`dashboard/console/console.js`: `rememberComplete()` also writes the last complete pooled answer (attention items, sessions) to `sessionStorage` under `console.lastComplete.<key>`; `fillFromLastComplete()` reads it when page memory has none. Every read/write is in try/catch: with storage absent, full or unreadable it is page memory only, as before. The two-minute window (`PARTIAL_GRACE_MS` in `fillPartial`) and all fill rules are unchanged.
|
|
5
|
+
|
|
6
|
+
## Evidence
|
|
7
|
+
Live check 2026-11-02 against 1.3.1611: Home loaded with all machines, then a page reload whose pooled attention read dropped the Mac Mini (simulated `pool.failed`). The Mac Mini's needs-you row disappeared, because the remembered answer lived in page memory, which a reload clears. Reopening the iPhone app is a reload. Same check on the preview with this change: the Mac Mini's row stays after the reload.
|
|
8
|
+
|
|
9
|
+
## 1. Over-block
|
|
10
|
+
None.
|
|
11
|
+
|
|
12
|
+
## 2. Under-block
|
|
13
|
+
After two minutes the stored answer is ignored, exactly as in memory. A new browser tab has its own session storage.
|
|
14
|
+
|
|
15
|
+
## 3. Level-of-abstraction fit
|
|
16
|
+
Same place the memory copy is kept.
|
|
17
|
+
|
|
18
|
+
## 4. Signal vs authority
|
|
19
|
+
Display only.
|
|
20
|
+
|
|
21
|
+
## 5. Interactions
|
|
22
|
+
None; `fillPartial` is unchanged.
|
|
23
|
+
|
|
24
|
+
## 6. External surfaces
|
|
25
|
+
Browser session storage holds up to two answers (attention items ~380 KB, sessions) for the tab's lifetime. They contain what the page already displays.
|
|
26
|
+
|
|
27
|
+
## 6b. Operator-surface quality (Operator-Surface Quality standard)
|
|
28
|
+
Reopening the app while one machine is slow keeps that machine's needs-you rows and shows no "could not be checked" for the first two minutes, the same as navigating inside the app.
|
|
29
|
+
|
|
30
|
+
## 7. Multi-machine posture
|
|
31
|
+
Client-side only.
|
|
32
|
+
|
|
33
|
+
## 8. Rollback cost
|
|
34
|
+
Revert.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# ELI16 — why tests were filling the Mac's Accessibility list, and the fix
|
|
2
|
+
|
|
3
|
+
Macs keep a list of programs allowed to control the computer (Accessibility) and to see the screen (Screen Recording). The list remembers each program by where it sits on disk. Instar's remote-desktop helper is one such program, and the one that matters lives in a fixed place in your home folder.
|
|
4
|
+
|
|
5
|
+
Our automated tests started the real helper too. They ran it from a fresh copy of the code each time, often a throwaway copy used to check a pull request. Each copy sits somewhere new on disk, so each test run added another "instar-desktop-host" line to the list, all switched off. One test even asked macOS for permission, which made pop-ups appear on the screen of the person using that Mac. On the Mac Studio this produced dozens of lines and kept adding more after Justin removed them.
|
|
6
|
+
|
|
7
|
+
The fix has two parts. First, the code that starts the helper now refuses to start the real one when it is running inside the test tool, unless someone sets a specific switch meaning "this Mac is set aside for these tests". Second, the handful of tests that genuinely need the real helper now run only with that switch on. Tests that use a pretend helper, which is most of them, keep running as before.
|
|
8
|
+
|
|
9
|
+
Nothing changes for an agent running normally: the check only applies inside the test tool. The old lines already on a Mac stay until someone removes them with the minus button. The one at ~/.instar/desktop-helper/instar-desktop-host must stay on.
|
|
10
|
+
|
|
11
|
+
The decision for a reader: whether real-helper tests should ever run on an operator's everyday Mac. This change says no, by default.
|
|
12
|
+
|
|
13
|
+
One small fix came with it: the page that reports whether locked-screen access is set up now answers "cannot read it here" straight away when the helper cannot be checked, instead of waiting until the request times out.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Side-Effects Review — test runs stop touching the Mac's privacy lists
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `desktop-tests-stop-touching-tcc`
|
|
4
|
+
**Date:** `2026-11-02`
|
|
5
|
+
**Author:** `Echo`
|
|
6
|
+
**Second-pass reviewer:** `independent reviewer subagent (see below)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Operator report 2026-11-02 10:37 (topic 134088, with a photo): "instar-desktop-host" entries kept appearing in the Studio's Accessibility list after he removed them, plus a "tmux would like to access data from other apps" pop-up. Cause: desktop tests ran the real packaged helper (`dist/native/instar-desktop-host`) from each checkout / Local Verify clone. macOS keys privacy entries by path, so each run added a row; `desktop-own-identity` also runs `request-permission`, which raises real prompts.
|
|
11
|
+
|
|
12
|
+
Changes:
|
|
13
|
+
1. `src/core/MacDesktopHost.ts`: `packagedBinary()` throws `mac-desktop-host-test-guard` when `VITEST` is set, `INSTAR_REAL_DESKTOP_HELPER_TESTS` is not `1`, and the resolved binary is the real packaged helper. Logic in the exported pure function `realHelperBlockedUnderTest`.
|
|
14
|
+
2. `tests/e2e/desktop-own-identity.test.ts`, `tests/e2e/desktop-helper-retention-lifecycle.test.ts`: skip unless the opt-in is set (they spawn the binary directly or build their own signed copies, so the host guard alone does not cover them).
|
|
15
|
+
3. `tests/e2e/desktop-boot-lifecycle.test.ts`: the three live-Mac-target cases skip unless the opt-in is set. `tests/unit/desktop-helper-state-op.test.ts` (spawns the real binary to decide whether to skip) gets the same gate.
|
|
16
|
+
5. `src/core/MachineLockStateReader.ts`: its default probe, the one other direct spawn of the real helper, applies the same rule and reports "cannot run" (null) under a test runner.
|
|
17
|
+
4. `tests/unit/desktop-real-helper-test-guard.test.ts`: both sides of the guard.
|
|
18
|
+
|
|
19
|
+
6. `src/server/routes.ts` `GET /desktop/locked/status`: when the helper's locked state cannot be read (helper missing, refused, invalid), it answers 503 with a reason code instead of leaving the request hanging. Found by Local Verify: with the guard, the e2e test's request hung for 30 s (408). The real-helper case of `desktop-locked-lifecycle` is now opt-in; a new non-opt-in case checks the route answers.
|
|
20
|
+
|
|
21
|
+
## Decision-point inventory
|
|
22
|
+
|
|
23
|
+
- `realHelperBlockedUnderTest` — add — invariant: a test runner never starts the real packaged helper without an explicit opt-in. Deterministic by design: the harm (a permanent privacy-list row on the operator's Mac, real pop-ups) lands the moment the binary runs.
|
|
24
|
+
|
|
25
|
+
## 1. Over-block
|
|
26
|
+
|
|
27
|
+
Under a test runner, any code path that starts the real packaged helper now fails with `mac-desktop-host-test-guard` instead of running. The comparison uses the expected path, not a resolved one, so fake helpers in `dist/native` still run on a Mac where the real helper was never built (the second-pass review caught an earlier draft that resolved the real path and failed when it was missing). Tests that relied on the real helper are exactly the ones now opt-in gated; route-wiring assertions in the other desktop e2e files tolerate an unavailable probe (they already run on Linux CI where the helper does not exist). Production (no `VITEST`) is unaffected.
|
|
28
|
+
|
|
29
|
+
## 2. Under-block
|
|
30
|
+
|
|
31
|
+
A test that copies the helper to a new name and runs it (as the retention test does) is not caught by the host guard; that test is gated by the env opt-in instead. A future test doing the same would not be caught structurally; the unit test documents the rule, and the opt-in name is greppable.
|
|
32
|
+
|
|
33
|
+
Coverage that now runs only with the opt-in: the own-identity re-exec check, the signed kept-copy retention lifecycle, the live Mac target on the production route, and the helper state verb. These were already Mac-only (never on Linux CI). They need a deliberate opt-in run on a Mac set aside for it; tracked as commitment CMT-1241 (run them on the Laptop test user during the locked-Mac proof sitting).
|
|
34
|
+
|
|
35
|
+
## 3. Level-of-abstraction fit
|
|
36
|
+
|
|
37
|
+
The host's `packagedBinary()` is the single place every real-helper spawn resolves through (probe, capture, input, indicator, relock guard), so one check covers all verbs.
|
|
38
|
+
|
|
39
|
+
## 4. Signal vs authority compliance
|
|
40
|
+
|
|
41
|
+
The guard is a hard refusal, but only inside a test runner and only for one exact path; it gates no user-facing behaviour. Reference: docs/signal-vs-authority.md.
|
|
42
|
+
|
|
43
|
+
## 5. Interactions
|
|
44
|
+
|
|
45
|
+
`DesktopHelperRetention` is already disabled under a test runner, so no kept copy is used in tests; the guard covers the packaged fallback. No interaction with Local Verify beyond fewer rows on the verifying Mac.
|
|
46
|
+
|
|
47
|
+
## 6. External surfaces
|
|
48
|
+
|
|
49
|
+
Removes an external side effect of the test suite (privacy-list rows, permission prompts on the operator's Mac). No runtime surface changes.
|
|
50
|
+
|
|
51
|
+
## 7. Multi-machine posture
|
|
52
|
+
|
|
53
|
+
Machine-local by design (`hardware-bound-resource`: a Mac's privacy database is per machine). Every Mac that runs tests benefits independently.
|
|
54
|
+
|
|
55
|
+
## 8. Rollback cost
|
|
56
|
+
|
|
57
|
+
Revert the commit. Rows would start appearing again on test-running Macs; no data or state to repair.
|
|
58
|
+
|
|
59
|
+
## Second-pass review
|
|
60
|
+
|
|
61
|
+
**Reviewer:** independent reviewer subagent (two rounds, 2026-11-02)
|
|
62
|
+
|
|
63
|
+
Round 1 raised a concern: the guard resolved the default helper path on disk, so every fake-helper test failed on a Mac without a built helper (43 of 70), and `desktop-helper-state-op.test.ts` plus `MachineLockStateReader`'s default probe still ran the real helper. All three were fixed. Round 2: "Concur with the review" — fake-helper and guard tests 70/70 with and without a built helper; state-op, lock-state and watch-only tests 27 passed, 6 skipped (opt-in). Noted, no action: a symlink at the default path pointing to another name would pass the guard; nothing creates one.
|
|
64
|
+
|
|
65
|
+
Follow-up (route fix, item 6): independent reviewer — Concur with the review. Only `status()` is inside the try; the error passes through only when it matches the `mac-desktop-host-*` code shape, else a fixed code; success and pool paths unchanged; the test split is sound (the new case proves the route answers, not that a 503 happens, which is its job).
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# A topic's lost replies show as one alert, not one per message
|
|
2
|
+
|
|
3
|
+
When one of the agent's machines writes a reply but is not the machine allowed to send to Telegram right then, the reply is held and handed to the machine that is. If that hand-over does not happen within six hours, the reply expires and the agent raises an alert saying a reply to that topic could not be delivered. Each expired reply raised its own alert. Over two weeks the machines piled up about 750 of these open alerts, 109 for a single topic. They are kept out of the operator's alert topic, but every time the dashboard asks all the machines for open alerts, it downloads all of them, about three quarters of a megabyte, which is why the first load after a restart took 25 seconds.
|
|
4
|
+
|
|
5
|
+
Now all expiries for the same topic share one alert. The first one opens it; later ones add to its count and refresh its text, so a topic shows one row saying how many replies were lost instead of a wall of identical rows. Every expiry is still recorded in the delivery log with its own identifier, so nothing is hidden. Alerts created before this change are not merged, but they close on their own within two weeks.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Side-effects review: held-reply expiry alerts are one row per topic
|
|
2
|
+
|
|
3
|
+
## Change
|
|
4
|
+
`src/messaging/telegram-origin/OriginForwardToHolder.ts`: new `heldExpiryItem(row)` builds the attention item raised when a held reply expires. The id stays per operation; the new `conditionKey` is `telegram-origin-held:<topic>:expired` (or `unknown`). `src/commands/server.ts` `onHeldForwardExpired` raises that item (same category, priority, title, wording, source, `hubOnly`). With the attention lifecycle on, a later expiry for the same topic lands on the existing open row (occurrences +1, alias recorded, summary refreshed) instead of a new row.
|
|
5
|
+
|
|
6
|
+
## Evidence
|
|
7
|
+
On 2026-11-02 the pooled read `GET /attention?scope=pool&status=OPEN` returned 978 open items (742 KB); 744 were keyless "Held reply to topic N expired" rows (109 for topic 33890 alone), mostly from the Mac Mini, Laptop and Mama PC. The lifecycle could not merge them: their derived keys differ by operation id. The first read after a restart took 25 s, and the console showed "Checking in…" for that long. The held-reply logs (Mini, Laptop) show the expiries were mostly automated job output and stopped after 11-01.
|
|
8
|
+
|
|
9
|
+
## 1. Over-block
|
|
10
|
+
None; nothing is suppressed. Every expiry is still reported once and written to the held-reply audit log with its operation id.
|
|
11
|
+
|
|
12
|
+
## 2. Under-block
|
|
13
|
+
Rows raised before this change (no conditionKey) do not merge with the new keyed row; they age out under the existing 14-day automated window.
|
|
14
|
+
|
|
15
|
+
## 3. Level-of-abstraction fit
|
|
16
|
+
The producer names its condition; the lifecycle's one-row-per-condition rule does the merging, as documented for recurring items.
|
|
17
|
+
|
|
18
|
+
## 4. Signal vs authority
|
|
19
|
+
No decision logic; a notification's grouping only.
|
|
20
|
+
|
|
21
|
+
## 5. Interactions
|
|
22
|
+
`hubOnly` and priority unchanged. The aggregate/held-active items (`HeldForwardItemCollapser`) keep their own ids and keys. The `telegram-origin-held` resolver is unchanged.
|
|
23
|
+
|
|
24
|
+
## 6. External surfaces
|
|
25
|
+
The Attention list shows one expiry row per topic with a count instead of one row per lost message.
|
|
26
|
+
|
|
27
|
+
## 6b. Operator-surface quality (Operator-Surface Quality standard)
|
|
28
|
+
A topic that loses several replies reads as one row with its count, not a wall of identical rows; pooled reads stay small, so Home answers promptly instead of "Checking in…".
|
|
29
|
+
|
|
30
|
+
## 7. Multi-machine posture
|
|
31
|
+
Machine-local attention store per machine, unchanged; pooled read merges them.
|
|
32
|
+
|
|
33
|
+
## 8. Rollback cost
|
|
34
|
+
Revert.
|
|
35
|
+
|
|
36
|
+
## Class closure
|
|
37
|
+
unbounded-self-action: n/a. Changes the grouping key of an existing notification; adds no action, loop or trigger.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Side-Effects Review — locked-Mac proof driver and test-user setup scripts
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `locked-proof-driver`
|
|
4
|
+
**Date:** `2026-11-02`
|
|
5
|
+
**Author:** `Echo`
|
|
6
|
+
**Tier:** 1 (maintainer scripts, no runtime source change, nothing runs without `--execute`). Size: 7 new files under `scripts/locked-proof/` (~750 lines), 1 test file. Effort: no spec rounds; first commit attempt.
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
The locked-Mac proof plan (`.instar/lanes/locked-mac-proof-plan-134088.md`, items M3 and M5) needs a driver for P12/P15 and repeatable throwaway-user setup. This adds them as scripts only. One change from the plan, per the brief for this work: the agent types the throwaway user's password through the remote-desktop viewer, instead of the operator typing it about 30 times. The password is generated by the setup script, never chosen by a person, kept in the agent vault (`locked_proof_user_password`, stored through `credential-store.mjs` with the value on stdin, Bitwarden first), and read once into the driver's memory. It is not the owner's password: the spec's "no agent ever handles it" concerns the owner's real password, which this run never touches (the test account is empty and throwaway). The user is created without a forced password change at first login (`pwpolicy -clearaccountpolicies`; the plan's `newPasswordRequired` step is dropped).
|
|
11
|
+
|
|
12
|
+
Files: `lib.mjs` (cases, expected table, runner, verdict, password generator, attempt reset rule, target check), `driver.mjs` (real target: sudo against the proof user, the proof agents' HTTP API, the real viewer page through playwright-core with system Chrome), `create-proof-user.sh`, `pty-feed.py`, `stage-proof-agents.sh`, `configure-proof-agent.mjs`, `reset-attempts.py`, `remove-proof-user.sh`.
|
|
13
|
+
|
|
14
|
+
## Decision-point inventory
|
|
15
|
+
|
|
16
|
+
None in runtime code. The driver's verdict is a test-harness judgement recorded with its evidence; it gates nothing by itself (a passing record still goes through a normal PR to the proven-versions table).
|
|
17
|
+
|
|
18
|
+
## Review follow-ups (independent review on #2598)
|
|
19
|
+
|
|
20
|
+
1. Signals are owner-checked: `serverPid` asks `lsof -a -u instarproof` (only the proof user's listener), and `kill()` reads the pid's owner (`ps -o user=`) immediately before every signal and throws for anyone but `instarproof`. `assertProofTarget` now gets the real owner of each agent's listening process and of each agent directory, not a constant.
|
|
21
|
+
2. `runCase` runs the case in try/finally: whatever throws, `target.recover()` resumes every pid the run SIGSTOPped, clears the guard-stopper and locks the proof session; a recovery problem or the error fails the case by name. `close()` runs the same recovery.
|
|
22
|
+
3. `configure-proof-agent.mjs` and `reset-attempts.py` refuse without `--execute`, and refuse anything that does not resolve inside `/Users/instarproof` (the reset script's only other accepted root is a test root inside the system temporary directory named by `LOCKED_PROOF_TEST_ROOT`).
|
|
23
|
+
4. Expectations without a measurement now fail as "not measured" instead of passing silently. Measured from the run's own samples: relock time after a transport drop (15.5/15.6, must be at least the 30 s resume window) and whether the password field is left dirty at the end (15.9, from the store's `dirty` flag, which blocks any later Enter). Lock calls against another session (12.12/12.13) have no observable source yet, so those two cases fail as "not measured" until one exists.
|
|
24
|
+
5. Bitwarden: the agent's `credential-store.mjs` put the base64 item (which holds the password) on `bw`'s command line. Echo's copy on this machine now pipes it on stdin (`bw create item` / `bw edit item <id>` read the encoded JSON from stdin, per `bw --help`, version 2026.7.0). `create-proof-user.sh` refuses to run if the helper it would call still puts the item on the command line, so the Laptop's copy must be updated before setup. The password is on no command line in this flow: stdin to the helper, stdin to `bw`, the pty prompt to `sysadminctl` and `dscl`.
|
|
25
|
+
|
|
26
|
+
## 1. Over-block
|
|
27
|
+
|
|
28
|
+
None: no runtime path changes.
|
|
29
|
+
|
|
30
|
+
## 2. Under-block
|
|
31
|
+
|
|
32
|
+
- The verdict can only judge what it observes. "Lock call against another session" (12.12/12.13) is not directly observable from outside the helper; the driver records the marker outcome and leaves that sub-check to the helper's own audit rows, read at the sitting.
|
|
33
|
+
- The loginwindow submission count is recorded as raw evidence (`loginwindowLines`); its log pattern is unverified until the sitting, so the verdict relies on the helper's `enterPendingAt` records for "no submission after close".
|
|
34
|
+
- Timing of "immediately before Enter" depends on calibration; the plan's rerun rule (offset off by more than 500 ms) is applied by the person running the sitting from the recorded timestamps.
|
|
35
|
+
|
|
36
|
+
## 3. Level-of-abstraction fit
|
|
37
|
+
|
|
38
|
+
Maintainer tooling outside `src/`, driving the same surfaces an owner uses (viewer page, revoke route, PIN route). It adds no test-only hooks to the helper, so the helper under proof is byte-identical to the shipped one.
|
|
39
|
+
|
|
40
|
+
## 4. Signal vs authority compliance
|
|
41
|
+
|
|
42
|
+
Not applicable: no runtime authority. The scripts refuse to act on anything but the `instarproof` user running a `-lockedproof` build.
|
|
43
|
+
|
|
44
|
+
## 5. Interactions
|
|
45
|
+
|
|
46
|
+
- Uses `sudo -n` (passwordless sudo on the proof Mac) to signal only the proof user's processes and read its relock store. `reset-attempts.py` takes the helper's own `state.lock` and refuses while a duty is open.
|
|
47
|
+
- `create-proof-user.sh` depends on `sysadminctl -password -` prompting on its terminal; if a macOS version does not prompt, `pty-feed.py` fails loudly ("never asked") after storing the vault value, and no user exists. Rerun with `--replace-vault`.
|
|
48
|
+
- Starting the proof servers in the proof user's session needs that session to exist (the person's first login).
|
|
49
|
+
|
|
50
|
+
## 6. External surfaces
|
|
51
|
+
|
|
52
|
+
None for users or agents. A Bitwarden item "Instar locked-proof test user (<host>)" is created when setup runs.
|
|
53
|
+
|
|
54
|
+
## 7. Multi-machine posture
|
|
55
|
+
|
|
56
|
+
Machine-local by design: the proof runs on one Mac, against a user on that Mac.
|
|
57
|
+
|
|
58
|
+
## 8. Rollback cost
|
|
59
|
+
|
|
60
|
+
Delete the scripts. If setup was run, `remove-proof-user.sh --execute` deletes the user; the vault key is deleted by the agent.
|
|
61
|
+
|
|
62
|
+
## Conclusion
|
|
63
|
+
|
|
64
|
+
Adds the missing driver and setup tooling, with the agent typing the throwaway password so the operator's part is two permission grants and a few local actions. Tier 1.
|
|
65
|
+
|
|
66
|
+
## Second-pass review (if required)
|
|
67
|
+
|
|
68
|
+
Not required: no runtime gate, session lifecycle or messaging change.
|
|
69
|
+
|
|
70
|
+
## Evidence pointers
|
|
71
|
+
|
|
72
|
+
- `tests/unit/locked-proof-driver.test.ts`
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Side-Effects Review — locked-screen test build script
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `locked-proof-test-build`
|
|
4
|
+
**Date:** `2026-11-02`
|
|
5
|
+
**Author:** `Echo`
|
|
6
|
+
**Tier:** 1 (maintainer script + test, no runtime source change). Size: 2 new scripts (~170 lines), 1 new test file. Effort: no spec rounds, first commit attempt.
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
The locked-screen spec says P12 and P15 run "on a test build" that "alone compiles in a test proven-versions table; no environment variable, config key or route can reach it in dist". That build did not exist. This adds:
|
|
11
|
+
1. `scripts/lib/locked-proof-build.mjs`: pure helpers (version naming, macOS major parsing, the table rewrite, the committed-table proof check).
|
|
12
|
+
2. `scripts/build-locked-proof.mjs`: `git archive` of a committed revision into a scratch directory outside the checkout, rewrite of the scratch copy's `src/core/DesktopProvenVersions.ts` (one `PROOF-RUN-IN-PROGRESS <version>` row), version `<release>-lockedproof.<n>`, `npm run build`, `npm pack --ignore-scripts`, manifest with commit, macOS major, helper `sourceHash`, tgz SHA-256.
|
|
13
|
+
3. `tests/unit/locked-proof-build.test.ts`, including a guard that every committed table row cites existing proof records for P12 and P15 and never the marker.
|
|
14
|
+
|
|
15
|
+
## Decision-point inventory
|
|
16
|
+
|
|
17
|
+
- The macOS-version admission decision (`provenVersion`) is unchanged. The test build only feeds it a different table in a scratch copy, which is the mechanism the spec names.
|
|
18
|
+
|
|
19
|
+
## 1. Over-block
|
|
20
|
+
|
|
21
|
+
None: no runtime path changes.
|
|
22
|
+
|
|
23
|
+
## 2. Under-block
|
|
24
|
+
|
|
25
|
+
- Someone could copy the marker row into the committed table by hand; the new unit test fails the suite in that case.
|
|
26
|
+
- The test package could be installed somewhere other than the throwaway user. It is never published to npm and carries a distinct version; installation is a deliberate act by the proof setup scripts. The proof agent must run with auto-update off, or the updater would replace it with the latest release (handled by the setup scripts in the next PR).
|
|
27
|
+
|
|
28
|
+
## 3. Level-of-abstraction fit
|
|
29
|
+
|
|
30
|
+
A build-time source rewrite is exactly what the spec asks for (no env/config/route reaches the table). Kept as a maintainer script, not shipped runtime code.
|
|
31
|
+
|
|
32
|
+
## 4. Signal vs authority compliance
|
|
33
|
+
|
|
34
|
+
No decision logic added. The unit guard is a CI check on committed data.
|
|
35
|
+
|
|
36
|
+
## 5. Interactions
|
|
37
|
+
|
|
38
|
+
- `npm run build` in the scratch copy runs the normal build chain with `INSTAR_RELEASE_PRIVATE_KEY_PEM`, `INSTAR_RELEASE_PRIVATE_KEY_PEM_PATH` and `INSTAR_RELEASE_BUILD` removed from its environment, so the lock-file signer writes an unsigned lock-file (the scratch tree, a fresh `git archive`, has no `.instar-release-keys/`). A test build is never release-signed.
|
|
39
|
+
- The scratch copy symlinks this checkout's `node_modules` for the build and removes the link before packing.
|
|
40
|
+
- `tests/e2e/desktop-locked-lifecycle.test.ts` still asserts the shipped table is empty; unchanged.
|
|
41
|
+
|
|
42
|
+
## 6. External surfaces
|
|
43
|
+
|
|
44
|
+
None for users or agents. `scripts/` ships in the npm package, so the script is present in installs but does nothing unless run from a git checkout.
|
|
45
|
+
|
|
46
|
+
## 7. Multi-machine posture
|
|
47
|
+
|
|
48
|
+
Machine-local by design: a build tool run on one Mac for that Mac's macOS major.
|
|
49
|
+
|
|
50
|
+
## 8. Rollback cost
|
|
51
|
+
|
|
52
|
+
Delete the scripts and test. Nothing persists.
|
|
53
|
+
|
|
54
|
+
## Conclusion
|
|
55
|
+
|
|
56
|
+
Adds the spec's missing test build, plus a guard that the committed table can only hold proven rows. Tier 1.
|
|
57
|
+
|
|
58
|
+
## Second-pass review (if required)
|
|
59
|
+
|
|
60
|
+
Not required: no gate, session lifecycle or messaging change.
|
|
61
|
+
|
|
62
|
+
## Evidence pointers
|
|
63
|
+
|
|
64
|
+
- `tests/unit/locked-proof-build.test.ts`
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Side-Effects Review — locked-screen security item reaches an attention list without Telegram
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `locked-security-item-no-telegram`
|
|
4
|
+
**Date:** `2026-11-02`
|
|
5
|
+
**Author:** `Echo`
|
|
6
|
+
**Tier:** 1 (small, reversible: a revert restores the previous behaviour exactly). Size: 5 src files changed + 1 new (about 150 lines), 3 new test files. Effort: no spec rounds (Tier 1), first commit attempt.
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
`AgentServer`'s `raiseSecurity` dependency for `DesktopLockedController` went only through `telegramAdapter.createAttentionItem`. With no Telegram adapter it logged a warning and returned `false`, so on an agent without Telegram the owner was never told that a Mac may still be unlocked (relock failed), that the relock record was unreadable, or that locked-screen access was turned off. The locked-Mac proof plan (topic 134088, P12 cases 12.8/12.11) needs this item to be raised within about 10 s on a test agent with no Telegram, and it is a real gap for any agent run without Telegram.
|
|
11
|
+
|
|
12
|
+
Changes:
|
|
13
|
+
1. New `src/core/LocalAttentionItems.ts`: a file-backed list (`state/local-attention-items.json`, 0600, atomic tmp+rename write), one row per id, a re-raise refreshes and reopens the row, bounded to 200 rows dropping the oldest closed rows first and never an open one. `raise()` returns `true` only when the row is on disk.
|
|
14
|
+
2. `AgentServer`: the `raiseSecurity` closure becomes a private method `raiseLockedSecurityItem`. With Telegram: unchanged. Without: stores the item locally (HIGH, category security, source agent) and calls `mobilePairing.push.onHubPost` (best effort); returns `false` with a degradation report when the write fails. The store is constructed only when there is no Telegram adapter, and passed to the route context.
|
|
15
|
+
3. `routes.ts`: `GET /attention`, `GET /attention/:id`, `PATCH /attention/:id` serve the local list when `ctx.telegram` is null and `ctx.localAttention` is set. Other attention routes (POST, lifecycle, clear, remote-ack) keep their 503 without Telegram.
|
|
16
|
+
4. `mobilePairingRoutes.ts`: the notification title lookup `GET /mobile/attention/:id` falls back to the local list.
|
|
17
|
+
5. CLAUDE.md template + `migrateClaudeMd` bullet (sniff `local-attention-items.json`).
|
|
18
|
+
|
|
19
|
+
## Decision-point inventory
|
|
20
|
+
|
|
21
|
+
- `DesktopLockedController` "told" decision (marks the relock record `reported`) — unchanged logic; its input (`raiseSecurity` result) is now honest on agents without Telegram (true only when stored).
|
|
22
|
+
- No new gate, filter or block.
|
|
23
|
+
|
|
24
|
+
## 1. Over-block
|
|
25
|
+
|
|
26
|
+
None. Nothing is refused that was accepted before. Routes that answered 503 now answer 200 only when a local list exists (no Telegram).
|
|
27
|
+
|
|
28
|
+
## 2. Under-block
|
|
29
|
+
|
|
30
|
+
- Without Telegram, `POST /attention` still answers 503: an agent cannot queue its own items there. This change is about the owner seeing security items; the agent itself writing to the list is a separate question with no current user.
|
|
31
|
+
- The phone push is best-effort; if APNs is not configured the item is still on the list and in the app.
|
|
32
|
+
- The local list is not merged into `GET /attention?scope=pool` from peers that have Telegram (a peer's pool read calls this machine's `/attention`, which now answers with the local list, so it IS visible to Telegram peers' pool view).
|
|
33
|
+
|
|
34
|
+
## 3. Level-of-abstraction fit
|
|
35
|
+
|
|
36
|
+
The attention store living inside `TelegramAdapter` is the root cause; moving it out is a large refactor. This adds the smallest store at the same route surface, so readers (`GET /attention`, the app) need no new address.
|
|
37
|
+
|
|
38
|
+
## 4. Signal vs authority compliance
|
|
39
|
+
|
|
40
|
+
Signal only: the store records and reports. No blocking authority added.
|
|
41
|
+
|
|
42
|
+
## 5. Interactions
|
|
43
|
+
|
|
44
|
+
- `DesktopLockedController.sweep` retries an unreported `relock-failed` each sweep until `raiseSecurity` returns true; now it gets true on a no-Telegram agent after the first successful write, so `mark-reported` runs and the guard's record closes as before with Telegram.
|
|
45
|
+
- `disable()` raises an item each time locked access is turned off; the row is reused (same id), so repeated disables refresh one row.
|
|
46
|
+
- Mobile push dedupe key includes a timestamp, so a re-raise can push again; push budgets (2-minute fold, daily cap) still bound it.
|
|
47
|
+
- No change when a Telegram adapter exists: `localAttention` is null and every route takes its old path.
|
|
48
|
+
|
|
49
|
+
## 6. External surfaces
|
|
50
|
+
|
|
51
|
+
`GET /attention` without Telegram changes from 503 to 200 with `surface: "local"`. Any client treating 503 as "no Telegram" now gets a list instead; the dashboard and the app already render an item list. CLAUDE.md gains one bullet.
|
|
52
|
+
|
|
53
|
+
## 7. Multi-machine posture
|
|
54
|
+
|
|
55
|
+
Machine-local by design: the security item concerns this Mac. Peers see it through their existing `GET /attention?scope=pool` fan-out, which calls this machine's `/attention`.
|
|
56
|
+
|
|
57
|
+
## 8. Rollback cost
|
|
58
|
+
|
|
59
|
+
Revert the PR. The leftover `state/local-attention-items.json` is inert. No migration.
|
|
60
|
+
|
|
61
|
+
## Class-Closure Declaration
|
|
62
|
+
|
|
63
|
+
- `unbounded-self-action`: closure `n/a`. No new self-triggered loop. The added local write and phone push run once per raise from the existing DesktopLockedController sweep, which stops raising a condition once `raiseSecurity` returns true (`mark-reported`); a failed write retries at the existing 10 s sweep cadence only while the condition is open. Each push passes the MobilePush per-device budgets and folding.
|
|
64
|
+
|
|
65
|
+
## Conclusion
|
|
66
|
+
|
|
67
|
+
Closes a real gap where a security warning was dropped on agents without Telegram, with no change for agents that have Telegram. Tier 1.
|
|
68
|
+
|
|
69
|
+
## Second-pass review (if required)
|
|
70
|
+
|
|
71
|
+
Not required: no block/allow, session lifecycle, gate or sentinel logic changes; this only gives an existing report a place to land.
|
|
72
|
+
|
|
73
|
+
## Evidence pointers
|
|
74
|
+
|
|
75
|
+
- `tests/unit/local-attention-items.test.ts`
|
|
76
|
+
- `tests/integration/attention-local-without-telegram.test.ts`
|
|
77
|
+
- `tests/e2e/locked-security-item-without-telegram.test.ts`
|
|
78
|
+
- `tests/unit/PostUpdateMigrator-attentionWithoutTelegram.test.ts`
|