meta-acct: Accounts Center read API (list-linked, security-status, login-activity)\n\nImplements the read ops from META-ACCOUNTS-API.md via the agent browser\nsession. Key finding: phone-OTP login leaves a full Meta session -\naccountscenter.meta.com loads without redirect.\n\nAlso: 646 node fully registered (warp-646, CDP 9430, active).

This commit is contained in:
operator
2026-10-03 19:20:47 +00:00
parent 1b3faf0126
commit 667755e312
5 changed files with 364 additions and 1 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ node name, chrome-box profile, API `--account`, and the agent's display name.
|-------|------|---------|------------|------------|-------|-----------|------------------|--------|-----------|----------|--------------|-------| |-------|------|---------|------------|------------|-------|-----------|------------------|--------|-----------|----------|--------------|-------|
| muse | muse | muse | email_otp | ltd.pixels.ltd@gmail.com | ltd.pixels.ltd@gmail.com | no | unknown | active | 104.28.195.181 | 9410 | muse | Main dev agent. Logged in 2026-10-03 via email OTP on bl. | | muse | muse | muse | email_otp | ltd.pixels.ltd@gmail.com | ltd.pixels.ltd@gmail.com | no | unknown | active | 104.28.195.181 | 9410 | muse | Main dev agent. Logged in 2026-10-03 via email OTP on bl. |
| pip | pip | pip | phone_otp | piparada | - | yes | yes | active | 104.28.195.181 | 9420 | pip | Phone OTP login 2026-10-03. Renamed from 'Muse' to 'pip'. Session lost on browser restart 2026-10-03; needs re-auth. IG avatar visible in account selector. | | pip | pip | pip | phone_otp | piparada | - | yes | yes | active | 104.28.195.181 | 9420 | pip | Phone OTP login 2026-10-03. Renamed from 'Muse' to 'pip'. Session lost on browser restart 2026-10-03; needs re-auth. IG avatar visible in account selector. |
| 646 | 646 | 646 | phone_otp | Meta Account | - | yes | unknown | pending_auth | - | - | 646 | Shares phone number with piparada's account. Logged in 2026-10-03 via phone OTP (first Meta Account option). Node not yet created on bl. | | 646 | 646 | 646 | phone_otp | Meta Account | - | yes | unknown | active | 104.28.195.181 | 9430 | 646 | Shares phone number with piparada's account. Logged in 2026-10-03 via phone OTP (first Meta Account option). Node created 2026-10-03 (warp-646, CDP 9430). Browser up, session active (verified 2026-10-03). |
## Login Type Details ## Login Type Details
+1
View File
@@ -10,3 +10,4 @@ Unified naming: node == agent == profile == API account.
## History ## History
- 2026-10-03: Renamed smoke->muse, phone-test->pip for unified naming. - 2026-10-03: Renamed smoke->muse, phone-test->pip for unified naming.
Profiles preserved, sessions persisted (muse). WireGuard identities renamed. Profiles preserved, sessions persisted (muse). WireGuard identities renamed.
| 646 | warp-646 | 104.28.195.181 | 9430 | active | 646 (phone_otp, first Meta Account option) |
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""Meta Accounts Center read API (via authenticated browser session).
Usage:
meta-acct.py list-linked <agent> # linked profiles in this Meta Account
meta-acct.py security-status <agent> # login & recovery / security checkup summary
meta-acct.py login-activity <agent> # "Where you're logged in" sessions
The agent's browser must have an active Meta session (phone/email OTP login).
Reads NODES.md for the CDP port. Outputs JSON.
Scope: Accounts Center linkage/security surface only. No writes.
"""
import json, sys, time, urllib.request, os
NETVM = os.environ.get("NETVM_DIR", os.path.expanduser("~/Projects/NetVM"))
AC_BASE = "https://accountscenter.meta.com"
def cdp_port(agent):
for line in open(os.path.join(NETVM, "NODES.md")):
line = line.strip()
if not line.startswith("|"):
continue
cells = [c.strip() for c in line.strip("|").split("|")]
if len(cells) >= 4 and cells[0] == agent:
return int(cells[3])
raise SystemExit(f"meta-acct: unknown agent '{agent}' (not in NODES.md)")
def cdp_get(port, path, method="GET", timeout=10):
req = urllib.request.Request(f"http://127.0.0.1:{port}{path}", method=method)
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.loads(r.read())
def get_ac_tab(port):
tabs = cdp_get(port, "/json/list")
for t in tabs:
if "accountscenter.meta.com" in t.get("url", "") and t.get("type") == "page":
return t
# create one
return cdp_get(port, f"/json/new?{AC_BASE}/", method="PUT")
def evaluate(port, ws_url, js, timeout=15):
import websocket
ws = websocket.create_connection(ws_url, timeout=timeout)
try:
ws.send(json.dumps({"id": 1, "method": "Page.navigate",
"params": {"url": js[0]}}))
ws.recv()
time.sleep(4)
ws.send(json.dumps({"id": 2, "method": "Runtime.evaluate",
"params": {"expression": js[1], "returnByValue": True}}))
resp = json.loads(ws.recv())
return json.loads(resp["result"]["result"]["value"])
finally:
ws.close()
def cmd_list_linked(port):
tab = get_ac_tab(port)
data = evaluate(port, tab["webSocketDebuggerUrl"], (
f"{AC_BASE}/account_overview/",
"""JSON.stringify((() => {
const h2 = [...document.querySelectorAll('h2')]
.find(e=>e.innerText.trim()==='Profiles');
// Full section is 3 levels up (H2 > DIV > DIV > MAIN)
const container = h2?.parentElement?.parentElement?.parentElement;
return {
email: (document.body.innerText.match(
/[\\w.+-]+@[\\w-]+\\.[\\w.]+/)||[])[0]||null,
section: container?.innerText?.slice(0,1000) || null
};
})())"""))
# Parse: lines after "Profiles" are name/type pairs until "Add more",
# then "AI and devices" section follows
profiles = []
section = data.get("section", "")
if section:
lines = [l.strip() for l in section.split("\n") if l.strip()]
try:
start = lines.index("Profiles") + 1
except ValueError:
start = 0
i = start
while i < len(lines):
if lines[i] == "Add more":
i += 1
continue
if lines[i] == "AI and devices":
# Next line is the device/service name
if i + 1 < len(lines):
profiles.append({"name": lines[i+1], "type": "ai_device"})
break
# Expect name + type pair
if i + 1 < len(lines) and lines[i+1] not in (
"Add more", "AI and devices", "Profiles"):
profiles.append({"name": lines[i],
"type": lines[i+1].lower()})
i += 2
else:
i += 1
return {"email": data.get("email"), "profiles": profiles}
def cmd_security_status(port):
tab = get_ac_tab(port)
data = evaluate(port, tab["webSocketDebuggerUrl"], (
f"{AC_BASE}/password_and_security/",
"""JSON.stringify({
checkup: document.body.innerText.match(
/Meta Security Checkup\\s*(\\d+) recommended actions/)?.[1] || null,
sections: [...document.querySelectorAll('h2')]
.map(e=>e.innerText.trim()).slice(0,10),
body: document.body.innerText.slice(0,2000)
})"""))
return {"security_checkup_actions": data.get("checkup"),
"sections": data.get("sections")}
def cmd_login_activity(port):
tab = get_ac_tab(port)
# "Where you're logged in" is on the password_and_security page
data = evaluate(port, tab["webSocketDebuggerUrl"], (
f"{AC_BASE}/password_and_security/",
"""JSON.stringify({
body: document.body.innerText.slice(0,4000)
})"""))
body = data.get("body", "")
# Extract the "Where you're logged in" section
idx = body.find("Where you're logged in")
section = body[idx:idx+1500] if idx >= 0 else ""
return {"where_logged_in": section}
def main():
if len(sys.argv) != 3:
print(__doc__.strip().split("\n")[0])
print("Usage: meta-acct.py <list-linked|security-status|login-activity> <agent>")
sys.exit(2)
cmd, agent = sys.argv[1], sys.argv[2]
port = cdp_port(agent)
try:
if cmd == "list-linked":
out = cmd_list_linked(port)
elif cmd == "security-status":
out = cmd_security_status(port)
elif cmd == "login-activity":
out = cmd_login_activity(port)
else:
raise SystemExit(f"meta-acct: unknown command '{cmd}'")
except Exception as e:
print(json.dumps({"error": str(e), "agent": agent}))
sys.exit(1)
out["agent"] = agent
out["checked_at"] = int(time.time())
print(json.dumps(out, indent=2))
if __name__ == "__main__":
main()
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
# meta-acct.sh — Meta Accounts Center read API (via authenticated browser session).
#
# Usage:
# meta-acct.sh list-linked <agent> # linked profiles in this Meta Account
# meta-acct.sh security-status <agent> # login & recovery / security checkup
# meta-acct.sh login-activity <agent> # "Where you're logged in" sessions
#
# The agent's browser must have an active Meta session (phone/email OTP login).
# Reads NODES.md for the CDP port. Outputs JSON.
#
# Scope: Accounts Center linkage/security surface only. No writes.
# Runs the Python implementation inside the agent's NetVM netns.
set -euo pipefail
NETVM_DIR="${NETVM_DIR:-$HOME/Projects/NetVM}"
SCRIPT="$NETVM_DIR/bin/meta-acct.py"
if [ $# -ne 2 ]; then
echo "Usage: meta-acct.sh <list-linked|security-status|login-activity> <agent>" >&2
exit 2
fi
cmd="$1"
agent="$2"
# Get the node name from ACCOUNTS.md (agent == node per unified naming)
node="$agent"
# Run inside the netns so we reach the browser's CDP on 127.0.0.1.
# NETVM_DIR must be explicit: sudo resets HOME to /root.
exec sudo -n ip netns exec "warp-$node" \
env NETVM_DIR="$NETVM_DIR" python3 "$SCRIPT" "$cmd" "$agent"
+174
View File
@@ -0,0 +1,174 @@
# Meta Accounts Center API
**Status:** active (2026-10-03)
**Motivation:** the phone number used in the muse.ai OTP pilot is linked to a
Facebook account with its own credentials. Meta account management must be a
separate, isolated API — never tangled with the phone-OTP flow.
## What it is
A dedicated operator CLI + privileged gateway for automating
[Accounts Center](https://accountscenter.meta.com/) operations: the Meta hub
where Facebook, Instagram, WhatsApp, and Meta accounts are linked, with
shared login, security settings, and connected experiences.
It is **not** a general Facebook/Instagram automation tool. It manages the
*account linkage and security surface* — which accounts are linked, how they
log into each other, 2FA/password state, login activity. Content operations
(posting, messaging) are out of scope.
## Separation guarantees
1. **Credential namespace:** uses `meta-creds.sh` types `facebook` /
`instagram` only. The phone number is a *contact method* on the Facebook
account, never a credential identifier. The `phone-otp` route (muse.ai
SMS flow) and Meta credentials never share IDs, profiles, or audit logs.
2. **Browser/session reuse:** Meta automation runs in the agent's
existing chrome-box profile — the same profile serves muse.ai and
accountscenter.meta.com. The 1:1 rule (node == agent == profile)
already gives us one egress per agent; no separate Meta node needed.
The phone-OTP login that established the muse.ai session also
establishes the Meta session, so read ops reuse it without
re-authenticating.
3. **Authorization without exposure:** follows `CREDENTIAL-GATEWAY.md`.
Agents know credential IDs and target actions; the privileged gateway
decrypts from `store.age` and injects via CDP. Secrets never enter agent
context, logs, or chat.
4. **Audit:** every call logs `{ts, credential_id, action, target}` —
never values. Separate audit trail from phone-OTP operations.
## Operations
### Read (operator may run freely)
| Command | What it returns |
|---|---|
| `meta-acct.sh list-linked <cred-id>` | Accounts in the Accounts Center (type, username, linked since) |
| `meta-acct.sh login-activity <cred-id>` | Recent logins: device, IP/geo, time; flags unrecognized |
| `meta-acct.sh security-status <cred-id>` | 2FA on/off + method, login alerts on/off, password age |
| `meta-acct.sh connected-experiences <cred-id>` | Per-experience toggles: cross-app login, cross-posting, etc. |
### Write (human-gated via `/etc/netvm/approvals/`)
| Command | Gate reason |
|---|---|
| `meta-acct.sh add-account <cred-id> --type <facebook\|instagram>` | Links a new account; changes login blast radius |
| `meta-acct.sh remove-account <cred-id> --account <id>` | Unlinks; some connected experiences don't revert cleanly |
| `meta-acct.sh set-login-linking <cred-id> --on\|--off` | Controls cross-app login (see 2026 change below) |
| `meta-acct.sh rotate-password <cred-id>` | Credential change; invalidates sessions |
| `meta-acct.sh set-2fa <cred-id> --method <sms\|app\|key>` | Security-critical |
Human gates use the existing `netvm-approve.sh` flow: the agent prepares the
exact action, the human approves the specific `(credential_id, action,
target)` tuple, the gateway executes once.
## The early-2026 login change
Meta is removing the "Logging in with accounts" toggle: starting early 2026,
**all accounts in the same Accounts Center log into each other by default**.
Accounts Center membership *becomes* the login boundary.
Implications for this API:
- `list-linked` is now security-critical: anyone with one account's
credentials can reach the others.
- The API must surface a "blast radius" view: for a given credential, which
accounts are reachable through the Accounts Center.
- `remove-account` becomes the only way to revoke cross-app login — it must
stay human-gated and be auditable.
- Recommend: keep each client's accounts in their own Accounts Center;
never mix clients (consistent with the one-client-one-credential-set rule).
## Architecture
```
Agent (untrusted) ──▶ meta-acct.sh (operator CLI)
│ validates scope, checks human-gate
▼
Gateway (privileged, runs as operator)
│ meta-creds.sh get <type> <id>
│ decrypts store.age, never logs values
▼
CDP ──▶ accountscenter.meta.com
(in the agent's chrome-box profile —
same browser as the muse.ai session)
```
- Transport: Unix socket IPC between agent and gateway (not network).
- Injection: CDP `Runtime.evaluate` into the Accounts Center DOM; falls
back to field-focus + virtual keyboard only if DOM injection fails.
- 2FA during a write op: the gateway pauses, requests the code through the
existing OTP handoff (human relays), resumes. The code is transient —
used once, never stored.
## Registry
`ACCOUNTS.md` rows per Meta login, same discipline as today:
`pending_auth` → `active`, `2fa-pending` where applicable. The `notes`
column records the Accounts Center linkage (e.g. "facebook + instagram
linked in one Accounts Center; blast radius = 2 accounts"). No credential
values, no phone numbers.
## Change detection
`bin/meta-ac-snapshot.py` turns the smoke test into a change detector.
Each run captures a structural snapshot via CDP inside the node's netns
and diffs against `snapshots/meta-ac/baseline.json`:
- **redirect_chain** (normalized: query params stripped — nonces change
every visit): seeded with the navigation target, then polls for where
Meta sends us. Catches auth-flow reroutes.
- **dom_markers**: form IDs, input names, button labels. Catches page
restructures.
- **final_title**: sanity signal.
Outcomes: `PASS` (matches), `CHANGED` (structural diff — baseline
untouched, human reviews then `--promote`), `FAIL` (automation broke).
Snapshots are timestamped JSON; the diff doubles as the audit trail for
what Meta changed and when.
Validated 2026-10-03 on bl: headless Chromium in `warp-phone` netns via
CDP reached accountscenter.meta.com → expected Meta auth redirect
(auth.meta.com OIDC), login page renders. Baseline established; second
run PASS.
## Out of scope (explicit)
- Posting, messaging, ad management, Page/Portfolio operations.
- The phone-OTP / muse.ai flow (separate API, separate credentials —
see `docs/PHONE-OTP.md`).
- WhatsApp linking (not available in all regions; revisit later).
- Business Suite / Partner access (different trust model; separate spec if
needed).
## Open questions
1. Which NetVM node hosts Meta automation — new dedicated node, or reuse
an existing client node? (Lean: dedicated `meta` node, one egress.)
2. Should read ops also require the approvals flow for high-sensitivity
accounts, or is operator-level enough?
3. Recovery codes: store in `store.age` (retrievable) or human-only?
## Implementation status (2026-10-03)
**Read ops implemented** in bin/meta-acct.py + bin/meta-acct.sh:
- list-linked AGENT — linked profiles from account_overview. Returns
email (if present), profiles array with name/type (instagram, ai_device).
Verified on pip (1 Instagram + Muse) and 646 (Muse only, no email).
- security-status AGENT — security checkup action count + section list
from password_and_security.
- login-activity AGENT — "Where you're logged in" section text.
(Currently returns the section header; the expandable session list needs
a click-through — future work.)
**Key finding:** the phone-OTP login for muse.ai leaves a full Meta session
in the browser profile. accountscenter.meta.com loads without a login
redirect. No separate Meta login needed — the agent profile IS the
Accounts Center session.
**Usage:** runs inside the agent's netns via meta-acct.sh wrapper:
~/Projects/NetVM/bin/meta-acct.sh list-linked pip
Write ops (add-account, remove-account, etc.) remain human-gated per the
spec above. Not yet implemented.