@volter/twin-xai 0.1.1 → 0.1.3

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.
@@ -10,7 +10,7 @@
10
10
  // Scenario scripting (xai-scenario.ts): a JSON scenario file — via the scenarioPath option,
11
11
  // the TWIN_XAI_SCENARIO env var, or `world-xai serve --scenario <path>` — scripts the exact
12
12
  // assistant turns for POST /v1/chat/completions. One session per server (nthCall/once state).
13
- import { serveHttp } from '@volter/world-core';
13
+ import { isReadOnlyRequest, serveHttp, withRequestScopes } from '@volter/world-core';
14
14
  import { twinManifest, twinPublicBase, worldNow } from '@volter/world-core';
15
15
  import { createXaiScenarioEngine, loadXaiScenarioDocument } from "./xai-scenario.js";
16
16
  import { handleXaiTwinRequest } from "./xai-twin.js";
@@ -68,10 +68,14 @@ function html(body, status = 200) {
68
68
  * (xai-device-auth-ui.ts), so the OAuth protocol UI is workerd-servable too.
69
69
  */
70
70
  export function createXaiTwinFetch(options = {}) {
71
- const readOnly = options.readOnly ?? false;
72
71
  const scenarioPath = options.scenarioPath ?? process.env.TWIN_XAI_SCENARIO;
73
72
  const scenarioEngine = scenarioPath ? createXaiScenarioEngine(loadXaiScenarioDocument(scenarioPath)) : undefined;
74
- return async function xaiTwinFetch(request) {
73
+ // a read-only request (x-volter-read-only, a World's read token) is a request to a read-only twin: no
74
+ // device grant is started or decided, no token minted, no completion run or usage recorded, and a
75
+ // deferred completion is only PEEKED (xai-twin.ts retrieveDeferredCompletion: the app's one retrieval
76
+ // is never spent by a viewer); the kernel refuses any write that slips past as xAI's own error shape
77
+ return withRequestScopes(async function xaiTwinFetch(request) {
78
+ const readOnly = (options.readOnly ?? false) || isReadOnlyRequest(request);
75
79
  const url = new URL(request.url);
76
80
  const path = url.pathname + (url.search || '');
77
81
  const cleanPath = url.pathname.replace(/\/+$/, '');
@@ -220,7 +224,7 @@ export function createXaiTwinFetch(options = {}) {
220
224
  ...(scenarioEngine ? { scenarioEngine } : {}),
221
225
  });
222
226
  return new Response(JSON.stringify(out), { status, headers: { 'content-type': 'application/json', 'x-request-id': 'req_twin', ...(outHeaders ?? {}) } });
223
- };
227
+ }, { refuse: () => new Response(JSON.stringify({ code: 'method_not_allowed', error: 'this request is read-only' }), { status: 405, headers: { 'content-type': 'application/json' } }) });
224
228
  }
225
229
  export async function createXaiTwinServer(options) {
226
230
  const server = await serveHttp({
@@ -543,6 +543,11 @@ async function retrieveDeferredCompletion(requestId, req) {
543
543
  // consume is a WRITE, so it is skipped (the lifecycle simply cannot advance in a read-only
544
544
  // mirror, and repeated read-only polls keep serving the still-unconsumed result). §9 found
545
545
  // this consume unguarded — a read-only GET was consuming the row.
546
+ // A World's READ token (x-volter-read-only) arrives here as readOnly too, and PEEKS rather than
547
+ // being refused: xAI's own shapes already say everything a viewer may see without spending
548
+ // anything — 202 with an empty body while pending, the completion once ready — so a peek answers
549
+ // exactly the vendor's shape for the row's current state and advances nothing (neither the first
550
+ // poll nor the one-time retrieval). The app's own poll then still gets its one retrieval.
546
551
  if (!req.readOnly) {
547
552
  await applyTwinWrite(SERVICE, {
548
553
  operation: 'deferred_completion.consume',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/twin-xai",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Local xAI (Grok) twin — a faithful, stateful local API plus the Grok Build device-OAuth browser surface. The model is stubbed (deterministic) and Live Search citations are labeled stubs, while the OAuth/account/usage world and API protocol envelopes remain coherent and vendor-shaped. Built on @volter/world-core.",
5
5
  "keywords": [
6
6
  "twin",
@@ -56,10 +56,10 @@
56
56
  "react-dom": "^19.2.7"
57
57
  },
58
58
  "peerDependencies": {
59
- "@volter/world-core": "2.0.1"
59
+ "@volter/world-core": "2.0.3"
60
60
  },
61
61
  "devDependencies": {
62
- "@volter/world-core": "2.0.1",
62
+ "@volter/world-core": "2.0.3",
63
63
  "@volter/world-tooling": "0.1.0",
64
64
  "@ai-sdk/xai": "^3.0.111",
65
65
  "ai": "^6.0.0",
package/src/xai-server.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  // Scenario scripting (xai-scenario.ts): a JSON scenario file — via the scenarioPath option,
11
11
  // the TWIN_XAI_SCENARIO env var, or `world-xai serve --scenario <path>` — scripts the exact
12
12
  // assistant turns for POST /v1/chat/completions. One session per server (nthCall/once state).
13
- import { serveHttp } from '@volter/world-core';
13
+ import { isReadOnlyRequest, serveHttp, withRequestScopes } from '@volter/world-core';
14
14
  import { twinManifest, twinPublicBase, worldNow } from '@volter/world-core';
15
15
  import { createXaiScenarioEngine, loadXaiScenarioDocument, type XaiScenarioEngine } from './xai-scenario.ts';
16
16
  import { handleXaiTwinRequest } from './xai-twin.ts';
@@ -89,10 +89,14 @@ export interface XaiTwinFetchOptions {
89
89
  * (xai-device-auth-ui.ts), so the OAuth protocol UI is workerd-servable too.
90
90
  */
91
91
  export function createXaiTwinFetch(options: XaiTwinFetchOptions = {}): (request: Request) => Promise<Response> {
92
- const readOnly = options.readOnly ?? false;
93
92
  const scenarioPath = options.scenarioPath ?? process.env.TWIN_XAI_SCENARIO;
94
93
  const scenarioEngine: XaiScenarioEngine | undefined = scenarioPath ? createXaiScenarioEngine(loadXaiScenarioDocument(scenarioPath)) : undefined;
95
- return async function xaiTwinFetch(request: Request): Promise<Response> {
94
+ // a read-only request (x-volter-read-only, a World's read token) is a request to a read-only twin: no
95
+ // device grant is started or decided, no token minted, no completion run or usage recorded, and a
96
+ // deferred completion is only PEEKED (xai-twin.ts retrieveDeferredCompletion: the app's one retrieval
97
+ // is never spent by a viewer); the kernel refuses any write that slips past as xAI's own error shape
98
+ return withRequestScopes(async function xaiTwinFetch(request: Request): Promise<Response> {
99
+ const readOnly = (options.readOnly ?? false) || isReadOnlyRequest(request);
96
100
  const url = new URL(request.url);
97
101
  const path = url.pathname + (url.search || '');
98
102
  const cleanPath = url.pathname.replace(/\/+$/, '');
@@ -245,7 +249,7 @@ export function createXaiTwinFetch(options: XaiTwinFetchOptions = {}): (request:
245
249
  ...(scenarioEngine ? { scenarioEngine } : {}),
246
250
  });
247
251
  return new Response(JSON.stringify(out), { status, headers: { 'content-type': 'application/json', 'x-request-id': 'req_twin', ...(outHeaders ?? {}) } });
248
- };
252
+ }, { refuse: () => new Response(JSON.stringify({ code: 'method_not_allowed', error: 'this request is read-only' }), { status: 405, headers: { 'content-type': 'application/json' } }) });
249
253
  }
250
254
 
251
255
  export async function createXaiTwinServer(options: { root?: string; port?: number; readOnly?: boolean; scenarioPath?: string }): Promise<{ port: number; stop: () => void }> {
package/src/xai-twin.ts CHANGED
@@ -629,6 +629,11 @@ async function retrieveDeferredCompletion(requestId: string, req: XaiRequest): P
629
629
  // consume is a WRITE, so it is skipped (the lifecycle simply cannot advance in a read-only
630
630
  // mirror, and repeated read-only polls keep serving the still-unconsumed result). §9 found
631
631
  // this consume unguarded — a read-only GET was consuming the row.
632
+ // A World's READ token (x-volter-read-only) arrives here as readOnly too, and PEEKS rather than
633
+ // being refused: xAI's own shapes already say everything a viewer may see without spending
634
+ // anything — 202 with an empty body while pending, the completion once ready — so a peek answers
635
+ // exactly the vendor's shape for the row's current state and advances nothing (neither the first
636
+ // poll nor the one-time retrieval). The app's own poll then still gets its one retrieval.
632
637
  if (!req.readOnly) {
633
638
  await applyTwinWrite(SERVICE, {
634
639
  operation: 'deferred_completion.consume',