Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agentlinkops",
"version": "0.6.6",
"version": "0.6.7",
"description": "Connect an agent to AgentLinkOps and run backlink work with it: the connect skill (views, the three discovery verbs, evidence rules), local ledger and campaign skills, a portable CRM, the source-cited content pipeline, user-managed disavow lists, HTML reports and monitoring-event sync.",
"author": {
"name": "AgentLinkOps"
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agentlinkops",
"version": "0.6.6",
"version": "0.6.7",
"description": "Connect an agent to AgentLinkOps and run backlink work with it: the connect skill (views, the three discovery verbs, evidence rules), local ledger and campaign skills, a portable CRM, the source-cited content pipeline, user-managed disavow lists, HTML reports and monitoring-event sync.",
"author": {
"name": "AgentLinkOps"
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
All notable changes to the `agentlinkops` package are recorded here. The version follows the
plugin manifests in `.claude-plugin/` and `.codex-plugin/`.

## 0.6.7 (2026-09-20)

- Default browser citation measurement to accountless mode. Saved session files are ignored unless `AGENTLINKOPS_BROWSER_AUTH=saved-session` is explicitly selected.
- Reject custom launch, environment, extension and Firefox-preference overrides in accountless mode. Proxy admission remains required; direct diagnostics still need explicit authorization and configuration.
- Keep anonymous login walls, blocked pages and failed answers unknown. Accountless mode does not guarantee that a provider will answer.

## 0.6.6 (2026-09-20)

- Retain supplied browser usage receipts in a bounded local journal after each attempt and cleanup. Missing meter values remain unknown; failed writes stop the run.
Expand Down
15 changes: 9 additions & 6 deletions cli/citation.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ const USAGE = `agentlinkops citation — AI citation watches in this repository
tallies land under .agentlinkops/citations/
agentlinkops citation run PANEL.json --engine chatgpt:web-own-browser
measure the consumer UI through Camoufox
(dedicated account, personal cadence; evidence
includes a screenshot of every answer)
(accountless by default; saved browser sessions
are not loaded)
agentlinkops citation sweep PANEL.json --max-usd USD --out INVENTORY.json [--dir DIR]
run a bounded epoch and freeze its retained evidence
agentlinkops citation inventory PANEL.json --out INVENTORY.json [--epoch ID] [--dir DIR]
Expand All @@ -52,8 +52,8 @@ const USAGE = `agentlinkops citation — AI citation watches in this repository
agentlinkops citation alert PANEL.json [--webhook URL --report REPORT.html]
check the latest epoch for declines;
logs an alert line if any cell declined
agentlinkops citation login ENGINE open a browser, log in once, save the session
(the engine adapter uses it on every run)
agentlinkops citation login ENGINE optional legacy saved-session setup
(not needed for accountless measurement)
agentlinkops citation logout ENGINE remove a saved session
agentlinkops citation sessions list saved engine sessions
agentlinkops citation sync [--dir DIR] [--evidence] push local epoch rows to the hosted
Expand All @@ -65,8 +65,11 @@ const USAGE = `agentlinkops citation — AI citation watches in this repository

Panel (JSON): targets (domain or url, brand, aliases), prompts, engines
([{ "engine": "mock" | "perplexity", "model": "…" }]), optional samples and maxUsd.
Live engines need their configured credentials or a usable browser session; the mock
engine runs everything at zero cost. Every run states its estimated spend, and the
API engines need their configured credentials. Browser measurement defaults to
AGENTLINKOPS_BROWSER_AUTH=accountless and never loads saved sessions in that mode.
Optional legacy saved-session mode requires explicit AGENTLINKOPS_BROWSER_AUTH=saved-session.
Anonymous login walls or failed answers remain unknown; access is not guaranteed.
The mock engine runs everything at zero cost. Every run states its estimated spend, and the
cap reserves estimated cost before each call. A supplier charge above that estimate
is recorded as an overrun and stops subsequent calls. Browser measurement requires
AGENTLINKOPS_PROXY_URL. Authorized direct diagnostics need
Expand Down
2 changes: 1 addition & 1 deletion cli/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {restResponseContract} from '../shared/response-contract.js';
// A small HTTP client for the cloud. Scoped keys only; no human session ever reaches here.
// DP-0036-T13: the CLI names itself so the usage counters can tell it from other REST callers.
// The version follows the plugin manifests; bump it with them.
export const CLI_VERSION = '0.6.6';
export const CLI_VERSION = '0.6.7';
export const CLI_USER_AGENT = `agentlinkops-cli/${CLI_VERSION}`;
export class CloudError extends Error {
constructor(code, status, details = null) {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agentlinkops",
"version": "0.6.6",
"version": "0.6.7",
"description": "A backlink and AI citation ledger that lives in your repository: local link and citation checks with dated evidence, supplier-export imports and optional sync to the hosted AgentLinkOps service.",
"author": "AgentLinkOps (https://agentlinkops.com)",
"license": "Apache-2.0",
Expand Down
4 changes: 3 additions & 1 deletion references/citation-panels.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ This command defaults to the mock engine. Its suggested questions are writing pr

Use `--competitors competitors.json` to add domains to check beside your own. The file is an array of objects with `domain`, `brand` and optional `aliases` fields. Each target gets separate results for each question. A target missing from one answer is only missing from that sample; it does not establish absence from the engine.

Choose a live engine explicitly after checking access and budget. For an existing browser setup, use `--engine chatgpt:web-own-browser` or `--engine grok:web-own-browser`. `--engine google-aio` requires your configured supplier access. This workflow does not create accounts or buy services.
Choose a live engine explicitly after checking access and budget. For accountless browser measurement, use `--engine chatgpt:web-own-browser` or `--engine grok:web-own-browser`. `--engine google-aio` requires your configured supplier access. This workflow does not create accounts or buy services.

Starting with CLI 0.6.7, browser authentication defaults to `AGENTLINKOPS_BROWSER_AUTH=accountless`. This mode does not load saved browser sessions, even if session files already exist. A login wall, blocked page or failed answer remains unknown; anonymous access is not guaranteed. Use accountless measurement for this workflow. The optional legacy `AGENTLINKOPS_BROWSER_AUTH=saved-session` mode is separate and requires an explicit opt-in; logging in is not a prerequisite for the default workflow.

Starting with CLI 0.6.5, browser measurement requires `AGENTLINKOPS_PROXY_URL`. Missing, failed or quarantined proxy admission never selects direct traffic. An explicitly authorized direct diagnostic requires `AGENTLINKOPS_BROWSER_EGRESS=direct-diagnostic` with no proxy configured. The standard URL proxy has no usage meter; estimated costs are not a provider invoice cap.

Expand Down
2 changes: 1 addition & 1 deletion skills/agentlinkops-campaigns/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ If the user authorizes outreach, use their existing email tool and honor their s

When a placement is earned, save source and destination URLs and its relationship to the opportunity. Use connected monitoring tools when requested; local campaign work remains useful without cloud access. Do not promise rankings, response rates, exhaustive competitor coverage, or improvements unsupported by evidence.

For citation panels, read [the panel authoring reference](../../references/citation-panels.md). Gather real audience questions with the user's available sources and record where each came from. `agentlinkops citation panel` drafts editable JSON with a mock engine by default. Suggested questions are not evidence of search demand. Choose a live engine only after checking the user's access and budget; keep citations, mentions, competitors and locale context separate in the results.
For citation panels, read [the panel authoring reference](../../references/citation-panels.md). Gather real audience questions with the user's available sources and record where each came from. `agentlinkops citation panel` drafts editable JSON with a mock engine by default. Suggested questions are not evidence of search demand. Choose a live engine only after checking the user's access and budget; keep citations, mentions, competitors and locale context separate in the results. Starting with CLI 0.6.7, browser citation measurement defaults to accountless and does not load saved sessions. Do not ask the user to log in as a prerequisite. Anonymous failures remain unknown. The panel reference keeps required proxy admission separate from an explicitly authorized direct diagnostic.

Use the [campaign handoff recipe](../../references/recipes/qualified-campaign-handoff.md) when preparing records and reviewed drafts for the person's CRM or sender. It defines checkpoints and later readback before treating an uncertain write as complete.
19 changes: 18 additions & 1 deletion src/citations/browser/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ export function createBrowserEngine({ engineName, env = process.env, onEvent = (
if (!BROWSER_PROVIDERS.includes(engineName)) {
throw new EngineError(`unknown browser engine: ${engineName} (known: ${BROWSER_PROVIDERS.join(', ')})`);
}
const authentication = env.AGENTLINKOPS_BROWSER_AUTH ?? 'accountless';
if (!['accountless', 'saved-session'].includes(authentication)) {
throw Object.assign(new EngineError('Unsupported browser authentication policy.'), { code: 'BROWSER_AUTH_POLICY_INVALID' });
}
// The vendored resolver reads process.env directly; arbitrary launch/profile
// and extension overrides must not reintroduce ambient identity.
const assertAccountlessLaunch = () => {
if (authentication === 'accountless' && ['CAMOUFOX_EXTRA_LAUNCH_JSON', 'CAMOUFOX_ENV_JSON', 'CAMOUFOX_ADDONS', 'CAMOUFOX_ARGS', 'CAMOUFOX_FIREFOX_USER_PREFS_JSON'].some(key => env[key] || process.env[key])) {
throw Object.assign(new EngineError('Accountless measurement does not accept custom browser launch or extension overrides.'), { code: 'BROWSER_AUTH_POLICY_INVALID' });
}
};
assertAccountlessLaunch();
const identity = { engine: engineName, provider: 'web-own-browser', model: engineName };
const sessionDir = runtime.sessionDir ?? join(homedir(), '.agentlinkops', 'citations', 'sessions');
const load = runtime.loadModule ?? lazy;
Expand Down Expand Up @@ -110,6 +122,7 @@ export function createBrowserEngine({ engineName, env = process.env, onEvent = (
const proxy = admission.proxy;
await event(proxy?'egress: proxy-required':'egress: direct-diagnostic');

assertAccountlessLaunch();
const options = await resolveCamoufoxLaunchOptions({
provider: engineName,
proxy: proxy ?? undefined,
Expand All @@ -123,6 +136,7 @@ export function createBrowserEngine({ engineName, env = process.env, onEvent = (
const launchOpts = { ...options, executablePath: options.executablePath };
if (proxy) launchOpts.proxy = proxy;
admission.assertReady();
assertAccountlessLaunch();
const browser = await fw.launch(launchOpts);

browserHandle = {
Expand All @@ -139,6 +153,8 @@ export function createBrowserEngine({ engineName, env = process.env, onEvent = (
}

async function loadStorageState() {
// Accountless measurement never opens session files, even when they exist.
if (authentication === 'accountless') return undefined;
try {
const { readFile } = await load('node:fs/promises');
return JSON.parse(await readFile(join(sessionDir, `${engineName}.json`), 'utf8'));
Expand Down Expand Up @@ -362,7 +378,7 @@ export function createBrowserEngine({ engineName, env = process.env, onEvent = (
},
};
const sample=engine.run.bind(engine),closeBrowser=engine.close.bind(engine);
const policyCodes=new Set(['PROXY_REQUIRED','PROXY_POLICY_INVALID','PROXY_POLICY_CONFLICT','PROXY_CONFIGURATION_INVALID','PROXY_ACQUIRE_FAILED','PROXY_SUPPLIER_INVALID','PROXY_LEASE_INVALID','PROXY_LEASE_EXPIRED','PROXY_QUARANTINE_FAILED','PROXY_RELEASE_FAILED','PROXY_RECEIPT_FAILED','PROXY_CUSTODY_UNAVAILABLE','BROWSER_CLEANUP_UNCONFIRMED','BROWSER_RUN_IN_PROGRESS']);
const policyCodes=new Set(['BROWSER_AUTH_POLICY_INVALID','PROXY_REQUIRED','PROXY_POLICY_INVALID','PROXY_POLICY_CONFLICT','PROXY_CONFIGURATION_INVALID','PROXY_ACQUIRE_FAILED','PROXY_SUPPLIER_INVALID','PROXY_LEASE_INVALID','PROXY_LEASE_EXPIRED','PROXY_QUARANTINE_FAILED','PROXY_RELEASE_FAILED','PROXY_RECEIPT_FAILED','PROXY_CUSTODY_UNAVAILABLE','BROWSER_CLEANUP_UNCONFIRMED','BROWSER_RUN_IN_PROGRESS']);
const safeError=cause=>{
const code=policyCodes.has(cause?.code)?cause.code:'BROWSER_RUN_FAILED';
const error=Object.assign(new EngineError(code==='BROWSER_RUN_FAILED'?'Browser sample failed.':'Browser admission or cleanup failed.',{retriable:code==='BROWSER_RUN_FAILED'&&cause?.retriable===true}),{code});
Expand All @@ -384,6 +400,7 @@ export function createBrowserEngine({ engineName, env = process.env, onEvent = (
if(running)throw Object.assign(new EngineError('A browser sample is already running.'),{code:'BROWSER_RUN_IN_PROGRESS'});
running=true;let result,error,reason=null,admitted=false;
try{
assertAccountlessLaunch();
if(admission.expired)await retire('lease_expiry');
await admission.begin();admitted=true;
result=await sample(input);
Expand Down
Loading
Loading