aismemory 0.7.5 → 0.8.2
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/README.md +247 -7
- package/dist/__tests__/agent-archive.test.d.ts +1 -0
- package/dist/__tests__/agent-archive.test.js +153 -0
- package/dist/__tests__/agent-archive.test.js.map +1 -0
- package/dist/__tests__/agent-key-mode.test.js +121 -6
- package/dist/__tests__/agent-key-mode.test.js.map +1 -1
- package/dist/__tests__/agent-load-persistence.test.js +22 -6
- package/dist/__tests__/agent-load-persistence.test.js.map +1 -1
- package/dist/__tests__/ais-http.test.js +23 -3
- package/dist/__tests__/ais-http.test.js.map +1 -1
- package/dist/__tests__/auto-handoff.test.js +56 -0
- package/dist/__tests__/auto-handoff.test.js.map +1 -1
- package/dist/__tests__/bond-link-survives-exit.test.d.ts +1 -0
- package/dist/__tests__/bond-link-survives-exit.test.js +177 -0
- package/dist/__tests__/bond-link-survives-exit.test.js.map +1 -0
- package/dist/__tests__/credential-resolution.test.js +56 -8
- package/dist/__tests__/credential-resolution.test.js.map +1 -1
- package/dist/__tests__/credential-whoami.test.d.ts +8 -0
- package/dist/__tests__/credential-whoami.test.js +93 -0
- package/dist/__tests__/credential-whoami.test.js.map +1 -0
- package/dist/__tests__/device-flow-gate-e2e.test.js +6 -4
- package/dist/__tests__/device-flow-gate-e2e.test.js.map +1 -1
- package/dist/__tests__/doctor.test.js +176 -85
- package/dist/__tests__/doctor.test.js.map +1 -1
- package/dist/__tests__/full-context-view-forwarding.test.d.ts +1 -0
- package/dist/__tests__/full-context-view-forwarding.test.js +80 -0
- package/dist/__tests__/full-context-view-forwarding.test.js.map +1 -0
- package/dist/__tests__/hydration.test.js +56 -1
- package/dist/__tests__/hydration.test.js.map +1 -1
- package/dist/__tests__/install.test.d.ts +1 -0
- package/dist/__tests__/install.test.js +126 -0
- package/dist/__tests__/install.test.js.map +1 -0
- package/dist/__tests__/integration.test.js +1 -0
- package/dist/__tests__/integration.test.js.map +1 -1
- package/dist/__tests__/lost-agent.test.js +51 -0
- package/dist/__tests__/lost-agent.test.js.map +1 -1
- package/dist/__tests__/mcp-tools.test.js +210 -3
- package/dist/__tests__/mcp-tools.test.js.map +1 -1
- package/dist/__tests__/memory-write-failure.test.js +4 -8
- package/dist/__tests__/memory-write-failure.test.js.map +1 -1
- package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.js +4 -7
- package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.js.map +1 -1
- package/dist/__tests__/pending-device-flow-e2e.test.js +22 -1
- package/dist/__tests__/pending-device-flow-e2e.test.js.map +1 -1
- package/dist/__tests__/pending-device-flow-resume.test.d.ts +1 -0
- package/dist/__tests__/pending-device-flow-resume.test.js +104 -0
- package/dist/__tests__/pending-device-flow-resume.test.js.map +1 -0
- package/dist/__tests__/prompt-resource.test.js +1 -0
- package/dist/__tests__/prompt-resource.test.js.map +1 -1
- package/dist/__tests__/provisional-register-mcp-remember.test.js +4 -8
- package/dist/__tests__/provisional-register-mcp-remember.test.js.map +1 -1
- package/dist/__tests__/reconnect-survival-e2e.test.js +3 -4
- package/dist/__tests__/reconnect-survival-e2e.test.js.map +1 -1
- package/dist/__tests__/refresh.test.js +24 -2
- package/dist/__tests__/refresh.test.js.map +1 -1
- package/dist/__tests__/request-timeout.test.d.ts +1 -0
- package/dist/__tests__/request-timeout.test.js +40 -0
- package/dist/__tests__/request-timeout.test.js.map +1 -0
- package/dist/__tests__/resolve-agent-preference-e2e.test.js +15 -3
- package/dist/__tests__/resolve-agent-preference-e2e.test.js.map +1 -1
- package/dist/__tests__/retry-after.test.d.ts +4 -0
- package/dist/__tests__/retry-after.test.js +134 -0
- package/dist/__tests__/retry-after.test.js.map +1 -0
- package/dist/__tests__/run-pass-mode.test.d.ts +1 -0
- package/dist/__tests__/run-pass-mode.test.js +130 -0
- package/dist/__tests__/run-pass-mode.test.js.map +1 -0
- package/dist/__tests__/session-identity.test.d.ts +1 -0
- package/dist/__tests__/session-identity.test.js +213 -0
- package/dist/__tests__/session-identity.test.js.map +1 -0
- package/dist/__tests__/version-check.test.js +1 -1
- package/dist/__tests__/workspace-key-mode.test.js +194 -16
- package/dist/__tests__/workspace-key-mode.test.js.map +1 -1
- package/dist/agent-archive.d.ts +36 -0
- package/dist/agent-archive.js +69 -0
- package/dist/agent-archive.js.map +1 -0
- package/dist/agent-key-mode.js +86 -29
- package/dist/agent-key-mode.js.map +1 -1
- package/dist/ais-http-error.d.ts +7 -2
- package/dist/ais-http-error.js +18 -5
- package/dist/ais-http-error.js.map +1 -1
- package/dist/ais-http.d.ts +2 -0
- package/dist/ais-http.js +41 -19
- package/dist/ais-http.js.map +1 -1
- package/dist/auto-handoff.js +33 -1
- package/dist/auto-handoff.js.map +1 -1
- package/dist/cli/doctor.d.ts +5 -2
- package/dist/cli/doctor.js +72 -15
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/install.d.ts +6 -0
- package/dist/cli/install.js +52 -0
- package/dist/cli/install.js.map +1 -0
- package/dist/cli/orientation.d.ts +2 -0
- package/dist/cli/orientation.js +31 -1
- package/dist/cli/orientation.js.map +1 -1
- package/dist/credential-whoami.d.ts +50 -0
- package/dist/credential-whoami.js +91 -0
- package/dist/credential-whoami.js.map +1 -0
- package/dist/hydration.js +8 -0
- package/dist/hydration.js.map +1 -1
- package/dist/index.js +213 -50
- package/dist/index.js.map +1 -1
- package/dist/mcp/tools.d.ts +18 -4
- package/dist/mcp/tools.js +107 -12
- package/dist/mcp/tools.js.map +1 -1
- package/dist/pending-device-flow.d.ts +31 -0
- package/dist/pending-device-flow.js +38 -0
- package/dist/pending-device-flow.js.map +1 -1
- package/dist/refresh.js +5 -1
- package/dist/refresh.js.map +1 -1
- package/dist/request-timeout.d.ts +26 -0
- package/dist/request-timeout.js +49 -0
- package/dist/request-timeout.js.map +1 -0
- package/dist/retry-after.d.ts +26 -0
- package/dist/retry-after.js +42 -0
- package/dist/retry-after.js.map +1 -0
- package/dist/run-pass-mode.d.ts +16 -0
- package/dist/run-pass-mode.js +274 -0
- package/dist/run-pass-mode.js.map +1 -0
- package/dist/session-identity.d.ts +40 -0
- package/dist/session-identity.js +179 -0
- package/dist/session-identity.js.map +1 -0
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.js.map +1 -1
- package/dist/tool-args.d.ts +13 -1
- package/dist/tool-args.js +27 -1
- package/dist/tool-args.js.map +1 -1
- package/dist/types.d.ts +32 -0
- package/dist/workspace-key-mode.js +100 -26
- package/dist/workspace-key-mode.js.map +1 -1
- package/package.json +4 -6
- package/dist/harness-identity.d.ts +0 -17
- package/dist/harness-identity.js +0 -105
- package/dist/harness-identity.js.map +0 -1
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ Add to your IDE's MCP server config:
|
|
|
37
37
|
"mcpServers": {
|
|
38
38
|
"aismemory": {
|
|
39
39
|
"command": "npx",
|
|
40
|
-
"args": ["aismemory@
|
|
40
|
+
"args": ["aismemory@0.8.2"],
|
|
41
41
|
"env": {
|
|
42
42
|
"npm_config_audit": "false",
|
|
43
43
|
"npm_config_fund": "false"
|
|
@@ -47,10 +47,16 @@ Add to your IDE's MCP server config:
|
|
|
47
47
|
}
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
Pin
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
Pin an exact version rather than `aismemory@latest`. With `@latest`, every
|
|
51
|
+
client startup makes `npx` resolve the tag against the npm registry before it
|
|
52
|
+
can even launch the cached install — on a slow or congested connection that
|
|
53
|
+
resolution call can itself hang, and clients with a startup connect timeout
|
|
54
|
+
(for example Claude Code's 30s `CONNECT_TIMEOUT`) will kill the launch before
|
|
55
|
+
the server ever starts (GitHub issue #6635). A pinned version skips that
|
|
56
|
+
resolution call entirely and reuses the local `npx` cache. The version-check
|
|
57
|
+
module (`AISMEMORY_VERSION_CHECK`) already tells you in-session when a newer
|
|
58
|
+
release is available, so pinning no longer trades away staleness detection —
|
|
59
|
+
bump the pinned version when it does.
|
|
54
60
|
|
|
55
61
|
npm's post-install audit call can stall a cold `npx` start for minutes; the
|
|
56
62
|
`env` block above skips it and changes nothing about the server itself.
|
|
@@ -74,7 +80,8 @@ After bonding, your agent has persistent memory. Every future session signs in o
|
|
|
74
80
|
| `remember` | Store a memory (fact, event, lesson, context, goal, task) |
|
|
75
81
|
| `recall` | Search memories by natural language query |
|
|
76
82
|
| `whoami` | Get agent identity and status |
|
|
77
|
-
| `
|
|
83
|
+
| `orientation_check` | Diff an owner-approved expected-environment manifest against this session's evidence — read-only, same comparator as `aismemory doctor --orientation`, reachable without a filesystem |
|
|
84
|
+
| `full_context` | Load complete agent context (personality, goals, facts, skills, and the versioned operating layer — constitution rules plus the skills index) |
|
|
78
85
|
| `dream` | Trigger memory consolidation (promotes important, archives old) |
|
|
79
86
|
| `handoff` | Save session context for the next session |
|
|
80
87
|
| `agent_load` | Switch the active agent by name — triggers on "get me X", "bring me X", "load X", "/agent X", "switch to X" |
|
|
@@ -85,6 +92,25 @@ After bonding, your agent has persistent memory. Every future session signs in o
|
|
|
85
92
|
| `goal_list` | List goals for the current agent, with status/parent filters |
|
|
86
93
|
| `goal_update_progress` | Update a goal's progress (0-100) |
|
|
87
94
|
| `goal_complete` | Mark a goal complete (success or failure) |
|
|
95
|
+
| `agent_archive` | Archive one of your own agents by id. Restorable; nothing is deleted. Owner sign-in only |
|
|
96
|
+
| `agent_delete` | Compatibility alias for `agent_archive`. It archives; it does not delete |
|
|
97
|
+
|
|
98
|
+
### Archiving an agent you own
|
|
99
|
+
|
|
100
|
+
Archive is the only lifecycle action this package offers. Ask your agent to
|
|
101
|
+
call `agent_archive` with the id of an agent you own:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
agent_archive { "agentId": "<agent id>" }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The agent's status becomes `archived` and it stops showing as a live agent.
|
|
108
|
+
Its memories are kept, and you can restore it. `agent_delete` does exactly the
|
|
109
|
+
same thing and says so; the name is kept only so older prompts keep working.
|
|
110
|
+
|
|
111
|
+
Archiving needs your own sign-in (the default, browser-approved mode). The
|
|
112
|
+
service refuses an agent that belongs to someone else. Agent API keys and
|
|
113
|
+
workspace keys cannot archive any agent, their own included.
|
|
88
114
|
|
|
89
115
|
## Works With
|
|
90
116
|
|
|
@@ -106,6 +132,8 @@ Authentication uses the OAuth device flow (RFC 8628). Credentials are saved to `
|
|
|
106
132
|
| Variable | Default | Description |
|
|
107
133
|
|----------|---------|-------------|
|
|
108
134
|
| `AIS_URL` | `https://ais.agentsandswarms.ai` | AIS server URL |
|
|
135
|
+
| `AIS_WORKSPACE_KEY` | unset | Workspace key (`ais_wk_...`) for several agents; see [One key for many agents](#one-key-for-many-agents-ais_workspace_key) |
|
|
136
|
+
| `AIS_MCP_REQUEST_TIMEOUT_MS` | `20000` | Client-side timeout, in milliseconds, for every HTTP call this server makes to AIS. A stalled call fails with a clear error instead of hanging. Must be a positive number; anything else falls back to the default |
|
|
109
137
|
| `AISMEMORY_VERSION_CHECK` | on | Set to `off` to stop the daily check for a newer published version of this package |
|
|
110
138
|
|
|
111
139
|
## Unattended machines (agent API keys)
|
|
@@ -140,11 +168,140 @@ flow or creates a new agent on your behalf.
|
|
|
140
168
|
This credential is intentionally scoped: it can only read/write that one
|
|
141
169
|
agent's `memory`, `goals`, `dream`, `full_context`, and `handoff` data. Tools
|
|
142
170
|
that need a broader route — `agent_load` (switching agents, which needs
|
|
143
|
-
tenant-wide agent listing)
|
|
171
|
+
tenant-wide agent listing) and `agent_archive` / `agent_delete` (archiving is
|
|
172
|
+
for the owner sign-in only) — return an explicit "not available with agent API
|
|
144
173
|
key auth" error rather than failing silently or faking success. `whoami`
|
|
145
174
|
answers from `AIS_AGENT_ID` directly instead of calling the (unscoped)
|
|
146
175
|
identity route.
|
|
147
176
|
|
|
177
|
+
## Many agents on one machine
|
|
178
|
+
|
|
179
|
+
Identity is **per session**, never per machine. A session picks its agent at
|
|
180
|
+
launch — `AIS_AGENT_ID` exported in the shell that starts `aismemory`, or set
|
|
181
|
+
in the `env` block of that session's own MCP config — and can switch agents
|
|
182
|
+
in-session with the `agent_load` tool. Run as many sessions as you like side by
|
|
183
|
+
side on one machine, each as a different agent; nothing pins a machine, an
|
|
184
|
+
install, or a harness to one agent.
|
|
185
|
+
|
|
186
|
+
Generate a separate portable MCP config for each agent (use its **agent ID**
|
|
187
|
+
from `whoami`, not its display name; this offline command does not resolve names):
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
aismemory install --identity <agent-a-id> --out ./agent-configs
|
|
191
|
+
aismemory install --identity <agent-b-id> --out ./agent-configs
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Each command writes `aismemory-<agent-id>.mcp.json` with that ID in the server's
|
|
195
|
+
`AIS_AGENT_ID` env, and prints a shell/start-script snippet:
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
AIS_AGENT_ID='<agent-a-id>' claude --strict-mcp-config --mcp-config '/absolute/path/agent-configs/aismemory-<agent-a-id>.mcp.json'
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The default output directory is `./aismemory-configs`. Existing files are never
|
|
202
|
+
overwritten; use a new output directory to regenerate. The config contains no
|
|
203
|
+
secrets and copies no credentials. Authenticate as needed and run `whoami` in
|
|
204
|
+
each new session. `--strict-mcp-config` selects only this file for the session;
|
|
205
|
+
add other needed MCP servers to it. Other clients can load the generated MCP
|
|
206
|
+
server entry in their **session-specific** config.
|
|
207
|
+
|
|
208
|
+
Never put `AIS_AGENT_ID` in user-scope `~/.claude.json` or a shared shell profile.
|
|
209
|
+
Install does not edit shared config or create legacy harness pins. Doctor warns
|
|
210
|
+
when user-scope Claude env blocks contain an agent ID and explains how to move
|
|
211
|
+
to separate session configs; it never edits or deletes those defaults.
|
|
212
|
+
|
|
213
|
+
Setting `AIS_AGENT_ID` also arms an identity gate: before `remember`,
|
|
214
|
+
`handoff`, `dream`, `inbox_send`, and `inbox_read` with `ack`, the client asks
|
|
215
|
+
`whoami` and **refuses the write** if the live agent is not the one this
|
|
216
|
+
session selected. It refuses a mismatched write; it never pins anything
|
|
217
|
+
outside this process.
|
|
218
|
+
|
|
219
|
+
`aismemory doctor` reports the identity of the process you run it in. Earlier
|
|
220
|
+
0.7.x builds could install a per-harness pin file under
|
|
221
|
+
`~/.aismemory/harnesses/`; that mechanism is gone, nothing reads those files,
|
|
222
|
+
and doctor only warns if it finds one left behind — delete them yourself when
|
|
223
|
+
you want them gone.
|
|
224
|
+
|
|
225
|
+
## Unattended credentials
|
|
226
|
+
|
|
227
|
+
Credential mode order is deterministic:
|
|
228
|
+
|
|
229
|
+
1. `AIS_WORKSPACE_KEY` (or `AIS_API_KEY=ais_wk_...`)
|
|
230
|
+
2. `AIS_AGENT_API_KEY` (or `AIS_API_KEY=ais_ak_...`)
|
|
231
|
+
3. `AIS_RUN_PASS_FILE`
|
|
232
|
+
4. saved sign-in / browser sign-in
|
|
233
|
+
|
|
234
|
+
When `AIS_RUN_PASS_FILE` is unset, explicit keys keep their existing
|
|
235
|
+
precedence over saved sign-in. If `AIS_RUN_PASS_FILE` is set together with
|
|
236
|
+
`AIS_WORKSPACE_KEY`, `AIS_AGENT_API_KEY`, or `AIS_API_KEY`, startup is refused
|
|
237
|
+
instead of silently falling back to a long-lived key.
|
|
238
|
+
|
|
239
|
+
## One key for many agents (`AIS_WORKSPACE_KEY`)
|
|
240
|
+
|
|
241
|
+
If you run several agents, the owner of those agents can mint one
|
|
242
|
+
**workspace key** (it starts with `ais_wk_`) and grant it to exactly the
|
|
243
|
+
agents it should reach. One connection then serves all of them; you switch
|
|
244
|
+
between them with `agent_load`.
|
|
245
|
+
|
|
246
|
+
How it differs from an agent API key:
|
|
247
|
+
|
|
248
|
+
| | Agent API key | Workspace key |
|
|
249
|
+
|---|---|---|
|
|
250
|
+
| Variable | `AIS_AGENT_API_KEY` | `AIS_WORKSPACE_KEY` |
|
|
251
|
+
| Prefix | `ais_ak_` | `ais_wk_` |
|
|
252
|
+
| Reaches | the one agent it was minted for | every agent its owner granted it, and no others |
|
|
253
|
+
| `agent_load` | not available | switches between granted agents |
|
|
254
|
+
| HTTP header | `x-agent-api-key` | `x-workspace-key` |
|
|
255
|
+
|
|
256
|
+
A workspace key never reaches an agent it was not granted: `agent_load` for
|
|
257
|
+
such an agent fails with a "not granted" error, and the owner has to add that
|
|
258
|
+
agent to the key. Treat the key like a password; if it leaks, revoke it and
|
|
259
|
+
mint a new one.
|
|
260
|
+
|
|
261
|
+
**Local MCP server.** Set the key and the agent the session should start as:
|
|
262
|
+
|
|
263
|
+
```json
|
|
264
|
+
{
|
|
265
|
+
"mcpServers": {
|
|
266
|
+
"aismemory": {
|
|
267
|
+
"command": "npx",
|
|
268
|
+
"args": ["aismemory@0.8.2"],
|
|
269
|
+
"env": {
|
|
270
|
+
"AIS_WORKSPACE_KEY": "ais_wk_...",
|
|
271
|
+
"AIS_AGENT_ID": "<the agent to start with>",
|
|
272
|
+
"npm_config_audit": "false",
|
|
273
|
+
"npm_config_fund": "false"
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`AIS_AGENT_ID` is required with a workspace key. If it is missing, or the key
|
|
281
|
+
does not start with `ais_wk_`, the server prints the reason and exits instead
|
|
282
|
+
of falling back to the browser sign-in. `AIS_WORKSPACE_KEY` wins over
|
|
283
|
+
`AIS_AGENT_API_KEY` when both are set.
|
|
284
|
+
|
|
285
|
+
## One run pass for one run (`AIS_RUN_PASS_FILE`)
|
|
286
|
+
|
|
287
|
+
Launchers can set `AIS_RUN_PASS_FILE` to the path of a file containing a
|
|
288
|
+
short-lived run pass (`ais_rp_...`) and `AIS_AGENT_ID` to the agent for that
|
|
289
|
+
run. On every AIS request, aismemory re-reads the file and sends only:
|
|
290
|
+
|
|
291
|
+
```http
|
|
292
|
+
x-run-pass: ais_rp_...
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
It never sends a workspace key, agent key, bearer token, or service key in
|
|
296
|
+
run-pass mode. If the file is missing, unreadable, empty, or does not contain
|
|
297
|
+
a valid run pass, the request is refused locally and the pass value is never
|
|
298
|
+
printed.
|
|
299
|
+
|
|
300
|
+
**Hosted endpoint.** Clients that connect to
|
|
301
|
+
`https://ais.agentsandswarms.ai/mcp` directly send the key in the
|
|
302
|
+
`X-Workspace-Key` header instead of signing in. The session starts with no
|
|
303
|
+
active agent: call `agent_load` with one of the granted agents first.
|
|
304
|
+
|
|
148
305
|
## Backing up memories to a local file
|
|
149
306
|
|
|
150
307
|
`aismemory pull` copies this agent's memories out of Agent Identity Service and
|
|
@@ -172,6 +329,73 @@ reads only the memories of the agent named in `AIS_AGENT_ID`. It works with a
|
|
|
172
329
|
scoped agent API key — no broader credential is needed and nothing is written
|
|
173
330
|
back to the service.
|
|
174
331
|
|
|
332
|
+
## Tags
|
|
333
|
+
|
|
334
|
+
`remember` accepts `tags` (for example `["work", "acme"]`) and `recall` accepts
|
|
335
|
+
`tags` to return only memories carrying all of them. Over REST:
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
POST /v1/agents/<agent id>/memory {"content": "...", "type": "fact", "tags": ["work", "acme"]}
|
|
339
|
+
PATCH /v1/agents/<agent id>/memory/<memory id> {"tags": ["work"]} (replaces the set; [] clears it)
|
|
340
|
+
GET /v1/agents/<agent id>/memory?query=...&tags=work,acme (carries every tag)
|
|
341
|
+
GET /v1/agents/<agent id>/memory?anyTags=work,acme (carries at least one)
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
- At most 20 tags per memory, each 1-64 characters of `a-z 0-9 . _ : -`. Tags
|
|
345
|
+
are trimmed and lower-cased; duplicates are dropped; the `ais:` prefix is
|
|
346
|
+
reserved. Anything else is a 400 that says which tag is wrong.
|
|
347
|
+
- Set tags with the `tags` field only. A `tags` key inside `metadata` is
|
|
348
|
+
ignored, and replacing `metadata` keeps the memory's tags.
|
|
349
|
+
- Every memory in a response carries `tags` (`[]` when it has none).
|
|
350
|
+
- The filter applies before ranking, on list and recall, with or without
|
|
351
|
+
`query`, and with cursor paging. A recall never widens past it: few or zero
|
|
352
|
+
results is the answer when few memories carry the tags.
|
|
353
|
+
- Tags are a filter, not a permission. Anyone who can read the agent can read
|
|
354
|
+
every memory and every tag. To keep work and personal memories truly
|
|
355
|
+
apart, use two agents.
|
|
356
|
+
|
|
357
|
+
## Listing and paging memories over REST
|
|
358
|
+
|
|
359
|
+
`recall` is a ranked search. To list every memory, call the REST route without
|
|
360
|
+
a query and follow the cursor:
|
|
361
|
+
|
|
362
|
+
```
|
|
363
|
+
GET /v1/agents/<agent id>/memory?sortBy=createdAt&order=desc&limit=100
|
|
364
|
+
GET /v1/agents/<agent id>/memory?limit=100&cursor=<data.nextCursor from the previous page>
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
- `limit` is 1-100. Asking for more returns a 400 that names the maximum.
|
|
368
|
+
- Every page ordered by `createdAt` or `updatedAt` carries `data.nextCursor`.
|
|
369
|
+
Pass it back unchanged as `cursor`; stop when it is `null`. The cursor
|
|
370
|
+
remembers the order, so memories written while you page never make a page
|
|
371
|
+
repeat or skip a row. Send the same filters (`types`, `tiers`, ...) on every
|
|
372
|
+
page.
|
|
373
|
+
- Every memory carries `updatedAt`: the time of its last edit (content, type,
|
|
374
|
+
metadata, importance, goal, or being superseded). Recalling a memory does
|
|
375
|
+
not change it. A memory never edited has `updatedAt` equal to `createdAt`.
|
|
376
|
+
- `sortBy=updatedAt` lists the most recently edited memories first
|
|
377
|
+
(`order=desc`) — the way to pick up changes since your last sync. A memory
|
|
378
|
+
edited while you page moves to the newest end: an `order=desc` walk will not
|
|
379
|
+
see that edit (start a new walk to pick it up), and an `order=asc` walk
|
|
380
|
+
serves it again on a later page.
|
|
381
|
+
- `cursor` cannot be combined with `query`, `offset` or `page`. `offset`/`page`
|
|
382
|
+
still work, but can serve a row twice when memories are written mid-walk.
|
|
383
|
+
- Superseded memories (replaced by a newer one) are hidden unless you pass
|
|
384
|
+
`includeSuperseded=true`; each memory's `temporal.supersededBy` and
|
|
385
|
+
`temporal.isCurrentFact` say which is which. Expired memories need
|
|
386
|
+
`includeExpired=true`.
|
|
387
|
+
- `data.totalCount` counts every matching memory, not the ones still to come.
|
|
388
|
+
|
|
389
|
+
## When AIS is busy
|
|
390
|
+
|
|
391
|
+
Every `429` (rate limit or quota) and `503` (overloaded or briefly
|
|
392
|
+
unavailable) from AIS carries a `Retry-After` header in seconds. aismemory
|
|
393
|
+
reads it: a read that is asked to wait 5 seconds or less waits and tries once
|
|
394
|
+
more on its own; anything longer, and every write, fails right away with an
|
|
395
|
+
error that says how many seconds AIS asked you to wait. Wait that long, then
|
|
396
|
+
run the same call again. Writes are never retried automatically, so a memory
|
|
397
|
+
is never stored twice by a retry.
|
|
398
|
+
|
|
175
399
|
## Troubleshooting
|
|
176
400
|
|
|
177
401
|
If aismemory "doesn't work," run the built-in self-diagnosis before filing a
|
|
@@ -188,6 +412,22 @@ with a PASS/WARN/FAIL line and a remediation hint. Nothing is written to the
|
|
|
188
412
|
API. Pass `--json` for machine-readable output. Exits `0` unless a check
|
|
189
413
|
FAILs.
|
|
190
414
|
|
|
415
|
+
## Source and provenance
|
|
416
|
+
|
|
417
|
+
Source is private. This package is published from GitHub Actions via npm
|
|
418
|
+
Trusted Publishing (OIDC) — no long-lived token, and the publish is bound to
|
|
419
|
+
this project's own CI workflow (verify with `npm view aismemory --json` and
|
|
420
|
+
check `_npmUser.trustedPublisher`). Every package on npm, including this one,
|
|
421
|
+
carries npm's registry signature, checkable locally with `npm audit
|
|
422
|
+
signatures`. That is separate from npm's build provenance/attestation
|
|
423
|
+
feature (a public, source-linked attestation), which requires a public
|
|
424
|
+
source repository and is not enabled here while the source stays private.
|
|
425
|
+
|
|
426
|
+
Official domains: https://www.agentsandswarms.ai and
|
|
427
|
+
https://ais.agentsandswarms.ai. Security contact: security@agentsandswarms.ai.
|
|
428
|
+
The human bonding link above (`agentsandswarms.ai/activate`) is issued at
|
|
429
|
+
runtime by Agent Identity Service, not hardcoded in this package.
|
|
430
|
+
|
|
191
431
|
## License
|
|
192
432
|
|
|
193
433
|
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent_archive and the agent_delete alias in the published package
|
|
3
|
+
* (CORBOT-BA5AADB0, John record 2f8a3a49: archive-your-own only).
|
|
4
|
+
*
|
|
5
|
+
* Pinned here:
|
|
6
|
+
* - under the owner sign-in, agent_archive sends
|
|
7
|
+
* PATCH /v1/agents/<id> {"status":"archived"} with the session bearer, and
|
|
8
|
+
* the response says "archived";
|
|
9
|
+
* - agent_delete does exactly the same and says it is an alias that archives;
|
|
10
|
+
* - an agent API key or workspace key cannot archive any agent — the call
|
|
11
|
+
* fails before any request leaves the process;
|
|
12
|
+
* - the tool descriptions say archive, never delete.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
15
|
+
import { ActivityTracker } from '../auto-handoff.js';
|
|
16
|
+
import { createOAuthCallToolHandler } from '../mcp/tools.js';
|
|
17
|
+
import { dispatchToolKeyMode, OutOfScopeToolError } from '../agent-key-mode.js';
|
|
18
|
+
import { dispatchToolWorkspaceKeyMode } from '../workspace-key-mode.js';
|
|
19
|
+
import { AGENT_ARCHIVE_DESCRIPTION, AGENT_DELETE_ALIAS_DESCRIPTION, archiveAgent, } from '../agent-archive.js';
|
|
20
|
+
const aisGet = vi.hoisted(() => vi.fn());
|
|
21
|
+
const aisPost = vi.hoisted(() => vi.fn());
|
|
22
|
+
const aisPatch = vi.hoisted(() => vi.fn());
|
|
23
|
+
vi.mock('../ais-http.js', () => ({ aisGet, aisPost, aisPatch }));
|
|
24
|
+
const SESSION_AGENT = '11111111-1111-4111-8111-111111111111';
|
|
25
|
+
const LEFTOVER = '22222222-2222-4222-8222-222222222222';
|
|
26
|
+
const CREDS = {
|
|
27
|
+
token: 'owner-token',
|
|
28
|
+
agentId: SESSION_AGENT,
|
|
29
|
+
tenantId: 'tenant-1',
|
|
30
|
+
expiresAt: new Date(Date.now() + 3_600_000).toISOString(),
|
|
31
|
+
};
|
|
32
|
+
function call(name, args) {
|
|
33
|
+
return { method: 'tools/call', params: { name, arguments: args } };
|
|
34
|
+
}
|
|
35
|
+
function makeOwnerHandler() {
|
|
36
|
+
let creds = { ...CREDS };
|
|
37
|
+
const serverStub = { notification: async () => undefined };
|
|
38
|
+
const ctx = {
|
|
39
|
+
aisUrl: 'https://ais.test',
|
|
40
|
+
// The archive path never touches the server; only notification() exists.
|
|
41
|
+
server: serverStub,
|
|
42
|
+
activityTracker: new ActivityTracker(),
|
|
43
|
+
getSessionIdentity: () => null,
|
|
44
|
+
setSessionIdentity: () => { },
|
|
45
|
+
getCredentials: () => creds,
|
|
46
|
+
setCredentials: (next) => { creds = next; },
|
|
47
|
+
setSessionAgentOverride: () => undefined,
|
|
48
|
+
getAgentContext: () => null,
|
|
49
|
+
setAgentContext: () => undefined,
|
|
50
|
+
ensureCredentials: async () => { creds = creds ?? { ...CREDS }; return creds; },
|
|
51
|
+
invalidateToken: () => undefined,
|
|
52
|
+
applyMemoryRefresh: () => undefined,
|
|
53
|
+
resolveAgentForLoad: async () => ({ id: SESSION_AGENT, name: 'Desk', status: 'active' }),
|
|
54
|
+
persistClaudeSessionAgentName: () => undefined,
|
|
55
|
+
isBondPendingError: () => false,
|
|
56
|
+
stderr: () => undefined,
|
|
57
|
+
};
|
|
58
|
+
return createOAuthCallToolHandler(ctx);
|
|
59
|
+
}
|
|
60
|
+
function parse(res) {
|
|
61
|
+
return JSON.parse(res.content[0]?.text ?? '{}');
|
|
62
|
+
}
|
|
63
|
+
describe('agent_archive under the owner sign-in', () => {
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
aisGet.mockReset();
|
|
66
|
+
aisPost.mockReset();
|
|
67
|
+
aisPatch.mockReset();
|
|
68
|
+
aisPatch.mockResolvedValue({ success: true, data: { id: LEFTOVER, status: 'archived' } });
|
|
69
|
+
});
|
|
70
|
+
it('archives the named agent with PATCH status=archived and the session bearer', async () => {
|
|
71
|
+
const res = await makeOwnerHandler()(call('agent_archive', { agentId: LEFTOVER }));
|
|
72
|
+
expect(res.isError).toBeUndefined();
|
|
73
|
+
expect(aisPatch).toHaveBeenCalledTimes(1);
|
|
74
|
+
expect(aisPatch).toHaveBeenCalledWith(`/v1/agents/${LEFTOVER}`, { status: 'archived' }, 'owner-token');
|
|
75
|
+
expect(aisPost).not.toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
it('says "archived" and never reports a delete', async () => {
|
|
78
|
+
const body = parse(await makeOwnerHandler()(call('agent_archive', { agentId: LEFTOVER })));
|
|
79
|
+
expect(body).toEqual({
|
|
80
|
+
success: true,
|
|
81
|
+
archived: true,
|
|
82
|
+
agentId: LEFTOVER,
|
|
83
|
+
status: 'archived',
|
|
84
|
+
message: expect.stringMatching(/archived/),
|
|
85
|
+
});
|
|
86
|
+
expect(String(body['message'])).not.toMatch(/agent (was )?deleted|soft delete/i);
|
|
87
|
+
});
|
|
88
|
+
it('agent_delete is an alias: the same request, the same result, plus a note that it archived', async () => {
|
|
89
|
+
const handler = makeOwnerHandler();
|
|
90
|
+
const archived = parse(await handler(call('agent_archive', { agentId: LEFTOVER })));
|
|
91
|
+
const aliased = parse(await handler(call('agent_delete', { agentId: LEFTOVER })));
|
|
92
|
+
expect(aisPatch.mock.calls[1]).toEqual(aisPatch.mock.calls[0]);
|
|
93
|
+
const { aliasOf, note, ...aliasCore } = aliased;
|
|
94
|
+
expect(aliasCore).toEqual(archived);
|
|
95
|
+
expect(aliasOf).toBe('agent_archive');
|
|
96
|
+
expect(note).toMatch(/alias for agent_archive/);
|
|
97
|
+
expect(note).toMatch(/archived/);
|
|
98
|
+
});
|
|
99
|
+
it.each(['agent_archive', 'agent_delete'])('%s refuses a missing or malformed agentId without a request', async (tool) => {
|
|
100
|
+
const handler = makeOwnerHandler();
|
|
101
|
+
const missing = await handler(call(tool, {}));
|
|
102
|
+
const malformed = await handler(call(tool, { agentId: 'not-a-uuid' }));
|
|
103
|
+
expect(missing.isError).toBe(true);
|
|
104
|
+
expect(malformed.isError).toBe(true);
|
|
105
|
+
expect(parse(malformed)['error']).toMatch(/valid agent UUID/);
|
|
106
|
+
expect(aisPatch).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
it('surfaces a server refusal as an error, not as "archived"', async () => {
|
|
109
|
+
aisPatch.mockRejectedValue(new Error('AIS PATCH /v1/agents/x failed (HTTP 403): This agent is bonded to a different user'));
|
|
110
|
+
const res = await makeOwnerHandler()(call('agent_archive', { agentId: LEFTOVER }));
|
|
111
|
+
expect(res.isError).toBe(true);
|
|
112
|
+
expect(parse(res)['error']).toMatch(/403/);
|
|
113
|
+
expect(parse(res)).not.toHaveProperty('archived');
|
|
114
|
+
});
|
|
115
|
+
it('passes a token-rejection body through so the re-auth retry still fires', async () => {
|
|
116
|
+
const rejected = { success: false, error: { code: 'INVALID_TOKEN', message: 'expired' } };
|
|
117
|
+
await expect(archiveAgent('agent_archive', { agentId: LEFTOVER }, async () => rejected))
|
|
118
|
+
.resolves.toBe(rejected);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe('key modes cannot archive any agent', () => {
|
|
122
|
+
function neverFetch() {
|
|
123
|
+
return vi.fn(async () => {
|
|
124
|
+
throw new Error('no request may leave the process');
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
it.each(['agent_archive', 'agent_delete'])('agent API key: %s is out of scope, for another agent or its own', async (tool) => {
|
|
128
|
+
const fetchImpl = neverFetch();
|
|
129
|
+
const config = { apiKey: 'ais_ak_test', agentId: SESSION_AGENT };
|
|
130
|
+
for (const target of [LEFTOVER, SESSION_AGENT]) {
|
|
131
|
+
await expect(dispatchToolKeyMode('https://ais.test', config, tool, { agentId: target }, fetchImpl))
|
|
132
|
+
.rejects.toThrow(OutOfScopeToolError);
|
|
133
|
+
}
|
|
134
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
it.each(['agent_archive', 'agent_delete'])('workspace key: %s is out of scope', async (tool) => {
|
|
137
|
+
const fetchImpl = neverFetch();
|
|
138
|
+
const config = { workspaceKey: 'ais_wk_test', agentId: SESSION_AGENT };
|
|
139
|
+
await expect(dispatchToolWorkspaceKeyMode('https://ais.test', config, tool, { agentId: LEFTOVER }, fetchImpl))
|
|
140
|
+
.rejects.toThrow(/owner sign-in/);
|
|
141
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('tool descriptions', () => {
|
|
145
|
+
it('agent_archive describes an archive, not a delete', () => {
|
|
146
|
+
expect(AGENT_ARCHIVE_DESCRIPTION).toMatch(/^Archive one of your own agents/);
|
|
147
|
+
expect(AGENT_ARCHIVE_DESCRIPTION).toMatch(/Nothing is deleted/);
|
|
148
|
+
});
|
|
149
|
+
it('agent_delete says it is an alias that archives', () => {
|
|
150
|
+
expect(AGENT_DELETE_ALIAS_DESCRIPTION).toMatch(/^Compatibility alias for agent_archive\. It ARCHIVES the agent; it does not delete it\./);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
//# sourceMappingURL=agent-archive.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-archive.test.js","sourceRoot":"","sources":["../../src/__tests__/agent-archive.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* hitting the network; a 403 from the server is surfaced, not swallowed.
|
|
11
11
|
*/
|
|
12
12
|
import { describe, expect, it, vi } from 'vitest';
|
|
13
|
-
import {
|
|
13
|
+
import { AisHttpError } from '../ais-http-error.js';
|
|
14
|
+
import { AgentApiKeyConfigError, aisKeyGet, dispatchToolKeyMode, loadAgentApiKeyConfig, OutOfScopeToolError, UNKNOWN_TOOL_KEY_MODE, } from '../agent-key-mode.js';
|
|
14
15
|
const AIS_URL = 'https://ais.example.test';
|
|
15
16
|
function jsonResponse(status, body) {
|
|
16
17
|
return new Response(JSON.stringify(body), {
|
|
@@ -95,6 +96,22 @@ describe('dispatchToolKeyMode — in-scope tools send the scoped header only', (
|
|
|
95
96
|
const [url] = fetchMock.mock.calls[0];
|
|
96
97
|
expect(String(url)).toContain('/v1/agents/agent-42/memory?query=test');
|
|
97
98
|
});
|
|
99
|
+
it('remember and recall pass tags through (Jaire C5)', async () => {
|
|
100
|
+
const fetchMock = vi.fn(async (_url, _init) => jsonResponse(200, { success: true, data: { memories: [] } }));
|
|
101
|
+
await dispatchToolKeyMode(AIS_URL, config, 'remember', { content: 'x', tags: ['work', 'acme'] }, fetchMock);
|
|
102
|
+
await dispatchToolKeyMode(AIS_URL, config, 'recall', { query: 'x', tags: ['work', 'acme'] }, fetchMock);
|
|
103
|
+
await dispatchToolKeyMode(AIS_URL, config, 'remember', { content: 'y' }, fetchMock);
|
|
104
|
+
const body = JSON.parse(String(fetchMock.mock.calls[0][1]?.body));
|
|
105
|
+
expect(body['tags']).toEqual(['work', 'acme']);
|
|
106
|
+
expect(String(fetchMock.mock.calls[1][0])).toContain('tags=work%2Cacme');
|
|
107
|
+
const untagged = JSON.parse(String(fetchMock.mock.calls[2][1]?.body));
|
|
108
|
+
expect(untagged).not.toHaveProperty('tags');
|
|
109
|
+
});
|
|
110
|
+
it('rejects a non-array tags argument before any request (Jaire C5)', async () => {
|
|
111
|
+
const fetchMock = vi.fn();
|
|
112
|
+
await expect(dispatchToolKeyMode(AIS_URL, config, 'remember', { content: 'x', tags: 'work' }, fetchMock)).rejects.toThrow(/tags/);
|
|
113
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
98
115
|
it('full_context: GETs /v1/agents/:id/full-context', async () => {
|
|
99
116
|
const fetchMock = vi.fn(async (_url, _init) => jsonResponse(200, { success: true, data: {} }));
|
|
100
117
|
await dispatchToolKeyMode(AIS_URL, config, 'full_context', {}, fetchMock);
|
|
@@ -124,22 +141,120 @@ describe('dispatchToolKeyMode — in-scope tools send the scoped header only', (
|
|
|
124
141
|
const [url] = fetchMock.mock.calls[0];
|
|
125
142
|
expect(url).toBe(`${AIS_URL}/v1/agents/agent-42/memory`);
|
|
126
143
|
});
|
|
127
|
-
it('whoami:
|
|
144
|
+
it('whoami: asks the self-description route and passes its fields through (CORBOT-49535E96)', async () => {
|
|
128
145
|
const calledPaths = [];
|
|
129
146
|
const fetchMock = vi.fn(async (url) => {
|
|
130
147
|
calledPaths.push(String(url));
|
|
131
|
-
return jsonResponse(200, {
|
|
148
|
+
return jsonResponse(200, {
|
|
149
|
+
success: true,
|
|
150
|
+
data: {
|
|
151
|
+
agentId: 'agent-42',
|
|
152
|
+
tenantId: 'tenant-1',
|
|
153
|
+
mode: 'agent-api-key',
|
|
154
|
+
scopeModel: 'allowlist',
|
|
155
|
+
scopes: ['memory', 'whoami'],
|
|
156
|
+
restrictions: ['cannot mint, list or revoke API keys'],
|
|
157
|
+
actsAsGrantedAgent: false,
|
|
158
|
+
expiresAt: '2027-01-01T00:00:00.000Z',
|
|
159
|
+
},
|
|
160
|
+
});
|
|
132
161
|
});
|
|
133
162
|
const result = await dispatchToolKeyMode(AIS_URL, config, 'whoami', {}, fetchMock);
|
|
134
|
-
expect(calledPaths).
|
|
135
|
-
expect(calledPaths[0]).toContain('/v1/agents/agent-42/memory?limit=1');
|
|
163
|
+
expect(calledPaths).toEqual([`${AIS_URL}/v1/agents/agent-42/whoami`]);
|
|
136
164
|
// Must never call the bare identity route — it has no allowlisted subresource.
|
|
137
165
|
expect(calledPaths.some((p) => /\/v1\/agents\/agent-42$/.test(p))).toBe(false);
|
|
138
166
|
expect(result).toMatchObject({
|
|
139
167
|
success: true,
|
|
140
|
-
data: {
|
|
168
|
+
data: {
|
|
169
|
+
agentId: 'agent-42',
|
|
170
|
+
authMode: 'agent-api-key',
|
|
171
|
+
keyValid: true,
|
|
172
|
+
selfDescribed: true,
|
|
173
|
+
scopes: ['memory', 'whoami'],
|
|
174
|
+
expiresAt: '2027-01-01T00:00:00.000Z',
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
it('whoami: an older server without the route still gets the liveness probe answer', async () => {
|
|
179
|
+
const calledPaths = [];
|
|
180
|
+
const fetchMock = vi.fn(async (url) => {
|
|
181
|
+
calledPaths.push(String(url));
|
|
182
|
+
return String(url).endsWith('/whoami')
|
|
183
|
+
? jsonResponse(403, { success: false, error: { code: 'FORBIDDEN', message: 'scoped' } })
|
|
184
|
+
: jsonResponse(200, { success: true, data: { memories: [] } });
|
|
185
|
+
});
|
|
186
|
+
const result = await dispatchToolKeyMode(AIS_URL, config, 'whoami', {}, fetchMock);
|
|
187
|
+
expect(calledPaths).toHaveLength(2);
|
|
188
|
+
expect(calledPaths[1]).toContain('/v1/agents/agent-42/memory?limit=1');
|
|
189
|
+
expect(result).toMatchObject({
|
|
190
|
+
success: true,
|
|
191
|
+
data: { agentId: 'agent-42', authMode: 'agent-api-key', keyValid: true, selfDescribed: false },
|
|
141
192
|
});
|
|
142
193
|
});
|
|
194
|
+
it('orientation_check: evaluates locally, without any fetch call', async () => {
|
|
195
|
+
const fetchMock = vi.fn();
|
|
196
|
+
const manifest = {
|
|
197
|
+
version: 1,
|
|
198
|
+
identity: '00000000-0000-4000-8000-000000000001',
|
|
199
|
+
requirements: { host: 'cloud-vm', account: 'fleet-account', channels: ['ais'] },
|
|
200
|
+
current: { host: 'cloud-vm', account: 'fleet-account', session: 'agent-key-1' },
|
|
201
|
+
evidence: {
|
|
202
|
+
session: 'agent-key-1', host: 'cloud-vm', account: 'fleet-account',
|
|
203
|
+
credentials: 'pass', credentialSource: 'ais-agent-key',
|
|
204
|
+
channels: { ais: 'pass', mcp: 'missing', inbox: 'missing' },
|
|
205
|
+
},
|
|
206
|
+
relocation: { verified: false },
|
|
207
|
+
};
|
|
208
|
+
const result = await dispatchToolKeyMode(AIS_URL, config, 'orientation_check', manifest, fetchMock);
|
|
209
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
210
|
+
expect(result).toMatchObject({ scope: 'public-orientation', exitCode: 0 });
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
describe('aisKeyGet retries once on a transient 502/503 (CORBOT-6F45EDFA)', () => {
|
|
214
|
+
// Mirrors ais-http.test.ts's aisGet retry coverage (CORBOT-9492F31A):
|
|
215
|
+
// the same idempotent-GET reasoning applies to x-agent-api-key GETs.
|
|
216
|
+
it('bounds scoped-key GETs with an abort signal so whoami cannot hang indefinitely', async () => {
|
|
217
|
+
const fetchMock = vi.fn(async () => jsonResponse(200, { success: true, data: { id: 'agent-42' } }));
|
|
218
|
+
await expect(aisKeyGet(AIS_URL, '/v1/agents/agent-42/whoami', 'ais_ak_scoped', fetchMock)).resolves.toEqual({ success: true, data: { id: 'agent-42' } });
|
|
219
|
+
const [, init] = fetchMock.mock.calls[0];
|
|
220
|
+
expect(init?.signal).toBeInstanceOf(AbortSignal);
|
|
221
|
+
});
|
|
222
|
+
it('times out with a clear error instead of hanging when AIS never responds (issue #6635)', async () => {
|
|
223
|
+
process.env['AIS_MCP_REQUEST_TIMEOUT_MS'] = '20';
|
|
224
|
+
const hangingFetch = vi.fn((_url, init) => new Promise((_resolve, reject) => {
|
|
225
|
+
init?.signal?.addEventListener('abort', () => reject(init.signal.reason));
|
|
226
|
+
}));
|
|
227
|
+
await expect(aisKeyGet(AIS_URL, '/v1/agents/agent-42/whoami', 'ais_ak_scoped', hangingFetch)).rejects.toThrow('AIS request to /v1/agents/agent-42/whoami timed out after 20ms');
|
|
228
|
+
delete process.env['AIS_MCP_REQUEST_TIMEOUT_MS'];
|
|
229
|
+
});
|
|
230
|
+
it('retries once on a 502 and returns the retry success', async () => {
|
|
231
|
+
const successBody = { success: true, data: { id: 'agent-42' } };
|
|
232
|
+
const fetchMock = vi.fn();
|
|
233
|
+
fetchMock
|
|
234
|
+
.mockResolvedValueOnce(new Response('upstream error', { status: 502 }))
|
|
235
|
+
.mockResolvedValueOnce(jsonResponse(200, successBody));
|
|
236
|
+
await expect(aisKeyGet(AIS_URL, '/v1/agents/agent-42', 'ais_ak_scoped', fetchMock)).resolves.toEqual(successBody);
|
|
237
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
238
|
+
});
|
|
239
|
+
it('retries once on a 503 and returns the retry success', async () => {
|
|
240
|
+
const successBody = { success: true, data: { id: 'agent-42' } };
|
|
241
|
+
const fetchMock = vi.fn();
|
|
242
|
+
fetchMock
|
|
243
|
+
.mockResolvedValueOnce(new Response('service unavailable', { status: 503 }))
|
|
244
|
+
.mockResolvedValueOnce(jsonResponse(200, successBody));
|
|
245
|
+
await expect(aisKeyGet(AIS_URL, '/v1/agents/agent-42', 'ais_ak_scoped', fetchMock)).resolves.toEqual(successBody);
|
|
246
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
247
|
+
});
|
|
248
|
+
it('only retries once — a second 502 still throws AisHttpError', async () => {
|
|
249
|
+
const fetchMock = vi.fn(async () => jsonResponse(502, { error: { code: 'UPSTREAM', message: 'upstream error' } }));
|
|
250
|
+
await expect(aisKeyGet(AIS_URL, '/v1/agents/agent-42', 'ais_ak_scoped', fetchMock)).rejects.toBeInstanceOf(AisHttpError);
|
|
251
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
252
|
+
});
|
|
253
|
+
it('does not retry non-retryable statuses (e.g. 500)', async () => {
|
|
254
|
+
const fetchMock = vi.fn(async () => jsonResponse(500, { error: { code: 'INTERNAL', message: 'server error' } }));
|
|
255
|
+
await expect(aisKeyGet(AIS_URL, '/v1/agents/agent-42', 'ais_ak_scoped', fetchMock)).rejects.toBeInstanceOf(AisHttpError);
|
|
256
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
257
|
+
});
|
|
143
258
|
});
|
|
144
259
|
describe('dispatchToolKeyMode — inbox tools (CORBOT-7115B8AE, in-scope per CORBOT-04DABA89)', () => {
|
|
145
260
|
const config = { apiKey: 'ais_ak_scoped', agentId: 'agent-42' };
|