@intentcomputing/koko-agent-sdk 1.0.1 → 1.1.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 +6 -0
- package/README.md +12 -5
- package/README.zh-CN.md +11 -4
- package/docs/BACKEND_AUTH_DEMOS.md +38 -0
- package/docs/BACKEND_AUTH_DEMOS.zh-CN.md +187 -0
- package/docs/INTEGRATION.md +13 -6
- package/docs/INTEGRATION.zh-CN.md +11 -4
- package/lib/commonjs/KokoAgent.js +13 -11
- package/lib/commonjs/KokoAgent.js.map +1 -1
- package/lib/commonjs/client/AgentSession.js +3 -1
- package/lib/commonjs/client/AgentSession.js.map +1 -1
- package/lib/commonjs/client/GatewayClient.js +66 -12
- package/lib/commonjs/client/GatewayClient.js.map +1 -1
- package/lib/commonjs/cursor/CursorOverlay.js +1 -2
- package/lib/commonjs/cursor/CursorOverlay.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/provider/KokoProvider.js +1 -2
- package/lib/commonjs/provider/KokoProvider.js.map +1 -1
- package/lib/commonjs/provider/KokoUiContext.js +5 -7
- package/lib/commonjs/provider/KokoUiContext.js.map +1 -1
- package/lib/commonjs/ui/KokoChatSheet.js +1 -2
- package/lib/commonjs/ui/KokoChatSheet.js.map +1 -1
- package/lib/commonjs/ui/KokoComposer.js +1 -2
- package/lib/commonjs/ui/KokoComposer.js.map +1 -1
- package/lib/commonjs/ui/KokoDialogHost.js +1 -2
- package/lib/commonjs/ui/KokoDialogHost.js.map +1 -1
- package/lib/commonjs/ui/KokoObserver.js +1 -2
- package/lib/commonjs/ui/KokoObserver.js.map +1 -1
- package/lib/commonjs/ui/KokoSubtitle.js +1 -2
- package/lib/commonjs/ui/KokoSubtitle.js.map +1 -1
- package/lib/commonjs/ui/KokoTaskPanel.js +1 -2
- package/lib/commonjs/ui/KokoTaskPanel.js.map +1 -1
- package/lib/module/KokoAgent.js +12 -9
- package/lib/module/KokoAgent.js.map +1 -1
- package/lib/module/client/AgentSession.js +3 -1
- package/lib/module/client/AgentSession.js.map +1 -1
- package/lib/module/client/GatewayClient.js +66 -12
- package/lib/module/client/GatewayClient.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/provider/KokoUiContext.js +4 -5
- package/lib/module/provider/KokoUiContext.js.map +1 -1
- package/lib/typescript/commonjs/KokoAgent.d.ts.map +1 -1
- package/lib/typescript/commonjs/client/AgentSession.d.ts.map +1 -1
- package/lib/typescript/commonjs/client/GatewayClient.d.ts +24 -2
- package/lib/typescript/commonjs/client/GatewayClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/provider/KokoUiContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +7 -2
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/module/KokoAgent.d.ts.map +1 -1
- package/lib/typescript/module/client/AgentSession.d.ts.map +1 -1
- package/lib/typescript/module/client/GatewayClient.d.ts +24 -2
- package/lib/typescript/module/client/GatewayClient.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/provider/KokoUiContext.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +7 -2
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/package.json +3 -1
- package/src/KokoAgent.ts +12 -9
- package/src/client/AgentSession.ts +2 -0
- package/src/client/GatewayClient.ts +88 -14
- package/src/index.ts +2 -0
- package/src/provider/KokoUiContext.tsx +4 -5
- package/src/types.ts +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
- Add production `tokenProvider` authentication with expiry-aware caching and one forced refresh after a 401.
|
|
6
|
+
- Add host-backend token exchange documentation and Node.js, Java, Go, Python, PHP and C# examples.
|
|
7
|
+
- Keep static app tokens as a documented legacy development/migration path.
|
|
8
|
+
|
|
3
9
|
## 1.0.1
|
|
4
10
|
|
|
5
11
|
- Clarify npm setup, host-provided entry controls, regional gateways, and privacy responsibilities in the published documentation.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Koko Agent SDK
|
|
2
2
|
|
|
3
|
-
React Native SDK for an observable in-app assistant. The host supplies
|
|
3
|
+
React Native SDK for an observable in-app assistant. The host supplies a short-lived access token and presents a user goal; the SDK observes the screen, asks the hosted gateway for a next step, and performs supported actions inside the app.
|
|
4
4
|
|
|
5
5
|
[npm package](https://www.npmjs.com/package/@intentcomputing/koko-agent-sdk) · [中文](README.zh-CN.md) · [Integration guide](docs/INTEGRATION.md) · [Privacy](docs/PRIVACY.md) · [Contributing](CONTRIBUTING.md)
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ React Native SDK for an observable in-app assistant. The host supplies an app to
|
|
|
8
8
|
|
|
9
9
|
- React Native 0.73 or later and React 18 or later; iOS 15 or later or an Android host supporting React Native autolinking.
|
|
10
10
|
- Expo development build or a native build; Expo Go cannot load this native module.
|
|
11
|
-
-
|
|
11
|
+
- A host backend that exchanges its server-only client secret for short-lived mobile access tokens. Neither credential is an OpenAI or DeepSeek API key.
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
@@ -21,7 +21,7 @@ Rebuild and reinstall the native app after installation or a native SDK upgrade.
|
|
|
21
21
|
|
|
22
22
|
## Quick start
|
|
23
23
|
|
|
24
|
-
Mount the provider around the host navigation tree and connect `openKokoChat()` to a host button.
|
|
24
|
+
Mount the provider around the host navigation tree and connect `openKokoChat()` to a host button. Supply short-lived credentials through `tokenProvider`.
|
|
25
25
|
|
|
26
26
|
```tsx
|
|
27
27
|
import React from 'react';
|
|
@@ -29,8 +29,15 @@ import { Button, View } from 'react-native';
|
|
|
29
29
|
import { KokoProvider, openKokoChat } from '@intentcomputing/koko-agent-sdk';
|
|
30
30
|
|
|
31
31
|
export default function App() {
|
|
32
|
+
const getKokoToken = React.useCallback(async () => {
|
|
33
|
+
const response = await fetch('https://api.your-app.example/koko/access-token', {
|
|
34
|
+
headers: { Authorization: `Bearer ${await getHostSessionToken()}` },
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) throw new Error(`Koko token ${response.status}`);
|
|
37
|
+
return response.json(); // { accessToken, expiresAt } (Unix milliseconds)
|
|
38
|
+
}, []);
|
|
32
39
|
return (
|
|
33
|
-
<KokoProvider config={{ gateway: {
|
|
40
|
+
<KokoProvider config={{ gateway: { tokenProvider: getKokoToken }, locale: 'en' }}>
|
|
34
41
|
<View style={{ flex: 1 }}>
|
|
35
42
|
{/* Your app and navigation go here. */}
|
|
36
43
|
<Button title="Open assistant" onPress={() => openKokoChat()} />
|
|
@@ -40,7 +47,7 @@ export default function App() {
|
|
|
40
47
|
}
|
|
41
48
|
```
|
|
42
49
|
|
|
43
|
-
The default gateway is `https://sdk.intentcomputing.net` (OpenAI). For China, set `gateway.baseUrl` to `https://sdk.intentcomputing.cn` (DeepSeek).
|
|
50
|
+
The default gateway is `https://sdk.intentcomputing.net` (OpenAI). For China, set `gateway.baseUrl` to `https://sdk.intentcomputing.cn` (DeepSeek). The SDK caches short-lived tokens, refreshes before expiry, and retries once after a 401. Long-lived client secrets and model-provider keys must never be bundled into the app. See the [backend authentication demos](docs/BACKEND_AUTH_DEMOS.md) for Node.js, Java, Go, Python, PHP and C#.
|
|
44
51
|
|
|
45
52
|
Call `setKokoAppearance({ locale, theme })` when host preferences change. Configure branding through `config.branding`; custom branding is subject to the app's entitlement. See the [integration guide](docs/INTEGRATION.md) for host actions, privacy gates, and lifecycle details.
|
|
46
53
|
|
package/README.zh-CN.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
面向 React Native 的应用内助手 SDK。用户描述任务后,SDK 观察宿主当前页面,通过网关规划下一步,并在宿主内执行支持的操作,过程对用户可见。
|
|
4
4
|
|
|
5
|
-
[npm 包](https://www.npmjs.com/package/@intentcomputing/koko-agent-sdk) · [English](README.md) · [接入指南](docs/INTEGRATION.zh-CN.md) · [隐私保护](docs/PRIVACY.zh-CN.md)
|
|
5
|
+
[npm 包](https://www.npmjs.com/package/@intentcomputing/koko-agent-sdk) · [English](README.md) · [接入指南](docs/INTEGRATION.zh-CN.md) · [后端鉴权 Demo](docs/BACKEND_AUTH_DEMOS.zh-CN.md) · [隐私保护](docs/PRIVACY.zh-CN.md)
|
|
6
6
|
|
|
7
7
|
## 前置条件
|
|
8
8
|
|
|
9
9
|
- React Native ≥ 0.73、React ≥ 18;iOS ≥ 15,或支持原生自动链接的 Android 宿主。
|
|
10
10
|
- Expo 使用开发构建或原生构建,不支持 Expo Go。
|
|
11
|
-
-
|
|
11
|
+
- 生产环境需要宿主后端:长期 `clientSecret` 只保存在后端,App 仅取得短期 access token。它们都不是模型 API 密钥。
|
|
12
12
|
|
|
13
13
|
## 安装
|
|
14
14
|
|
|
@@ -27,9 +27,16 @@ import { Button, View } from 'react-native';
|
|
|
27
27
|
import { KokoProvider, openKokoChat } from '@intentcomputing/koko-agent-sdk';
|
|
28
28
|
|
|
29
29
|
export default function App() {
|
|
30
|
+
const getKokoToken = React.useCallback(async () => {
|
|
31
|
+
const response = await fetch('https://api.your-app.example/koko/access-token', {
|
|
32
|
+
headers: { Authorization: `Bearer ${await getHostSessionToken()}` },
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) throw new Error(`Koko token ${response.status}`);
|
|
35
|
+
return response.json(); // { accessToken, expiresAt };expiresAt 为 Unix 毫秒
|
|
36
|
+
}, []);
|
|
30
37
|
return (
|
|
31
38
|
<KokoProvider config={{
|
|
32
|
-
gateway: {
|
|
39
|
+
gateway: { tokenProvider: getKokoToken, baseUrl: 'https://sdk.intentcomputing.cn' },
|
|
33
40
|
locale: 'zh',
|
|
34
41
|
}}>
|
|
35
42
|
<View style={{ flex: 1 }}>
|
|
@@ -41,7 +48,7 @@ export default function App() {
|
|
|
41
48
|
}
|
|
42
49
|
```
|
|
43
50
|
|
|
44
|
-
默认网关为海外的 `https://sdk.intentcomputing.net`(OpenAI);国内请显式选择 `https://sdk.intentcomputing.cn`(DeepSeek
|
|
51
|
+
默认网关为海外的 `https://sdk.intentcomputing.net`(OpenAI);国内请显式选择 `https://sdk.intentcomputing.cn`(DeepSeek)。SDK 会缓存短期 token、在过期前刷新,并在 401 后强制刷新一次。长期 `clientSecret` 和模型 API 密钥都不得进入 App。完整三端流程及 Node.js、Java、Go、Python、PHP、C# Demo 见[后端鉴权 Demo](docs/BACKEND_AUTH_DEMOS.zh-CN.md)。
|
|
45
52
|
|
|
46
53
|
宿主必须在展示密码、私钥、助记词及其验证页面前设置隐私标记和用户操作门禁,由用户亲自完成敏感步骤。脱敏的页面信息和任务上下文会发送到所选网关进行规划,接入方应使用虚构数据验证隐私边界。详见[隐私说明](docs/PRIVACY.zh-CN.md)。
|
|
47
54
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Host-backend authentication demos
|
|
2
|
+
|
|
3
|
+
Production authentication uses three parties: the mobile app authenticates to its existing host backend; the host backend exchanges its server-only `clientId` and `clientSecret` with Koko; the app receives a 5–15 minute access token for the SDK.
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
Host app --host session--> Host backend --client secret--> Koko /v1/sdk/token
|
|
7
|
+
Host app <--short token--- Host backend <--short token---- Koko
|
|
8
|
+
Host app --short token-----------------------------------> Koko /v1/agent/*
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Rules
|
|
12
|
+
|
|
13
|
+
- Store `KOKO_CLIENT_SECRET` only in the host backend's secret manager or environment. Never return it to the app or place it in Git or logs.
|
|
14
|
+
- Protect and rate-limit the host `/koko/access-token` endpoint with the host's existing authentication.
|
|
15
|
+
- Derive `subject` on the server from the authenticated user. Send a stable pseudonymous value such as `HMAC-SHA256(hostUserId, separateSalt)`, never an email, phone number, wallet address or advertising ID.
|
|
16
|
+
- Use `https://sdk.intentcomputing.cn` for China and `https://sdk.intentcomputing.net` globally. Credentials and access tokens are region-bound.
|
|
17
|
+
- Koko returns `access_token`, `expires_in`, and `expires_at` (Unix seconds). Return `{ accessToken, expiresAt }` to the SDK, with `expiresAt` expressed in Unix milliseconds.
|
|
18
|
+
- Use a short timeout. Do not log authorization headers or response tokens. Rotate long-lived client secrets.
|
|
19
|
+
|
|
20
|
+
Complete, copyable examples for **Node.js/TypeScript (Express), Java (Spring Boot), Go, Python (FastAPI), PHP (Laravel), and C# (ASP.NET Core)** are maintained in the [Chinese guide](BACKEND_AUTH_DEMOS.zh-CN.md); all examples use standard HTTP Basic authentication for the server-to-server exchange and derive the subject from the host-authenticated user.
|
|
21
|
+
|
|
22
|
+
## React Native
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
const tokenProvider = React.useCallback(async () => {
|
|
26
|
+
const response = await hostApi.get('/koko/access-token');
|
|
27
|
+
return response.data as { accessToken: string; expiresAt: number };
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
<KokoProvider config={{
|
|
31
|
+
gateway: {
|
|
32
|
+
baseUrl: 'https://sdk.intentcomputing.net',
|
|
33
|
+
tokenProvider,
|
|
34
|
+
},
|
|
35
|
+
}}>{children}</KokoProvider>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Do not persist the access token in AsyncStorage or send it to logs/analytics. The SDK keeps it in memory, refreshes shortly before expiry, and clears it before one forced `tokenProvider` retry after a 401.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# 宿主后端鉴权与多语言 Demo
|
|
2
|
+
|
|
3
|
+
生产环境采用三端通信:宿主 App 通过自己的登录态请求宿主后端;宿主后端使用仅存于服务器的 `clientId` / `clientSecret` 向 Koko 换取 5~15 分钟的 access token;App 再把短期 token 交给 SDK。
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
宿主 App ──宿主登录态──> 宿主后端 ──clientSecret──> Koko /v1/sdk/token
|
|
7
|
+
宿主 App <──短期 token── 宿主后端 <──短期 token── Koko
|
|
8
|
+
宿主 App ──短期 token──> Koko /v1/agent/*
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 安全约定
|
|
12
|
+
|
|
13
|
+
- `KOKO_CLIENT_SECRET` 只能放在宿主后端的密钥系统或环境变量中,不能进入 App、Git、日志或前端响应。
|
|
14
|
+
- 宿主的 `/koko/access-token` 必须验证已有登录态并限流,不能做成匿名转发接口。
|
|
15
|
+
- `subject` 必须由后端根据已认证用户生成稳定的匿名值;不要发送邮箱、手机号、钱包地址或设备广告 ID。建议 `HMAC-SHA256(宿主用户ID, 独立盐)`。
|
|
16
|
+
- 国内使用 `https://sdk.intentcomputing.cn`,海外使用 `https://sdk.intentcomputing.net`;凭证和 token 不跨区域使用。
|
|
17
|
+
- Koko 返回字段为 `access_token`、`expires_in`、`expires_at`(Unix 秒)。宿主接口应转换为 SDK 接受的 `{ accessToken, expiresAt }`,其中 `expiresAt` 是 Unix 毫秒。
|
|
18
|
+
- 请求 Koko 必须设置短超时,不记录 Authorization 头或响应 token。长期凭证应定期轮换。
|
|
19
|
+
|
|
20
|
+
以下示例中的 `authenticatedUserId` 必须来自宿主已经验证的 session/JWT,不得从请求 body 或 query 直接采用。
|
|
21
|
+
|
|
22
|
+
## Node.js / TypeScript(Express)
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import crypto from 'node:crypto';
|
|
26
|
+
import express from 'express';
|
|
27
|
+
|
|
28
|
+
const app = express();
|
|
29
|
+
const gateway = process.env.KOKO_REGION === 'cn'
|
|
30
|
+
? 'https://sdk.intentcomputing.cn'
|
|
31
|
+
: 'https://sdk.intentcomputing.net';
|
|
32
|
+
|
|
33
|
+
app.get('/koko/access-token', requireHostLogin, async (req, res) => {
|
|
34
|
+
const subject = crypto.createHmac('sha256', process.env.KOKO_SUBJECT_SALT!)
|
|
35
|
+
.update(req.user.id) // 只取服务端认证结果
|
|
36
|
+
.digest('hex');
|
|
37
|
+
const basic = Buffer.from(
|
|
38
|
+
`${process.env.KOKO_CLIENT_ID}:${process.env.KOKO_CLIENT_SECRET}`,
|
|
39
|
+
).toString('base64');
|
|
40
|
+
const upstream = await fetch(`${gateway}/v1/sdk/token`, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { Authorization: `Basic ${basic}`, 'Content-Type': 'application/json' },
|
|
43
|
+
body: JSON.stringify({ app_id: process.env.KOKO_APP_ID, subject }),
|
|
44
|
+
signal: AbortSignal.timeout(5000),
|
|
45
|
+
});
|
|
46
|
+
if (!upstream.ok) return res.status(502).json({ error: 'koko_token_unavailable' });
|
|
47
|
+
const token = await upstream.json();
|
|
48
|
+
res.set('Cache-Control', 'no-store').json({
|
|
49
|
+
accessToken: token.access_token,
|
|
50
|
+
expiresAt: token.expires_at * 1000,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Java(Spring Boot 3)
|
|
56
|
+
|
|
57
|
+
```java
|
|
58
|
+
record KokoToken(String access_token, long expires_at) {}
|
|
59
|
+
record AppToken(String accessToken, long expiresAt) {}
|
|
60
|
+
|
|
61
|
+
AppToken exchange(String authenticatedUserId) throws Exception {
|
|
62
|
+
String subject = hmacSha256Hex(System.getenv("KOKO_SUBJECT_SALT"), authenticatedUserId);
|
|
63
|
+
String basic = Base64.getEncoder().encodeToString(
|
|
64
|
+
(System.getenv("KOKO_CLIENT_ID") + ":" + System.getenv("KOKO_CLIENT_SECRET"))
|
|
65
|
+
.getBytes(StandardCharsets.UTF_8));
|
|
66
|
+
String body = objectMapper.writeValueAsString(Map.of(
|
|
67
|
+
"app_id", System.getenv("KOKO_APP_ID"), "subject", subject));
|
|
68
|
+
HttpRequest request = HttpRequest.newBuilder(URI.create(kokoGateway + "/v1/sdk/token"))
|
|
69
|
+
.timeout(Duration.ofSeconds(5)).header("Authorization", "Basic " + basic)
|
|
70
|
+
.header("Content-Type", "application/json")
|
|
71
|
+
.POST(HttpRequest.BodyPublishers.ofString(body)).build();
|
|
72
|
+
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
|
73
|
+
if (response.statusCode() != 200) throw new IllegalStateException("Koko token unavailable");
|
|
74
|
+
KokoToken token = objectMapper.readValue(response.body(), KokoToken.class);
|
|
75
|
+
return new AppToken(token.access_token(), token.expires_at() * 1000L);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Spring Controller 必须从 `Principal` / SecurityContext 取得 `authenticatedUserId`,并返回 `Cache-Control: no-store`。
|
|
80
|
+
|
|
81
|
+
## Go(net/http)
|
|
82
|
+
|
|
83
|
+
```go
|
|
84
|
+
func exchangeKokoToken(ctx context.Context, authenticatedUserID string) (string, int64, error) {
|
|
85
|
+
mac := hmac.New(sha256.New, []byte(os.Getenv("KOKO_SUBJECT_SALT")))
|
|
86
|
+
mac.Write([]byte(authenticatedUserID))
|
|
87
|
+
body, _ := json.Marshal(map[string]string{
|
|
88
|
+
"app_id": os.Getenv("KOKO_APP_ID"), "subject": hex.EncodeToString(mac.Sum(nil)),
|
|
89
|
+
})
|
|
90
|
+
ctx, cancel := context.WithTimeout(ctx, 5*time.Second); defer cancel()
|
|
91
|
+
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, kokoGateway+"/v1/sdk/token", bytes.NewReader(body))
|
|
92
|
+
req.SetBasicAuth(os.Getenv("KOKO_CLIENT_ID"), os.Getenv("KOKO_CLIENT_SECRET"))
|
|
93
|
+
req.Header.Set("Content-Type", "application/json")
|
|
94
|
+
resp, err := http.DefaultClient.Do(req); if err != nil { return "", 0, err }
|
|
95
|
+
defer resp.Body.Close()
|
|
96
|
+
if resp.StatusCode != http.StatusOK { return "", 0, fmt.Errorf("koko token: %s", resp.Status) }
|
|
97
|
+
var out struct { AccessToken string `json:"access_token"`; ExpiresAt int64 `json:"expires_at"` }
|
|
98
|
+
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return "", 0, err }
|
|
99
|
+
return out.AccessToken, out.ExpiresAt * 1000, nil
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Python(FastAPI + HTTPX)
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import hashlib, hmac, os
|
|
107
|
+
import httpx
|
|
108
|
+
from fastapi import Depends, HTTPException, Response
|
|
109
|
+
|
|
110
|
+
@app.get('/koko/access-token')
|
|
111
|
+
async def koko_token(response: Response, user=Depends(require_host_user)):
|
|
112
|
+
subject = hmac.new(os.environ['KOKO_SUBJECT_SALT'].encode(), str(user.id).encode(), hashlib.sha256).hexdigest()
|
|
113
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
114
|
+
upstream = await client.post(
|
|
115
|
+
f'{KOKO_GATEWAY}/v1/sdk/token',
|
|
116
|
+
auth=(os.environ['KOKO_CLIENT_ID'], os.environ['KOKO_CLIENT_SECRET']),
|
|
117
|
+
json={'app_id': os.environ['KOKO_APP_ID'], 'subject': subject},
|
|
118
|
+
)
|
|
119
|
+
if upstream.status_code != 200:
|
|
120
|
+
raise HTTPException(502, 'Koko token unavailable')
|
|
121
|
+
token = upstream.json()
|
|
122
|
+
response.headers['Cache-Control'] = 'no-store'
|
|
123
|
+
return {'accessToken': token['access_token'], 'expiresAt': token['expires_at'] * 1000}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## PHP(Laravel)
|
|
127
|
+
|
|
128
|
+
```php
|
|
129
|
+
public function token(Request $request): JsonResponse
|
|
130
|
+
{
|
|
131
|
+
$userId = (string) $request->user()->getAuthIdentifier(); // auth middleware 已验证
|
|
132
|
+
$subject = hash_hmac('sha256', $userId, env('KOKO_SUBJECT_SALT'));
|
|
133
|
+
$upstream = Http::timeout(5)
|
|
134
|
+
->withBasicAuth(env('KOKO_CLIENT_ID'), env('KOKO_CLIENT_SECRET'))
|
|
135
|
+
->post(config('services.koko.gateway').'/v1/sdk/token', [
|
|
136
|
+
'app_id' => env('KOKO_APP_ID'), 'subject' => $subject,
|
|
137
|
+
]);
|
|
138
|
+
abort_unless($upstream->successful(), 502, 'Koko token unavailable');
|
|
139
|
+
return response()->json([
|
|
140
|
+
'accessToken' => $upstream['access_token'],
|
|
141
|
+
'expiresAt' => $upstream['expires_at'] * 1000,
|
|
142
|
+
])->header('Cache-Control', 'no-store');
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## C#(ASP.NET Core)
|
|
147
|
+
|
|
148
|
+
```csharp
|
|
149
|
+
app.MapGet("/koko/access-token", async (ClaimsPrincipal user, IHttpClientFactory factory) => {
|
|
150
|
+
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier)
|
|
151
|
+
?? throw new UnauthorizedAccessException();
|
|
152
|
+
var subject = Convert.ToHexString(HMACSHA256.HashData(
|
|
153
|
+
Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("KOKO_SUBJECT_SALT")!),
|
|
154
|
+
Encoding.UTF8.GetBytes(userId))).ToLowerInvariant();
|
|
155
|
+
var clientId = Environment.GetEnvironmentVariable("KOKO_CLIENT_ID")!;
|
|
156
|
+
var clientSecret = Environment.GetEnvironmentVariable("KOKO_CLIENT_SECRET")!;
|
|
157
|
+
var appId = Environment.GetEnvironmentVariable("KOKO_APP_ID")!;
|
|
158
|
+
var request = new HttpRequestMessage(HttpMethod.Post, $"{kokoGateway}/v1/sdk/token");
|
|
159
|
+
var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes(
|
|
160
|
+
$"{clientId}:{clientSecret}"));
|
|
161
|
+
request.Headers.Authorization = new("Basic", credentials);
|
|
162
|
+
request.Content = JsonContent.Create(new { app_id = appId, subject });
|
|
163
|
+
var upstream = await factory.CreateClient("koko").SendAsync(request);
|
|
164
|
+
if (!upstream.IsSuccessStatusCode) return Results.Problem(statusCode: 502, title: "Koko token unavailable");
|
|
165
|
+
var token = await upstream.Content.ReadFromJsonAsync<KokoToken>();
|
|
166
|
+
return Results.Json(new { accessToken = token!.access_token, expiresAt = token.expires_at * 1000 },
|
|
167
|
+
headers: new Dictionary<string, string> { ["Cache-Control"] = "no-store" });
|
|
168
|
+
}).RequireAuthorization();
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## App 端
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
const tokenProvider = React.useCallback(async () => {
|
|
175
|
+
const response = await hostApi.get('/koko/access-token');
|
|
176
|
+
return response.data as { accessToken: string; expiresAt: number };
|
|
177
|
+
}, []);
|
|
178
|
+
|
|
179
|
+
<KokoProvider config={{
|
|
180
|
+
gateway: {
|
|
181
|
+
baseUrl: 'https://sdk.intentcomputing.cn',
|
|
182
|
+
tokenProvider,
|
|
183
|
+
},
|
|
184
|
+
}}>{children}</KokoProvider>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
不要把 token 写入 AsyncStorage、日志或分析事件。SDK 自己在内存中缓存,到期前刷新;如果网关返回 401,会清除缓存并强制调用一次 `tokenProvider`。
|
package/docs/INTEGRATION.md
CHANGED
|
@@ -8,14 +8,14 @@ For React Native host apps. After installing the package you must link native co
|
|
|
8
8
|
|
|
9
9
|
- React Native ≥ 0.73 (or Expo Dev Client / `expo prebuild` — **not Expo Go**)
|
|
10
10
|
- iOS deployment target ≥ 15; ability to run `pod install`
|
|
11
|
-
- An
|
|
11
|
+
- An Intent Computing `clientId` and `clientSecret`; keep the secret on the host backend only
|
|
12
12
|
|
|
13
13
|
## Checklist
|
|
14
14
|
|
|
15
15
|
1. Install the npm package
|
|
16
16
|
2. iOS: `pod install` (Android usually autolinks)
|
|
17
17
|
3. **Rebuild native** (Debug or Release)
|
|
18
|
-
4.
|
|
18
|
+
4. Exchange credentials on the host backend and provide short-lived tokens to `KokoProvider`
|
|
19
19
|
5. Launch and send a natural-language goal
|
|
20
20
|
|
|
21
21
|
JS-only config changes can hot-reload via Metro. **First integration or any native SDK change requires a native rebuild.**
|
|
@@ -65,11 +65,15 @@ Wrap at the app root (or outermost navigation shell):
|
|
|
65
65
|
import { KokoProvider } from '@intentcomputing/koko-agent-sdk';
|
|
66
66
|
|
|
67
67
|
export default function App() {
|
|
68
|
+
const tokenProvider = React.useCallback(async () => {
|
|
69
|
+
const response = await hostApi.get('/koko/access-token');
|
|
70
|
+
return response.data as { accessToken: string; expiresAt: number };
|
|
71
|
+
}, []);
|
|
68
72
|
return (
|
|
69
73
|
<KokoProvider
|
|
70
74
|
config={{
|
|
71
75
|
gateway: {
|
|
72
|
-
|
|
76
|
+
tokenProvider,
|
|
73
77
|
// Default: https://sdk.intentcomputing.net
|
|
74
78
|
// China: https://sdk.intentcomputing.cn
|
|
75
79
|
// baseUrl: 'https://sdk.intentcomputing.cn',
|
|
@@ -85,9 +89,11 @@ export default function App() {
|
|
|
85
89
|
}
|
|
86
90
|
```
|
|
87
91
|
|
|
92
|
+
`tokenProvider` calls your backend, never Koko's exchange endpoint directly: the `clientSecret` must not enter the app. The SDK caches credentials using `expiresAt` or JWT `exp` and refreshes once after a 401. See the [backend demos](BACKEND_AUTH_DEMOS.md) for complete examples. Static `gateway.token` remains available only for local development and migration.
|
|
93
|
+
|
|
88
94
|
`KokoProvider` will:
|
|
89
95
|
|
|
90
|
-
- Initialize the agent with `gateway.
|
|
96
|
+
- Initialize the agent with `gateway.tokenProvider`
|
|
91
97
|
- Mount cursor, progress, and chat sheet (optional — see below)
|
|
92
98
|
|
|
93
99
|
Open chat from host UI:
|
|
@@ -138,7 +144,7 @@ For custom private UIs, declare the gate before navigation or reveal and keep it
|
|
|
138
144
|
|
|
139
145
|
1. Launch the app and use your host button wired to `openKokoChat()`. The provider does not create a host entry button.
|
|
140
146
|
2. Send a verifiable goal; you should see dump → tap with a visible cursor.
|
|
141
|
-
3. Check that requests use the selected official gateway and a token bound to this app. Do not bundle
|
|
147
|
+
3. Check that requests use the selected official gateway and a short-lived token bound to this app. Do not bundle a client secret, long-lived token or LLM API key. Validate privacy gates and outbound payloads using synthetic data.
|
|
142
148
|
|
|
143
149
|
---
|
|
144
150
|
|
|
@@ -157,7 +163,7 @@ Not supported. Use a Dev Client, bare RN, or a production host binary.
|
|
|
157
163
|
Use `portal:` / `file:`. If the package lives **outside** the monorepo, Metro may need `watchFolders` + `extraNodeModules` (usually unnecessary after npm publish).
|
|
158
164
|
|
|
159
165
|
**Where does the token come from?**
|
|
160
|
-
Intent Computing
|
|
166
|
+
Your backend exchanges its server-only Intent Computing client credentials for a short-lived token. It is not a model-vendor API key.
|
|
161
167
|
|
|
162
168
|
---
|
|
163
169
|
|
|
@@ -166,3 +172,4 @@ Intent Computing console / commercial onboarding — not a model-vendor API key.
|
|
|
166
172
|
- Package overview: [../README.md](../README.md)
|
|
167
173
|
- npm publish: [PUBLISH.md](./PUBLISH.md)
|
|
168
174
|
- 中文接入指南: [INTEGRATION.zh-CN.md](./INTEGRATION.zh-CN.md)
|
|
175
|
+
- Backend authentication demos: [BACKEND_AUTH_DEMOS.md](./BACKEND_AUTH_DEMOS.md)
|
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
|
|
9
9
|
- React Native ≥ 0.73(或 Expo Dev Client / `expo prebuild`,**不支持 Expo Go**)
|
|
10
10
|
- iOS 部署目标 ≥ 15;需本机可 `pod install`
|
|
11
|
-
- 从 Intent Computing 拿到
|
|
11
|
+
- 从 Intent Computing 拿到 `clientId` / `clientSecret`;`clientSecret` 只放宿主后端
|
|
12
12
|
|
|
13
13
|
## 步骤一览
|
|
14
14
|
|
|
15
15
|
1. 安装 npm 包
|
|
16
16
|
2. iOS `pod install`(Android 一般靠 autolinking)
|
|
17
17
|
3. **重新编译原生**(Debug / Release 均可)
|
|
18
|
-
4.
|
|
18
|
+
4. 宿主后端换取短期 token,App 用 `tokenProvider` 提供给 `KokoProvider`
|
|
19
19
|
5. 将 `openKokoChat()` 对接宿主入口,再发自然语言任务
|
|
20
20
|
|
|
21
21
|
之后若只改 JS 配置或升级包的纯 JS 部分,Metro 热更新即可;**SDK 含原生改动或首次接入,必须再编原生。**
|
|
@@ -63,11 +63,15 @@ npx react-native run-android
|
|
|
63
63
|
import { KokoProvider } from '@intentcomputing/koko-agent-sdk';
|
|
64
64
|
|
|
65
65
|
export default function App() {
|
|
66
|
+
const tokenProvider = React.useCallback(async () => {
|
|
67
|
+
const response = await hostApi.get('/koko/access-token');
|
|
68
|
+
return response.data as { accessToken: string; expiresAt: number };
|
|
69
|
+
}, []);
|
|
66
70
|
return (
|
|
67
71
|
<KokoProvider
|
|
68
72
|
config={{
|
|
69
73
|
gateway: {
|
|
70
|
-
|
|
74
|
+
tokenProvider,
|
|
71
75
|
// 默认 https://sdk.intentcomputing.net(OpenAI)
|
|
72
76
|
// 国内:https://sdk.intentcomputing.cn(DeepSeek)
|
|
73
77
|
baseUrl: 'https://sdk.intentcomputing.cn',
|
|
@@ -82,6 +86,8 @@ export default function App() {
|
|
|
82
86
|
}
|
|
83
87
|
```
|
|
84
88
|
|
|
89
|
+
`tokenProvider` 应调用宿主后端,不能直接调用 Koko 换 token 接口,因为 `clientSecret` 绝不能进入 App。SDK 会按 `expiresAt` 或 JWT `exp` 缓存凭证,并在 401 后刷新一次。宿主后端完整实现见[六种主流语言 Demo](BACKEND_AUTH_DEMOS.zh-CN.md)。`gateway.token` 仅保留给本地开发和迁移,不建议用于正式包。
|
|
90
|
+
|
|
85
91
|
宿主也可主动打开会话:
|
|
86
92
|
|
|
87
93
|
```tsx
|
|
@@ -113,7 +119,7 @@ getKokoAgent().openChat();
|
|
|
113
119
|
|
|
114
120
|
1. 启动 App,通过接有 `openKokoChat()` 的宿主按钮打开助手;Provider 不会自动创建宿主入口。
|
|
115
121
|
2. 输入可核验目标,应看到 dump → tap 与光标。
|
|
116
|
-
3. 请求打到官网网关(或 `baseUrl`),App
|
|
122
|
+
3. 请求打到官网网关(或 `baseUrl`),App 内无 `clientSecret`、长期 token 或任何 LLM API Key。
|
|
117
123
|
|
|
118
124
|
---
|
|
119
125
|
|
|
@@ -138,3 +144,4 @@ getKokoAgent().openChat();
|
|
|
138
144
|
- 英文总览:[../README.md](../README.md)
|
|
139
145
|
- 中文总览:[../README.zh-CN.md](../README.zh-CN.md)
|
|
140
146
|
- 发布:[PUBLISH.zh-CN.md](./PUBLISH.zh-CN.md)
|
|
147
|
+
- 后端鉴权 Demo:[BACKEND_AUTH_DEMOS.zh-CN.md](./BACKEND_AUTH_DEMOS.zh-CN.md)
|
|
@@ -29,8 +29,7 @@ var _ExecutionGuard = require("./commercial/ExecutionGuard.js");
|
|
|
29
29
|
var _AgentDebug = require("./commercial/AgentDebug.js");
|
|
30
30
|
var _index = require("./i18n/index.js");
|
|
31
31
|
var _ocrTarget = require("./assist/ocrTarget.js");
|
|
32
|
-
var
|
|
33
|
-
function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var r, c, a = Object.defineProperty, n = { __proto__: null, default: e }; if (Object(e) !== e) return n; if (cache || "function" != typeof WeakMap || (cache = new WeakMap()), cache) { if (cache.has(e)) return cache.get(e); cache.set(e, n); } for (c in e) "default" !== c && {}.hasOwnProperty.call(e, c) && ((r = a && Object.getOwnPropertyDescriptor(e, c)) && (r.get || r.set) ? a(n, c, r) : n[c] = e[c]); return n; }
|
|
32
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
34
33
|
/**
|
|
35
34
|
* Koko Agent SDK runtime — dump / find / tap / swipe / pinch / type + cursor + confirm gate + task log.
|
|
36
35
|
*/
|
|
@@ -173,14 +172,18 @@ class KokoAgent {
|
|
|
173
172
|
if (this.activeSession || this.claimingCheckpoint) {
|
|
174
173
|
throw new Error((0, _index.runtimeText)('runtime.alreadyRunning', undefined, this.config?.locale));
|
|
175
174
|
}
|
|
176
|
-
const
|
|
177
|
-
|
|
175
|
+
const gateway = {
|
|
176
|
+
token: opts?.token ?? this.config.gateway?.token,
|
|
177
|
+
tokenProvider: opts?.tokenProvider ?? this.config.gateway?.tokenProvider,
|
|
178
|
+
appId: opts?.appId ?? this.config.gateway?.appId
|
|
179
|
+
};
|
|
180
|
+
if (!gateway.token && !gateway.tokenProvider) {
|
|
178
181
|
throw new Error(this.text('runtime.missingToken'));
|
|
179
182
|
}
|
|
180
183
|
const baseUrl = opts?.baseUrl ?? this.config.gateway?.baseUrl;
|
|
181
184
|
const locale = (0, _index.gatewayLocale)((0, _index.currentLocale)(opts?.locale ?? this.config.locale));
|
|
182
185
|
void _AccountStore.AccountStore.refresh({
|
|
183
|
-
|
|
186
|
+
...gateway,
|
|
184
187
|
baseUrl
|
|
185
188
|
});
|
|
186
189
|
const session = this.createSession({
|
|
@@ -191,7 +194,7 @@ class KokoAgent {
|
|
|
191
194
|
onAskUser: opts?.onAskUser,
|
|
192
195
|
onNarration: opts?.onNarration,
|
|
193
196
|
baseUrl,
|
|
194
|
-
|
|
197
|
+
...gateway
|
|
195
198
|
});
|
|
196
199
|
this.activeSession = session;
|
|
197
200
|
// An action can take down the context running it (locale/theme/RTL change,
|
|
@@ -206,7 +209,7 @@ class KokoAgent {
|
|
|
206
209
|
await _RunCheckpoint.RunCheckpoint.clear().catch(() => {});
|
|
207
210
|
if (this.activeSession === session) this.activeSession = null;
|
|
208
211
|
void _AccountStore.AccountStore.refresh({
|
|
209
|
-
|
|
212
|
+
...gateway,
|
|
210
213
|
baseUrl
|
|
211
214
|
});
|
|
212
215
|
}
|
|
@@ -263,11 +266,10 @@ class KokoAgent {
|
|
|
263
266
|
|
|
264
267
|
/** Refresh plan / quota / entitlements from gateway. */
|
|
265
268
|
async refreshAccount() {
|
|
266
|
-
const
|
|
267
|
-
if (!token) return null;
|
|
269
|
+
const gateway = this.config.gateway;
|
|
270
|
+
if (!gateway?.token && !gateway?.tokenProvider) return null;
|
|
268
271
|
return _AccountStore.AccountStore.refresh({
|
|
269
|
-
|
|
270
|
-
baseUrl: this.config.gateway?.baseUrl
|
|
272
|
+
...gateway
|
|
271
273
|
});
|
|
272
274
|
}
|
|
273
275
|
getAccount() {
|