@lobu/cli 7.0.0 → 7.2.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 (137) hide show
  1. package/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
  2. package/dist/commands/_lib/apply/apply-cmd.js +160 -12
  3. package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
  4. package/dist/commands/_lib/apply/client.d.ts +106 -0
  5. package/dist/commands/_lib/apply/client.d.ts.map +1 -1
  6. package/dist/commands/_lib/apply/client.js +163 -2
  7. package/dist/commands/_lib/apply/client.js.map +1 -1
  8. package/dist/commands/_lib/apply/desired-state.d.ts +53 -0
  9. package/dist/commands/_lib/apply/desired-state.d.ts.map +1 -1
  10. package/dist/commands/_lib/apply/desired-state.js +182 -5
  11. package/dist/commands/_lib/apply/desired-state.js.map +1 -1
  12. package/dist/commands/_lib/apply/diff.d.ts +12 -1
  13. package/dist/commands/_lib/apply/diff.d.ts.map +1 -1
  14. package/dist/commands/_lib/apply/diff.js +106 -7
  15. package/dist/commands/_lib/apply/diff.js.map +1 -1
  16. package/dist/commands/_lib/connector-loader.d.ts +3 -0
  17. package/dist/commands/_lib/connector-loader.d.ts.map +1 -0
  18. package/dist/commands/_lib/connector-loader.js +129 -0
  19. package/dist/commands/_lib/connector-loader.js.map +1 -0
  20. package/dist/commands/_lib/connector-run-cmd.d.ts +35 -0
  21. package/dist/commands/_lib/connector-run-cmd.d.ts.map +1 -0
  22. package/dist/commands/_lib/connector-run-cmd.js +351 -0
  23. package/dist/commands/_lib/connector-run-cmd.js.map +1 -0
  24. package/dist/commands/_lib/export/export-cmd.d.ts +35 -0
  25. package/dist/commands/_lib/export/export-cmd.d.ts.map +1 -0
  26. package/dist/commands/_lib/export/export-cmd.js +329 -0
  27. package/dist/commands/_lib/export/export-cmd.js.map +1 -0
  28. package/dist/commands/agent.d.ts.map +1 -1
  29. package/dist/commands/agent.js +11 -14
  30. package/dist/commands/agent.js.map +1 -1
  31. package/dist/commands/chat.d.ts.map +1 -1
  32. package/dist/commands/chat.js +19 -5
  33. package/dist/commands/chat.js.map +1 -1
  34. package/dist/commands/connector.d.ts +3 -0
  35. package/dist/commands/connector.d.ts.map +1 -0
  36. package/dist/commands/connector.js +5 -0
  37. package/dist/commands/connector.js.map +1 -0
  38. package/dist/commands/context.d.ts +7 -0
  39. package/dist/commands/context.d.ts.map +1 -1
  40. package/dist/commands/context.js +19 -2
  41. package/dist/commands/context.js.map +1 -1
  42. package/dist/commands/dev.d.ts +15 -0
  43. package/dist/commands/dev.d.ts.map +1 -1
  44. package/dist/commands/dev.js +156 -4
  45. package/dist/commands/dev.js.map +1 -1
  46. package/dist/commands/doctor.d.ts.map +1 -1
  47. package/dist/commands/doctor.js +2 -3
  48. package/dist/commands/doctor.js.map +1 -1
  49. package/dist/commands/eval.d.ts.map +1 -1
  50. package/dist/commands/eval.js +12 -13
  51. package/dist/commands/eval.js.map +1 -1
  52. package/dist/commands/init.d.ts.map +1 -1
  53. package/dist/commands/init.js +5 -1
  54. package/dist/commands/init.js.map +1 -1
  55. package/dist/commands/login.d.ts.map +1 -1
  56. package/dist/commands/login.js +22 -16
  57. package/dist/commands/login.js.map +1 -1
  58. package/dist/commands/memory/_lib/browser-auth-cmd.d.ts.map +1 -1
  59. package/dist/commands/memory/_lib/browser-auth-cmd.js +15 -144
  60. package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
  61. package/dist/commands/token.d.ts.map +1 -1
  62. package/dist/commands/token.js +1 -4
  63. package/dist/commands/token.js.map +1 -1
  64. package/dist/commands/validate.d.ts.map +1 -1
  65. package/dist/commands/validate.js +4 -13
  66. package/dist/commands/validate.js.map +1 -1
  67. package/dist/config/loader.js +2 -2
  68. package/dist/config/loader.js.map +1 -1
  69. package/dist/connectors/README.md +0 -1
  70. package/dist/connectors/apple_photos.ts +178 -0
  71. package/dist/connectors/browser-scraper-utils.ts +76 -0
  72. package/dist/connectors/chrome.ts +351 -0
  73. package/dist/connectors/chrome_bookmarks.ts +79 -0
  74. package/dist/connectors/chrome_downloads.ts +80 -0
  75. package/dist/connectors/chrome_history.ts +80 -0
  76. package/dist/connectors/github.ts +1 -0
  77. package/dist/connectors/google_calendar.ts +14 -2
  78. package/dist/connectors/google_play.ts +22 -2
  79. package/dist/connectors/hackernews.ts +37 -2
  80. package/dist/connectors/index.ts +15 -1
  81. package/dist/connectors/reddit.ts +1 -0
  82. package/dist/connectors/revolut.ts +10 -13
  83. package/dist/connectors/rss.ts +33 -8
  84. package/dist/connectors/trustpilot.ts +31 -20
  85. package/dist/connectors/website.ts +7 -68
  86. package/dist/connectors/whatsapp.ts +12 -21
  87. package/dist/db/migrations/20260514130000_connection_action_modes.sql +103 -0
  88. package/dist/db/migrations/20260514160000_auth_profiles_mirror_mode.sql +32 -0
  89. package/dist/db/migrations/20260515120000_agents_per_org_pk.sql +66 -0
  90. package/dist/db/migrations/20260515150000_geo_enrichment.sql +208 -0
  91. package/dist/db/migrations/20260515160000_drop_agents_org_id_unique.sql +24 -0
  92. package/dist/db/migrations/20260515170000_auth_profiles_default_for_connector.sql +23 -0
  93. package/dist/db/migrations/20260516120000_agents_per_org_pk_swap.sql +125 -0
  94. package/dist/db/migrations/20260516200000_events_search_tsv.sql +134 -0
  95. package/dist/db/migrations/20260516200100_events_lifecycle_changes_index.sql +25 -0
  96. package/dist/db/migrations/20260517010000_drop_unused_indexes.sql +49 -0
  97. package/dist/db/migrations/20260517020000_softdelete_orphan_feeds.sql +56 -0
  98. package/dist/db/migrations/20260517030000_pat_worker_id_binding.sql +27 -0
  99. package/dist/db/migrations/20260517040000_archive_orphan_watchers.sql +30 -0
  100. package/dist/db/migrations/20260517050000_watcher_agent_id_not_null.sql +34 -0
  101. package/dist/db/migrations/20260517060000_watcher_schema_additions.sql +78 -0
  102. package/dist/db/migrations/20260517150000_goals_primitive.sql +55 -0
  103. package/dist/db/migrations/20260517160000_drop_goals_primitive.sql +45 -0
  104. package/dist/db/migrations/20260518000000_pending_interactions.sql +49 -0
  105. package/dist/db/migrations/20260518010000_runs_heartbeat_reaper_index.sql +22 -0
  106. package/dist/db/migrations/20260518020000_runs_heartbeat_inflight_narrow.sql +36 -0
  107. package/dist/db/migrations/20260518040000_agent_transcript_snapshot.sql +54 -0
  108. package/dist/db/migrations/20260518050000_runs_denormalize_agent_conversation.sql +36 -0
  109. package/dist/db/migrations/20260518060000_revert_runs_denormalize.sql +29 -0
  110. package/dist/db/migrations/20260518070000_runs_heartbeat_inflight_widen.sql +33 -0
  111. package/dist/eval/client.d.ts.map +1 -1
  112. package/dist/eval/client.js +11 -0
  113. package/dist/eval/client.js.map +1 -1
  114. package/dist/eval/grader.js +2 -1
  115. package/dist/eval/grader.js.map +1 -1
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +84 -1
  118. package/dist/index.js.map +1 -1
  119. package/dist/internal/context.d.ts +13 -1
  120. package/dist/internal/context.d.ts.map +1 -1
  121. package/dist/internal/context.js +83 -8
  122. package/dist/internal/context.js.map +1 -1
  123. package/dist/internal/credentials.d.ts +5 -0
  124. package/dist/internal/credentials.d.ts.map +1 -1
  125. package/dist/internal/credentials.js +75 -1
  126. package/dist/internal/credentials.js.map +1 -1
  127. package/dist/internal/index.d.ts +2 -2
  128. package/dist/internal/index.d.ts.map +1 -1
  129. package/dist/internal/index.js +2 -2
  130. package/dist/internal/index.js.map +1 -1
  131. package/dist/internal/local-env.d.ts.map +1 -1
  132. package/dist/internal/local-env.js +9 -2
  133. package/dist/internal/local-env.js.map +1 -1
  134. package/dist/server.bundle.mjs +7085 -2832
  135. package/dist/start-local.bundle.mjs +8269 -3656
  136. package/package.json +7 -5
  137. package/dist/connectors/google_photos.ts +0 -776
@@ -1 +1 @@
1
- {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAuB,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpE,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC;AAY3C;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,GAAW;IAC1C,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;IAE9C,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;YACL,KAAK,EAAE,MAAM,eAAe,aAAa,GAAG,EAAE;YAC9C,OAAO,EAAE,CAAC,gCAAgC,CAAC;SAC5C,CAAC;IACJ,CAAC;IAED,IAAI,MAA+B,CAAC;IACpC,IAAI,CAAC;QACH,MAAM,GAAG,SAAS,CAAC,GAAG,CAA4B,CAAC;IACrD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,KAAK,EAAE,0BAA0B,eAAe,EAAE;YAClD,OAAO,EAAE,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;SAC5D,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,gBAAgB,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAClD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CACrC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,CACvD,CAAC;QACF,OAAO,EAAE,KAAK,EAAE,WAAW,eAAe,EAAE,EAAE,OAAO,EAAE,CAAC;IAC1D,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,WAAW,CACzB,MAA8B;IAE9B,OAAO,OAAO,IAAI,MAAM,CAAC;AAC3B,CAAC"}
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAuB,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpE,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC;AAY3C;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,GAAW;IAC1C,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;IAE9C,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;YACL,KAAK,EAAE,MAAM,eAAe,aAAa,GAAG,EAAE;YAC9C,OAAO,EAAE,CAAC,gCAAgC,CAAC;SAC5C,CAAC;IACJ,CAAC;IAED,IAAI,MAA+B,CAAC;IACpC,IAAI,CAAC;QACH,MAAM,GAAG,SAAS,CAAC,GAAG,CAA4B,CAAC;IACrD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,KAAK,EAAE,0BAA0B,UAAU,EAAE;YAC7C,OAAO,EAAE,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;SAC5D,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,gBAAgB,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAClD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CACrC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,CACvD,CAAC;QACF,OAAO,EAAE,KAAK,EAAE,WAAW,UAAU,EAAE,EAAE,OAAO,EAAE,CAAC;IACrD,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,WAAW,CACzB,MAA8B;IAE9B,OAAO,OAAO,IAAI,MAAM,CAAC;AAC3B,CAAC"}
@@ -520,7 +520,6 @@ This means edits to `.ts` files in `connectors/` take effect on the next sync wi
520
520
  | `github` | oauth/env_keys | issues, PRs, comments, discussions | create/close/reopen issues, PRs |
521
521
  | `glassdoor` | none | reviews | - |
522
522
  | `gmaps` | env_keys | reviews | - |
523
- | `google_photos` | browser (CDP) | photos | - |
524
523
  | `google_play` | none | reviews | - |
525
524
  | `hackernews` | none | stories, comments | - |
526
525
  | `ios_appstore` | none | reviews | - |
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Apple Photos Connector (V1 runtime) — Lobu Mac app.
3
+ *
4
+ * Runs on a Mac advertising the `photos` capability. The bridge holds the
5
+ * `NSPhotoLibraryUsageDescription` Info.plist string and prompts the user via
6
+ * TCC the first time a job is claimed. Once granted, PhotoKit exposes the
7
+ * user's local Photos library — which is mirrored from iCloud Photos when
8
+ * that's enabled — including the rich metadata Google's Photos Library API
9
+ * does NOT expose: location (lat/lng), people (Apple's on-device face
10
+ * recognition), albums, captions, keywords, and Vision OCR text.
11
+ *
12
+ * One feed in v1:
13
+ *
14
+ * - `library`: every PHAsset in the user's library, with stable origin ids
15
+ * derived from the asset's localIdentifier. Re-runs upsert by origin id.
16
+ *
17
+ * v1 ingests metadata + remote references (asset_local_id, asset_cloud_id,
18
+ * source_url for the photos.apple.com deep link). The actual image bytes
19
+ * are NOT embedded in events; future connector actions (`fetch_thumbnail`,
20
+ * `fetch_original`) will let an agent pull bytes on demand via the Mac
21
+ * worker.
22
+ *
23
+ * The connector DEFINITION here is the source of truth for shape; EXECUTION
24
+ * lives in the Mac app's PhotosSyncService, which polls /api/workers/* with
25
+ * `photos: true` and streams events back through the standard worker
26
+ * protocol — same `runs` lifecycle as every other device-bound connector.
27
+ *
28
+ * The TS sync()/execute() are safety stubs: if a server-side worker somehow
29
+ * bypassed the capability gate (`required_capability='photos'`), the run
30
+ * throws immediately instead of silently producing zero events.
31
+ */
32
+
33
+ import {
34
+ type ActionResult,
35
+ type ConnectorDefinition,
36
+ ConnectorRuntime,
37
+ type SyncContext,
38
+ type SyncResult,
39
+ } from '@lobu/connector-sdk';
40
+
41
+ const BRIDGE_ONLY_MESSAGE =
42
+ 'apple.photos runs only on a worker advertising capability "photos" (Lobu Mac app with Photos permission). ' +
43
+ 'This run was claimed by a worker without that capability — check connector_definitions.required_capability and the poll-time capability filter.';
44
+
45
+ export default class ApplePhotosConnector extends ConnectorRuntime {
46
+ readonly definition: ConnectorDefinition = {
47
+ key: 'apple.photos',
48
+ name: 'Apple Photos',
49
+ description:
50
+ 'Sync your Photos library (local or iCloud-mirrored) from the Lobu Mac app. ' +
51
+ 'Includes location, people, albums, captions, keywords, and Vision OCR text — ' +
52
+ 'data Google Photos\' API does not expose.',
53
+ version: '0.1.0',
54
+ faviconDomain: 'apple.com',
55
+ requiredCapability: 'photos',
56
+ runtime: {
57
+ platforms: ['macos'],
58
+ scopes: ['date', 'location', 'people', 'albums', 'captions', 'keywords', 'ocr'],
59
+ },
60
+ authSchema: {
61
+ methods: [{ type: 'none' }],
62
+ },
63
+ feeds: {
64
+ library: {
65
+ key: 'library',
66
+ name: 'Library',
67
+ description:
68
+ 'Every photo in your library. Each event carries the photo\'s metadata ' +
69
+ '(date taken, location, people, albums, captions, OCR text) plus stable ' +
70
+ 'asset identifiers so agents can fetch the image bytes on demand.',
71
+ configSchema: {
72
+ type: 'object',
73
+ properties: {
74
+ backfill_days: {
75
+ type: 'integer',
76
+ minimum: 1,
77
+ maximum: 36500,
78
+ default: 3650,
79
+ description:
80
+ 'How many days back the bridge backfills on a fresh sync. Default 10 years; ' +
81
+ 'incremental runs only re-query the modification window since last_sync_at.',
82
+ },
83
+ include_screenshots: {
84
+ type: 'boolean',
85
+ default: true,
86
+ description: 'Include screenshots (PHAssetMediaSubtype.photoScreenshot).',
87
+ },
88
+ include_videos: {
89
+ type: 'boolean',
90
+ default: false,
91
+ description: 'Include video assets in addition to photos.',
92
+ },
93
+ },
94
+ },
95
+ eventKinds: {
96
+ photo: {
97
+ description:
98
+ 'A single photo (or video, if enabled) from the user\'s Apple Photos library. ' +
99
+ 'v1 (this PR) populates: asset_local_id, media_type, media_subtypes, ' +
100
+ 'date_taken, date_modified, width, height, duration_s, latitude/longitude/altitude_m, ' +
101
+ 'albums, is_favorite, is_hidden — everything PhotoKit\'s public API exposes. ' +
102
+ 'v2 will add: asset_cloud_id, place_name (reverse geocoding), people, ' +
103
+ 'keywords, caption, ocr_text — all of which require direct reads against ' +
104
+ 'the Photos.sqlite bundle (FDA + schema-pinned, osxphotos-style). ' +
105
+ 'Schema allows nulls so v1 events validate cleanly.',
106
+ metadataSchema: {
107
+ type: 'object',
108
+ required: ['source', 'origin_id', 'asset_local_id'],
109
+ properties: {
110
+ source: { type: 'string', const: 'apple_photos' },
111
+ origin_id: { type: 'string' },
112
+ asset_local_id: {
113
+ type: 'string',
114
+ description: 'PHAsset.localIdentifier — stable per-device handle.',
115
+ },
116
+ asset_cloud_id: {
117
+ type: ['string', 'null'],
118
+ description: 'iCloud asset id when synced via iCloud Photos.',
119
+ },
120
+ media_type: {
121
+ type: 'string',
122
+ enum: ['image', 'video', 'audio', 'unknown'],
123
+ },
124
+ media_subtypes: {
125
+ type: 'array',
126
+ items: { type: 'string' },
127
+ description:
128
+ 'PHAssetMediaSubtype flags: live, hdr, screenshot, panorama, portrait, etc.',
129
+ },
130
+ date_taken: { type: ['string', 'null'], format: 'date-time' },
131
+ date_modified: { type: ['string', 'null'], format: 'date-time' },
132
+ width: { type: ['integer', 'null'] },
133
+ height: { type: ['integer', 'null'] },
134
+ duration_s: {
135
+ type: ['number', 'null'],
136
+ description: 'Duration in seconds — videos and Live Photos only.',
137
+ },
138
+ latitude: { type: ['number', 'null'] },
139
+ longitude: { type: ['number', 'null'] },
140
+ altitude_m: { type: ['number', 'null'] },
141
+ place_name: {
142
+ type: ['string', 'null'],
143
+ description:
144
+ 'Reverse-geocoded human-readable place from CLGeocoder when available offline.',
145
+ },
146
+ people: {
147
+ type: 'array',
148
+ items: { type: 'string' },
149
+ description: 'Named-person tags from Apple\'s on-device face recognition.',
150
+ },
151
+ albums: {
152
+ type: 'array',
153
+ items: { type: 'string' },
154
+ description: 'User album names this asset belongs to.',
155
+ },
156
+ keywords: {
157
+ type: 'array',
158
+ items: { type: 'string' },
159
+ },
160
+ caption: { type: ['string', 'null'] },
161
+ is_favorite: { type: 'boolean' },
162
+ is_hidden: { type: 'boolean' },
163
+ },
164
+ },
165
+ },
166
+ },
167
+ },
168
+ },
169
+ };
170
+
171
+ async sync(_ctx: SyncContext): Promise<SyncResult> {
172
+ throw new Error(BRIDGE_ONLY_MESSAGE);
173
+ }
174
+
175
+ async execute(): Promise<ActionResult> {
176
+ throw new Error(BRIDGE_ONLY_MESSAGE);
177
+ }
178
+ }
@@ -82,6 +82,82 @@ export function validateCookieNotExpired(
82
82
  // URL validation
83
83
  // -----------------------------------------------------------------------------
84
84
 
85
+ /**
86
+ * Validates a URL is safe for server-side fetching.
87
+ * Blocks private/internal network addresses to prevent SSRF attacks.
88
+ *
89
+ * Returns silently when the URL is safe; throws with a descriptive message
90
+ * otherwise. Connectors that fetch URLs derived from remote/untrusted input
91
+ * (sitemaps, HN story links, RSS feeds configured by users, etc.) MUST call
92
+ * this at the trust boundary before issuing the request.
93
+ */
94
+ export function validatePublicUrl(url: string): void {
95
+ let parsed: URL;
96
+ try {
97
+ parsed = new URL(url);
98
+ } catch {
99
+ throw new Error(`Invalid URL: ${url}`);
100
+ }
101
+
102
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
103
+ throw new Error(`URL must use http: or https: protocol, got ${parsed.protocol}`);
104
+ }
105
+
106
+ const hostname = parsed.hostname.toLowerCase();
107
+
108
+ // Block localhost variants
109
+ if (hostname === 'localhost' || hostname === '[::1]' || hostname.endsWith('.localhost')) {
110
+ throw new Error(`URL must not point to localhost: ${hostname}`);
111
+ }
112
+
113
+ // IPv4 private/loopback/link-local/cloud-metadata/CGNAT ranges
114
+ const ipv4Match = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
115
+ if (ipv4Match) {
116
+ const [, a, b] = ipv4Match.map(Number);
117
+ if (
118
+ a === 127 || // loopback
119
+ a === 10 || // private
120
+ (a === 172 && b >= 16 && b <= 31) || // private
121
+ (a === 192 && b === 168) || // private
122
+ (a === 169 && b === 254) || // link-local incl. 169.254.169.254 cloud metadata
123
+ (a === 100 && b >= 64 && b <= 127) || // CGNAT 100.64.0.0/10
124
+ a === 0
125
+ ) {
126
+ throw new Error(`URL must not point to a private/internal IP address: ${hostname}`);
127
+ }
128
+ }
129
+
130
+ // IPv6 private ranges (bracketed notation)
131
+ if (hostname.startsWith('[')) {
132
+ const ipv6 = hostname.slice(1, -1).toLowerCase();
133
+ // Link-local fe80::/10 covers fe80:..fec0: (first byte 1111 1110 1x).
134
+ const linkLocalPrefix = /^fe[89ab][0-9a-f]?:/;
135
+ // Multicast ff00::/8 — any address starting with ff.
136
+ const multicastPrefix = /^ff[0-9a-f]{2}:/;
137
+ if (
138
+ ipv6 === '::1' ||
139
+ linkLocalPrefix.test(ipv6) ||
140
+ multicastPrefix.test(ipv6) ||
141
+ ipv6.startsWith('fc') || // unique local fc00::/7
142
+ ipv6.startsWith('fd') ||
143
+ ipv6 === '::' ||
144
+ ipv6.startsWith('::ffff:') // IPv4-mapped IPv6
145
+ ) {
146
+ throw new Error(`URL must not point to a private/internal IPv6 address: ${hostname}`);
147
+ }
148
+ }
149
+
150
+ // Common internal hostnames
151
+ if (
152
+ hostname.endsWith('.internal') ||
153
+ hostname.endsWith('.local') ||
154
+ hostname.endsWith('.corp') ||
155
+ hostname.endsWith('.lan')
156
+ ) {
157
+ throw new Error(`URL must not point to an internal hostname: ${hostname}`);
158
+ }
159
+ }
160
+
85
161
  /**
86
162
  * Validate that a URL is well-formed, uses HTTPS, and belongs to the expected
87
163
  * domain (hostname ends with `expectedDomain`).
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Chrome Connector — Owletto for Chrome only.
3
+ *
4
+ * One connector per paired Chrome profile. The cloud-side definition is
5
+ * pure metadata; all execution happens in the extension's service worker
6
+ * (apps/chrome/background.js: dispatchToolRun) against the user's signed-in
7
+ * Chrome via chrome.debugger + chrome.scripting + chrome.tabs.
8
+ *
9
+ * Surface:
10
+ *
11
+ * feeds.open_tabs
12
+ * Auto-wired snapshot feed. The extension emits one event per open tab
13
+ * each sync cycle. Cheap and read-only.
14
+ *
15
+ * actions.navigate
16
+ * Page.navigate the target tab (default: a fresh background tab; opt
17
+ * out via open_in_new_tab=false) to `url`.
18
+ *
19
+ * actions.get_accessibility_tree
20
+ * Inject the bundled accessibility-tree.js content script and return a
21
+ * structured snapshot of the visible interactive nodes, each with a
22
+ * stable {frame_id, document_epoch, ref_id} that subsequent
23
+ * click_ref/type_ref calls can target. Sensitive fields (password,
24
+ * one-time-code, credit-card autocomplete) are redacted in the page
25
+ * before the snapshot leaves it.
26
+ *
27
+ * actions.click_ref / actions.type_ref
28
+ * Act on a ref returned by get_accessibility_tree, in the same tab,
29
+ * using chrome.debugger Input.dispatchMouseEvent / dispatchKeyEvent /
30
+ * insertText. Refs become stale on navigation or DOM replacement; the
31
+ * extension surfaces a clear error and the caller re-snapshots.
32
+ *
33
+ * actions.wait_for_selector
34
+ * Poll the page for a CSS selector via Runtime.evaluate. Returns when
35
+ * it appears or rejects on timeout (default 10s).
36
+ *
37
+ * actions.screenshot
38
+ * Page.captureScreenshot. PNG, base64-encoded.
39
+ *
40
+ * actions.evaluate
41
+ * Runtime.evaluate(expression). Returns the JSON-serialised result.
42
+ * Last-resort escape hatch — prefer ref-based actions because the
43
+ * script string is harder to audit.
44
+ *
45
+ * The connector author writes a normal server-side sync() that sequences
46
+ * these actions through `ctx.chrome.<tool>(args)` (helper added in a
47
+ * later PR — for v1 the actions are reachable directly via the run
48
+ * scheduling API). No bespoke executor code lives in the extension; new
49
+ * connectors compose existing tools.
50
+ *
51
+ * URL allowlist: each connector that runs on top of this dispatcher
52
+ * declares `allowedOrigins` on its own definition. The extension refuses
53
+ * any tool call whose target URL is outside the allowlist.
54
+ *
55
+ * Required worker capability is `browser.debugger`.
56
+ *
57
+ * Cloud-side `sync()` / `execute()` throw — actual work happens in the
58
+ * extension's service worker.
59
+ */
60
+
61
+ import {
62
+ type ActionResult,
63
+ type ConnectorDefinition,
64
+ ConnectorRuntime,
65
+ type SyncContext,
66
+ type SyncResult,
67
+ } from '@lobu/connector-sdk';
68
+
69
+ const BRIDGE_ONLY =
70
+ 'chrome runs only on a worker advertising capability "browser.debugger" (Owletto for Chrome).';
71
+
72
+ const tabIdSchema = {
73
+ type: 'integer',
74
+ description: 'Tab to act on. Defaults to the run-scoped scratch tab.',
75
+ } as const;
76
+
77
+ const refIdSchema = {
78
+ type: 'object',
79
+ required: ['document_epoch', 'ref_id'],
80
+ properties: {
81
+ document_epoch: { type: 'integer' },
82
+ ref_id: { type: 'integer' },
83
+ },
84
+ description:
85
+ 'Element reference returned by a prior get_accessibility_tree call on the same tab + document. frame_id is reserved for future iframe support; v1 dispatches against the main frame.',
86
+ } as const;
87
+
88
+ export default class ChromeConnector extends ConnectorRuntime {
89
+ readonly definition: ConnectorDefinition = {
90
+ key: 'chrome',
91
+ name: 'Chrome',
92
+ description:
93
+ 'Paired Chrome profile. Tab snapshots + a fixed set of typed browser actions (navigate, click, type, wait, screenshot, accessibility snapshot, evaluate) that connectors compose without shipping per-connector code into the extension.',
94
+ version: '0.2.0',
95
+ faviconDomain: 'google.com',
96
+ requiredCapability: 'browser.debugger',
97
+ runtime: { platforms: ['chrome-extension'] as unknown as ['macos'] },
98
+ authSchema: { methods: [{ type: 'none' }] },
99
+ feeds: {
100
+ open_tabs: {
101
+ key: 'open_tabs',
102
+ name: 'Open tabs',
103
+ description: 'Snapshot of the tabs currently open in this Chrome profile.',
104
+ configSchema: { type: 'object', properties: {} },
105
+ eventKinds: {
106
+ tab_snapshot: {
107
+ description: 'One row per tab observed in the active poll cycle.',
108
+ metadataSchema: {
109
+ type: 'object',
110
+ required: ['source', 'origin_id', 'url'],
111
+ properties: {
112
+ source: { type: 'string', const: 'chrome_tabs' },
113
+ origin_id: { type: 'string' },
114
+ url: { type: 'string', format: 'uri' },
115
+ title: { type: 'string' },
116
+ window_id: { type: 'integer' },
117
+ active: { type: 'boolean' },
118
+ },
119
+ },
120
+ },
121
+ },
122
+ },
123
+ tab_events: {
124
+ key: 'tab_events',
125
+ name: 'Tab events',
126
+ description:
127
+ 'Live stream of tab creates / closes / URL changes / focus changes. Each event has a timestamp, so this is the lossless "browsing timeline" companion to the open_tabs snapshot. No extra permission required (baseline `tabs`).',
128
+ configSchema: { type: 'object', properties: {} },
129
+ eventKinds: {
130
+ tab_event: {
131
+ description:
132
+ 'One row per tab lifecycle event. event_type is one of: created, removed, updated, activated.',
133
+ metadataSchema: {
134
+ type: 'object',
135
+ required: ['source', 'origin_id', 'event_type'],
136
+ properties: {
137
+ source: { type: 'string', const: 'chrome_tab_events' },
138
+ origin_id: { type: 'string' },
139
+ event_type: {
140
+ enum: ['created', 'removed', 'updated', 'activated'],
141
+ },
142
+ tab_id: { type: 'integer' },
143
+ url: { type: 'string' },
144
+ title: { type: 'string' },
145
+ window_id: { type: 'integer' },
146
+ from_url: {
147
+ type: 'string',
148
+ description: 'For updated events, the URL the tab was on before the change.',
149
+ },
150
+ },
151
+ },
152
+ },
153
+ },
154
+ },
155
+ },
156
+ actions: {
157
+ navigate: {
158
+ key: 'navigate',
159
+ name: 'Navigate',
160
+ description: 'Open a URL in a fresh background tab (default) or an existing tab.',
161
+ requiresApproval: false,
162
+ inputSchema: {
163
+ type: 'object',
164
+ required: ['url'],
165
+ properties: {
166
+ url: { type: 'string', format: 'uri' },
167
+ tab_id: tabIdSchema,
168
+ open_in_new_tab: {
169
+ type: 'boolean',
170
+ description: 'Default true. Opt out for active-tab control.',
171
+ },
172
+ wait_for_load: {
173
+ type: 'boolean',
174
+ description:
175
+ 'Wait for Page.frameStoppedLoading on the main frame before returning. Default true.',
176
+ },
177
+ },
178
+ },
179
+ outputSchema: {
180
+ type: 'object',
181
+ properties: {
182
+ tab_id: { type: 'integer' },
183
+ current_url: { type: 'string' },
184
+ title: { type: 'string' },
185
+ },
186
+ },
187
+ },
188
+ get_accessibility_tree: {
189
+ key: 'get_accessibility_tree',
190
+ name: 'Get accessibility tree',
191
+ description:
192
+ 'Return a structured snapshot of the visible interactive elements on the page, with stable refs for click_ref/type_ref. Sensitive fields are redacted.',
193
+ requiresApproval: false,
194
+ inputSchema: {
195
+ type: 'object',
196
+ properties: {
197
+ tab_id: tabIdSchema,
198
+ filter: {
199
+ enum: ['interactive', 'visible', 'all'],
200
+ description: 'Default "interactive". "all" is for debugging only.',
201
+ },
202
+ },
203
+ },
204
+ outputSchema: {
205
+ type: 'object',
206
+ properties: {
207
+ document_epoch: { type: 'integer' },
208
+ current_url: { type: 'string' },
209
+ title: { type: 'string' },
210
+ tree: { type: 'array' },
211
+ },
212
+ },
213
+ },
214
+ click_ref: {
215
+ key: 'click_ref',
216
+ name: 'Click element by ref',
217
+ description:
218
+ 'Dispatch a mouse click on the element identified by a ref from a prior accessibility snapshot of the same tab + document.',
219
+ requiresApproval: false,
220
+ inputSchema: {
221
+ type: 'object',
222
+ required: ['ref'],
223
+ properties: {
224
+ ref: refIdSchema,
225
+ tab_id: tabIdSchema,
226
+ button: {
227
+ enum: ['left', 'right', 'middle'],
228
+ description: 'Default "left".',
229
+ },
230
+ click_count: {
231
+ type: 'integer',
232
+ minimum: 1,
233
+ maximum: 3,
234
+ description: 'Default 1. Use 2 for double-click, 3 for triple-click.',
235
+ },
236
+ },
237
+ },
238
+ },
239
+ type_ref: {
240
+ key: 'type_ref',
241
+ name: 'Type into element by ref',
242
+ description:
243
+ 'Focus the element identified by a ref and dispatch keystrokes to enter the given text. Existing value is replaced by default.',
244
+ requiresApproval: false,
245
+ inputSchema: {
246
+ type: 'object',
247
+ required: ['ref', 'text'],
248
+ properties: {
249
+ ref: refIdSchema,
250
+ tab_id: tabIdSchema,
251
+ text: { type: 'string' },
252
+ clear_first: {
253
+ type: 'boolean',
254
+ description: 'Default true. Selects all + deletes before typing.',
255
+ },
256
+ },
257
+ },
258
+ },
259
+ wait_for_selector: {
260
+ key: 'wait_for_selector',
261
+ name: 'Wait for selector',
262
+ description:
263
+ 'Poll the page for the first match of a CSS selector and return when it appears.',
264
+ requiresApproval: false,
265
+ inputSchema: {
266
+ type: 'object',
267
+ required: ['selector'],
268
+ properties: {
269
+ selector: { type: 'string' },
270
+ tab_id: tabIdSchema,
271
+ timeout_ms: {
272
+ type: 'integer',
273
+ minimum: 100,
274
+ maximum: 60_000,
275
+ description: 'Default 10000.',
276
+ },
277
+ },
278
+ },
279
+ },
280
+ screenshot: {
281
+ key: 'screenshot',
282
+ name: 'Screenshot',
283
+ description: 'Capture the visible viewport as a PNG.',
284
+ requiresApproval: false,
285
+ inputSchema: {
286
+ type: 'object',
287
+ properties: {
288
+ tab_id: tabIdSchema,
289
+ },
290
+ },
291
+ outputSchema: {
292
+ type: 'object',
293
+ properties: {
294
+ data_url: {
295
+ type: 'string',
296
+ description: 'data:image/png;base64,... — caller decodes.',
297
+ },
298
+ width: { type: 'integer' },
299
+ height: { type: 'integer' },
300
+ },
301
+ },
302
+ },
303
+ close_tab: {
304
+ key: 'close_tab',
305
+ name: 'Close tab',
306
+ description:
307
+ 'Close a tab the extension created for this connector. Required at the end of any multi-step session — tabs the extension owned across navigate / get_accessibility_tree / click_ref / etc. are NOT auto-disposed (that would break the natural flow). A reaper closes orphaned owned tabs after 30 minutes.',
308
+ requiresApproval: false,
309
+ inputSchema: {
310
+ type: 'object',
311
+ required: ['tab_id'],
312
+ properties: { tab_id: { type: 'integer' } },
313
+ },
314
+ },
315
+ evaluate: {
316
+ key: 'evaluate',
317
+ name: 'Evaluate JS',
318
+ description:
319
+ 'Last-resort escape hatch: run a JS expression with Runtime.evaluate and return the JSON-serialised result. Prefer ref-based actions when possible — scripts are harder to audit.',
320
+ requiresApproval: false,
321
+ inputSchema: {
322
+ type: 'object',
323
+ required: ['expression'],
324
+ properties: {
325
+ expression: { type: 'string' },
326
+ tab_id: tabIdSchema,
327
+ await_promise: {
328
+ type: 'boolean',
329
+ description: 'Default true.',
330
+ },
331
+ },
332
+ },
333
+ outputSchema: {
334
+ type: 'object',
335
+ properties: {
336
+ value: {},
337
+ exception: { type: 'string' },
338
+ },
339
+ },
340
+ },
341
+ },
342
+ };
343
+
344
+ async sync(_ctx: SyncContext): Promise<SyncResult> {
345
+ throw new Error(BRIDGE_ONLY);
346
+ }
347
+
348
+ async execute(): Promise<ActionResult> {
349
+ throw new Error(BRIDGE_ONLY);
350
+ }
351
+ }