ifURI examples
← all examples

39 — controlled social autonomy with a LinkedIn-shaped site

This example is the development version of "full autonomy on a social site":

Domain, host, port, feed path, and host-mapping values live in .env. The default .env.example uses SOCIAL_BROWSER_HOSTNAME=linkedin.com and maps that hostname to SOCIAL_HOST_RESOLVER_TARGET=127.0.0.1 with Chrome's --host-resolver-rules. That means the address bar shows http://linkedin.com:<port>/feed, while the traffic goes to the controlled development server started by the script. The write flow refuses unmapped public hosts, including real https://linkedin.com/feed.

For the real public LinkedIn site, use the read-only/supervised browser path in ../36-remote-browser-cdp: observe the page, inspect OCR/CDP data, and prepare drafts. Autonomous external posting, messaging, commenting, liking, following, password entry, and similar social actions stay out of this unattended write example.

Run

cd /home/tom/github/if-uri/examples/39-local-social-autonomy
cp .env.example .env
python3 autonomous_browser.py

Relevant .env keys:

SOCIAL_ROUTE_DOMAIN=linkedin.com
SOCIAL_BROWSER_SCHEME=http
SOCIAL_BROWSER_HOSTNAME=linkedin.com
SOCIAL_FEED_PATH=/feed
SOCIAL_MAP_BROWSER_HOST=true
SOCIAL_HOST_RESOLVER_TARGET=127.0.0.1
SOCIAL_LOCAL_SUFFIXES=localhost,127.0.0.1,::1,.local,.test,.internal,.lan

Expected result:

{
  "ok": true,
  "url": "http://linkedin.com:/feed",
  "login": {"ok": true},
  "publish": {"ok": true},
  "apiPosts": [{"content": "..."}]
}

Custom post:

python3 autonomous_browser.py --post "Zaloguj sie"
python3 autonomous_browser.py --post "Testowa publikacja z pelnej lokalnej autonomii."
python3 autonomous_browser.py --post "publikacja postu na temat programowania"

Read-only URI command runtime

uri_runtime.py exposes a small typed-URI command vocabulary that drives an attach-only CDP Chrome session: navigate, search via the site's own search, scroll, extract posts, extract comments, OCR low-text blocks, snapshot, and append a markdown capture. It reacts to what it finds on the page (DOM via Runtime.evaluate plus Tesseract OCR for image-heavy blocks) and writes only to local files. There is no publish/comment/like/message/follow/type/ click command — by registry, not by convention. A test asserts that.

Start Chrome once with a debugging port:

google-chrome --remote-debugging-port=9222

Run the default program (feed → search the configured phrase → scroll → extract posts/comments → OCR the main column → append to .state/captures.md):

python3 uri_runtime.py --query "system design"
python3 uri_runtime.py --hashtag python

Run a custom JSON program:

python3 uri_runtime.py --program my_program.json --query " distributed systems"

my_program.json:

[
  {"uri": "chrome://scout/search?scope=posts&q=__QUERY__", "why": "search phrase"},
  {"uri": "chrome://scout/scroll?steps=6", "why": "load results"},
  {"uri": "chrome://scout/extract_posts", "why": "grab posts"},
  {"uri": "chrome://scout/extract_comments", "why": "grab visible comments"},
  {"uri": "chrome://scout/ocr?selector=main", "why": "OCR image-heavy blocks"},
  {"uri": "chrome://scout/append_markdown?path=.state/distributed.md"}
]

Ready-to-run scenarios live in SCENARIOS.md and programs/. They cover feed capture, search+filter+save, hashtag watching, saved-post archiving, profile activity review, and comments/OCR/ snapshot capture:

python3 uri_runtime.py --program programs/01-feed-save.json
python3 uri_runtime.py --program programs/02-search-filter-save.json --query "system design"
python3 uri_runtime.py --program programs/03-hashtag-watch.json --hashtag python
python3 uri_runtime.py --program programs/04-saved-posts-archive.json
python3 uri_runtime.py --program programs/05-profile-posts-review.json
python3 uri_runtime.py --program programs/06-comments-ocr-snapshot.json --query "agentic workflow"

Command reference

URIparamseffect
chrome://scout/navigateurl, settlego to an absolute http(s) URL
chrome://scout/searchq, `scope=content\posts\people, settle`use the site's own search results URL
chrome://scout/scrollsteps, delayscroll steps times by ~one viewport
chrome://scout/filterqset/clear a client-side text filter for the next extract
chrome://scout/extract_postsmin_text_lenpull posts (author/text/url) into the buffer
chrome://scout/extract_commentsmin_text_lenpull visible comments into the buffer
chrome://scout/ocrselectorOCR the bounding box of a CSS selector via Tesseract
chrome://scout/snapshotpathsave a PNG screenshot of the current page
chrome://scout/append_markdownpath, headingflush buffer to markdown, then reset

Result:

{
  "ok": true,
  "results": [
    {"ok": true, "command": "navigate", "navigated": {"title": "Feed", "href": "https://www.linkedin.com/feed/"}},
    {"ok": true, "command": "search", "scope": "posts", "query": "system design", "url": "..."},
    {"ok": true, "command": "extract_posts", "count": 12},
    {"ok": true, "command": "append_markdown", "path": ".state/captures.md", "posts": 12, "comments": 0}
  ],
  "captured": 12
}

Scenario programs

programs/*.json are ready-to-run programs that combine the commands above into common read-only workflows. Each one only uses commands from the registry — verified by test_program_files_use_known_uri_commands. Run any of them with:

python3 uri_runtime.py --program programs/NN-NAME.json --query "..."
programuse caseplaceholders
01-feed-save.jsonarchive the home feed
02-search-filter-save.jsonsearch a phrase and keep only matching posts/comments__QUERY__
03-hashtag-watch.jsonfollow a hashtag feed__HASHTAG__
04-saved-posts-archive.jsonback up your saved posts
05-profile-posts-review.jsonreview your own recent activity__PROFILE_PATH__
06-comments-ocr-snapshot.jsoncapture posts + comments + OCR + PNG__QUERY__
07-people-search-bio-scan.jsonfind people by skill/company, OCR their bio cards__QUERY__
08-multi-hashtag-compare.jsoncompare two hashtags side by side into one file__HASHTAG_A__, __HASHTAG_B__
09-competitor-monitor.jsontrack posts/comments mentioning a competitor__QUERY__
10-weekly-feed-and-saved-archive.jsonweekly backup of feed + saved posts
11-deep-comment-thread.jsonextract a post + scroll for nested replies + OCR__QUERY__
12-multi-keyword-tracker.jsonfilter the same feed by two keywords sequentially__KEYWORD_A__, __KEYWORD_B__

Multi-value placeholders are passed via repeatable --define KEY=VALUE:

python3 uri_runtime.py --program programs/08-multi-hashtag-compare.json \
  --define HASHTAG_A=python --define HASHTAG_B=rust

python3 uri_runtime.py --program programs/12-multi-keyword-tracker.json \
  --define KEYWORD_A=release --define KEYWORD_B=security

--define HASHTAG_A=python replaces every __HASHTAG_A__ in the program (both spelled-out and bare-key forms work). Each value is URL-encoded automatically.

scout.py attaches to a Chrome session you already run with --remote-debugging-port (logged in as you) and walks the read pages: home feed, your recent activity, saved posts, and a hashtag/topic page. It scrolls, parses, de-duplicates by URL+text, and appends interesting posts to .state/captures.md. It never types, never clicks publish, and never navigates away from these read pages — the publish step stays a human action.

Start Chrome once with a debugging port and your normal profile:

google-chrome --remote-debugging-port=9222

Then run the scout:

python3 scout.py
python3 scout.py --pages feed,saved
python3 scout.py --out .state/captures-python.md

Relevant .env keys:

LI_DEBUG_PORT=9222
LI_DEBUG_PORTS=9222,9223,9224
LI_CDP_ENDPOINTS=chrome=http://127.0.0.1:9222,brave=http://127.0.0.1:9223,chromium=http://127.0.0.1:9224
LI_PROFILE_PATH=/in/tom-developer/recent-activity/
LI_HASHTAG=programming
LI_SCROLL_STEPS=4
LI_SCROLL_DELAY=1.5
LI_MIN_TEXT_LEN=80

Expected result:

{
  "ok": true,
  "pages": {
    "feed": {"url": "https://www.linkedin.com/feed/", "count": 12},
    "myposts": {"url": "https://www.linkedin.com/in/tom-developer/recent-activity/", "count": 3},
    "saved": {"url": "https://www.linkedin.com/my-items/saved-posts/", "count": 5},
    "hashtag": {"url": "https://www.linkedin.com/feed/hashtag/?keywords=programming", "count": 8}
  },
  "captured": 23,
  "out": ".state/captures.md"
}

Find the Existing LinkedIn Session via URI

Before scouting, ask urirun which already-running browser/profile has the LinkedIn session. This is read-only and does not launch a browser.

Start candidate browsers with separate CDP ports:

google-chrome --remote-debugging-port=9222
brave-browser --remote-debugging-port=9223
chromium --remote-debugging-port=9224

Set endpoints in .env:

LI_CDP_ENDPOINTS=chrome=http://127.0.0.1:9222,brave=http://127.0.0.1:9223,chromium=http://127.0.0.1:9224

Run the URI query:

./run_session_probe.sh

The script compiles and runs:

browser://local/linkedin/session/query/find

The result reports the browser label, CDP endpoint, browser version, LinkedIn tabs, and whether the li_at session cookie exists. Cookie values are never printed.

Natural Language Via urirun

The shortest prompt-driven command is:

./run_prompt.sh 'opublikuj "publikacja z promptu NL przez urirun agent run"'

That script uses built-in urirun pieces:

PYTHONPATH=/home/tom/github/if-uri/urirun/adapters/python:$PWD \
  python3 nl_autonomy.py --env .env --write-bindings .state/local-social.bindings.json

PYTHONPATH=/home/tom/github/if-uri/urirun/adapters/python \
  python3 -m urirun.runtime.v2 compile .state/local-social.bindings.json \
    --out .state/local-social.registry.json

PYTHONPATH=/home/tom/github/if-uri/urirun/adapters/python:$PWD \
  python3 -m urirun.runtime.v2 agent run .state/local-social.registry.json \
    --planner nl_autonomy:planner \
    --allow 'social://**' \
    --allow-commands \
    --goal 'opublikuj "Testowa publikacja z promptu NL przez urirun agent run"'

The registry exposes one command:

social://linkedin.com/post/command/publish

nl_autonomy:planner maps the NL prompt to that typed URI payload, and urirun's agent runner validates and executes the command under policy. The URI deliberately uses SOCIAL_ROUTE_DOMAIN as the social domain in the action space; the handler still starts the controlled development server and maps the browser hostname when SOCIAL_MAP_BROWSER_HOST=true.

Run only the fake site:

python3 mock_linkedin.py --port 8080

Then open:

http://127.0.0.1:8080/feed

The credentials are the values in .env.

Files

Boundary

This is intentionally full autonomy only for controlled hosts such as localhost, 127.0.0.1, .local, .test, .internal, .lan, or an explicitly mapped browser hostname like linkedin.com:<port> in this example. It is the place to develop selectors, OCR, closed-loop repair, and URI flow generation before any real human-approved workflow.

Files

.env.env.example.gitignoreREADME.mdSCENARIOS.mdautonomous_browser.pybindings.jsonextract_invoices.pylenovo-office-workflow.yamlmock_linkedin.pynl_autonomy.pyrun_email_connector.pyrun_prompt.shrun_session_probe.shscout.pysession-probe.bindings.jsonsession_probe.pytest_local_social.pytest_scout.pytest_session_probe.pytest_uri_runtime.pyuri_runtime.py.state/programs/

View on GitHub →