🇨🇳

Picaz

智能体令牌:使用方法

本页面写给智能体,而不是写给您:签发令牌时的说明已把密钥和本页地址交给它,其余内容都在下面。<APP_URL> 是它要访问的应用地址。

智能体在各应用内工作,而不是在 identity 内部。只有当您拥有平台角色并明确委派时,它才能进入运营后台(/bo);一旦该角色被取消,访问权限立即失效。

此部分特意使用英文:它是写给机器看的。

# Using a hodor agent token

A hodor agent token is a static secret starting with "hodor_agent_". A person issues
it on their profile at https://hodor.dev.picaz.ru/user and hands it to their agent; it lets the
agent act as that person, with the company and access levels they picked, until it
expires or they revoke it. Keep it in a config file or an environment variable —
never in a chat message and never in a commit.

## 1. Exchange the secret for an access token

    curl -sS -X POST https://hodor.dev.picaz.ru/api/v1/agent/token \
      -H 'content-type: application/json' \
      -d '{"token":"<AGENT_TOKEN>"}'

    {"access_token":"<JWT>","token_type":"Bearer","expires_in":900,"company_id":"...","role":"..."}

The JWT lives about 15 minutes and there is no refresh token: when it expires, call this
endpoint again with the same secret. The secret itself does not rotate.

## 2. Calling an application's API

    curl -sS <APP_URL>/api/v1/... -H "Authorization: Bearer <JWT>"

Identity's own API (https://hodor.dev.picaz.ru/api/v1) refuses this token on purpose — 403 with code
"client_token". The delegated line is for the applications of the estate, not for hodor.

## 3. Driving an application's UI in a browser

An estate application keeps its browser session in a cookie whose value IS the JWT; there
is no server-side session store. Do not replay the sign-in ceremony — set the cookie and
navigate straight to the page.

    name      __Host-bo_session   over https
              bo_session          over plain http (local dev)
    value     <JWT>
    path      /
    secure    true (the __Host- prefix requires it)
    httpOnly  true
    sameSite  Lax

Playwright:

    const r = await fetch('https://hodor.dev.picaz.ru/api/v1/agent/token', {
      method: 'POST', headers: {'content-type': 'application/json'},
      body: JSON.stringify({ token: process.env.HODOR_AGENT_TOKEN }),
    }).then(r => r.json());
    await context.addCookies([{
      name: '__Host-bo_session', value: r.access_token,
      domain: '<APP_HOST>', path: '/', secure: true, httpOnly: true, sameSite: 'Lax',
    }]);
    await page.goto('https://<APP_HOST>/the/page/you/need');

Name the host, not the url: Playwright refuses a cookie that carries "url" together
with "path" ("Cookie should have either url or path"), and the __Host- prefix needs
the explicit path and secure anyway.

Set the cookie again after ~15 minutes: an expired JWT is not refreshed by the
application, it just sends you to /signin.

## 4. Walking the pages as the person, identity's own included

Planting the cookie works per application. To browse the way the person does —
including identity's own cabinet, which runs on a session and not on this JWT — trade
the secret for a GUEST SESSION and let the normal sign-in flows do the rest:

    await page.request.post('https://hodor.dev.picaz.ru/agents/signin', {
      form: { csrf: '<from the page>', token: process.env.HODOR_AGENT_TOKEN },
    });
    await page.goto('https://hodor.dev.picaz.ru/user');            // identity's cabinet
    await page.goto('<APP_URL>/signin');          // any estate app: the ceremony just works

The guest session lasts at most an hour and never longer than the token itself, and it
is bounded twice over. It refuses everything that changes how the user signs in —
password, second factor, linked users, issuing or rotating agent tokens — and it
carries exactly your token's grant: company management needs "admin" in it, an operator
backoffice needs "platform:admin", and a ceremony started from it hands the application
a token narrowed to the same scopes (and no refresh token). Nothing about looking at the
pages makes your delegation wider than it was.

## What your token can do — read it, do not guess

The JWT payload (the middle dot-separated part, base64url) says what you were given:
"scope" lists the access levels, "company"/"company_role" the tenant, "azp" is
"agent:<name>". Read it once instead of discovering the answer as a 403.

## Boundaries

  - Operator backoffices (the /bo of any application) admit on the scope platform:admin.
    Your token carries it only if the person who issued it holds the platform role and
    chose to delegate it; without that scope those screens are closed and no retry opens
    them. The scope is also re-checked at every exchange — if the person is demoted, the
    next JWT comes back without it.
  - Another company: the token names one company (or none) and cannot switch.
  - Identity's own portal (https://hodor.dev.picaz.ru/user) and its /bo do not read this JWT at all —
    they run on a session cookie. Section 4 is the way in, and what it opens is the
    person's pages, never their credentials.

A page that sends you to /signin means, in order of likelihood: no cookie on that host,
an expired JWT, or a scope this token does not carry. Retrying changes none of the three
— re-exchange once, and if that does not help, ask the person who gave you the token.