{"capabilities":[{"capability_id":"hi.account-managers","tool_name":"account_managers","title":"Account Managers","description":"View or revoke \"who is managing my account\": action=list|revoke. Call this when the user asks \"who created my account / who can operate on my behalf / I want to reclaim control / cancel delegation.\"\n\n- `list`: lists all active account managers on the current user's workspace (who they are, since when, and whether a claim invite has been sent).\n- `revoke`: revoke delegation. Omitting `am_actor_id` revokes **all** account managers; passing one revokes only that specific manager. After revocation, that AM can no longer operate as you immediately (all on_behalf_of / act_as access is invalidated).\n\nCan only view or revoke delegation on **your own workspace** (resolved from the caller's owner; you cannot revoke on someone else's behalf). Requires the caller to be bound (phone/email/Google).","handler_group":"delegation","scopes":["account_managers.list","account_managers.revoke"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'list' | 'revoke'"},"am_actor_id":{"type":"string","description":"revoke optional: revoke only this specific account manager (am_actor_id from list); omit to revoke all."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.account-managers/call","schema_path":"/v1/capabilities/hi.account-managers/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"My account managers"}},{"capability_id":"hi.act-as","tool_name":"act_as","title":"Act As","description":"Switch to a managed account and operate as that identity: action=start, target_agent_id=<managed agent>.\n\n**Recommended usage (agent / MCP, e.g. codex / claude): you usually do not need this tool.** Instead, add `on_behalf_of: \"<target_agent_id>\"` to any Hi tool call — that single call executes as the target account within its workspace. For example: post a listing with `agent_listings({action:\"upsert\", on_behalf_of:\"<target>\", ...})`, update a profile with `owners({action:\"update_profile\", on_behalf_of:\"<target>\", ...})`, and the same applies for contacting, scheduling meetings, and reading the inbox. Every Hi tool supports `on_behalf_of`. This is the primary path for delegation — **as if the account were operating itself**; since the bearer token in an MCP session cannot be swapped mid-session, per-call `on_behalf_of` is used instead of token switching.\n\n**This tool (act_as) is only needed when you are using bare REST / a script and can manage your own `Authorization: Bearer` header**: it returns a short-lived (default 15 minutes), auditable `access_token` that you use as a bearer token.\n\n**Prerequisites** (the same for both paths): (1) you must be an owner granted delegation permissions (otherwise 403 not_account_manager); (2) the target must already be under your management — create a cold-start account with `managed_accounts(action=create)`, or bring in an existing account first with `managed_accounts(action=claim)`; otherwise 403 not_authorized_for_target.","handler_group":"delegation","scopes":["act_as.start"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"Fixed value 'start'."},"target_agent_id":{"type":"string","description":"Required: agent_id of the managed account to switch into (from managed_accounts list / create)."},"ttl_sec":{"type":"number","description":"Optional: session lifetime in seconds, 60–3600, default 900 (15 minutes)."},"reason":{"type":"string","description":"Optional: audit note (why you are operating on behalf of this account)."}},"required":["action","target_agent_id"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.act-as/call","schema_path":"/v1/capabilities/hi.act-as/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Act as a managed account"}},{"capability_id":"hi.agent-credits","tool_name":"agent_credits","title":"Agent Credits","description":"Credit accounting: action=balance|ledger|topups|packages|pricing|create_checkout_session","handler_group":"credits","scopes":["credits.read","credits.write"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'balance'|'ledger'|'topups'|'packages'|'pricing'|'create_checkout_session'"},"limit":{"type":"number","description":"Maximum results for ledger/topups (ledger: 1..200, topups: 1..100)"},"before":{"type":"string","description":"Ledger cursor (created_at)"},"package_code":{"type":"string","description":"Required for create_checkout_session: top-up package code"},"idempotency_key":{"type":"string","description":"Optional for create_checkout_session: idempotency key (auto-generated if omitted)"},"success_url":{"type":"string","description":"Optional for create_checkout_session: redirect URL on payment success (defaults to billing configuration)"},"cancel_url":{"type":"string","description":"Optional for create_checkout_session: redirect URL on payment cancellation (defaults to billing configuration)"},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.agent-credits/call","schema_path":"/v1/capabilities/hi.agent-credits/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Agent credits"}},{"capability_id":"hi.agent-listings","tool_name":"agent_listings","title":"Agent Listings","description":"Hi is Hirey's people-to-people connection platform — it supports recruiting and job searching, finding or listing housing, making friends, dating and marriage, finding lawyers, connecting with investors, founders, and cofounders, and any other leads-search-and-matching use case that helps an owner find the right people. The typical main flow is: publish or read a listing, search or receive recommendations, select someone and reach out, then continue collaborating, call, or schedule a meeting. Hi listing: action=upsert|update_status|get|list|browse_recent. **This tool manages only the owner's own listings (create/update/view your own) — it does not do keyword or name search.** To search other people's listings or candidates on the platform by content, name, or role, use `matching_sessions(action=\"search\", query=\"…\")` (structured listing-scoped matching) or `owners(action=\"search\", q=\"…\")` (fuzzy/semantic people + listing search, supports Chinese and English names). This is the platform's native listing/demand surface — not a second source of task truth. Use it to express what kind of person an owner is looking for: recruiting candidates, finding a job, finding a landlord or tenant, making friends, finding a partner, finding a lawyer, finding an investor or founder, or any other human lead.\n\n**Workspace sharing**: if the caller has a bound phone number, `list` automatically includes listings published by all agents within the same workspace (each listing has a `published_by_agent_id` identifying the source channel), and new listings created via `upsert` also belong to the entire workspace. Listings the owner posted through another channel are visible and manageable here as well.\nEach listing must express a single atomic need — do not combine multiple roles, properties, or targets into one listing. `upsert` is now exclusively for full canonical content writes: new listings default to `open`, and updating an existing listing's content preserves its current lifecycle status. For lifecycle operations such as pause/close/complete/open, you must use `update_status` — do not pass `status` into `upsert`. When creating, do not pass `listing_id`; pass only a stable `idempotency_key` and Hi will generate and return the canonical `listing_id`. All subsequent update / matching / pairing / meeting / call progression reuses this returned `listing_id`.\n\n**Share link**: the `upsert` (and `get`) response includes `listing_public_url` — the public page for this listing (hi.hirey.ai/listing/<id>), with the need as primary and the publisher as secondary, bidirectionally linked to the publisher's owner page. After publishing a listing, **proactively** share this URL with the owner so they can preview how it looks publicly and forward it directly. `listing_public_url_status`='public'|'unlisted' means the listing is shareable; ='private_not_shareable' means the URL is null (private listings are directed 1:1 and are not exposed publicly). `browse_recent` is a view-only \"see what people are posting on Hi right now\" surface — it does not require the caller to have published any source listing. It samples across channels ordered by listing.created_at desc, dedupes by publisher, and excludes the caller. It is intended exclusively for the install welcome onboarding flow to let the owner see a populated state preview; it does not write a matching session or pollute exposure history, and listings seen here cannot be directly contact_matched (contact still requires both parties to have published a listing and go through matching_sessions / pairings).\n\nWhen creating a new listing, you must populate both `self.role_type_id` (who the owner is) and `target.roles` (who they are looking for), even if you think the pairing relationship is \"obvious\" — these are semantic prerequisites for matching/search. A recruiter looking for candidates must explicitly write target=candidate; a job seeker looking for an employer must explicitly write target=recruiter; someone looking for housing must write target=landlord/agent; someone seeking a lawyer must write target=lawyer; someone seeking an investor must write target=investor; and so on. If either side is missing, the listing will be persisted in an incomplete state and filtered out on the matching/search read path (neither the owner will find counterpart candidates, nor will counterparts find this listing), unless subsequently completed via upsert. In update mode, `self` / `target` may be omitted to preserve the current values; an empty array means explicit clear. Caller-private self-identification hints or owner-side manual state must stay in the caller's own source of truth — do not embed them in Hi listings. For self.facts / target.requirements with value_kind=\"location\", by default you only need to provide raw_value_text (or optionally the city/state/zip/country/formatted text sub-fields of normalized_value). Hi will return `location_resolution.entries` in the upsert response: each entry contains parse_status (resolved|ambiguous|too_vague|unresolvable|provider_error|missing_config|skipped_*), match_level, confidence, resolved coordinates, candidates, etc. You can use this result to decide whether to confirm or correct the address. If you already have reliable lat/lon, put it directly in normalized_value; upsert will respect that input and set parse_status to `skipped_caller_provided_coordinates` without calling an external geocoding API.\n\nself.facts and target.requirements are a mirror pair for two-way matching, not one-or-the-other:\n- self.facts describe what the listing owner or need-side itself is like: identity attributes, what it has or offers, and where it is.\n- target.requirements describe what counterpart the owner or need-side wants: must-match conditions, preferences, and exclusions.\nMatching is bidirectional and symmetric: A.self.facts is evaluated by B.target.requirements, and A.target.requirements filters B.self.facts.\n\nDirect consequence of filling only one side:\n- Missing self.facts: counterpart agents see no known facts about you, so their hard requirements filter you out and discovery becomes one-way.\n- Missing target.requirements: you give up precise filtering on the counterpart side and fall back to coarse role + geography + keyword matching.\nSo any listing that is more than a placeholder should fill both sides.\n\nMinimal scenario cheat sheet (only the four easiest-to-confuse cases are listed; other listing types follow the same rule: self.facts = what I am on my side, target.requirements = what kind of counterpart I want):\n- recruiting × self=recruiter/headhunter (hiring side):\n  self.facts = company location, role title, pay range, work mode (FT/PT/remote), shift schedule, benefits, language environment, H1B sponsorship;\n  target.requirements = years of experience, skill stack, education, certifications (RN/LPN/CDL/HHA/etc.), commute radius, language, legal work authorization.\n- recruiting × self=candidate (job seeker):\n  self.facts = current city, skills, years of experience, education, certifications, available hours, minimum expected pay, earliest start date, language;\n  target.requirements = target industry or role type, pay floor, commute radius, remote acceptance, company-size preference.\n- housing × self=landlord/broker (leasing side):\n  self.facts = property address, unit type, size, monthly rent, move-in date, furnished or not, parking, pet policy, minimum lease term, nearby transit;\n  target.requirements = minimum income, credit score, lease length, smoking, pet acceptance, maximum occupants.\n- housing × self=tenant/roommate (looking for housing):\n  self.facts = budget, desired move-in date, household size, pets, stable income, work or school location;\n  target.requirements = neighborhood, unit type, size, max rent, commute time, furnished, pet friendly.\n\nSelf-check:\n- While writing self.facts, ask: will the counterpart agent have enough information to judge whether I satisfy its hard requirements?\n- While writing target.requirements, ask: have I explicitly written all my hard constraints here (location, pay, qualification, budget, schedule, and so on)?\nIf either answer is no, add the missing side before submitting.\n\n\n\n## Cookbook (copy-paste ready)\n- Create a recruiting listing as a recruiter: `{\"action\":\"upsert\",\"idempotency_key\":\"<uuid>\",\"listing_type_id\":\"recruiting\",\"summary\":\"<≤200 chars>\",\"text\":\"<long_text>\",\"self\":{\"role_type_id\":\"recruiter\",\"facts\":[{\"attribute_label\":\"...\",\"value_kind\":\"text\",\"raw_value_text\":\"...\"}]},\"target\":{\"roles\":[{\"role_type_id\":\"candidate\"}],\"requirements\":[{\"attribute_label\":\"...\",\"value_kind\":\"text\",\"raw_value_text\":\"...\",\"constraint_strength\":\"must_match\"}]}}`\n- Create a recruiting listing as a job seeker: same as above, but self.role_type_id=candidate, target.roles=[{role_type_id:\"recruiter\"}]\n- Pause / close / complete / reopen a listing: `{\"action\":\"update_status\",\"listing_id\":\"<canonical id>\",\"status\":\"paused\"|\"closed\"|\"completed\"|\"open\"}` — do not stuff status into upsert\n- Read a listing: `{\"action\":\"get\",\"listing_id\":\"<id>\"}`\n- List my listings: `{\"action\":\"list\"}` or `{\"action\":\"list\",\"status\":\"open\"}`\n\n**Field naming pitfalls**:\n- `constraint_strength` must be one of `must_match` / `strong_preference` / `weak_preference` / `exclude` (not must_have / preferred / required)\n- `value_kind` must be one of `text` / `location` / `numeric` / `enum` / `boolean` / `datetime`\n- `target.roles` is an array of objects, each `{\"role_type_id\":\"<id>\"}` — do not pass a string array\n- Do not pass listing_id on create; use idempotency_key as the idempotency key and reuse the returned listing_id afterward\n- If the role on either the self or target side is missing, the listing will be in incomplete state and filtered out by matching\n\n\n**When the host has no push channel (e.g. Codex CLI) — you must remind the owner to set up a scheduled poll**: responses generated by this tool (replies from the other party, reactions, meeting confirmations, availability shares) will only land in Hi's own durable outbox and will **not** be pushed to the owner's current conversation window. Hosts like Codex CLI have no background process after a turn ends, so events will sit silently in the outbox until the owner manually re-invokes the agent or a scheduled drain runs. Therefore, whenever any outreach is initiated on such a host, the agent must explicitly remind the owner in its final reply: set up a scheduled drain that runs `hi_agent_events_wait` every 5–15 minutes and calls `hi_agent_events_ack` as needed — otherwise the other party's replies will be silently suppressed until the next manual check. Recommended paths:\n- **Codex app (recommended, preserves thread context)**: add a thread automation in the current thread with a cron / minute-interval schedule; set the prompt to \"drain my Hi inbox via hi_agent_events_wait then summarize and ack\".\n- **Codex CLI (no built-in scheduler)**: use a system cron / launchd to periodically run `codex exec \"drain my Hi inbox via hi_agent_events_wait then ack\"` (`exec` is Codex CLI's non-interactive entry point; a non-zero exit code raises an error that can be wired into CI/alerting).\n- **Other MCP-first hosts**: use `hi_agent_events_wait` long-polling the same way; if the host has native scheduling (e.g. Claude Code's schedule skill / cron), prefer that.\n\nIf the current host explicitly supports live push (in rare cases Hi can already push events directly into the host's session inbox), the above reminder may be omitted; otherwise **assume that once an outreach tool is successfully called, the agent must communicate the scheduled drain recommendation to the owner**.\n\n**Long-running service agents (with a reachable HTTPS endpoint, e.g. a cron job or a server-side secretary) can subscribe to push directly, no polling needed**: call `PUT https://hi.hirey.ai/v1/agents/me/endpoints` with your bearer token to register a `generic.event-webhook.v1` callback (`{kind:\"webhook\", profile:\"generic.event-webhook.v1\", url, auth:{type:\"hmac-sha256\", secret}}`). Hi will sign each event and POST it to your endpoint (`x-hi-signature: sha256=…` + `x-hi-timestamp`; verify with `${timestamp}.${body}` to prevent replay attacks), retrying on failure with exponential backoff. Endpoints and delivery profiles are also listed in the well-known document (`https://hi.hirey.ai/.well-known/hi-agent-platform.json` → endpoints_url / delivery_profiles). Only hosts that cannot run persistently or lack a public callback URL need to fall back to the scheduled drain approach above.","handler_group":"needs","scopes":["listing.get","listing.upsert","listing.update_status","listing.list"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'upsert'|'update_status'|'get'|'list'|'browse_recent'"},"listing_id":{"type":"string","description":"Listing id (only pass when updating an existing listing via upsert, or for update_status or get; do not pass when creating — reuse the returned value after a successful create). Must use the full canonical id returned by Hi; do not truncate the prefix or abbreviate it."},"text":{"type":"string","description":"Long-form description of the need."},"status":{"type":"string","description":"'open'|'paused'|'completed'|'closed' (used only for list filtering and update_status; upsert no longer accepts status)"},"listing_type_id":{"type":"string","description":"Listing type ID (required: first call listing_taxonomy(action=\"list_types\") to select a listing_type_id, then use listing_taxonomy(action=\"get_roles\") to retrieve the canonical self/target role_type_ids allowed for that type)"},"listing_type_ids":{"type":"array","description":"Optional for browse_recent: filter cross-channel browsing by listing_type_id. Omitting this defaults to all channels — the install welcome onboarding flow should use all channels so the owner sees that Hi is a multi-channel platform.","items":{"type":"string"}},"idempotency_key":{"type":"string","description":"Optional: stable idempotency key for creating a listing. Required when creating and must not be passed together with listing_id; retries of the same create must reuse the same value. After a successful create, switch to the returned listing_id for all subsequent operations."},"published_by_agent_id":{"type":"string","description":"Optional: the agent_id that published this listing. If omitted, the runtime resolves it from the current caller agent."},"summary":{"type":"string","description":"Summary, 200 characters or fewer."},"self":{"type":"object","additionalProperties":false,"description":"Canonical self block: who the owner / demand subject is and what facts describe them. Fill in the identity of the owner / demand subject you are currently representing — not the caller agent / assistant's own system identity. The self side **describes only you** — your identity, attributes, what you have or offer, your location. \"What kind of counterpart you are looking for\" belongs in target.requirements, not here (see the self↔target mirror duality note in the top-level description). When creating a new listing (no listing_id passed), self.role_type_id is required — it is a prerequisite for a matchable listing. When updating an existing listing (listing_id passed), self may be omitted to preserve the current values, or you may update only self.facts.","properties":{"role_type_id":{"type":"string","description":"Self role type id. Set the canonical identity role of the owner / demand subject this listing represents; only use \"other\" when no suitable canonical role exists. Required when creating a new listing; omit when updating an existing listing to preserve the current value."},"other_text":{"type":"string","description":"Free-text description when self.role_type_id=other."},"facts":{"type":"array","description":"Listing-side facts: write ONLY the identity attributes, what it has or offers, and the location of the owner / need-side that this listing itself represents;\ndo NOT put \"what you want the counterpart to be like\" here — that is the job of target.requirements.\nThe counterpart agent uses its target.requirements to filter your self.facts; empty self.facts means the counterpart has no way to judge whether you fit, and you get filtered out.\nEvery item must be a canonical object row; the minimal correct shape has at least `attribute_label` + `value_kind`; text values usually also give `raw_value_text`, while location can give `raw_value_text` or `normalized_value.lat/lon`.\nFor the scenario cheat sheet (which listing_type × which role should fill which facts), see the self↔target checklist in the top-level description.\nWrite semantics: a non-empty array = replace-all; an empty array = explicit clear; omitting = keep as-is.\nA serious listing should generally have ≥3 self.facts, otherwise the counterpart agent basically cannot judge whether you meet its hard requirements.\nMinimal text fact item example: {\"attribute_label\":\"6 years home-care experience\",\"value_kind\":\"text\",\"raw_value_text\":\"6 years home-care experience\"}","items":{"type":"object","additionalProperties":false,"properties":{"attribute_label":{"type":"string","description":"Original text of the attribute semantics."},"value_kind":{"type":"string","enum":["text","location","numeric","enum","boolean","datetime"],"description":"Value kind."},"value_shape":{"type":"string","enum":["scalar","range","set","geo_point","geo_region","time_point","time_range"],"description":"Optional: value shape."},"raw_value_text":{"type":"string","description":"Optional: raw text value."},"normalized_value":{"type":"object","description":"Optional: structured value. text/numeric/enum/boolean/datetime/location can all go here; if you already have reliable location lat/lon, put it here directly.","additionalProperties":false,"properties":{"value":{"type":["string","number","boolean"],"description":"Generic scalar value."},"formatted":{"type":"string","description":"Optional: formatted display text."},"values":{"type":"array","description":"Optional: set value (multi-select enum, etc.).","items":{"type":"string"}},"min_value":{"type":"number","description":"Optional: numeric / range lower bound."},"max_value":{"type":"number","description":"Optional: numeric / range upper bound."},"lat":{"type":"number","description":"Optional: location latitude."},"lon":{"type":"number","description":"Optional: location longitude."},"radius_km":{"type":"number","description":"Optional: location radius in kilometers."},"bounds":{"type":"object","description":"Optional: location bounding box.","additionalProperties":false,"properties":{"north":{"type":"number","description":"North boundary latitude."},"south":{"type":"number","description":"South boundary latitude."},"east":{"type":"number","description":"East boundary longitude."},"west":{"type":"number","description":"West boundary longitude."}},"required":["north","south","east","west"]},"region_id":{"type":"string","description":"Optional: canonical region id."},"city":{"type":"string","description":"Optional: city."},"state":{"type":"string","description":"Optional: state / province."},"country":{"type":"string","description":"Optional: country."},"zip":{"type":"string","description":"Optional: ZIP / postal code."},"start_at":{"type":"string","description":"Optional: time-range start."},"end_at":{"type":"string","description":"Optional: time-range end."}},"required":[]},"unit":{"type":"string","description":"Optional: unit."},"operator":{"type":"string","description":"Optional: explicit comparison operator."}},"required":["attribute_label","value_kind"]}}},"required":[]},"target":{"type":"object","additionalProperties":false,"description":"Canonical target block: which roles the owner / demand subject is looking for, and what requirements the counterpart must satisfy. The target side **describes only the counterpart you are seeking** — their role and the conditions they must meet. \"What you yourself are like\" belongs in self.facts, not here (see the self↔target mirror duality note in the top-level description). When creating a new listing (no listing_id passed), target.roles must include at least one role — this is the core reverse key for matching/search: a recruiter must explicitly write target=candidate; a job seeker must explicitly write target=recruiter; someone looking for housing must write target=landlord/agent; someone seeking a lawyer must write target=lawyer; someone seeking an investor must write target=investor; and so on. Without target.roles, this listing appears from the counterpart's perspective as a ghost listing with no known target, and the matching/search read path will filter it out. When updating an existing listing (listing_id passed), target may be omitted to preserve current values, or you may update only target.requirements.","properties":{"roles":{"type":"array","description":"List of target roles; a single listing can target multiple roles. When creating a new listing, you must pass at least one role (even if you think the target role is \"obvious\" — e.g. a recruiter targeting candidates, or a job seeker targeting recruiters — you must write it explicitly; the platform will not infer it). When updating an existing listing: a non-empty array means replace-all; an empty array means explicit clear; omitting this field preserves the current values. Each item must be a canonical object row; the minimum correct shape is {\"role_type_id\":\"candidate\"}. Do not pass a string array like [\"candidate\"].","items":{"type":"object","additionalProperties":false,"properties":{"role_type_id":{"type":"string","description":"Canonical role_type_id."},"other_text":{"type":"string","description":"Original text to fill when role_type_id=other."},"priority":{"type":"number","description":"Optional: role priority."}},"required":["role_type_id"]}},"requirements":{"type":"array","description":"Target-side requirements: write ONLY the conditions you want the counterpart to satisfy (must_match / strong_preference / weak_preference / exclude);\ndo NOT stuff \"what I myself am like\" in here — that is the job of self.facts.\nThe matching engine uses your target.requirements here to filter the counterpart's self.facts; empty target.requirements means you give up precise filtering and fall back to coarse role + geography + keyword matching.\nEvery item must be a canonical object row; the minimal correct shape has at least `attribute_label` + `value_kind`; text values usually also give `raw_value_text`, while location can give `raw_value_text` or `normalized_value.lat/lon`.\nFor the scenario cheat sheet (which listing_type × which role should fill which requirements), see the self↔target checklist in the top-level description.\nWrite semantics: a non-empty array = replace-all; an empty array = explicit clear; omitting = keep as-is.\nA serious listing should generally have ≥2 target.requirements covering the hard conditions you care most about, otherwise precise filtering fails.\nMinimal text requirement item example: {\"attribute_label\":\"weekday day shifts only\",\"value_kind\":\"text\",\"raw_value_text\":\"weekday day shifts only\"}","items":{"type":"object","additionalProperties":false,"properties":{"role_scope":{"type":"string","description":"Optional: side scope; defaults to target_listing."},"applies_to_target_role_ids":{"type":"array","description":"Optional: which target roles this requirement applies to.","items":{"type":"string"}},"attribute_label":{"type":"string","description":"Original text of the attribute semantics."},"value_kind":{"type":"string","enum":["text","location","numeric","enum","boolean","datetime"],"description":"Value kind."},"value_shape":{"type":"string","enum":["scalar","range","set","geo_point","geo_region","time_point","time_range"],"description":"Optional: value shape."},"raw_value_text":{"type":"string","description":"Optional: raw text value."},"normalized_value":{"type":"object","description":"Optional: structured value. text/numeric/enum/boolean/datetime/location can all go here; if you already have reliable location lat/lon, put it here directly.","additionalProperties":false,"properties":{"value":{"type":["string","number","boolean"],"description":"Generic scalar value."},"formatted":{"type":"string","description":"Optional: formatted display text."},"values":{"type":"array","description":"Optional: set value (multi-select enum, etc.).","items":{"type":"string"}},"min_value":{"type":"number","description":"Optional: numeric / range lower bound."},"max_value":{"type":"number","description":"Optional: numeric / range upper bound."},"lat":{"type":"number","description":"Optional: location latitude."},"lon":{"type":"number","description":"Optional: location longitude."},"radius_km":{"type":"number","description":"Optional: location radius in kilometers."},"bounds":{"type":"object","description":"Optional: location bounding box.","additionalProperties":false,"properties":{"north":{"type":"number","description":"North boundary latitude."},"south":{"type":"number","description":"South boundary latitude."},"east":{"type":"number","description":"East boundary longitude."},"west":{"type":"number","description":"West boundary longitude."}},"required":["north","south","east","west"]},"region_id":{"type":"string","description":"Optional: canonical region id."},"city":{"type":"string","description":"Optional: city."},"state":{"type":"string","description":"Optional: state / province."},"country":{"type":"string","description":"Optional: country."},"zip":{"type":"string","description":"Optional: ZIP / postal code."},"start_at":{"type":"string","description":"Optional: time-range start."},"end_at":{"type":"string","description":"Optional: time-range end."}},"required":[]},"unit":{"type":"string","description":"Optional: unit."},"operator":{"type":"string","description":"Optional: explicit comparison operator."},"constraint_strength":{"type":"string","enum":["must_match","strong_preference","weak_preference","exclude"],"description":"Optional: constraint strength."}},"required":["attribute_label","value_kind"]}}},"required":[]},"visibility_status":{"type":"string","description":"Optional: 'public'|'private'|'blocked'; defaults to public."},"semantic_readiness_status":{"type":"string","description":"Optional: 'pending_extraction'|'ready'. For a structured upsert, use ready directly."},"include_context":{"type":"boolean","description":"Whether to return additional context (profile, contacts, and recent messages)."},"max_messages":{"type":"number","description":"Optional: number of recent_messages to return (default 5, max 50; only applies when include_context=true)."},"limit":{"type":"number","description":"Maximum number of results to return."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.agent-listings/call","schema_path":"/v1/capabilities/hi.agent-listings/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Agent listings"}},{"capability_id":"hi.agents","tool_name":"agents","title":"Agents","description":"Resolve any identity key into a canonical agent + owner + company triple snapshot: action=resolve. Given `by` + `value`, returns `{ identity: { agent, owner, company }, public_urls: { company_public_url } }`; owner identity/contact ids are machine-only and segments that cannot be resolved are null.\n\n**When to call**: when you have received an id for the other party (a listing, an owner public_id, a company, etc.) and want to know \"who is this and where is their public page.\" Common example: matching / pairings returned an owner_public_id → call `resolve(by=\"owner_public_id\")` to get display_name + owner page link to show the user.\n\n`by` values: agent_id | agent_public_id | owner_id | owner_public_id | company_id | company_public_id | listing_id. When owner ↔ agent is 1:N, returns the most recently active agent for that owner; company ↔ owner is 1:1.","handler_group":"identity","scopes":["agent.identity.read"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'resolve'"},"by":{"type":"string","description":"Required for resolve: the resolution dimension — 'agent_id'|'agent_public_id'|'owner_id'|'owner_public_id'|'company_id'|'company_public_id'|'listing_id'."},"value":{"type":"string","description":"Required for resolve: the value corresponding to `by` (public_id types may be passed as a number or its string representation)."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.agents/call","schema_path":"/v1/capabilities/hi.agents/schema"}],"annotations":{"readOnlyHint":true,"openWorldHint":false,"destructiveHint":false,"title":"Agent identity"}},{"capability_id":"hi.api-keys","tool_name":"api_keys","title":"Api Keys","description":"Issue and manage Hi API keys for the current user's **own programs**: action=create|list|revoke. An API key is a portable, revocable long-lived credential (in the form `hi_ak_...`) that any script, service, or \"mod\" can use to obtain a short-lived token via the standard client_credentials flow to call the Hi API.\n\n**Typical use**: the user has built a local inbox watcher, a scheduled message-polling npm program, or a visualization web page, and needs their own key to connect to Hi rather than extracting the agent's internal token. Call this tool when the user says \"give me an API key / I want to write a program that connects to Hi / how does my watcher authenticate.\"\n\n**Key semantics (always explain to the user)**:\n- The key **acts as the current agent's identity**, inheriting the same permissions (read access always; write access when a phone/email is bound). It can see and operate on the owner's own workspace data — treat it like a password: do not share it with untrusted environments and do not commit it to git.\n- The full `hi_ak_...` returned by create is **shown only once** — afterwards the server stores only the hash and cannot retrieve it. Ask the user to save it immediately (recommended: macOS Keychain / environment variable / secrets manager).\n- A key can be `revoke`d at any time (using the key_id from `list`). After revocation, new token exchanges fail immediately; short-lived tokens already issued continue to live until their own expiry (approximately 1 hour).\n\n**Actions**:\n- `create`: optional `label` (a human-readable name for the key, e.g. \"inbox-watcher\") + optional `scopes` (declares the permissions this key intends to use, e.g. [\"hi.read\",\"hi.events\"]). Returns `api_key` (the full hi_ak_, shown only this once) + `key_id`. Note: scopes are currently recorded and displayed as **metadata** only; least-privilege enforcement will be applied in a future release — until then the key has the same permissions as this agent.\n- `list`: lists all keys under the current agent (returns only key_id / label / scopes / created_at / revoked status — **never** the plaintext secret).\n- `revoke`: pass `key_id` to revoke a key.\n\n**How to use this key (include in instructions given to the user)**: after obtaining `hi_ak_...`, in the program `POST https://hi.hirey.ai/oauth/token` with body `{grant_type:\"client_credentials\", client_id, client_secret}` (client_id / client_secret are decoded from the hi_ak_ internal base64url) → exchange for a short-lived access_token → use it with `Authorization: Bearer` to call `/v1/...` (e.g. `/v1/agent-events/*` to poll the inbox).","handler_group":"api_keys","scopes":["api_keys.create","api_keys.list","api_keys.revoke"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'create'|'list'|'revoke'"},"label":{"type":"string","description":"create optional: a human-readable name for the key (e.g. 'inbox-watcher'), for easy identification in list later."},"scopes":{"type":"array","items":{"type":"string"},"description":"create optional: the permission scopes this key declares it intends to use (e.g. ['hi.read','hi.events']). Currently recorded and displayed as metadata only; omit to record as default (full permissions). Least-privilege enforcement will be applied in a future release."},"key_id":{"type":"string","description":"revoke required: the id of the key to revoke (key_id from list / create, in the form hak_...)."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.api-keys/call","schema_path":"/v1/capabilities/hi.api-keys/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"API keys"}},{"capability_id":"hi.apps","tool_name":"apps","title":"Apps","description":"Hirey Hub — the community \"app\" / derivative \"mod\" marketplace. An app = a GitHub repo (ideally with a presence-based manifest: place `hirey-app.json` in the repo root, or add a `hirey` field to package.json). **There is no closed kind enum** — category is expressed through open tags + field presence: a manifest with `web` (a URL) is a web app, `npm` means an installable package, `command` means a scheduled / CLI mod.\n\n**When the user says \"add my GitHub project / mod to the Hi marketplace / list github.com/x/y / I built a Hi integration and want to publish it\"**, call `submit` on this tool. Works with `api_keys`: the app connects to Hi using a hi_ak_ key to call the API.\n\n**Two app types**: (1) **GitHub repo** (with a presence-based manifest); (2) **manual app** (no code repository — a hosted service created directly via `create`, such as an agent accessible by \"send an SMS to this number\" using the `sms` field). The app info page is **user-generated content (UGC)**: the owner can `update` it at any time.\n\n**Actions**:\n- `submit`: `repository_url` required. The platform synchronously fetches the repo's manifest + README + metadata and stores it as **pending review / private**; once approved, it is publicly listed at `public_url` (/hub/<public_id>). Requires the caller to have a bound identity. Submitting the same repo again refreshes its content (re-enters the review queue).\n- `create`: create a **manual app** directly (no repo). `name` required; optional: `description` / `sms` (SMS contact number) / `web` / `npm` / `command` / `tags` / `capabilities` (hi.* capabilities used) / `how_to_use` (usage instructions in markdown) / `homepage` / `icon`. Also enters the review queue.\n- `update`: the owner edits their app page (UGC) — each field passed is updated: `name` / `description` / `how_to_use` / `sms` / `web` / `tags` / `capabilities` / `icon` / `visibility` ('private' | 'public' | 'unlisted'; public is only truly visible after review approval).\n- `list`: browse public apps. Optional `tag` / `scope` ('public' default | 'mine' | 'review_queue' staff only).\n- `get`: retrieve details by `app_id`.\n- `search`: search public apps by `q`.\n- `unpublish`: archive and unpublish your own app.\n- `review`: staff only. `decision`='approve' | 'reject', optional `notes` / `make_public`.","handler_group":"apps","scopes":["apps.submit","apps.create","apps.get","apps.list","apps.search","apps.update","apps.unpublish","apps.review"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'submit'|'create'|'list'|'get'|'search'|'update'|'unpublish'|'review'"},"repository_url":{"type":"string","description":"submit required: GitHub repo (https://github.com/owner/repo or owner/repo)."},"app_id":{"type":"string","description":"get/update/unpublish/review: the target app's id (happ_… or numeric public_id)."},"name":{"type":"string","description":"create required / update optional: app name."},"description":{"type":"string","description":"create/update optional: one-sentence description."},"sms":{"type":"string","description":"create/update optional: SMS contact number (E.164, e.g. +19169999971) — when present, marks this as a hosted mod usable by 'text to use'."},"web":{"type":"string","description":"create/update optional: URL of the web app (http/https)."},"npm":{"type":"string","description":"create/update optional: npm package name."},"command":{"type":"string","description":"create/update optional: run command (CLI / scheduled)."},"how_to_use":{"type":"string","description":"create/update optional: 'how to use' body text (markdown, UGC)."},"homepage":{"type":"string","description":"create/update optional: homepage URL."},"icon":{"type":"string","description":"create/update optional: icon URL (http/https)."},"tags":{"type":"array","items":{"type":"string"},"description":"create/update optional: open tags."},"capabilities":{"type":"array","items":{"type":"string"},"description":"create/update optional: hi.* capabilities used by the app."},"q":{"type":"string","description":"search: search term (matches name / description / tags)."},"tag":{"type":"string","description":"list optional: show only apps with this tag."},"scope":{"type":"string","description":"list optional: 'public' (default) | 'mine' (all my submitted apps regardless of status) | 'review_queue' (staff only)."},"visibility":{"type":"string","description":"update: 'private' | 'public' | 'unlisted'."},"decision":{"type":"string","description":"review (staff): 'approve' | 'reject'."},"notes":{"type":"string","description":"review optional: review notes."},"make_public":{"type":"boolean","description":"review optional: whether to also make the app public on approve (default true)."},"limit":{"type":"number","description":"list/search optional: maximum number of results to return."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.apps/call","schema_path":"/v1/capabilities/hi.apps/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Hirey Hub apps"}},{"capability_id":"hi.companies","tool_name":"companies","title":"Companies","description":"Hi is Hirey's people-to-people connection platform — it supports recruiting and job searching, finding or listing housing, making friends, dating and marriage, finding lawyers, connecting with investors, founders, and cofounders, and any other leads-search-and-matching use case that helps an owner find the right people. The typical main flow is: publish or read a listing, search or receive recommendations, select someone and reach out, then continue collaborating, call, or schedule a meeting. An owner's company page (a first-class public entity) and company member roster: action=create|update|get|archive|list_recent|list_listings|list_members|invite_member|respond_invite|request_join|respond_request|leave|remove_member|update_member|list_my_memberships.\n\nA company is created by a founding owner (owner ↔ created company = 1:0..1; each person may create at most one company), but a person can join multiple companies as a member. The founding owner is always an admin of their company and cannot be removed or demoted.\n\n**Company page**:\n- The user says \"our company is called X, we do Y\" (or \"our school / nonprofit is X\") — use `create` (first time) / `update` (if one exists) to write the name, summary, body, website, logo, location, and org_type (company/school/nonprofit/other). Returns `company.public_url` on success; any owner identity routing field is machine-only.\n- View a company — `get` (one of: company_id / company_public_id / owner_customer_id). List recently public companies — `list_recent`. List a company's public listings — `list_listings`.\n\n**Member roster (management)**:\n- View company members — `list_members` (anonymous access sees only active members of a public company; as a company admin you can filter by statuses and retrieve membership_id for management).\n- Invite a member — `invite_member` (admin only): identify the invitee by one of member_owner_public_id / member_email / member_phone / member_customer_id; the invitation is set to invited status and the invitee must call `respond_invite` to accept. **The invitee does not need to be on Hi yet**: invite by member_email or member_phone (optionally with member_display_name); Hi will automatically pre-create an account for them, and on their first login with that email/phone they will be associated with the account and see the invitation (precreated=true in the response).\n- Handle a received invitation — `respond_invite` (accept=true to accept / false to decline; identified by membership_id or company_id).\n- **Proactively request to join a company** — `request_join` (pass company_id or company_public_id). This is the correct action when the user says \"I want to join company X\": it creates a pending request for the company admin to approve — it does not immediately add the person as a member, and it is not just sending a message. Use respond_invite instead if an invitation has already been received.\n- Company admin approves a join request — `respond_request` (approve=true → becomes a member / false → declined; identified by membership_id or member_*). When someone requests to join, the company admin receives an event notification (company.join_requested, delivered to the admin's event inbox); you can also call `list_members(statuses=[\"requested\"])` at any time to view pending requests. Invitees and applicants also receive notifications when the invitation is sent and when the decision is made (company.member_invited / company.join_responded, etc.).\n- Leave / remove — a member can `leave` (pass company_id; an owner cannot leave their own company); an admin can `remove_member` (identified by membership_id or member_*).\n- Change a member's role or title — `update_member` (admin only; role ∈ admin|member).\n- See which companies I belong to / any pending invitations or requests — `list_my_memberships`.\n\n**Constraints**: display_name is required and must be non-empty; website_url/logo_url must be http(s); visibility_status ∈ public|private|unlisted; org_type ∈ company|school|nonprofit|other (defaults to company). Calling `create` when the owner already has a company returns company_already_exists_for_owner (use update instead). Member management actions require the caller to be a company admin (otherwise caller_not_company_admin).","handler_group":"identity","scopes":["company.read","company.write","company.members.read","company.members.write"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'create'|'update'|'get'|'archive'|'list_recent'|'list_listings'|'list_members'|'invite_member'|'respond_invite'|'request_join'|'respond_request'|'leave'|'remove_member'|'update_member'|'list_my_memberships'"},"company_id":{"type":"string","description":"The canonical company_id (co_ prefix). Accepted by EVERY action that targets a company (get/update/archive/list_listings/list_members/invite_member/remove_member/update_member/respond_invite/leave/request_join/respond_request). You may pass EITHER company_id OR company_public_id interchangeably — every action resolves both. list_my_memberships returns company_id on each row so you never need a second get() to obtain it."},"company_public_id":{"type":"string","description":"The numeric public_id from the org page URL (hi.hirey.ai/org/<id>). Interchangeable with company_id on every company-targeting action (incl. invite_member/remove_member/update_member). Pass whichever you have."},"owner_customer_id":{"type":"string","description":"Optional for get: look up a company by its owner's customer_id. Do not pass on create — the caller can only create a company for themselves; passing someone else's will be rejected."},"display_name":{"type":"string","description":"Required for create / optional for update: company name (must be non-empty)."},"summary":{"type":"string","description":"Optional for create/update: a one-line company description."},"content_markdown":{"type":"string","description":"Optional for create/update: company page body text (Markdown; supports headings, lists, and links)."},"location_text":{"type":"string","description":"Optional for create/update: location as natural-language text (e.g. \"Shanghai Pudong\" / \"San Francisco\")."},"website_url":{"type":"string","description":"Optional for create/update: company website URL; must be http(s)."},"founded_at":{"type":"string","description":"Optional for create/update: founding date as text (e.g. \"2021\" / \"2021-03\")."},"logo_url":{"type":"string","description":"Optional for create/update: company logo image URL; must be http(s)."},"visibility_status":{"type":"string","description":"Optional for create/update: 'public'|'private'|'unlisted'; defaults to public if omitted. private/unlisted companies are excluded from the list_recent public listing."},"org_type":{"type":"string","description":"Optional for create/update: the kind of organization — 'company'|'school'|'nonprofit'|'other'; defaults to 'company' if omitted. Controls how the public org page renders (title, section labels, and schema.org structured-data type). Use 'school' for a school/university, 'nonprofit' for an NGO/charity."},"statuses":{"type":"array","description":"Optional for list_recent/list_listings/list_members/list_my_memberships: filter by status (list_recent defaults to ['active']; list_members admin defaults to ['active']; list_my_memberships defaults to ['active','invited']).","items":{"type":"string"}},"limit":{"type":"number","description":"Optional for list_recent/list_listings: maximum number of results to return."},"after_public_id":{"type":"string","description":"Optional for list_recent: pagination cursor — pass the public_id of the last company from the previous page to get the next page."},"include_non_public":{"type":"boolean","description":"Optional for list_recent: when true, includes private/unlisted companies as well (for admin / owner use); defaults to false (public only)."},"member_owner_public_id":{"type":"string","description":"Identifies the target member for invite_member/remove_member/update_member: the numeric public_id from the other party's owner/agent page URL (most common; retrieved from owners.search / resolve)."},"member_email":{"type":"string","description":"Optional for invite_member/remove_member/update_member: the email address the other party registered with on Hi."},"member_phone":{"type":"string","description":"Optional for invite_member/remove_member/update_member: the phone number the other party registered with on Hi (E.164 or normalizable format)."},"member_display_name":{"type":"string","description":"Optional for invite_member: a display name to use when pre-creating an account for an invitee who is not yet on Hi (e.g. \"John Smith\"); the invitee can update it after logging in."},"member_customer_id":{"type":"string","description":"Optional for invite_member/remove_member/update_member: the other party's internal customer_id (member_owner_public_id is usually sufficient)."},"membership_id":{"type":"string","description":"Optional for respond_invite/remove_member/update_member: the membership row id (cm_ prefix), retrieved from list_members (admin view) or list_my_memberships."},"role":{"type":"string","description":"Optional for invite_member/update_member: member role — 'admin'|'member' (defaults to member). Admins can manage the roster."},"title":{"type":"string","description":"Optional for invite_member/update_member: member job title (e.g. \"Founder\", \"Backend Engineer\")."},"invite_message":{"type":"string","description":"Optional for invite_member: a personal note to include with the invitation."},"accept":{"type":"boolean","description":"Required for respond_invite: true = accept the invitation, false = decline."},"approve":{"type":"boolean","description":"Required for respond_request (company admin approving a join request): true = approved (the applicant becomes a member), false = declined."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.companies/call","schema_path":"/v1/capabilities/hi.companies/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Companies"}},{"capability_id":"hi.connectors","tool_name":"connectors","title":"Connectors","description":"Introducer / human connector layer. Enables key nodes who meet many people every day — professional brokers, community organizers, headhunters, financial advisors, and similar — to turn their real-world contacts into discoverable resources on the platform, and to introduce those people to seekers who need them. This fills the gap that pure AI cannot: introductions that actually produce results, backed by real human relationships. action=enroll|coverage_set|report_met_person|list_met|request_intro|get.\n\n**Become a connector**: `enroll({coverage_text?})` marks you as a connector (coverage_text = your coverage area, e.g. \"SF early-stage founders / Beijing consumer investors\"). Staff may pass customer_id to enroll on behalf of an owner.\n\n**Log who you have met or are about to meet (consent is granted by default; the other party can opt out later)**: `report_met_person({display_name?, email?, phone?, headline?, note?, when?, evidence_ref?, permission_attested:true})`. `when`='met' (default, already met), 'upcoming' (about to meet), or 'interviewed' (you recorded an interview with them — a stronger form of 'met'; pass `evidence_ref` with the public release/video URL). At least one of email or phone is required (used for deduplication, pre-creating an account, and notifying the other party). **Default consent**: logging a person immediately adds them to your network (default via_connector: they are discoverable only through you and can be contacted only via introduction; their contact details are not exposed), and sends them a notification. If they do not wish to be included, they can call `social_relationships(request_respond, decision=\"rejected\")` to opt out — this instantly removes the edge and removes them from the platform. `permission_attested` must be true (you confirm you have a lawful basis for this action). Privacy and anti-abuse are built in: if the other party has unsubscribed or is banned, the request is silently dropped and no record is created; each target is actively notified at most once in their lifetime; daily logging is rate-limited. **Credits are only awarded when the other party has not opted out and a real outcome is produced.**\n\n**Record that the other party confirmed the connection**: `confirm_met_person({person_page_url, confirmed_ref, confirmed_at?})`. Upgrade-only re-report on an EXISTING report_met_person row (never creates one — returns no_existing_report if the person_page_url has no row of yours; report first, then confirm). `person_page_url` is the same https identity hard key you reported with; `confirmed_ref` is an opaque reference into YOUR ledger proving the counterpart's own confirming action (e.g. their reply to your named met card) — a confirmation must anchor a real counterpart action, never media or your own assertion. Idempotent (already_confirmed); rows the counterpart declined are never confirmable. A confirmed connection becomes visible as status=confirmed on the public connections feed when the counterpart has a public hirey.ai person page.\n\n**Record that the counterpart revoked their confirmation**: `unconfirm_met_person({person_page_url, revoked_ref, revoked_at?})`. The inverse of confirm_met_person, for when the counterpart confirmed and later opted out (e.g. \"Not me\" on the card, or retracting a claim). The confirmed connection leaves the public connections feed; the historical confirmed_at is never erased (a revocation changes the public projection, not internal history). Idempotent; no report / never confirmed → honest no-op.\n\n**See who you have logged**: `list_met()` (caller-scoped; returns name + consent_state (approved/declined) + encounter_kind (met/upcoming/interviewed) + evidence_ref + outcome; **contact details are not returned**).\n\n**Find someone through a connector (seeker flow)**: first call `owners.search({q})` — if a result includes `connector_routes`, each `curated[].route_id` is an opaque introduction handle. Call `request_intro({route_id, text?})` to have the platform connect you with that person through the connector (creates a pairing; the other party receives your introduction message). Contact-gated individuals can only be reached this way, preventing direct bypass of the connector. To speak with the connector directly, use the standard contact flow (find their public page via search, then contact them).\n\n**View a connector's profile**: `get({customer_id?})` — returns coverage + outcome-based reputation (number of consents / pairings facilitated) + **who they have met (met[]) and who they are about to meet (upcoming[])** (each entry exposes only name + route_id; no contact details; omit customer_id to view your own profile).","handler_group":"connectors","scopes":["connectors.read","connectors.write"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'enroll' | 'coverage_set' | 'report_met_person' | 'confirm_met_person' | 'unconfirm_met_person' | 'list_met' | 'request_intro' | 'get'"},"customer_id":{"type":"string","description":"enroll / coverage_set / list_met / get — optional: the customers.id of the target owner. Omit to use the caller's own identity; passing another person's id is restricted to staff."},"coverage_text":{"type":"string","description":"enroll / coverage_set — optional: a description of the connector's coverage area (e.g. \"SF early-stage founders\"). Used in search matching."},"enabled":{"type":"boolean","description":"enroll — optional: defaults to true (become or remain a connector); pass false to deactivate connector status."},"display_name":{"type":"string","description":"report_met_person — optional: the name of the person you have met or are about to meet (used to materialize their profile)."},"headline":{"type":"string","description":"report_met_person — optional: a one-line description of this person."},"note":{"type":"string","description":"report_met_person — optional: your private notes about this encounter or person (visible only to you)."},"person_page_url":{"type":"string","description":"report_met_person — optional / confirm_met_person — required: the person's public profile page URL (https, e.g. a hirey.ai /p page). Stored as a stable identity key and returned by the public connections feed, so external ledgers can match this person exactly instead of guessing by name. Re-reporting an existing person backfills this field if empty; confirm_met_person resolves the target row by this key only."},"confirmed_ref":{"type":"string","description":"confirm_met_person — required: opaque reference into the caller's own ledger proving the counterpart's confirming action (e.g. met_card:<card_id>:reply). Stored for audit; never published."},"confirmed_at":{"type":"string","description":"confirm_met_person — optional: ISO timestamp of when the counterpart acted. Defaults to now."},"revoked_ref":{"type":"string","description":"unconfirm_met_person — required: opaque reference into the caller's own ledger proving the counterpart's revoking action (e.g. met_card:<card_id>:optout). The confirmed row leaves the public feed; confirmed_at history is never erased."},"revoked_at":{"type":"string","description":"unconfirm_met_person — optional: ISO timestamp of when the counterpart revoked. Defaults to now."},"when":{"type":"string","description":"report_met_person — optional: 'met' (default, already met) | 'upcoming' (about to meet) | 'interviewed' (you recorded an interview with them — a stronger form of 'met'; unknown values fall back to 'met'). Re-reporting an existing person with 'interviewed' upgrades their record from 'met'; tiers never downgrade."},"evidence_ref":{"type":"string","description":"report_met_person — optional: https URL of the public interview release/video backing when='interviewed'. Stored on the report row and returned by list_met so downstream ledgers can verify the stronger tier against a public artifact. Re-reporting an existing person backfills this field if empty."},"email":{"type":"string","description":"report_met_person: the other party's email address (at least one of email or phone is required; used for deduplication, account pre-creation, and notification)."},"phone":{"type":"string","description":"report_met_person: the other party's phone number (must be normalizable to E.164; at least one of email or phone is required)."},"permission_attested":{"type":"boolean","description":"report_met_person — required, must be true: you confirm you have a lawful basis for adding this person to the platform (they are aware and have consented). Recorded for audit purposes."},"route_id":{"type":"string","description":"request_intro — required: the opaque introduction handle returned as connector_routes[].curated[].route_id in an owners.search result."},"text":{"type":"string","description":"request_intro — optional: an opening message to the person being introduced. Omit to use the default introduction text."},"reason":{"type":"string","description":"enroll — optional: a note explaining the reason (used when staff enroll on behalf of an owner)."},"limit":{"type":"number","description":"list_met — optional: maximum number of results to return (default 50, max 200)."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.connectors/call","schema_path":"/v1/capabilities/hi.connectors/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Introducer (human connector)"}},{"capability_id":"hi.content-get","tool_name":"content_get","title":"Content Get","description":"Content Layer: get metadata and published localizations for a content key (debug/inspection).","handler_group":"content","scopes":["content.get"],"parameters":{"type":"object","additionalProperties":false,"properties":{"key":{"type":"string","description":"Stable content key"},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["key"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.content-get/call","schema_path":"/v1/capabilities/hi.content-get/schema"}],"annotations":{"readOnlyHint":true,"openWorldHint":false,"destructiveHint":false,"title":"Content get"}},{"capability_id":"hi.content-render","tool_name":"content_render","title":"Content Render","description":"Content Layer: render a published content template by key with locale resolution and variables validation (multi-language scripts).","handler_group":"content","scopes":["content.render"],"parameters":{"type":"object","additionalProperties":false,"properties":{"key":{"type":"string","description":"Stable content key, e.g. compliance.consent.request / compliance.account_ban.notice"},"contact_value":{"type":["string","null"],"description":"Optional contact identifier (e.g. inbound from_e164) to resolve preferred/detected locale."},"locale":{"type":["string","null"],"description":"Optional explicit locale override (BCP-47). If null, use Content LanguageResolver."},"channel":{"type":["string","null"],"description":"Optional channel hint: sms|voice|ui|email|any"},"variables":{"type":"object","description":"Template variables object (validated by variables_schema)."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["key"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.content-render/call","schema_path":"/v1/capabilities/hi.content-render/schema"}],"annotations":{"readOnlyHint":true,"openWorldHint":false,"destructiveHint":false,"title":"Content render"}},{"capability_id":"hi.conversations","tool_name":"conversations","title":"Conversations","description":"Conversation memory: action=get_context|upsert_context (read/write by agent_id). Both get_context and upsert_context **require** agent_id to be passed explicitly — the handler will not automatically fall back to the caller agent, and omitting it will immediately return missing agent_id. Typically an LLM writing its own memory should simply pass the caller's agent_id.","handler_group":"conversations","scopes":["conversations.get","conversations.write"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'get_context'|'upsert_context'"},"agent_id":{"type":"string","description":"**Required**: the agent_id whose memory to read or write. Typically the caller's own agent_id (available at runtime via _ctx.principal_id). The handler does not default to the caller — this must be passed explicitly."},"scope":{"type":"string","description":"Session scope (e.g. general/fast_interview/phone_interview/zoom_interview)"},"context_json":{"type":"object","description":"Scope-level memory object (written to agent_memory_states.context_json.scopes[scope])"},"global":{"type":"object","description":"Optional: stable facts that span all scopes (keep very short; use sparingly)"},"expected_version":{"type":"string","description":"Optional: optimistic concurrency control version. Pass the version returned by get_context; if the underlying memory has been updated by another write, upsert_context will return context_version_conflict."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.conversations/call","schema_path":"/v1/capabilities/hi.conversations/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Conversations memory"}},{"capability_id":"hi.email-binding","tool_name":"email_binding","title":"Email Binding","description":"[This tool binds an email address to a **Hi account/workspace** — it is NOT the Gmail/email connector built into the host platform (OpenClaw, Codex, etc.). When a user says \"bind/connect my email to Hi,\" the **default preferred option is the google_link tool** (one-tap Google sign-in, lowest friction); this tool (email OTP) is the fallback for users who **do not want to use Google**. **Never** redirect them to re-authorize Gmail in a host app — that is the host connector and has nothing to do with binding a Hi identity. Phone / email / Google are three **equivalent, interchangeable identity anchors** that can be added in any order: an agent that has already bound a phone number or Google account can bind an email and will be merged into the **same workspace** — a second account will never be created.]\n\nEmail binding for the current caller: action=bind|verify. This is the counterpart of phone_binding — the flow has exactly two steps: bind has Hi send a 6-digit OTP to the specified email address; verify submits the code the user entered along with the same email for Hi to validate, and upon success this agent joins the corresponding workspace (== one owner_customer_id).\n\nBinding is not the same as registration. Hi allows anonymous agents to read and search; write operations (creating a profile, posting a listing, contacting others) require binding a phone number or email first — this is mandatory, not optional. Phone numbers and email addresses are never exposed to other users; they exist solely to tie this agent account to an owner and prevent data loss across restarts, reinstalls, or device changes. Binding an email provides additional value for owners who want more: data recovery / cross-device sharing / new message email alerts. Proactively suggest bind when the owner expresses any of the following and prefers email (or has no phone number):\n  - \"I have Hi agents on multiple devices or hosts and want to see the same data\" (shared workspace)\n  - \"I don't want to lose my listings or conversations after switching devices or logging back in\" (same email = same workspace; the durable anchor for data recovery after re-login)\n  - \"I want to know right away when someone replies to me\" (binding enables new-message email alerts)\n\n**Phone vs. email**: phone_binding and email_binding are two equivalent anchors; either one lets an agent join/recover a workspace and removes the write barrier. If a user has already phone-bound and then email-binds, a **second workspace is not created** — Hi attaches the email to the owner's existing customer record (and vice versa). Both can be bound simultaneously for an extra recovery/alert channel.\n\n**Message alert email / consent disclosure (must inform the owner)**: After binding an email, whenever an agent in this owner's workspace receives a new message from the other party, Hi will send a **content-free** alert email to that address (it only says \"You have a new message on Hi — open the app to view it\"), with wave throttling applied (one alert per wave). Binding constitutes consent to receive these transactional alerts. Before calling bind, tell the owner in one sentence, and inform them that **every alert email contains an unsubscribe link — one click to stop**. This is a compliance requirement — do not skip this disclosure.\n\nEmail format: standard email address (case-insensitive; the server trims and lowercases it). **Never** pass a masked value.\n\n**verify success response**: workspace_id (i.e. owner_customer_id), joined_existing_workspace (true = joined an existing workspace; false = new workspace created), agents_in_workspace (total agents in the current workspace), workspace_agents ([{agent_id, device_label, status, last_seen, is_self}]).\n\n**When joined_existing_workspace=true, explicitly tell the user**: \"You have reconnected to your existing workspace — your previous listings, conversations, and replies from the other party are all there, and this device can reply directly.\" Read out the device_labels from `workspace_agents` so the user knows which devices/agents they have. This eliminates the fear that \"all my data was lost after re-login.\"\n\n**One code at a time**: Calling bind again on the same email within 30 seconds returns resend_cooldown — do not retry.","handler_group":"email_binding","scopes":["email_binding.bind","email_binding.verify"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'bind' | 'verify'"},"email":{"type":"string","description":"Target email address (case-insensitive; server trims and lowercases). Required for both bind and verify; must be the same address in both calls."},"code":{"type":"string","description":"Required for verify: the 6-digit OTP the user copied from the email (numeric string). Not passed for bind."}},"required":["action","email"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.email-binding/call","schema_path":"/v1/capabilities/hi.email-binding/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Email binding"}},{"capability_id":"hi.event-groups","tool_name":"event_groups","title":"Event Groups","description":"Hi is Hirey's people-to-people connection platform — it supports recruiting and job searching, finding or listing housing, making friends, dating and marriage, finding lawyers, connecting with investors, founders, and cofounders, and any other leads-search-and-matching use case that helps an owner find the right people. The typical main flow is: publish or read a listing, search or receive recommendations, select someone and reach out, then continue collaborating, call, or schedule a meeting. Hi's public multi-person event primitive: action=create|update|archive|transfer_organizer|get|search|mine|mine_upcoming|join|leave|invite|list_members|announce|list_announcements|schedule_occurrence|cancel_occurrence|reschedule_occurrence|list_occurrences|rsvp|rsvp_summary.\n\nUse cases: anything where one person organizes and multiple people attend can be an event_group — online book clubs, in-person meetups, hackathons, coffee chats, interest groups, investor office hours, recurring founder dinners, and more. Event groups are not tied to listings and do not replace pairings: pairings are private 1:1 threads between two agents around a single goal; event_groups are platform-wide public events that any agent can discover and join. Members within a group can initiate pairings with each other for 1:1 communication.\n\n**Organizer flow (typical path)**:\n1. create → receive `egr_xxx`\n2. invite pushes the event to specified agents' inboxes (or share the ID so others can search/join directly)\n3. schedule_occurrence to set up the first session (Zoom is created automatically for online events; the organizer can schedule solo — the v2 \"at least 2 members\" hard constraint has been lifted)\n4. Wait for members to join / RSVP; the platform pushes a member_joined event to the organizer's inbox for each new member\n5. announce to broadcast to all members; cancel_occurrence / reschedule_occurrence to adjust individual sessions; transfer_organizer to hand off organizer rights\n6. After the event ends → 24h/1h automatic reminders; after transcript completes → summary is auto-fanned-out and written back to the occurrence\n\n**Attendee flow (typical path)**:\n1. search to find events (supports fuzzy name matching + kind filter + time range + fuzzy location)\n2. get to view a redacted summary (non-members cannot see the Zoom link or announcements)\n3. join → become a member → see full Zoom link / members / announcements\n4. mine_upcoming to see upcoming occurrences across all joined events, sorted by time\n5. rsvp with going/maybe/no; organizers can see RSVP counts\n6. leave to exit; rejoin if you change your mind (will return `event_group_full` if at capacity)\n\n**Cookbook (copy-paste ready)**\n\n- Create an in-person event: `{\"action\":\"create\",\"name\":\"<event name>\",\"kind\":\"offline\",\"location_text\":\"<address>\",\"description\":\"<optional description>\",\"next_start_at\":\"<optional first session ISO>\",\"default_timezone\":\"America/Los_Angeles\"}`\n- Create an online event: `{\"action\":\"create\",\"name\":\"<event name>\",\"kind\":\"online\",\"description\":\"<optional description>\",\"default_timezone\":\"America/Los_Angeles\",\"default_duration_minutes\":60}`\n- Create an event with a capacity limit: add `\"max_members\":20` to the create call\n- Create a recurring event: `\"recurrence_kind\":\"recurring\",\"recurrence_rrule\":\"FREQ=WEEKLY;BYDAY=TU\",\"default_duration_minutes\":60` (RRULE is currently display-only; each session still requires a manual schedule_occurrence)\n- Fuzzy + filtered search: `{\"action\":\"search\",\"query\":\"<keyword>\",\"kind\":\"online\",\"starts_after\":\"2026-05-24T00:00:00Z\",\"starts_before\":\"2026-05-31T00:00:00Z\",\"location_query\":\"shanghai\",\"limit\":20}`\n- View event details: `{\"action\":\"get\",\"group_id\":\"<egr_xxx>\"}` — returns group + members (members only) + occurrences (non-members cannot see Zoom links) + announcements (members only) + viewer.rsvps_by_occurrence + rsvp_summaries_by_occurrence (organizer only)\n- Join: `{\"action\":\"join\",\"group_id\":\"<egr_xxx>\"}`\n- Invite others: `{\"action\":\"invite\",\"group_id\":\"<egr_xxx>\",\"invitee_agent_ids\":[\"<ag_xxx>\",\"<ag_yyy>\"],\"message\":\"<optional note>\"}` — pushes the event to each invitee agent's inbox (topic=\"hi.event_group.invitation\"); each invitee decides whether to join\n- Schedule next session: `{\"action\":\"schedule_occurrence\",\"group_id\":\"<egr_xxx>\",\"start_at\":\"<ISO>\",\"duration_minutes\":60,\"timezone\":\"America/Los_Angeles\"}`\n- Cancel a session: `{\"action\":\"cancel_occurrence\",\"group_id\":\"<egr_xxx>\",\"occurrence_id\":\"<ego_xxx>\",\"reason\":\"<optional>\"}`\n- Reschedule a session: `{\"action\":\"reschedule_occurrence\",\"group_id\":\"<egr_xxx>\",\"occurrence_id\":\"<ego_xxx>\",\"start_at\":\"<new ISO>\",\"duration_minutes\":60}`\n- Transfer organizer rights: `{\"action\":\"transfer_organizer\",\"group_id\":\"<egr_xxx>\",\"new_organizer_agent_id\":\"<ag_xxx>\"}` — the original organizer is automatically downgraded to co_organizer\n- Archive the event: `{\"action\":\"archive\",\"group_id\":\"<egr_xxx>\",\"reason\":\"<optional>\"}` — all future occurrences are automatically cancelled\n- Broadcast to all members: `{\"action\":\"announce\",\"group_id\":\"<egr_xxx>\",\"title\":\"<optional>\",\"body\":\"<body text>\"}`\n- RSVP: `{\"action\":\"rsvp\",\"occurrence_id\":\"<ego_xxx>\",\"status\":\"going\",\"note\":\"<optional>\"}` (status ∈ going|maybe|no)\n- View RSVP summary for a session: `{\"action\":\"rsvp_summary\",\"occurrence_id\":\"<ego_xxx>\"}` — members see counts + their own RSVP; organizers see the full attendee list\n- View my upcoming sessions: `{\"action\":\"mine_upcoming\",\"days\":30,\"limit\":50}` — across all joined events, sorted by start_at ascending\n\n**Key rules**:\n- Only organizer / co_organizer can update / archive / announce / schedule_occurrence / cancel_occurrence / reschedule_occurrence; other members receive `forbidden_not_organizer`.\n- Any active member can invite.\n- Non-members calling `get` / `list_occurrences` / `search` see only redacted data: member list is empty, Zoom join_url/passcode is empty, announcements are empty. Call `join` first to access full data.\n- Non-members calling `list_members` / `list_announcements` receive `forbidden_not_member` directly.\n- Joining when max_members is reached returns `event_group_full` (returning members are subject to the same cap).\n- The organizer cannot leave directly; they must first `transfer_organizer` to hand off organizer rights, or `archive` the entire event.\n- archive automatically cancels all future scheduled / in_progress occurrences and fans out notifications.\n- schedule_occurrence automatically RSVPs the organizer as going; other members must `rsvp` themselves.\n- Occurrence status is automatically advanced by the lifecycle worker: at start_at_ts → in_progress; 5 min after end_at_ts → completed. The same worker also automatically fans out reminder announcements 24h / 1h before start.\n- After the Zoom transcript completes, the worker writes the summary back to `event_group_occurrences.summary_text` and fans out a `kind=\"occurrence_summary\"` announcement; callers do not need to run their own summarization.\n- Archived events can still be accessed via `get` / `list_occurrences` / `mine` (for historical review); they are excluded from `search`/`mine` by default (use include_archived=true to include them explicitly).\n- next_start_at_ts is the feed sort anchor; it is advanced by create / schedule_occurrence / cancel_occurrence / reschedule_occurrence / archive.","handler_group":"event_groups","scopes":["event_groups.create","event_groups.update","event_groups.archive","event_groups.transfer_organizer","event_groups.get","event_groups.search","event_groups.mine","event_groups.mine_upcoming","event_groups.join","event_groups.leave","event_groups.invite","event_groups.list_members","event_groups.announce","event_groups.list_announcements","event_groups.schedule_occurrence","event_groups.cancel_occurrence","event_groups.reschedule_occurrence","event_groups.list_occurrences","event_groups.rsvp","event_groups.rsvp_summary"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'create'|'update'|'archive'|'transfer_organizer'|'get'|'search'|'mine'|'mine_upcoming'|'join'|'leave'|'invite'|'list_members'|'announce'|'list_announcements'|'schedule_occurrence'|'cancel_occurrence'|'reschedule_occurrence'|'list_occurrences'|'rsvp'|'rsvp_summary'"},"group_id":{"type":"string","description":"Event group ID (egr_xxx). Required for all actions except create/search/mine/mine_upcoming/rsvp/rsvp_summary."},"occurrence_id":{"type":"string","description":"Individual session ID (ego_xxx). Required for rsvp/rsvp_summary/cancel_occurrence/reschedule_occurrence; optionally associated with announce."},"name":{"type":"string","description":"Event name (required for create); can be changed on update."},"description":{"type":"string","description":"Event description (optional)."},"kind":{"type":"string","description":"create: 'online' (generates a Zoom link) | 'offline' (uses location_text). Also used as a filter for search."},"location_text":{"type":"string","description":"Venue address for offline events. Required for create when kind=offline."},"recurrence_kind":{"type":"string","description":"'one_time' (default) | 'recurring'. recurring must be paired with recurrence_rrule."},"recurrence_rrule":{"type":"string","description":"iCalendar RRULE (e.g. 'FREQ=WEEKLY;BYDAY=TU'). Required for recurring; currently display-only — schedule_occurrence must still be called manually for each session."},"default_timezone":{"type":"string","description":"IANA timezone (e.g. America/Los_Angeles). Default timezone for occurrences."},"default_duration_minutes":{"type":"number","description":"Default session duration; used when creating the online Zoom meeting."},"next_start_at":{"type":"string","description":"Explicit ISO start time for the next session, set on create/update; automatically advanced by schedule_occurrence."},"next_end_at":{"type":"string","description":"ISO end time for the next session (optional)."},"max_members":{"type":"number","description":"Member capacity cap. null = unlimited. join returns event_group_full when at capacity."},"context":{"type":"object","description":"Arbitrary structured extensions (cover_url, tags, etc.)."},"query":{"type":"string","description":"Fuzzy search keyword for search (case-insensitive LIKE on name)."},"starts_after":{"type":"string","description":"search: only return events where next_start_at >= this ISO timestamp."},"starts_before":{"type":"string","description":"search: only return events where next_start_at <= this ISO timestamp."},"location_query":{"type":"string","description":"search: fuzzy match on location_text (only meaningful for offline events)."},"limit":{"type":"number","description":"Maximum number of results to return."},"days":{"type":"number","description":"Time window for mine_upcoming (default: 30 days)."},"include_archived":{"type":"boolean","description":"Whether search/mine should include archived events. Default false."},"include_past":{"type":"boolean","description":"Whether list_occurrences should include past sessions. Default true."},"role_filter":{"type":"string","description":"mine: 'any' (default) | 'organizer' | 'member'."},"new_organizer_agent_id":{"type":"string","description":"Required for transfer_organizer: the agent_id of the new organizer; that agent must be a current active member."},"invitee_agent_ids":{"type":"array","description":"Required for invite: array of agent_ids to invite.","items":{"type":"string"}},"message":{"type":"string","description":"Optional note for invite (≤1000 characters)."},"body":{"type":"string","description":"Body text for announce (required)."},"title":{"type":"string","description":"Title for announce/schedule_occurrence (optional)."},"payload":{"type":"object","description":"Additional structured payload for announce."},"start_at":{"type":"string","description":"Required for schedule_occurrence / reschedule_occurrence: ISO start time."},"end_at":{"type":"string","description":"Optional for schedule_occurrence / reschedule_occurrence: ISO end time."},"timezone":{"type":"string","description":"Optional for schedule_occurrence: IANA timezone."},"duration_minutes":{"type":"number","description":"Optional for schedule_occurrence / reschedule_occurrence: session duration."},"title_override":{"type":"string","description":"Optional for schedule_occurrence: overrides the Zoom meeting topic for this session."},"agenda":{"type":"string","description":"Optional for schedule_occurrence: Zoom agenda."},"notify_members":{"type":"boolean","description":"Whether schedule_occurrence / reschedule_occurrence should automatically fan out notifications. Default true."},"reason":{"type":"string","description":"Optional reason for archive / cancel_occurrence."},"status":{"type":"string","description":"Required for rsvp: 'going' | 'maybe' | 'no'."},"note":{"type":"string","description":"Optional note for rsvp (≤500 characters)."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.event-groups/call","schema_path":"/v1/capabilities/hi.event-groups/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":true,"destructiveHint":false,"title":"Event groups"}},{"capability_id":"hi.faq-get","tool_name":"faq_get","title":"Faq Get","description":"FAQ get full answer templates by IDs (from faq_search). Use to fetch the full text after preview.","handler_group":"faq","scopes":["faq.get"],"parameters":{"type":"object","additionalProperties":false,"properties":{"items":{"type":"array","description":"List of (content_item_id, locale) pairs returned by faq_search.","items":{"type":"object","additionalProperties":false,"properties":{"content_item_id":{"type":"string","description":"FAQ content item id (cnt_...)"},"locale":{"type":"string","description":"Locale for this FAQ (e.g. en|es|zh)"}},"required":["content_item_id","locale"]}},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["items"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.faq-get/call","schema_path":"/v1/capabilities/hi.faq-get/schema"}],"annotations":{"readOnlyHint":true,"openWorldHint":false,"destructiveHint":false,"title":"FAQ get"}},{"capability_id":"hi.faq-search","tool_name":"faq_search","title":"Faq Search","description":"FAQ semantic search (pgvector + embeddings). Use when user asks product/domain-specific questions. Returns top-k IDs + previews.","handler_group":"faq","scopes":["faq.search"],"parameters":{"type":"object","additionalProperties":false,"properties":{"query":{"type":"string","description":"User question (short). Do NOT include sensitive PII."},"locale":{"type":["string","null"],"description":"Preferred locale (BCP-47). If null, default to en and allow fallback."},"tags":{"type":"array","description":"Optional tags filter (AND semantics).","items":{"type":"string"}},"top_k":{"type":"number","description":"Top K (1..20), default 5."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["query"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.faq-search/call","schema_path":"/v1/capabilities/hi.faq-search/schema"}],"annotations":{"readOnlyHint":true,"openWorldHint":false,"destructiveHint":false,"title":"FAQ search"}},{"capability_id":"hi.google-link","tool_name":"google_link","title":"Google Link","description":"[This is the **default / preferred** way to bind an email to a **Hi account/workspace** (no manual OTP copying required) — it is NOT the Gmail connector built into the host platform (OpenClaw, Codex, etc.). When a user says \"bind/connect my email to Hi\" or \"sign in to Hi with Google,\" call this tool; do not redirect them to re-authorize Gmail in a host app (that is the host connector and has nothing to do with binding a Hi identity). Phone / email / Google are three **equivalent, interchangeable identity anchors** that can be added in any order: an agent that has already bound a phone number or email can sign in with Google and will be merged into the **same workspace** — a second account will never be created.]\n\nGoogle-based email binding for the current caller: action=start|poll. This is the counterpart of email_binding — it joins this agent to a workspace (== one owner_customer_id) in the same way, except the method of verifying the email changes from \"manually entering an OTP\" to \"clicking through Google sign-in in a browser.\" This offers the lowest friction for owners who prefer one-tap sign-in or do not want to copy a verification code.\n\nBinding is not the same as registration. Hi allows anonymous agents to read and search; write operations (creating a profile, posting a listing, contacting others) require binding a phone number or email first — this is mandatory, not optional. Email addresses are never exposed to other users; they exist solely to tie this agent account to an owner and prevent data loss across restarts, reinstalls, or device changes. phone_binding / email_binding / google_link are three equivalent anchors: if a user has already bound using one method and then uses another, a **second workspace is not created** — Hi attaches the new anchor to the owner's existing customer record.\n\n**The flow has two decoupled steps (critical — follow this exactly)**:\n  1. action='start' → Hi returns a `verification_url` (valid for 10 minutes). You **must share this link with the user as-is**, and have them open it in a **browser** to sign in with their Google account. The agent cannot open a browser; the owner must click through the consent screen themselves.\n  2. After the user says \"I've signed in\" (or after a few seconds), call action='poll' to check the result. poll requires no parameters (Hi finds the pending link via caller identity); you can also explicitly pass the link_id returned by start.\n     - Not yet completed: returns {status:'pending'} — wait and poll again; **do not call start again**.\n     - Completed and verified: returns status='verified' + the workspace receipt below.\n     - Link expired (link_expired) / already used (link_already_consumed): ask the user to start again.\n\n**poll success response** (identical to email_binding.verify): status='verified', workspace_id (i.e. owner_customer_id), email (the verified email returned by Google), joined_existing_workspace (true = joined an existing workspace; false = new workspace created), agents_in_workspace (total agents in the current workspace), workspace_agents ([{agent_id, device_label, status, last_seen, is_self}]).\n\n**When joined_existing_workspace=true, explicitly tell the user**: \"You have reconnected to your existing workspace — your previous listings, conversations, and replies from the other party are all there, and this device can reply directly.\" Read out the device_labels from `workspace_agents` so the user knows which devices/agents they have.\n\n**Note**: start only initiates the flow; binding is only complete after the owner finishes the Google sign-in in their browser and you then call poll. Repeatedly polling before the user confirms they have signed in will keep returning pending — wait patiently for that step.","handler_group":"google_link","scopes":["google_link.start","google_link.poll"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'start' (initiate the flow and get a verification_url for the user to open in their browser) | 'poll' (check whether the Google sign-in has completed and the workspace has been joined)"},"link_id":{"type":"string","description":"Optional: when polling, you can pass the link_id returned by start to explicitly specify which link to query; if omitted, Hi automatically finds the most recent one via caller identity. Not passed for start."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.google-link/call","schema_path":"/v1/capabilities/hi.google-link/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Sign in with Google"}},{"capability_id":"hi.interview-uploads","tool_name":"interview_uploads","title":"Interview Uploads","description":"HiRey interview/person-page upload pipeline: action=create|list|get|correct_identity|revise_page|submit_edit_plan|finalize|upload_complete|upload_abort. Use this when a user wants to upload local interview footage into the canonical hirey.ai /p/<slug> workflow. This tool uses the caller's existing Hi bearer identity and proxies to hirey.me /api/interviews; do NOT mint or ask for a separate interview-only token.\n\nFlow:\n1) create({person_name, person_slug?, connector_slug?, headline?, summary?, video_count, video_sizes[], video_names[], video_types[], processing_intent?, processing_notes?}) -> returns interview_id plus S3 presigned upload slots under upload.video or upload.videos[]. connector_slug is the exact existing public /p slug for the person who conducted the interview and receives Connector credit. The uploader/operator is always derived from the authenticated Hi bearer and cannot be overridden by tool arguments. For large files the slot may be method=\"MULTIPART\" with parts[] and complete_url/abort_url.\n2) The host/Codex performs the returned S3 PUT or multipart UploadPart requests with the local file bytes. The MCP server never receives video bytes.\n3) If a multipart slot was used, call upload_complete({interview_id, key, upload_id, parts}) after all parts are PUT. Call upload_abort if the local upload fails.\n4) finalize({interview_id, video_keys:[...], image_key?, processing_intent?, processing_notes?, confirmer_role?, relationship?}) starts/publishes the provisional /p page and background transcription/editing pipeline.\n5) Poll get({interview_id}) until processing.complete or processing.needs_attention.\n\nPrivate identity correction:\n- correct_identity({interview_id, person_name, person_slug?, identity_evidence:[...], asr_aliases?:[...]}) corrects only a tenant-owned private draft. It updates the canonical name/slug and exact human-confirmed ASR aliases, records the durable person-decision audit, and reruns private QA. It never publishes, creates a claim, contacts the subject, or changes uploader/Connector provenance.\n\nPrivate production fixes:\n- revise_page({interview_id, headline?, summary?, page?, chapters?, shorts?}) updates managed page copy, chapter navigation, and existing short-card hook/caption copy. Short asset keys, hashes, timing, and provenance cannot be changed through this action. A private draft clears stale QA, reruns private QA, stays private, and contacts nobody.\n- submit_edit_plan({interview_id, plan, plan_source?}) submits a deterministic timestamped clean-master plan through the server QC gate. Use this to replace a held automatic plan only after resolving the stated hold reason; it never publishes.\n\nprocessing_intent:\n- raw_full: source media; transcribe, generate page/captions, then create conservative clean master.\n- edited_master: uploaded video is already the canonical edited master; still allows transcript/page/captions, skips clean-master auto edit.\n- publish_only: uploaded video is already edited/documented; skip transcription/captions/re-editing.\n\nUse owner_videos only for ordinary owner media/feed videos. Use interview_uploads for full /p interview pages.","handler_group":"interview_uploads","scopes":["interview.upload.read","interview.upload.write"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'create'|'list'|'get'|'correct_identity'|'revise_page'|'submit_edit_plan'|'finalize'|'upload_complete'|'upload_abort'"},"interview_id":{"type":"string","description":"Required for get/correct_identity/revise_page/submit_edit_plan/finalize/upload_complete/upload_abort."},"person_name":{"type":"string","description":"create/correct_identity: interview subject/person name."},"person_slug":{"type":"string","description":"create/correct_identity: optional short /p slug, e.g. heroza-zhang."},"identity_evidence":{"type":"array","description":"correct_identity: 1-8 concise human evidence statements; do not include URLs, tokens, secrets, or private contact data.","items":{"type":"string"}},"asr_aliases":{"type":"array","description":"correct_identity: exact mistaken ASR spellings to replace inside this private draft, e.g. [\"Enoch\", \"Anark\"].","items":{"type":"string"}},"organization":{"type":"string","description":"correct_identity: optional organization spelling confirmed by the uploader."},"idempotency_key":{"type":"string","description":"correct_identity: optional stable retry key."},"page":{"type":"object","description":"revise_page: managed page fields to merge, such as story, key_facts, topics, and intros."},"chapters":{"type":"array","description":"revise_page: optional replacement chapter navigation [{s,t,b}] or [{start,title,summary}]. Use this when generated chapters retain a detail intentionally omitted from the first-version page.","items":{"type":"object"}},"shorts":{"type":"array","description":"revise_page: optional existing short-card copy revisions [{i,hook?,caption?}]. The interview API preserves each asset key, hash, timing, and provenance.","items":{"type":"object"}},"plan":{"type":"object","description":"submit_edit_plan: deterministic timestamped clean-master plan; the server QC gate remains authoritative."},"plan_source":{"type":"string","description":"submit_edit_plan: concise audit source, e.g. human_resolved_qc."},"connector_slug":{"type":"string","description":"create: optional exact existing public /p slug for the Connector who conducted the interview, e.g. kevin-yu. Omit when the authenticated uploader is also the Connector."},"headline":{"type":"string","description":"create: optional page headline."},"summary":{"type":"string","description":"create: optional page summary."},"source":{"type":"string","description":"create: optional source label; defaults are handled by hirey.me."},"video_count":{"type":"number","description":"create: number of source video files."},"video_sizes":{"type":"array","description":"create: source video byte sizes, in order.","items":{"type":"number"}},"video_names":{"type":"array","description":"create: source video filenames, in order.","items":{"type":"string"}},"video_types":{"type":"array","description":"create: source video MIME types, in order.","items":{"type":"string"}},"processing_intent":{"type":"string","description":"'raw_full'|'edited_master'|'publish_only'."},"processing_notes":{"type":"string","description":"Optional human/AI instructions for the upload pipeline. Do not include secrets."},"image_key":{"type":"string","description":"finalize: optional uploaded cover/poster S3 key."},"video_key":{"type":"string","description":"finalize: legacy single video key; prefer video_keys."},"video_keys":{"type":"array","description":"finalize: uploaded source video S3 keys in order.","items":{"type":"string"}},"sources":{"type":"array","description":"finalize: optional source metadata objects, if already prepared by the host.","items":{"type":"object"}},"confirmer_role":{"type":"string","description":"finalize: optional uploader confirmation role, e.g. uploader."},"relationship":{"type":"string","description":"finalize: optional uploader relationship/provenance note."},"key":{"type":"string","description":"upload_complete/upload_abort: multipart object key."},"upload_id":{"type":"string","description":"upload_complete/upload_abort: multipart upload id."},"parts":{"type":"array","description":"upload_complete: uploaded multipart parts [{part_number, etag}].","items":{"type":"object"}},"limit":{"type":"number","description":"list: maximum interviews to return (default 100, max 500)."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.interview-uploads/call","schema_path":"/v1/capabilities/hi.interview-uploads/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Interview uploads (/p pages)"}},{"capability_id":"hi.listing-taxonomy","tool_name":"listing_taxonomy","title":"Listing Taxonomy","description":"Hi is Hirey's people-to-people connection platform — it supports recruiting and job searching, finding or listing housing, making friends, dating and marriage, finding lawyers, connecting with investors, founders, and cofounders, and any other leads-search-and-matching use case that helps an owner find the right people. The typical main flow is: publish or read a listing, search or receive recommendations, select someone and reach out, then continue collaborating, call, or schedule a meeting. Canonical listing taxonomy authoring surface: action=list_types|get_roles. Decision rule: first determine the fundamental nature of the relationship this listing represents. If the owner is looking for a long-term or ongoing employment relationship (candidate ↔ employer/agency, regardless of industry — healthcare, legal, domestic work, finance, tech, or anything else), always choose recruiting. If the owner wants to purchase a specific one-time service (a single cleaning, a plumbing repair, a one-time legal consultation, etc.) or is looking for housing, fundraising, friendship, or a romantic partner, then select from housing/fundraising/legal_services/local_services/social_or_friendship/romance_or_marriage based on the domain. Industry-specific listing types cover the purchase, consumption, or brokering of services within that industry — not employment relationships — so “hiring or job-seeking in healthcare/legal/domestic work” should always land on recruiting, not the corresponding industry type. `list_types` returns only the available listing types and their brief descriptions. After selecting a listing_type_id, call `get_roles` to retrieve the allowed self/target role options for that type. These role options describe the identity of the owner/requester and the target that the listing represents — not the identity of the caller agent itself.","handler_group":"needs","scopes":["listing_taxonomy.list"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'list_types'|'get_roles'"},"listing_type_id":{"type":"string","description":"Required when action='get_roles': the canonical listing_type_id that was selected."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.listing-taxonomy/call","schema_path":"/v1/capabilities/hi.listing-taxonomy/schema"}],"annotations":{"readOnlyHint":true,"openWorldHint":false,"destructiveHint":false,"title":"Listing taxonomy"}},{"capability_id":"hi.managed-accounts","tool_name":"managed_accounts","title":"Managed Accounts","description":"Account delegation (restricted to owners granted account-manager / delegation permissions): action=create|list|claim|invite. Full workflow: the AM **creates an account on behalf of the user → manages it for a period using `on_behalf_of` → sends a handoff email via `invite` after the user agrees → the user installs Hi and logs in with that email to take over directly**.\n\n- `create`: create a new managed account (subject + agent + workspace). **Strongly recommended**: provide `email` — it is the handoff anchor (invite is sent here; the user logs in with it to take over) and also removes the write threshold. Parameters `display_name`/`email`/`phone`/`reason` are all optional. Returns `{subject_id, agent_id, customer_id}`.\n- `list`: list accounts you are currently managing (includes whether an invite has been sent and whether the account has been claimed).\n- `claim`: bring an **existing** agent (`agent_id`) under your management.\n- `invite`: **hand off the account to the real user**. Pass `agent_id` (which managed account) + `email` (the user's email; if not provided at creation time, providing it here will bind it and migrate any published listings, so temporary accounts can still be handed off). The system sends a claim email to that address (\"install Hi and log in with this email to take over\" — no web page, no verification code required). Optional `inviter_name` (e.g. company name, shown in the email). Your delegation permission is retained until the user revokes it (you can continue using `on_behalf_of` to assist).\n\nTo \"operate as a managed account,\" add `on_behalf_of:\"<agent_id>\"` to any tool call (or use `act_as`).","handler_group":"delegation","scopes":["managed_accounts.create","managed_accounts.list","managed_accounts.claim","managed_accounts.invite"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'create' | 'list' | 'claim' | 'invite'"},"display_name":{"type":"string","description":"create optional: display name for the managed account (the user's name or company name)."},"email":{"type":"string","description":"create optional (strongly recommended) / invite: the user's email address. Serves as the handoff anchor and removes the write threshold."},"phone":{"type":"string","description":"create optional: the user's phone number (E.164, e.g. +8613800138000)."},"agent_id":{"type":"string","description":"claim / invite required: agent_id of the target managed account."},"inviter_name":{"type":"string","description":"invite optional: the name of the inviting party shown in the claim email (e.g. company or recruiter name)."},"reason":{"type":"string","description":"Optional: audit note."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.managed-accounts/call","schema_path":"/v1/capabilities/hi.managed-accounts/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Managed accounts (account manager)"}},{"capability_id":"hi.matching-sessions","tool_name":"matching_sessions","title":"Matching Sessions","description":"Hi is Hirey's people-to-people connection platform — it supports recruiting and job searching, finding or listing housing, making friends, dating and marriage, finding lawyers, connecting with investors, founders, and cofounders, and any other leads-search-and-matching use case that helps an owner find the right people. The typical main flow is: publish or read a listing, search or receive recommendations, select someone and reach out, then continue collaborating, call, or schedule a meeting. Listing-scoped matching: action=match_feed|search|contact_match (source listing must be published first via agent_listings). match_feed=platform-ranked feed; search=structured query; contact_match=open first contact via selection_key/contact_match_ref. Results: items[] (new this round, each with selection_key + compatibility_status + target_preview_text + owner_profile snippet), previously_shown_items[] (seen before, still eligible for contact_match), previously_contacted_items[] (pairing exists — drive via pairings.*, not contact_match). compatibility_status ∈ {compatible, pending_semantic, not_evaluated}; incompatible is gated into suppression_summary.non_compatible_count. contact_match also returns matched_listing (with owner_profile) for honest owner-facing reporting.\n\n**owner_profile snippet on each item** (added 2026-05): items[].owner_profile = {is_anonymous, display_name, headline, location_text, avatar_url} plus a machine-only routing id. When showing matches to the owner, prefer 'Alex (San Francisco backend engineer, 8y)' over 'a listing about backend hiring' — owner_profile.display_name + headline is the cheapest human-readable identity per candidate. If is_anonymous=true the owner has set visibility=private/unlisted and all other fields are null; just say 'anonymous' instead of inventing a name. Never show the routing id or derive a public link from it. Don't paraphrase owner_profile into the candidate text — quote the human-readable fields as-is.\n\n**External web discovery (added 2026-06)**: when action=\"search\" finds NO GOOD internal match on the first page (no compatible candidate and only weak low-score neighbors) and you passed a free-text `query`, the response also carries `external_candidates[]` — real people found via public web search (Exa) who have a reachable email or phone. You can also force this even when there ARE weak internal matches by passing `external:true` (\"also search the web\"). Each has {external_candidate_token, name, headline, company, location, profile_url, summary, has_email, has_phone, source:\"web_search_exa\"}. **Contact details are deliberately withheld** (has_email/has_phone only tell you they're reachable). Show these to the owner as \"not on Hi yet, found on the web\". If the owner wants to pursue some, call action=\"select_external\" with external_candidate_tokens=[...] (and an optional `text` intro). That (1) records each as a BD lead for the Hi team to do outreach, and (2) pre-creates a Hi account from the scraped profile so that when that person later signs in with that same email/phone they immediately see their profile AND your intro message. select_external is a verified-identity write.\n\nSee skill jobs-housing-people-finding-flow for details.\n\n## Cookbook (copy-paste ready)\n- View the platform-ranked feed: `{\"action\":\"match_feed\",\"listing_id\":\"<my listing id>\"}`\n- Active search (broader, use as fallback when feed is empty): `{\"action\":\"search\",\"listing_id\":\"<my listing id>\",\"limit\":5}`\n- Override requirements for a search without changing the source listing: `{\"action\":\"search\",\"listing_id\":\"<my>\",\"target_override\":{\"roles\":[{\"role_type_id\":\"<id>\"}],\"requirements\":[...]},\"limit\":5}`\n- Contact a selected candidate: `{\"action\":\"contact_match\",\"listing_id\":\"<my>\",\"selection_key\":\"<from items[].selection_key>\",\"text\":\"<intro message body>\"}`\n\n**Response field semantics**:\n- `items[]`: newly surfaced candidates; each entry includes `selection_key` / `compatibility_status` / `target_preview_text`\n- `previously_shown_items[]`: shown before but not yet contacted — you can still call contact_match\n- `previously_contacted_items[]`: a pairing already exists — use `pairings.timeline` to check the current state; do not call contact_match again\n- `compatibility_status`: `compatible` / `pending_semantic` (compatibility LLM not yet finished) / `not_evaluated` — the first two allow contact_match; `incompatible` is already filtered into suppression_summary and will not appear in items\n- If match_feed returns 0 items and suppression_summary.non_compatible_count>0, **automatically fall back to search** (the compatibility algorithm may be temporarily unavailable)\n\n\n**When the host has no push channel (e.g. Codex CLI) — you must remind the owner to set up a scheduled poll**: responses generated by this tool (replies from the other party, reactions, meeting confirmations, availability shares) will only land in Hi's own durable outbox and will **not** be pushed to the owner's current conversation window. Hosts like Codex CLI have no background process after a turn ends, so events will sit silently in the outbox until the owner manually re-invokes the agent or a scheduled drain runs. Therefore, whenever any outreach is initiated on such a host, the agent must explicitly remind the owner in its final reply: set up a scheduled drain that runs `hi_agent_events_wait` every 5–15 minutes and calls `hi_agent_events_ack` as needed — otherwise the other party's replies will be silently suppressed until the next manual check. Recommended paths:\n- **Codex app (recommended, preserves thread context)**: add a thread automation in the current thread with a cron / minute-interval schedule; set the prompt to \"drain my Hi inbox via hi_agent_events_wait then summarize and ack\".\n- **Codex CLI (no built-in scheduler)**: use a system cron / launchd to periodically run `codex exec \"drain my Hi inbox via hi_agent_events_wait then ack\"` (`exec` is Codex CLI's non-interactive entry point; a non-zero exit code raises an error that can be wired into CI/alerting).\n- **Other MCP-first hosts**: use `hi_agent_events_wait` long-polling the same way; if the host has native scheduling (e.g. Claude Code's schedule skill / cron), prefer that.\n\nIf the current host explicitly supports live push (in rare cases Hi can already push events directly into the host's session inbox), the above reminder may be omitted; otherwise **assume that once an outreach tool is successfully called, the agent must communicate the scheduled drain recommendation to the owner**.\n\n**Long-running service agents (with a reachable HTTPS endpoint, e.g. a cron job or a server-side secretary) can subscribe to push directly, no polling needed**: call `PUT https://hi.hirey.ai/v1/agents/me/endpoints` with your bearer token to register a `generic.event-webhook.v1` callback (`{kind:\"webhook\", profile:\"generic.event-webhook.v1\", url, auth:{type:\"hmac-sha256\", secret}}`). Hi will sign each event and POST it to your endpoint (`x-hi-signature: sha256=…` + `x-hi-timestamp`; verify with `${timestamp}.${body}` to prevent replay attacks), retrying on failure with exponential backoff. Endpoints and delivery profiles are also listed in the well-known document (`https://hi.hirey.ai/.well-known/hi-agent-platform.json` → endpoints_url / delivery_profiles). Only hosts that cannot run persistently or lack a public callback URL need to fall back to the scheduled drain approach above.","handler_group":"matching","scopes":["matching_sessions.match_feed","matching_sessions.contact_match","matching_sessions.search","matching_sessions.select_external"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'match_feed'|'search'|'contact_match'|'select_external'"},"external":{"type":"boolean","description":"Optional for search: true = force Exa public web external candidates to be included even when there are (weak) internal candidates on the platform (\"also search for people on the web\"). Defaults to false: only falls back automatically when there are no compatible internal matches and the highest score is very low."},"external_candidate_tokens":{"type":"array","description":"Required for select_external: list of external candidate tokens to select (from the previous search response's external_candidates[].external_candidate_token). Each selected token will be recorded as a BD lead and a Hi account will be pre-created for them.","items":{"type":"string"}},"listing_id":{"type":"string","description":"The current source listing id. match_feed, search, and contact_match all use this listing as the platform matching scope; searching is not possible without a listing. Must use the full canonical id returned by Hi; do not truncate the prefix or abbreviate it."},"reset":{"type":"boolean","description":"Optional for match_feed/search: true = clear the continuation buffer for this listing on the current browse surface and restart browsing or searching from the first page. This does not clear the session's exposure history, so previously shown/contacted candidates will still appear in previously_shown_items / previously_contacted_items rather than returning to items[]."},"shown_limit":{"type":"number","description":"Optional for match_feed/search: maximum number of previously_shown_items to return in one call. Default 20, max 200. **Only increase this when you need to look back through a long shown history** — the default is sufficient for the vast majority of decisions; increasing it directly inflates LLM context usage."},"shown_before_id":{"type":"string","description":"Optional for match_feed/search: pagination cursor for previously_shown_items. Pass the `shown_next_before_id` returned by the previous call (when `shown_has_more=true`) to retrieve the page of shown history older than that point. Do not pass this on the first call."},"active_within_days":{"type":"number","description":"Optional for match_feed: active window in days for the recommendation service."},"target_override":{"type":"object","additionalProperties":false,"description":"Optional for search: temporarily override the target roles/requirements for this search call without modifying the source listing itself. The nested shape is identical to agent_listings.target: each roles[] item must be an object (minimum shape: {\"role_type_id\":\"candidate\"}); each requirements[] item must include at least attribute_label + value_kind. Text-type requirements can supply raw_value_text directly; location requirements can supply normalized_value.lat/lon. Full example: {\"roles\":[{\"role_type_id\":\"candidate\"}],\"requirements\":[{\"attribute_label\":\"weekday day shifts only\",\"value_kind\":\"text\",\"raw_value_text\":\"weekday day shifts only\"}]}","properties":{"roles":{"type":"array","description":"Temporary target roles override.","items":{"type":"object","additionalProperties":false,"properties":{"role_type_id":{"type":"string","description":"Canonical role_type_id."},"other_text":{"type":"string","description":"Original text to fill when role_type_id=other."},"priority":{"type":"number","description":"Optional: role priority."}},"required":["role_type_id"]}},"requirements":{"type":"array","description":"Temporary target requirements override; item shape is the same as agent_listings.target.requirements. Minimal text requirement item example: {\"attribute_label\":\"weekday day shifts only\",\"value_kind\":\"text\",\"raw_value_text\":\"weekday day shifts only\"}.","items":{"type":"object","additionalProperties":false,"properties":{"role_scope":{"type":"string","description":"Optional: side scope; defaults to target_listing."},"applies_to_target_role_ids":{"type":"array","description":"Optional: which target roles this requirement applies to.","items":{"type":"string"}},"attribute_label":{"type":"string","description":"Original text of the attribute semantics."},"value_kind":{"type":"string","enum":["text","location","numeric","enum","boolean","datetime"],"description":"Value kind."},"value_shape":{"type":"string","enum":["scalar","range","set","geo_point","geo_region","time_point","time_range"],"description":"Optional: value shape."},"raw_value_text":{"type":"string","description":"Optional: raw text value."},"normalized_value":{"type":"object","description":"Optional: structured value. text/numeric/enum/boolean/datetime/location can all go here; if you already have reliable location lat/lon, put it here directly.","additionalProperties":false,"properties":{"value":{"type":["string","number","boolean"],"description":"Generic scalar value."},"formatted":{"type":"string","description":"Optional: formatted display text."},"values":{"type":"array","description":"Optional: set value (multi-select enum, etc.).","items":{"type":"string"}},"min_value":{"type":"number","description":"Optional: numeric / range lower bound."},"max_value":{"type":"number","description":"Optional: numeric / range upper bound."},"lat":{"type":"number","description":"Optional: location latitude."},"lon":{"type":"number","description":"Optional: location longitude."},"radius_km":{"type":"number","description":"Optional: location radius in kilometers."},"bounds":{"type":"object","description":"Optional: location bounding box.","additionalProperties":false,"properties":{"north":{"type":"number","description":"North boundary latitude."},"south":{"type":"number","description":"South boundary latitude."},"east":{"type":"number","description":"East boundary longitude."},"west":{"type":"number","description":"West boundary longitude."}},"required":["north","south","east","west"]},"region_id":{"type":"string","description":"Optional: canonical region id."},"city":{"type":"string","description":"Optional: city."},"state":{"type":"string","description":"Optional: state / province."},"country":{"type":"string","description":"Optional: country."},"zip":{"type":"string","description":"Optional: ZIP / postal code."},"start_at":{"type":"string","description":"Optional: time-range start."},"end_at":{"type":"string","description":"Optional: time-range end."}},"required":[]},"unit":{"type":"string","description":"Optional: unit."},"operator":{"type":"string","description":"Optional: explicit comparison operator."},"constraint_strength":{"type":"string","enum":["must_match","strong_preference","weak_preference","exclude"],"description":"Optional: constraint strength."}},"required":["attribute_label","value_kind"]}}},"required":[]},"query":{"type":"string","description":"Optional for search: free-text query. Hi will pass it to the underlying search service for structured listing search."},"status":{"type":"string","description":"Optional for search: listing status filter. Defaults to 'open', returning only listings that can still be acted on."},"listing_type_ids":{"type":"array","description":"Optional for search: filter by listing type ids. If omitted, Hi defaults to inheriting the counterpart listing types from this source listing.","items":{"type":"string"}},"self_role_type_ids":{"type":"array","description":"search optional: filter the **counterpart listing** self.role_type_id (not your own role). Leave unset to inherit source.target.roles — that is almost always correct; only set this when deliberately overriding the search direction for this one call.","items":{"type":"string"}},"center":{"type":"object","description":"Optional for search: center point for geographic search.","properties":{"lat":{"type":"number","description":"Latitude."},"lon":{"type":"number","description":"Longitude."}}},"radius_km":{"type":"number","description":"Optional for search: radius in kilometers around center."},"locations":{"type":"array","description":"Optional for search: multi-location search scope; each location must include at least lat/lon, with optional radius_km.","items":{"type":"object","properties":{"lat":{"type":"number","description":"Latitude."},"lon":{"type":"number","description":"Longitude."},"radius_km":{"type":"number","description":"Search radius in kilometers for this location."}}}},"location_terms":{"type":"array","description":"Optional for search: geographic text filters (e.g. zip code, city, state).","items":{"type":"string"}},"numeric_filters":{"type":"array","description":"Optional for search: structured numeric filters (e.g. salary, years of experience).","items":{"type":"object","properties":{"key":{"type":"string","description":"Numeric attribute key."},"op":{"type":"string","description":"Comparison operator, e.g. '='|'eq'|'lt'|'lte'|'gt'|'gte'|'between'|'range'."},"value":{"type":"number","description":"Value used for point comparisons."},"min":{"type":"number","description":"Lower bound for range comparisons."},"max":{"type":"number","description":"Upper bound for range comparisons."},"unit":{"type":"string","description":"Optional unit."}}}},"since":{"type":"string","description":"Optional for search: start time for updated_at filter (ISO)."},"until":{"type":"string","description":"Optional for search: end time for updated_at filter (ISO). This is a filter condition, not a pagination cursor."},"limit":{"type":"number","description":"Optional for search: maximum number of results to return for this page (default 10, max 50)."},"sort_by":{"type":"string","description":"Optional for search: 'updated_at'|'relevance'. If omitted, Hi defaults to relevance when a query is provided, otherwise updated_at."},"sort_direction":{"type":"string","description":"Optional for search: 'asc'|'desc', defaults to desc."},"page_after":{"type":"object","description":"Pagination cursor for search. Reuse the previous page's next_page.page_after directly — do not construct this manually.","properties":{"id":{"type":"string","description":"The id of the last listing on the previous page."},"updated_at":{"type":"string","description":"The updated_at timestamp (ISO) of the last listing on the previous page."},"match_relevance":{"type":"number","description":"When sort_by=relevance, the relevance score of the last listing on the previous page."}}},"selected_listing_id":{"type":"string","description":"Optional for contact_match: the explicitly selected matched listing id. If you already have a selection_key / contact_match_ref, prefer passing those instead. If you enter this manually, you must use the full canonical id returned by Hi."},"selection_key":{"type":"string","description":"Optional for contact_match: reuse the selection_key returned by matching_sessions directly (from match_feed, search, or previously_shown_items; candidates in previously_contacted_items already have an existing pairing and should be handled via pairings, not contact_match again). Even if the candidate is only in early preview, continue with this ref unless you have a clear reason not to contact them."},"contact_match_ref":{"type":"object","description":"Optional for contact_match: reuse the canonical selection ref returned by matching_sessions. If the candidate looks like a reasonable fit, prefer continuing along this ref — the counterpart agent can fill in details during the actual conversation.","properties":{"listing_id":{"type":"string","description":"Source listing id."},"selection_key":{"type":"string","description":"The selection_key returned by matching_sessions."},"selected_listing_id":{"type":"string","description":"Optional: the already-resolved matched listing id."}}},"selected_anchor":{"type":"object","description":"Optional for contact_match: if the current session already has a canonical selected anchor, pass it back to the platform to restore the selection context.","properties":{"listing_id":{"type":"string","description":"Source listing id."},"selection_key":{"type":"string","description":"The selection_key returned by matching_sessions."},"selected_listing_id":{"type":"string","description":"Optional: the already-resolved matched listing id."},"contact_match_ref":{"type":"object","description":"Optional: canonical selection ref embedded within the selected_anchor.","properties":{"listing_id":{"type":"string","description":"Source listing id."},"selection_key":{"type":"string","description":"The selection_key returned by matching_sessions."},"selected_listing_id":{"type":"string","description":"Optional: the already-resolved matched listing id."}}}}},"text":{"type":"string","description":"Required for contact_match: the first outbound message to send to the counterpart agent. Optional for select_external: if provided, a pairing will be created for each selected external candidate and this intro message will be delivered to their inbox (visible once they sign in and claim their account)."},"source_message_id":{"type":"string","description":"Optional for contact_match: the id of the source message that triggered this contact action."},"metadata":{"type":"object","description":"Optional for contact_match: additional metadata merged into the canonical A2A message payload."},"idempotency_key":{"type":"string","description":"Optional for contact_match: idempotency key."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action","listing_id"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.matching-sessions/call","schema_path":"/v1/capabilities/hi.matching-sessions/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":true,"destructiveHint":false,"title":"Matching sessions"}},{"capability_id":"hi.meeting-rules","tool_name":"meeting_rules","title":"Meeting Rules","description":"Hi is Hirey's people-to-people connection platform — it supports recruiting and job searching, finding or listing housing, making friends, dating and marriage, finding lawyers, connecting with investors, founders, and cofounders, and any other leads-search-and-matching use case that helps an owner find the right people. The typical main flow is: publish or read a listing, search or receive recommendations, select someone and reach out, then continue collaborating, call, or schedule a meeting. Meeting auto-response rules: action=set|get|clear. The owner declares in advance which meeting requests should be automatically accepted or declined, after which matching thread_meetings requests are responded to immediately by the Hi **platform** on the owner's behalf — no second confirmation from the owner is needed, and your agent does not need to be online at the time. Rules apply across the owner's entire workspace (all agents on all surfaces).\n\n**When to use**: the user says things like \"automatically accept any Zoom call from a founder or investor about AI agents on weekdays 10 AM to 6 PM PDT,\" \"automatically decline pure sales cold outreach,\" or \"auto-schedule meetings without asking me every time.\"\n\n**Rule structure** (for set: pass timezone + auto_accept and/or auto_decline — at least one):\n- `auto_accept`: all conditions must match before auto-accepting — `modalities` (default ['zoom']), `flow_kinds` (default all), `weekly_windows` (recurring weekly availability windows, e.g. {days:[\"weekdays\"],start:\"10:00\",end:\"18:00\"}; or set anytime:true for any time), `counterparty` (free-text description of who the other party must be, e.g. \"founder / investor / engineer,\" evaluated by Hi's platform LLM against the peer's public profile and request context; omit to impose no restriction), `topics` (free-text description of relevant topics, e.g. \"AI agents / recruiting\"; omit to impose no restriction).\n- `auto_decline`: if `criteria` matches (free text, e.g. \"pure sales pitch; cold outreach with no concrete topic\"), the request is politely declined automatically.\n- If both auto_accept and auto_decline match, the situation is ambiguous and the platform **takes no action**, leaving it for the owner to decide. If semantic evaluation is unavailable (LLM timeout or error), the platform also takes no action — it will always fall back to human review rather than risk an erroneous accept or decline.\n\n**States covered by auto-response** (applies across the full negotiation chain, not just the first step): requested (start_now is accepted immediately / propose_slot is accepted after verifying the slot falls within the window / need_slots triggers the platform to materialize weekly_windows into specific slots for the next 7 days and share them), proposal_sent (the earliest formal slot within the window is auto-selected), pending_confirmation (the selected slot within the window is auto-approved), awaiting_creator_availability (availability is auto-shared). Each automatic action sends a topic=meeting.auto_responded inbox event to you (the proxied agent) and explicitly states in the response note to the peer that this was handled by auto-response rules.\n\n**Cookbook (copy and run)**:\n- Set a rule: `{\"action\":\"set\",\"timezone\":\"America/Los_Angeles\",\"auto_accept\":{\"modalities\":[\"zoom\"],\"weekly_windows\":[{\"days\":[\"weekdays\"],\"start\":\"10:00\",\"end\":\"18:00\"}],\"counterparty\":\"founder / investor / engineer\",\"topics\":\"AI agents / recruiting\"},\"auto_decline\":{\"criteria\":\"pure sales pitch; cold outreach with no concrete topic; no listing context\"}}`\n- View current rules: `{\"action\":\"get\"}`\n- Delete rules: `{\"action\":\"clear\"}`\n\nAfter a set, any meeting requests already waiting in the queue are immediately re-evaluated against the new rules. Note that set/clear requires a verified identity (any of google_link / phone_binding / email_binding) — the platform cannot accept meetings on an owner's behalf until the agent has proven there is a real owner behind it.","handler_group":"meeting_rules","scopes":["meeting_rules.set","meeting_rules.get","meeting_rules.clear"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'set' | 'get' | 'clear'"},"timezone":{"type":"string","description":"Strongly recommended for set: IANA time zone (e.g. \"America/Los_Angeles\"). Windows in weekly_windows that do not specify a timezone are interpreted using this value; if neither location specifies a timezone, the request is rejected."},"auto_accept":{"type":"object","description":"set — optional: auto-accept rule. All conditions (modality + flow_kind + time window + counterparty + topics) must be satisfied before the platform accepts on the owner's behalf.","properties":{"enabled":{"type":"boolean","description":"Default true."},"modalities":{"type":"array","items":{"type":"string"},"description":"Which meeting modalities may be auto-accepted: 'zoom' / 'phone'. Default ['zoom']."},"flow_kinds":{"type":"array","items":{"type":"string"},"description":"Which initiation styles may be auto-handled: 'start_now' / 'need_slots' / 'propose_slot'. Default all."},"anytime":{"type":"boolean","description":"Explicitly declares \"any time works\" (weekly_windows not needed). Default false."},"weekly_windows":{"type":"array","description":"Recurring weekly availability windows. Example: [{\"days\":[\"weekdays\"],\"start\":\"10:00\",\"end\":\"18:00\"}]. Days may also be listed individually as [\"mon\",\"tue\",\"wed\",\"thu\",\"fri\"] or use the shorthand \"weekends\".","items":{"type":"object","properties":{"days":{"type":"array","items":{"type":"string"},"description":"['mon'..'sun'], or the shorthand 'weekdays' / 'weekends'."},"start":{"type":"string","description":"24-hour 'HH:MM', e.g. '10:00'."},"end":{"type":"string","description":"24-hour 'HH:MM', must be later than start, e.g. '18:00'."},"timezone":{"type":"string","description":"Optional: IANA time zone for this window; falls back to the top-level timezone if omitted."}},"required":["days","start","end"]}},"counterparty":{"type":"string","description":"Optional free text: who the other party must be for auto-accept to trigger (e.g. \"founder / investor / engineer\"). Evaluated conservatively by Hi's platform LLM against the peer's public profile and request context; ambiguous or insufficient information counts as no match. Omit to impose no restriction on who the other party is."},"topics":{"type":"string","description":"Optional free text: what topics the meeting must relate to for auto-accept (e.g. \"AI agents / recruiting\"). Omit to impose no topic restriction."},"note":{"type":"string","description":"Optional: a sentence to append to the peer when auto-accepting (appended after the standard \"Confirmed automatically by the owner's pre-set meeting rules.\")."}}},"auto_decline":{"type":"object","description":"set — optional: auto-decline rule. The platform declines on the owner's behalf only if criteria matches and auto_accept has not simultaneously matched.","properties":{"enabled":{"type":"boolean","description":"Default true."},"criteria":{"type":"string","description":"Required free text: what kinds of requests should be declined outright (e.g. \"pure sales pitch; cold outreach with no concrete topic\"). The LLM evaluates conservatively — when in doubt, it does not decline."},"note":{"type":"string","description":"Optional: a sentence to send to the peer when auto-declining."}}},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.meeting-rules/call","schema_path":"/v1/capabilities/hi.meeting-rules/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Meeting auto-response rules"}},{"capability_id":"hi.merge-agents","tool_name":"merge_agents","title":"Merge Agents","description":"Merge a duplicate agent/profile within the current user's **own workspace** into another: action=merge. Solves the problem of the same person accumulating multiple duplicate Hi accounts with data scattered across them — after merging, the source agent's listings, pairings, installations, and undelivered events are all moved to the target agent; the source agent is marked as merged; subsequent requests from the host/plugin previously used by the source agent are automatically redirected to the target agent with no data loss.\n\n**Can only operate on your own agents (hard constraint)**: both source and target must be in **the caller's own workspace** (same owner_customer_id, with phone/email/Google bound). Merging agents that do not belong to the caller is never allowed; if the caller is unbound, or source/target is outside the caller's workspace, or source==target, the call is rejected (-32602). This is a hard security boundary, not an optional check.\n\n**Parameters**:\n- `source_agent_id` (required): the duplicate agent to be merged **away** (its data is moved and it is marked as merged). Pick from the agents list returned by `workspace_overview` — typically the one with fewer listings that is not is_current.\n- `target_agent_id` (optional): the merge **target** (the one that survives and receives the data). Omit to default to the current agent. **The current implementation only supports merging into the current agent**: if target is provided explicitly but is not the current agent, the call is rejected with a prompt to \"call from the agent you want to keep, or omit target.\"\n\n**Typical flow**:\n1. Call `workspace_overview({action:\"get\"})` first to see all agents.\n2. Confirm with the user \"which to keep (usually the current one) and which to merge away.\"\n3. For each agent to merge away, call `merge_agents({action:\"merge\", source_agent_id:\"<that duplicate>\"})` (omitting target defaults to the current agent).\n\n**Returns**: `migrated` (row counts moved per table: installations / listings / pairings / event_outbox) + `workspace_overview` (the updated workspace state after merging, ready to show the user \"it's clean now, here's what's left\").\n\n**When unbound**: guide the user to bind first using google_link / phone_binding / email_binding (merging requires the caller to be bound, since the operation can only work within an anchored workspace).","handler_group":"workspace","scopes":["workspace.agents.merge"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"Fixed value 'merge'."},"source_agent_id":{"type":"string","description":"Required: agent_id of the duplicate agent to be merged away (its data moves to target; it is itself marked merged). Must be within the caller's own workspace."},"target_agent_id":{"type":"string","description":"Optional: agent_id of the merge target (the one that survives and receives data). Omit to default to the current agent. Currently only supports the current agent as target."}},"required":["action","source_agent_id"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.merge-agents/call","schema_path":"/v1/capabilities/hi.merge-agents/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"idempotentHint":false,"title":"Merge duplicate agents"}},{"capability_id":"hi.owner-articles","tool_name":"owner_articles","title":"Owner Articles","description":"Owner 的文章（信息流里的长文 / 博客帖）—— 三种 `kind`：`post`（自由文章）/ `portfolio`（精选代表作）/ `listing_attached`（挂某条 listing 的说明长文，**listing_id 必填**）。action=create|update|get|list|set_visibility|review|delete|report|list_reports|browse_feed。\n\n**文章 = 标题 + 摘要 + 封面 + markdown 正文。** 正文里可**嵌入 owner 自己已上传的图片/视频**：用 token 引用而不是外链 —— `![alt](hi-image:oi_xxx)` 嵌图片、`![](hi-video:ov_xxx)` 嵌视频（各自单独一行）。SSR 渲染时只对「owner 本人 public+approved 的资产」解析成受控 redirect，非本人/未审/不存在的 id 一律降级占位（绝不外链任意 URL）。所以嵌入前要先用 `owner_images` / `owner_videos` 上传并审过那张图/那条视频，拿到它的 id 再写进正文。\n\n**审核流：先过后审（AI 先审 + 用户举报）**：\n- AI moderation 跑在 `title` + `body_markdown` 上（文章正文可读）；**create / update 内容写入时 inline 完成**（无 worker）；\n- AI 判 approve 且 `request_public_on_approve=true`（默认）→ 自动 approved + public（reviewed_by='ai_moderator_v1'）；flag/error → pending+private 进 staff 队列；\n- **`update` 改了内容会重新过审**（回到 pending+private，AI 复审通过再自动公开）——所以编辑已公开文章会短暂下线到审过为止；\n- 任何能看到该文章的 caller 都可 `report`；staff 任意时刻可 `review` / `set_visibility` / `delete`。\n\n**用法**：\n- `create({title:\"...\", summary?, body_markdown:\"## 正文...\\n\\n![](hi-image:oi_abc)\", cover_image_id?:\"oi_xxx\", kind?, listing_id?})` → 返回 `article`（id=oa_xxx）+ `disclaimer`。\n- `update({article_id:\"oa_xxx\", title?, summary?, body_markdown?, cover_image_id?, ...})` → 只传要改的字段。\n- `cover_image_id` 必须是同一 owner 名下的 owner_images.id（否则 `cover_image_not_owned_by_caller`）。\n\n**browse_feed（全站发现 feed，匿名可读）**：`browse_feed({limit?, cursor?, kind?})` → `{ articles:[{id,kind,title,summary,cover_image_id,reading_time_minutes,owner:{display_name,headline,avatar_url,internal owner route (not returned to agents):null,is_anonymous},...}], next_cursor }`（不含正文；正文走 get / 公开文章页）。匿名 feed 不暴露 `/owner` URL；消费端用 consented `/p/<slug>` 数据解析公开人页。\n\n**权限规则**（跟 owner_videos 同口径）：\n- `create` / `update` 自己的 customer_id：任何 caller；给别人的只有 staff（staff create 默认 auto_approve=true 一步写完 approved+public）。\n- `review` / `list_reports`：仅 staff。`set_visibility` 把 pending 文章直接设 public：self 会 409 `article_not_approved`，staff 才允许。\n- `get` / `list` 别人 owner：默认只看 approved+public；staff 看全。`browse_feed`：匿名可读，永远只露 public+approved+feed_eligible。","handler_group":"owner_articles","scopes":["owner.article.read","owner.article.write","owner.article.review"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'create' | 'update' | 'get' | 'list' | 'set_visibility' | 'review' | 'delete' | 'report' | 'list_reports' | 'browse_feed'"},"customer_id":{"type":"string","description":"create / list 可选：目标 owner 的 customers.id。不传 = caller 自己。staff 路径必填别人的 customer_id。"},"article_id":{"type":"string","description":"update / get / set_visibility / review / delete / report 必填：article.id（oa_xxx）。list_reports 可选（按某文过滤）。"},"kind":{"type":"string","description":"create / update 可选：'post' | 'portfolio' | 'listing_attached'（默认 'post'）。browse_feed 可选：只看某 kind。"},"listing_id":{"type":"string","description":"create / update：kind='listing_attached' 时**必填**，且该 listing 必须属于同一个 owner；其它 kind 不要带。"},"title":{"type":"string","description":"create 必填 / update 可选：文章标题（≤200 字符）。会进 AI 先审。"},"summary":{"type":"string","description":"create / update 可选：信息流卡 / og 摘要（缺省由正文派生）。"},"body_markdown":{"type":"string","description":"create / update 可选：富文本正文 markdown（≤100000 字符）。可嵌 `![alt](hi-image:oi_xxx)` / `![](hi-video:ov_xxx)` 引用 owner 自己已上传且审过的图片/视频。会进 AI 先审。"},"cover_image_id":{"type":"string","description":"create / update 可选：封面图，引用同一 owner 名下的 owner_images.id（oi_xxx）。"},"request_public_on_approve":{"type":"boolean","description":"create / update 可选；默认 true。AI 审过后立即公开；设 false 则即使 AI 通过也只 approved+private。"},"feed_eligible":{"type":"boolean","description":"create / update 可选；默认 true（可进全站发现 feed）。设 false 则只在自己主页展示，不进 browse_feed。"},"auto_approve":{"type":"boolean","description":"create / update 可选；只在 staff 路径生效（create 默认 true，update 默认 false）；跳过 AI 审一步写成 approved+public。self 路径忽略。"},"review_status":{"type":"string","description":"review 必填：'approved' | 'rejected' | 'pending'。"},"review_notes":{"type":"string","description":"review 可选：审核意见（≤5000 字符）。"},"set_public_on_approve":{"type":"boolean","description":"review 配合 review_status='approved' 可选：默认 false；传 true 会一步到位 set 为 public。"},"visibility_status":{"type":"string","description":"set_visibility 必填：'private' | 'public'。self 路径只允许把 approved 文章设为 public。"},"kinds":{"type":"array","description":"list 可选：按 kind 过滤。","items":{"type":"string"}},"review_statuses":{"type":"array","description":"list 可选（仅 self/staff）：按状态过滤；非 owner/non-staff caller 此参数会被强制覆写为 ['approved']。","items":{"type":"string"}},"visibility":{"type":"string","description":"list 可选：'private' | 'public'。非 owner/non-staff caller 一律按 'public' 看。"},"limit":{"type":"number","description":"list 可选（默认 50，最大 200）/ browse_feed 可选（默认 20，最大 50）：返回条数上限。"},"offset":{"type":"number","description":"list 可选：分页 offset。"},"cursor":{"type":"string","description":"browse_feed 可选：上一页返回的 next_cursor（opaque）。不传 = 从最新一条开始。"},"reason_text":{"type":"string","description":"report 必填：举报原因（≤2000 字符）。"},"reason_category":{"type":"string","description":"report 可选：'spam' | 'illegal' | 'pii' | 'fraud' | 'copyright' | 'hate' | 'other'。不在白名单的归为 'other'。"},"status":{"type":"string","description":"list_reports 可选：'open' | 'dismissed' | 'upheld'。不传 = 全部。"},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.owner-articles/call","schema_path":"/v1/capabilities/hi.owner-articles/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Owner articles (rich-text posts with embedded media)"}},{"capability_id":"hi.owner-documents","tool_name":"owner_documents","title":"Owner Documents","description":"Owner public profile attachments — for posting portfolio / business_plan / pitch deck / resume / other documents as PDF / PPT / PPTX / DOCX. action=presign_upload|finalize_upload|submit_url|list|get|set_visibility|review|delete|report|list_reports.\n\n**Moderation flow: auto-approve with human override + user reports**:\n- After the owner uploads, a worker extracts text and immediately runs LLM moderation (OpenAI gpt-5.2) to check for obvious red flags (illegal content, adult content, hate speech, third-party PII violations, spam, etc.).\n- **AI approves** and the owner set `request_public_on_approve=true` (default) → automatically sets review_status='approved' + visibility_status='public', reviewed_by_principal_id='ai_moderator_v1'.\n- **AI flags** → document stays pending + private; enters the staff queue for human review.\n- Any caller who can see the document can `report` it. Reports do not affect visibility; they only queue the document for staff inspection (document.report_count increments).\n- Staff can `review` (override the AI decision), `set_visibility` to force private/public, or `delete` at any time.\n\n**Disclaimer**: Hi provides file hosting and display only; it is not responsible for the accuracy, legality, or views expressed in documents. Owners should be informed of this disclaimer before uploading (presign_upload / submit_url responses include `disclaimer.text`; agents should show it to the owner and have them confirm before issuing the PUT).\n\n**Two upload paths**:\n\n1) Hi-hosted direct upload (recommended for local PDF/PPTX files the owner has on hand):\n   - `presign_upload` → returns `document` (id=od_xxx, upload_status='pending_upload') + `upload` (presigned PUT URL). Required fields: `original_filename` + `mime_type` (must be in the allowlist, e.g. 'application/pdf' / 'application/vnd.openxmlformats-officedocument.presentationml.presentation'); optional `size_bytes` (pass if known — enables early rejection if the file is too large).\n   - The caller performs `fetch(upload.url, { method:'PUT', body: <bytes>, headers: upload.required_headers })` to upload the file directly to S3.\n   - `finalize_upload({document_id})` → Hi sends a HEAD request to the S3 object to get the real size + mime and enqueues text extraction. **The document is not visible and will not be extracted until finalize is called.**\n   - Defaults to `visibility_status='private'` + `review_status='pending'`. After receiving an approved notification, the owner calls `set_visibility({visibility_status:'public'})` to make it visible; staff can use `review` to complete both steps at once.\n\n2) Externally hosted (owner has already hosted the PDF on Notion / a file-sharing service / their own CDN):\n   - `submit_url({customer_id, source_url, mime_type, kind, title, ...})` → directly inserts a record with storage_kind='external_url'; visibility/review rules are the same as above.\n   - The extraction worker will make a best-effort GET request to fetch and extract text from the URL; if the external link is missing / returns 401 / 403, the error is recorded on the document but the caller is not blocked.\n\n**Permission rules** (same as owner_intro_videos):\n- `presign_upload` / `submit_url` / `finalize_upload` for your own customer_id: any caller.\n- The same three actions for another owner's customer_id: staff only (staff_global_authorities.state='active').\n- `review`: staff only. `set_visibility` setting a pending document directly to public: returns 409 `document_not_approved` on the self path; allowed on the staff path.\n- `get` / `list` for another owner: by default only documents with review_status='approved' + visibility_status='public' are visible; staff can see everything.\n\n**Read state**: In addition to `review_status` (pending/approved/rejected), responses include a derived `ai_review_state` field indicating how far along AI pre-moderation is, so owners know where things stand: `awaiting_upload` (presigned but not yet PUT/finalized) → `extraction_pending` (finalized; text extraction in progress) → `ai_review_in_progress` (extraction done; AI moderation running) → terminal states `approved`/`rejected`, or intermediate states `flagged_pending_review` (AI flagged; awaiting human review) / `ai_review_error` / `extraction_failed` / `pending_review` (external_url awaiting human review). **Important**: when review_status='pending' and ai_review_verdict='flag', do not just tell the owner \"waiting for review\" — say \"the AI flagged this for human review,\" explain the reason from `ai_review_notes`, and guide them to take action rather than just wait.\n\n**Typical agent usage**:\n- \"I want to add my portfolio PDF to my profile\" → `presign_upload({kind:'portfolio', original_filename:'portfolio_2026.pdf', mime_type:'application/pdf'})`, get back `upload.url`, have the owner/agent PUT directly, then `finalize_upload`. After receiving an approved notification, the owner calls `set_visibility(public)`.\n- \"I've already uploaded my business plan to a file-sharing service, here's the link: https://...\" → `submit_url({kind:'business_plan', source_url:'https://...', mime_type:'application/pdf'})`.\n- \"List all public portfolio documents for this owner\" → `list({customer_id:\"cust_xxx\", kinds:['portfolio'], visibility:'public'})` (allowed even if the caller is not the owner or staff, but only approved + public documents are visible).\n\n**Allowed MIME types** (all others return `unsupported_mime_type`):\n- application/pdf\n- application/vnd.openxmlformats-officedocument.presentationml.presentation (pptx)\n- application/vnd.ms-powerpoint (ppt — extraction not yet supported; stored as attachment only)\n- application/vnd.openxmlformats-officedocument.wordprocessingml.document (docx)\n- application/msword (doc)\n- application/vnd.apple.keynote (key — stored only; extraction not yet supported)\n\n**Maximum file size**: 50MB (BLOB_MAX_UPLOAD_BYTES, configurable). Presigned PUT URLs expire after 15 minutes by default.","handler_group":"owner_documents","scopes":["owner.document.read","owner.document.write","owner.document.review"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'presign_upload' | 'finalize_upload' | 'submit_url' | 'list' | 'get' | 'set_visibility' | 'review' | 'delete' | 'report' | 'list_reports'"},"customer_id":{"type":"string","description":"Optional for presign_upload / submit_url / list: the target owner's customers.id. Omit to default to the caller's own. Required on the staff path when acting on another owner's customer_id."},"document_id":{"type":"string","description":"Required for finalize_upload / get / set_visibility / review / delete / report: the document.id (od_xxx)."},"request_public_on_approve":{"type":"boolean","description":"Optional for presign_upload / submit_url; default true. Indicates that the owner wants the document made public immediately upon AI approval. Set to false if you want AI approval to only set review_status=approved + visibility=private, leaving it to the owner to call set_visibility=public manually."},"reason_text":{"type":"string","description":"Required for report: the report reason (≤2000 characters). Staff will see this text verbatim in list_reports / the review queue."},"reason_category":{"type":"string","description":"Optional for report: report category — 'spam' | 'illegal' | 'pii' | 'fraud' | 'copyright' | 'hate' | 'other'. Omitting or passing a value outside the allowlist results in null (still retrievable via list_reports)."},"status":{"type":"string","description":"Optional for list_reports: 'open' | 'dismissed' | 'upheld'. Omit to return all."},"kind":{"type":"string","description":"Optional for presign_upload / submit_url: 'portfolio' | 'business_plan' | 'pitch_deck' | 'resume' | 'other' (default 'other'). Determines how the document is grouped on the public profile page."},"original_filename":{"type":"string","description":"Required for presign_upload, optional for submit_url: the original filename (≤256 characters). Used as the S3 key suffix and the Content-Disposition for the GET presigned URL."},"mime_type":{"type":"string","description":"Required for presign_upload, optional for submit_url: MIME type (must be in the allowlist)."},"size_bytes":{"type":"number","description":"Optional for presign_upload / submit_url: file size in bytes. For presign_upload, passing this enforces Content-Length on the S3 side; files exceeding BLOB_MAX_UPLOAD_BYTES (default 50MB) are rejected immediately."},"source_url":{"type":"string","description":"Required for submit_url: https direct link, length ≤ 2048."},"title":{"type":"string","description":"Optional for presign_upload / submit_url: title (≤200 characters)."},"notes_markdown":{"type":"string","description":"Optional for presign_upload / submit_url: notes in markdown (≤5000 characters)."},"auto_approve":{"type":"boolean","description":"Optional for submit_url; only takes effect on the staff path (default true, same behavior as owner_intro_videos.submit_url)."},"reported_size_bytes":{"type":"number","description":"Optional for finalize_upload: the actual size the caller observed after uploading. The server uses this for a sanity hint only — the source of truth is the S3 HEAD response."},"review_status":{"type":"string","description":"Required for review: 'approved' | 'rejected' | 'pending'."},"review_notes":{"type":"string","description":"Optional for review: review comments (≤5000 characters)."},"set_public_on_approve":{"type":"boolean","description":"Optional for review when review_status='approved': default false (the owner decides when to make it public); pass true to set the document as visible in one step."},"visibility_status":{"type":"string","description":"Required for set_visibility: 'private' | 'public'. The self path only allows setting an approved document to public (pending/rejected documents are always private)."},"kinds":{"type":"array","description":"Optional for list: filter by kind, e.g. ['portfolio','business_plan'].","items":{"type":"string"}},"review_statuses":{"type":"array","description":"Optional for list (self/staff only): filter by review status. For non-owner/non-staff callers this parameter is always overridden to ['approved'].","items":{"type":"string"}},"visibility":{"type":"string","description":"Optional for list: 'private' | 'public'. Non-owner/non-staff callers always see 'public' documents only."},"limit":{"type":"number","description":"Optional for list: maximum number of results (default 50, max 200)."},"offset":{"type":"number","description":"Optional for list: pagination offset."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.owner-documents/call","schema_path":"/v1/capabilities/hi.owner-documents/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Owner documents (portfolio / BP / deck)"}},{"capability_id":"hi.owner-images","tool_name":"owner_images","title":"Owner Images","description":"Owner 的多张图片（照片 / 截图 / 海报 / 作品图）—— 三种 `kind`：`post`（信息流图片帖，独立于 listing）/ `portfolio`（精选作品图集）/ `listing_attached`（挂在某条 listing 下的配图，**listing_id 必填**）。action=presign_upload|finalize_upload|submit_url|list|get|get_download_url|set_visibility|review|delete|report|list_reports|browse_feed。\n\n**审核流：先过后审（AI 先审 + 用户举报）**，跟 owner_videos 完全同口径：\n- AI moderation 跑在 `title` + `caption_markdown` + `alt_text` 上（v1 不看像素 —— 像素视觉审核是后续 fast-follow）；\n- `submit_url`（external_url）/ `finalize_upload`（hi_local_s3）时 **inline 完成**；\n- AI 判 approve → review_status='approved'（reviewed_by='ai_moderator_v1'）；只有 `request_public_on_approve=true`（默认）才自动 public；`false` 则 approved+private，owner 后续可 `set_visibility('public')`；flag/error → pending+private 进 staff 队列；\n- 任何能看到该图片的 caller 都可 `report`（不动可见性，只入队，report_count 累加）；staff 任意时刻可 `review` / `set_visibility` / `delete`。\n\n**免责协议**：`presign_upload` / `submit_url` 返回值附带 `disclaimer.text`，agent 应在 UI 让 owner 看到并确认后再 PUT / 提交。\n\n**两条写入路径**：\n1) Hi 自托管上传：`presign_upload({kind, original_filename:\"photo.jpg\", mime_type:\"image/jpeg\", size_bytes?, width?, height?, alt_text?, title?, caption_markdown?})` → 返回 `image`（id=oi_xxx, upload_status='pending_upload'）+ `upload`（presigned PUT）。caller `fetch(upload.url, {method:'PUT', body:<bytes>, headers:upload.required_headers})` 直传 S3，然后 `finalize_upload({image_id})` → Hi HEAD 对象 + inline 跑 AI 先审。白名单 mime：image/jpeg / image/png / image/webp / image/gif。单文件 ≤ 25MB（OWNER_IMAGE_MAX_UPLOAD_BYTES，可配置）。\n2) 外链托管：`submit_url({kind, source_url:\"https://.../photo.jpg\", mime_type?, alt_text?, title?, caption_markdown?, listing_id?})` → 直接 INSERT storage_kind='external_url'，提交即跑 AI 先审。\n\n**listing_attached 规则**：`kind='listing_attached'` 时 `listing_id` 必填且必须属于同一 owner（否则 `listing_not_owned_by_caller` / `listing_not_found`）。其它 kind 不要带 listing_id。\n\n**browse_feed（全站发现 feed，匿名可读）**：`browse_feed({limit?, cursor?, kind?})` —— 游标分页列全站 public+approved+uploaded+feed_eligible 的图片，按 (created_at,id) 倒序。返回 `{ images:[{id,kind,title,caption_markdown,alt_text,width,height,download,media_projection?,owner:{display_name,headline,avatar_url,internal owner route (not returned to agents):null,is_anonymous},...}], next_cursor }`。`media_projection` 只在 owner 本身也公开且图片通过 Media Kernel 全部公开资格时出现，且只含 opaque media/backing refs、revision 与 active 状态；不含 customer/storage/internal gateway。匿名 feed 不暴露 `/owner` URL；消费端用 consented `/p/<slug>` 数据解析公开人页。owner 想让某图在自己主页可见但不进全站 feed，写入时传 `feed_eligible=false`。\n\n**权限规则**（跟 owner_videos 同口径）：\n- `presign_upload` / `submit_url` / `finalize_upload` 自己的 customer_id：任何 caller；给别人的只有 staff（staff submit_url 默认 auto_approve=true 一步写完 approved+public）。\n- `review` / `list_reports`：仅 staff。`set_visibility` 把 pending 图片直接设 public：self 会 409 `image_not_approved`，staff 才允许。\n- `get` / `get_download_url` / `list` 别人 owner：默认只看 approved+public；staff 看全。`browse_feed`：匿名可读，永远只露 public+approved+uploaded+feed_eligible。\n\n**Image endorsements / tagging people (tag_add / tag_respond / tag_list / tag_inbox)**, exactly the same semantics as owner_videos: the uploader can tag people who are pictured, featured, or mentioned in the image — this is not an ordinary @mention, but an **evidence-backed endorsement**: \"X uploaded this image and linked it to this person.\"\n- `tag_add({image_id, tags:[{tagged_customer_id?|tagged_email?|tagged_phone?, tagged_name?, tag_role?, note?}]})` (for a single tag, you may omit `tags` and use top-level tagged_* fields directly) — only the image owner or staff can add tags; the endorsement is always attributed to the image owner. Three resolution paths for the tagged person: an existing Hi owner (matched by tagged_customer_id or email/phone) is bound directly and receives an inbox notification; unmatched individuals create a **pending stranger** tag storing the email/phone. **Pending strangers are attached automatically when that person joins Hi and binds the matching phone/email (anchor backfill)** — their inbox notification is re-sent at that moment so they can accept/reject. No `claim_url` is returned anymore; to invite someone to claim, use the hirey.ai claim flow (the claim link on their /p page, or the \"text HiRey\" springboard) rather than forwarding a platform link. Default `tag_status='pending'` (weak mention). Rate limits: ≤ 50 tags per image, ≤ 100 per owner per day.\n- `tag_respond({tag_id, decision:'accepted'|'rejected'|'hidden'})` — **the tagged person themselves** (or staff) confirms, rejects, or hides the tag. `accepted` upgrades to a strong endorsement (public page shows \"featuring <Person>\" + appears in the tagged person's profile social proof) and **suggests a relationship edge** to the uploader; `rejected` is permanent and never displayed; `hidden` is a reversible hide.\n- `tag_remove({tag_id})` — the image owner (uploader) or staff removes a tag / joint participant. Hard-deletes it. (The tagged person removing themselves uses `tag_respond` with decision 'rejected'/'hidden'.)\n- `tag_list({image_id})` — lists all tags on an image. The image owner / staff see all statuses (staff can also see pending strangers' email/phone); other callers see only accepted (strong) + pending (weak \"mentions\") and never receive the tagged person's email/phone.\n- `tag_inbox({direction:'incoming'|'outgoing', tag_status?})` — \"which images am I tagged in\" (incoming) / \"who have I tagged\" (outgoing).\n- Public rendering intensity is a pure function of tag_status: tags are only shown when the image itself is public+approved; accepted = strong endorsement, pending = weak mention, rejected/hidden = not shown.","handler_group":"owner_images","scopes":["owner.image.read","owner.image.write","owner.image.review"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'presign_upload' | 'finalize_upload' | 'submit_url' | 'list' | 'get' | 'get_download_url' | 'set_visibility' | 'review' | 'delete' | 'report' | 'list_reports' | 'browse_feed' | 'tag_add' | 'tag_respond' | 'tag_remove' | 'tag_list' | 'tag_inbox'"},"customer_id":{"type":"string","description":"presign_upload / submit_url / list 可选：目标 owner 的 customers.id。不传 = caller 自己。staff 路径必填别人的 customer_id。tag_inbox：staff 可传它代查别人；否则忽略（默认 caller）。"},"image_id":{"type":"string","description":"finalize_upload / get / get_download_url / set_visibility / review / delete / report / tag_add / tag_list 必填：image.id（oi_xxx）。list_reports 可选（按某图过滤）。"},"kind":{"type":"string","description":"presign_upload / submit_url 可选：'post' | 'portfolio' | 'listing_attached'（默认 'post'）。browse_feed 可选：只看某 kind。"},"listing_id":{"type":"string","description":"presign_upload / submit_url：kind='listing_attached' 时**必填**，且该 listing 必须属于同一个 owner；其它 kind 不要带。"},"original_filename":{"type":"string","description":"presign_upload 必填，submit_url 可选：原始文件名（≤256 字符）。会出现在 S3 key 尾段。"},"mime_type":{"type":"string","description":"presign_upload 必填（白名单：image/jpeg / image/png / image/webp / image/gif），submit_url 可选。"},"size_bytes":{"type":"number","description":"presign_upload / submit_url 可选：文件大小（bytes）。presign_upload 时传了会 enforce S3 端 Content-Length；超过 OWNER_IMAGE_MAX_UPLOAD_BYTES（默认 25MB）直接拒。"},"width":{"type":"number","description":"presign_upload / submit_url 可选：图片宽（像素），metadata，前端排版/防 layout shift 用。"},"height":{"type":"number","description":"presign_upload / submit_url 可选：图片高（像素），metadata。"},"alt_text":{"type":"string","description":"presign_upload / submit_url 可选：无障碍/SEO 的 <img alt> 文案（≤2000 字符）。会进 AI 先审。"},"source_url":{"type":"string","description":"submit_url 必填：图片 https 直链，长度 ≤ 2048。"},"title":{"type":"string","description":"presign_upload / submit_url 可选：标题（≤200 字符）。会进 AI 先审。"},"caption_markdown":{"type":"string","description":"presign_upload / submit_url 可选：说明文案 markdown（≤5000 字符）。会进 AI 先审。"},"request_public_on_approve":{"type":"boolean","description":"presign_upload / submit_url 可选；默认 true。AI 审过后立即公开；设 false 则即使 AI 通过也只 approved+private，owner 再 set_visibility=public。"},"feed_eligible":{"type":"boolean","description":"presign_upload / submit_url 可选；默认 true（可进全站发现 feed）。设 false 则只在自己主页 / listing 页展示，不进 browse_feed。"},"auto_approve":{"type":"boolean","description":"submit_url 可选；只在 staff 路径生效（默认 true）；staff 路径设 false 才进 pending 队列。self 路径忽略。"},"reported_size_bytes":{"type":"number","description":"finalize_upload 可选：caller 上传完拿到的实际 size，server 只做 sanity 提示——真相还是 S3 HEAD。"},"review_status":{"type":"string","description":"review 必填：'approved' | 'rejected' | 'pending'。"},"review_notes":{"type":"string","description":"review 可选：审核意见（≤5000 字符）。"},"set_public_on_approve":{"type":"boolean","description":"review 配合 review_status='approved' 可选：默认 false；传 true 会一步到位 set 为 public。"},"visibility_status":{"type":"string","description":"set_visibility 必填：'private' | 'public'。self 路径只允许把 approved 图片设为 public。"},"kinds":{"type":"array","description":"list 可选：按 kind 过滤，如 ['post','portfolio']。","items":{"type":"string"}},"review_statuses":{"type":"array","description":"list 可选（仅 self/staff）：按状态过滤；非 owner/non-staff caller 此参数会被强制覆写为 ['approved']。","items":{"type":"string"}},"visibility":{"type":"string","description":"list 可选：'private' | 'public'。非 owner/non-staff caller 一律按 'public' 看。"},"limit":{"type":"number","description":"list 可选（默认 50，最大 200）/ browse_feed 可选（默认 20，最大 50）：返回条数上限。"},"offset":{"type":"number","description":"list 可选：分页 offset。"},"cursor":{"type":"string","description":"browse_feed 可选：上一页返回的 next_cursor（opaque）。不传 = 从最新一条开始。"},"reason_text":{"type":"string","description":"report 必填：举报原因（≤2000 字符）。"},"reason_category":{"type":"string","description":"report 可选：'spam' | 'illegal' | 'pii' | 'fraud' | 'copyright' | 'hate' | 'other'。不在白名单的归为 'other'。"},"status":{"type":"string","description":"list_reports 可选：'open' | 'dismissed' | 'upheld'。不传 = 全部。"},"tags":{"type":"array","description":"tag_add 可选（batch）：要 tag 的人列表；每项 { tagged_customer_id? | tagged_email? | tagged_phone?（选一个标识人——命中既有 Hi owner 直接绑定，否则创建 pending stranger），tagged_name?（pending 时的展示名），tag_role?（默认 featured），note? }。单个 tag 可省略 tags 直接用顶层 tagged_* 字段。","items":{"type":"object"}},"tagged_customer_id":{"type":"string","description":"tag_add 单项可选：要 tag 的既有 Hi owner 的 customers.id。"},"tagged_email":{"type":"string","description":"tag_add 单项可选：被 tag 人的 email。命中既有 Hi owner 直接绑定；否则创建 pending tag（对方登录绑定该邮箱时回填）。"},"tagged_phone":{"type":"string","description":"tag_add 单项可选：被 tag 人的手机号（任意格式，server 归一化为 E.164）。语义同 tagged_email。"},"tagged_name":{"type":"string","description":"tag_add 单项可选：pending stranger 的展示名（对方上 Hi 前公开页弱渲染 \"mentions <name>\"）。"},"tag_role":{"type":"string","description":"tag_add 可选：被 tag 人在图片里的角色 —— 'interviewee' | 'guest' | 'cohost' | 'coowner' | 'mentioned' | 'featured'（默认 'featured'）。interviewee/guest/cohost/coowner 为联合人角色（accept 后进对方信息流）；mentioned/featured 为弱提及（不进对方流）。"},"note":{"type":"string","description":"tag_add 可选：uploader 对这条 tag 的备注（≤2000 字符）。"},"tag_id":{"type":"string","description":"tag_respond / tag_remove 必填：owner_image_tags.id（oit_xxx）。"},"decision":{"type":"string","description":"tag_respond 必填：被 tag 人对 tag 的回应 —— 'accepted'（升级为强背书 + 进对方 profile）| 'rejected'（终态，永不展示）| 'hidden'（可逆隐藏）。accept 还会向 uploader 建议一条关系边。"},"direction":{"type":"string","description":"tag_inbox 可选：'incoming'（我被 tag，默认）| 'outgoing'（我 tag 过别人）。"},"tag_status":{"type":"string","description":"tag_inbox 可选：按状态过滤 —— 'pending' | 'accepted' | 'rejected' | 'hidden'。"},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.owner-images/call","schema_path":"/v1/capabilities/hi.owner-images/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Owner images (post / portfolio / listing image)"}},{"capability_id":"hi.owner-intro-videos","tool_name":"owner_intro_videos","title":"Owner Intro Videos","description":"Owner intro video — the short video attached to an owner's account profile. Its identity/media route is internal and is not a canonical public person-page link. action=presign_upload|finalize_upload|submit_url|list|get|get_download_url|set_active|review|delete.\n\n**Two upload paths** (aligned with owner_documents):\n\n1) Hi-hosted direct upload (recommended for local videos the owner has on hand):\n   - `presign_upload({original_filename:\"intro.mp4\", mime_type:\"video/mp4\", size_bytes, duration_seconds, title})` → returns `video` (id=oiv_xxx, upload_status='pending_upload') + `upload` (presigned PUT URL + required_headers).\n   - The caller performs `fetch(upload.url, { method:'PUT', body:<bytes>, headers:upload.required_headers })` to upload the video directly to S3.\n   - `finalize_upload({video_id})` → Hi sends a HEAD request to the S3 object to get the real size/mime and sets upload_status='uploaded'. Videos default to review_status='pending'; **the self path requires staff approval** before `set_active` can make the video visible on the public profile.\n   - Allowed video MIME types: video/mp4 / video/webm / video/quicktime / video/x-matroska. Maximum file size: 2GB (INTRO_VIDEO_MAX_UPLOAD_BYTES, configurable).\n\n2) Externally hosted (owner stores the video on their own CDN or a file-sharing service):\n   - `submit_url({source_url:\"https://.../intro.mp4\", mime_type, ...})` — same semantics as above; storage_kind is set to 'external_url'.\n\n**Typical usage**:\n- Owner says \"I want to add an intro video to my profile\": prefer `presign_upload` → PUT → `finalize_upload`. Avoid using external platform URLs as long-term source_url values — those links can break when the external platform deprecates them.\n- Staff/admin uploading on behalf of an owner: use the staff path; `submit_url` defaults to `auto_approve=true + set_active_on_approve=true` to complete in one step.\n- View an owner's video history: `list`.\n- Get a short-lived signed GET URL for playback: `get_download_url({video_id})` — returns a presigned URL for hi_local_s3 videos (default 1 hour) or the original URL for external_url videos.\n- Review a pending video: `review` + review_status=approved|rejected.\n- Switch the video playing on the public profile: `set_active` + video_id (must be approved and belong to this owner).\n\n**Permission rules**:\n- submit_url for your own customer_id: any caller. submit_url for another owner's customer_id: staff only.\n- auto_approve=true only takes effect on the staff path; it is ignored on the self path.\n- review / set_active for any owner: staff only.\n- list / get for your own owner: any caller. list / get for another owner: staff only (exception: any caller can call get on a video that is approved — for share link scenarios).\n\n**Video URL requirements**:\n- Must be https, length ≤ 2048. The platform does not infer the MIME type — the caller declares it in the `mime_type` field (e.g. 'video/mp4').\n- Callers may optionally pass size_bytes / duration_seconds / title / notes_markdown (5KB limit) as metadata — all optional.\n\n**Owner public profile rendering rules**:\n- Only the video pointed to by owner_profiles.intro_video_id, with review_status='approved', is rendered.\n- When a video is rejected, the service automatically clears owner_profiles.intro_video_id (to prevent a rejected video from continuing to play).\n- Videos in pending status do not appear on the public profile, but the owner can see them via their own list call.\n\nDo not use this tool to upload an owner's avatar (external avatar URLs go through owners.update_profile's avatar_url field; local images use owners.request_avatar_upload). Also do not use this tool for listing-related videos (that belongs in listing.text, not here).","handler_group":"owner_intro_videos","scopes":["owner.intro_video.read","owner.intro_video.write","owner.intro_video.review"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'presign_upload' | 'finalize_upload' | 'submit_url' | 'list' | 'get' | 'get_download_url' | 'set_active' | 'review' | 'delete' (delete: removes a video; the owner can delete their own, staff can delete any; pass video_id)"},"customer_id":{"type":"string","description":"Optional for presign_upload / submit_url / list / set_active: the target owner's customers.id. Omit to default to the caller's own. Required on the staff path when acting on another owner's customer_id."},"video_id":{"type":"string","description":"Required for finalize_upload / get / get_download_url / set_active / review / delete. Pass null for set_active to clear the active video (stop playing any video on the public profile)."},"original_filename":{"type":"string","description":"Required for presign_upload: the original filename (≤256 characters), used as the S3 key suffix and the Content-Disposition for the GET presigned URL."},"reported_size_bytes":{"type":"number","description":"Optional for finalize_upload: the size the caller observed after uploading to S3. The server uses this for a sanity check only — the source of truth is the S3 HEAD response."},"source_url":{"type":"string","description":"Required for submit_url: the video's https direct link, length ≤ 2048."},"storage_kind":{"type":"string","description":"Optional for submit_url: 'external_url' (default; https direct link) | 'hi_local_s3' (reserved; the presign_upload path selects this automatically — no need to pass it in submit_url)."},"mime_type":{"type":"string","description":"Optional for submit_url: MIME type, e.g. 'video/mp4' / 'video/webm'. Defaults to unknown if omitted."},"size_bytes":{"type":"number","description":"Optional for submit_url: video file size in bytes. Stored as metadata only; not strictly validated."},"duration_seconds":{"type":"number","description":"Optional for submit_url: duration in seconds. Stored as metadata only."},"title":{"type":"string","description":"Optional for submit_url: video title, ≤200 characters."},"notes_markdown":{"type":"string","description":"Optional for submit_url: notes in markdown, ≤5000 characters. When staff uploads on behalf of an owner, this typically records audit info such as \"recorded offline on YYYY-MM-DD\"."},"auto_approve":{"type":"boolean","description":"Optional for submit_url; only takes effect on the staff path. **Defaults to true for staff** (videos recorded offline by staff are considered pre-reviewed and do not need another review pass). Pass false explicitly to route the video into the pending queue. Ignored on the self path — always goes through pending."},"set_active_on_approve":{"type":"boolean","description":"Optional for submit_url (paired with auto_approve=true) or review (paired with review_status=approved). Default true: once approved, the video is immediately set as the active video on the owner's public profile."},"review_status":{"type":"string","description":"Required for review: 'approved' | 'rejected' | 'pending'."},"review_notes":{"type":"string","description":"Optional for review: review comments, ≤5000 characters. Stored verbatim in the review_notes field for later retrieval."},"review_statuses":{"type":"array","description":"Optional for list: filter by status, e.g. ['pending']. Omit to return all statuses.","items":{"type":"string"}},"limit":{"type":"number","description":"Optional for list: maximum number of results (default 50, max 200)."},"offset":{"type":"number","description":"Optional for list: pagination offset."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.owner-intro-videos/call","schema_path":"/v1/capabilities/hi.owner-intro-videos/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Owner intro videos"}},{"capability_id":"hi.owner-videos","tool_name":"owner_videos","title":"Owner Videos","description":"An owner's multiple videos (distinct from the single hero video in owner_intro_videos) — three `kind` values: `post` (a free-form short video post, independent of any listing) / `portfolio` (a curated showcase video collection) / `listing_attached` (an intro video attached to a specific listing; **listing_id required**). action=presign_upload|finalize_upload|submit_url|list|get|get_download_url|set_visibility|review|delete|report|list_reports|browse_feed.\n\n**Moderation flow: publish-first, review-after (AI pre-screening + user reports)**:\n- Videos have no extractable text body, so AI moderation runs on `title` + `caption_markdown` (OpenAI gpt-5.2, checking for obvious red flags: illegal content, adult content, hate speech, privacy violations, fraud, piracy, etc.);\n- Moderation runs **inline** at `submit_url` (external_url) / `finalize_upload` (hi_local_s3) — no separate worker, minimal wait time;\n- **AI approves** and the owner set `request_public_on_approve=true` (default) → automatically sets review_status='approved' + visibility_status='public', reviewed_by_principal_id='ai_moderator_v1';\n- **AI flags / errors** → leaves as pending + private, enters the staff queue for human review;\n- Any caller who can see the video may `report` it (reports do not affect visibility — they only queue it for staff review and increment video.report_count);\n- Staff may `review` (override AI decision), `set_visibility` to force private/public, or `delete` at any time.\n\n**Disclaimer**: Hi provides video hosting and display only; it is not responsible for a video's accuracy, legality, or expressed views. The response from `presign_upload` / `submit_url` includes a `disclaimer.text` — the agent should show it to the owner and get their acknowledgment before executing the PUT / submit.\n\n**Two upload paths**:\n\n1) Hi self-hosted upload (recommended for owner's local videos):\n   - `presign_upload({kind, original_filename:\"clip.mp4\", mime_type:\"video/mp4\", size_bytes, duration_seconds, title, caption_markdown, thumbnail_filename?, thumbnail_mime_type?})` → returns `video` (id=ov_xxx, upload_status='pending_upload') + `upload` (presigned PUT for the video itself) + `thumbnail_upload` (**a second presigned PUT for the thumbnail image**, non-null only when thumbnail_filename was provided).\n   - The caller performs `fetch(upload.url, { method:'PUT', body:<bytes>, headers:upload.required_headers })` to upload the video to S3; if a thumbnail was provided, similarly PUT it to `thumbnail_upload.url`.\n   - `finalize_upload({video_id})` → Hi HEADs the video object (must exist) + HEADs the thumbnail object (**optional** — absence is allowed; missing thumbnail is left as null), then **runs AI pre-screening inline**.\n   - Allowed video MIME types: video/mp4 / video/webm / video/quicktime / video/x-matroska. Allowed thumbnail MIME types: image/jpeg / image/png / image/webp. Single file limit: ≤ 2 GB (OWNER_VIDEO_MAX_UPLOAD_BYTES, configurable).\n\n2) External-hosted URL (owner already has the video on their own CDN or a hosting service):\n   - `submit_url({kind, source_url:\"https://.../clip.mp4\", mime_type?, thumbnail_url?, title?, caption_markdown?, listing_id?})` → inserts directly with storage_kind='external_url'; AI pre-screening runs immediately on submit; visibility/review rules are the same as above.\n\n**Rename / edit (update)**: `update({video_id, title?, caption_markdown?})` — the video owner or staff renames a video or edits its caption after upload (omit a field to leave it unchanged). If the text actually changes, the video is re-submitted to AI moderation (review resets to pending + private, then re-auto-publishes if clean), so an already-public video may briefly go private while re-moderating.\n\n**联合视频 (joint video)**: a video tagged with a JOINT role (interviewee/guest/cohost/coowner) that the other person has `accepted` becomes a joint video — it appears in **every** accepted participant's feed (not just the uploader's), each card labeled with the counterpart owner(s). Use `tag_add` with `tag_role:'coowner'` (equal co-presenter) for a symmetric joint video, or interviewee/guest/cohost for an interview. Remove a participant with `tag_remove`.\n\n**listing_attached rules**: when `kind='listing_attached'`, `listing_id` is required and that listing must exist and belong to the same owner (otherwise `listing_not_owned_by_caller` / `listing_not_found`). Other kinds must not include listing_id (it will be ignored and set to null).\n\n**browse_feed (platform-wide discovery feed, readable anonymously)**: `browse_feed({limit?, cursor?, kind?})` — cursor-paginated list of all platform-wide public + approved + uploaded + feed_eligible videos, ordered by (created_at, id) descending. `listing_attached` videos additionally require their listing to exist and be public, otherwise they are excluded from the feed. Returns `{ videos:[{id,kind,title,caption_markdown,download,thumbnail_download,owner:{display_name,headline,avatar_url,internal owner route (not returned to agents):null,is_anonymous},...}], next_cursor }`; `next_cursor` being null means the end of the list. Anonymous feed responses do not expose `/owner` URLs; resolve public person pages from consented `/p/<slug>` data in the consuming surface. **Private reviewer notes and AI moderation details are never returned.** To make a video visible on the owner's profile page but exclude it from the platform-wide feed, pass `feed_eligible=false` when writing it.\n\n**Video endorsements / tagging people (tag_add / tag_respond / tag_list / tag_inbox)**: the uploader can tag people who are interviewed, featured, or mentioned in the video — this is not an ordinary @mention, but an **evidence-backed endorsement**: \"X uploaded this video and linked it to this person.\"\n- `tag_add({video_id, tags:[{tagged_customer_id?|tagged_email?|tagged_phone?, tagged_name?, tag_role?, note?}]})` (for a single tag, you may omit `tags` and use top-level tagged_* fields directly) — only the video owner or staff can add tags; the endorsement is always attributed to the video owner. Three resolution paths for the tagged person: an existing Hi owner (matched by tagged_customer_id or email/phone) is bound directly and receives an inbox notification; unmatched individuals create a **pending stranger** tag storing the email/phone. **Pending strangers are attached automatically when that person joins Hi and binds the matching phone/email (anchor backfill)** — their inbox notification is re-sent at that moment so they can accept/reject. No `claim_url` is returned anymore; to invite someone to claim, use the hirey.ai claim flow (the claim link on their /p page, or the \"text HiRey\" springboard) rather than forwarding a platform link. Default `tag_status='pending'` (weak mention). Rate limits: ≤ 50 tags per video, ≤ 100 per owner per day.\n- `tag_respond({tag_id, decision:'accepted'|'rejected'|'hidden'})` — **the tagged person themselves** (or staff) confirms, rejects, or hides the tag. `accepted` upgrades to a strong endorsement (public page shows \"X interviewed/featuring <Person>\" + appears in the tagged person's profile social proof), and **suggests a relationship edge** to the uploader; `rejected` is permanent and never displayed; `hidden` is a reversible hide.\n- `tag_remove({tag_id})` — the video owner (uploader) or staff removes a tag / 联合人 (the \"edit joint participants\" delete path). Hard-deletes the tag; the joint video immediately drops off that person's feed. (The tagged person removing themselves uses `tag_respond` with decision 'rejected'/'hidden'.)\n- `tag_list({video_id})` — lists all tags on a video. The video owner / staff see all statuses (staff can also see pending strangers' email/phone); other callers see only accepted (strong) + pending (weak \"mentions\") and never receive the tagged person's email/phone.\n- `tag_inbox({direction:'incoming'|'outgoing', tag_status?})` — \"which videos am I tagged in\" (incoming) / \"who have I tagged\" (outgoing).\n- Public rendering intensity is a pure function of tag_status: tags are only shown when the video itself is public+approved; accepted = strong endorsement, pending = weak mention, rejected/hidden = not shown.\n\n**Permission rules** (same as owner_documents):\n- `presign_upload` / `submit_url` / `finalize_upload` for the caller's own customer_id: any caller; for another person's customer_id: staff only (staff submit_url defaults to auto_approve=true, completing the write as approved+public in one step).\n- `review` / `list_reports`: staff only. `set_visibility` to set a pending video directly to public: the self path returns 409 `video_not_approved`; the staff path allows it.\n- `get` / `get_download_url` / `list` for another owner: defaults to review_status='approved' + visibility_status='public' only; staff sees everything.\n- `browse_feed`: readable anonymously, always limited to public+approved+uploaded+feed_eligible (and, for listing_attached, the listing must be public).","handler_group":"owner_videos","scopes":["owner.video.read","owner.video.write","owner.video.review"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'presign_upload' | 'finalize_upload' | 'submit_url' | 'update' | 'list' | 'get' | 'get_download_url' | 'set_visibility' | 'review' | 'delete' | 'report' | 'list_reports' | 'browse_feed' | 'tag_add' | 'tag_respond' | 'tag_remove' | 'tag_list' | 'tag_inbox'"},"customer_id":{"type":"string","description":"presign_upload / submit_url / list optional: the target owner's customers.id. Omit to default to the caller. For the staff path, required when targeting another owner. tag_inbox: staff may pass it to query on behalf of another user; otherwise ignored (defaults to caller)."},"video_id":{"type":"string","description":"finalize_upload / get / get_download_url / set_visibility / review / delete / report required: video.id (ov_xxx). list_reports optional (filter by a specific video)."},"kind":{"type":"string","description":"presign_upload / submit_url optional: 'post' | 'portfolio' | 'listing_attached' (default 'post'). browse_feed optional: filter to a specific kind."},"listing_id":{"type":"string","description":"presign_upload / submit_url: **required** when kind='listing_attached'; that listing must belong to the same owner. Must not be provided for other kinds."},"original_filename":{"type":"string","description":"presign_upload required, submit_url optional: original filename (≤256 characters). Appears in the S3 key suffix."},"mime_type":{"type":"string","description":"presign_upload required (allowed: video/mp4 / video/webm / video/quicktime / video/x-matroska), submit_url optional."},"size_bytes":{"type":"number","description":"presign_upload / submit_url optional: file size in bytes. When provided for presign_upload, enforces Content-Length on the S3 side; rejected immediately if it exceeds OWNER_VIDEO_MAX_UPLOAD_BYTES (default 2 GB)."},"duration_seconds":{"type":"number","description":"presign_upload / submit_url optional: duration in seconds; stored as metadata."},"thumbnail_filename":{"type":"string","description":"presign_upload optional: thumbnail (poster) image filename. Only when provided will the response include a second presigned PUT (thumbnail_upload)."},"thumbnail_mime_type":{"type":"string","description":"presign_upload optional: thumbnail MIME type (allowed: image/jpeg / image/png / image/webp; default image/jpeg)."},"thumbnail_url":{"type":"string","description":"submit_url optional: external thumbnail image direct HTTPS URL (≤2048 characters)."},"source_url":{"type":"string","description":"submit_url required: video direct HTTPS URL, length ≤ 2048."},"title":{"type":"string","description":"presign_upload / submit_url optional: title (≤200 characters). Included in AI pre-screening."},"caption_markdown":{"type":"string","description":"presign_upload / submit_url optional: caption text in markdown (≤5000 characters). Included in AI pre-screening."},"request_public_on_approve":{"type":"boolean","description":"presign_upload / submit_url optional; default true. Makes the video public immediately after AI approval. Set to false to keep it at review_status=approved + visibility=private even after AI approval — the owner then sets visibility to public manually."},"feed_eligible":{"type":"boolean","description":"presign_upload / submit_url optional; default true (eligible for the platform-wide discovery feed). Set to false to show only on the owner's profile / listing page, excluding it from browse_feed."},"auto_approve":{"type":"boolean","description":"submit_url optional; effective only on the staff path (default true, consistent with owner_documents.submit_url); set false on the staff path to put it in the pending queue instead. Ignored on the self path."},"reported_size_bytes":{"type":"number","description":"finalize_upload optional: the actual size obtained by the caller after upload; the server uses it only as a sanity hint — the source of truth is the S3 HEAD response."},"review_status":{"type":"string","description":"review required: 'approved' | 'rejected' | 'pending'."},"review_notes":{"type":"string","description":"review optional: reviewer notes (≤5000 characters)."},"set_public_on_approve":{"type":"boolean","description":"review optional, used with review_status='approved': default false; pass true to also set the video to public in one step."},"visibility_status":{"type":"string","description":"set_visibility required: 'private' | 'public'. The self path only allows setting an approved video to public."},"kinds":{"type":"array","description":"list optional: filter by kind, e.g. ['post','portfolio'].","items":{"type":"string"}},"review_statuses":{"type":"array","description":"list optional (self/staff only): filter by review status; for non-owner/non-staff callers this parameter is forced to ['approved'].","items":{"type":"string"}},"visibility":{"type":"string","description":"list optional: 'private' | 'public'. Non-owner/non-staff callers always see 'public' only."},"limit":{"type":"number","description":"list optional (default 50, max 200) / browse_feed optional (default 20, max 50): maximum number of results to return."},"offset":{"type":"number","description":"list optional: pagination offset."},"cursor":{"type":"string","description":"browse_feed optional: the next_cursor returned by the previous page (opaque). Omit to start from the most recent."},"reason_text":{"type":"string","description":"report required: reason for the report (≤2000 characters)."},"reason_category":{"type":"string","description":"report optional: 'spam' | 'illegal' | 'pii' | 'fraud' | 'copyright' | 'hate' | 'other'. Values not in the allowlist are mapped to 'other'."},"status":{"type":"string","description":"list_reports optional: 'open' | 'dismissed' | 'upheld'. Omit to return all."},"tags":{"type":"array","description":"tag_add optional (batch): list of people to tag; each item is { tagged_customer_id? | tagged_email? | tagged_phone? (pick one to identify the person — if matched to an existing Hi owner they are bound directly; otherwise a pending stranger is created), tagged_name? (display name while pending), tag_role? (default featured), note? }. For a single tag, you may omit tags and use the top-level tagged_* fields instead.","items":{"type":"object"}},"tagged_customer_id":{"type":"string","description":"tag_add single-item optional: customers.id of an existing Hi owner to tag."},"tagged_email":{"type":"string","description":"tag_add single-item optional: email address of the person to tag. If matched to an existing Hi owner, bound directly; otherwise creates a pending tag (back-filled when they sign in and bind that email)."},"tagged_phone":{"type":"string","description":"tag_add single-item optional: phone number of the person to tag (any format; server normalizes to E.164). Same semantics as tagged_email."},"tagged_name":{"type":"string","description":"tag_add single-item optional: display name for a pending stranger (shown on the public page as a weak \"mentions <name>\" before they join Hi)."},"tag_role":{"type":"string","description":"tag_add optional: the tagged person's role in the video — 'interviewee' | 'guest' | 'cohost' | 'coowner' | 'mentioned' | 'featured' (default 'featured'). 'interviewee' | 'guest' | 'cohost' | 'coowner' are JOINT roles (联合人): once accepted, the video becomes a 联合视频 and appears in that person's feed too, labeled with the counterpart owner. 'coowner' = equal co-presenter (joint video). 'mentioned' | 'featured' are weak mentions (don't enter the other person's feed). Determines public copy: interviewee → 'X interviewed <Person>', others → 'featuring <Person>'."},"note":{"type":"string","description":"tag_add optional: uploader's note about this tag (≤2000 characters)."},"tag_id":{"type":"string","description":"tag_respond required: owner_video_tags.id (ovt_xxx)."},"decision":{"type":"string","description":"tag_respond required: the tagged person's response to the tag — 'accepted' (upgrades to strong endorsement + appears on their profile) | 'rejected' (permanent, never shown) | 'hidden' (reversible hide). Accepting also suggests a relationship edge to the uploader."},"direction":{"type":"string","description":"tag_inbox optional: 'incoming' (I have been tagged, default) | 'outgoing' (I have tagged others)."},"tag_status":{"type":"string","description":"tag_inbox optional: filter by status — 'pending' | 'accepted' | 'rejected' | 'hidden'."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.owner-videos/call","schema_path":"/v1/capabilities/hi.owner-videos/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Owner videos (post / portfolio / listing video)"}},{"capability_id":"hi.owners","tool_name":"owners","title":"Owners","description":"Hi is Hirey's people-to-people connection platform — it supports recruiting and job searching, finding or listing housing, making friends, dating and marriage, finding lawyers, connecting with investors, founders, and cofounders, and any other leads-search-and-matching use case that helps an owner find the right people. The typical main flow is: publish or read a listing, search or receive recommendations, select someone and reach out, then continue collaborating, call, or schedule a meeting. Canonical public profile and profile-scoped discovery for an owner (the real person behind a listing): action=update_profile|import_profile|request_avatar_upload|finalize_avatar_upload|get|list_listings|peers_feed|search|inbox|inbox_ack|reliability.\n\n**When to call**:\n- The user **introduces themselves for the first time** (name, title, bio, location, personal links) or updates that information — immediately call `update_profile` to write the structured fields. The LLM should parse natural language and fill only the fields that are present; leave anything missing blank — do not fabricate values to appear complete. In particular, NEVER infer `location_text` from your host environment, IP address, VPN/proxy, timezone, or locale — only set location from what the user actually says (a VPN/remote user gets mislocated otherwise, and location drives the map + same-city matching); if unstated, leave it blank and ask. Typical example: \"I'm Alex, 8 years of backend in San Francisco, looking to hire a frontend engineer\" — write display_name=\"Alex\", headline=\"San Francisco backend engineer (8y)\", bio_markdown=\"...\"; the second half of that statement (\"hire a frontend engineer\") is a listing need and should be handled separately via `agent_listings.upsert`.\n- The user has an existing bio elsewhere and wants to **fill their Hi profile from it fast** — e.g. \"here's my LinkedIn / my bio, set up my profile\" and pastes a blob of text. Call `import_profile` with `bio_text`=<the pasted text>. It returns a structured `draft` (display_name / headline / bio_markdown / location_text / website_url / handles) extracted from the text — show the draft to the user, then call `import_profile` again with `apply:true` to save it (only non-empty fields are written). **Paste-text only**: Hi never fetches a URL or scrapes a page on your behalf; if the user only has a link, ask them to paste the page's text.\n- A peer agent returned a listing or pairing and you want to learn more about who the other owner is — call `get` with `owner_public_id` or `customer_id` to read their profile.\n- See what other publicly active listings a given owner has — call `list_listings`.\n- The user casually says \"show me who else is interesting on Hi\" / \"recommend some people I might connect with\" / right after install during the welcome flow — call `peers_feed` to get a set of profile-scoped recommended owner cards.\n- The user **explicitly provides a name or keyword** to find someone or find a listing (\"find someone named Walter\" / \"is there a founder working on agent infra\" / \"search for backend roles in San Francisco\") — call `search` (q=keyword). This is an anonymous fuzzy search across owner profiles and public listings with partial matching and typo tolerance; **no listing is required first**. Use `matching_sessions.search` only for structured role/requirement matching when a listing already exists.\n- The user asks **\"what needs my attention\" / \"what's in my inbox\" / \"anything I need to respond to\" / \"catch me up\"** — call `inbox`. This is a single owner-scoped overview that aggregates the caller's actionable items across surfaces (in-flight pairings, pending/upcoming meetings, open tasks, and unread inbound messages) into one time-sorted list, deduplicated across all your devices/agents in the same workspace. Each item carries {kind, id, title, preview, counterparty, occurred_at, unread, cta, link}; the `cta` is the exact follow-up call ({capability, action, args}) to act on that item. Bounded (limit default 30, max 100) with a `next_cursor` for paging. Anonymous/unbound callers get an empty list + onboarding hint. The response also carries `workspace_reply_enabled` (boolean capability probe): when true, a sibling workspace agent may reply on behalf of a pairing participant, so a web surface can light up reply/action buttons directly.\n- The human has **actually read message items on a web surface** (e.g. the hirey.ai/me inbox) — call `inbox_ack` with the `event_ids` of the kind='message' items they saw. This marks those message-class inbox events as seen by the human: it stops the platform's \"you have a new message\" reminder emails/SMS for those messages, and other hosts will no longer receive them from the event stream. Only message-class events targeted at the caller's own workspace agents can be consumed (meeting/company/task/system events are not consumable here — they stay in the inbox until acted on); re-acking an already-consumed event is harmless (consumed:0). Returns {ok, consumed, skipped}. Do NOT call this speculatively — only after the human has genuinely seen the messages.\n- The user (or another agent vetting a counterparty) wants a **trust / track-record signal for a person** (\"is this person reliable\", \"has anyone here actually shown up to meetings\") — call `reliability` (owner_public_id, or omit to read your own). Returns a PUBLIC-SAFE summary {owner_public_id, meetings_completed, response_rate, sample_size, signals[], low_sample, computed_at}: meetings the owner has actually attended on Hi + how often they reply to inbound contacts. It deliberately **never** exposes no-show counts. When `low_sample` is true the sample is too small to display a badge — render nothing rather than implying a low score.\n\n  - **External web fallback (finding people outside the platform)**: when `search` finds **neither an owner nor a listing** on Hi (both people and listings are empty) and `q` was provided, the response includes `external_candidates[]` — real people found via Exa web search who **have a contactable email or phone** but are not yet on Hi. Each entry contains {external_candidate_token, name, headline, company, location, profile_url, summary, has_email, has_phone}; **contact details are intentionally withheld**. This is the correct path for \"find me someone who works at Exa / this company\" when the person is not on Hi. You can also pass `external:true` to force external candidates even when there are on-platform results. To actually reach out or record the lead → `matching_sessions(action=\"select_external\", external_candidate_tokens=[...], text=\"<opening message>\")`: Hi records it as a BD lead for ops follow-up, pre-creates an account from the scraped data, and the person will see the profile and your message once they sign in with that email or phone.\n\n**Boundaries of update_profile**:\n- The caller can only modify their own profile (the caller owner injected at runtime); passing `customer_id` to modify someone else will result in a 403.\n- All fields are nullable with partial validation: avatar_url / website_url / linkedin_url must be http(s) URLs; visibility_status must be 'public' / 'private' / 'unlisted' (omit to keep current; defaults to public).\n- bio_markdown accepts Markdown and is rendered on the owner-facing public page.\n- Owner identity/contact routing ids in tool results are machine-only. Never display an owner id or construct/share a profile URL from it. A canonical public person link must come from an independently verified `https://hirey.ai/p/<slug>` authority; if none is present, say that the public page link is not ready.\n\n**Two paths for avatar upload**:\n1) The owner already has a public image URL → `update_profile({avatar_url:\"https://...\"})` and the platform stores it as-is.\n2) The owner only has a local image file (most common) → self-hosted upload via Hi, three steps:\n   - `request_avatar_upload({mime_type:\"image/jpeg\", original_filename?, size_bytes?})` → returns `avatar` (id=oav_xxx) + `upload` (presigned PUT URL + required_headers).\n   - The caller performs `fetch(upload.url, { method:'PUT', body:<bytes>, headers:upload.required_headers })` to upload the image.\n   - `finalize_avatar_upload({avatar_id})` → the platform verifies via S3 HEAD and points the account profile at its stable media route; no further call to update_profile is needed. The media result is not a canonical public person-page link.\n   - Allowed MIME types: image/jpeg | image/png | image/webp | image/gif (heic/svg not accepted); maximum 5 MB per file. Uploading again replaces the previous avatar (the last finalized upload wins).\n\n**Boundaries of peers_feed / avoid misuse**:\n- Returns `{items[], caller_profile_ready}`. Each item in items[] is a profile snippet (display_name / headline / location_text / avatar_url) plus an internal owner routing id and `suggested_because` ('recent_active' | 'same_location'). The routing id is only for a follow-up tool call and must never be shown to the user.\n- This is **shallow discovery**, not a contact entry point — the response **intentionally omits** `listing_id` / `selection_key` / `contact_match_ref`. After displaying results, actually contacting someone still requires both parties to have a listing and to go through the matching_sessions / pairings main flow. Do not attempt to use owner_public_id directly with `pairings.create`.\n- If `caller_profile_ready=false` (the caller's own profile is missing display_name or headline), first prompt the user to call `update_profile` to fill those in, so their card doesn't look like a ghost to others.\n- Owners with visibility=private/unlisted will not appear in items[]; owners who already have a pairing with the caller are also excluded — deduplication across peers_feed calls is platform-guaranteed.\n\n**Profile before listing**: before publishing a listing, make sure the profile has at least display_name + headline — matching_sessions / pairings counterpart summaries read these two fields (if missing, they fall back to customers.name, which looks awkward).","handler_group":"identity","scopes":["owner.profile.read","owner.profile.write","owner.listings.read","owner.peers.read"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'update_profile'|'request_avatar_upload'|'finalize_avatar_upload'|'get'|'list_listings'|'peers_feed'|'search'|'set_device_label'|'list_agents'|'inbox'|'inbox_ack'|'reliability'. list_agents: lists all devices/agents in your current workspace ({agent_id, device_label, status, last_seen, is_self} + agents_in_workspace). Used in the onboarding 'new user vs. returning user' branch — returning users can bind the same phone/email and use this to confirm their existing devices before deciding to continue on this one or claim and reuse an existing agent. No parameters required (defaults to your own workspace); if unbound, returns only this device. inbox: owner-scoped unified list of actionable items across pairings/meetings/tasks/unread-messages (caller-scoped; no parameters required beyond optional limit/cursor). Person-bearing items expose counterparty {display_name, owner_public_id, avatar_url} when available; owner_public_id is machine-only and must never be displayed. inbox_ack: mark message-class inbox events as seen by the human on a web surface (pass event_ids); stops platform reminder emails/SMS for those messages, and other hosts will no longer receive them from the event stream. reliability: PUBLIC-SAFE trust summary for an owner (owner_public_id, or omit to read your own)."},"q":{"type":"string","description":"Required for search: free-text query. Performs fuzzy search across owner profiles (display_name / headline / bio / location, with customers.name as fallback) and public listings (body + summary) — partial matching with typo tolerance (≥2 characters; all words must match in multi-word queries). This is the anonymous-accessible entry point for 'find a person or listing by name/content' — no listing is required first. Returns two groups: people[] (owner profile cards plus a machine-only routing id) and listings[] (listing previews + publisher profile cards). Never show the routing id or derive a URL from it. Typical examples: 'find someone named Walter', 'is there a founder or listing for agent infra'."},"display_name":{"type":"string","description":"Optional for update_profile: the owner's public display name (e.g. 'I'm Alex' in the user's words). Omit to keep current."},"headline":{"type":"string","description":"Optional for update_profile: a single-line identity summary (e.g. 'San Francisco backend engineer, 8y' / 'Beijing landlord, Chaoyang 3BR' / 'Founder of XYZ, hiring eng #2'). Referenced in matching/pairings counterpart summaries."},"bio_markdown":{"type":"string","description":"Optional for update_profile: longer self-introduction (Markdown). Displayed on the owner public page and in the pairings initial contact card."},"location_text":{"type":"string","description":"Optional for update_profile: location as natural-language text (e.g. \"San Francisco, USA\" / \"Shanghai Pudong\"). Set this ONLY from what the user explicitly tells you. NEVER infer location from the host environment, IP address, VPN/proxy, timezone, or locale — a VPN or remote user will be silently mislocated, and location drives the map and same-city matching. If the user has not stated a location, leave it blank and ask them. Listings also have a location field, but storing one on the owner profile lets the other party see it at a glance."},"avatar_url":{"type":"string","description":"Optional for update_profile: avatar image URL (external link); must be http(s). Leave blank if the owner has none — do not guess or construct one. If the owner has a local image file, use request_avatar_upload instead."},"mime_type":{"type":"string","description":"Required for request_avatar_upload: image MIME type — 'image/jpeg' | 'image/png' | 'image/webp' | 'image/gif'."},"original_filename":{"type":"string","description":"Optional for request_avatar_upload: original filename (≤256 characters), used as the trailing segment of the S3 key."},"size_bytes":{"type":"number","description":"Optional for request_avatar_upload: image file size in bytes (≤5 MB). When provided, it is baked into the presigned PUT signature for strict enforcement."},"avatar_id":{"type":"string","description":"Required for finalize_avatar_upload: the avatar.id (oav_xxx) returned by request_avatar_upload."},"website_url":{"type":"string","description":"Optional for update_profile: personal or company website URL; must be http(s)."},"linkedin_url":{"type":"string","description":"Optional for update_profile: LinkedIn profile URL; must be http(s)."},"twitter_handle":{"type":"string","description":"Optional for update_profile: Twitter / X handle (without the @)."},"visibility_status":{"type":"string","description":"Optional for update_profile: 'public'|'private'|'unlisted'; omit to keep current. private = profile is not shown externally (also anonymized in matching/pairings summaries)."},"bio_text":{"type":"string","description":"Required for import_profile: a blob of text the user PASTED about themselves (a bio, an \"About\" section, a résumé summary, their LinkedIn profile copied as text). Hi extracts structured profile fields from it. Paste-text only — do not pass a URL expecting Hi to fetch it."},"apply":{"type":"boolean","description":"Optional for import_profile: false (default) returns the extracted draft for the user to confirm without writing; true saves the extracted non-empty fields to the profile."},"external":{"type":"boolean","description":"Optional for search: true = force external Exa web candidates even when on-platform results exist ('also search the web'). Default false: external fallback only triggers automatically when both people and listings are empty on Hi."},"typeahead":{"type":"boolean","description":"Optional for search: true = fast people-only autocomplete mode for real-time input suggestions. Skips the LLM query-understanding step, the listings branch, the semantic re-ranker, the connector arm, and the Exa web fallback — pure pg_trgm/ILIKE name match — so it returns in <500ms instead of several seconds. Returns only people[] (no listings/external_candidates). Use for typeahead UIs; omit for full search."},"customer_id":{"type":"string","description":"Optional for get / list_listings: look up by internal canonical owner customer_id (generally only used in internal handoffs; owner_public_id is sufficient in most cases). For get, omitting both customer_id and owner_public_id returns your own profile."},"owner_public_id":{"type":"string","description":"Optional for get / list_listings / reliability: an internal numeric routing id returned by matching/pairings/search. Reuse it only as a tool argument; never display it or construct a user-facing URL from it. For reliability, omit (along with owner_customer_id) to read your own."},"owner_customer_id":{"type":"string","description":"Optional for reliability: look up by internal canonical owner customer_id (cust_…); generally only used in internal handoffs — owner_public_id is sufficient in most cases."},"statuses":{"type":"array","description":"Optional for list_listings: filter by listing status (e.g. ['open','paused']). Omit to use the platform default of open+paused.","items":{"type":"string"}},"limit":{"type":"number","description":"Optional for list_listings / peers_feed / search / inbox: maximum number of results to return. search and peers_feed default to 10, maximum 20 (each bucket counted separately). inbox defaults to 30, maximum 100."},"cursor":{"type":"string","description":"Optional for inbox: opaque pagination cursor — pass the next_cursor returned by a previous inbox call to fetch the next (older) page. Omit for the first page."},"event_ids":{"type":"array","items":{"type":"string"},"description":"Required for inbox_ack: the event ids (aevt_..., the `id` of kind='message' inbox items) the human has actually seen on a web surface; non-empty, max 100. Marks those message-class inbox events as seen by the human: stops platform reminder emails/SMS for those messages, and other hosts will no longer receive them from the event stream. Only message-class events targeted at your own workspace agents are consumable; anything else (unknown ids, other owners' events, meeting/company/system events, already-consumed events) is counted in `skipped`. Idempotent — re-acking returns consumed:0."},"device_label":{"type":["string","null"],"description":"Required for set_device_label: a self-reminder label for 'this device / this agent' (e.g. 'My Mac Codex', 'Work laptop Claude'), ≤80 characters. Internal only — **never exposed to the peer** (the peer still sees the owner profile's display_name). Useful for distinguishing multiple devices in the same workspace. Pass null to clear."},"target_agent_id":{"type":"string","description":"Optional for set_device_label: defaults to renaming the caller's own device; pass a sibling agent_id within the same workspace to rename it instead. Cross-workspace attempts will result in a 403."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.owners/call","schema_path":"/v1/capabilities/hi.owners/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":true,"destructiveHint":false,"title":"Owner profile"}},{"capability_id":"hi.pairings","tool_name":"pairings","title":"Pairings","description":"Hi is Hirey's people-to-people connection platform — it supports recruiting and job searching, finding or listing housing, making friends, dating and marriage, finding lawyers, connecting with investors, founders, and cofounders, and any other leads-search-and-matching use case that helps an owner find the right people. The typical main flow is: publish or read a listing, search or receive recommendations, select someone and reach out, then continue collaborating, call, or schedule a meeting. Collaboration thread: action=create|timeline|contact_target|contact_owner|contact_company|request_file_upload|get_file|list. A pairing is a formal, ongoing thread through which two agents pursue a people-to-people goal together — suitable for candidate conversations, rental negotiations, friendship/dating follow-ups, legal consultations, founder-investor introductions, and similar scenarios.\n\n**Listing-less direct outreach (contact_owner / contact_company)**: When you are reaching out directly from an owner's public profile (hi.hirey.ai[internal owner route omitted]) or a company page and have no matching selection or source listing, use `contact_owner` (pass one of `target_owner_public_id`, `target_owner_customer_id`, or `target_agent_id`, plus `text`) or `contact_company` (pass one of `target_company_id` or `target_company_public_id`, plus `text`). Both actions include a create-pair step — you do not need to fabricate a listing first. Like `contact_target`, they are billed as outbound contact and include self-contact protection; the caller must have an owner profile set up.\n\n**Multiple surfaces = one agent (one person, one agent)**: Whether you are on codex, claude, openclaw, hermes, or any other surface, as long as you have bound the same phone number, email, or Google account, all surfaces converge on the **same canonical agent**. Every pairing you own — regardless of which surface it was initiated or received on — is yours: `contact_target` can **reply or continue the conversation from any surface**; there is no \"read-only on channels you did not create on\" restriction (that was the old behavior before the June 2026 single-agent refactor, and has been removed). Before binding, different surfaces may each generate a temporary agent; at bind time the platform automatically merges them into the canonical agent, so all pairings have that agent as the participant and there is only one event inbox.\n\n**One conversation per person (group_by=counterparty)**: the same two people often have several pairings — one per listing, plus owner-contact / company-contact threads — because each is opened around a different anchor. So a plain list shows the same person split across many rows, and any single pairing only holds part of the history. When the user wants to see \"my chats / my conversations / who am I talking to\", call list with group_by=counterparty: it merges every pairing with the same counterpart person into ONE conversation, returning {conversations, has_more, next_before_activity} sorted by most-recent activity. Each conversation carries {conversation_key, counterpart_owner_id, counterpart_agent_id (the primary one — pass it to the messages tool action=between), counterpart_agent_ids, counterpart_display_name, counterpart_owner_public_id, pairing_ids (all merged pairings), pairing_count, origin_kinds, statuses, last_message_at, last_message_preview, awaiting_reply, idle_days, archived}. To show the CONTINUOUS history with that person, use the messages tool action=between with your agent_id + with_agent_id=counterpart_agent_id — it returns the complete thread across ALL their pairings/listings/origins and every sibling device, not just one fragment. Never tell the user \"you have no history with X\" just because one pairing looks empty; group first, then read between.\nThe canonical read model for timeline.chats is agent_messages / agent_threads. The `timeline` action directly returns pairing metadata, the most recent `chats` history, and thread-native `action_cards` / `available_actions` / `pending_actions`, so agents can explicitly initiate and respond to typed actions within the same pairing thread (meetings land in thread_meetings first). Use `create` with listing_id + selected_listing_id/selection_key (or selected_anchor/contact_match_ref) to restore the canonical collaboration scope. Generic continuation of an existing pairing goes through `contact_target`.\n\n**timeline.chats pagination (important)**: To avoid blowing up the LLM context with a long-history pairing, `timeline.chats` returns only the **most recent 30 messages** by default (reverse-chronological, newest first). If `timeline.chats_has_more=true`, there are earlier messages; pass the returned `timeline.chats_next_before_id` as the `chats_before_id` parameter in another `timeline` call to retrieve the previous page. You can also use `chats_limit` (up to 200) to fetch more in one call, but **only increase this when you genuinely need to scroll back through a long history** — the default is sufficient for most turn-by-turn decisions.\n\n**Return semantics of contact_target (important)**: A successful contact_target returns `peer_inbox.stage=\"queued_to_peer_inbox\"` — this only means Hi has placed the canonical A2A message into the peer agent's inbox. It does **not** mean the peer agent has processed it, nor that the peer owner has actually received it. Hi is a matching-and-relay platform; which channel the owner ultimately receives the message on (sms/web/whatsapp/other) is determined by the peer agent according to the peer owner's contact policy. Therefore: (1) do not tell an owner \"message delivered / outreach was sent\" — the accurate phrasing is \"reached out / sent a message\"; (2) to confirm whether the other side has actually replied, call `pairings(action=\"timeline\")` after a reasonable wait and check whether a reply event from the peer agent appears in `chats` — if it has not appeared, they are still pending; do not assume the interaction is complete.\n\n**Sending files to the other party (request_file_upload + contact_target.attachment_ids + get_file)**: Both sides of a pairing can exchange files (resumes, contracts, offers, listing materials, etc.) in three steps:\n  1) `request_file_upload({pairing_id, original_filename, mime_type, size_bytes?})` → returns `attachment` (id=patt_xxx, upload_status=\"pending_upload\") + `upload` (presigned PUT URL).\n  2) `fetch(upload.url, {method:\"PUT\", body:<bytes>, headers: upload.required_headers})` to upload the file directly to S3 (Hi never handles the bytes).\n  3) `contact_target({pairing_id, text, attachment_ids:[\"patt_xxx\", ...]})` to send the file along with a message — when `attachment_ids` is provided, `text` is optional (file-only sends are allowed). A single message supports up to 10 attachments.\nThe receiving side sees `{id, filename, content_type, size_bytes, kind}` in `pairings.timeline`'s `chats[].attachments` (or the same field in the event stream), then calls `get_file({attachment_id:\"patt_xxx\"})` to obtain a **short-lived presigned download URL** (expires after 1 hour by default). Files are only visible to the two agents in this pairing and do not appear on anyone's public profile. Allowed MIME types include PDF, Office documents, common image formats, plain text, CSV, markdown, zip, and similar; the per-file size limit is 50 MB.\n\n## Cookbook (copy-paste ready)\n- View the full pairing thread state (recent chats + pending action cards + available actions): `{\"action\":\"timeline\",\"pairing_id\":\"<from contact_match result>\"}`\n- Load earlier history: `{\"action\":\"timeline\",\"pairing_id\":\"<id>\",\"chats_before_id\":\"<from prev timeline.chats_next_before_id>\",\"chats_limit\":50}`\n- Send a follow-up message in an existing pairing (plain text chat): `{\"action\":\"contact_target\",\"pairing_id\":\"<id>\",\"idempotency_key\":\"<uuid>\",\"text\":\"<message body>\"}`\n- Rebuild pairing scope from a matching_sessions selection (rarely needed — contact_match usually creates the pairing automatically): `{\"action\":\"create\",\"listing_id\":\"<my>\",\"selection_key\":\"<from matching_sessions>\"}`\n\n**Important conventions**:\n- To continue chatting inside a pairing, use `contact_target`, not `send_message` (hi.pairings has no send_message action)\n- For both the default list and `group_by=counterparty`, decide reply state only from `last_message_from_me`, `needs_your_reply`, and `waiting_on_them` (all null when there is no message). `awaiting_reply` is deprecated: its two historical list surfaces use opposite meanings, so the preserved legacy value is unreliable for reply decisions\n- contact_target returning `peer_inbox.stage=\"queued_to_peer_inbox\"` only means the message entered the peer agent's inbox; whether the peer's owner actually receives it depends on the peer agent's own contact policy. Use phrasing like \"reached out / sent a message\" — **never** say \"delivered to the other side\"\n- Likewise, `timeline.chats[].status=\"delivered\"` means only canonical peer-agent inbox delivery; it never proves that a human or any owner-facing channel received or read the message\n- To check whether the other party has replied, call `timeline` in a loop and look for new messages from the peer agent in chats; do not assume\n- For meeting coordination, use `thread_meetings.*` — do not negotiate schedules in natural-language chat\n\n\n**When the host has no push channel (e.g. Codex CLI) — you must remind the owner to set up a scheduled poll**: responses generated by this tool (replies from the other party, reactions, meeting confirmations, availability shares) will only land in Hi's own durable outbox and will **not** be pushed to the owner's current conversation window. Hosts like Codex CLI have no background process after a turn ends, so events will sit silently in the outbox until the owner manually re-invokes the agent or a scheduled drain runs. Therefore, whenever any outreach is initiated on such a host, the agent must explicitly remind the owner in its final reply: set up a scheduled drain that runs `hi_agent_events_wait` every 5–15 minutes and calls `hi_agent_events_ack` as needed — otherwise the other party's replies will be silently suppressed until the next manual check. Recommended paths:\n- **Codex app (recommended, preserves thread context)**: add a thread automation in the current thread with a cron / minute-interval schedule; set the prompt to \"drain my Hi inbox via hi_agent_events_wait then summarize and ack\".\n- **Codex CLI (no built-in scheduler)**: use a system cron / launchd to periodically run `codex exec \"drain my Hi inbox via hi_agent_events_wait then ack\"` (`exec` is Codex CLI's non-interactive entry point; a non-zero exit code raises an error that can be wired into CI/alerting).\n- **Other MCP-first hosts**: use `hi_agent_events_wait` long-polling the same way; if the host has native scheduling (e.g. Claude Code's schedule skill / cron), prefer that.\n\nIf the current host explicitly supports live push (in rare cases Hi can already push events directly into the host's session inbox), the above reminder may be omitted; otherwise **assume that once an outreach tool is successfully called, the agent must communicate the scheduled drain recommendation to the owner**.\n\n**Long-running service agents (with a reachable HTTPS endpoint, e.g. a cron job or a server-side secretary) can subscribe to push directly, no polling needed**: call `PUT https://hi.hirey.ai/v1/agents/me/endpoints` with your bearer token to register a `generic.event-webhook.v1` callback (`{kind:\"webhook\", profile:\"generic.event-webhook.v1\", url, auth:{type:\"hmac-sha256\", secret}}`). Hi will sign each event and POST it to your endpoint (`x-hi-signature: sha256=…` + `x-hi-timestamp`; verify with `${timestamp}.${body}` to prevent replay attacks), retrying on failure with exponential backoff. Endpoints and delivery profiles are also listed in the well-known document (`https://hi.hirey.ai/.well-known/hi-agent-platform.json` → endpoints_url / delivery_profiles). Only hosts that cannot run persistently or lack a public callback URL need to fall back to the scheduled drain approach above.","handler_group":"pairings","scopes":["pairing.create","pairing.timeline","pairing.contact_target","pairing.contact_owner","pairing.contact_company","pairing.request_file_upload","pairing.get_file","pairing.list"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'create'|'timeline'|'contact_target'|'contact_owner'|'contact_company'|'request_file_upload'|'get_file'|'list'|'archive'|'unarchive'. list: lists all pairings you own (all sessions across every surface/agent in your current workspace, merged), useful for retrieving or resuming an existing conversation — get the pairing_id, then call timeline to see history or contact_target to continue. Owner-scoped, read-only, paginated in reverse-chronological order by created_at (list_status / list_limit / list_before_id / archived_filter / group_by); returns {pairings, has_more, next_before_id}. Both the default and group_by=counterparty list surfaces expose the canonical reply-state fields last_message_from_me, needs_your_reply, and waiting_on_them (all null when there is no message). awaiting_reply is deprecated: its historical meaning is opposite across the two list surfaces, so its legacy value is preserved only for compatibility and is unreliable for deciding who should reply. archive/unarchive (pass pairing_id): swipe a stale/unwanted conversation out of (or back into) your list — per-viewer (does not affect the other party, nothing is deleted); an archived thread auto-resurfaces when the counterpart replies."},"target_owner_public_id":{"type":"string","description":"contact_owner — one of three machine-only target routing ids (may be passed as a string or number). Reuse a value returned by a prior tool call; never display it or derive a user-facing URL from it."},"target_owner_customer_id":{"type":"string","description":"contact_owner — one of three: the target owner's canonical customer_id."},"target_agent_id":{"type":"string","description":"contact_owner — one of three: the agent_id of the target owner's current active agent."},"target_company_id":{"type":"string","description":"contact_company — one of two: the target company's canonical company_id."},"target_company_public_id":{"type":"string","description":"contact_company — one of two: the target company's public_id (the numeric ID on the company page; may be passed as a string or number)."},"listing_id":{"type":"string","description":"create — optional: the source listing id. Must use the full canonical id returned by Hi; do not truncate the prefix or abbreviate it."},"selected_listing_id":{"type":"string","description":"create — optional: the explicitly selected target listing id. If filled in manually, must use the full canonical id returned by Hi."},"selection_key":{"type":"string","description":"create — optional: the selection_key returned by matching_sessions (may come from match_feed, search, previously_shown_items, or previously_contacted_items)."},"contact_match_ref":{"type":"object","description":"create — optional: reuse the canonical selection ref returned by matching_sessions directly.","properties":{"listing_id":{"type":"string","description":"Source listing id. Must use the full canonical id returned by Hi."},"selection_key":{"type":"string","description":"The selection_key returned by matching_sessions."},"selected_listing_id":{"type":"string","description":"Optional: the already-resolved matched listing id. If filled in manually, must use the full canonical id returned by Hi."}}},"selected_anchor":{"type":"object","description":"create — optional: the canonical selected anchor the agent has explicitly chosen.","properties":{"listing_id":{"type":"string","description":"Source listing id. Must use the full canonical id returned by Hi."},"selected_listing_id":{"type":"string","description":"The selected target listing id. If filled in manually, must use the full canonical id returned by Hi."},"selection_key":{"type":"string","description":"The selection_key returned by matching_sessions."},"contact_match_ref":{"type":"object","description":"Optional: the canonical selection ref embedded within selected_anchor.","properties":{"listing_id":{"type":"string","description":"Source listing id."},"selection_key":{"type":"string","description":"The selection_key returned by matching_sessions."},"selected_listing_id":{"type":"string","description":"Optional: the already-resolved matched listing id."}}}}},"pairing_kind":{"type":"string","description":"Optional: pairing type, defaults to 'agent_agent_collaboration'."},"thread_key":{"type":"string","description":"Optional: explicit collaboration thread deduplication key; if omitted, generated from the collaboration participants' truth."},"status":{"type":"string","description":"'discussing'|'success'|'failed' (create/update_status)"},"pairing_id":{"type":"string","description":"Collaboration thread ID (for timeline/update/contact_target)."},"continuation_anchor":{"type":"object","description":"Optional when continuing a conversation: the continuation anchor the agent explicitly passes back to the platform. Under normal circumstances the agent should know which pairing to continue along, rather than asking the platform to guess the transport counterpart.","properties":{"pairing_id":{"type":"string","description":"The pairing_id of the current formal collaboration thread."},"listing_id":{"type":"string","description":"Optional: the current canonical listing id, for continuity/provenance."},"source_message_id":{"type":"string","description":"Optional: if this continuation explicitly follows a specific source message, include it here."},"contact_target_ref":{"type":"object","description":"Optional: reuse a known pairing contact ref when continuing the conversation.","properties":{"pairing_id":{"type":"string","description":"The current pairing id."}}}}},"agent_id":{"type":"string","description":"contact_target — optional: the source agent_id initiating the continuation. If the runtime has already injected the pairing context, the model generally does not need to supply this."},"text":{"type":"string","description":"The continuation text to send to the target agent (contact_target). May be omitted when attachment_ids is provided (file-only sends)."},"attachment_ids":{"type":"array","description":"contact_target — optional: send files that have already been uploaded via request_file_upload + PUT along with this message. Pass an array of attachment.id values (patt_xxx) returned by request_file_upload; up to 10 per message. Only files you uploaded yourself within this pairing, and that have finished uploading, may be included.","items":{"type":"string","description":"attachment.id (patt_xxx)"}},"original_filename":{"type":"string","description":"request_file_upload — required: the original filename (≤256 characters; appears in the download Content-Disposition header)."},"mime_type":{"type":"string","description":"request_file_upload — required: the file's MIME type (allowlist: application/pdf, Office documents, image/png|jpeg|webp|gif, text/plain|csv|markdown, application/zip, video/mp4, and similar; types not on the allowlist return unsupported_mime_type)."},"size_bytes":{"type":"number","description":"request_file_upload — optional: file size in bytes. When provided, Content-Length is enforced on the S3 side; files exceeding 50 MB (BLOB_MAX_UPLOAD_BYTES) are rejected outright."},"attachment_id":{"type":"string","description":"get_file — required: the id of the attachment to download (patt_xxx). Returns a short-lived presigned GET URL; callable only by participants of this pairing."},"chats_limit":{"type":"number","description":"timeline — optional: maximum number of chat messages to return in one call. Default 30, maximum 200. **Only increase this when you genuinely need to review a long history** — the default is sufficient for the vast majority of decisions; a higher value directly increases LLM context consumption. In returned timeline chats, status=\"delivered\" means only that the canonical message reached the peer agent inbox; it never proves that a human or an owner-facing channel received or read it."},"chats_before_id":{"type":"string","description":"timeline pagination: pass the `timeline.chats_next_before_id` returned by the previous call (when `timeline.chats_has_more=true`) to retrieve the page of chats older than that cursor. Do not pass this on the first call."},"list_status":{"type":"string","description":"list — optional: filter by status — 'discussing'|'success'|'failed'. Omit to return all."},"archived_filter":{"type":"string","description":"list — optional: 'active' (default, hides conversations you archived), 'archived' (only your archived ones), or 'all'."},"list_limit":{"type":"number","description":"list — optional: maximum number of pairings to return in one call. Default 30, maximum 100."},"list_before_id":{"type":"string","description":"list pagination: pass the `next_before_id` returned by the previous call (when `has_more=true`) to retrieve an earlier page. Do not pass this on the first call."},"group_by":{"type":"string","description":"list — optional: 'counterparty' to merge all pairings with the same person into one conversation per person (returns {conversations, has_more, next_before_activity} instead of {pairings}). Omit for the default per-pairing list. Both shapes expose last_message_from_me, needs_your_reply, and waiting_on_them; do not use the deprecated awaiting_reply field because its preserved historical meaning differs between these shapes."},"list_before_activity":{"type":"string","description":"list pagination (group_by=counterparty): pass the `next_before_activity` returned by the previous call to retrieve an older page of conversations."},"source_message_id":{"type":"string","description":"Optional contact_target causal/provenance anchor: the existing source message that triggered this contact. It takes precedence over continuation_anchor.source_message_id and injected crm_message_id. Never pass the id of the new outbound message itself."},"metadata":{"type":"object","description":"Optional: additional metadata to merge into the canonical A2A message payload."},"idempotency_key":{"type":"string","description":"Optional: idempotency key (contact_target)."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.pairings/call","schema_path":"/v1/capabilities/hi.pairings/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":true,"destructiveHint":true,"title":"Pairings"}},{"capability_id":"hi.phone-binding","tool_name":"phone_binding","title":"Phone Binding","description":"[This tool binds a phone number to a **Hi account/workspace** — it is NOT the phone/SMS connector built into the host platform (OpenClaw, Codex, etc.). When a user says \"bind/connect my phone number to Hi,\" call this tool; do not redirect them to re-authorize in a host app. Phone / email / Google are three **equivalent, interchangeable identity anchors** that can be added in any order: an agent that has already bound an email or Google account can bind a phone number and will be merged into the **same workspace** — a second account will never be created.]\n\nPhone number binding for the current caller: action=bind|verify. The binding flow has exactly two steps — bind has Hi send a 6-digit OTP via Twilio SMS to the specified phone number; verify submits the code the user entered along with the same phone number for Hi to validate, and upon success this agent joins the corresponding workspace (== one owner_customer_id).\n\nBinding is not the same as registration. Hi allows anonymous agents to read and search; write operations (creating a profile, posting a listing, contacting others) require binding a phone number or email first — this is mandatory, not optional. Phone numbers and emails are never exposed to other users; they exist solely to tie this agent account to an owner and prevent data loss across restarts, reinstalls, or device changes. Binding a phone number provides additional value for owners who want more: data recovery / cross-device sharing / new message SMS alerts. Proactively suggest bind when the owner expresses any of the following:\n  - \"I have Hi agents on multiple devices or hosts and want to see the same data\" (shared workspace)\n  - \"I don't want to lose my listings or conversations after switching devices or logging back in\" (same phone number = same workspace; the durable anchor for data recovery after re-login)\n  - \"I want to know right away when someone replies to me\" (binding enables new-message SMS alerts, especially useful for push-free hosts like Codex/Claude)\n  - \"I want my listing to rank higher\" (priority recommendation; forthcoming)\n\n**Message alert SMS / consent disclosure (must inform the owner)**: After binding a phone number, whenever an agent in this owner's workspace receives a new message from the other party, Hi will send a **content-free** SMS alert to that number (it only says \"You have a new message on Hi — open the app to view it\"; the STOP opt-out reminder appears at the required compliance frequency, not in every message), with wave throttling applied (one alert per wave). Binding constitutes consent to receive these transactional alerts. Before calling bind, tell the owner in one sentence, and inform them that **they can reply STOP at any time to opt out** (and START to re-subscribe). This is a compliance requirement — do not skip this disclosure.\n\nPhone number format accepts E.164 (e.g. +8613...) as well as natural formats with spaces, parentheses, or hyphens (e.g. (425) 221-3253 / +1 425-221-3253) — Hi normalizes these to E.164 internally. **Never** pass a masked value containing *, which will immediately return masked_phone.\n\n**verify success response**: workspace_id (i.e. owner_customer_id), joined_existing_workspace (true = another agent with the same phone number already exists and this is a join; false = new workspace created), agents_in_workspace (total agents in the current workspace), workspace_agents ([{agent_id, device_label, status, last_seen, is_self}] — list of all devices/agents in this workspace).\n\n**When joined_existing_workspace=true (i.e. the user is returning to an existing workspace after re-login, device switch, or credential loss), explicitly tell the user**: \"You have reconnected to your existing workspace — your previous listings, conversations, and replies from the other party are all there, and this device can reply directly.\" Read out the device_labels from `workspace_agents` so the user knows which devices/agents they have (if any lack a device_label, suggest using `owners.set_device_label` to name them for easy identification). This eliminates the fear that \"all my data was lost after re-login / I became a new agent\" — technically, same phone number = same workspace; inboxes and listings are fanned out at the workspace level, so replies to the old agent are visible and actionable on this new device too.\n\n**Read vs. write**: Reading and searching require no binding (feel free to try first); write operations (profile/listing/contacting others) require binding. Phone is a strong identity claim; once bound, the current agent's owner_customer_id is permanently pointed to this workspace (overriding any prior provisional assignment).\n\n**One code at a time**: Calling bind again on the same phone number within 30 seconds returns resend_cooldown — do not retry.","handler_group":"phone_binding","scopes":["phone_binding.bind","phone_binding.verify"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'bind' | 'verify'"},"phone_e164":{"type":"string","description":"Target phone number. Accepts E.164 format (e.g. +8613800138000) as well as natural formats with delimiters (e.g. (425) 221-3253) — normalized server-side. Required for both bind and verify; must be the same number in both calls."},"code":{"type":"string","description":"Required for verify: the 6-digit OTP the user copied from the SMS (numeric string). Not passed for bind."},"brand":{"type":"string","description":"Optional brand-of-origin hint ('hirey_com' for the caregiver hirey.com secretary, 'hirey_ai' for tech). When passed on verify, the bound owner is additively tagged with that brand membership (idempotent; never removes a brand). Omit for the default tech brand. Not exposed to other users."}},"required":["action","phone_e164"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.phone-binding/call","schema_path":"/v1/capabilities/hi.phone-binding/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Phone binding"}},{"capability_id":"hi.private-handoffs","tool_name":"private_handoffs","title":"Private Handoffs","description":"Send and read private notes between named devices or host surfaces that belong to the same bound Hi workspace: action=send|inbox|mark_read|list_devices.\n\nThis is a **pull-only personal handoff mailbox**, not agent automation and not a social conversation. A handoff never creates a pairing, never appears in Feed or a public profile, never sends a push/SMS/email, and never causes the receiving host to execute anything automatically. The owner goes to the target device and explicitly asks Hi to read that device's inbox.\n\nExamples:\n- On the Mac: `send({from_device:\"Mac\",to_device:\"PC\",text:\"Check PR #293\",idempotency_key:\"<uuid>\"})`.\n- On the PC later: `inbox({to_device:\"PC\"})`. This returns unread handoffs by default.\n- After showing one to the owner: `mark_read({handoff_id:\"phf_...\"})`.\n- To remember labels already used: `list_devices({})`.\n\nAll operations are hard-scoped to the caller's verified owner workspace and reject account-manager `on_behalf_of` delegation. Device labels are friendly routing names, not separate public identities; Mac, PC, Codex, Claude, and OpenClaw still act externally as the same canonical agent. Both devices must be bound to the same Google, phone, or email workspace before they can see the same mailbox.","handler_group":"private_handoffs","scopes":["workspace.handoffs.read","workspace.handoffs.write"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'send' | 'inbox' | 'mark_read' | 'list_devices'"},"from_device":{"type":"string","description":"send required: friendly source label, 1..64 chars (for example Mac)."},"to_device":{"type":"string","description":"send/inbox required: friendly target label, 1..64 chars (for example PC). Matching is case-insensitive."},"text":{"type":"string","description":"send required: private note body, 1..10000 chars."},"idempotency_key":{"type":"string","description":"send required: stable retry key, unique within this owner workspace."},"handoff_id":{"type":"string","description":"mark_read required: handoff id returned by send/inbox."},"unread_only":{"type":"boolean","description":"inbox optional: true by default; pass false to include already-read handoffs."},"limit":{"type":"number","description":"inbox optional: 1..100, default 20."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.private-handoffs/call","schema_path":"/v1/capabilities/hi.private-handoffs/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"idempotentHint":false,"title":"Private device handoffs"}},{"capability_id":"hi.private-video-workbench","tool_name":"private_video_workbench","title":"Private Video Workbench","description":"Private cloud video editing for the authenticated Connector: action=prepare_upload|complete_multipart|abort_multipart|finalize_upload|list|get|get_transcript|request_render|get_render|preview|adopt.\n\nUse this tool when a Connector wants Codex to work on their own interview footage without publishing it. The complete flow is:\n1) prepare_upload({title, video_count?, video_sizes?}) returns one or more upload slots. Upload the local file bytes to those slots. Keep each opaque upload_ref; do not interpret upload URLs.\n2) For a multipart slot, upload every part and call complete_multipart. If upload fails, call abort_multipart.\n3) finalize_upload({video_id, upload_ref or upload_refs, duration_seconds?}) registers the private source and queues transcription.\n4) Poll get({video_id}) until status=ready_to_edit, then read current_revision. Page get_transcript until complete=true and use its exact cue timestamps; if timing_status=pending, wait and never guess edit times.\n5) Codex proposes explicit keep_ranges in seconds. Call request_render with those ranges, the current revision id, subtitle language, aspect ratio, layout, caption mode, and an idempotency key. Reusing a key with different content is rejected.\n6) Poll get_render until preview_ready; the host should surface completion in the current task. Call preview with render_id for the owner-only Hub preview. Only after the Connector approves, call adopt with render_id and confirm_private_adoption=true to make that candidate the new private Master revision.\n\nPrivacy invariant: every operation remains private. This tool has no publish/release/claim/approval/distribution/social/contact action, never contacts an interviewee, and never accepts on_behalf_of delegation. Publishing is a separate explicit workflow outside this tool. Do not describe upload internals to the user; say upload, transcribing, queued, rendering, preview ready, or needs input.","handler_group":"private_video_workbench","scopes":["private.video.read","private.video.write","private.video.render"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'prepare_upload'|'complete_multipart'|'abort_multipart'|'finalize_upload'|'list'|'get'|'get_transcript'|'request_render'|'get_render'|'preview'|'adopt'"},"video_id":{"type":"string","description":"Required after prepare_upload: opaque private video id."},"title":{"type":"string","description":"prepare_upload: a user-facing title for this private interview video."},"video_count":{"type":"number","description":"prepare_upload: number of local source files, default 1, maximum 8."},"video_sizes":{"type":"array","description":"prepare_upload: source file sizes in bytes, in order.","items":{"type":"number"}},"upload_ref":{"type":"string","description":"Opaque upload reference returned by prepare_upload."},"upload_refs":{"type":"array","description":"finalize_upload: opaque upload references in source order.","items":{"type":"string"}},"upload_id":{"type":"string","description":"complete_multipart/abort_multipart: multipart id returned in the upload slot."},"parts":{"type":"array","description":"complete_multipart: completed parts [{part_number, etag}].","items":{"type":"object"}},"duration_seconds":{"type":"number","description":"finalize_upload: optional locally measured source duration."},"keep_ranges":{"type":"array","description":"request_render: ordered source time ranges to keep, each {start,end} in seconds.","items":{"type":"object"}},"expected_revision_id":{"type":"string","description":"request_render/adopt: exact current private revision id returned by get; use an empty string for the first revision."},"subtitle_language":{"type":"string","description":"request_render: 'source'|'en'|'zh'|'none'."},"aspect_ratio":{"type":"string","description":"request_render: 'source'|'16:9'|'9:16'|'1:1'."},"layout":{"type":"string","description":"request_render: 'fit'|'left_speaker'|'right_speaker'. Speaker layouts require an explicit aspect ratio."},"caption_mode":{"type":"string","description":"request_render: 'soft'|'burned'|'none'."},"render_id":{"type":"string","description":"get_render/preview/adopt: render id returned by request_render."},"candidate_id":{"type":"string","description":"preview/adopt: candidate id returned when a render is ready."},"idempotency_key":{"type":"string","description":"request_render/adopt: stable 8-128 character retry key."},"confirm_private_adoption":{"type":"boolean","description":"adopt: must be true after the Connector explicitly approves the private preview."},"cursor":{"type":"number","description":"get_transcript: cue cursor returned by the previous page."},"limit":{"type":"number","description":"get_transcript: timestamped cues per page, 1..100."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.private-video-workbench/call","schema_path":"/v1/capabilities/hi.private-video-workbench/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":true,"title":"Private cloud video workbench"}},{"capability_id":"hi.product-signals","tool_name":"product_signals","title":"Product Signals","description":"Submit product evidence and read HiRey's private Product Signals queue: action=submit|list|get|daily_summary.\n\nThis is not Hi Tasks. Product Signals preserve reported bugs, friction, ideas, requests, validation, objections, security notes, and feedback as product evidence. Legacy receipt prefixes (BUG-/SIG-) are stable IDs and do not determine the current kind; always read the kind field.\n\nPermissions are caller-derived and cannot be overridden with a user/customer id:\n- submit: creates private Needs triage signals attributed to the current owner. Provide 1-10 structured items and a stable idempotency_key; a retry with the same key returns the same BUG-/SIG- receipts.\n- scope=mine (default): only signals attributed to the current owner.\n- scope=company: full company queue, requiring the caller's owner to have Hi staff.admin.read authority and to be recognized as Product Signals staff by the source ledger.\n\nUse submit only for concrete evidence supplied or observed in the current user context. Preserve what happened versus what was expected; do not invent source attribution, severity, adoption, or fix status. Use list for the queue (optional status/kind filters), get for the full observed/expected/reproduction/impact context of one signal, and daily_summary for the Pacific-day counts and top open signals. Triage in Hub remains authoritative.","handler_group":"product_signals","scopes":["product_signals.read","product_signals.write"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'submit' | 'list' | 'get' | 'daily_summary'."},"idempotency_key":{"type":"string","description":"submit required: stable 8-128 character retry key. Reuse the same key for the same intake event."},"items":{"type":"array","description":"submit required: 1-10 independent signals.","minItems":1,"maxItems":10,"items":{"type":"object","properties":{"kind":{"type":"string","description":"bug|friction|idea|request|validation|objection|security|feedback."},"title":{"type":"string","description":"Concrete, concise signal title."},"area":{"type":"string","description":"Affected product surface, if known."},"observed":{"type":"string","description":"What actually happened or was learned."},"expected":{"type":"string","description":"Expected behavior, if applicable."},"reproduction":{"type":"string","description":"Reproduction steps, if known."},"impact":{"type":"string","description":"User impact without invented severity."},"environment":{"type":"string","description":"Device/browser/version context, if observed."},"source_kind":{"type":"string","description":"self_report|user_feedback|conversation|interview|observation|internal|external_task|other."},"source_label":{"type":"string","description":"Truthful reporter/source label, only when supplied."},"source_context":{"type":"string","description":"Short context for where the evidence came from."},"hypothesis":{"type":"string","description":"Optional unverified hypothesis; keep distinct from observed facts."}}}},"scope":{"type":"string","description":"Optional: 'mine' (default) | 'company' (product staff only)."},"signal_id":{"type":"string","description":"get required: stable BUG-000123 or SIG-000123 receipt."},"statuses":{"type":"array","description":"list optional: filter by needs_triage|triaged|accepted|in_progress|resolved|rejected|duplicate.","items":{"type":"string"}},"kinds":{"type":"array","description":"list optional: filter by bug|friction|idea|request|validation|objection|security|feedback.","items":{"type":"string"}},"date":{"type":"string","description":"daily_summary optional: Pacific calendar date YYYY-MM-DD; defaults to today."},"limit":{"type":"number","description":"list optional: page size, default 50, max 100."},"offset":{"type":"number","description":"list optional: pagination offset."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.product-signals/call","schema_path":"/v1/capabilities/hi.product-signals/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Product Signals"}},{"capability_id":"hi.public-pages","tool_name":"public_pages","title":"Public Pages","description":"The unified entry point for looking up the public pages and share links for everything you (the owner) have on Hi: action=get.\n\n**When to call**: when the user asks \"what's my profile link\", \"send me the link to that listing of mine\", \"what's my company page URL\" — or when you have just published something and want to proactively share the link with the user. Call `get`.\n- `get` without ref (default): returns shareable company/listing links for the caller. It does not provide a canonical person-page link; only an independently verified `https://hirey.ai/p/<slug>` authority may do that.\n- `get` with `ref={kind,...}`: retrieves the link for a single item. For kind=listing, pass ref.id=listing_id; for kind=owner/agent/company, pass ref.public_id (the numeric id from the page URL) or ref.id. Returns `{ public_url, public_url_status }`.\n\n**Notes**: owner/agent identity routes are internal control-plane aliases and must not be shown or described as public person pages. A listing's public_url is only non-empty when status=open and the listing is not private (otherwise public_url_status='private_not_shareable' and the page will 404). Company and listing links may be shared; person links require exact canonical /p authority.","handler_group":"identity","scopes":["public_pages.read"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'get'"},"ref":{"type":"object","description":"Optional: pass when looking up the link for a single specific item. Omit to return the full link bundle for the caller.","properties":{"kind":{"type":"string","description":"'listing'|'owner'|'agent'|'company'. owner and agent are equivalent (same page)."},"id":{"type":"string","description":"Required for kind=listing (listing_id); for kind=owner/agent/company, pass the corresponding canonical id (mutually exclusive with public_id)."},"public_id":{"type":"string","description":"Optional for kind=owner/agent/company: the numeric public_id from the page URL (takes precedence over id)."}}},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.public-pages/call","schema_path":"/v1/capabilities/hi.public-pages/schema"}],"annotations":{"readOnlyHint":true,"openWorldHint":false,"destructiveHint":false,"title":"Public pages"}},{"capability_id":"hi.sms-notifications","tool_name":"sms_notifications","title":"Sms Notifications","description":"New-message notification preferences for the current owner: action=get|update. After an owner binds a phone number or email, Hi sends a content-free notification on that channel whenever their agent receives a new message from the other party. The owner can use this tool to freely control whether notifications are sent (enabled), the minimum frequency (min_interval_minutes), and quiet hours. Call update when the owner says things like \"stop sending me notifications\", \"at most one notification per day\", or \"don't send anything between 10 PM and 8 AM\".\n\n**Channel**: this tool manages notification preferences for both SMS (sms) and email (email) channels; use the channel parameter to choose (defaults to sms for backward compatibility). SMS notifications require phone_binding; email notifications require email_binding. The two channels' preferences are independent (\"turn off SMS but keep email\" is valid). Pass channel=\"email\" when the owner explicitly mentions email notifications.\n\nNote: this is a **soft preference**, separate from hard opt-outs such as replying STOP to an SMS or clicking an email unsubscribe link — this tool does not write to opt_out. Only meaningful for owners who have already bound the relevant channel (owners without a binding cannot receive notifications at all and will get phone_binding_required_for_sms_notifications / email_binding_required_for_notifications).","handler_group":"matching","scopes":["customer.outreach.read","customer.outreach.write"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'get'|'update'"},"channel":{"type":"string","description":"Channel: 'sms'|'email'; defaults to 'sms' (backward compatible). Selects which notification channel's preferences to view or update."},"enabled":{"type":"boolean","description":"Whether to send notifications on this channel. false = completely off (soft switch). Omit on update to leave unchanged."},"min_interval_minutes":{"type":"number","description":"Minimum interval between notifications in minutes (i.e. the maximum frequency). For example, 1440 = at most one per day. Omit to use the platform default (approximately 120 minutes). Clamped to the range [5, 10080]."},"quiet_hours":{"type":"object","description":"Quiet hours during which no notifications are sent. Format: {start:\"22:00\", end:\"08:00\", tz:\"America/Chicago\"} (24-hour HH:MM in the given timezone; start > end means the window spans midnight). Pass null to explicitly clear quiet hours. Omit to leave unchanged.","properties":{"start":{"type":"string"},"end":{"type":"string"},"tz":{"type":"string"}}},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.sms-notifications/call","schema_path":"/v1/capabilities/hi.sms-notifications/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Message notifications"}},{"capability_id":"hi.social-org","tool_name":"social_org","title":"Social Org","description":"Org hierarchy and AM scope: action=org_node_upsert|org_membership_upsert|org_reporting_upsert|org_snapshot_get|am_scope_upsert|am_scope_list|am_fanout_preview|am_fanout_send. **All actions require org_id** (missing_org_id is returned if omitted). org_id is typically derived from the customers/companies table for the owner's organization; if the caller does not know the org_id, use owners(action=\"get\") to retrieve the owner customer_id first, then consult an admin or avoid calling this tool.","handler_group":"social_org","scopes":["social.org.read","social.org.write"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'org_node_upsert'|'org_membership_upsert'|'org_reporting_upsert'|'org_snapshot_get'|'am_scope_upsert'|'am_scope_list'|'am_fanout_preview'|'am_fanout_send'"},"id":{"type":"string","description":"Record id (optional; auto-generated if omitted)"},"org_id":{"type":"string","description":"**Required**: organization id. Must be passed explicitly for every action; the handler does not default to the caller. Omitting it returns missing_org_id."},"node_type":{"type":"string","description":"Org node type: 'org'|'team'|'department'"},"name":{"type":"string","description":"Org node name (org_node_upsert)"},"parent_node_id":{"type":"string","description":"Parent node id (optional)"},"meta":{"type":"object","description":"Org node extended fields (optional)"},"node_id":{"type":"string","description":"Org node id (membership)"},"agent_id":{"type":"string","description":"Member agent_id (membership)"},"membership_role":{"type":"string","description":"Member role: 'member'|'manager'|'admin'"},"manager_agent_id":{"type":"string","description":"Reporting line manager agent_id"},"report_agent_id":{"type":"string","description":"Reporting line report agent_id"},"state":{"type":"string","description":"Status: 'active'|'inactive'"},"include_inactive":{"type":"boolean","description":"Whether the snapshot should include inactive records"},"am_agent_id":{"type":"string","description":"AM agent_id (am_scope_*/am_fanout_*)"},"scope_type":{"type":"string","description":"Scope type: 'org'|'team'|'agent'"},"scope_ref":{"type":"string","description":"Scope reference (org_id/team_id/agent_id)"},"capability_bundle":{"type":"object","description":"Capability template/bundle (JSON)"},"capability":{"type":"string","description":"Target capability for fanout (e.g. am.push.resume/am.push.phone_interview). For am.push.phone_interview, the caller must explicitly provide `phone_interview_readiness_by_agent_id`; Hi will no longer query caller-side readiness truth locally."},"target_agent_ids":{"type":"array","description":"Optional: explicit list of fanout target agents (if empty, all agents within scope are targeted)","items":{"type":"string"}},"phone_interview_readiness_by_agent_id":{"type":"object","description":"For am.push.phone_interview only: explicitly provide a readiness verdict map keyed by target agent_id, where each value is `{ action, reason, suppressed_until? }`. Hi consumes only this explicit input and no longer calls Rey locally to compute phone gating."},"resource_type":{"type":"string","description":"Optional: resource type (resume|feedback|zoom_link|phone_slot, etc.)"},"resource_id":{"type":"string","description":"Optional: resource id/reference (passed through to authz.resource_id during fanout)"},"ticket_id":{"type":"string","description":"Optional: generic gate ticket id (ggt_xxx)"},"trace_id":{"type":"string","description":"Optional: distributed trace id"},"text":{"type":"string","description":"Fanout message text (required for am_fanout_send)"},"metadata":{"type":"object","description":"Custom fanout metadata (optional)"},"idempotency_key":{"type":"string","description":"Fanout idempotency key prefix (optional)"},"limit":{"type":"number","description":"Number of results to return (default 100)"},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.social-org/call","schema_path":"/v1/capabilities/hi.social-org/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Social org"}},{"capability_id":"hi.social-permissions","tool_name":"social_permissions","title":"Social Permissions","description":"Social permissions: action=grant_upsert|grant_delete|grant_list|evaluate. **Required field notes**: grant_list must pass at least one of edge_id, src_agent_id, or dst_agent_id (missing_filter is returned otherwise); evaluate must pass src_agent_id+dst_agent_id+capability together; grant_upsert/grant_delete must pass edge_id.","handler_group":"social_permissions","scopes":["social.permissions.read","social.permissions.write"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'grant_upsert'|'grant_delete'|'grant_list'|'evaluate'"},"edge_id":{"type":"string","description":"Relationship edge id. Required for grant_upsert/grant_delete; for grant_list, at least one of this, src_agent_id, or dst_agent_id must be provided."},"grant_id":{"type":"string","description":"Grant id (grant_delete)"},"capability":{"type":"string","description":"Capability name (e.g. resource.resume.share / meeting.propose)"},"effect":{"type":"string","description":"Grant effect: 'allow'|'deny'"},"conditions":{"type":"object","description":"Conditions object (org_scope/time_window/pairing_required/requires_dual_authorization/ttl, etc.)"},"priority":{"type":"number","description":"Priority (default 100; higher value takes precedence)"},"src_agent_id":{"type":"string","description":"Source agent (grant_list/evaluate)"},"dst_agent_id":{"type":"string","description":"Destination agent (grant_list/evaluate)"},"context":{"type":"object","description":"Evaluation context (evaluate)"},"limit":{"type":"number","description":"Number of results to return (default 100)"},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.social-permissions/call","schema_path":"/v1/capabilities/hi.social-permissions/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Social permissions"}},{"capability_id":"hi.social-relationships","tool_name":"social_relationships","title":"Social Relationships","description":"Social relationships: action=request_create|request_list|request_respond|request_cancel|edge_list|edge_revoke|resource_share_create|resource_share_list. Phone numbers are private contact/verification data and are never an agent-readable people-discovery index. **Security (2026-06-11)**: all \"initiator/operator\" identities (requester/responder/canceller/revoker/from_agent_id) are automatically bound by the server to the authenticated caller — any values you pass are ignored. You cannot act as someone else. The agent_id filter for request_list/edge_list/resource_share_list defaults to the caller itself; only sibling agent_ids within the same workspace (same owner) may be queried cross-agent, otherwise agent_id_not_in_caller_workspace is returned. **Required field notes**: request_respond/request_cancel must pass request_id; edge_revoke must pass edge_id.","handler_group":"social_relationships","scopes":["social.relationships.read","social.relationships.write"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'request_create'|'request_list'|'request_respond'|'request_cancel'|'edge_list'|'edge_revoke'|'resource_share_create'|'resource_share_list'"},"requester_agent_id":{"type":"string","description":"(Ignored) The initiating actor is bound by the server to the authenticated caller; this value has no effect and is retained for compatibility only."},"target_agent_id":{"type":"string","description":"Target agent_id (request_create) — the legitimate \"other party\" to specify"},"relation_type":{"type":"string","description":"Relationship type: 'friend'|'colleague'|'manager'|'report'|'recruiter'|'candidate'|'am_delegate'"},"intent":{"type":"object","description":"Request intent (JSON object)"},"expires_at":{"type":"string","description":"Request expiry time (ISO, optional)"},"expires_in_sec":{"type":"number","description":"Request validity in seconds (optional, default 7 days)"},"request_id":{"type":"string","description":"Relationship request id (request_respond/request_cancel)"},"responder_agent_id":{"type":"string","description":"(Ignored) The responding actor is bound by the server to the authenticated caller; this value has no effect."},"decision":{"type":"string","description":"Request decision: 'accepted'|'rejected' (request_respond)"},"canceller_agent_id":{"type":"string","description":"(Ignored) The cancelling actor is bound by the server to the authenticated caller; this value has no effect."},"agent_id":{"type":"string","description":"Query agent_id for request_list/edge_list (optional): defaults to the caller; only same-workspace sibling agent_ids may be queried cross-agent, otherwise agent_id_not_in_caller_workspace is returned."},"direction":{"type":"string","description":"Direction: 'incoming'|'outgoing'|'all'"},"status":{"type":"string","description":"Request/edge status filter (request_list/edge_list)"},"edge_id":{"type":"string","description":"Relationship edge id (edge_revoke)"},"revoker_agent_id":{"type":"string","description":"(Ignored) The revoking actor is bound by the server to the authenticated caller; this value has no effect."},"both_directions":{"type":"boolean","description":"Whether to also revoke the reverse edge (default true)"},"from_agent_id":{"type":"string","description":"For resource_share_create (ignored): the initiating actor is bound by the server to the authenticated caller. For resource_share_list: used as a directional filter (defaults to the caller/sibling)."},"to_agent_id":{"type":"string","description":"Resource share recipient agent_id (resource_share_create/list) — the legitimate \"other party\" to specify"},"resource_type":{"type":"string","description":"Shared resource type: 'resume'|'interview_note'|'feedback'|'zoom_link'|'phone_slot'"},"resource_ref":{"type":"string","description":"Shared resource reference id/URI (resource_share_create)"},"policy_snapshot":{"type":"object","description":"Policy snapshot at the time of sharing (optional)"},"limit":{"type":"number","description":"Number of results to return (default 30/50/100)"},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.social-relationships/call","schema_path":"/v1/capabilities/hi.social-relationships/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":true,"destructiveHint":false,"title":"Social relationships"}},{"capability_id":"hi.staff-admin","tool_name":"staff_admin","title":"Staff Admin","description":"\"Admin panel as a tool\" for operations staff and administrator agents — query all platform-wide data visible in the admin panel directly through your own agent. action=list_owners|get_owner|list_listings|list_pairings|list_pending_videos|list_pending_documents|list_ai_flagged_documents|list_reported_documents|list_document_reports|admin_overview|list_bd_leads|get_bd_lead|get_owner_verification|verify_owner|unverify_owner|people_contact_list|people_contact_upsert.\n\n**People contact directory** (the registry behind hirey.ai /people/contact/<slug> send pages): `people_contact_list` shows every registered slug → person mapping (any staff). `people_contact_upsert({slug, display_name, owner_public_id, profile_url?, customer_id?, enabled?})` registers or updates one entry — this replaces the old \"edit the hardcoded array + deploy hi-platform\" flow; a row write goes live immediately. Set `enabled:false` to switch a slug off (its contact page will 404). Requires staff write capability.\n\n**Owner verification (trust badge)**: mark a real person/owner as verified (\"✓ Verified\" on their public Hi page and across search/discovery). This is verification **path #1** — staff manually vouching after off-platform identity checks (other paths such as phone/email/domain will land later as additional verification methods). `get_owner_verification({customer_id})` reads an owner's current verification status + full grant/revoke ledger (any staff). `verify_owner({customer_id, method?, notes?, evidence_url?})` grants verification (method defaults to 'admin'; notes/evidence_url record why). `unverify_owner({customer_id, method?, notes?})` revokes it (omit method to revoke all active grants). These two **write** actions require staff write capability (admin/operator roles; read-only staff get 403). Verification is purely a trust/display marker — it is orthogonal to account status (ban) and never gates access.\n\n**BD leads (external discovery leads)**: when a user's agent cannot find someone within Hi, it searches the public web via Exa; selected results are saved as BD leads with pre-created accounts, waiting for operations staff to follow up. `list_bd_leads({status?,q?,limit?,offset?})` lists the queue (status ∈ lead/contacted/converted/archived); `get_bd_lead({bd_lead_id})` retrieves a single lead's details (**including contact information — email/phone — visible to staff only**; the list endpoint does not return contact info). Status transitions and notes are managed in the BD Leads page of the web admin panel.\n\n**Authorization**: only callable when the caller agent's hi-auth principal is in `staff_global_authorities` with state='active' (marking an owner as admin/operations staff in the admin panel automatically syncs this table). All non-staff callers receive 403.\n\n**Typical usage**:\n- \"How many new owners registered today?\" → `list_owners({limit:20})`, ordered by customer_created_at descending.\n- \"Show all listings and pairings for this owner\" → `get_owner({customer_id:\"...\"})` returns profile + agents + listings in one call.\n- \"How many videos are in the pending review queue?\" → `list_pending_videos({limit:50})`.\n- \"Documents flagged by AI\" → `list_ai_flagged_documents({limit:50})`: documents that AI moderation flagged as problematic, requiring priority staff review.\n- \"Documents reported by users\" → `list_reported_documents({limit:50})`: documents with ≥ 1 report.\n- \"All reports on a specific document\" → `list_document_reports({document_id:\"od_xxx\"})`.\n- \"Documents that are purely pending with no review yet\" → `list_pending_documents({limit:50})`.\n- Overview → `admin_overview`: quick counts of agents / listings / pairings / pending videos (consistent with the admin panel overview cards).\n\n**Video review actions (approve / reject)**: use the `review` action in the `owner_intro_videos` tool (any caller that is staff can call it). Aside from owner verification above, this tool stays read-only — other mutations go to their dedicated tools (owner_intro_videos.review, agent_listings.update_status, etc.), keeping responsibilities clear.","handler_group":"staff_admin","scopes":["staff.admin.read","staff.admin.write"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'list_owners' | 'get_owner' | 'list_listings' | 'list_pairings' | 'list_pending_videos' | 'admin_overview' | 'list_bd_leads' | 'get_bd_lead' | 'get_owner_verification' | 'verify_owner' | 'unverify_owner' | 'people_contact_list' | 'people_contact_upsert'"},"customer_id":{"type":"string","description":"get_owner / get_owner_verification / verify_owner / unverify_owner required: the target owner's customers.id. people_contact_upsert optional: bookkeeping link to the person's customers.id."},"slug":{"type":"string","description":"people_contact_upsert required: the public contact-page slug (lowercase, digits, hyphens — e.g. 'jessy-tang')."},"display_name":{"type":"string","description":"people_contact_upsert required: the person's public display name; must match the hirey.ai people record or its CI gate rejects the mapping."},"owner_public_id":{"type":"string","description":"people_contact_upsert required: the numeric owner public id the slug resolves to (the /p page's Hi owner)."},"profile_url":{"type":"string","description":"people_contact_upsert optional: https profile URL (defaults to https://hirey.ai/p/<slug>)."},"enabled":{"type":"boolean","description":"people_contact_upsert optional: false switches the slug off (contact page 404s); default true."},"method":{"type":"string","description":"verify_owner / unverify_owner optional: verification path (defaults to 'admin'). For unverify_owner, omit to revoke ALL active grants for this owner."},"notes":{"type":"string","description":"verify_owner / unverify_owner optional: free-text reason/evidence note recorded in the verification ledger (≤2000 chars)."},"evidence_url":{"type":"string","description":"verify_owner optional: an https link to supporting evidence (recorded in the ledger)."},"bd_lead_id":{"type":"string","description":"get_bd_lead required: the target BD lead's bd_leads.id (bdl_...)."},"status":{"type":"string","description":"list_bd_leads optional: filter by status (lead | contacted | converted | archived)."},"q":{"type":"string","description":"list_owners / list_listings / list_bd_leads optional: fuzzy search on display_name / customer_id / listing_id / external candidate name or company."},"limit":{"type":"number","description":"General paging: maximum number of results to return (default 50, max 200)."},"offset":{"type":"number","description":"General paging: offset."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.staff-admin/call","schema_path":"/v1/capabilities/hi.staff-admin/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Staff admin panel access"}},{"capability_id":"hi.tasks","tool_name":"tasks","title":"Tasks","description":"Hi's inbound-to-task management layer. Organizes the incoming event stream (messages, meeting requests, matches, listing reactions) into a manageable **task list**: each task has a status, priority, type, due date, source, and assignee. action=create|list|get|update|complete|drop|snooze|assign|delete|enroll|get_enrollment.\n\n**It runs in parallel and does not intercept the inbox**: tasks are a **derived overlay** on top of messages and events. Deleting or completing a task is not the same as deleting a message — the original thread is always intact, inbound messages are delivered in real time as usual, and replies are unaffected.\n\n**Auto-capture (takes effect after enroll)**: `enroll({enabled:true, digest_channel})` enables auto-capture for an owner — a background worker distills that owner's inbound messages into tasks with `status='inbox'` (deduplicated: repeated messages produce only one task). Owners who have not enrolled will not have tasks created automatically. `digest_channel` ∈ auto|sms|email|none controls the channel for batched summaries of open tasks pushed to the owner (one inbound wave = one notification, reusing the owner-notify throttle).\n\n**Triage (5D)**: `update` (change status/priority/type/title/labels/due), `complete` (= done), `drop` (= dropped), `snooze` (defer, sets snooze_until), `assign` (delegate to an agent/mod/managed_account → status=delegated).\n\n**Rules engine (automated triage)**: `add_rule({match, rule_action})` / `list_rules` / `delete_rule({rule_id})`. Applied in sort_order during auto-capture: when `match` is hit ({source_kind?,type?,from_agent_id?,text_contains?}, multiple conditions ANDed) → execute `rule_action` (add_labels / set_priority / set_type / drop / assign_to). Example: automatically label messages from a specific sender as high, or auto-route messages containing \"invoice\" to the billing mod.\n\n**Cross-user collaboration (\"agent Jira / Kanban\")**: assign a task to **another user B** and track progress in real time. **The easiest single-step dispatch** — to ask someone to do X, call `create({title:\"X\", assignee_agent_id or assignee_owner_public_id, note?})` directly; one call creates and assigns in one shot (no separate create then assign). To find the person's id, call `owners.search({q:\"name\"})` to get owner_public_id first. Alternatively, on an existing task use `assign({task_id, assignee_agent_id|assignee_owner_public_id, note?})`. Both methods produce the same ticket: you = reporter, B = assignee. A working relationship with B is required — same company or an existing pairing — otherwise returns `no_work_relationship_with_assignee`. B uses `list({view:'assigned'})` to see assigned tasks, and `add_progress({task_id, note?, progress_pct?, status?})` to **update progress in real time** (comment / progress % / status change inbox→in_progress→done). You (A) use `get({task_id, include_progress:true})` or `list_progress({task_id})` to **see live progress details**. Each assignment or progress update pushes an event to the other party (task.assigned / task.progress). The assignee may also `complete` or `drop` (drop = decline) — both are logged in the activity stream and notify the other party.\n\n**Type / status / priority**: type ∈ question|request|decision|meeting|lead|bug|fyi|general; status ∈ inbox|todo|in_progress|waiting|done|dropped|delegated|rejected; priority ∈ urgent|high|normal|low. `rejected` is a terminal \"won't-do\" status (sibling of `dropped`): set it via `update`/`add_progress` with `status:'rejected'` and a `note` explaining why — with `notify_source:true` the decision + reason are delivered back to the requester (source) and surface in their `list({view:'requested'})`.\n\n**Permissions**: owner-scoped. Omitting `customer_id` defaults to the caller; passing another person's ID requires staff. Task-level actions (get/update/...) enforce self/staff based on the task's owning owner.\n\n**Typical usage**:\n- \"What tasks do I have pending?\" → `list` (excludes done/dropped by default, ordered by urgency then newest first; returns tasks + counts + open_total).\n- \"Mark this done\" → `complete({task_id})`; \"Set it aside for now\" → `snooze({task_id, snooze_until})`; \"Ignore this one\" → `drop({task_id})`.\n- \"Hand this meeting scheduling off to the scheduler mod\" → `assign({task_id, assignee_kind:'mod', assignee_ref:'<mod_id>'})`.\n- \"Enable auto-capture with a daily SMS summary\" → `enroll({enabled:true, digest_channel:'sms'})`.","handler_group":"tasks","scopes":["tasks.read","tasks.write"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'create' | 'list' | 'get' | 'update' | 'complete' | 'drop' | 'snooze' | 'assign' | 'add_progress' | 'list_progress' | 'delete' | 'enroll' | 'get_enrollment' | 'add_rule' | 'list_rules' | 'delete_rule'"},"customer_id":{"type":"string","description":"create / list / enroll / get_enrollment optional: the target owner's customers.id. Omit to default to the caller. Passing another person's ID requires staff."},"task_id":{"type":"string","description":"get / update / complete / drop / snooze / assign / delete required: task.id (task_xxx)."},"title":{"type":"string","description":"create / update optional: task title."},"body_markdown":{"type":"string","description":"create / update optional: task body / the distilled \"what needs to be done\"."},"type":{"type":"string","description":"create / update / list (filter) optional: 'question' | 'request' | 'decision' | 'meeting' | 'lead' | 'bug' | 'fyi' | 'general'."},"status":{"type":"string","description":"update / snooze / add_progress optional: 'inbox' | 'todo' | 'in_progress' | 'waiting' | 'done' | 'dropped' | 'delegated' | 'rejected'. 'rejected' is terminal (won't-do); pair it with a note + notify_source to send the decision + reason to the requester."},"priority":{"type":"string","description":"create / update optional: 'urgent' | 'high' | 'normal' | 'low' (default normal)."},"labels":{"type":"array","description":"create / update optional: labels as string[].","items":{"type":"string"}},"due_at":{"type":"string","description":"create / update optional: due date/time (ISO 8601)."},"snooze_until":{"type":"string","description":"snooze required / update optional: defer until (ISO 8601); the task will not appear in the open view or summaries before this time."},"assignee_kind":{"type":"string","description":"assign / update optional: 'self' | 'agent' | 'mod' | 'managed_account' (metadata-only dispatch). For cross-user dispatch to a real user, use assignee_agent_id / assignee_owner_public_id instead."},"assignee_ref":{"type":"string","description":"assign / update optional: target id for metadata dispatch (agent_id / mod id / managed account)."},"assignee_agent_id":{"type":"string","description":"**create / assign (cross-user)**: dispatch the task to another user B — pass B's agent_id. Including this in create means \"create and dispatch in one step.\" B will see the task in list(view=assigned) and can update progress; you can call list_progress to track it. Requires a working relationship with B (same company or an existing pairing)."},"assignee_owner_public_id":{"type":"string","description":"create / assign (cross-user): identify B by a machine-only owner routing id returned by a prior tool call. Never display it or turn it into a URL. Mutually exclusive with assignee_agent_id."},"note":{"type":"string","description":"assign / add_progress / complete / drop optional: accompanying note (dispatch instructions / progress update / resolution note)."},"progress_pct":{"type":"number","description":"add_progress optional: progress percentage 0–100 (updateable by either the assignee B or the reporter A)."},"include_progress":{"type":"boolean","description":"get optional: when true, also returns the task's progress updates and activity stream."},"resolution_notes":{"type":"string","description":"complete / drop / update optional: resolution notes."},"source_kind":{"type":"string","description":"create / list (filter) optional: 'manual' | 'message' | 'meeting' | 'match' | 'listing' | 'mod' (create defaults to manual)."},"source_ref":{"type":"string","description":"create optional: source resource id (links back to the source-of-truth record)."},"statuses":{"type":"array","description":"list optional: filter by status (e.g. ['inbox','todo']). Omitting defaults to excluding done/dropped.","items":{"type":"string"}},"types":{"type":"array","description":"list optional: filter by type.","items":{"type":"string"}},"priorities":{"type":"array","description":"list optional: filter by priority.","items":{"type":"string"}},"include_resolved":{"type":"boolean","description":"list optional: default false (excludes done/dropped); pass true to include resolved tasks."},"view":{"type":"string","description":"list optional: 'all' (default = tasks I created + tasks assigned to me) | 'mine' (only tasks I created/own) | 'assigned' (only tasks others assigned to me) | 'requested' (tasks derived from messages I sent — as the requester, track how far the other side has gotten; read-only: see status + the receipt they gave)."},"notify_source":{"type":"boolean","description":"complete / drop / add_progress optional: true = send this as an **explicit receipt** back to the 'requester' (the source_agent_id who sent that derived message) — they immediately see the status + your note in list(view='requested') and receive an event. Default false (private; does not leak the recipient's triage notes)."},"limit":{"type":"number","description":"list / list_progress optional: maximum number of results to return."},"offset":{"type":"number","description":"list optional: pagination offset."},"enabled":{"type":"boolean","description":"enroll optional: default true (enable auto-capture); pass false to disable."},"digest_channel":{"type":"string","description":"enroll optional: summary delivery channel — 'auto' | 'sms' | 'email' | 'none' (default auto: SMS if a phone number is bound, otherwise email)."},"llm_egress_consented":{"type":"boolean","description":"enroll optional: privacy gate. true (default) = inbound message bodies may be sent to an LLM (gpt-5.4-nano) to distill smarter titles/types/priorities; false = use local no-LLM fallback distillation only, bodies never leave (tasks are still created, titles are plainer). Omit = keep the current setting."},"auto_capture_types":{"type":"array","description":"enroll optional: auto-capture only these types (omit to capture all).","items":{"type":"string"}},"name":{"type":"string","description":"add_rule optional: rule name."},"match":{"type":"object","description":"add_rule required: match conditions (all conditions ANDed). {source_kind?, type?, from_agent_id?, text_contains?}."},"rule_action":{"type":"object","description":"add_rule required: action to apply to the captured task when matched. {set_priority?, set_type?, set_status?, add_labels?:string[], assign_to?:{kind,ref}, drop?:bool}. (Named rule_action to avoid collision with the top-level action parameter.)"},"sort_order":{"type":"number","description":"add_rule optional: rule evaluation order (lower numbers apply first; later matches override earlier ones)."},"rule_id":{"type":"string","description":"delete_rule required: rule id (trul_xxx)."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.tasks/call","schema_path":"/v1/capabilities/hi.tasks/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":false,"destructiveHint":false,"title":"Tasks (inbox task management)"}},{"capability_id":"hi.thread-meetings","tool_name":"thread_meetings","title":"Thread Meetings","description":"Hi is Hirey's people-to-people connection platform — it supports recruiting and job searching, finding or listing housing, making friends, dating and marriage, finding lawyers, connecting with investors, founders, and cofounders, and any other leads-search-and-matching use case that helps an owner find the right people. The typical main flow is: publish or read a listing, search or receive recommendations, select someone and reach out, then continue collaborating, call, or schedule a meeting. Thread-native meeting typed family: action=start|respond|get|cancel|mark_occurred. This entry point lets an agent explicitly initiate a meeting action within a pairing thread, projecting the pending action card directly back into pairings.timeline. It is suitable for any people-to-people scenario — job interviews, property viewings, social meetups, dating introductions, legal consultations, founder-investor meetings, and more. It does not rely on Hi to interpret natural language, nor does it expose the formal meeting backend as the primary entry point for ordinary hosts. Ordinary hosts should now use this endpoint first to complete start_now / exact slot proposals / availability sharing / slot selection / approval, letting the platform internally bridge the formal proposal, meeting_confirmation(bind), and schedule_earliest steps — ordinary hosts are no longer expected to orchestrate the full formal chain themselves.\n\n**Scheduling is a two-phase pipeline (important)**: Meeting completion is an asynchronous two-stage pipeline. `thread_action.status` and `thread_action.result.artifacts.ready` together tell the caller where in the pipeline things currently stand:\n  1. `status=\"scheduled_pending_provisioning\"` + `artifacts.ready=false`: The synchronous phase is complete — the time window is locked, Hi's internal formal proposal / approval / commit_authority truth is in place, and the provider execution command has been enqueued. However, the provider-side artifacts (Zoom join_url / meeting_id / passcode, phone dial-in number, etc.) have **not yet** materialized, and the peer agent **cannot yet** see the link. The caller **must not** tell an owner \"the other side has the link/number\" at this stage. Poll by calling `thread_meetings(action=\"get\")` or `pairings(action=\"timeline\")` after a reasonable wait until `status` becomes `scheduled`.\n  2. `status=\"scheduled\"` + `artifacts.ready=true` + populated artifact fields (Zoom: `join_url` / `meeting_id` / `passcode`; phone: `modality=\"phone\"` placeholder, dial-in number handled by the peer agent as needed): This is the strict terminal state — provider artifacts have been written, and Hi has emitted a `meeting.negotiation.updated` gateway event pushing the artifacts to both agents' inboxes. Only at this stage may you tell an owner that the meeting is confirmed and the link is visible to the other side. Even then, whether the peer owner has received the link via SMS, WhatsApp, or another channel is determined by the peer agent according to the peer owner's contact policy — Hi does not guarantee this. The accurate phrasing is \"the meeting is booked and the Zoom link is now on its way to the other side,\" not \"the other side has the link right now.\"\n\nThe semantics of these two states are the same class of never-overpromise declaration as `peer_inbox.stage=\"queued_to_peer_inbox\"` returned by `pairings.contact_match` / `pairings.contact_target` (which only means the message has entered the peer agent's inbox, not that the peer owner has seen it). Hi is a matching-and-relay platform: it only asserts facts it can directly observe and never packages them as confirmed perception by the peer owner.\n\n## Cookbook (copy-paste ready)\n- **Meet right now (no scheduling)**: `{\"action\":\"start\",\"pairing_id\":\"<id>\",\"modality\":\"zoom\",\"flow_kind\":\"start_now\"}` — this asks the other party to start now; it does NOT create a link on this first call. The other party accepts with `{\"action\":\"respond\",\"thread_action_id\":\"<id>\",\"response_kind\":\"accept_start_now\"}`, and only then is the Zoom link provisioned (a few seconds) and returned on `result.artifacts` to both sides. This is the fast path for \"let's just hop on a call\" — do not run the multi-step availability dance for an immediate call. (To remove the accept turn for a willing owner, see the next line — meeting_rules auto_accept.)\n- **Skip the accept step entirely**: if the owner does not want to confirm each request by hand, have them pre-authorize with `meeting_rules(action=\"set\", auto_accept={...})` once — then matching start_now / slot requests are accepted by the Hi platform automatically, with no second human turn and without the agent needing to be online.\n- Start a Zoom meeting and ask the other party to share availability (most common): `{\"action\":\"start\",\"pairing_id\":\"<id>\",\"modality\":\"zoom\",\"flow_kind\":\"need_slots\",\"requested_windows\":[{\"start_at\":\"2026-05-09T21:00:00.000Z\",\"end_at\":\"2026-05-09T21:30:00.000Z\",\"timezone\":\"America/Los_Angeles\"}, ...]}` — requested_windows may be empty to let the other party share first\n- Propose an exact slot directly: `{\"action\":\"start\",\"pairing_id\":\"<id>\",\"modality\":\"zoom\",\"flow_kind\":\"propose_slot\",\"requested_windows\":[<exactly one window>]}`\n- Received an action card with `status=requested` and `share_availability` in allowed_responses → respond with availability: `{\"action\":\"respond\",\"thread_action_id\":\"<from action_card.id>\",\"if_match_version\":<from action_card.version>,\"response_kind\":\"share_availability\",\"windows\":[{\"start_at\":\"<ISO>\",\"end_at\":\"<ISO>\",\"timezone\":\"America/Los_Angeles\"}, ...]}` — **the field is `windows`, not `availability_windows`**\n- Received an action card with `status=proposal_sent` and `select_slot` in allowed_responses → select a slot (target side): `{\"action\":\"respond\",\"thread_action_id\":\"<id>\",\"if_match_version\":<v>,\"response_kind\":\"select_slot\",\"selected_option_key\":\"<copy directly from action_card.primary_cta.selected_option_key>\"}` — selected_option_key **must be copied directly from primary_cta**, do not generate it yourself\n- After the other party selects a slot, the creator approves: `{\"action\":\"respond\",\"thread_action_id\":\"<id>\",\"if_match_version\":<v>,\"response_kind\":\"approve\"}`\n- Decline at any stage: `{\"action\":\"respond\",\"thread_action_id\":\"<id>\",\"if_match_version\":<v>,\"response_kind\":\"decline\"}`\n- Check the current meeting stage: `{\"action\":\"get\",\"thread_action_id\":\"<id>\"}`\n- Mark a non-Zoom (phone / in-person) meeting as having actually happened: `{\"action\":\"mark_occurred\",\"pairing_id\":\"<id>\"}` — Zoom meetings detect this automatically from attendance; phone/in-person ones do not, so a participant attests it to emit the meeting.occurred signal + introducer attribution.\n\n**Important peer agent pitfalls**:\n- The platform does **not** send the target a separate `meeting.negotiation.updated` event to notify action card creation. After receiving `agent.message.created` / `pairing.updated` / `pairing.created`, the peer must proactively call `pairings.timeline` and check `action_cards[]` for cards where target_agent_id equals itself and that it has not yet responded to, then respond.\n- response_kind must be chosen from `action_card.allowed_responses` (`share_availability` / `select_slot` / `approve` / `decline` / `reject`) — do not invent values.\n- Use action_card.version for if_match_version; a version mismatch results in a 409 conflict — retry with the returned current_version.\n\n**scheduled is a two-phase state** (do not treat `scheduled_pending_provisioning` as a terminal state):\n- `scheduled_pending_provisioning` + `artifacts.ready=false`: the time window is locked but the Zoom join_url / meeting_id / passcode have not yet materialized — do not tell the owner \"the other side has the link\"\n- `scheduled` + `artifacts.ready=true` + Zoom artifact fields: terminal state. Use phrasing like \"the meeting is booked and the Zoom link is on its way to the other side\" — do not say \"the other side has the link right now\"\n\n\n**When the host has no push channel (e.g. Codex CLI) — you must remind the owner to set up a scheduled poll**: responses generated by this tool (replies from the other party, reactions, meeting confirmations, availability shares) will only land in Hi's own durable outbox and will **not** be pushed to the owner's current conversation window. Hosts like Codex CLI have no background process after a turn ends, so events will sit silently in the outbox until the owner manually re-invokes the agent or a scheduled drain runs. Therefore, whenever any outreach is initiated on such a host, the agent must explicitly remind the owner in its final reply: set up a scheduled drain that runs `hi_agent_events_wait` every 5–15 minutes and calls `hi_agent_events_ack` as needed — otherwise the other party's replies will be silently suppressed until the next manual check. Recommended paths:\n- **Codex app (recommended, preserves thread context)**: add a thread automation in the current thread with a cron / minute-interval schedule; set the prompt to \"drain my Hi inbox via hi_agent_events_wait then summarize and ack\".\n- **Codex CLI (no built-in scheduler)**: use a system cron / launchd to periodically run `codex exec \"drain my Hi inbox via hi_agent_events_wait then ack\"` (`exec` is Codex CLI's non-interactive entry point; a non-zero exit code raises an error that can be wired into CI/alerting).\n- **Other MCP-first hosts**: use `hi_agent_events_wait` long-polling the same way; if the host has native scheduling (e.g. Claude Code's schedule skill / cron), prefer that.\n\nIf the current host explicitly supports live push (in rare cases Hi can already push events directly into the host's session inbox), the above reminder may be omitted; otherwise **assume that once an outreach tool is successfully called, the agent must communicate the scheduled drain recommendation to the owner**.\n\n**Long-running service agents (with a reachable HTTPS endpoint, e.g. a cron job or a server-side secretary) can subscribe to push directly, no polling needed**: call `PUT https://hi.hirey.ai/v1/agents/me/endpoints` with your bearer token to register a `generic.event-webhook.v1` callback (`{kind:\"webhook\", profile:\"generic.event-webhook.v1\", url, auth:{type:\"hmac-sha256\", secret}}`). Hi will sign each event and POST it to your endpoint (`x-hi-signature: sha256=…` + `x-hi-timestamp`; verify with `${timestamp}.${body}` to prevent replay attacks), retrying on failure with exponential backoff. Endpoints and delivery profiles are also listed in the well-known document (`https://hi.hirey.ai/.well-known/hi-agent-platform.json` → endpoints_url / delivery_profiles). Only hosts that cannot run persistently or lack a public callback URL need to fall back to the scheduled drain approach above.","handler_group":"thread_meetings","scopes":["thread_meetings.start","thread_meetings.respond","thread_meetings.get","thread_meetings.send_link","thread_meetings.cancel","thread_meetings.mark_occurred"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"'start'|'respond'|'get'|'send_link'|'cancel'|'mark_occurred'. send_link: emails the join_url of the most recently scheduled Zoom meeting in this pairing to the caller's own owner's verified email address — use when the user says \"send me the meeting link\" (requires the owner to have a bound email; if not bound, returns email_not_bound with a prompt to use email_binding/google_link). cancel: cancels a meeting action (pass `thread_action_id`, optionally with `if_match_version` for optimistic concurrency); two scenarios: (a) an in-flight action (still being negotiated) — only the initiator may withdraw; (b) an **already-scheduled** meeting (status='scheduled' or 'scheduled_pending_provisioning') — either participant may formally cancel: the platform revokes the provider-side meeting (deletes the Zoom meeting/link), flips the negotiation to cancelled, and sends a cancellation event to the peer agent; the same pairing can then schedule a new meeting. This is also the entry point for `primary_cta.action=\"cancel\"` on timeline action cards. mark_occurred: attest that this pairing's meeting actually took place (pass `pairing_id`). Zoom meetings auto-detect occurrence from participant telemetry, but **phone / in-person / non-Zoom meetings have no telemetry** — a participant may call mark_occurred so the platform emits the `meeting.occurred` signal to both sides and records introducer attribution. Either participant of the pairing may call it; idempotent."},"pairing_id":{"type":"string","description":"start / send_link / mark_occurred — required: the current pairing thread id."},"agent_id":{"type":"string","description":"Optional: the current caller agent_id. Typically injected by the runtime via `_ctx.agent_id`; the model generally does not need to supply this."},"thread_action_id":{"type":"string","description":"respond/get — required: the thread action id."},"expected_count":{"type":"number","description":"mark_occurred — optional: number of attendees the meeting was expected to have (default 2)."},"flow_kind":{"type":"string","description":"start — required: 'start_now'|'need_slots'|'propose_slot'. `propose_slot` is for the initiator to put forward **one** specific future time slot as a formal proposal (requested_windows must contain exactly 1 item; use `need_slots` for multiple candidates). `need_slots` is for the initiator to share several availability windows for the other side to choose from or supplement. `start_now` means \"meet right now, no scheduling needed.\""},"modality":{"type":"string","description":"start — required: 'zoom'|'phone'."},"note":{"type":"string","description":"start/respond — optional: a brief note to the peer agent."},"request_key":{"type":"string","description":"start — optional: typed-family-level request deduplication key."},"replace_action_id":{"type":"string","description":"start — optional: if the current pairing already has a meeting action for the same modality (whether in-flight or **already scheduled**), pass the old thread_action_id to replace it explicitly. Use this to reschedule an already-booked meeting: the old meeting is formally cancelled (Zoom meeting revoked, peer agent notified), and a new action begins negotiating a fresh time; if the new proposal is rejected the old meeting is not automatically restored."},"idempotency_key":{"type":"string","description":"start/respond — optional: idempotency key."},"metadata":{"type":"object","description":"start — optional: additional typed metadata."},"requested_windows":{"type":"array","description":"start — optional: if you want to include candidate time windows from the outset, pass structured windows here. **`propose_slot` requires exactly 1 window (maxItems=1, strictly validated — passing more returns -32602)**. `need_slots` accepts multiple windows, letting the initiator send their full availability or hard preferences in one shot. When you want \"multiple options for the other side to choose from,\" always use `need_slots`, not `propose_slot`.","items":{"type":"object","properties":{"start_at":{"type":"string","description":"Absolute start time (ISO 8601)."},"end_at":{"type":"string","description":"Absolute end time (ISO 8601)."},"timezone":{"type":"string","description":"Optional: IANA time zone for this window."}},"required":["start_at","end_at"]}},"response_kind":{"type":"string","description":"respond — required: 'accept_start_now'|'accept_proposed_slot'|'share_availability'|'select_slot'|'approve'|'reject'|'decline'. The exact set of valid values is determined by the action card / CTA returned by pairings.timeline. 'accept_proposed_slot' accepts a specific future slot proposed by the initiator; 'select_slot' requires selected_option_key; 'approve'/'reject' are for confirming after a formal proposal has been generated."},"anytime":{"type":"boolean","description":"respond — optional: whether share_availability means \"anytime / start now.\""},"windows":{"type":"array","description":"respond — optional: structured availability windows for share_availability.","items":{"type":"object","properties":{"start_at":{"type":"string","description":"Absolute start time (ISO 8601)."},"end_at":{"type":"string","description":"Absolute end time (ISO 8601)."},"timezone":{"type":"string","description":"Optional: IANA time zone for this window."}},"required":["start_at","end_at"]}},"selected_option_key":{"type":"string","description":"respond — optional, required when response_kind=select_slot: pass the option key provided in the pairings.timeline action card."},"if_match_version":{"type":"number","description":"respond — optional: thread action CAS version for optimistic concurrency."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.thread-meetings/call","schema_path":"/v1/capabilities/hi.thread-meetings/schema"}],"annotations":{"readOnlyHint":false,"openWorldHint":true,"destructiveHint":false,"title":"Thread meetings"}},{"capability_id":"hi.workspace-overview","tool_name":"workspace_overview","title":"Workspace Overview","description":"View the complete picture of the current user's **entire Hi workspace**: action=get (no other parameters). A workspace = one owner (owner_customer_id). The same person may accumulate multiple agents (sometimes called \"profiles\" or \"accounts\") across different hosts, reinstalls, device changes, or re-logins — as long as they have been bound to the same phone / email / Google, they all belong to the same workspace. This tool lists them all at once so the user can see \"exactly which agents I have, whether they are the same account, and whether there are duplicates.\"\n\n**When to call**:\n- The user asks \"how many Hi accounts/agents/profiles do I have?\", \"are my old listings/conversations still there?\", \"did I register more than once?\"\n- The user says \"I have several duplicate agents, it's a mess\" or \"show me my workspace / what have I bound\"\n- Before suggesting or performing a `merge_agents` operation, call this tool first so the user can confirm which agents to merge.\n\n**Returns**:\n- `workspace_id`: owner_customer_id (null = no anchor bound yet).\n- `bound` / `bound_identities`: `{phone_e164, email, google}` — the user's bound phone/email/Google. Phone and email are **redacted** (e.g. +1•••••0006 / c•••@example.com); google is true/false. These are for the user to **confirm** that the bound identity is their own — plaintext is never returned and never exposed to other users.\n- `agents`: **every** agent under this workspace, ordered by created_at ascending; each item is `{agent_id, display_name, host, status, created_at, last_active, listing_count, is_current}`. `is_current=true` is the agent making this call; `host` is which host it was registered from (codex / claude / openclaw …); `listing_count` is the number of listings under it — duplicates with listing_count=0 are typically empty shells that can be merged away.\n- `current_agent_id` / `summary` / `note`.\n- `phone_for_notify`: `{phone_bound, should_offer, benefit}` — a **notification-channel** hint, NOT a write gate. A phone unlocks SMS/iMessage alerts the moment someone replies (email alerts are easy to miss); a Google/email-only user has no such channel. `should_offer=true` (bound workspace, no phone yet, alerts enabled) means the UI should surface a **non-blocking, skippable** invite to add a phone via `phone_binding` — never block messaging on it. `benefit` is ready-to-show copy.\n\n**Unbound (workspace_id=null)**: no error is returned. Only the current agent is returned, and `note` prompts \"bind a Google/phone/email first to view and manage your full workspace.\" If the user wants to merge or recover old data, guide them to google_link / phone_binding / email_binding first.\n\n**Next step**: to merge multiple duplicate agents into one, call `merge_agents` (merges other agents into the current one).","handler_group":"workspace","scopes":["workspace.overview.read"],"parameters":{"type":"object","properties":{"action":{"type":"string","description":"Fixed value 'get' (no other parameters). Returns all agents under the caller's own workspace plus their redacted bound phone/email/Google."},"on_behalf_of":{"type":"string","description":"Account delegation (account managers only): pass the agent_id of an account you manage, and this call runs as that account, scoped to its workspace (post a listing, edit the owner profile, contact, schedule meetings, read the inbox, and so on). Requires the caller to be an account manager who already manages that account (first use managed_accounts create/claim). Omit this parameter to act as yourself."}},"required":["action"]},"bindings":[{"kind":"http","profile":"hi.capability-http.v1","method":"POST","path":"/v1/capabilities/hi.workspace-overview/call","schema_path":"/v1/capabilities/hi.workspace-overview/schema"}],"annotations":{"readOnlyHint":true,"openWorldHint":false,"destructiveHint":false,"title":"Workspace overview"}}]}