> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ando.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Our reasoning

export const connectionProviders = [{
  id: "linear",
  name: "Linear",
  logo: "https://app.ando.so/linear-logo.png",
  docsUrl: "https://linear.app/developers/oauth-2-0-authentication"
}, {
  id: "notion",
  name: "Notion",
  logo: "https://app.ando.so/notion-logo.png",
  docsUrl: "https://developers.notion.com/guides/get-started/authorization"
}, {
  id: "github",
  name: "GitHub",
  logo: "https://app.ando.so/github-logo.png",
  docsUrl: "https://docs.github.com/en/apps/using-github-apps/authorizing-github-apps"
}, {
  id: "hubspot",
  name: "HubSpot",
  logo: "https://app.ando.so/hubspot-logo.png",
  docsUrl: "https://developers.hubspot.com/docs/apps/developer-platform/build-apps/authentication/oauth/working-with-oauth"
}, {
  id: "granola",
  name: "Granola",
  logo: "https://app.ando.so/granola-logo.png",
  docsUrl: "https://docs.granola.ai/help-center/sharing/integrations/mcp"
}, {
  id: "sentry",
  name: "Sentry",
  logo: "https://app.ando.so/sentry-logo.png",
  docsUrl: "https://docs.sentry.io/api/permissions/"
}];

export const connectionConstraints = [{
  id: "target",
  label: "Where it connects",
  technicalLabel: "Target",
  summary: "Account · workspace · organization · tenant",
  definition: "The provider account or workspace linked to Ando.",
  owner: "provider",
  providerValues: {
    linear: "Workspace",
    notion: "Workspace",
    github: "Organization or user account",
    hubspot: "HubSpot account or portal",
    granola: "Personal or team space",
    sentry: "Organization"
  }
}, {
  id: "principal",
  label: "Who it acts as",
  technicalLabel: "Principal",
  summary: "Person · app · bot · service account",
  definition: "The person, app, bot, or service account whose provider permissions the connection uses.",
  owner: "provider",
  providerValues: {
    linear: "User or app actor",
    notion: "Integration bot authorized by a user",
    github: "App or bot, or a user token",
    hubspot: "Authorizing user",
    granola: "User or enterprise credential",
    sentry: "User or organization integration"
  }
}, {
  id: "grant-ceiling",
  label: "What it can do",
  technicalLabel: "Grant ceiling",
  summary: "Actions allowed by scopes, capabilities, and roles",
  definition: "The actions the provider allows through scopes, capabilities, roles, and the identity's current access.",
  owner: "provider",
  providerValues: {
    linear: "Read, write, admin, and targeted OAuth scopes",
    notion: "Read, update, insert, comments, and user-information capabilities",
    github: "Granular repository, organization, and account permissions",
    hubspot: "Required, conditionally required, and optional OAuth scopes",
    granola: "Server-defined tools available through OAuth or an API key",
    sentry: "Organization, project, team, member, and event scopes"
  }
}, {
  id: "resource-boundary",
  label: "What it can reach",
  technicalLabel: "Resource boundary",
  summary: "All resources · selected resources · identity-based access",
  definition: "The provider data available to the connection: everything, selected resources, or whatever the acting identity can access.",
  owner: "provider",
  providerValues: {
    linear: "Derived from workspace membership and provider permissions",
    notion: "Selected pages and databases, plus their descendants",
    github: "All or selected repositories",
    hubspot: "Mostly account- and object-level",
    granola: "Owned or shared notes, or the enterprise team space",
    sentry: "Organization-specific, with optional organization and project constraints"
  }
}, {
  id: "ando-policy",
  label: "Rules for use",
  technicalLabel: "Ando policy",
  summary: "Allow · ask first · block · filter resources",
  definition: "Fine grained control over tools",
  owner: "ando",
  providerValues: {
    linear: "Allow search; ask before creating or updating issues",
    notion: "Allow reading; ask before editing shared pages",
    github: "Allow investigation; ask before opening or merging pull requests",
    hubspot: "Allow lookup; ask before changing CRM records",
    granola: "Allow reading notes; block sharing or export when needed",
    sentry: "Allow investigation; ask before mutating issues"
  }
}, {
  id: "consumers",
  label: "Who can use it",
  technicalLabel: "Consumers",
  summary: "Agents · automations · product features",
  definition: "You have fine grain controls for which agents and even humans are delegated which credentials",
  owner: "ando",
  providerValues: {
    linear: "Product, project-management, and engineering agents",
    notion: "Knowledge, research, and onboarding agents",
    github: "Engineering and security agents",
    hubspot: "Sales and support agents",
    granola: "Personal meeting and follow-up agents",
    sentry: "Engineering and incident-response agents"
  }
}];

export const ProviderMark = ({providerId, linked = false}) => {
  const provider = connectionProviders.find(item => item.id === providerId);
  const contents = <>
      <span className="ando-reasoning-provider-logo">
        <img src={provider.logo} alt="" />
      </span>
      <span>{provider.name}</span>
      {linked ? <span className="ando-reasoning-external" aria-hidden="true">
          ↗
        </span> : null}
    </>;
  return linked ? <a className="ando-reasoning-provider-mark" href={provider.docsUrl} target="_blank" rel="noreferrer">
      {contents}
    </a> : <span className="ando-reasoning-provider-mark">{contents}</span>;
};

export const ConstraintCard = ({constraint}) => {
  const detailsId = `ando-reasoning-${constraint.id}-details`;
  return <div className="ando-reasoning-constraint-shell" data-selected={constraint.id === "target" ? "true" : "false"}>
      <button type="button" className="ando-reasoning-constraint" data-owner={constraint.owner} data-constraint={constraint.id} aria-describedby={detailsId} aria-controls={`${detailsId}-card`} aria-pressed={constraint.id === "target"}>
        <span className="ando-reasoning-glyph" aria-hidden="true">
          <svg viewBox="0 0 80 94">
            <path className="ando-reasoning-glyph-top" d="M40 10 70 25 40 40 10 25Z" />
            <path className="ando-reasoning-glyph-left" d="M10 25 40 40 40 84 10 69Z" />
            <path className="ando-reasoning-glyph-right" d="M40 40 70 25 70 69 40 84Z" />
            <path className="ando-reasoning-glyph-layer" d="m10 52 30 15 30-15" />
            <path className="ando-reasoning-glyph-hatch" d="m17 31 23 12m-23-4 23 12m-23-4 23 12m-23-4 23 12m-23-4 23 12" />
          </svg>
        </span>

        <span className="ando-reasoning-constraint-heading">
          <span>{constraint.label}</span>
        </span>
        <span className="ando-reasoning-visually-hidden">
          {constraint.summary}
        </span>
      </button>
    </div>;
};

export const ConstraintDetails = ({constraint}) => {
  const detailsId = `ando-reasoning-${constraint.id}-details`;
  const isDefault = constraint.id === "target";
  const isRulesForUse = constraint.id === "ando-policy";
  const isWhoCanUseIt = constraint.id === "consumers";
  return <div id={`${detailsId}-card`} className="ando-reasoning-detail-card" data-constraint={constraint.id} data-visible={isDefault ? "true" : "false"} aria-hidden={isDefault ? "false" : "true"}>
      <div className="ando-reasoning-detail-heading">
        <span>{constraint.technicalLabel}</span>
        <strong>{constraint.label}</strong>
        <p id={detailsId}>{constraint.definition}</p>
      </div>

      {isRulesForUse ? <a className="ando-reasoning-rules-image-link" href="/images/rules-for-use-granola.png" target="_blank" rel="noreferrer" aria-label="Open the Granola Rules for Use example at full size">
          <img className="ando-reasoning-rules-image" src="/images/rules-for-use-granola.png" alt="Granola Rules for Use with all six tools enabled." />
        </a> : isWhoCanUseIt ? <a className="ando-reasoning-consumers-image-link" href="/images/who-can-use-it-github-access.png" target="_blank" rel="noreferrer" aria-label="Open the GitHub access delegation example at full size">
          <img className="ando-reasoning-consumers-image" src="/images/who-can-use-it-github-access.png" alt="GitHub access settings showing workspace and personal credentials delegated with different access levels." />
        </a> : <div className="ando-reasoning-provider-grid">
          {connectionProviders.map(provider => <div className="ando-reasoning-provider-row" key={provider.id}>
              <ProviderMark providerId={provider.id} linked={constraint.owner === "provider"} />
              <p>{constraint.providerValues[provider.id]}</p>
            </div>)}
        </div>}
    </div>;
};

export const updateConstraintConnector = (root, constraintId) => {
  const boundary = root.querySelector(".ando-reasoning-boundary");
  const source = root.querySelector(`.ando-reasoning-constraint[data-constraint="${constraintId}"] .ando-reasoning-constraint-heading`);
  const destination = root.querySelector(`.ando-reasoning-detail-card[data-constraint="${constraintId}"]`);
  const path = root.querySelector(`.ando-reasoning-selection-lines path[data-constraint="${constraintId}"]`);
  if (!boundary || !source || !destination || !path) return;
  const boundaryRect = boundary.getBoundingClientRect();
  const sourceRect = source.getBoundingClientRect();
  const destinationRect = destination.getBoundingClientRect();
  const scaleX = 900 / boundaryRect.width;
  const scaleY = 420 / 420;
  const startX = (sourceRect.right - boundaryRect.left) * scaleX;
  const startY = (sourceRect.top + sourceRect.height / 2 - boundaryRect.top) * scaleY;
  const endX = (destinationRect.left - boundaryRect.left) * scaleX;
  const endY = (destinationRect.top + destinationRect.height / 2 - boundaryRect.top) * scaleY;
  const bend = Math.max((endX - startX) * 0.48, 36);
  path.setAttribute("d", `M ${startX} ${startY} C ${startX + bend} ${startY}, ${endX - bend} ${endY}, ${endX} ${endY}`);
};

export const setActiveConstraint = (root, constraintId) => {
  root.dataset.activeConstraint = constraintId;
  root.querySelectorAll(".ando-reasoning-constraint-shell").forEach(shell => {
    const button = shell.querySelector(".ando-reasoning-constraint");
    const isActive = button?.dataset.constraint === constraintId;
    shell.dataset.selected = isActive ? "true" : "false";
    shell.querySelector(".ando-reasoning-constraint")?.setAttribute("aria-pressed", isActive ? "true" : "false");
  });
  root.querySelectorAll(".ando-reasoning-detail-card").forEach(card => {
    const isActive = card.dataset.constraint === constraintId;
    card.dataset.visible = isActive ? "true" : "false";
    card.setAttribute("aria-hidden", isActive ? "false" : "true");
  });
  updateConstraintConnector(root, constraintId);
};

export const ConnectionReasoning = () => {
  const activateFromEvent = event => {
    const button = event.target.closest?.(".ando-reasoning-constraint");
    if (button && event.currentTarget.contains(button)) {
      setActiveConstraint(event.currentTarget, button.dataset.constraint);
    }
  };
  return <div className="ando-reasoning-model" data-active-constraint="target" ref={root => {
    if (root) {
      window.requestAnimationFrame(() => setActiveConstraint(root, root.dataset.activeConstraint));
    }
  }} onFocus={activateFromEvent} onClick={activateFromEvent}>
      <div className="ando-reasoning-boundary" aria-label="App connections are the intersection of provider opinions and Ando opinions">
        <div className="ando-reasoning-scene">
          <svg className="ando-reasoning-grid" viewBox="0 0 900 540" preserveAspectRatio="none" aria-hidden="true">
            <defs>
              <pattern id="ando-reasoning-grid-pattern" width="42" height="24" patternUnits="userSpaceOnUse">
                <path d="M-21 12 0 0l21 12L0 24Z" />
                <path d="M21 12 42 0l21 12-21 12Z" />
              </pattern>
            </defs>
            <path className="ando-reasoning-grid-plane" d="M450 18 884 270 450 522 16 270Z" />
            <path className="ando-reasoning-grid-lines" d="M450 18 884 270 450 522 16 270Z" />
            <path className="ando-reasoning-flow-line" d="M256 250c37 54 103 92 178 131" />
            <path className="ando-reasoning-flow-line" d="M644 250c-37 54-103 92-178 131" />
          </svg>

          <div className="ando-reasoning-inputs">
            <section className="ando-reasoning-input-group" data-owner="provider" aria-labelledby="provider-limits-heading">
              <header className="ando-reasoning-input-heading">
                <strong id="provider-limits-heading">Provider Opinions</strong>
                <span>What external systems permit</span>
              </header>

              <div className="ando-reasoning-constraints">
                {connectionConstraints.filter(constraint => constraint.owner === "provider").map(constraint => <ConstraintCard key={constraint.id} constraint={constraint} />)}
              </div>
            </section>

            <div className="ando-reasoning-intersection" aria-hidden="true">
              <span>∩</span>
              <small>evaluated together</small>
            </div>

            <section className="ando-reasoning-input-group" data-owner="ando" aria-labelledby="ando-limits-heading">
              <header className="ando-reasoning-input-heading">
                <strong id="ando-limits-heading">Ando Opinions</strong>
                <span>What Ando permits</span>
              </header>

              <div className="ando-reasoning-constraints">
                {connectionConstraints.filter(constraint => constraint.owner === "ando").map(constraint => <ConstraintCard key={constraint.id} constraint={constraint} />)}
              </div>
            </section>
          </div>

          <div className="ando-reasoning-effective">
            <div className="ando-reasoning-effective-object" aria-hidden="true">
              <span></span>
              <span></span>
              <span></span>
              <span></span>
              <span></span>
            </div>
            <div className="ando-reasoning-effective-copy">
              <strong>App Connections</strong>
              <span>Provider opinions ∩ Ando opinions</span>
              <p>The access left after every constraint is applied together is what forms app connections.</p>
            </div>
          </div>
        </div>

        <svg className="ando-reasoning-selection-lines" viewBox="0 0 900 420" preserveAspectRatio="none" aria-hidden="true">
          <path data-constraint="target" d="M102 130 C 310 130, 470 154, 610 154" />
          <path data-constraint="principal" d="M210 130 C 350 130, 490 154, 610 154" />
          <path data-constraint="grant-ceiling" d="M102 208 C 310 208, 470 166, 610 154" />
          <path data-constraint="resource-boundary" d="M210 208 C 350 208, 490 166, 610 154" />
          <path data-constraint="ando-policy" d="M390 130 C 475 130, 535 146, 610 154" />
          <path data-constraint="consumers" d="M500 130 C 545 130, 575 146, 610 154" />
        </svg>

        <aside className="ando-reasoning-detail-rail" aria-label="Selected constraint details" aria-live="polite">
          <span className="ando-reasoning-detail-kicker">Selected constraint</span>
          {connectionConstraints.map(constraint => <ConstraintDetails key={constraint.id} constraint={constraint} />)}
        </aside>
      </div>
    </div>;
};

This doc covers how Ando maps the wide universe of provider permission models into one consistent connection model.

In our attempt to build the Lowest Common Denominator™ permission model, we studied how a bunch of apps handle permissions, especially as human and agent tool use starts to intermingle. There’s surprisingly little consensus. Some believe agents should always be “leashed” to a human, while others, like us, treat agents as first-class users with seats of their own.

Click on any of the elements in the diagram to see more detail.

<ConnectionReasoning />
