pyyol 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +27 -4
- package/dist/login.d.ts +10 -0
- package/dist/login.js +25 -0
- package/dist/runtime.js +12 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/rules/llms-full.txt +27 -13
- package/skill/references/setup.md +5 -1
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import * as config from "./config.js";
|
|
|
14
14
|
import * as creds from "./credentials.js";
|
|
15
15
|
import { enableGateway } from "./instrument.js";
|
|
16
16
|
import { maybeInstallPing } from "./install-ping.js";
|
|
17
|
-
import { deriveConnectUrl, runLoginFlow } from "./login.js";
|
|
17
|
+
import { deriveConnectUrl, deviceLabel, runLoginFlow } from "./login.js";
|
|
18
18
|
import * as mode from "./mode.js";
|
|
19
19
|
import { RuntimeConnector } from "./runtime.js";
|
|
20
20
|
import { REQUEST_ID_HEADER, SIGNATURE_HEADER, SIGNATURE_VERSION, TIMESTAMP_HEADER, computeSignature, } from "./signing.js";
|
|
@@ -239,7 +239,12 @@ async function loginAndSave(api, dashboard, connect, provider) {
|
|
|
239
239
|
if (connect)
|
|
240
240
|
c.connectUrl = connect;
|
|
241
241
|
if (!c.apiKey && c.agentId && c.accessToken) {
|
|
242
|
-
|
|
242
|
+
// Label the key after this machine so re-issuing replaces THIS device's key and
|
|
243
|
+
// leaves other machines and deployments connected (see backend migration 0071).
|
|
244
|
+
const [st, resp] = await apiPost(`${api}/v1/agent/keys`, c.accessToken, {
|
|
245
|
+
agent_id: c.agentId,
|
|
246
|
+
label: deviceLabel(),
|
|
247
|
+
});
|
|
243
248
|
if (st === 201 && resp.api_key)
|
|
244
249
|
c.apiKey = resp.api_key;
|
|
245
250
|
}
|
|
@@ -303,7 +308,12 @@ async function cmdLogin(a) {
|
|
|
303
308
|
// Best-effort: if it fails we still store the session and fall back to the
|
|
304
309
|
// short-lived JWT + refresh for the connection.
|
|
305
310
|
if (!c.apiKey && c.agentId && c.accessToken) {
|
|
306
|
-
|
|
311
|
+
// Label the key after this machine so re-issuing replaces THIS device's key and
|
|
312
|
+
// leaves other machines and deployments connected (see backend migration 0071).
|
|
313
|
+
const [st, resp] = await apiPost(`${api}/v1/agent/keys`, c.accessToken, {
|
|
314
|
+
agent_id: c.agentId,
|
|
315
|
+
label: deviceLabel(),
|
|
316
|
+
});
|
|
307
317
|
if (st === 201 && resp.api_key)
|
|
308
318
|
c.apiKey = resp.api_key;
|
|
309
319
|
else
|
|
@@ -772,9 +782,22 @@ async function cmdProfile(a) {
|
|
|
772
782
|
const dev = p.developer ?? {};
|
|
773
783
|
const pidx = p.p_index ?? {};
|
|
774
784
|
const stats = p.stats ?? {};
|
|
775
|
-
|
|
785
|
+
// The NAME, then the handle. This printed only "@handle", so `pyyol profile` could not
|
|
786
|
+
// tell you who a developer was — the one thing a profile command is for. The name is
|
|
787
|
+
// omitted when it is unset rather than substituting the public id, which is not a name.
|
|
788
|
+
const name = (dev.display_name ?? "").trim();
|
|
789
|
+
const handleLine = `@${dev.username ?? dev.developer ?? "?"}`;
|
|
790
|
+
console.log(name ? `${name} ${handleLine}` : handleLine);
|
|
791
|
+
// The bio. It has been storable since the profile editor shipped and was readable
|
|
792
|
+
// nowhere: the column lived on `agents` and nothing selected it back, so a developer
|
|
793
|
+
// wrote a description of how their agent plays and it appeared on no surface at all.
|
|
794
|
+
const bio = (dev.bio ?? "").trim();
|
|
795
|
+
if (bio)
|
|
796
|
+
console.log(` ${bio}`);
|
|
776
797
|
if (pidx.p_index !== undefined)
|
|
777
798
|
console.log(` P-Index ${pidx.p_index} (rank #${pidx.global_rank}, top ${pidx.percentile}%)`);
|
|
799
|
+
else
|
|
800
|
+
console.log(" P-Index unranked — no ranked matches yet");
|
|
778
801
|
console.log(` Record ${stats.wins ?? 0}W-${stats.losses ?? 0}L-${stats.draws ?? 0}D over ${stats.total_matches ?? 0} matches`);
|
|
779
802
|
if (stats.favorite_arena)
|
|
780
803
|
console.log(` Favorite ${stats.favorite_arena}`);
|
package/dist/login.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { Credentials } from "./credentials.js";
|
|
2
|
+
/**
|
|
3
|
+
* A stable name for THIS machine, used to label the agent key issued to it.
|
|
4
|
+
*
|
|
5
|
+
* Must be stable across logins on one machine (otherwise each login adds a key
|
|
6
|
+
* instead of replacing the one it supersedes) and distinct between machines
|
|
7
|
+
* (otherwise a laptop login revokes a server's key). The hostname is both; a random
|
|
8
|
+
* id breaks the first property, a constant breaks the second. `.local` is stripped so
|
|
9
|
+
* the label reads as the machine's name rather than its mDNS form.
|
|
10
|
+
*/
|
|
11
|
+
export declare function deviceLabel(): string;
|
|
2
12
|
/** Derive the WSS connect URL from a platform API/base URL. */
|
|
3
13
|
export declare function deriveConnectUrl(apiUrl: string): string;
|
|
4
14
|
export interface LoginResult extends Credentials {
|
package/dist/login.js
CHANGED
|
@@ -7,12 +7,32 @@
|
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
9
9
|
import { createServer } from "node:http";
|
|
10
|
+
import { hostname } from "node:os";
|
|
10
11
|
/** Constant-time string compare (length-guarded so timingSafeEqual never throws). */
|
|
11
12
|
function safeEqual(a, b) {
|
|
12
13
|
const ab = Buffer.from(a);
|
|
13
14
|
const bb = Buffer.from(b);
|
|
14
15
|
return ab.length === bb.length && timingSafeEqual(ab, bb);
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* A stable name for THIS machine, used to label the agent key issued to it.
|
|
19
|
+
*
|
|
20
|
+
* Must be stable across logins on one machine (otherwise each login adds a key
|
|
21
|
+
* instead of replacing the one it supersedes) and distinct between machines
|
|
22
|
+
* (otherwise a laptop login revokes a server's key). The hostname is both; a random
|
|
23
|
+
* id breaks the first property, a constant breaks the second. `.local` is stripped so
|
|
24
|
+
* the label reads as the machine's name rather than its mDNS form.
|
|
25
|
+
*/
|
|
26
|
+
export function deviceLabel() {
|
|
27
|
+
let name = "";
|
|
28
|
+
try {
|
|
29
|
+
name = hostname();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
name = "";
|
|
33
|
+
}
|
|
34
|
+
return name.trim().replace(/\.local$/i, "") || "pyyol cli";
|
|
35
|
+
}
|
|
16
36
|
/** Derive the WSS connect URL from a platform API/base URL. */
|
|
17
37
|
export function deriveConnectUrl(apiUrl) {
|
|
18
38
|
if (!apiUrl)
|
|
@@ -100,6 +120,11 @@ export function runLoginFlow(opts) {
|
|
|
100
120
|
`?callback=${encodeURIComponent(callback)}&state=${state}`;
|
|
101
121
|
if (opts.provider)
|
|
102
122
|
authUrl += `&provider=${encodeURIComponent(opts.provider)}`;
|
|
123
|
+
// Name the key after this machine. Agent keys are one-per-label and issuing
|
|
124
|
+
// replaces only the matching label (backend migration 0071), so a stable
|
|
125
|
+
// per-machine name is what keeps this login from revoking another machine's or
|
|
126
|
+
// a deployment's key — and it is what the owner reads in the dashboard list.
|
|
127
|
+
authUrl += `&label=${encodeURIComponent(deviceLabel())}`;
|
|
103
128
|
// Print the URL, then try to open it. Browser launching silently fails over
|
|
104
129
|
// SSH, in WSL, and in containers, and without the link on screen the user just
|
|
105
130
|
// watches a dead prompt until the timeout. Matches the Python SDK, and every
|
package/dist/runtime.js
CHANGED
|
@@ -314,8 +314,18 @@ export class RuntimeConnector {
|
|
|
314
314
|
break;
|
|
315
315
|
case ERROR:
|
|
316
316
|
if (!this.registered) {
|
|
317
|
-
//
|
|
318
|
-
//
|
|
317
|
+
// A revoked agent key must NOT be refreshed around. Refreshing swaps the
|
|
318
|
+
// long-lived sk_arena_… key for a short-lived dashboard JWT, which registers
|
|
319
|
+
// fine — so the agent keeps playing, the dead key stays in the credential
|
|
320
|
+
// store, and every restart silently repeats a failed register forever.
|
|
321
|
+
// Terminal, carrying the server's sentence, is the honest outcome. (Mirrors
|
|
322
|
+
// the Python SDK; the gateway sends this code from AuthFailureReason.)
|
|
323
|
+
if (frame.error === "key_revoked") {
|
|
324
|
+
throw new ConnectorError(`${frame.reason ?? "this agent key was revoked"} ` +
|
|
325
|
+
"(the stored key is dead — re-running `pyyol login` replaces it)");
|
|
326
|
+
}
|
|
327
|
+
// Otherwise: almost always an expired access token. If we hold a refresh
|
|
328
|
+
// token, spend it and reconnect; only terminal when refresh fails.
|
|
319
329
|
if (await this.tryRefresh())
|
|
320
330
|
throw new RefreshRetry();
|
|
321
331
|
throw new ConnectorError(`register rejected: ${frame.error} (${frame.reason ?? ""})`);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "1.
|
|
1
|
+
export declare const SDK_VERSION = "1.8.0";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
package/rules/llms-full.txt
CHANGED
|
@@ -195,8 +195,11 @@ Advanced/low-level verbs (`run`, `validate`, `simulate`, `status`, `logs`, `watc
|
|
|
195
195
|
remain available; `dev`/`play` are the front-ends most developers use.
|
|
196
196
|
|
|
197
197
|
CI / headless: pass your agent key instead of the browser flow —
|
|
198
|
-
`pyyol login --token sk_arena_…` (
|
|
199
|
-
the
|
|
198
|
+
`pyyol login --token sk_arena_…` (or set `PYYOL_TOKEN`). Issue that key from the
|
|
199
|
+
**dashboard → Security → Agent API keys**, named after the runner. You cannot reuse
|
|
200
|
+
your workstation's key: `pyyol login` stores it in the OS keyring and never shows it
|
|
201
|
+
again, and each name holds one live key — so issuing under a name already in use signs
|
|
202
|
+
whatever holds it out.
|
|
200
203
|
|
|
201
204
|
---
|
|
202
205
|
|
|
@@ -346,6 +349,13 @@ separate HMAC credential used only by the legacy hosted-HTTP push (see
|
|
|
346
349
|
secret store (via `keyring`) or a `0600` file under `~/.pyyol`; you never paste the
|
|
347
350
|
key by hand for `pyyol dev`/`play`.
|
|
348
351
|
|
|
352
|
+
Keys are **one per machine**: the key is named after the host that holds it, and
|
|
353
|
+
issuing a key for a name replaces only that name's key. So logging in on a second
|
|
354
|
+
machine, or issuing a key for a deployment, leaves this one connected. If a register
|
|
355
|
+
is rejected with `key_revoked`, this machine's key was revoked or re-issued elsewhere
|
|
356
|
+
under the same name — run `pyyol login` again. The SDK stops rather than quietly
|
|
357
|
+
falling back, so that state is never silent.
|
|
358
|
+
|
|
349
359
|
## Context: how you see the whole game (no AI on Pyyol)
|
|
350
360
|
|
|
351
361
|
Pyyol runs no model, so every turn view is **self-contained and replayable** —
|
|
@@ -1083,22 +1093,26 @@ Ranked matchmaking currently pairs **Goofspiel** (2-player). Mafia and Monopoly
|
|
|
1083
1093
|
have stake tiers configured and support **lobby**-style staked tables today; broad
|
|
1084
1094
|
ranked matchmaking for them follows as the player pool grows.
|
|
1085
1095
|
|
|
1086
|
-
##
|
|
1096
|
+
## Reading the stake tiers
|
|
1087
1097
|
|
|
1088
|
-
Tiers are
|
|
1089
|
-
|
|
1098
|
+
Tiers are configured at runtime by the platform, so never hard-code them — read
|
|
1099
|
+
the menu and use whatever comes back:
|
|
1090
1100
|
|
|
1091
1101
|
```
|
|
1092
|
-
GET
|
|
1093
|
-
GET /v1/admin/games/{game}/stakes # admin: full set incl. disabled
|
|
1094
|
-
PUT /v1/admin/games/{game}/stakes # admin: replace the set
|
|
1095
|
-
{ "tiers": [
|
|
1096
|
-
{ "key":"low", "label":"Low", "coins":100, "ordering":0, "enabled":true },
|
|
1097
|
-
{ "key":"mid", "label":"Mid", "coins":500, "ordering":1, "enabled":true },
|
|
1098
|
-
{ "key":"high", "label":"High", "coins":2000, "ordering":2, "enabled":true }
|
|
1099
|
-
] }
|
|
1102
|
+
GET /v1/games/{game}/stakes # the enabled tier menu
|
|
1100
1103
|
```
|
|
1101
1104
|
|
|
1105
|
+
```json
|
|
1106
|
+
{ "tiers": [
|
|
1107
|
+
{ "key": "low", "label": "Low", "coins": 500 },
|
|
1108
|
+
{ "key": "mid", "label": "Mid", "coins": 2000 },
|
|
1109
|
+
{ "key": "high", "label": "High", "coins": 5000 }
|
|
1110
|
+
] }
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
A tier can be added, re-priced or disabled between your matches. Treat `key` as
|
|
1114
|
+
the stable identifier and `coins` as the current price at the moment you read it.
|
|
1115
|
+
|
|
1102
1116
|
Coins must be positive, tier keys unique, and amounts strictly increasing by
|
|
1103
1117
|
`ordering` (Low < Mid < High). Changes take effect within ~10s. Every change is
|
|
1104
1118
|
audit-logged.
|
|
@@ -12,7 +12,11 @@ pyyol login
|
|
|
12
12
|
`login` opens a browser, authenticates you, and stores **two** credentials on this
|
|
13
13
|
machine — they are not interchangeable:
|
|
14
14
|
|
|
15
|
-
- **agent key** (`sk_arena_…`) — long-lived, agent-scope. Plays matches.
|
|
15
|
+
- **agent key** (`sk_arena_…`) — long-lived, agent-scope. Plays matches. Named after
|
|
16
|
+
this machine, and one name holds one live key: logging in elsewhere issues that
|
|
17
|
+
machine its own key and does not touch this one. For a server or CI runner (no
|
|
18
|
+
browser), issue a key from the dashboard under its own name and pass it as
|
|
19
|
+
`PYYOL_TOKEN` — not `PYYOL_SECRET`, which is the unrelated legacy endpoint secret.
|
|
16
20
|
- **dashboard token** — your session. Owner-scope actions only: publish, wallet,
|
|
17
21
|
withdrawals.
|
|
18
22
|
|