things-api 0.17.0 → 0.19.0

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.
Files changed (277) hide show
  1. package/README.md +16 -9
  2. package/deputy/AppIcon.icns +0 -0
  3. package/deputy/VERSION +1 -0
  4. package/deputy/helpers-Info.plist +27 -0
  5. package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
  6. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Info.plist +22 -0
  7. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
  8. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Resources/AppIcon.icns +0 -0
  9. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/_CodeSignature/CodeResources +128 -0
  10. package/deputy/prebuilt/Things API Helper.app/Contents/Info.plist +27 -0
  11. package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
  12. package/deputy/prebuilt/Things API Helper.app/Contents/Resources/AppIcon.icns +0 -0
  13. package/deputy/prebuilt/Things API Helper.app/Contents/_CodeSignature/CodeResources +137 -0
  14. package/deputy/reader/Info.plist +22 -0
  15. package/deputy/reader/entitlements.plist +45 -0
  16. package/deputy/reader/main.swift +580 -0
  17. package/deputy/src/main.swift +133 -0
  18. package/deputy/src/osascript.swift +102 -0
  19. package/deputy/src/server.swift +373 -0
  20. package/deputy/src/sqlite.swift +121 -0
  21. package/deputy/src/tcc.swift +58 -0
  22. package/dist/audit/schema.d.ts +13 -1
  23. package/dist/audit/schema.js.map +1 -1
  24. package/dist/capability.d.ts +160 -0
  25. package/dist/capability.js +416 -0
  26. package/dist/capability.js.map +1 -0
  27. package/dist/cli/commands/doctor.js +181 -2
  28. package/dist/cli/commands/doctor.js.map +1 -1
  29. package/dist/cli/commands/helpers.d.ts +17 -0
  30. package/dist/cli/commands/helpers.js +275 -0
  31. package/dist/cli/commands/helpers.js.map +1 -0
  32. package/dist/cli/commands/op-result.d.ts +15 -0
  33. package/dist/cli/commands/op-result.js +45 -0
  34. package/dist/cli/commands/op-result.js.map +1 -0
  35. package/dist/cli/commands/reads.js +47 -1
  36. package/dist/cli/commands/reads.js.map +1 -1
  37. package/dist/cli/commands/setup.d.ts +9 -0
  38. package/dist/cli/commands/setup.js +63 -83
  39. package/dist/cli/commands/setup.js.map +1 -1
  40. package/dist/cli/commands/writes.d.ts +1 -1
  41. package/dist/cli/commands/writes.js +262 -109
  42. package/dist/cli/commands/writes.js.map +1 -1
  43. package/dist/cli/dry-run.d.ts +1 -1
  44. package/dist/cli/dry-run.js +1 -1
  45. package/dist/cli/glyphs.d.ts +23 -4
  46. package/dist/cli/glyphs.js +46 -19
  47. package/dist/cli/glyphs.js.map +1 -1
  48. package/dist/cli/help.d.ts +1 -1
  49. package/dist/cli/help.js +53 -27
  50. package/dist/cli/help.js.map +1 -1
  51. package/dist/cli/helpers-check.d.ts +10 -0
  52. package/dist/cli/helpers-check.js +59 -0
  53. package/dist/cli/helpers-check.js.map +1 -0
  54. package/dist/cli/main.js +22 -1
  55. package/dist/cli/main.js.map +1 -1
  56. package/dist/cli/period.d.ts +3 -1
  57. package/dist/cli/period.js +1 -1
  58. package/dist/cli/period.js.map +1 -1
  59. package/dist/cli/read-driver.js +26 -1
  60. package/dist/cli/read-driver.js.map +1 -1
  61. package/dist/cli/render.d.ts +31 -4
  62. package/dist/cli/render.js +127 -15
  63. package/dist/cli/render.js.map +1 -1
  64. package/dist/client.d.ts +22 -2
  65. package/dist/client.js +52 -15
  66. package/dist/client.js.map +1 -1
  67. package/dist/config.d.ts +31 -0
  68. package/dist/config.js +9 -0
  69. package/dist/config.js.map +1 -1
  70. package/dist/contracts.d.ts +6 -2
  71. package/dist/contracts.js +1 -1
  72. package/dist/contracts.js.map +1 -1
  73. package/dist/db/baselines/db-v26.js +8 -4
  74. package/dist/db/baselines/db-v26.js.map +1 -1
  75. package/dist/db/baselines/db-v27.d.ts +42 -0
  76. package/dist/db/baselines/db-v27.js +47 -0
  77. package/dist/db/baselines/db-v27.js.map +1 -0
  78. package/dist/db/baselines/index.js +2 -1
  79. package/dist/db/baselines/index.js.map +1 -1
  80. package/dist/db/locate.d.ts +4 -1
  81. package/dist/db/locate.js +3 -1
  82. package/dist/db/locate.js.map +1 -1
  83. package/dist/db/schema.d.ts +1 -1
  84. package/dist/db/schema.js +9 -0
  85. package/dist/db/schema.js.map +1 -1
  86. package/dist/deputy/bridge-worker.d.ts +1 -0
  87. package/dist/deputy/bridge-worker.js +79 -0
  88. package/dist/deputy/bridge-worker.js.map +1 -0
  89. package/dist/deputy/bridge.d.ts +14 -0
  90. package/dist/deputy/bridge.js +70 -0
  91. package/dist/deputy/bridge.js.map +1 -0
  92. package/dist/deputy/client.d.ts +19 -0
  93. package/dist/deputy/client.js +104 -0
  94. package/dist/deputy/client.js.map +1 -0
  95. package/dist/deputy/db-facade.d.ts +18 -0
  96. package/dist/deputy/db-facade.js +71 -0
  97. package/dist/deputy/db-facade.js.map +1 -0
  98. package/dist/deputy/files.d.ts +1 -0
  99. package/dist/deputy/files.js +16 -0
  100. package/dist/deputy/files.js.map +1 -0
  101. package/dist/deputy/install.d.ts +319 -0
  102. package/dist/deputy/install.js +1239 -0
  103. package/dist/deputy/install.js.map +1 -0
  104. package/dist/deputy/notice.d.ts +6 -0
  105. package/dist/deputy/notice.js +28 -0
  106. package/dist/deputy/notice.js.map +1 -0
  107. package/dist/deputy/notices.d.ts +31 -0
  108. package/dist/deputy/notices.js +94 -0
  109. package/dist/deputy/notices.js.map +1 -0
  110. package/dist/deputy/osa.d.ts +21 -0
  111. package/dist/deputy/osa.js +72 -0
  112. package/dist/deputy/osa.js.map +1 -0
  113. package/dist/deputy/protocol.d.ts +140 -0
  114. package/dist/deputy/protocol.js +143 -0
  115. package/dist/deputy/protocol.js.map +1 -0
  116. package/dist/deputy/routing.d.ts +84 -0
  117. package/dist/deputy/routing.js +422 -0
  118. package/dist/deputy/routing.js.map +1 -0
  119. package/dist/deputy/shortcuts-exec.d.ts +10 -0
  120. package/dist/deputy/shortcuts-exec.js +50 -0
  121. package/dist/deputy/shortcuts-exec.js.map +1 -0
  122. package/dist/diagnose.d.ts +57 -2
  123. package/dist/diagnose.js +105 -3
  124. package/dist/diagnose.js.map +1 -1
  125. package/dist/direct-setup.d.ts +78 -0
  126. package/dist/direct-setup.js +484 -0
  127. package/dist/direct-setup.js.map +1 -0
  128. package/dist/host-access.d.ts +63 -0
  129. package/dist/host-access.js +165 -0
  130. package/dist/host-access.js.map +1 -0
  131. package/dist/index.d.ts +31 -5
  132. package/dist/index.js +23 -1
  133. package/dist/index.js.map +1 -1
  134. package/dist/mcp/server.d.ts +27 -1
  135. package/dist/mcp/server.js +300 -97
  136. package/dist/mcp/server.js.map +1 -1
  137. package/dist/model/mappers.d.ts +12 -3
  138. package/dist/model/mappers.js +6 -1
  139. package/dist/model/mappers.js.map +1 -1
  140. package/dist/model/recurrence.d.ts +5 -1
  141. package/dist/model/recurrence.js +19 -1
  142. package/dist/model/recurrence.js.map +1 -1
  143. package/dist/model/template-projection.d.ts +26 -0
  144. package/dist/model/template-projection.js +113 -0
  145. package/dist/model/template-projection.js.map +1 -0
  146. package/dist/op-result.d.ts +55 -0
  147. package/dist/op-result.js +161 -0
  148. package/dist/op-result.js.map +1 -0
  149. package/dist/read/detail.js +7 -1
  150. package/dist/read/detail.js.map +1 -1
  151. package/dist/read/filter-contract.d.ts +1 -1
  152. package/dist/read/filter-contract.js +4 -0
  153. package/dist/read/filter-contract.js.map +1 -1
  154. package/dist/read/queries.js +8 -1
  155. package/dist/read/queries.js.map +1 -1
  156. package/dist/read/shape.js +4 -0
  157. package/dist/read/shape.js.map +1 -1
  158. package/dist/read/views.d.ts +35 -4
  159. package/dist/read/views.js +120 -18
  160. package/dist/read/views.js.map +1 -1
  161. package/dist/session-grant.d.ts +48 -0
  162. package/dist/session-grant.js +170 -0
  163. package/dist/session-grant.js.map +1 -0
  164. package/dist/surface-copy.d.ts +8 -0
  165. package/dist/surface-copy.js +8 -0
  166. package/dist/surface-copy.js.map +1 -1
  167. package/dist/sync-health.js +6 -2
  168. package/dist/sync-health.js.map +1 -1
  169. package/dist/wizard.d.ts +58 -0
  170. package/dist/wizard.js +167 -0
  171. package/dist/wizard.js.map +1 -0
  172. package/dist/write/accessibility-probe.js +4 -2
  173. package/dist/write/accessibility-probe.js.map +1 -1
  174. package/dist/write/automation-probe.js +5 -1
  175. package/dist/write/automation-probe.js.map +1 -1
  176. package/dist/write/availability.d.ts +1 -1
  177. package/dist/write/availability.js +10 -6
  178. package/dist/write/availability.js.map +1 -1
  179. package/dist/write/batch.d.ts +20 -0
  180. package/dist/write/batch.js +94 -15
  181. package/dist/write/batch.js.map +1 -1
  182. package/dist/write/capabilities.d.ts +9 -0
  183. package/dist/write/capabilities.js +2 -0
  184. package/dist/write/capabilities.js.map +1 -1
  185. package/dist/write/clear-reminder.js +1 -1
  186. package/dist/write/clear-reminder.js.map +1 -1
  187. package/dist/write/commands.d.ts +12 -1
  188. package/dist/write/commands.js +170 -237
  189. package/dist/write/commands.js.map +1 -1
  190. package/dist/write/experimental.d.ts +19 -0
  191. package/dist/write/experimental.js +50 -0
  192. package/dist/write/experimental.js.map +1 -1
  193. package/dist/write/failure-hints.d.ts +3 -1
  194. package/dist/write/failure-hints.js.map +1 -1
  195. package/dist/write/guards.js +19 -2
  196. package/dist/write/guards.js.map +1 -1
  197. package/dist/write/lock.d.ts +16 -13
  198. package/dist/write/lock.js +55 -0
  199. package/dist/write/lock.js.map +1 -1
  200. package/dist/write/make-repeating-project.js +9 -13
  201. package/dist/write/make-repeating-project.js.map +1 -1
  202. package/dist/write/move.js +14 -6
  203. package/dist/write/move.js.map +1 -1
  204. package/dist/write/operations.d.ts +64 -25
  205. package/dist/write/operations.js +7 -0
  206. package/dist/write/operations.js.map +1 -1
  207. package/dist/write/opid.d.ts +14 -0
  208. package/dist/write/opid.js +22 -0
  209. package/dist/write/opid.js.map +1 -1
  210. package/dist/write/param-schema.d.ts +124 -0
  211. package/dist/write/param-schema.js +747 -0
  212. package/dist/write/param-schema.js.map +1 -0
  213. package/dist/write/pipeline.d.ts +78 -2
  214. package/dist/write/pipeline.js +249 -4
  215. package/dist/write/pipeline.js.map +1 -1
  216. package/dist/write/pre-state.d.ts +8 -0
  217. package/dist/write/pre-state.js +62 -17
  218. package/dist/write/pre-state.js.map +1 -1
  219. package/dist/write/promote-clone.js +635 -367
  220. package/dist/write/promote-clone.js.map +1 -1
  221. package/dist/write/reorder.js +95 -29
  222. package/dist/write/reorder.js.map +1 -1
  223. package/dist/write/repeat-anchor.d.ts +30 -0
  224. package/dist/write/repeat-anchor.js +166 -1
  225. package/dist/write/repeat-anchor.js.map +1 -1
  226. package/dist/write/repeat-rule.d.ts +28 -1
  227. package/dist/write/repeat-rule.js +64 -4
  228. package/dist/write/repeat-rule.js.map +1 -1
  229. package/dist/write/resolution-timestamps.d.ts +11 -0
  230. package/dist/write/resolution-timestamps.js +83 -38
  231. package/dist/write/resolution-timestamps.js.map +1 -1
  232. package/dist/write/reversibility.js +4 -0
  233. package/dist/write/reversibility.js.map +1 -1
  234. package/dist/write/template-mutation.d.ts +137 -0
  235. package/dist/write/template-mutation.js +451 -0
  236. package/dist/write/template-mutation.js.map +1 -0
  237. package/dist/write/undo.js +10 -20
  238. package/dist/write/undo.js.map +1 -1
  239. package/dist/write/update-fields.d.ts +151 -0
  240. package/dist/write/update-fields.js +427 -0
  241. package/dist/write/update-fields.js.map +1 -0
  242. package/dist/write/vectors/applescript.js +10 -16
  243. package/dist/write/vectors/applescript.js.map +1 -1
  244. package/dist/write/vectors/registry.d.ts +1 -1
  245. package/dist/write/vectors/registry.js +10 -2
  246. package/dist/write/vectors/registry.js.map +1 -1
  247. package/dist/write/vectors/session-reachability.d.ts +55 -0
  248. package/dist/write/vectors/session-reachability.js +56 -8
  249. package/dist/write/vectors/session-reachability.js.map +1 -1
  250. package/dist/write/vectors/shortcuts.js +7 -16
  251. package/dist/write/vectors/shortcuts.js.map +1 -1
  252. package/dist/write/vectors/simulator.d.ts +16 -1
  253. package/dist/write/vectors/simulator.js +68 -12
  254. package/dist/write/vectors/simulator.js.map +1 -1
  255. package/dist/write/vectors/types.d.ts +124 -1
  256. package/dist/write/vectors/ui-recipes.d.ts +19 -0
  257. package/dist/write/vectors/ui-recipes.js +158 -38
  258. package/dist/write/vectors/ui-recipes.js.map +1 -1
  259. package/dist/write/vectors/ui.d.ts +89 -0
  260. package/dist/write/vectors/ui.js +504 -49
  261. package/dist/write/vectors/ui.js.map +1 -1
  262. package/dist/write/verify/delta.d.ts +21 -0
  263. package/dist/write/verify/delta.js.map +1 -1
  264. package/dist/write/verify/poller.d.ts +13 -0
  265. package/dist/write/verify/poller.js +38 -0
  266. package/dist/write/verify/poller.js.map +1 -1
  267. package/package.json +9 -1
  268. package/schema/envelope.schema.json +5 -3
  269. package/scripts/build-helpers.sh +109 -0
  270. package/scripts/deputy-cert-setup.sh +60 -0
  271. package/skills/things-cli/SKILL.md +12 -5
  272. package/skills/things-cli/references/contracts.md +1 -0
  273. package/skills/things-cli/references/data-model.md +3 -0
  274. package/skills/things-cli/references/errors.md +5 -1
  275. package/skills/things-cli/references/gui.md +1 -0
  276. package/skills/things-cli/references/repeating.md +75 -0
  277. package/skills/things-cli/references/tag-cleanup.md +64 -0
@@ -0,0 +1,416 @@
1
+ /**
2
+ * Prompt-free capability detection (docs/design/permissions-doctrine.md,
3
+ * Articles I–III).
4
+ *
5
+ * Every entry point must know what it is allowed to do BEFORE it touches the
6
+ * live library, and it must find out without putting a macOS consent dialog on
7
+ * screen. This module is the single place that answers that question, so the
8
+ * read gate, the write gate, `things doctor`, and the `things setup` ceremony
9
+ * all quote the same verdict.
10
+ *
11
+ * Read capability is GROUND TRUTH per invocation, never a stored flag. Nothing
12
+ * here is memoized across calls and nothing is persisted except the one marker
13
+ * that records a live-instance fact (./session-grant.ts). The whole preflight
14
+ * is a single `open(2)` in the common case.
15
+ *
16
+ * The paths, in the order they are consulted:
17
+ *
18
+ * 1. an explicit database path — outside the doctrine entirely (Article VI);
19
+ * 2. the helpers — when the reader is serving this process's reads, the READ
20
+ * ITSELF is the check. The reader resolves its security-scoped bookmark on
21
+ * every verb and answers `not-granted` / `not-found` as typed errors, so
22
+ * there is nothing to pre-probe and no hello round-trip is added here. When
23
+ * the helpers are expected but cannot serve, the read FAILS LOUDLY — a
24
+ * silent fall-through to direct would move consent back onto the terminal,
25
+ * which is the exact churn the helpers exist to end;
26
+ * 3. Full Disk Access — one read-open of the user TCC database. FDA-class
27
+ * files never raise a dialog: the open either works or fails with a silent
28
+ * EPERM;
29
+ * 4. a witnessed session app-data grant — see ./session-grant.ts;
30
+ * 5. otherwise: refuse, naming both setup ceremonies.
31
+ *
32
+ * Step 2 asks nothing of this host's grants. Since helpers 1.3.0 the reader's
33
+ * socket and token live in `<state>/reader` — launchd owns the socket, install
34
+ * mints the token, and both are ordinary files this user owns — so "is the
35
+ * reader serving?" is answerable identically from every host app, with no
36
+ * consent class in play. (Before 1.3.0 they sat in the reader's App Sandbox
37
+ * container and that question was itself a cross-app container access.)
38
+ *
39
+ * The Things group container is NEVER opened as a probe (Article I corollary):
40
+ * the open is itself what raises the app-data consent, so "try it and see" is
41
+ * forbidden — except inside `things setup`, which provokes it deliberately.
42
+ *
43
+ * On the direct WRITE probe — why TCC introspection and not the AppleEvents
44
+ * API. The doctrine names `AEDeterminePermissionToAutomateTarget(askUserIfNeeded:
45
+ * false)` as the direct-path Automation probe, which is exactly what the deputy
46
+ * calls (deputy/src/tcc.swift). From a JXA/ObjC host that call is NOT reachable:
47
+ * JavaScriptCore's bridge cannot marshal an `AEDesc` struct. MEASURED 2026-08-24
48
+ * on macOS 24.6 — `AECreateDesc` fills an untyped `Ref()`, but passing that Ref
49
+ * to any function taking `^{AEDesc=…}` throws "Ref has incompatible type", and
50
+ * the one spelling the bridge accepts (`ref[0]`, a dereferenced copy) arrives
51
+ * zeroed: `AEGetDescDataSize` reports 0 bytes for a 36-byte bundle id, and every
52
+ * target then answers -50 paramErr. A probe that returns the same wrong answer
53
+ * for every input is worse than no probe, so it is not shipped. Reading the
54
+ * Automation row out of TCC.db is the other introspection the Article I
55
+ * corollary names, it is exactly as prompt-free, and it costs nothing extra:
56
+ * direct mode's floor is FDA anyway, so a process that can act at all can
57
+ * already read that file. Where the row cannot be read the verdict is an honest
58
+ * "unknown", which the write gate refuses on rather than resolving with a dialog.
59
+ */
60
+ import { DatabaseSync } from "node:sqlite";
61
+ import { loadConfig } from "./config.js";
62
+ import { deputyRouting, deputyRoutesDb, helpersExpected } from "./deputy/routing.js";
63
+ import { fdaGranted, hostApp, hostDisplayName, resetHostAccessForTests, tccDbPath, } from "./host-access.js";
64
+ import { sessionGrantValid } from "./session-grant.js";
65
+ export { fdaGranted, hostApp, hostDisplayName, tccDbPath, } from "./host-access.js";
66
+ /** The Things application's bundle identifier — the Automation grant's target. */
67
+ export const THINGS_BUNDLE_ID = "com.culturedcode.ThingsMac";
68
+ /** True when this verdict permits opening the live container. */
69
+ export function readAllowed(capability) {
70
+ return capability.mode !== "none" && capability.mode !== "helpers-unavailable";
71
+ }
72
+ /** True when this verdict permits driving the Things window. */
73
+ export function uiAllowed(capability) {
74
+ return capability.mode === "helpers" || capability.mode === "direct-escape";
75
+ }
76
+ /** True when app automation may be dispatched. */
77
+ export function writeAllowed(capability) {
78
+ return capability.mode === "deputy" || capability.mode === "direct-granted";
79
+ }
80
+ // ── The prompt-free probes ───────────────────────────────────────────────────
81
+ /**
82
+ * One Automation row from TCC.db: the `auth_value` macOS records for `client`
83
+ * driving `target`, or null when there is no row (never asked) or the file is
84
+ * unreadable (no FDA). Read-only, prompt-free, and defensive — the schema is
85
+ * Apple's private business, so ANY failure degrades to null rather than
86
+ * throwing into a caller that asked a yes/no question.
87
+ */
88
+ function automationAuthValueDefault(env, client, target) {
89
+ let db;
90
+ try {
91
+ db = new DatabaseSync(tccDbPath(env), { readOnly: true });
92
+ const row = db
93
+ .prepare("SELECT auth_value FROM access WHERE service = 'kTCCServiceAppleEvents' " +
94
+ "AND client = ? AND indirect_object_identifier = ? LIMIT 1")
95
+ .get(client, target);
96
+ const value = row?.auth_value;
97
+ return typeof value === "number" ? value : null;
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ finally {
103
+ try {
104
+ db?.close();
105
+ }
106
+ catch {
107
+ // A close failure cannot change the answer we already have.
108
+ }
109
+ }
110
+ }
111
+ // ── Read capability (Article I + II) ─────────────────────────────────────────
112
+ /**
113
+ * Errnos that mean a plain "no" rather than something worth telling the user
114
+ * about. EPERM is the FDA denial proper; EACCES is its sandboxed cousin; ENOENT
115
+ * simply means this account has no TCC database yet. Anything else is an
116
+ * anomaly and gets named, so a genuinely odd failure is never silently folded
117
+ * into "you lack permission".
118
+ */
119
+ const ORDINARY_DENIALS = new Set(["EPERM", "EACCES", "ENOENT"]);
120
+ /** The ways a machine can earn read capability, phrased for a human. */
121
+ function readRemediation(hostName) {
122
+ return [
123
+ "run `things helpers setup` — reads then flow through a helper that holds its own durable grant",
124
+ `or grant Full Disk Access to ${hostName} in System Settings ▸ Privacy & Security ▸ Full Disk Access`,
125
+ "or run `things setup`, which walks through both",
126
+ ];
127
+ }
128
+ /**
129
+ * Can this process read the live Things library, and on whose authority?
130
+ *
131
+ * Stateless: every call re-derives the verdict, because a grant can appear or
132
+ * vanish between one invocation and the next and a cached "yes" would be a
133
+ * stored onboarding flag by another name. The common case costs one `open(2)`.
134
+ */
135
+ export function readCapability(options = {}, deps = {}) {
136
+ const env = deps.env ?? process.env;
137
+ const host = hostApp(deps);
138
+ // Article VI — a path the caller already owns gets plain file semantics.
139
+ if (options.dbPath !== undefined || (env["THINGS_DB"] ?? "") !== "") {
140
+ return {
141
+ mode: "explicit-db",
142
+ detail: "an explicit database path was supplied — plain file semantics apply",
143
+ remediation: [],
144
+ host,
145
+ };
146
+ }
147
+ // The helpers path. Reads that ride the reader are not pre-checked here: the
148
+ // reader resolves its bookmark on every verb, so the read IS the check and a
149
+ // probe would only add latency and a second answer to disagree with.
150
+ const serving = (deps.helpersServing ?? (() => deputyRoutesDb(options, env)))();
151
+ if (serving) {
152
+ return {
153
+ mode: "helpers",
154
+ detail: "database reads are served by the sandboxed reader",
155
+ remediation: [],
156
+ host,
157
+ };
158
+ }
159
+ // No silent fall-through: a machine that asked for the helpers and cannot
160
+ // have them is refused, not quietly downgraded onto the terminal's own grants.
161
+ // Nothing gates this question any more — the rendezvous is ours, so whether
162
+ // the helpers are expected and whether they are serving are both answerable
163
+ // from every host app alike.
164
+ const expected = (deps.helpersExpected ?? (() => helpersExpected(env)))();
165
+ if (expected) {
166
+ const why = (deps.helpersReason ?? (() => null))();
167
+ return {
168
+ mode: "helpers-unavailable",
169
+ detail: `the helpers are enabled on this machine but are not serving reads${why !== null ? ` (${why})` : ""}`,
170
+ remediation: [
171
+ "run `things helpers setup` to finish onboarding them",
172
+ "or `things helpers status` to see which half is unhealthy",
173
+ "or `things --no-helpers …` to run this one invocation directly",
174
+ ],
175
+ host,
176
+ };
177
+ }
178
+ const fda = fdaGranted(deps);
179
+ if (fda.granted) {
180
+ return {
181
+ mode: "direct-fda",
182
+ detail: `Full Disk Access is held by ${hostDisplayName(deps)}${host.bundleId !== null ? ` (${host.bundleId})` : ""}`,
183
+ remediation: [],
184
+ host,
185
+ };
186
+ }
187
+ // The sub-FDA tier: a grant a ceremony witnessed, still live for this very
188
+ // app instance. Only consulted when FDA has already said no.
189
+ const session = sessionGrantValid(host.bundleId, deps);
190
+ if (session.valid) {
191
+ return {
192
+ mode: "session-grant",
193
+ detail: `${hostDisplayName(deps)} was granted access to the Things data folder for as long as it stays open`,
194
+ remediation: [],
195
+ host,
196
+ };
197
+ }
198
+ return {
199
+ mode: "none",
200
+ detail: `${hostDisplayName(deps)} cannot open the Things data folder — ${session.reason}` +
201
+ (fda.code !== null && !ORDINARY_DENIALS.has(fda.code)
202
+ ? ` (the access check ended in ${fda.code})`
203
+ : ""),
204
+ remediation: readRemediation(hostDisplayName(deps)),
205
+ host,
206
+ };
207
+ }
208
+ /** Thrown when a read is refused for want of capability (Article II). */
209
+ export class ReadCapabilityError extends Error {
210
+ remediation;
211
+ capability;
212
+ constructor(capability) {
213
+ super(`the Things database cannot be read: ${capability.detail}. ` +
214
+ `${capability.remediation.join("; ")}.`);
215
+ this.name = "ReadCapabilityError";
216
+ this.remediation = capability.remediation;
217
+ this.capability = capability;
218
+ }
219
+ }
220
+ // ── Write capability (Article I + II) ────────────────────────────────────────
221
+ /**
222
+ * macOS `auth_value` for an Automation record. 0 is a refusal; 2 (allowed) and
223
+ * 3 both mean the event will be delivered.
224
+ */
225
+ function classifyAuthValue(value) {
226
+ if (value === null)
227
+ return "unknown";
228
+ if (value === 0)
229
+ return "denied";
230
+ if (value >= 2)
231
+ return "granted";
232
+ return "unknown";
233
+ }
234
+ /**
235
+ * May this process drive Things over Apple Events, and on whose authority?
236
+ *
237
+ * The deputy wins when it is onboarded (its own handshake reports the grant it
238
+ * holds). Otherwise the host app's own Automation record is read out of TCC —
239
+ * granted, denied, or, when there is no record at all, `direct-unknown`. That
240
+ * last state is deliberately NOT resolved here: resolving it means sending a
241
+ * real Apple Event, which is what raises the dialog, and Article I reserves
242
+ * that for `things setup`.
243
+ */
244
+ export function writeCapability(deps = {}) {
245
+ const env = deps.env ?? process.env;
246
+ const host = hostApp(deps);
247
+ const deputyThings = (deps.deputyAutomation ?? (() => deputyRouting(env).hello?.automation?.things))();
248
+ if (deputyThings === "granted") {
249
+ return {
250
+ mode: "deputy",
251
+ detail: "the deputy is onboarded and holds app control for Things",
252
+ remediation: [],
253
+ host,
254
+ };
255
+ }
256
+ if (host.bundleId === null) {
257
+ return {
258
+ mode: "direct-unknown",
259
+ detail: "this process does not descend from an application bundle, so macOS has no identity " +
260
+ "to record app control against",
261
+ remediation: [
262
+ "run `things helpers setup` — app control then attaches to a helper that always has an identity",
263
+ ],
264
+ host,
265
+ };
266
+ }
267
+ const authValue = (deps.automationAuthValue ??
268
+ ((client, target) => automationAuthValueDefault(env, client, target)))(host.bundleId, THINGS_BUNDLE_ID);
269
+ const hostName = hostDisplayName(deps);
270
+ switch (classifyAuthValue(authValue)) {
271
+ case "granted":
272
+ return {
273
+ mode: "direct-granted",
274
+ detail: `${hostName} (${host.bundleId}) holds app control for Things`,
275
+ remediation: [],
276
+ host,
277
+ };
278
+ case "denied":
279
+ return {
280
+ mode: "direct-denied",
281
+ detail: `macOS records a refusal of app control for ${hostName} (${host.bundleId}) — it will not ask again`,
282
+ remediation: [
283
+ `turn on Things3 for ${hostName} under System Settings ▸ Privacy & Security ▸ Automation`,
284
+ `or re-arm the request with \`tccutil reset AppleEvents ${host.bundleId}\`, then run \`things setup\``,
285
+ ],
286
+ host,
287
+ };
288
+ default:
289
+ return {
290
+ mode: "direct-unknown",
291
+ detail: `macOS has no app-control record for ${hostName} (${host.bundleId}) yet`,
292
+ remediation: [
293
+ "run `things setup` — it asks for app control once, while you are at the machine",
294
+ "or run `things helpers setup` to attach the grant to a helper instead",
295
+ ],
296
+ host,
297
+ };
298
+ }
299
+ }
300
+ /** Thrown when app automation is refused for want of capability (Article II). */
301
+ export class WriteCapabilityError extends Error {
302
+ remediation;
303
+ capability;
304
+ constructor(capability) {
305
+ super(`Things cannot be driven: ${capability.detail}. ${capability.remediation.join("; ")}.`);
306
+ this.name = "WriteCapabilityError";
307
+ this.remediation = capability.remediation;
308
+ this.capability = capability;
309
+ }
310
+ }
311
+ // ── UI capability (Article IV) ───────────────────────────────────────────────
312
+ /**
313
+ * The LAB's escape hatch, and deliberately not consumer surface. The VM lab and
314
+ * the guest e2e bundle drive the UI vector DIRECT — the in-guest Accessibility
315
+ * grant is held by the runner's own processes (the AXVM1 layer), there is no
316
+ * helper bundle in a disposable clone, and there is nobody to answer a dialog
317
+ * either. Setting this to `1` restores direct UI-vector availability for that
318
+ * one situation. It is documented in docs/lab/harness.md and exported by the
319
+ * lab's guest environment; nothing consumer-facing mentions it, and it does not
320
+ * bypass `ui.enabled` — a lab clone still sets that key explicitly.
321
+ */
322
+ export const UI_DIRECT_ESCAPE_ENV = "THINGS_API_UI_DIRECT";
323
+ /** The command that gathers the GUI-driving tier, named in every UI refusal. */
324
+ const GUI_SETUP_COMMAND = "run `things helpers setup --gui` to grant GUI-driving to the helpers";
325
+ function deputyGuiStandingDefault(env) {
326
+ const hello = deputyRouting(env).hello;
327
+ if (hello === undefined || hello === null)
328
+ return null;
329
+ return { axTrusted: hello.axTrusted, systemEvents: hello.automation?.systemEvents };
330
+ }
331
+ /**
332
+ * May this process drive the Things WINDOW, and on whose authority?
333
+ *
334
+ * Article IV admits exactly one provenance: the helper pair. Accessibility on a
335
+ * general-purpose host app (a terminal, an agent harness, an MCP host) has a
336
+ * blast radius far beyond Things, churns with every host update, and has no
337
+ * sane story at all over ssh — so direct AX is unsupported, and a refusal here
338
+ * names the config knob and `things helpers setup --gui` rather than raising an
339
+ * Accessibility prompt against whatever happens to be running us.
340
+ *
341
+ * Every answer is prompt-free: the config key is a file read, and the deputy's
342
+ * `hello` carries `AXIsProcessTrusted()` plus its own `AEDeterminePermission`
343
+ * verdict for System Events.
344
+ */
345
+ export function uiCapability(deps = {}) {
346
+ const env = deps.env ?? process.env;
347
+ const host = hostApp(deps);
348
+ const enabled = (deps.uiEnabled ?? (() => loadConfig(env).ui.enabled))();
349
+ if (!enabled) {
350
+ return {
351
+ mode: "config-disabled",
352
+ detail: "GUI-driving is switched off on this machine (`ui-enabled` is false)",
353
+ remediation: [
354
+ "run `things config set ui-enabled true` to opt in",
355
+ `then ${GUI_SETUP_COMMAND}`,
356
+ ],
357
+ host,
358
+ };
359
+ }
360
+ if ((env[UI_DIRECT_ESCAPE_ENV] ?? "") === "1") {
361
+ return {
362
+ mode: "direct-escape",
363
+ detail: `${UI_DIRECT_ESCAPE_ENV}=1 — GUI-driving runs directly under this process (lab escape)`,
364
+ remediation: [],
365
+ host,
366
+ };
367
+ }
368
+ const standing = (deps.deputyGuiStanding ?? (() => deputyGuiStandingDefault(env)))();
369
+ if (standing === null) {
370
+ return {
371
+ mode: "helpers-missing",
372
+ detail: "GUI-driving is granted only to the helpers, and no helper is answering on this machine",
373
+ remediation: [GUI_SETUP_COMMAND, "or `things helpers status` to see which half is unhealthy"],
374
+ host,
375
+ };
376
+ }
377
+ const missing = [];
378
+ if (standing.axTrusted !== true) {
379
+ missing.push(standing.axTrusted === undefined
380
+ ? "Accessibility (these helpers predate the permission handshake — rebuild)"
381
+ : "Accessibility");
382
+ }
383
+ if (standing.systemEvents !== "granted") {
384
+ missing.push(`automation → System Events (${standing.systemEvents ?? "unknown"})`);
385
+ }
386
+ if (missing.length === 0) {
387
+ return {
388
+ mode: "helpers",
389
+ detail: "the helpers hold Accessibility and app control for System Events",
390
+ remediation: [],
391
+ host,
392
+ };
393
+ }
394
+ return {
395
+ mode: "tier-incomplete",
396
+ detail: `the helpers are onboarded but the GUI-driving tier is incomplete — missing ${missing.join("; ")}`,
397
+ remediation: [GUI_SETUP_COMMAND],
398
+ host,
399
+ };
400
+ }
401
+ /** Thrown when GUI-driving is refused for want of capability (Article IV). */
402
+ export class UiCapabilityError extends Error {
403
+ remediation;
404
+ capability;
405
+ constructor(capability) {
406
+ super(`the Things window cannot be driven: ${capability.detail}. ${capability.remediation.join("; ")}.`);
407
+ this.name = "UiCapabilityError";
408
+ this.remediation = capability.remediation;
409
+ this.capability = capability;
410
+ }
411
+ }
412
+ /** Test seam: forget the one memo this module keeps (the host's display name). */
413
+ export function resetCapabilityForTests() {
414
+ resetHostAccessForTests();
415
+ }
416
+ //# sourceMappingURL=capability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capability.js","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,121 @@
1
1
  import { describeEnvironmentChanges, diagnose, errorEnvelope, okEnvelope, } from "../../index.js";
2
2
  // Back-compat alias for pre-seam consumers of the CLI module.
3
3
  export const runDoctor = diagnose;
4
+ /**
5
+ * What this process may do and who holds the grant that lets it
6
+ * (docs/design/permissions-doctrine.md, Article II). Two lines, always first:
7
+ * every other row below is downstream of these two answers.
8
+ */
9
+ function capabilityLines(capability) {
10
+ const { read, write } = capability;
11
+ const host = read.host.bundleId !== null ? `${read.host.name} (${read.host.bundleId})` : read.host.name;
12
+ const lines = [
13
+ `host app: ${host}`,
14
+ `read access: ${read.mode} — ${read.detail}`,
15
+ `app control: ${write.mode} — ${write.detail}`,
16
+ ];
17
+ for (const line of [...read.remediation, ...write.remediation]) {
18
+ lines.push(` next: ${line}`);
19
+ }
20
+ return lines;
21
+ }
22
+ /**
23
+ * The `── Permissions ──` section: one row per vector, and for each the STATE,
24
+ * the PROVENANCE (which identity actually holds the grant — the question the
25
+ * three summary rows above cannot answer), and the remediation when it is
26
+ * missing (docs/design/permissions-doctrine.md, Article II).
27
+ *
28
+ * Every value here comes from the prompt-free verdict `diagnose` already took.
29
+ * Doctor was an Article I violation once — it opened the container to find out
30
+ * whether it could — and this section must never regress that: it renders, it
31
+ * does not probe.
32
+ */
33
+ function permissionLines(report) {
34
+ const { read, write, ui } = report.capability;
35
+ const hostLabel = read.host.bundleId !== null ? `${read.host.name} (${read.host.bundleId})` : read.host.name;
36
+ const readProvenance = () => {
37
+ switch (read.mode) {
38
+ case "helpers":
39
+ return "reader bookmark (things-reader)";
40
+ case "direct-fda":
41
+ return `host Full Disk Access (${hostLabel})`;
42
+ case "session-grant":
43
+ return `session marker (${hostLabel}, until it quits)`;
44
+ case "explicit-db":
45
+ return "none needed (an explicit --db path)";
46
+ default:
47
+ return "none";
48
+ }
49
+ };
50
+ const writeProvenance = () => {
51
+ switch (write.mode) {
52
+ case "deputy":
53
+ return "deputy TCC Automation (Things API Helper)";
54
+ case "direct-granted":
55
+ return `host TCC Automation (${hostLabel})`;
56
+ default:
57
+ return "none";
58
+ }
59
+ };
60
+ const uiProvenance = () => {
61
+ switch (ui.mode) {
62
+ case "helpers":
63
+ return "deputy TCC Accessibility + System Events (Things API Helper)";
64
+ case "direct-escape":
65
+ return "the lab's direct escape (not consumer surface)";
66
+ default:
67
+ return "none — helpers only (Article IV)";
68
+ }
69
+ };
70
+ const rows = [
71
+ {
72
+ vector: "read",
73
+ state: read.mode,
74
+ provenance: readProvenance(),
75
+ next: read.remediation,
76
+ },
77
+ {
78
+ vector: "applescript",
79
+ state: write.mode,
80
+ provenance: writeProvenance(),
81
+ next: write.remediation,
82
+ },
83
+ {
84
+ vector: "url-scheme",
85
+ state: report.availability.urlScheme.enabled === true
86
+ ? "enabled"
87
+ : report.availability.urlScheme.enabled === false
88
+ ? "disabled"
89
+ : "unknown",
90
+ // No TCC class at all: `open -g things:///…` is a LaunchServices dispatch.
91
+ provenance: "none needed — the app's own 'Enable Things URLs' setting",
92
+ next: report.availability.urlScheme.enabled === true
93
+ ? []
94
+ : ["turn on Things ▸ Settings ▸ General ▸ Enable Things URLs"],
95
+ },
96
+ {
97
+ vector: "shortcuts",
98
+ state: report.availability.shortcuts.missing.length === 0
99
+ ? "installed"
100
+ : `${report.availability.shortcuts.present.length}/${report.availability.shortcuts.present.length +
101
+ report.availability.shortcuts.missing.length} installed`,
102
+ provenance: "none needed — each shortcut's own Always Allow",
103
+ next: report.availability.shortcuts.missing.length === 0
104
+ ? []
105
+ : ["run `things setup` to install the missing proxy shortcuts"],
106
+ },
107
+ { vector: "ui", state: ui.mode, provenance: uiProvenance(), next: ui.remediation },
108
+ ];
109
+ const vectorWidth = Math.max(...rows.map((r) => r.vector.length));
110
+ const stateWidth = Math.max(...rows.map((r) => r.state.length));
111
+ const lines = ["── Permissions (per vector) ──", `host app: ${hostLabel}`];
112
+ for (const row of rows) {
113
+ lines.push(` ${row.vector.padEnd(vectorWidth)} ${row.state.padEnd(stateWidth)} ${row.provenance}`);
114
+ for (const next of row.next)
115
+ lines.push(`${" ".repeat(vectorWidth + stateWidth + 6)}next: ${next}`);
116
+ }
117
+ return lines;
118
+ }
4
119
  function environmentLine(env) {
5
120
  if (env.lastVerifiedWrite === null) {
6
121
  return "no verified write recorded yet (the tuple is recorded on the first successful write)";
@@ -21,6 +136,66 @@ function syncHealthLines(sh) {
21
136
  `cloud: ${sh.cloud.verdict}`,
22
137
  ];
23
138
  }
139
+ /** One half's line: liveness, launchd registration, version, signing. */
140
+ function helperHalfLine(half, running) {
141
+ const facts = [
142
+ half.plistInstalled
143
+ ? half.loaded
144
+ ? "launchd loaded"
145
+ : "launchd registered, NOT loaded"
146
+ : "no launchd registration",
147
+ ];
148
+ if (half.hello !== null) {
149
+ facts.push(`v${half.hello.deputyVersion}, protocol ${half.hello.protocol}, pid ${half.hello.pid}`);
150
+ // Only the deputy carries TCC standing, and only from helpers v1.2.0 on —
151
+ // an older one simply has no row here rather than a guessed one.
152
+ if (half.hello.automation !== undefined) {
153
+ facts.push(`automation: Things ${half.hello.automation.things}, System Events ${half.hello.automation.systemEvents}`);
154
+ }
155
+ if (half.hello.axTrusted !== undefined) {
156
+ facts.push(`accessibility: ${half.hello.axTrusted ? "granted" : "NOT granted"}`);
157
+ }
158
+ }
159
+ if (half.signing !== null) {
160
+ facts.push(half.signing.state === "signed"
161
+ ? `signed (${half.signing.authority ?? "unknown authority"})`
162
+ : `${half.signing.state} — macOS grants will NOT survive rebuilds`);
163
+ }
164
+ return `${running} · ${facts.join(" · ")}`;
165
+ }
166
+ /** The `── Helpers ──` section: routing resolution + both halves' health. */
167
+ function helpersLines(helpers) {
168
+ const { status, routing } = helpers;
169
+ const resolved = helpers.mode === "false"
170
+ ? "direct (routing off)"
171
+ : `automation ${routing.automation ? "via the deputy" : "DIRECT"}, database reads ${routing.files ? "via the reader" : "DIRECT"}`;
172
+ const lines = [
173
+ "── Helpers ──",
174
+ `mode: ${helpers.mode} — ${helpers.detail}`,
175
+ `routing: ${resolved}`,
176
+ `bundle: ${status.bundleInstalled
177
+ ? `installed${helpers.installedVersion !== null ? ` (v${helpers.installedVersion})` : ""}${helpers.versionSkew ? `, this package expects v${helpers.expectedVersion}` : ""}`
178
+ : "not installed"}`,
179
+ ];
180
+ if (status.bundleInstalled || status.deputy.plistInstalled) {
181
+ lines.push(`deputy: ${helperHalfLine(status.deputy, status.deputy.running
182
+ ? "running"
183
+ : status.deputy.hungSocket
184
+ ? "SOCKET PRESENT, NOT ANSWERING"
185
+ : "not running")}`, `reader: ${status.reader.installed
186
+ ? helperHalfLine(status.reader, status.reader.running
187
+ ? status.reader.granted
188
+ ? "running, granted"
189
+ : "running, NOT granted"
190
+ : status.reader.hungSocket
191
+ ? "SOCKET PRESENT, NOT ANSWERING"
192
+ : "not running")
193
+ : "not installed (the bundle was built without an Apple-issued signing identity)"}`);
194
+ }
195
+ if (helpers.remedy !== null)
196
+ lines.push(`next: ${helpers.remedy}`);
197
+ return lines;
198
+ }
24
199
  /** The `── ui vector ──` section: config + app + Accessibility + certification. */
25
200
  function uiVectorLines(ui) {
26
201
  const total = ui.certification.length;
@@ -42,9 +217,10 @@ export function registerDoctor(program) {
42
217
  .description("Check environment health: database location, database schema compatibility, app " +
43
218
  "presence, any one-time setup still needed (macOS permissions, the app's " +
44
219
  "'Enable Things URLs' setting), whether the environment changed since the last " +
45
- "successful write, and a sync-health summary (whether the app is running, how recently " +
220
+ "successful write, a sync-health summary (whether the app is running, how recently " +
46
221
  "the data changed, and — when a Things Cloud account is attached — the last sync " +
47
- "attempt) with steps to fix. " +
222
+ "attempt), and the state of the optional helpers (installed, running, granted, signed, " +
223
+ "and what routing resolved to) — with steps to fix. " +
48
224
  "Exit 0 healthy; 5 schema drift (writes disabled); 7 environment problem.")
49
225
  .option("--json", "emit versioned JSON envelope on stdout")
50
226
  .option("--db <path>", "explicit database path (overrides THINGS_DB and discovery)")
@@ -67,6 +243,7 @@ export function registerDoctor(program) {
67
243
  }
68
244
  else if (report) {
69
245
  const lines = [
246
+ ...capabilityLines(report.capability),
70
247
  `db: ${report.db.path} (${report.db.source})`,
71
248
  `db version: ${report.db.databaseVersion ?? "unknown"}`,
72
249
  `fingerprint: ${report.fingerprint.status} (${report.fingerprint.value.slice(0, 23)}…)`,
@@ -102,6 +279,8 @@ export function registerDoctor(program) {
102
279
  ]
103
280
  : []),
104
281
  ...syncHealthLines(report.syncHealth),
282
+ ...permissionLines(report),
283
+ ...helpersLines(report.helpers),
105
284
  ...uiVectorLines(report.ui),
106
285
  ];
107
286
  process.stdout.write(`${lines.join("\n")}\n`);