@pouchy_ai/world-sdk 0.19.0 → 0.20.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/CHANGELOG.md +21 -0
- package/README.md +20 -4
- package/conformance.mjs +52 -5
- package/dist/index.d.ts +58 -1
- package/dist/index.js +60 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @pouchy_ai/world-sdk
|
|
2
2
|
|
|
3
|
+
## 0.20.0
|
|
4
|
+
|
|
5
|
+
- **Seven methods for routes that already existed.** `getWorldOverview`,
|
|
6
|
+
`getWorldCost`, `listWorldVersions`, `listStoryPackageVersions`,
|
|
7
|
+
`setWorldDisabled`, `setStoryPackageDisabled` (all `adminKey`-capable), and
|
|
8
|
+
`preflightWorld` (owner plane only — no `/admin` mirror).
|
|
9
|
+
- **Two of them the README already promised.** Its credential table listed
|
|
10
|
+
`overview` and `cost` as `adminKey` reads, and there was no method to make
|
|
11
|
+
them with. `getWorldOverview` is the one to reach for first: it carries
|
|
12
|
+
`ready` and the `blockers` behind a false, and unlike `preflightWorld` a
|
|
13
|
+
machine can call it.
|
|
14
|
+
- **`conformance.mjs` no longer fails a correctly-configured project.** Its
|
|
15
|
+
header claimed an admin key was enough; two of its own gates call
|
|
16
|
+
`replayLedgerToEnd` and `createScriptDraft`, whose routes have no `/admin`
|
|
17
|
+
mirror, so on a key alone they threw "needs an owner token". Those gates now
|
|
18
|
+
SKIP with the reason printed, and skips are counted apart from passes rather
|
|
19
|
+
than folded into the ratio.
|
|
20
|
+
- No API change — `WORLD_API_VERSION` stays at 1.24.0. Every route these
|
|
21
|
+
methods call has been served on both planes since Batch 1; only the client
|
|
22
|
+
was missing.
|
|
23
|
+
|
|
3
24
|
## 0.19.0
|
|
4
25
|
|
|
5
26
|
- `getApprovedExport(environmentId, worldInstanceId, draftId, editorialId, exportId)`
|
package/README.md
CHANGED
|
@@ -52,7 +52,10 @@ directions a beat could take, then commit the one the player picked. Off unless
|
|
|
52
52
|
the world's published revision declares it; the `envelope` is server-side only.
|
|
53
53
|
|
|
54
54
|
**Reading** — `getWorldState`, `getProgress`, `getTurn`, `listTurns`,
|
|
55
|
-
`listTurnsSince`, `replayLedger`, `replayLedgerToEnd`, `scanConsistency
|
|
55
|
+
`listTurnsSince`, `replayLedger`, `replayLedgerToEnd`, `scanConsistency`,
|
|
56
|
+
`getWorldOverview`, `getWorldCost`, `listWorldVersions`,
|
|
57
|
+
`listStoryPackageVersions`, `setWorldDisabled`, `setStoryPackageDisabled`,
|
|
58
|
+
`preflightWorld`.
|
|
56
59
|
|
|
57
60
|
**Which credential does what.** This matters more than it looks: one of them
|
|
58
61
|
expires within the hour.
|
|
@@ -64,9 +67,22 @@ expires within the hour.
|
|
|
64
67
|
| `adminToken` (Firebase ID token, ~1h) | everything above plus the content loop | outlive the hour |
|
|
65
68
|
|
|
66
69
|
A server holds `secretKey` + `signing` + `adminKey` and needs **no browser login
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
for the runtime loop**: author a story package and a world, mint sessions, drive
|
|
71
|
+
turns, and read everything back. `adminToken` is for a person, and the content
|
|
72
|
+
loop's review step is a human decision — the point of it rather than an obstacle.
|
|
73
|
+
|
|
74
|
+
**Which calls have no `/admin` mirror, and therefore need `adminToken`:** the
|
|
75
|
+
whole content loop (`*ScriptDraft*`, `*EditorialDraft*`, `createApprovedExport`,
|
|
76
|
+
`deriveStoryPackageCandidate`), the delivery-queue ACTIONS (`drainDeliveries`,
|
|
77
|
+
`requeueDelivery`, `rehydrateDelivery`, `resolveDeliveryGap`), `replayLedger`,
|
|
78
|
+
`archiveLedger`, `evaluateWorld`, and `preflightWorld`. Reading an approved
|
|
79
|
+
export back IS mirrored (`getApprovedExport`) — that is how a machine collects
|
|
80
|
+
what a person approved, with the id arriving on the `world.script_approved`
|
|
81
|
+
webhook.
|
|
82
|
+
|
|
83
|
+
`conformance.mjs` follows the same line: on an admin key alone it runs every
|
|
84
|
+
runtime gate and SKIPS the two whose routes are owner-plane, printing why.
|
|
85
|
+
Setting `POUCHY_ADMIN_TOKEN` as well runs all of them.
|
|
70
86
|
|
|
71
87
|
```ts
|
|
72
88
|
const world = new PouchyWorldClient({
|
package/conformance.mjs
CHANGED
|
@@ -94,12 +94,26 @@ console.log(`Pouchy World conformance — ${scenario.label} scenario\n`);
|
|
|
94
94
|
const results = [];
|
|
95
95
|
let failed = false;
|
|
96
96
|
|
|
97
|
+
/** Marks a gate that CANNOT run on the credentials in hand.
|
|
98
|
+
*
|
|
99
|
+
* Not a pass and not a failure. Two gates below drive the content loop and
|
|
100
|
+
* the ledger replay, and neither route has an `/admin` mirror — they are
|
|
101
|
+
* owner-plane by design ("creating stays human, reading does not"). Reporting
|
|
102
|
+
* them as FAIL would make a correctly-configured project look broken; leaving
|
|
103
|
+
* them out silently would make a partial run look complete. */
|
|
104
|
+
class NeedsOwnerToken extends Error {}
|
|
105
|
+
|
|
97
106
|
async function check(name, fn) {
|
|
98
107
|
try {
|
|
99
108
|
const detail = await fn();
|
|
100
109
|
results.push({ name, ok: true, detail });
|
|
101
110
|
console.log(`PASS ${name}${detail ? ` — ${detail}` : ''}`);
|
|
102
111
|
} catch (err) {
|
|
112
|
+
if (err instanceof NeedsOwnerToken) {
|
|
113
|
+
results.push({ name, ok: null, detail: err.message });
|
|
114
|
+
console.log(`SKIP ${name} — ${err.message}`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
103
117
|
failed = true;
|
|
104
118
|
const detail =
|
|
105
119
|
err instanceof WorldApiError
|
|
@@ -114,14 +128,21 @@ async function check(name, fn) {
|
|
|
114
128
|
|
|
115
129
|
const world = new PouchyWorldClient({
|
|
116
130
|
projectId: env('POUCHY_PROJECT_ID'),
|
|
117
|
-
// Batch 10: an ADMIN KEY
|
|
131
|
+
// Batch 10: an ADMIN KEY carries the RUNTIME loop, which is the point.
|
|
118
132
|
//
|
|
119
133
|
// This harness used to demand POUCHY_ADMIN_TOKEN — a Firebase ID token,
|
|
120
134
|
// browser-minted and good for about an hour. So the one check that proves a
|
|
121
135
|
// project's world plane is wired correctly could not run without a person
|
|
122
136
|
// opening a browser first, which is exactly the thing the rest of this SDK
|
|
123
|
-
// stopped requiring.
|
|
124
|
-
//
|
|
137
|
+
// stopped requiring.
|
|
138
|
+
//
|
|
139
|
+
// CORRECTED: "either credential works" was too strong, and this file was the
|
|
140
|
+
// proof — two of its own gates call `replayLedgerToEnd` and
|
|
141
|
+
// `createScriptDraft`, whose routes have NO `/admin` mirror. On an admin key
|
|
142
|
+
// alone they threw "needs an owner token", so the harness could not pass a
|
|
143
|
+
// correctly-configured project. Those two gates now SKIP with the reason
|
|
144
|
+
// printed; every runtime gate runs on the key. Set POUCHY_ADMIN_TOKEN as
|
|
145
|
+
// well to run all of them.
|
|
125
146
|
...(process.env.POUCHY_ADMIN_KEY
|
|
126
147
|
? { adminKey: process.env.POUCHY_ADMIN_KEY }
|
|
127
148
|
: { adminToken: env('POUCHY_ADMIN_TOKEN') }),
|
|
@@ -133,6 +154,15 @@ const world = new PouchyWorldClient({
|
|
|
133
154
|
},
|
|
134
155
|
...(process.env.POUCHY_BASE_URL ? { baseUrl: process.env.POUCHY_BASE_URL } : {})
|
|
135
156
|
});
|
|
157
|
+
/** Throws a SKIP for a gate whose route is owner-plane only. */
|
|
158
|
+
function requireOwnerToken(route) {
|
|
159
|
+
if (!process.env.POUCHY_ADMIN_TOKEN) {
|
|
160
|
+
throw new NeedsOwnerToken(
|
|
161
|
+
`${route} has no /admin mirror — set POUCHY_ADMIN_TOKEN to run this gate`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
136
166
|
const agentA = env('POUCHY_AGENT_A');
|
|
137
167
|
const agentB = env('POUCHY_AGENT_B');
|
|
138
168
|
const stamp = Date.now().toString(36);
|
|
@@ -237,6 +267,7 @@ await check('world state reads back (user projection, no private layer)', async
|
|
|
237
267
|
});
|
|
238
268
|
|
|
239
269
|
await check('replay verifies the ledger against the served state', async () => {
|
|
270
|
+
requireOwnerToken('/ledger/replay');
|
|
240
271
|
const report = await world.replayLedgerToEnd(created.environmentId, instanceId);
|
|
241
272
|
if (report.verdict !== 'consistent') {
|
|
242
273
|
throw new Error(`${report.verdict}${report.detail ? `: ${report.detail}` : ''}`);
|
|
@@ -246,6 +277,7 @@ await check('replay verifies the ledger against the served state', async () => {
|
|
|
246
277
|
});
|
|
247
278
|
|
|
248
279
|
await check('a script draft generates and refuses export before review', async () => {
|
|
280
|
+
requireOwnerToken('/script-drafts');
|
|
249
281
|
const draft = await world.createScriptDraft(created.environmentId, instanceId);
|
|
250
282
|
if (draft.content?.humanReviewRequired !== true) {
|
|
251
283
|
throw new Error('draft did not carry humanReviewRequired');
|
|
@@ -284,9 +316,24 @@ await check('a WRONG signature is refused (the second proof is real)', async ()
|
|
|
284
316
|
});
|
|
285
317
|
|
|
286
318
|
console.log('');
|
|
287
|
-
|
|
319
|
+
// SKIPPED is counted apart from PASSED, never folded into a ratio. A gate that
|
|
320
|
+
// could not run is not a gate that passed, and "8/10 passed" would read as two
|
|
321
|
+
// failures — the opposite of what happened.
|
|
322
|
+
const passed = results.filter((r) => r.ok === true).length;
|
|
323
|
+
const skipped = results.filter((r) => r.ok === null).length;
|
|
324
|
+
const ran = results.length - skipped;
|
|
325
|
+
console.log(
|
|
326
|
+
`${passed}/${ran} checks passed` + (skipped ? ` · ${skipped} skipped (no owner token)` : '')
|
|
327
|
+
);
|
|
288
328
|
if (failed) {
|
|
289
329
|
console.error('Conformance FAILED — fix the first failure above before writing product code.');
|
|
290
330
|
process.exit(1);
|
|
291
331
|
}
|
|
292
|
-
|
|
332
|
+
if (skipped) {
|
|
333
|
+
console.log(
|
|
334
|
+
`The RUNTIME plane is wired correctly. ${skipped} content-loop gate(s) did not run: ` +
|
|
335
|
+
`their routes are owner-plane only. Set POUCHY_ADMIN_TOKEN to cover them too.`
|
|
336
|
+
);
|
|
337
|
+
} else {
|
|
338
|
+
console.log('Conformance passed. The world plane is wired correctly for this project.');
|
|
339
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* a project is running — which is exactly the field you reach for when a
|
|
5
5
|
* customer's integration behaves like an older SDK than they say they have.
|
|
6
6
|
* It sat at '0.1.0' for eight releases before anything compared the two. */
|
|
7
|
-
export declare const WORLD_SDK_VERSION = "0.
|
|
7
|
+
export declare const WORLD_SDK_VERSION = "0.20.0";
|
|
8
8
|
export declare const DEFAULT_BASE_URL = "https://pouchy.ai/v1";
|
|
9
9
|
/** One direction a beat could take. The WHOLE of what a deliberation shows a
|
|
10
10
|
* player: no reasoning, no role secrets, no simulated effects, no scores. */
|
|
@@ -892,6 +892,63 @@ export declare class PouchyWorldClient {
|
|
|
892
892
|
contentHash: string;
|
|
893
893
|
}>;
|
|
894
894
|
getWorld(environmentId: string): Promise<unknown>;
|
|
895
|
+
/** The world's readiness and operational aggregate — what the dashboard's
|
|
896
|
+
* World Overview binds to.
|
|
897
|
+
*
|
|
898
|
+
* This is the call to make BEFORE driving anything: `ready` plus the
|
|
899
|
+
* `blockers` that explain a false, instance counts, and the delivery
|
|
900
|
+
* queue's shape. It was reachable on both planes from the start and simply
|
|
901
|
+
* had no method here, which meant the README's own credential table
|
|
902
|
+
* promised an `adminKey` read the SDK could not perform. */
|
|
903
|
+
getWorldOverview(environmentId: string): Promise<unknown>;
|
|
904
|
+
/** What this world has cost, by month.
|
|
905
|
+
*
|
|
906
|
+
* `months` is clamped server-side; omit it for the default window. */
|
|
907
|
+
getWorldCost(environmentId: string, months?: number): Promise<unknown>;
|
|
908
|
+
/** Every published revision of this world.
|
|
909
|
+
*
|
|
910
|
+
* An instance is pinned to the revision it was created on, so this is how a
|
|
911
|
+
* backend learns which definition a running story is actually playing. */
|
|
912
|
+
listWorldVersions(environmentId: string): Promise<{
|
|
913
|
+
versions: unknown[];
|
|
914
|
+
}>;
|
|
915
|
+
/** Every published revision of a story package.
|
|
916
|
+
*
|
|
917
|
+
* The response carries `hasUnsupported` / `unsupportedCount` as well as the
|
|
918
|
+
* versions: a history this build cannot read whole is reported as PARTIAL
|
|
919
|
+
* rather than passing its newest readable revision off as the newest one.
|
|
920
|
+
* Check that flag before treating the last entry as current. */
|
|
921
|
+
listStoryPackageVersions(packageId: string): Promise<{
|
|
922
|
+
versions: unknown[];
|
|
923
|
+
unsupported?: unknown;
|
|
924
|
+
unsupportedCount?: number;
|
|
925
|
+
hasUnsupported?: boolean;
|
|
926
|
+
}>;
|
|
927
|
+
/** Stop (or restart) a world.
|
|
928
|
+
*
|
|
929
|
+
* A disabled world refuses new sessions and new turns. Existing instances
|
|
930
|
+
* are not deleted and their ledgers are untouched — this is a door being
|
|
931
|
+
* shut, not history being erased, and passing `false` opens it again. */
|
|
932
|
+
setWorldDisabled(environmentId: string, disabled: boolean): Promise<{
|
|
933
|
+
environmentId: string;
|
|
934
|
+
disabled: boolean;
|
|
935
|
+
}>;
|
|
936
|
+
/** Stop (or restart) a story package.
|
|
937
|
+
*
|
|
938
|
+
* Worlds already PINNED to one of its revisions keep running: an instance
|
|
939
|
+
* holds its own revision for life. This stops new pins. */
|
|
940
|
+
setStoryPackageDisabled(packageId: string, disabled: boolean): Promise<{
|
|
941
|
+
packageId: string;
|
|
942
|
+
disabled: boolean;
|
|
943
|
+
}>;
|
|
944
|
+
/** The pre-launch checklist for a world: every reason it would refuse to
|
|
945
|
+
* run, gathered before you try.
|
|
946
|
+
*
|
|
947
|
+
* OWNER TOKEN ONLY — there is no `/admin` mirror for this route, so an
|
|
948
|
+
* unattended backend cannot call it. That is not an oversight to work
|
|
949
|
+
* around: `getWorldOverview` carries `ready` and `blockers` and IS
|
|
950
|
+
* mirrored, so a machine has the readiness answer it needs. */
|
|
951
|
+
preflightWorld(environmentId: string): Promise<unknown>;
|
|
895
952
|
/** Publish the next world revision. Existing world INSTANCES keep the
|
|
896
953
|
* revision they were created on — a published change reaches new instances
|
|
897
954
|
* only, which is what keeps a running story from changing runtime or rules
|
package/dist/index.js
CHANGED
|
@@ -23,7 +23,7 @@ import { createHash, createHmac, randomUUID } from 'node:crypto';
|
|
|
23
23
|
* a project is running — which is exactly the field you reach for when a
|
|
24
24
|
* customer's integration behaves like an older SDK than they say they have.
|
|
25
25
|
* It sat at '0.1.0' for eight releases before anything compared the two. */
|
|
26
|
-
export const WORLD_SDK_VERSION = '0.
|
|
26
|
+
export const WORLD_SDK_VERSION = '0.20.0';
|
|
27
27
|
export const DEFAULT_BASE_URL = 'https://pouchy.ai/v1';
|
|
28
28
|
/** Read the commit turn id out of an envelope, so the signature covers the id
|
|
29
29
|
* the server will commit under. The payload half is base64url JSON; the
|
|
@@ -287,6 +287,65 @@ export class PouchyWorldClient {
|
|
|
287
287
|
getWorld(environmentId) {
|
|
288
288
|
return this.machine('GET', `/projects/${this.projectId}/environments/${environmentId}`, `/admin/environments/${environmentId}`);
|
|
289
289
|
}
|
|
290
|
+
/** The world's readiness and operational aggregate — what the dashboard's
|
|
291
|
+
* World Overview binds to.
|
|
292
|
+
*
|
|
293
|
+
* This is the call to make BEFORE driving anything: `ready` plus the
|
|
294
|
+
* `blockers` that explain a false, instance counts, and the delivery
|
|
295
|
+
* queue's shape. It was reachable on both planes from the start and simply
|
|
296
|
+
* had no method here, which meant the README's own credential table
|
|
297
|
+
* promised an `adminKey` read the SDK could not perform. */
|
|
298
|
+
getWorldOverview(environmentId) {
|
|
299
|
+
return this.machine('GET', `/projects/${this.projectId}/environments/${environmentId}/overview`, `/admin/environments/${environmentId}/overview`);
|
|
300
|
+
}
|
|
301
|
+
/** What this world has cost, by month.
|
|
302
|
+
*
|
|
303
|
+
* `months` is clamped server-side; omit it for the default window. */
|
|
304
|
+
getWorldCost(environmentId, months) {
|
|
305
|
+
const query = months === undefined ? '' : `?months=${encodeURIComponent(String(months))}`;
|
|
306
|
+
return this.machine('GET', `/projects/${this.projectId}/environments/${environmentId}/cost`, `/admin/environments/${environmentId}/cost`, undefined, query);
|
|
307
|
+
}
|
|
308
|
+
/** Every published revision of this world.
|
|
309
|
+
*
|
|
310
|
+
* An instance is pinned to the revision it was created on, so this is how a
|
|
311
|
+
* backend learns which definition a running story is actually playing. */
|
|
312
|
+
listWorldVersions(environmentId) {
|
|
313
|
+
return this.machine('GET', `/projects/${this.projectId}/environments/${environmentId}/versions`, `/admin/environments/${environmentId}/versions`);
|
|
314
|
+
}
|
|
315
|
+
/** Every published revision of a story package.
|
|
316
|
+
*
|
|
317
|
+
* The response carries `hasUnsupported` / `unsupportedCount` as well as the
|
|
318
|
+
* versions: a history this build cannot read whole is reported as PARTIAL
|
|
319
|
+
* rather than passing its newest readable revision off as the newest one.
|
|
320
|
+
* Check that flag before treating the last entry as current. */
|
|
321
|
+
listStoryPackageVersions(packageId) {
|
|
322
|
+
return this.machine('GET', `/projects/${this.projectId}/story-packages/${packageId}/versions`, `/admin/story-packages/${packageId}/versions`);
|
|
323
|
+
}
|
|
324
|
+
/** Stop (or restart) a world.
|
|
325
|
+
*
|
|
326
|
+
* A disabled world refuses new sessions and new turns. Existing instances
|
|
327
|
+
* are not deleted and their ledgers are untouched — this is a door being
|
|
328
|
+
* shut, not history being erased, and passing `false` opens it again. */
|
|
329
|
+
setWorldDisabled(environmentId, disabled) {
|
|
330
|
+
return this.machine('POST', `/projects/${this.projectId}/environments/${environmentId}/disable`, `/admin/environments/${environmentId}/disable`, { disabled });
|
|
331
|
+
}
|
|
332
|
+
/** Stop (or restart) a story package.
|
|
333
|
+
*
|
|
334
|
+
* Worlds already PINNED to one of its revisions keep running: an instance
|
|
335
|
+
* holds its own revision for life. This stops new pins. */
|
|
336
|
+
setStoryPackageDisabled(packageId, disabled) {
|
|
337
|
+
return this.machine('POST', `/projects/${this.projectId}/story-packages/${packageId}/disable`, `/admin/story-packages/${packageId}/disable`, { disabled });
|
|
338
|
+
}
|
|
339
|
+
/** The pre-launch checklist for a world: every reason it would refuse to
|
|
340
|
+
* run, gathered before you try.
|
|
341
|
+
*
|
|
342
|
+
* OWNER TOKEN ONLY — there is no `/admin` mirror for this route, so an
|
|
343
|
+
* unattended backend cannot call it. That is not an oversight to work
|
|
344
|
+
* around: `getWorldOverview` carries `ready` and `blockers` and IS
|
|
345
|
+
* mirrored, so a machine has the readiness answer it needs. */
|
|
346
|
+
preflightWorld(environmentId) {
|
|
347
|
+
return this.owner('GET', `/projects/${this.projectId}/environments/${environmentId}/preflight`);
|
|
348
|
+
}
|
|
290
349
|
/** Publish the next world revision. Existing world INSTANCES keep the
|
|
291
350
|
* revision they were created on — a published change reaches new instances
|
|
292
351
|
* only, which is what keeps a running story from changing runtime or rules
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pouchy_ai/world-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Server-side TypeScript client for Pouchy World \u2014 story packages, world definitions, world sessions, coordinated turns, trusted events, replay verification and script drafts. Node only: it holds a project Secret Key and a source signing key, which never belong in a browser or a mobile app.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|