aki-pro-max 2.3.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.
- package/.env.example +34 -0
- package/AICOWORKER-NATIVE-TOOLS.md +60 -0
- package/CLAUDE-LIVE-TOOLS-EVIDENCE.json +94 -0
- package/FULL-TRACE-EVIDENCE.json +109 -0
- package/ISSUE-1-REMOTE.json +1 -0
- package/ISSUE-2-POSTREVIEW.json +1 -0
- package/ISSUE-2-REMOTE.json +1 -0
- package/KEY-ROTATION-EVIDENCE.json +9 -0
- package/LICENSE +21 -0
- package/PMN-9ROUTER-FINAL.md +13 -0
- package/RAPID-BASIL-RECONCILIATION.json +10 -0
- package/README.md +232 -0
- package/RELEASE-ARCHIVE.json +15 -0
- package/SECURITY-RECONCILIATION.json +30 -0
- package/SECURITY.md +16 -0
- package/TEST-ISSUES12-FINAL.txt +0 -0
- package/TEST-ISSUES12-HARNESS.txt +0 -0
- package/VERIFICATION-REPORT.md +58 -0
- package/VERIFIER-ISSUES12-FINAL.txt +0 -0
- package/VERIFY-RELEASE-ISSUES12-FINAL.txt +0 -0
- package/VERIFY-RELEASE-ISSUES12-HARNESS.txt +0 -0
- package/bin/aki-pro-max.js +98 -0
- package/docs/ADMIN-GUI-CONTRACT.md +29 -0
- package/docs/ARCHITECTURE.md +109 -0
- package/docs/CAPABILITY-MATRIX.md +44 -0
- package/docs/CORRELATION-DESIGN.md +226 -0
- package/docs/FAIL-CLOSED-ISSUE-HARNESS.md +21 -0
- package/docs/WEB-SESSION-TRANSPORT-DESIGN.md +423 -0
- package/docs/assets/control-plane.jpg +0 -0
- package/gitleaks-report-all.json +1 -0
- package/gitleaks-report-latest.json +1 -0
- package/gitleaks-report.json +1 -0
- package/package.json +33 -0
- package/scripts/eventual-tool-loop.mjs +55 -0
- package/scripts/install-local.ps1 +35 -0
- package/scripts/live-eventual-multitool.mjs +18 -0
- package/scripts/upgrade-admin-v232.mjs +33 -0
- package/scripts/verify-issue-closure.mjs +81 -0
- package/scripts/verify-release.mjs +31 -0
- package/src/admin-auth.mjs +94 -0
- package/src/admin.mjs +133 -0
- package/src/canonical.mjs +23 -0
- package/src/config.mjs +88 -0
- package/src/correlation-store.mjs +120 -0
- package/src/errors.mjs +18 -0
- package/src/index.mjs +4 -0
- package/src/openai-response.mjs +72 -0
- package/src/openai.mjs +104 -0
- package/src/postman-events.mjs +43 -0
- package/src/postman-request.mjs +49 -0
- package/src/schema.mjs +35 -0
- package/src/server.mjs +73 -0
- package/src/session-store.mjs +48 -0
- package/src/sse.mjs +13 -0
- package/src/transport.mjs +90 -0
- package/src/web-session-events.mjs +358 -0
- package/src/web-session-request.mjs +280 -0
- package/test/9router-executor.integration.test.mjs +207 -0
- package/test/admin-auth.test.mjs +47 -0
- package/test/admin.test.mjs +68 -0
- package/test/config.test.mjs +14 -0
- package/test/contract.test.mjs +14 -0
- package/test/correlation-store.test.mjs +19 -0
- package/test/correlation.integration.test.mjs +48 -0
- package/test/eventual-tool-loop.test.mjs +42 -0
- package/test/fixtures/text.json +8 -0
- package/test/fixtures/tool.json +7 -0
- package/test/fixtures/web-session-observed-done.json +12 -0
- package/test/fixtures/web-session-tool-fragments.json +14 -0
- package/test/full-ingress/alias-loader.mjs +22 -0
- package/test/full-ingress/run-full-ingress.mjs +207 -0
- package/test/full-ingress/seed-9router.mjs +36 -0
- package/test/helpers.mjs +9 -0
- package/test/issue-closure-harness.test.mjs +49 -0
- package/test/model-thinking.test.mjs +20 -0
- package/test/protocol.test.mjs +16 -0
- package/test/request.test.mjs +10 -0
- package/test/session-store.test.mjs +20 -0
- package/test/web-session-builder.test.mjs +124 -0
- package/test/web-session-events.test.mjs +241 -0
- package/test/web-session-integration.test.mjs +103 -0
- package/test/web-session-tools.test.mjs +52 -0
- package/version.json +8 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
# Web-session transport design (offline, implementation-ready)
|
|
2
|
+
|
|
3
|
+
Date: 2026-09-12 (Asia/Bangkok)
|
|
4
|
+
Status: design only; no Postman request, browser access, credential discovery, configuration change, or implementation was performed.
|
|
5
|
+
|
|
6
|
+
## 1. Decision
|
|
7
|
+
|
|
8
|
+
Add a **separate upstream transport strategy**, `web_session`, beside the current `access_token` strategy. Do not replace, auto-select, or fail over between them.
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
POSTMAN_TRANSPORT_STRATEGY = access_token | web_session
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The first implementation slice is deliberately narrow:
|
|
15
|
+
|
|
16
|
+
- keep `/v1/chat/completions`, `/v1/models`, OpenAI normalization, response shaping, limits, concurrency, and downstream bearer authentication unchanged;
|
|
17
|
+
- add only an owner-configured `postman.sid` transport to `POST https://{subdomain}.postman.co/_gw/chat`;
|
|
18
|
+
- support a fresh, non-tool, text request only;
|
|
19
|
+
- do not create or accept an invented caller session ID;
|
|
20
|
+
- do not enable conversation reuse, tool advertising, tool-result continuation, metadata discovery, account failover, user-setting mutation, or challenge handling in this slice;
|
|
21
|
+
- add no runtime dependency.
|
|
22
|
+
|
|
23
|
+
This is an original local design informed by static source observations. The two inspected repositories have no demonstrated reuse license, so their code is not to be copied.
|
|
24
|
+
|
|
25
|
+
## 2. Evidence boundary and exact references
|
|
26
|
+
|
|
27
|
+
### Pinned public sources
|
|
28
|
+
|
|
29
|
+
- `TTGREEN/postman2api-full@0c3fd893767655aa10afd25135f20ed344b447d8`
|
|
30
|
+
- `src/provider/postman.ts:82`: `CHAT_ENDPOINT = "/_gw/chat"`.
|
|
31
|
+
- `src/provider/postman.ts:336-350`: `buildHeaders` sends `Cookie: postman.sid=...`, JSON content type, SSE accept, `x-pstmn-req-service`, a browser User-Agent, and same-subdomain `Origin`/`Referer`.
|
|
32
|
+
- `src/provider/postman.ts:813-971`: request body builder uses `platform: "WEB"`, `mandatoryContext.workspaceId`, and a selected model in `devModeOptions`; it also contains much broader tool/settings behavior that is not adopted here.
|
|
33
|
+
- `src/provider/postman.ts:1584+`: streaming path reads response-body chunks rather than waiting for an upstream `[DONE]` token.
|
|
34
|
+
- `src/provider/sse-stream.ts:89-143`: parses JSON only from `data:` lines and recognizes `usage`, `conversation`, `textChunk`, `thinkingChunk`, `failure/error`, `toolCallChunk`, `loopApprovalChunk`, plus ignored informational events.
|
|
35
|
+
- `src/provider/sse-stream.ts:145-149`: `finish()` synthesizes `stop` or `tool_calls` when the reader is finished; this is implementation behavior, not proof of a native upstream terminal event.
|
|
36
|
+
- `gfushilu07-arch/postman2api@62aee6d2d87841ebcd42618c4ac5d9cf22f4dc36`
|
|
37
|
+
- `src/provider/postman.ts:44`: `CHAT_ENDPOINT = "/_gw/chat"`.
|
|
38
|
+
- `src/provider/postman.ts:209-222`: the same cookie, SSE, service, User-Agent, Origin, and Referer header pattern.
|
|
39
|
+
- `src/provider/postman.ts:318-407`: body builder uses `platform: "WEB"`, `mandatoryContext.workspaceId`, static catalog metadata, and selected-model options.
|
|
40
|
+
- `src/provider/postman.ts:692+`, especially body-read `done` branches around `:763` and `:915`: EOF drives reader finalization and downstream `[DONE]` emission.
|
|
41
|
+
- `src/provider/sse-stream.ts:86-133`: parses the same main event families.
|
|
42
|
+
- `src/provider/sse-stream.ts:135-139`: also synthesizes finish on reader completion.
|
|
43
|
+
- `src/provider/sse-stream.ts:286+`: observes an upstream model label and can reject a mismatch against the requested/selected model.
|
|
44
|
+
|
|
45
|
+
These sources demonstrate a plausible cookie-authenticated direct HTTP route. They do **not** prove that arbitrary EOF is a successful terminal signal, that PMAK exchanges into this session, that every account receives the same event shapes, or that the static model/catalog values are authoritative now.
|
|
46
|
+
|
|
47
|
+
### Existing local provider
|
|
48
|
+
|
|
49
|
+
- `src/config.mjs`
|
|
50
|
+
- currently requires `POSTMAN_ACCESS_TOKEN`, exact HTTPS `POSTMAN_ORIGIN`, workspace ID, explicit models, bounded integer settings, and visible-ASCII catalog tokens;
|
|
51
|
+
- `upstreamUrl()` verifies the constructed URL retains the configured origin.
|
|
52
|
+
- `src/postman-request.mjs`
|
|
53
|
+
- currently sends `x-access-token`, `redirect: 'error'`, and an operator-owned request envelope;
|
|
54
|
+
- request-body fields cannot override configured catalog metadata.
|
|
55
|
+
- `src/transport.mjs`
|
|
56
|
+
- currently requires exact upstream `data: [DONE]`; EOF first is `upstream_incomplete_stream`.
|
|
57
|
+
- `src/postman-events.mjs`
|
|
58
|
+
- validates bounded event shapes and maps failures to redacted stable errors.
|
|
59
|
+
- `src/correlation-store.mjs` and `src/server.mjs`
|
|
60
|
+
- correlation is admitted only after trusted upstream conversation/tool IDs and binds principal, workspace, model, transcript, tools, and catalog metadata;
|
|
61
|
+
- uncertain dispatched continuations are not retried automatically.
|
|
62
|
+
- `test/9router-executor.integration.test.mjs`
|
|
63
|
+
- seven actual pinned-9router tests exercise exact facade URL/auth behavior, JSON, SSE, native tool roundtrip/replay, errors, and abort propagation.
|
|
64
|
+
|
|
65
|
+
## 3. Configuration contract
|
|
66
|
+
|
|
67
|
+
### 3.1 Strategy and mutually exclusive secrets
|
|
68
|
+
|
|
69
|
+
The strategy is explicit and closed:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
POSTMAN_TRANSPORT_STRATEGY=access_token
|
|
73
|
+
requires: POSTMAN_ACCESS_TOKEN, POSTMAN_ORIGIN, POSTMAN_WORKSPACE_ID
|
|
74
|
+
forbids: POSTMAN_SESSION_COOKIE, POSTMAN_WORKSPACE_SUBDOMAIN
|
|
75
|
+
|
|
76
|
+
POSTMAN_TRANSPORT_STRATEGY=web_session
|
|
77
|
+
requires: POSTMAN_SESSION_COOKIE, POSTMAN_WORKSPACE_SUBDOMAIN, POSTMAN_WORKSPACE_ID
|
|
78
|
+
forbids: POSTMAN_ACCESS_TOKEN, POSTMAN_ORIGIN, POSTMAN_CHAT_PATH
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Concrete choices:
|
|
82
|
+
|
|
83
|
+
1. No default strategy after this feature is enabled. An explicit value prevents an old deployment with a newly injected secret from silently changing behavior.
|
|
84
|
+
2. Both secrets configured is a startup error, even if one appears unused.
|
|
85
|
+
3. A secret belonging to the other strategy is a startup error, not ignored configuration.
|
|
86
|
+
4. The cookie value is the **value of `postman.sid` only**, not an entire `Cookie` header. Suggested field name: `POSTMAN_SESSION_COOKIE`; do not name it a generic token.
|
|
87
|
+
5. The cookie is explicitly owner-provided. The service does not read browser storage, handshake responses, files outside its configuration mechanism, or repository examples.
|
|
88
|
+
6. Secret bounds: string, 1-4096 bytes UTF-8, visible ASCII `0x21-0x7e`, and reject semicolon, comma, backslash, whitespace, CR, LF, NUL, and all control bytes. The builder adds the single literal prefix `postman.sid=`.
|
|
89
|
+
7. Subdomain bounds: lowercase ASCII DNS label, 1-63 characters, regex `^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$`; reject dots, percent encoding, Unicode, underscores, leading/trailing hyphen, URL syntax, CR/LF, and whitespace.
|
|
90
|
+
8. Workspace ID remains an opaque configured identifier under the existing visible-ASCII/no-whitespace discipline, with an explicit upper bound (recommended 256 bytes). It is never learned from request content.
|
|
91
|
+
9. Continue existing configuration-source precedence; do not add a request-body or header override for strategy, cookie, subdomain, origin, path, workspace, or catalog metadata.
|
|
92
|
+
|
|
93
|
+
The downstream `PROVIDER_API_KEY` remains a distinct facade credential and is never forwarded upstream.
|
|
94
|
+
|
|
95
|
+
### 3.2 Exact web-session URL
|
|
96
|
+
|
|
97
|
+
For `web_session`, the URL is constructed internally and cannot be operator-replaced:
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
https://${POSTMAN_WORKSPACE_SUBDOMAIN}.postman.co/_gw/chat
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Validation after construction must assert all of:
|
|
104
|
+
|
|
105
|
+
- protocol exactly `https:`;
|
|
106
|
+
- username/password empty;
|
|
107
|
+
- hostname exactly `${validatedSubdomain}.postman.co`;
|
|
108
|
+
- hostname has the `.postman.co` suffix and contains exactly the expected validated left label;
|
|
109
|
+
- explicit/default port resolves to 443; reject a configured port because none is accepted as input;
|
|
110
|
+
- pathname exactly `/_gw/chat`;
|
|
111
|
+
- query and fragment empty;
|
|
112
|
+
- serialized origin exactly `https://${validatedSubdomain}.postman.co`.
|
|
113
|
+
|
|
114
|
+
Use `redirect: 'error'`. Do not manually follow redirects. Any 3xx is a bounded upstream error, so the cookie cannot be replayed to a redirected host or path.
|
|
115
|
+
|
|
116
|
+
The existing `access_token` exact-origin/path construction remains unchanged.
|
|
117
|
+
|
|
118
|
+
## 4. Header policy
|
|
119
|
+
|
|
120
|
+
### 4.1 `web_session` request headers
|
|
121
|
+
|
|
122
|
+
Build a fresh allowlisted header object:
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
content-type: application/json
|
|
126
|
+
accept: text/event-stream
|
|
127
|
+
cookie: postman.sid=<owner-provided value>
|
|
128
|
+
x-pstmn-req-service: agent-mode-service
|
|
129
|
+
origin: https://<subdomain>.postman.co
|
|
130
|
+
referer: https://<subdomain>.postman.co/
|
|
131
|
+
user-agent: <operator-configured bounded value or one documented fixed compatibility value>
|
|
132
|
+
[x-app-version: <operator-configured bounded value only if explicitly set>]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Decisions:
|
|
136
|
+
|
|
137
|
+
- Never send `x-access-token`, downstream `Authorization`, `PROVIDER_API_KEY`, host headers, proxy headers, or arbitrary operator headers on this strategy.
|
|
138
|
+
- Never import caller headers wholesale.
|
|
139
|
+
- `Origin` and `Referer` are derived from the validated subdomain and cannot be independently configured.
|
|
140
|
+
- Every configured header value uses bounded visible ASCII and rejects CR/LF/NUL. Header names are constants.
|
|
141
|
+
- Do not claim a browser identity dynamically. If compatibility requires User-Agent, use one bounded, documented constant or a separately validated operator string; it grants no browser automation behavior.
|
|
142
|
+
- `x-app-version` is optional and operator-pinned. Do not fetch or infer it in the transport path.
|
|
143
|
+
|
|
144
|
+
### 4.2 Redaction
|
|
145
|
+
|
|
146
|
+
Maintain a strategy-level secret set containing the active upstream secret and downstream facade key. Error/log serialization must redact:
|
|
147
|
+
|
|
148
|
+
- exact secret values and any `postman.sid=...` sequence;
|
|
149
|
+
- `Cookie`, `Authorization`, and `x-access-token` header values case-insensitively;
|
|
150
|
+
- URL userinfo (although validation forbids it);
|
|
151
|
+
- upstream response bodies and event messages by default.
|
|
152
|
+
|
|
153
|
+
Log only request ID, strategy name, HTTP status, stable error code, retry-after after bounded parsing, event type, byte counts, and boolean terminal/model observations. Do not log request bodies, response text, cookie length, workspace ID, conversation IDs, tool IDs, or model content in generic error records.
|
|
154
|
+
|
|
155
|
+
## 5. Request body: smallest text-first profile
|
|
156
|
+
|
|
157
|
+
Create a strategy-specific request builder rather than conditionally mutating the current envelope.
|
|
158
|
+
|
|
159
|
+
For the first slice, accept only normalized requests satisfying:
|
|
160
|
+
|
|
161
|
+
- no tools;
|
|
162
|
+
- no trailing tool result;
|
|
163
|
+
- no parallel-tool option;
|
|
164
|
+
- no request field purporting to supply session/conversation/group IDs;
|
|
165
|
+
- text-capable messages already accepted by current ingress;
|
|
166
|
+
- model is in the existing operator allowlist.
|
|
167
|
+
|
|
168
|
+
Suggested web-session body:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"input": {
|
|
173
|
+
"chatType": "USER_QUERY",
|
|
174
|
+
"query": "<bounded current local transcript representation>",
|
|
175
|
+
"conversationId": null,
|
|
176
|
+
"product": "workspace_v12",
|
|
177
|
+
"useCase": null,
|
|
178
|
+
"agent": null,
|
|
179
|
+
"startedFrom": "CHAT_INPUT"
|
|
180
|
+
},
|
|
181
|
+
"platform": "WEB",
|
|
182
|
+
"clientTools": { "native": [] },
|
|
183
|
+
"clientKBTerms": { "native": [] },
|
|
184
|
+
"mandatoryContext": { "workspaceId": "<configured>" },
|
|
185
|
+
"selectedContext": [],
|
|
186
|
+
"backgroundContext": [],
|
|
187
|
+
"devModeOptions": {
|
|
188
|
+
"selectedModel": "<mapped operator-pinned upstream value>",
|
|
189
|
+
"isParallelToolCallingSupported": false,
|
|
190
|
+
"autoRun": false,
|
|
191
|
+
"supportsAskUser": false,
|
|
192
|
+
"supportsActionRecommendations": false,
|
|
193
|
+
"useThinkingModeIfAvailable": false
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Required decisions before implementation:
|
|
199
|
+
|
|
200
|
+
- `query` uses the current deterministic transcript serializer or a new bounded text serializer; it must not fabricate the source repositories' private `_sessionId`, seeding acknowledgements, priming turns, or tool groups.
|
|
201
|
+
- Keep existing `MAX_BODY_BYTES`; serialize once, measure UTF-8 bytes, and reject before fetch.
|
|
202
|
+
- Request fields are constants/config-derived/normalized-request-derived only. Unknown client body fields cannot override any upstream field.
|
|
203
|
+
- `selectedModel` needs an explicit operator-owned mapping if public facade IDs differ from the upstream enum. Do not copy a static model catalog from either repository and do not auto-discover entitlement.
|
|
204
|
+
- Do not send `userSettings.ai_user_agent_mode`, enable-web flags, loop-approval automation, available skills, third-party tool groups, native hashes, or native terms hashes in this text-only profile.
|
|
205
|
+
|
|
206
|
+
If the minimal no-tools body proves insufficient later, any catalog/settings addition is a separately reviewed profile revision with fixtures; it is not an automatic fallback.
|
|
207
|
+
|
|
208
|
+
## 6. Stream and completion state machine
|
|
209
|
+
|
|
210
|
+
### 6.1 Preserve legacy behavior
|
|
211
|
+
|
|
212
|
+
`access_token` keeps its current rule exactly:
|
|
213
|
+
|
|
214
|
+
```text
|
|
215
|
+
exact SSE data payload [DONE] -> complete
|
|
216
|
+
EOF before [DONE] -> upstream_incomplete_stream
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
This preserves the 54-test path and its protection against false success.
|
|
220
|
+
|
|
221
|
+
### 6.2 Separate web-session decoder
|
|
222
|
+
|
|
223
|
+
The pinned web-session sources parse JSON `data:` lines and finalize on body EOF, then synthesize downstream finish. They do not demonstrate upstream `[DONE]`. Therefore `web_session` must not reuse a universal `require [DONE]` rule, but it also must not equate every EOF with success.
|
|
224
|
+
|
|
225
|
+
Track these independent facts:
|
|
226
|
+
|
|
227
|
+
```text
|
|
228
|
+
saw_valid_event
|
|
229
|
+
saw_meaningful_output // text or a later-supported complete tool call
|
|
230
|
+
saw_conversation_id
|
|
231
|
+
saw_usage
|
|
232
|
+
saw_failure
|
|
233
|
+
saw_native_terminal // only an explicitly enumerated source/live-fixture-backed event
|
|
234
|
+
transport_eof
|
|
235
|
+
parser_buffer_complete // no partial SSE frame / UTF-8 / JSON payload
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Event mapping for the text-first slice:
|
|
239
|
+
|
|
240
|
+
- `conversation` with a bounded non-empty ID: record internally as an observation only; do not expose it or persist it yet.
|
|
241
|
+
- `textChunk.data.textContent`: bounded text delta.
|
|
242
|
+
- `usage`: bounded known numeric fields; unknown credit states remain fail-closed under current error policy.
|
|
243
|
+
- `thinkingChunk`, `thinkingComplete`, `usageUpdate`, `ping`, `info`, `planningChunk`, `progressUpdate`, `todoChunk`, `streamingFormat`: ignore only after a valid bounded JSON object and known event name.
|
|
244
|
+
- `failure`, `error`, failure result, or known error type: terminal failure; never emit successful finish.
|
|
245
|
+
- In the first text-only slice, `toolCallChunk` and `loopApprovalChunk` were explicit unsupported-profile errors. The later bounded offline tool slice supports source-backed `toolCallChunk` forms as documented by `WEB-SESSION-TOOLS-INTEGRATION.md`; loop approval remains unsupported.
|
|
246
|
+
- unknown event type: current fail-closed `upstream_unverified_event_shape` behavior.
|
|
247
|
+
|
|
248
|
+
### 6.3 Terminal outcome rules
|
|
249
|
+
|
|
250
|
+
The decoder exposes a transport outcome, not an OpenAI finish reason:
|
|
251
|
+
|
|
252
|
+
1. `failure` observed at any point -> `FAILED`; partial downstream text may already exist, but do not emit `finish_reason` or `[DONE]`.
|
|
253
|
+
2. Exact `[DONE]` observed, if the web route happens to emit it -> `COMPLETE_EXPLICIT` after validating no pending partial frame.
|
|
254
|
+
3. A separately enumerated native terminal event supported by a pinned/live sanitized fixture -> `COMPLETE_EXPLICIT`. **No such event is proven by the two current sources**, so the initial enum is empty.
|
|
255
|
+
4. EOF with partial UTF-8, SSE frame, or JSON -> `INCOMPLETE_TRUNCATED`.
|
|
256
|
+
5. EOF after valid meaningful output but without explicit terminal evidence -> `AMBIGUOUS_EOF`.
|
|
257
|
+
6. EOF after only metadata/heartbeats or no valid event -> `INCOMPLETE_EMPTY`.
|
|
258
|
+
7. timeout, abort, reader error, or socket error -> existing abort/timeout/upstream error classification, never completion.
|
|
259
|
+
|
|
260
|
+
Initial production behavior for `AMBIGUOUS_EOF` is fail-closed: stable `upstream_completion_ambiguous`, no successful OpenAI finish, and no downstream `[DONE]`. This avoids both bad universals: forcing upstream `[DONE]` and accepting a truncated EOF as success.
|
|
261
|
+
|
|
262
|
+
A later owner-approved, content-discarding protocol observation may justify one of two bounded changes:
|
|
263
|
+
|
|
264
|
+
- identify a real native terminal event and add it to `saw_native_terminal`; or
|
|
265
|
+
- establish a precise EOF completion contract (for example, a required ordered set of final event categories and a complete frame) and version that rule with fixtures.
|
|
266
|
+
|
|
267
|
+
Do not loosen EOF semantics based only on the repositories' `finish()` helper.
|
|
268
|
+
|
|
269
|
+
### 6.4 Downstream finish emission
|
|
270
|
+
|
|
271
|
+
`server.mjs` emits OpenAI `finish_reason` and downstream `[DONE]` only after `canonicalEvents` yields a successful canonical `finish`. A web-session ambiguous/truncated/error outcome yields no canonical finish, matching the existing post-header failure behavior tested through actual 9router.
|
|
272
|
+
|
|
273
|
+
## 7. Requested model versus observed model
|
|
274
|
+
|
|
275
|
+
Represent identity as two fields internally:
|
|
276
|
+
|
|
277
|
+
```text
|
|
278
|
+
requestedModel = validated public model ID from the caller
|
|
279
|
+
selectedUpstreamModel = operator-pinned mapping used in the request
|
|
280
|
+
observedUpstreamModel = bounded label parsed from supported event metadata, or null
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Rules:
|
|
284
|
+
|
|
285
|
+
- OpenAI response `model` remains the requested public ID for compatibility, but that field is not evidence of the actual upstream model.
|
|
286
|
+
- Record `observedUpstreamModel` only when a recognized event contains a bounded supported model field. Do not manufacture it from the request.
|
|
287
|
+
- If no model is observed, mark internal verification state `unobserved`; do not state that the upstream model was verified.
|
|
288
|
+
- Add an operator mapping/alias table only if needed. Comparison must be exact against the configured allowed observed labels for that public ID; do not infer fuzzy aliases.
|
|
289
|
+
- Default mismatch behavior: fail before successful finish with `upstream_model_mismatch`; never silently accept replacement.
|
|
290
|
+
- If output arrived before a mismatch was observed, terminate as post-header failure without `[DONE]`.
|
|
291
|
+
- Logs contain only `model_observed: true|false` and `model_match: true|false|unknown` unless an explicitly approved diagnostic mode permits bounded labels.
|
|
292
|
+
|
|
293
|
+
## 8. Correlation and sessions
|
|
294
|
+
|
|
295
|
+
Do not reuse current correlation merely because a `conversation` event appears.
|
|
296
|
+
|
|
297
|
+
Current correlation is valid for one purpose: native tool-result continuation after the service has admitted trusted upstream tool IDs, conversation ID, group information, transcript, tools binding, catalog metadata, principal, workspace, and model. The text-first `web_session` slice has no tools, so it does not admit correlation records.
|
|
298
|
+
|
|
299
|
+
Ordinary conversation continuation is deferred because it needs a different store/key contract. It must not:
|
|
300
|
+
|
|
301
|
+
- accept a fabricated `_sessionId` or upstream `conversationId` from the OpenAI body;
|
|
302
|
+
- expose upstream IDs downstream;
|
|
303
|
+
- conflate a 9router request ID with an upstream conversation;
|
|
304
|
+
- reuse the tool-call store without a separately defined principal-scoped session anchor and replay semantics.
|
|
305
|
+
|
|
306
|
+
A future session design may reuse the existing principal derivation, workspace/model binding, TTL/capacity patterns, and uncertain-dispatch state machine, but it requires its own record type and tests.
|
|
307
|
+
|
|
308
|
+
## 9. Errors, expiry, and prohibited automatic behavior
|
|
309
|
+
|
|
310
|
+
Stable web-session errors should distinguish:
|
|
311
|
+
|
|
312
|
+
- invalid startup configuration;
|
|
313
|
+
- `upstream_web_session_auth_failed` for 401/403 (cookie absent, expired, account/workspace mismatch, or entitlement unknown);
|
|
314
|
+
- `upstream_redirect_rejected` for 3xx;
|
|
315
|
+
- current rate-limit/quota/status classes without reflecting response bodies;
|
|
316
|
+
- `upstream_content_type_error`;
|
|
317
|
+
- malformed/oversized SSE and output;
|
|
318
|
+
- `upstream_completion_ambiguous`, `upstream_incomplete_stream`, and `upstream_empty_stream`;
|
|
319
|
+
- `upstream_model_mismatch`;
|
|
320
|
+
- unsupported text-profile events (`toolCallChunk`, `loopApprovalChunk`).
|
|
321
|
+
|
|
322
|
+
There is no silent refresh, retry storm, credential exchange, account rotation, strategy failover, browser login, CAPTCHA/Turnstile action, challenge workaround, or automatic change to `ai_user_agent_mode`. A 401/403 fails the request and tells the owner, through a redacted stable message, that the configured web session may need manual replacement.
|
|
323
|
+
|
|
324
|
+
Retries, if ever added, must retain the current dispatch-uncertainty discipline; no automatic retry after a request may have reached upstream.
|
|
325
|
+
|
|
326
|
+
## 10. Optional future metadata discovery (separate permission)
|
|
327
|
+
|
|
328
|
+
TTGREEN contains read-only HTML/asset inspection intended to discover app/native catalog metadata. This is not part of transport setup or fallback.
|
|
329
|
+
|
|
330
|
+
If requested later, design it as a separate read-only command or offline fixture importer with all of:
|
|
331
|
+
|
|
332
|
+
- explicit owner permission for that operation;
|
|
333
|
+
- same validated host and owner-provided cookie;
|
|
334
|
+
- GET only, redirect denied, strict byte/content-type/asset-count/time bounds;
|
|
335
|
+
- no update of user settings and no inference request;
|
|
336
|
+
- output is a reviewable candidate metadata artifact, never an automatic config mutation;
|
|
337
|
+
- operator must pin accepted values before the chat transport uses them;
|
|
338
|
+
- failure leaves existing pins unchanged.
|
|
339
|
+
|
|
340
|
+
## 11. Offline test matrix
|
|
341
|
+
|
|
342
|
+
All tests inject `fetchImpl`; no test uses a real credential, DNS, Postman endpoint, browser, model, recorder, or quota.
|
|
343
|
+
|
|
344
|
+
| Area | Required cases | Expected proof |
|
|
345
|
+
|---|---|---|
|
|
346
|
+
| Strategy enum | missing, unknown, `access_token`, `web_session` | only two explicit values accepted; no implicit branch |
|
|
347
|
+
| Secret exclusivity | neither, both, wrong-strategy secret, valid one-secret sets | startup fails except exact valid set |
|
|
348
|
+
| Cookie validation | min/max, >4096 bytes, CR/LF, NUL, whitespace, semicolon/comma/backslash, non-string | no header injection or whole-cookie input |
|
|
349
|
+
| Subdomain validation | valid hyphen label; dot, Unicode, `%`, URL, `@`, slash, port, leading/trailing hyphen, >63 | final host is exactly one label under `.postman.co` |
|
|
350
|
+
| URL | constructed exact HTTPS URL; path/query/fragment absent; 3xx fixture | exact `https://label.postman.co/_gw/chat`; redirect rejected |
|
|
351
|
+
| Header isolation | capture both strategy requests | cookie path has no x-access/downstream auth; token path has no cookie; Origin/Referer derived |
|
|
352
|
+
| Body override | caller supplies strategy/origin/cookie/workspace/platform/session-like fields | captured upstream body uses only config/constants/normalized input |
|
|
353
|
+
| Text profile | simple text, tools, tool result, unsupported options | text accepted; non-text profile rejected before fetch |
|
|
354
|
+
| Size bounds | body/event/output boundary and +1 | existing byte limits apply before unbounded allocation/send |
|
|
355
|
+
| Event shapes | conversation, text, usage, ignored known events, malformed JSON, unknown event | conservative canonical mapping and fail-closed unknowns |
|
|
356
|
+
| Failure redaction | upstream event/body includes fake Cookie/Bearer/session values | stable error contains none of them |
|
|
357
|
+
| Completion: legacy | exact `[DONE]`, EOF first, data after done | current tests unchanged |
|
|
358
|
+
| Completion: web | explicit `[DONE]`; EOF after text; EOF mid-frame; empty EOF; failure after partial text; timeout/abort | only explicit terminal succeeds initially; ambiguous/truncated/error emits no finish/DONE |
|
|
359
|
+
| Unsupported native event | toolCallChunk, loopApprovalChunk | stable unsupported-profile failure; no fabricated call/group/session |
|
|
360
|
+
| Model observation | absent, exact allowed observed label, mismatch before output, mismatch after partial output, overlong label | unobserved is honest; mismatch never finishes successfully |
|
|
361
|
+
| Correlation regression | existing admission/claim/replay/uncertain suites | unchanged access-token tool continuation behavior |
|
|
362
|
+
| Existing 54-test path | add `POSTMAN_TRANSPORT_STRATEGY=access_token` to shared legacy config fixtures, then run every existing behavioral case | only fixture configuration changes; zero changed legacy request/response expectations |
|
|
363
|
+
| Actual 9router JSON | add a second fixture config and capture URL/headers/body | DefaultExecutor still calls facade; facade selects exact web-session upstream request |
|
|
364
|
+
| Actual 9router SSE explicit finish | web fixture includes terminal evidence | router receives content, stop, and one downstream `[DONE]` |
|
|
365
|
+
| Actual 9router ambiguous EOF | web fixture ends after partial text without terminal evidence | post-header stable error, no stop, no `[DONE]` |
|
|
366
|
+
| Actual 9router auth/model/abort | 401/403, mismatch event, client abort | redacted stable errors and abort propagation; no fallback |
|
|
367
|
+
|
|
368
|
+
The legacy suite should remain a required separate command/path, not be rewritten to parameterize every old assertion through the new strategy. Add focused web-session files so regressions identify the responsible strategy.
|
|
369
|
+
|
|
370
|
+
## 12. Estimated implementation impact
|
|
371
|
+
|
|
372
|
+
Smallest credible implementation: **5 existing files modified, 3 new test/design files**, approximately **250-450 production lines and 300-500 test lines** depending on whether model mapping is placed in config or a small module.
|
|
373
|
+
|
|
374
|
+
Expected files:
|
|
375
|
+
|
|
376
|
+
1. `src/config.mjs` — strategy enum, mutually exclusive fields, cookie/subdomain validation, derived web endpoint settings.
|
|
377
|
+
2. `src/postman-request.mjs` — dispatch to separate request builders; preserve legacy builder byte-for-byte where practical.
|
|
378
|
+
3. `src/transport.mjs` — choose legacy versus web completion policy.
|
|
379
|
+
4. `src/postman-events.mjs` — bounded observed-model extraction and explicit unsupported text-profile events (or a new `src/web-session-events.mjs` to reduce legacy risk).
|
|
380
|
+
5. `src/server.mjs` — pass strategy/model-observation context without changing ingress routes/correlation admission.
|
|
381
|
+
6. `test/web-session-config.test.mjs` — new.
|
|
382
|
+
7. `test/web-session-transport.test.mjs` — new.
|
|
383
|
+
8. `test/9router-executor.integration.test.mjs` — add focused web-session cases; retain all current cases.
|
|
384
|
+
|
|
385
|
+
Optional separation that improves reviewability: add `src/web-session-request.mjs` and `src/web-session-events.mjs`, increasing new files while reducing conditional changes in legacy modules. No package change is required.
|
|
386
|
+
|
|
387
|
+
README/example environment updates should wait until the implementation exists and tests pass; they must state that `postman.sid` is manually owner-supplied and session expiry is expected.
|
|
388
|
+
|
|
389
|
+
## 13. Smallest safe next offline implementation
|
|
390
|
+
|
|
391
|
+
Implement only configuration and pure request construction first:
|
|
392
|
+
|
|
393
|
+
1. add `POSTMAN_TRANSPORT_STRATEGY` and strict mutual-exclusion/validation;
|
|
394
|
+
2. add a pure `buildWebSessionTextRequest()` returning exact URL/init/body for a normalized no-tools request;
|
|
395
|
+
3. add offline capture tests proving URL, redirect denial, header isolation, CRLF/bounds, body non-override, and unchanged legacy request snapshots;
|
|
396
|
+
4. do **not** wire `web_session` into `server.mjs` or make any live-capable request yet.
|
|
397
|
+
|
|
398
|
+
That slice is reviewable without a credential and cannot accidentally turn the service into a live web-session client. The next offline slice would add the separate event/completion state machine and actual-9router injected fixtures. Only after both slices pass and the owner separately approves a minimal live check should the route be considered for runtime use.
|
|
399
|
+
|
|
400
|
+
## 14. Required owner inputs and unresolved protocol facts
|
|
401
|
+
|
|
402
|
+
Inputs required before any future live validation:
|
|
403
|
+
|
|
404
|
+
- explicit approval for one bounded inference request;
|
|
405
|
+
- owner-supplied current `postman.sid` value (not harvested);
|
|
406
|
+
- exact workspace subdomain label;
|
|
407
|
+
- workspace/team ID used by `mandatoryContext.workspaceId`;
|
|
408
|
+
- one entitled public model ID plus its operator-pinned upstream selected-model value and allowed observed label(s);
|
|
409
|
+
- decision whether an operator-pinned User-Agent and `x-app-version` are required for the first check.
|
|
410
|
+
|
|
411
|
+
Unknowns that this design does not pretend to solve:
|
|
412
|
+
|
|
413
|
+
- whether the session is currently authorized for AI and the chosen workspace/model;
|
|
414
|
+
- exact upstream normal terminal evidence, if any, beyond connection EOF;
|
|
415
|
+
- whether `x-app-version` is required for text-only requests now;
|
|
416
|
+
- the current minimal body accepted across accounts;
|
|
417
|
+
- actual model-label locations/aliases for the chosen model;
|
|
418
|
+
- session expiry duration and revocation behavior;
|
|
419
|
+
- whether native catalog metadata is required only for tools or also for some text requests;
|
|
420
|
+
- grouped tool continuation shape (the pinned sources differ);
|
|
421
|
+
- ordinary multi-turn conversation persistence semantics for this facade.
|
|
422
|
+
|
|
423
|
+
The implementation must keep these as explicit unknowns until bounded evidence resolves them.
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aki-pro-max",
|
|
3
|
+
"version": "2.3.3",
|
|
4
|
+
"description": "Local-first OpenAI-compatible gateway and control plane for an authorized Postman web session.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"aki-pro-max": "bin/aki-pro-max.js",
|
|
9
|
+
"akimcp": "bin/aki-pro-max.js"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=22.15"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node --env-file-if-exists=.env src/index.mjs",
|
|
16
|
+
"test": "node --test test/*.test.mjs",
|
|
17
|
+
"test:contract": "node --test test/contract.test.mjs",
|
|
18
|
+
"verify:release": "node scripts/verify-release.mjs"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/khangtudo/aki-pro-max.git"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"openai-compatible",
|
|
26
|
+
"gateway",
|
|
27
|
+
"postman",
|
|
28
|
+
"local-first",
|
|
29
|
+
"tool-calling",
|
|
30
|
+
"mcp",
|
|
31
|
+
"cli"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const object = value => value && typeof value === 'object' && !Array.isArray(value);
|
|
2
|
+
|
|
3
|
+
function stable(value) {
|
|
4
|
+
if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
|
|
5
|
+
if (object(value)) return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stable(value[key])}`).join(',')}}`;
|
|
6
|
+
return JSON.stringify(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function toolCall(call) {
|
|
10
|
+
if (!object(call) || call.type !== 'function' || typeof call.id !== 'string' || !call.id || !object(call.function) || typeof call.function.name !== 'string' || typeof call.function.arguments !== 'string') throw Object.assign(new Error('invalid_tool_call'), { code: 'invalid_tool_call' });
|
|
11
|
+
let args;
|
|
12
|
+
try { args = JSON.parse(call.function.arguments || '{}'); }
|
|
13
|
+
catch { throw Object.assign(new Error('invalid_tool_arguments'), { code: 'invalid_tool_arguments' }); }
|
|
14
|
+
if (!object(args)) throw Object.assign(new Error('invalid_tool_arguments'), { code: 'invalid_tool_arguments' });
|
|
15
|
+
return { id: call.id, name: call.function.name, args, signature: `${call.function.name}:${stable(args)}` };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function runEventualToolLoop({ request, initialBody, executeTool, requiredTools, maxTurns = 3, maxDispatches = 4, validateFinal = () => true }) {
|
|
19
|
+
if (typeof request !== 'function' || typeof executeTool !== 'function' || !object(initialBody) || !Array.isArray(initialBody.messages) || !Array.isArray(requiredTools) || !requiredTools.length) throw new TypeError('invalid_eventual_tool_loop_options');
|
|
20
|
+
const required = new Set(requiredTools);
|
|
21
|
+
const executed = new Map();
|
|
22
|
+
const messages = structuredClone(initialBody.messages);
|
|
23
|
+
const trace = [];
|
|
24
|
+
let dispatches = 0;
|
|
25
|
+
|
|
26
|
+
for (let turn = 1; turn <= maxTurns; turn++) {
|
|
27
|
+
if (dispatches >= maxDispatches) throw Object.assign(new Error('dispatch_budget_exhausted'), { code: 'dispatch_budget_exhausted', trace });
|
|
28
|
+
const response = await request({ ...initialBody, messages, tool_choice: executed.size >= required.size ? 'auto' : (initialBody.tool_choice ?? 'required') }, { turn, dispatch: ++dispatches });
|
|
29
|
+
const message = response?.data?.choices?.[0]?.message;
|
|
30
|
+
const calls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
|
|
31
|
+
trace.push({ turn, status: response?.status ?? null, finishReason: response?.data?.choices?.[0]?.finish_reason ?? null, callCount: calls.length, callNames: calls.map(call => call?.function?.name ?? null) });
|
|
32
|
+
if (response?.status !== 200 || !object(message)) throw Object.assign(new Error('assistant_turn_failed'), { code: 'assistant_turn_failed', trace });
|
|
33
|
+
|
|
34
|
+
if (!calls.length) {
|
|
35
|
+
const missing = [...required].filter(name => ![...executed.values()].some(item => item.name === name));
|
|
36
|
+
const content = typeof message.content === 'string' ? message.content : '';
|
|
37
|
+
if (missing.length) throw Object.assign(new Error('assistant_stopped_before_required_tools'), { code: 'assistant_stopped_before_required_tools', missing, trace });
|
|
38
|
+
if (!validateFinal(content, executed)) throw Object.assign(new Error('final_validation_failed'), { code: 'final_validation_failed', trace });
|
|
39
|
+
return { content, messages: [...messages, message], turns: turn, dispatches, executed: [...executed.values()].map(({ id, name, signature }) => ({ id, name, signature })), trace };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
messages.push(message);
|
|
43
|
+
for (const raw of calls) {
|
|
44
|
+
const call = toolCall(raw);
|
|
45
|
+
if (!required.has(call.name)) throw Object.assign(new Error('unexpected_tool_call'), { code: 'unexpected_tool_call', name: call.name, trace });
|
|
46
|
+
if (executed.has(call.signature)) throw Object.assign(new Error('tool_call_replay'), { code: 'tool_call_replay', name: call.name, trace });
|
|
47
|
+
if ([...executed.values()].some(item => item.name === call.name)) throw Object.assign(new Error('tool_called_more_than_once'), { code: 'tool_called_more_than_once', name: call.name, trace });
|
|
48
|
+
const content = await executeTool(call.name, call.args, { turn, id: call.id });
|
|
49
|
+
if (typeof content !== 'string') throw Object.assign(new Error('invalid_tool_result'), { code: 'invalid_tool_result', name: call.name, trace });
|
|
50
|
+
executed.set(call.signature, { ...call, content });
|
|
51
|
+
messages.push({ role: 'tool', tool_call_id: call.id, content });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
throw Object.assign(new Error('turn_budget_exhausted'), { code: 'turn_budget_exhausted', trace });
|
|
55
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Aki Pro Max local installer / runner (Windows PowerShell)
|
|
2
|
+
# Usage:
|
|
3
|
+
# irm https://raw.githubusercontent.com/khangtudo/aki-pro-max/main/scripts/install-local.ps1 | iex
|
|
4
|
+
# or clone the repo then: .\scripts\install-local.ps1
|
|
5
|
+
|
|
6
|
+
$ErrorActionPreference = 'Stop'
|
|
7
|
+
$Package = 'aki-pro-max'
|
|
8
|
+
|
|
9
|
+
Write-Host "Aki Pro Max Local Installer" -ForegroundColor Cyan
|
|
10
|
+
Write-Host "Installing the Aki Pro Max server, local tray helper shortcut, and Control Plane launcher..." -ForegroundColor DarkGray
|
|
11
|
+
|
|
12
|
+
npm install -g $Package
|
|
13
|
+
|
|
14
|
+
$Bin = (Get-Command aki-pro-max -ErrorAction Stop).Source
|
|
15
|
+
$ShortcutDir = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs\Aki Pro Max'
|
|
16
|
+
New-Item -ItemType Directory -Force -Path $ShortcutDir | Out-Null
|
|
17
|
+
|
|
18
|
+
$VbsPath = Join-Path $ShortcutDir 'Start Aki Pro Max Control.vbs'
|
|
19
|
+
$VbsContent = @"
|
|
20
|
+
Set shell = CreateObject("WScript.Shell")
|
|
21
|
+
shell.Run "cmd /c aki-pro-max start", 0, False
|
|
22
|
+
"@
|
|
23
|
+
Set-Content -Path $VbsPath -Value $VbsContent -Encoding ASCII
|
|
24
|
+
|
|
25
|
+
$UrlPath = Join-Path $ShortcutDir 'Open Aki Pro Max Control.url'
|
|
26
|
+
$UrlContent = @"
|
|
27
|
+
[InternetShortcut]
|
|
28
|
+
URL=http://127.0.0.1:8788/admin
|
|
29
|
+
"@
|
|
30
|
+
Set-Content -Path $UrlPath -Value $UrlContent -Encoding ASCII
|
|
31
|
+
|
|
32
|
+
Write-Host "Installed command: $Bin" -ForegroundColor Green
|
|
33
|
+
Write-Host "Start server: aki-pro-max start" -ForegroundColor Yellow
|
|
34
|
+
Write-Host "Open Control Plane: http://127.0.0.1:8788/admin" -ForegroundColor Yellow
|
|
35
|
+
Write-Host "Start-menu shortcuts added under: $ShortcutDir" -ForegroundColor Green
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { once } from 'node:events';
|
|
3
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
4
|
+
import { createProviderServer } from '../src/server.mjs';
|
|
5
|
+
import { loadConfig } from '../src/config.mjs';
|
|
6
|
+
import { runEventualToolLoop } from './eventual-tool-loop.mjs';
|
|
7
|
+
|
|
8
|
+
const ROOT=new URL('../',import.meta.url);const REPORT=new URL('../reports/LIVE-EVENTUAL-MULTITOOL-V2.3.1.json',import.meta.url);
|
|
9
|
+
const COOKIE_PATH=process.env.AKI_POSTMAN_COOKIE_FILE||'D:/postman.sid.txt';const ORIGIN='https://akimax6.postman.co';const WORKSPACE_ID='358aa6cf-3b60-449f-8981-c60263000413';const MODEL='gpt-5.6-sol';const SELECTED='GPT_56_SOL';const TIMEOUT_MS=90000;
|
|
10
|
+
const apiKey=randomBytes(24).toString('base64url');const cookie=(await fs.readFile(COOKIE_PATH,'utf8')).replace(/^\uFEFF/,'').split(/\r?\n/).map(x=>x.trim()).find(Boolean);if(!cookie||!/^[\x21-\x7E]{1,4096}$/.test(cookie)||/[;,\\]/.test(cookie))throw new Error('cookie_format_invalid');
|
|
11
|
+
const markers={alpha:`ALPHA_${randomBytes(12).toString('hex')}`,beta:`BETA_${randomBytes(12).toString('hex')}`};
|
|
12
|
+
const tools=[{type:'function',function:{name:'alpha_probe',description:'Read the private alpha probe value. Call once with slot alpha.',parameters:{type:'object',properties:{slot:{type:'string',enum:['alpha']}},required:['slot'],additionalProperties:false}}},{type:'function',function:{name:'beta_probe',description:'Read the private beta probe value. Call once with slot beta.',parameters:{type:'object',properties:{slot:{type:'string',enum:['beta']}},required:['slot'],additionalProperties:false}}}];
|
|
13
|
+
const config=loadConfig({PROVIDER_HOST:'127.0.0.1',PROVIDER_PORT:'0',PROVIDER_API_KEY:apiKey,PROVIDER_MODELS:MODEL,POSTMAN_TRANSPORT_STRATEGY:'web_session',POSTMAN_SESSION_COOKIE:cookie,POSTMAN_WORKSPACE_SUBDOMAIN:'akimax6',POSTMAN_WORKSPACE_ID:WORKSPACE_ID,POSTMAN_WEB_SESSION_SELECTED_MODEL:SELECTED,POSTMAN_WEB_SESSION_OBSERVED_MODELS:'gpt-5.6-sol,GPT_56_SOL',REQUEST_TIMEOUT_MS:String(TIMEOUT_MS),MAX_BODY_BYTES:'104857600',MAX_EVENT_BYTES:'1048576',MAX_OUTPUT_BYTES:'1048576',MAX_TOOL_ARGUMENT_BYTES:'4096',MAX_TOOL_RESULT_BYTES:'4096',MAX_TOOLS:'8',MAX_CONCURRENT_REQUESTS:'1',CORRELATION_PRINCIPAL_KEY:randomBytes(32).toString('base64url')});
|
|
14
|
+
let upstreamDispatches=0;const upstreamShapes=[];const fetchImpl=async(url,init)=>{if(String(url)!==`${ORIGIN}/_gw/chat`)throw Object.assign(new Error('unexpected_upstream_url'),{code:'unexpected_upstream_url',preSend:true});upstreamDispatches++;if(upstreamDispatches>4)throw Object.assign(new Error('upstream_budget_exhausted'),{code:'upstream_budget_exhausted',preSend:true});const b=JSON.parse(init.body);upstreamShapes.push({dispatch:upstreamDispatches,chatType:b?.input?.chatType??null,toolCallIdPresent:Boolean(b?.input?.toolCallId),toolCallGroupIdPresent:Boolean(b?.input?.toolCallGroupId),toolResponsePresent:typeof b?.input?.toolResponse==='string',toolResponseCount:(()=>{try{const x=JSON.parse(b?.input?.toolResponse);return Array.isArray(x)?x.length:null}catch{return null}})(),catalogCount:Object.values(b?.clientTools?.thirdParty??{}).reduce((n,x)=>n+(Array.isArray(x?.tools)?x.tools.length:0),0),parallelSupported:b?.devModeOptions?.isParallelToolCallingSupported===true});return fetch(url,{...init,signal:init.signal})};
|
|
15
|
+
const logs=[];const logger={error(line){try{const x=JSON.parse(line);logs.push({event:x.event??null,status:x.status??null,code:x.code??null,upstreamCode:x.upstream?.code??null,requestIdPresent:Boolean(x.upstream?.requestId)})}catch{logs.push({event:'unparsed'})}}};const server=createProviderServer(config,{fetchImpl,logger});
|
|
16
|
+
const sessionId=`v231-${randomUUID()}`;const evidence={schema:'aki-live-eventual-multitool/v1',releaseVersion:'2.3.1-candidate',model:MODEL,policy:{maxTurns:3,maxFacadeDispatches:3,maxUpstreamDispatches:4,retry:false,failover:false},pass:false,facade:null,loop:null,upstreamShapes,logs,serverClosed:false,generatedAt:null};
|
|
17
|
+
const post=async(base,body)=>{const controller=new AbortController();const timer=setTimeout(()=>controller.abort(),TIMEOUT_MS+5000);try{const response=await fetch(`${base}/v1/chat/completions`,{method:'POST',signal:controller.signal,headers:{authorization:`Bearer ${apiKey}`,'content-type':'application/json','x-session-id':sessionId},body:JSON.stringify(body)});let data=null;try{data=await response.json()}catch{}return{status:response.status,data}}finally{clearTimeout(timer)}};
|
|
18
|
+
try{server.listen(0,'127.0.0.1');await once(server,'listening');const base=`http://127.0.0.1:${server.address().port}`;let facadeDispatches=0;const result=await runEventualToolLoop({initialBody:{model:MODEL,messages:[{role:'user',content:'Use both alpha_probe and beta_probe exactly once. They may be called in parallel or sequentially. Do not stop until both results are available, then reply with exactly ALPHA=<alpha result>;BETA=<beta result>.'}],tools,tool_choice:'required',parallel_tool_calls:true,stream:false},requiredTools:['alpha_probe','beta_probe'],maxTurns:3,maxDispatches:3,request:async body=>{facadeDispatches++;return post(base,body)},executeTool:async(name,args)=>{if(name==='alpha_probe'&&args.slot==='alpha')return markers.alpha;if(name==='beta_probe'&&args.slot==='beta')return markers.beta;throw Object.assign(new Error('tool_schema_mismatch'),{code:'tool_schema_mismatch'})},validateFinal:(content,executed)=>content.trim()===`ALPHA=${markers.alpha};BETA=${markers.beta}`&&[...executed.values()].every(item=>content.includes(item.content))});evidence.facade={dispatches:facadeDispatches};evidence.loop={turns:result.turns,dispatches:result.dispatches,executed:result.executed.map(x=>({name:x.name,signature:x.signature})),trace:result.trace,exactFinal:true};evidence.pass=result.executed.length===2&&new Set(result.executed.map(x=>x.name)).size===2&&result.dispatches<=3&&upstreamDispatches<=4;}catch(error){evidence.failure={code:error?.code??'unclassified',message:String(error?.message??'').slice(0,120),trace:error?.trace??null};process.exitCode=1}finally{server.closeAllConnections?.();if(server.listening){server.close();await once(server,'close')}evidence.serverClosed=!server.listening;evidence.generatedAt=new Date().toISOString();evidence.upstreamDispatches=upstreamDispatches;await fs.mkdir(new URL('../reports/',import.meta.url),{recursive:true});await fs.writeFile(REPORT,JSON.stringify(evidence,null,2)+'\n','utf8');console.log(JSON.stringify({report:REPORT.pathname,pass:evidence.pass,loop:evidence.loop,facade:evidence.facade,upstreamDispatches,upstreamShapes,serverClosed:evidence.serverClosed,failure:evidence.failure??null}));}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
const file = new URL('../src/admin.mjs', import.meta.url);
|
|
4
|
+
let text = fs.readFileSync(file, 'utf8');
|
|
5
|
+
|
|
6
|
+
text = text.replace(
|
|
7
|
+
'<body><main class="shell"><header>',
|
|
8
|
+
'<body><main class="shell"><section id="updateBanner" class="update hidden" aria-live="polite"><div><span class="pill">UPDATE AVAILABLE</span><h2 id="updateTitle">Aki Pro Max update available</h2><p id="updateCopy">Checking release status…</p></div><div class="update-actions"><button id="copyUpdate">Copy update command</button><button id="dismissUpdate">Later</button></div><ol id="updateSteps"><li>Stop the active proxy PID completely.</li><li>Open PowerShell and run the update command.</li><li>Click Reload after the command completes to start a new proxy PID.</li></ol><code id="updateCommand"></code><button id="reloadProxy">Reload Control Plane</button></section><header>'
|
|
9
|
+
);
|
|
10
|
+
text = text.replace(
|
|
11
|
+
'.status.up{color:#6ee7b7}',
|
|
12
|
+
'.status.up{color:#6ee7b7}.update{border-color:#f97316;background:linear-gradient(135deg,#351c0b,#111827);position:relative}.update.hidden{display:none}.update h2{margin:8px 0}.update p{margin:6px 0}.update-actions{position:absolute;right:20px;top:20px;display:flex;gap:8px}.update ol{margin:15px 0 12px;padding-left:20px;color:#cbd5e1;line-height:1.7}.update code{display:block;padding:10px;border-radius:9px;background:#020617;color:#fde68a;word-break:break-all}.update #reloadProxy{margin-top:10px;border-color:#fb923c;background:#c2410c}@media(max-width:760px){.update-actions{position:static;margin-top:10px}}'
|
|
13
|
+
);
|
|
14
|
+
text = text.replace(
|
|
15
|
+
"const $=id=>document.getElementById(id); let secret='';",
|
|
16
|
+
"const $=id=>document.getElementById(id); let secret=''; const semver=v=>String(v||'0.0.0').replace(/^v/,'').split('.').map(n=>Number(n)||0); const newer=(a,b)=>{const x=semver(a),y=semver(b);return x.some((n,i)=>n>(y[i]||0)&&x.slice(0,i).every((m,j)=>m===(y[j]||0)))};"
|
|
17
|
+
);
|
|
18
|
+
text = text.replace(
|
|
19
|
+
"$('snippet').textContent=JSON.stringify({baseURL:config.baseUrl,apiKey:'<reveal-and-copy-from-this-page>',model:config.models[0]},null,2); $('healthBoundary').textContent=health.upstream_verified?'upstream verified':'facade healthy · upstream not verified by /health';",
|
|
20
|
+
"$('snippet').textContent=JSON.stringify({baseURL:config.baseUrl,apiKey:'<reveal-and-copy-from-this-page>',model:config.models[0]},null,2); $('healthBoundary').textContent=health.upstream_verified?'upstream verified':'facade healthy · upstream not verified by /health'; await checkUpdates(config);"
|
|
21
|
+
);
|
|
22
|
+
text = text.replace(
|
|
23
|
+
"$('copyKey').addEventListener('click',()=>copy(secret));",
|
|
24
|
+
"async function checkUpdates(config){const url=config.updateManifestUrl;if(!url)return;try{const c=new AbortController();const t=setTimeout(()=>c.abort(),3000);const m=await fetch(url,{cache:'no-store',signal:c.signal}).then(r=>r.ok?r.json():null);clearTimeout(t);if(!m||!newer(m.latest,config.currentVersion)||localStorage.getItem('aki-update-dismissed')===m.latest)return;$('updateBanner').classList.remove('hidden');$('updateTitle').textContent='Aki Pro Max '+m.latest+' is available';$('updateCopy').textContent='Current version: '+config.currentVersion+'. Update manually to avoid interrupting an active request.';$('updateCommand').textContent=m.updateCommand||'npm update -g '+(m.installPackage||'aki-pro-max');$('copyUpdate').onclick=()=>copy($('updateCommand').textContent);$('dismissUpdate').onclick=()=>{localStorage.setItem('aki-update-dismissed',m.latest);$('updateBanner').classList.add('hidden')};$('reloadProxy').onclick=()=>location.reload()}catch(e){console.info('Update check skipped:',e.message)}}$('copyKey').addEventListener('click',()=>copy(secret));"
|
|
25
|
+
);
|
|
26
|
+
text = text.replace(
|
|
27
|
+
"workspaceId: config.workspaceId,\n models: config.models,",
|
|
28
|
+
"workspaceId: config.workspaceId,\n models: config.models,\n currentVersion: '2.3.3',\n updateManifestUrl: process.env.AKI_UPDATE_MANIFEST_URL || 'https://raw.githubusercontent.com/khangtudo/aki-pro-max/main/version.json',"
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
if (!text.includes('updateBanner') || !text.includes('checkUpdates')) throw new Error('Update patch markers missing');
|
|
32
|
+
fs.writeFileSync(file, text, 'utf8');
|
|
33
|
+
console.log('Updated admin control plane with safe update banner.');
|