@datalayer/agent-runtimes 1.3.105 → 1.3.108
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-embed/datalayer-app-main.js +85 -5
- package/lib/apps/apps/appspec.js +17 -0
- package/lib/apps/apps/checks.js +4 -0
- package/lib/apps/apps/computer.d.ts +55 -2
- package/lib/apps/apps/computer.js +82 -4
- package/lib/apps/plugins/app-computer/AppComputer.d.ts +7 -3
- package/lib/apps/plugins/app-computer/AppComputer.js +91 -4
- package/lib/apps/plugins/pap-example/index.d.ts +23 -0
- package/lib/apps/plugins/pap-example/index.js +25 -0
- package/lib/components/inspector/a2aSpans.js +8 -1
- package/lib/components/teams/SceneTranscript.js +8 -3
- package/lib/examples/LoopAppComputerExample.d.ts +4 -3
- package/lib/examples/LoopAppComputerExample.js +11 -6
- package/lib/examples/PapAgentIdentityExample.d.ts +4 -0
- package/lib/examples/PapAgentIdentityExample.js +23 -0
- package/lib/examples/PapAuthorizationBoundaryExample.d.ts +4 -0
- package/lib/examples/PapAuthorizationBoundaryExample.js +23 -0
- package/lib/examples/PapCompanyDiscoveryExample.d.ts +4 -0
- package/lib/examples/PapCompanyDiscoveryExample.js +23 -0
- package/lib/examples/PapDpopProofExample.d.ts +4 -0
- package/lib/examples/PapDpopProofExample.js +23 -0
- package/lib/examples/PapGuidedJourneyExample.d.ts +4 -0
- package/lib/examples/PapGuidedJourneyExample.js +23 -0
- package/lib/examples/PapSessionLifecycleExample.d.ts +4 -0
- package/lib/examples/PapSessionLifecycleExample.js +23 -0
- package/lib/examples/example-selector.js +9 -0
- package/lib/examples/exampleGroups.d.ts +1 -1
- package/lib/examples/exampleGroups.js +3 -0
- package/lib/examples/index.d.ts +6 -0
- package/lib/examples/index.js +6 -0
- package/lib/examples/main.js +9 -0
- package/lib/examples/pap/PapAgentIdentityView.d.ts +4 -0
- package/lib/examples/pap/PapAgentIdentityView.js +55 -0
- package/lib/examples/pap/PapAuthorizationBoundaryView.d.ts +4 -0
- package/lib/examples/pap/PapAuthorizationBoundaryView.js +59 -0
- package/lib/examples/pap/PapCompanyDiscoveryView.d.ts +4 -0
- package/lib/examples/pap/PapCompanyDiscoveryView.js +62 -0
- package/lib/examples/pap/PapDpopProofView.d.ts +4 -0
- package/lib/examples/pap/PapDpopProofView.js +57 -0
- package/lib/examples/pap/PapExamplePage.d.ts +14 -0
- package/lib/examples/pap/PapExamplePage.js +6 -0
- package/lib/examples/pap/PapGuidedJourneyView.d.ts +4 -0
- package/lib/examples/pap/PapGuidedJourneyView.js +448 -0
- package/lib/examples/pap/PapSessionLifecycleView.d.ts +4 -0
- package/lib/examples/pap/PapSessionLifecycleView.js +103 -0
- package/lib/specs/actions.js +22 -0
- package/lib/specs/apps.js +76 -15
- package/lib/specs/appspecSchema.js +27 -0
- package/lib/types/agentspecs.d.ts +14 -0
- package/package.json +2 -2
- package/scripts/codegen/generate_apps.py +9 -0
- package/scripts/validate_pap_examples.py +62 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
import { useEffect, useMemo, useRef, useState, } from 'react';
|
|
7
|
+
import { Box } from '@datalayer/primer-addons';
|
|
8
|
+
import { contribution, definePlugin } from '@datalayer/reactor';
|
|
9
|
+
import { Button, Heading, IconButton, Label, Text } from '@primer/react';
|
|
10
|
+
import { ChevronLeftIcon, ChevronRightIcon } from '@primer/octicons-react';
|
|
11
|
+
import { ConversationClient, DpopFetchClient, DpopProofProviders, SecretValue, buildPapReactor, buildDirectSignInRequest, buildSessionAssertionClaims, parseDiscoveryDocument, } from '@datalayer/personal-agent-protocol';
|
|
12
|
+
import { ChatMessageList } from '../../chat/messages/ChatMessageList';
|
|
13
|
+
import { PapExamplePage } from './PapExamplePage';
|
|
14
|
+
const conversationStart = [
|
|
15
|
+
{
|
|
16
|
+
speaker: 'user',
|
|
17
|
+
text: "My Trailhead jacket keeps the rain out, but it isn't warm enough. Can I return it?",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
speaker: 'atlas',
|
|
21
|
+
text: "I'll ask Northstar Outfitters about a return and whether they have something warmer.",
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
const STEPS = [
|
|
25
|
+
{
|
|
26
|
+
title: 'Finding Northstar',
|
|
27
|
+
summary: "Atlas reads Northstar's public PAP document to learn how returns, authorization, and the company agent are exposed.",
|
|
28
|
+
channel: 'Discovery',
|
|
29
|
+
messages: conversationStart,
|
|
30
|
+
facts: [
|
|
31
|
+
{ label: 'Publishes', value: 'poppy.json · draft 0.1' },
|
|
32
|
+
{ label: 'Returns', value: 'Company conversation agent' },
|
|
33
|
+
{ label: 'Sign-in', value: "Northstar's own page" },
|
|
34
|
+
],
|
|
35
|
+
method: 'GET',
|
|
36
|
+
target: 'https://northstar.example/.well-known/poppy.json',
|
|
37
|
+
detailLabel: 'Atlas → northstar.example',
|
|
38
|
+
request: 'Accept: application/json',
|
|
39
|
+
response: `200 OK
|
|
40
|
+
{
|
|
41
|
+
"protocol_version": "0.1",
|
|
42
|
+
"organization": { "name": "Northstar Outfitters" },
|
|
43
|
+
"auth": { "issuer": "https://auth.northstar.example" },
|
|
44
|
+
"agent": { "protocols": [{ "type": "poppy" }] }
|
|
45
|
+
}`,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
title: 'Starting signed out',
|
|
49
|
+
summary: 'Atlas opens a pairwise Session and can ask about the return before knowing who the user is. Northstar replies that order access needs sign-in.',
|
|
50
|
+
channel: 'Session + conversation',
|
|
51
|
+
messages: [
|
|
52
|
+
...conversationStart,
|
|
53
|
+
{
|
|
54
|
+
speaker: 'company',
|
|
55
|
+
text: 'I can help with that. Please sign in so I can find the order.',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
facts: [
|
|
59
|
+
{ label: 'Personal agent', value: 'Atlas' },
|
|
60
|
+
{ label: 'Session', value: 'Signed out · pairwise' },
|
|
61
|
+
{ label: 'Conversation', value: 'Waiting for sign-in' },
|
|
62
|
+
],
|
|
63
|
+
method: 'POST',
|
|
64
|
+
target: 'https://auth.northstar.example/oauth/token',
|
|
65
|
+
detailLabel: 'Atlas → authorization server',
|
|
66
|
+
request: `Content-Type: application/x-www-form-urlencoded
|
|
67
|
+
DPoP: [fresh host-only proof]
|
|
68
|
+
|
|
69
|
+
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
|
|
70
|
+
assertion=[host-only session assertion]
|
|
71
|
+
client_assertion=[host-only client assertion]`,
|
|
72
|
+
response: `200 OK
|
|
73
|
+
{
|
|
74
|
+
"access_token": "[stored by host]",
|
|
75
|
+
"token_type": "DPoP",
|
|
76
|
+
"session_id": "[opaque reference]",
|
|
77
|
+
"signed_in": false
|
|
78
|
+
}`,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
title: 'The user signs in',
|
|
82
|
+
summary: "Atlas opens Northstar's authorization page. The user signs in there and grants order access. Atlas never sees the password.",
|
|
83
|
+
channel: 'Direct Sign-In',
|
|
84
|
+
phoneMode: 'signin',
|
|
85
|
+
messages: conversationStart,
|
|
86
|
+
facts: [
|
|
87
|
+
{ label: 'User presence', value: 'Required' },
|
|
88
|
+
{ label: 'Requested access', value: 'Orders: read and return' },
|
|
89
|
+
{ label: 'Credentials', value: 'Visible only to Northstar' },
|
|
90
|
+
],
|
|
91
|
+
method: 'GET',
|
|
92
|
+
target: 'https://auth.northstar.example/oauth/authorize',
|
|
93
|
+
detailLabel: 'Trusted browser handoff',
|
|
94
|
+
request: `response_type=code
|
|
95
|
+
client_id=https://atlas.example/agent.json
|
|
96
|
+
redirect_uri=https://atlas.example/oauth/callback
|
|
97
|
+
scope=orders:read orders:return
|
|
98
|
+
state=[single-use, host-only]
|
|
99
|
+
code_challenge=[S256, host-only verifier]`,
|
|
100
|
+
response: `302 Found
|
|
101
|
+
Location: https://atlas.example/oauth/callback
|
|
102
|
+
?code=[single-use authorization code]
|
|
103
|
+
&state=[returned unchanged]`,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
title: 'An offer comes back',
|
|
107
|
+
summary: 'The signed-in Session continues the same conversation. Northstar finds the order and offers an even exchange or a refund.',
|
|
108
|
+
channel: 'Conversation',
|
|
109
|
+
messages: [
|
|
110
|
+
...conversationStart,
|
|
111
|
+
{ speaker: 'company', text: 'Please sign in so I can find the order.' },
|
|
112
|
+
{ speaker: 'user', text: "I'm signed in." },
|
|
113
|
+
{ speaker: 'atlas', text: "Thanks. I'll continue the same request." },
|
|
114
|
+
{
|
|
115
|
+
speaker: 'company',
|
|
116
|
+
text: 'I can exchange it for the warmer Summit jacket at no extra cost, or issue a full refund.',
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
facts: [
|
|
120
|
+
{ label: 'Order', value: 'NS-48213' },
|
|
121
|
+
{ label: 'Options', value: 'Even exchange · Full refund' },
|
|
122
|
+
{ label: 'Status', value: "Waiting for the user's choice" },
|
|
123
|
+
],
|
|
124
|
+
method: 'POST',
|
|
125
|
+
target: 'https://api.northstar.example/poppy/conversations/cnv_return/messages',
|
|
126
|
+
detailLabel: 'Atlas ↔ company agent',
|
|
127
|
+
request: `Authorization: DPoP [host-only Session Token]
|
|
128
|
+
DPoP: [fresh proof]
|
|
129
|
+
Content-Type: application/json
|
|
130
|
+
|
|
131
|
+
{
|
|
132
|
+
"message": {
|
|
133
|
+
"id": "msg_offer",
|
|
134
|
+
"sender": "agent",
|
|
135
|
+
"text": "The user has signed in. Continue the return request."
|
|
136
|
+
}
|
|
137
|
+
}`,
|
|
138
|
+
response: `202 Accepted
|
|
139
|
+
{
|
|
140
|
+
"conversation_id": "cnv_return",
|
|
141
|
+
"events": [{
|
|
142
|
+
"type": "message",
|
|
143
|
+
"message": {
|
|
144
|
+
"role": "company",
|
|
145
|
+
"text": "Choose an even exchange or refund."
|
|
146
|
+
}
|
|
147
|
+
}],
|
|
148
|
+
"status": "idle",
|
|
149
|
+
"responder": "agent"
|
|
150
|
+
}`,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
title: 'The exchange is set',
|
|
154
|
+
summary: "Atlas passes on the user's exact choice. Northstar confirms the exchange and the conversation closes with a durable result.",
|
|
155
|
+
channel: 'Conversation result',
|
|
156
|
+
messages: [
|
|
157
|
+
...conversationStart,
|
|
158
|
+
{ speaker: 'company', text: 'Please sign in so I can find the order.' },
|
|
159
|
+
{ speaker: 'user', text: "I'm signed in." },
|
|
160
|
+
{
|
|
161
|
+
speaker: 'company',
|
|
162
|
+
text: 'I can offer the warmer Summit jacket or a full refund.',
|
|
163
|
+
},
|
|
164
|
+
{ speaker: 'user', text: "Let's do the exchange." },
|
|
165
|
+
{
|
|
166
|
+
speaker: 'atlas',
|
|
167
|
+
text: 'Done. The Summit jacket ships tomorrow. A prepaid return label is in your email.',
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
facts: [
|
|
171
|
+
{ label: 'Result', value: 'Exchange arranged' },
|
|
172
|
+
{ label: 'Return code', value: 'NRT-7K4Q' },
|
|
173
|
+
{ label: 'Conversation', value: 'Closed' },
|
|
174
|
+
],
|
|
175
|
+
method: 'POST',
|
|
176
|
+
target: 'https://api.northstar.example/poppy/conversations/cnv_return/messages',
|
|
177
|
+
detailLabel: 'Atlas ↔ company agent',
|
|
178
|
+
request: `Authorization: DPoP [host-only Session Token]
|
|
179
|
+
DPoP: [fresh proof]
|
|
180
|
+
Content-Type: application/json
|
|
181
|
+
|
|
182
|
+
{
|
|
183
|
+
"message": {
|
|
184
|
+
"id": "msg_exchange_choice",
|
|
185
|
+
"sender": "agent",
|
|
186
|
+
"text": "The user chose the even exchange."
|
|
187
|
+
}
|
|
188
|
+
}`,
|
|
189
|
+
response: `202 Accepted
|
|
190
|
+
{
|
|
191
|
+
"conversation_id": "cnv_return",
|
|
192
|
+
"events": [{
|
|
193
|
+
"type": "message",
|
|
194
|
+
"message": {
|
|
195
|
+
"role": "company",
|
|
196
|
+
"text": "The exchange is arranged."
|
|
197
|
+
}
|
|
198
|
+
}],
|
|
199
|
+
"status": "closed"
|
|
200
|
+
}`,
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
const speakerName = {
|
|
204
|
+
user: 'You',
|
|
205
|
+
atlas: 'Atlas',
|
|
206
|
+
company: 'Northstar',
|
|
207
|
+
};
|
|
208
|
+
const companyReply = (response) => {
|
|
209
|
+
if (!('events' in response)) {
|
|
210
|
+
throw new Error('Northstar returned a receipt without a message');
|
|
211
|
+
}
|
|
212
|
+
const reply = response.events.find(event => event.type === 'message' && event.message?.role === 'company')?.message?.text;
|
|
213
|
+
if (!reply) {
|
|
214
|
+
throw new Error('Northstar returned no company reply');
|
|
215
|
+
}
|
|
216
|
+
return reply;
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* Run the visible transcript through the actual PAP conversation client.
|
|
220
|
+
*
|
|
221
|
+
* The company side is deliberately an in-page deterministic simulator: this
|
|
222
|
+
* makes the example runnable without an account while ensuring that requests,
|
|
223
|
+
* proof-bound transport, response parsing, and messages all use the real SDK.
|
|
224
|
+
*/
|
|
225
|
+
const runJourney = async () => {
|
|
226
|
+
parseDiscoveryDocument({
|
|
227
|
+
protocol_version: '0.1',
|
|
228
|
+
organization: { name: 'Northstar Outfitters', domain: 'northstar.example' },
|
|
229
|
+
auth: {
|
|
230
|
+
issuer: 'https://auth.northstar.example',
|
|
231
|
+
direct: { scopes: ['orders:read', 'orders:return'] },
|
|
232
|
+
custom_scopes: {},
|
|
233
|
+
},
|
|
234
|
+
agent: {
|
|
235
|
+
protocols: [
|
|
236
|
+
{
|
|
237
|
+
type: 'poppy',
|
|
238
|
+
endpoint: 'https://api.northstar.example/poppy/conversations',
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
buildSessionAssertionClaims({
|
|
244
|
+
clientId: 'https://atlas.example/agent.json',
|
|
245
|
+
pairwiseUserId: 'usr_host_only_journey',
|
|
246
|
+
tokenEndpoint: 'https://auth.northstar.example/oauth/token',
|
|
247
|
+
});
|
|
248
|
+
await buildDirectSignInRequest({
|
|
249
|
+
authorizationEndpoint: 'https://auth.northstar.example/oauth/authorize',
|
|
250
|
+
issuer: 'https://auth.northstar.example',
|
|
251
|
+
clientId: 'https://atlas.example/agent.json',
|
|
252
|
+
redirectUri: 'https://atlas.example/oauth/callback',
|
|
253
|
+
scopes: ['orders:read', 'orders:return'],
|
|
254
|
+
});
|
|
255
|
+
const replies = [
|
|
256
|
+
'I can help with that. Please sign in so I can find the order.',
|
|
257
|
+
'Order NS-48213 can be exchanged for the warmer Summit jacket at no extra cost, or refunded in full.',
|
|
258
|
+
'The exchange is arranged. The Summit jacket ships tomorrow, and return code NRT-7K4Q is ready.',
|
|
259
|
+
];
|
|
260
|
+
let requestIndex = 0;
|
|
261
|
+
const localCompanyFetch = async (_input, init) => {
|
|
262
|
+
const index = requestIndex++;
|
|
263
|
+
const reply = replies[index];
|
|
264
|
+
if (!reply) {
|
|
265
|
+
return new Response(JSON.stringify({ error: 'unexpected_request' }), {
|
|
266
|
+
status: 400,
|
|
267
|
+
headers: { 'content-type': 'application/json' },
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const incoming = JSON.parse(String(init?.body ?? '{}'));
|
|
271
|
+
return new Response(JSON.stringify({
|
|
272
|
+
conversation_id: 'cnv_return',
|
|
273
|
+
events: [
|
|
274
|
+
{
|
|
275
|
+
id: `evt_${index + 1}`,
|
|
276
|
+
type: 'message',
|
|
277
|
+
created_at: `2026-10-10T09:4${index}:00Z`,
|
|
278
|
+
message: {
|
|
279
|
+
id: `reply_${index + 1}`,
|
|
280
|
+
sender: 'agent',
|
|
281
|
+
role: 'company',
|
|
282
|
+
text: reply,
|
|
283
|
+
context: { in_reply_to: incoming.message?.id ?? 'unknown' },
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
cursor: `cursor_${index + 1}`,
|
|
288
|
+
has_more: false,
|
|
289
|
+
status: index === replies.length - 1 ? 'closed' : 'idle',
|
|
290
|
+
responder: 'agent',
|
|
291
|
+
}), {
|
|
292
|
+
status: index === 0 ? 201 : 202,
|
|
293
|
+
headers: { 'content-type': 'application/json' },
|
|
294
|
+
});
|
|
295
|
+
};
|
|
296
|
+
const securityPlugin = definePlugin({
|
|
297
|
+
name: '@datalayer/loop-example-pap-guided-security',
|
|
298
|
+
contributes: [
|
|
299
|
+
contribution(DpopProofProviders, {
|
|
300
|
+
async create() {
|
|
301
|
+
return new SecretValue(`host-only-proof-${requestIndex + 1}`);
|
|
302
|
+
},
|
|
303
|
+
}, { id: 'guided-example-dpop' }),
|
|
304
|
+
],
|
|
305
|
+
});
|
|
306
|
+
const reactor = buildPapReactor({ fetch: localCompanyFetch }, [
|
|
307
|
+
securityPlugin,
|
|
308
|
+
]);
|
|
309
|
+
try {
|
|
310
|
+
const transport = new DpopFetchClient({
|
|
311
|
+
reactor,
|
|
312
|
+
fetch: localCompanyFetch,
|
|
313
|
+
});
|
|
314
|
+
const conversations = new ConversationClient({ reactor, transport });
|
|
315
|
+
const session = (signedIn) => ({
|
|
316
|
+
accessToken: new SecretValue(signedIn ? 'host-only-signed-in-token' : 'host-only-session-token'),
|
|
317
|
+
tokenType: 'DPoP',
|
|
318
|
+
expiresIn: 300,
|
|
319
|
+
scopes: signedIn ? ['orders:read', 'orders:return'] : ['poppy:read'],
|
|
320
|
+
sessionId: 'ses_guided_example',
|
|
321
|
+
signedIn,
|
|
322
|
+
});
|
|
323
|
+
const requestOptions = (token) => ({
|
|
324
|
+
token,
|
|
325
|
+
tenantId: 'guided-example',
|
|
326
|
+
pairwiseUserId: 'usr_host_only_journey',
|
|
327
|
+
issuer: 'https://auth.northstar.example',
|
|
328
|
+
wait: 0,
|
|
329
|
+
});
|
|
330
|
+
const endpoint = 'https://api.northstar.example/poppy/conversations';
|
|
331
|
+
const started = await conversations.start(endpoint, {
|
|
332
|
+
id: conversations.generateMessageId(),
|
|
333
|
+
sender: 'agent',
|
|
334
|
+
text: conversationStart[0]?.text,
|
|
335
|
+
}, requestOptions(session(false)));
|
|
336
|
+
const signInReply = companyReply(started);
|
|
337
|
+
const offer = companyReply(await conversations.send(endpoint, 'cnv_return', {
|
|
338
|
+
id: conversations.generateMessageId(),
|
|
339
|
+
sender: 'agent',
|
|
340
|
+
text: 'The user has signed in. Continue the return request.',
|
|
341
|
+
}, requestOptions(session(true))));
|
|
342
|
+
const confirmation = companyReply(await conversations.send(endpoint, 'cnv_return', {
|
|
343
|
+
id: conversations.generateMessageId(),
|
|
344
|
+
sender: 'agent',
|
|
345
|
+
text: 'The user chose the even exchange.',
|
|
346
|
+
}, requestOptions(session(true))));
|
|
347
|
+
const signedInMessages = [
|
|
348
|
+
...conversationStart,
|
|
349
|
+
{ speaker: 'company', text: signInReply },
|
|
350
|
+
{ speaker: 'user', text: "I'm signed in." },
|
|
351
|
+
{ speaker: 'atlas', text: "Thanks. I'll continue the same request." },
|
|
352
|
+
{ speaker: 'company', text: offer },
|
|
353
|
+
];
|
|
354
|
+
return [
|
|
355
|
+
conversationStart,
|
|
356
|
+
[...conversationStart, { speaker: 'company', text: signInReply }],
|
|
357
|
+
conversationStart,
|
|
358
|
+
signedInMessages,
|
|
359
|
+
[
|
|
360
|
+
...signedInMessages,
|
|
361
|
+
{ speaker: 'user', text: "Let's do the exchange." },
|
|
362
|
+
{ speaker: 'company', text: confirmation },
|
|
363
|
+
],
|
|
364
|
+
];
|
|
365
|
+
}
|
|
366
|
+
finally {
|
|
367
|
+
reactor.stop();
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
const ChatPhone = ({ step, messages, onAuthorized }) => {
|
|
371
|
+
const messagesEndRef = useRef(null);
|
|
372
|
+
const displayItems = useMemo(() => messages.map((message, index) => ({
|
|
373
|
+
id: `guided-${step.channel}-${index}`,
|
|
374
|
+
role: message.speaker === 'user' ? 'user' : 'assistant',
|
|
375
|
+
content: message.text,
|
|
376
|
+
createdAt: new Date(`2026-10-10T09:4${index}:00Z`),
|
|
377
|
+
speaker: {
|
|
378
|
+
id: message.speaker,
|
|
379
|
+
name: speakerName[message.speaker],
|
|
380
|
+
role: message.speaker === 'atlas'
|
|
381
|
+
? 'personal agent'
|
|
382
|
+
: message.speaker === 'company'
|
|
383
|
+
? 'company agent'
|
|
384
|
+
: 'person',
|
|
385
|
+
tone: message.speaker === 'company' ? 'success' : 'accent',
|
|
386
|
+
initials: message.speaker === 'company'
|
|
387
|
+
? 'N'
|
|
388
|
+
: message.speaker === 'atlas'
|
|
389
|
+
? 'A'
|
|
390
|
+
: 'Y',
|
|
391
|
+
},
|
|
392
|
+
})), [messages, step.channel]);
|
|
393
|
+
return (_jsxs(Box, { width: "100%", maxWidth: 340, height: 560, mx: "auto", border: "7px solid", borderColor: "fg.default", borderRadius: 20, bg: "canvas.default", overflow: "hidden", boxShadow: "shadow.large", display: "flex", flexDirection: "column", children: [_jsxs(Box, { px: 3, py: 2, display: "flex", justifyContent: "space-between", borderBottom: "1px solid", borderColor: "border.default", children: [_jsx(Text, { sx: { fontSize: 0, fontWeight: 600 }, children: "9:41" }), _jsx(Text, { sx: { fontSize: 0, fontWeight: 600 }, children: "5G \u00B7 92%" })] }), step.phoneMode === 'signin' ? (_jsxs(Box, { flex: 1, display: "flex", flexDirection: "column", children: [_jsx(Box, { bg: "success.emphasis", color: "fg.onEmphasis", p: 3, children: _jsx(Text, { sx: { fontWeight: 700 }, children: "N \u00B7 Northstar Outfitters" }) }), _jsxs(Box, { p: 3, display: "flex", flexDirection: "column", gap: 3, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "auth.northstar.example" }), _jsx(Heading, { as: "h2", sx: { fontSize: 3 }, children: "Let Atlas access your order?" }), _jsx(Text, { children: "Atlas can read order NS-48213 and arrange the return you requested." }), _jsxs(Box, { p: 3, bg: "canvas.subtle", borderRadius: 2, children: [_jsx(Text, { sx: { fontWeight: 600 }, children: "Requested access" }), _jsx(Text, { as: "p", sx: { mt: 1, color: 'fg.muted' }, children: "Read this order \u00B7 Arrange one return" })] }), _jsx(Button, { variant: "primary", block: true, onClick: onAuthorized, children: "Authorize Atlas" }), _jsx(Text, { textAlign: "center", sx: { fontSize: 0, color: 'fg.muted' }, children: "Passwords and account credentials stay with Northstar." })] })] })) : (_jsxs(_Fragment, { children: [_jsxs(Box, { px: 3, py: 2, display: "flex", gap: 2, alignItems: "center", children: [_jsx(Box, { width: 30, height: 30, borderRadius: "50%", bg: "accent.emphasis", color: "fg.onEmphasis", display: "flex", alignItems: "center", justifyContent: "center", fontWeight: 700, children: "A" }), _jsxs(Box, { children: [_jsx(Text, { as: "div", sx: { fontWeight: 700 }, children: "Atlas" }), _jsx(Text, { as: "div", sx: { fontSize: 0, color: 'fg.muted' }, children: "Your personal agent" })] })] }), _jsx(Box, { flex: 1, overflow: "auto", bg: "canvas.subtle", children: _jsx(ChatMessageList, { displayItems: displayItems, isLoading: false, isStreaming: false, showLoadingIndicator: false, hideMessagesAfterToolUI: false, avatarConfig: {
|
|
394
|
+
userAvatar: 'Y',
|
|
395
|
+
assistantAvatar: 'A',
|
|
396
|
+
showAvatars: true,
|
|
397
|
+
avatarSize: 26,
|
|
398
|
+
userAvatarBg: 'accent.muted',
|
|
399
|
+
assistantAvatarBg: 'accent.emphasis',
|
|
400
|
+
}, padding: 2, emptyContent: null, messagesEndRef: messagesEndRef, onRespond: async () => undefined, density: "comfortable" }) })] }))] }));
|
|
401
|
+
};
|
|
402
|
+
/** A five-step user, company, and wire-level PAP walkthrough. */
|
|
403
|
+
const PapGuidedJourneyView = () => {
|
|
404
|
+
const [stepIndex, setStepIndex] = useState(0);
|
|
405
|
+
const [detailsOpen, setDetailsOpen] = useState(true);
|
|
406
|
+
const [interactionState, setInteractionState] = useState('checking');
|
|
407
|
+
const [liveMessages, setLiveMessages] = useState(null);
|
|
408
|
+
const step = STEPS[stepIndex] ?? STEPS[0];
|
|
409
|
+
const messages = liveMessages?.[stepIndex] ?? step.messages;
|
|
410
|
+
useEffect(() => {
|
|
411
|
+
void runJourney()
|
|
412
|
+
.then(result => {
|
|
413
|
+
setLiveMessages(result);
|
|
414
|
+
setInteractionState('valid');
|
|
415
|
+
})
|
|
416
|
+
.catch(() => setInteractionState('invalid'));
|
|
417
|
+
}, []);
|
|
418
|
+
const go = (next) => {
|
|
419
|
+
setStepIndex(Math.max(0, Math.min(STEPS.length - 1, next)));
|
|
420
|
+
};
|
|
421
|
+
return (_jsxs(PapExamplePage, { eyebrow: "PAP \u00B7 Guided interaction", title: "A return, from first request to confirmed exchange", description: "Follow what the user experiences, what the company is allowed to see, and which PAP exchange moves the work forward. Northstar's replies are parsed from real PAP ConversationClient requests against a deterministic in-page simulator; discovery and sign-in values use the SDK too. No external retailer, account, or language model is implied.", maxWidth: 1180, children: [_jsxs(Box, { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 2, flexWrap: "wrap", p: 3, bg: "canvas.default", border: "1px solid", borderColor: "border.default", borderRadius: 2, children: [_jsxs(Box, { children: [_jsx(Label, { variant: "accent", children: "Company agent \u00B7 Direct Sign-In" }), _jsx(Heading, { as: "h2", sx: { mt: 1, fontSize: 2 }, children: "The jacket is not warm enough" }), _jsx(Text, { sx: { color: 'fg.muted' }, children: "Atlas finds Northstar, keeps one Session, and asks before acting." })] }), _jsx(Label, { variant: interactionState === 'valid' ? 'success' : 'secondary', children: interactionState === 'valid'
|
|
422
|
+
? 'Live local PAP exchange complete'
|
|
423
|
+
: interactionState === 'invalid'
|
|
424
|
+
? 'Local PAP exchange failed'
|
|
425
|
+
: 'Running local PAP exchange…' })] }), _jsxs(Box, { display: "flex", gap: 2, alignItems: "center", children: [_jsx(IconButton, { icon: ChevronLeftIcon, "aria-label": "Previous journey tab", disabled: stepIndex === 0, onClick: () => go(stepIndex - 1) }), _jsx(Box, { display: "flex", gap: 2, overflow: "auto", flex: 1, "aria-label": "Journey steps", children: STEPS.map((candidate, index) => (_jsxs(Button, { variant: index === stepIndex ? 'primary' : 'default', onClick: () => go(index), sx: { flexShrink: 0 }, children: [index + 1, ". ", candidate.channel] }, candidate.title))) }), _jsx(IconButton, { icon: ChevronRightIcon, "aria-label": "Next journey tab", disabled: stepIndex === STEPS.length - 1, onClick: () => go(stepIndex + 1) })] }), _jsxs(Box, { display: "grid", sx: {
|
|
426
|
+
gridTemplateColumns: [
|
|
427
|
+
'1fr',
|
|
428
|
+
'minmax(300px, 0.78fr) minmax(460px, 1.4fr)',
|
|
429
|
+
],
|
|
430
|
+
}, gap: 4, alignItems: "start", children: [_jsx(ChatPhone, { step: step, messages: messages, onAuthorized: () => go(stepIndex + 1) }), _jsxs(Box, { minHeight: 560, bg: "canvas.default", border: "1px solid", borderColor: "border.default", borderRadius: 2, overflow: "hidden", display: "flex", flexDirection: "column", children: [_jsxs(Box, { p: 3, borderBottom: "1px solid", borderColor: "border.default", children: [_jsxs(Text, { as: "div", sx: { fontSize: 0, color: 'fg.muted' }, children: ["Step ", stepIndex + 1, " of ", STEPS.length, " \u00B7 ", step.channel] }), _jsx(Heading, { as: "h2", sx: { mt: 1, fontSize: 3 }, children: step.title }), _jsx(Text, { as: "p", sx: { mt: 2, color: 'fg.muted' }, children: step.summary })] }), _jsxs(Box, { p: 3, display: "flex", flexDirection: "column", gap: 3, children: [_jsxs(Box, { display: "flex", gap: 2, alignItems: "center", children: [_jsx(Box, { width: 24, height: 24, borderRadius: 1, bg: "success.emphasis", color: "fg.onEmphasis", display: "flex", alignItems: "center", justifyContent: "center", fontWeight: 700, children: "N" }), _jsx(Text, { sx: { fontWeight: 700 }, children: "What Northstar sees" })] }), _jsx(Box, { border: "1px solid", borderColor: "border.default", borderRadius: 2, bg: "canvas.subtle", px: 3, children: step.facts.map(fact => (_jsxs(Box, { display: "flex", justifyContent: "space-between", gap: 3, py: 2, borderBottom: "1px solid", borderColor: "border.muted", sx: { '&:last-child': { borderBottom: 0 } }, children: [_jsx(Text, { sx: { color: 'fg.muted' }, children: fact.label }), _jsx(Text, { textAlign: "right", sx: { fontWeight: 600 }, children: fact.value })] }, fact.label))) }), _jsxs(Button, { variant: "invisible", sx: { alignSelf: 'flex-start', px: 0 }, onClick: () => setDetailsOpen(open => !open), children: ['{ }', " ", detailsOpen ? 'Hide' : 'Show', " protocol details"] }), detailsOpen ? (_jsxs(Box, { bg: "#0d1117", color: "#c9d1d9", borderRadius: 2, overflow: "hidden", children: [_jsxs(Box, { px: 3, py: 2, display: "flex", justifyContent: "space-between", gap: 2, borderBottom: "1px solid", borderColor: "#30363d", children: [_jsx(Text, { sx: { fontFamily: 'mono', fontSize: 0 }, children: step.detailLabel }), _jsx(Label, { variant: "secondary", children: step.channel })] }), _jsxs(Box, { p: 3, maxHeight: 270, overflow: "auto", children: [_jsxs(Text, { as: "div", sx: {
|
|
431
|
+
fontFamily: 'mono',
|
|
432
|
+
fontWeight: 700,
|
|
433
|
+
color: '#ff7b72',
|
|
434
|
+
}, children: [step.method, " ", step.target] }), _jsx(Box, { as: "pre", mt: 2, mb: 3, sx: {
|
|
435
|
+
fontFamily: 'mono',
|
|
436
|
+
fontSize: 0,
|
|
437
|
+
whiteSpace: 'pre-wrap',
|
|
438
|
+
}, children: step.request }), _jsx(Text, { as: "div", sx: {
|
|
439
|
+
fontFamily: 'mono',
|
|
440
|
+
fontWeight: 700,
|
|
441
|
+
color: '#7ee787',
|
|
442
|
+
}, children: "Response" }), _jsx(Box, { as: "pre", mt: 2, mb: 0, sx: {
|
|
443
|
+
fontFamily: 'mono',
|
|
444
|
+
fontSize: 0,
|
|
445
|
+
whiteSpace: 'pre-wrap',
|
|
446
|
+
}, children: step.response })] })] })) : null] })] })] }), _jsxs(Box, { display: "flex", alignItems: "center", justifyContent: "center", gap: 3, children: [_jsx(Button, { disabled: stepIndex === 0, onClick: () => go(stepIndex - 1), children: "Previous" }), _jsx(Box, { display: "flex", gap: 2, "aria-label": "Current step", children: STEPS.map((candidate, index) => (_jsx(Box, { as: "button", "aria-label": `Go to step ${index + 1}: ${candidate.title}`, width: 10, height: 10, p: 0, border: 0, borderRadius: "50%", bg: index === stepIndex ? 'fg.default' : 'border.default', sx: { cursor: 'pointer' }, onClick: () => go(index) }, candidate.title))) }), _jsx(Button, { variant: stepIndex === STEPS.length - 1 ? 'default' : 'primary', disabled: stepIndex === STEPS.length - 1, onClick: () => go(stepIndex + 1), children: "Next step" })] })] }));
|
|
447
|
+
};
|
|
448
|
+
export default PapGuidedJourneyView;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
import { Box } from '@datalayer/primer-addons';
|
|
8
|
+
import { contribution, definePlugin } from '@datalayer/reactor';
|
|
9
|
+
import { Button, Text } from '@primer/react';
|
|
10
|
+
import { DpopProofProviders, SecretValue, SessionClient, Signers, buildPapReactor, } from '@datalayer/personal-agent-protocol';
|
|
11
|
+
import { PapExamplePage, PapResult } from './PapExamplePage';
|
|
12
|
+
/** Exercise the real Session client and retain only safe lifecycle evidence. */
|
|
13
|
+
const PapSessionLifecycleView = () => {
|
|
14
|
+
const [summary, setSummary] = useState(null);
|
|
15
|
+
const [error, setError] = useState(null);
|
|
16
|
+
const runLifecycle = async () => {
|
|
17
|
+
setError(null);
|
|
18
|
+
let calls = 0;
|
|
19
|
+
const forms = [];
|
|
20
|
+
const purposes = [];
|
|
21
|
+
const proofRequests = [];
|
|
22
|
+
const localFetch = async (_input, init) => {
|
|
23
|
+
calls += 1;
|
|
24
|
+
forms.push(new URLSearchParams(String(init?.body ?? '')));
|
|
25
|
+
if (calls === 1) {
|
|
26
|
+
return new Response(JSON.stringify({ error: 'use_dpop_nonce' }), {
|
|
27
|
+
status: 400,
|
|
28
|
+
headers: {
|
|
29
|
+
'content-type': 'application/json',
|
|
30
|
+
'DPoP-Nonce': 'host-only-company-nonce',
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return new Response(JSON.stringify({
|
|
35
|
+
access_token: `host-only-session-token-${calls}`,
|
|
36
|
+
token_type: 'DPoP',
|
|
37
|
+
expires_in: 300,
|
|
38
|
+
scope: 'poppy:read',
|
|
39
|
+
session_id: 'ses_example',
|
|
40
|
+
signed_in: false,
|
|
41
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
42
|
+
};
|
|
43
|
+
const securityPlugin = definePlugin({
|
|
44
|
+
name: '@datalayer/loop-example-pap-session-security',
|
|
45
|
+
contributes: [
|
|
46
|
+
contribution(Signers, {
|
|
47
|
+
async sign(_claims, options) {
|
|
48
|
+
purposes.push(options.purpose);
|
|
49
|
+
return new SecretValue(`host-only-${options.purpose}-${purposes.length}`);
|
|
50
|
+
},
|
|
51
|
+
}, { id: 'example-signer' }),
|
|
52
|
+
contribution(DpopProofProviders, {
|
|
53
|
+
async create(request) {
|
|
54
|
+
proofRequests.push(request);
|
|
55
|
+
return new SecretValue(`host-only-proof-${proofRequests.length}`);
|
|
56
|
+
},
|
|
57
|
+
}, { id: 'example-dpop' }),
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
const reactor = buildPapReactor({ fetch: localFetch }, [securityPlugin]);
|
|
61
|
+
try {
|
|
62
|
+
const client = new SessionClient({ reactor, fetch: localFetch });
|
|
63
|
+
const options = {
|
|
64
|
+
clientId: 'https://assistant.example/agent.json',
|
|
65
|
+
pairwiseUserId: 'usr_host_only_example',
|
|
66
|
+
issuer: 'https://company.example',
|
|
67
|
+
tokenEndpoint: 'https://company.example/oauth/token',
|
|
68
|
+
tenantId: 'example-tenant',
|
|
69
|
+
};
|
|
70
|
+
const started = await client.start(options);
|
|
71
|
+
const renewed = await client.renew({
|
|
72
|
+
...options,
|
|
73
|
+
sessionId: started.sessionId,
|
|
74
|
+
});
|
|
75
|
+
setSummary({
|
|
76
|
+
startStatus: 'created',
|
|
77
|
+
renewStatus: 'renewed',
|
|
78
|
+
signedIn: false,
|
|
79
|
+
tokenType: 'DPoP',
|
|
80
|
+
scopes: renewed.scopes,
|
|
81
|
+
sameSession: started.sessionId === renewed.sessionId,
|
|
82
|
+
nonceRetry: proofRequests.length === 3 &&
|
|
83
|
+
proofRequests[0]?.nonce === undefined &&
|
|
84
|
+
proofRequests[1]?.nonce === 'host-only-company-nonce' &&
|
|
85
|
+
proofRequests[2]?.nonce === undefined,
|
|
86
|
+
freshAssertions: purposes.length === 6,
|
|
87
|
+
freshProofs: proofRequests.length === 3,
|
|
88
|
+
renewalBoundToSession: forms.at(-1)?.has('session_id') === true,
|
|
89
|
+
sessionIdWithheld: true,
|
|
90
|
+
credentialsWithheld: true,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (cause) {
|
|
94
|
+
setSummary(null);
|
|
95
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
reactor.stop();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
return (_jsxs(PapExamplePage, { eyebrow: "PAP \u00B7 Signed-out Session", title: "Start and renew without giving credentials to the agent", description: "This Reactor view drives the real PAP SessionClient against a deterministic company transport. It handles a DPoP nonce challenge, generates fresh assertions and proofs, and renews the same Session. Only lifecycle evidence enters React state.", children: [_jsxs(Box, { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 3, p: 3, border: "1px solid", borderColor: "border.default", borderRadius: 2, bg: "canvas.default", children: [_jsx(Text, { sx: { color: 'fg.muted' }, children: "Exercise start, nonce retry, and renewal entirely inside this host." }), _jsx(Button, { variant: "primary", onClick: () => void runLifecycle(), children: "Run lifecycle" })] }), error ? _jsx(Text, { sx: { color: 'danger.fg' }, children: error }) : null, summary ? (_jsx(PapResult, { title: "Safe Session evidence", value: summary })) : null] }));
|
|
102
|
+
};
|
|
103
|
+
export default PapSessionLifecycleView;
|