@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.
- package/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
- package/dist/commands/_lib/apply/apply-cmd.js +160 -12
- package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
- package/dist/commands/_lib/apply/client.d.ts +106 -0
- package/dist/commands/_lib/apply/client.d.ts.map +1 -1
- package/dist/commands/_lib/apply/client.js +163 -2
- package/dist/commands/_lib/apply/client.js.map +1 -1
- package/dist/commands/_lib/apply/desired-state.d.ts +53 -0
- package/dist/commands/_lib/apply/desired-state.d.ts.map +1 -1
- package/dist/commands/_lib/apply/desired-state.js +182 -5
- package/dist/commands/_lib/apply/desired-state.js.map +1 -1
- package/dist/commands/_lib/apply/diff.d.ts +12 -1
- package/dist/commands/_lib/apply/diff.d.ts.map +1 -1
- package/dist/commands/_lib/apply/diff.js +106 -7
- package/dist/commands/_lib/apply/diff.js.map +1 -1
- package/dist/commands/_lib/connector-loader.d.ts +3 -0
- package/dist/commands/_lib/connector-loader.d.ts.map +1 -0
- package/dist/commands/_lib/connector-loader.js +129 -0
- package/dist/commands/_lib/connector-loader.js.map +1 -0
- package/dist/commands/_lib/connector-run-cmd.d.ts +35 -0
- package/dist/commands/_lib/connector-run-cmd.d.ts.map +1 -0
- package/dist/commands/_lib/connector-run-cmd.js +351 -0
- package/dist/commands/_lib/connector-run-cmd.js.map +1 -0
- package/dist/commands/_lib/export/export-cmd.d.ts +35 -0
- package/dist/commands/_lib/export/export-cmd.d.ts.map +1 -0
- package/dist/commands/_lib/export/export-cmd.js +329 -0
- package/dist/commands/_lib/export/export-cmd.js.map +1 -0
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +11 -14
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +19 -5
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/connector.d.ts +3 -0
- package/dist/commands/connector.d.ts.map +1 -0
- package/dist/commands/connector.js +5 -0
- package/dist/commands/connector.js.map +1 -0
- package/dist/commands/context.d.ts +7 -0
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +19 -2
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +156 -4
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +2 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/eval.d.ts.map +1 -1
- package/dist/commands/eval.js +12 -13
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +5 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +22 -16
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/memory/_lib/browser-auth-cmd.d.ts.map +1 -1
- package/dist/commands/memory/_lib/browser-auth-cmd.js +15 -144
- package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
- package/dist/commands/token.d.ts.map +1 -1
- package/dist/commands/token.js +1 -4
- package/dist/commands/token.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +4 -13
- package/dist/commands/validate.js.map +1 -1
- package/dist/config/loader.js +2 -2
- package/dist/config/loader.js.map +1 -1
- package/dist/connectors/README.md +0 -1
- package/dist/connectors/apple_photos.ts +178 -0
- package/dist/connectors/browser-scraper-utils.ts +76 -0
- package/dist/connectors/chrome.ts +351 -0
- package/dist/connectors/chrome_bookmarks.ts +79 -0
- package/dist/connectors/chrome_downloads.ts +80 -0
- package/dist/connectors/chrome_history.ts +80 -0
- package/dist/connectors/github.ts +1 -0
- package/dist/connectors/google_calendar.ts +14 -2
- package/dist/connectors/google_play.ts +22 -2
- package/dist/connectors/hackernews.ts +37 -2
- package/dist/connectors/index.ts +15 -1
- package/dist/connectors/reddit.ts +1 -0
- package/dist/connectors/revolut.ts +10 -13
- package/dist/connectors/rss.ts +33 -8
- package/dist/connectors/trustpilot.ts +31 -20
- package/dist/connectors/website.ts +7 -68
- package/dist/connectors/whatsapp.ts +12 -21
- package/dist/db/migrations/20260514130000_connection_action_modes.sql +103 -0
- package/dist/db/migrations/20260514160000_auth_profiles_mirror_mode.sql +32 -0
- package/dist/db/migrations/20260515120000_agents_per_org_pk.sql +66 -0
- package/dist/db/migrations/20260515150000_geo_enrichment.sql +208 -0
- package/dist/db/migrations/20260515160000_drop_agents_org_id_unique.sql +24 -0
- package/dist/db/migrations/20260515170000_auth_profiles_default_for_connector.sql +23 -0
- package/dist/db/migrations/20260516120000_agents_per_org_pk_swap.sql +125 -0
- package/dist/db/migrations/20260516200000_events_search_tsv.sql +134 -0
- package/dist/db/migrations/20260516200100_events_lifecycle_changes_index.sql +25 -0
- package/dist/db/migrations/20260517010000_drop_unused_indexes.sql +49 -0
- package/dist/db/migrations/20260517020000_softdelete_orphan_feeds.sql +56 -0
- package/dist/db/migrations/20260517030000_pat_worker_id_binding.sql +27 -0
- package/dist/db/migrations/20260517040000_archive_orphan_watchers.sql +30 -0
- package/dist/db/migrations/20260517050000_watcher_agent_id_not_null.sql +34 -0
- package/dist/db/migrations/20260517060000_watcher_schema_additions.sql +78 -0
- package/dist/db/migrations/20260517150000_goals_primitive.sql +55 -0
- package/dist/db/migrations/20260517160000_drop_goals_primitive.sql +45 -0
- package/dist/db/migrations/20260518000000_pending_interactions.sql +49 -0
- package/dist/db/migrations/20260518010000_runs_heartbeat_reaper_index.sql +22 -0
- package/dist/db/migrations/20260518020000_runs_heartbeat_inflight_narrow.sql +36 -0
- package/dist/db/migrations/20260518040000_agent_transcript_snapshot.sql +54 -0
- package/dist/db/migrations/20260518050000_runs_denormalize_agent_conversation.sql +36 -0
- package/dist/db/migrations/20260518060000_revert_runs_denormalize.sql +29 -0
- package/dist/db/migrations/20260518070000_runs_heartbeat_inflight_widen.sql +33 -0
- package/dist/eval/client.d.ts.map +1 -1
- package/dist/eval/client.js +11 -0
- package/dist/eval/client.js.map +1 -1
- package/dist/eval/grader.js +2 -1
- package/dist/eval/grader.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +84 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/context.d.ts +13 -1
- package/dist/internal/context.d.ts.map +1 -1
- package/dist/internal/context.js +83 -8
- package/dist/internal/context.js.map +1 -1
- package/dist/internal/credentials.d.ts +5 -0
- package/dist/internal/credentials.d.ts.map +1 -1
- package/dist/internal/credentials.js +75 -1
- package/dist/internal/credentials.js.map +1 -1
- package/dist/internal/index.d.ts +2 -2
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +2 -2
- package/dist/internal/index.js.map +1 -1
- package/dist/internal/local-env.d.ts.map +1 -1
- package/dist/internal/local-env.js +9 -2
- package/dist/internal/local-env.js.map +1 -1
- package/dist/server.bundle.mjs +7085 -2832
- package/dist/start-local.bundle.mjs +8269 -3656
- package/package.json +7 -5
- 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,
|
|
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
|
+
}
|