myagentmemory 0.4.14 → 0.4.16
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 +1 -1
- package/dist/cli.js +15 -3
- package/dist/plugin-bootstrap.d.ts +12 -0
- package/dist/plugin-host.d.ts +1 -1
- package/dist/plugin-host.js +1 -1
- package/dist/plugin-runtime.d.ts +3 -2
- package/dist/plugin-runtime.js +30 -0
- package/dist/plugin-service.d.ts +5 -1
- package/dist/plugin-service.js +186 -34
- package/docs/official-plugin-bootstrap.md +17 -15
- package/package.json +1 -1
- package/src/cli.ts +16 -3
- package/src/plugin-bootstrap.ts +13 -0
- package/src/plugin-host.ts +2 -2
- package/src/plugin-runtime.ts +30 -0
- package/src/plugin-service.ts +216 -39
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ AgentMemory is free, open-source software under the MIT License. The core is a l
|
|
|
30
30
|
|
|
31
31
|
### Optional official plugins
|
|
32
32
|
|
|
33
|
-
The MIT-licensed core can discover and host separately distributed, signed first-party plugins while remaining fully useful on its own. Optional plugins may use their own license and distribution terms; their implementation and browser assets are not part of the `myagentmemory` package. `agent-memory plugin list` and `plugin status` report local state. In an interactive terminal, `agent-memory plugin install` opens a nonce-bound loopback page for an email address, resumes the waiting command, verifies signed release metadata and the downloaded bundle, and installs atomically.
|
|
33
|
+
The MIT-licensed core can discover and host separately distributed, signed first-party plugins while remaining fully useful on its own. Optional plugins may use their own license and distribution terms; their implementation and browser assets are not part of the `myagentmemory` package. `agent-memory plugin list` and `plugin status` report local state. In an interactive terminal, `agent-memory plugin install` opens a nonce-bound loopback page for an email address, resumes the waiting command, verifies signed release metadata and the downloaded bundle, and installs atomically. The free plan uses a configurable daily agent-session allowance keyed by normalized email. D1 stores bounded activation metadata, a credential hash, and opaque SessionStart usage operations; it never receives memory, session content, queries, repository paths, raw agent session identifiers, IP addresses, or user-agent strings. Authentication and payment can extend this free activation flow later. See the [official plugin bootstrap and host contract](docs/official-plugin-bootstrap.md).
|
|
34
34
|
|
|
35
35
|
## Installation
|
|
36
36
|
|
package/dist/cli.js
CHANGED
|
@@ -178,7 +178,7 @@ function printPluginResult(result, json, allowBrowser) {
|
|
|
178
178
|
console.log("Run: agent-memory plugin install");
|
|
179
179
|
break;
|
|
180
180
|
case "auth_required":
|
|
181
|
-
console.log("Run this command in an interactive terminal to enter an email and activate
|
|
181
|
+
console.log("Run this command in an interactive terminal to enter an email and activate free daily access.");
|
|
182
182
|
break;
|
|
183
183
|
case "renewal_required":
|
|
184
184
|
console.log("Renew AgentMemory Pro to continue using paid capabilities.");
|
|
@@ -893,8 +893,8 @@ Usage:
|
|
|
893
893
|
agent-memory plugin manage [--no-browser]
|
|
894
894
|
|
|
895
895
|
The public core remains fully usable without AgentMemory Pro. Interactive install
|
|
896
|
-
opens a loopback website for
|
|
897
|
-
|
|
896
|
+
opens a loopback website for email activation and a configurable free daily
|
|
897
|
+
agent-session allowance. Memory and session content stay on this device.`);
|
|
898
898
|
}
|
|
899
899
|
function pluginCommandFailure(command, error) {
|
|
900
900
|
return {
|
|
@@ -1099,6 +1099,18 @@ async function main() {
|
|
|
1099
1099
|
if (!agent)
|
|
1100
1100
|
exitError("hook session-start requires --agent", json);
|
|
1101
1101
|
await cmdContext({ "no-search": true });
|
|
1102
|
+
try {
|
|
1103
|
+
const decision = await new InstalledPluginRuntimeV1({ coreVersion: VERSION }).runSessionStart({
|
|
1104
|
+
host: agent,
|
|
1105
|
+
cwd: process.cwd(),
|
|
1106
|
+
signal: new AbortController().signal,
|
|
1107
|
+
});
|
|
1108
|
+
if (decision?.state === "exhausted")
|
|
1109
|
+
console.error(`AgentMemory free session allowance resets at ${decision.resetAt}`);
|
|
1110
|
+
}
|
|
1111
|
+
catch {
|
|
1112
|
+
// Paid SessionStart work must never make public-core context unavailable.
|
|
1113
|
+
}
|
|
1102
1114
|
break;
|
|
1103
1115
|
}
|
|
1104
1116
|
case "plugin":
|
|
@@ -13,6 +13,15 @@ export interface PluginNextActionV1 {
|
|
|
13
13
|
userCode?: string;
|
|
14
14
|
message?: string;
|
|
15
15
|
}
|
|
16
|
+
export interface PluginSessionUsageDecisionV1 {
|
|
17
|
+
allowed: boolean;
|
|
18
|
+
state: "reserved" | "committed" | "released" | "exhausted" | "missing";
|
|
19
|
+
limit: number;
|
|
20
|
+
used: number;
|
|
21
|
+
remaining: number;
|
|
22
|
+
resetAt: string;
|
|
23
|
+
idempotent: boolean;
|
|
24
|
+
}
|
|
16
25
|
export interface PluginInstallReceiptV1 {
|
|
17
26
|
schemaVersion: 1;
|
|
18
27
|
bundleId: string;
|
|
@@ -105,6 +114,9 @@ export interface PluginBootstrapBackendV1 {
|
|
|
105
114
|
release: SignedPluginReleaseV1;
|
|
106
115
|
artifactGrant: string;
|
|
107
116
|
}): Promise<Uint8Array>;
|
|
117
|
+
reserveSession?(operationId: string): Promise<PluginSessionUsageDecisionV1>;
|
|
118
|
+
commitSession?(operationId: string): Promise<PluginSessionUsageDecisionV1>;
|
|
119
|
+
releaseSession?(operationId: string): Promise<PluginSessionUsageDecisionV1>;
|
|
108
120
|
getManagementAction(): Promise<PluginNextActionV1 | null>;
|
|
109
121
|
}
|
|
110
122
|
export interface PluginReleaseVerifierV1 {
|
package/dist/plugin-host.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type PluginPlanV1 = "free" | "trial" | "pro" | "team" | "enterprise";
|
|
|
5
5
|
export interface PluginCapabilityQuotaV1 {
|
|
6
6
|
limit: number;
|
|
7
7
|
window: "day";
|
|
8
|
-
scope: "device";
|
|
8
|
+
scope: "device" | "account";
|
|
9
9
|
}
|
|
10
10
|
export interface PluginCapabilityGrantV1 {
|
|
11
11
|
enabled: boolean;
|
package/dist/plugin-host.js
CHANGED
|
@@ -61,7 +61,7 @@ export function validatePluginEntitlementStatusV1(entitlement) {
|
|
|
61
61
|
if (grant.quota) {
|
|
62
62
|
if (!Number.isInteger(grant.quota.limit) || grant.quota.limit <= 0)
|
|
63
63
|
throw new Error(`Capability ${capability} has an invalid quota limit`);
|
|
64
|
-
if (grant.quota.window !== "day" || grant.quota.scope
|
|
64
|
+
if (grant.quota.window !== "day" || !["device", "account"].includes(grant.quota.scope))
|
|
65
65
|
throw new Error(`Capability ${capability} has an invalid quota policy`);
|
|
66
66
|
}
|
|
67
67
|
}
|
package/dist/plugin-runtime.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type PluginBootstrapBackendV1, type PluginInstallStoreV1, type SignedPluginReleaseV1 } from "./plugin-bootstrap.js";
|
|
2
|
-
import { type PluginCommandContextV1, type PluginCommandResultV1 } from "./plugin-host.js";
|
|
1
|
+
import { type PluginBootstrapBackendV1, type PluginInstallStoreV1, type PluginSessionUsageDecisionV1, type SignedPluginReleaseV1 } from "./plugin-bootstrap.js";
|
|
2
|
+
import { type PluginCommandContextV1, type PluginCommandResultV1, type PluginSessionStartHookV1 } from "./plugin-host.js";
|
|
3
3
|
export interface PluginRuntimeOptionsV1 {
|
|
4
4
|
coreVersion: string;
|
|
5
5
|
store?: PluginInstallStoreV1;
|
|
@@ -15,6 +15,7 @@ export declare class InstalledPluginRuntimeV1 {
|
|
|
15
15
|
constructor(options: PluginRuntimeOptionsV1);
|
|
16
16
|
load(): Promise<boolean>;
|
|
17
17
|
run(name: string, context: PluginCommandContextV1): Promise<PluginCommandResultV1 | null>;
|
|
18
|
+
runSessionStart(context: Parameters<PluginSessionStartHookV1["run"]>[0]): Promise<PluginSessionUsageDecisionV1 | null>;
|
|
18
19
|
private createHost;
|
|
19
20
|
private refreshEntitlement;
|
|
20
21
|
}
|
package/dist/plugin-runtime.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { pathToFileURL } from "node:url";
|
|
@@ -102,6 +103,35 @@ export class InstalledPluginRuntimeV1 {
|
|
|
102
103
|
};
|
|
103
104
|
return registered.command.run(context);
|
|
104
105
|
}
|
|
106
|
+
async runSessionStart(context) {
|
|
107
|
+
if (!(await this.load()) || this.hooks.length === 0)
|
|
108
|
+
return null;
|
|
109
|
+
const entitlement = await this.refreshEntitlement();
|
|
110
|
+
const eligible = this.hooks.filter((hook) => isPluginCapabilityEnabled(entitlement, hook.requiredCapability));
|
|
111
|
+
if (eligible.length === 0)
|
|
112
|
+
return null;
|
|
113
|
+
const metered = eligible.some((hook) => entitlement.capabilities[hook.requiredCapability]?.quota?.scope === "account");
|
|
114
|
+
if (!metered) {
|
|
115
|
+
for (const hook of eligible)
|
|
116
|
+
await hook.run(context);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
if (!this.backend.reserveSession || !this.backend.commitSession || !this.backend.releaseSession)
|
|
120
|
+
throw new PluginBootstrapFailure("session_usage_unavailable", "Account session metering is unavailable");
|
|
121
|
+
const operationId = randomUUID();
|
|
122
|
+
const reservation = await this.backend.reserveSession(operationId);
|
|
123
|
+
if (!reservation.allowed)
|
|
124
|
+
return reservation;
|
|
125
|
+
try {
|
|
126
|
+
for (const hook of eligible)
|
|
127
|
+
await hook.run(context);
|
|
128
|
+
return await this.backend.commitSession(operationId);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
await this.backend.releaseSession(operationId);
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
105
135
|
createHost(manifest) {
|
|
106
136
|
const descriptors = new Map(manifest.commands.map((command) => [command.name, command]));
|
|
107
137
|
const stateRoot = path.join(this.store.root, "state");
|
package/dist/plugin-service.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PluginAccessDecisionV1, type PluginBootstrapBackendV1, type PluginNextActionV1, type SignedPluginReleaseV1 } from "./plugin-bootstrap.js";
|
|
1
|
+
import { type PluginAccessDecisionV1, type PluginBootstrapBackendV1, type PluginNextActionV1, type PluginSessionUsageDecisionV1, type SignedPluginReleaseV1 } from "./plugin-bootstrap.js";
|
|
2
2
|
import { type PluginEntitlementStatusV1 } from "./plugin-host.js";
|
|
3
3
|
interface TemporaryPluginBackendOptions {
|
|
4
4
|
root?: string;
|
|
@@ -27,6 +27,9 @@ export declare class TemporaryPluginBackend implements PluginBootstrapBackendV1
|
|
|
27
27
|
channel: string;
|
|
28
28
|
allowAuthentication: boolean;
|
|
29
29
|
}): Promise<PluginAccessDecisionV1>;
|
|
30
|
+
reserveSession(operationId: string): Promise<PluginSessionUsageDecisionV1>;
|
|
31
|
+
commitSession(operationId: string): Promise<PluginSessionUsageDecisionV1>;
|
|
32
|
+
releaseSession(operationId: string): Promise<PluginSessionUsageDecisionV1>;
|
|
30
33
|
listReleases(request: {
|
|
31
34
|
bundleId: string;
|
|
32
35
|
channel: string;
|
|
@@ -40,6 +43,7 @@ export declare class TemporaryPluginBackend implements PluginBootstrapBackendV1
|
|
|
40
43
|
private activationPath;
|
|
41
44
|
private readActivation;
|
|
42
45
|
private writeActivation;
|
|
46
|
+
private sessionUsage;
|
|
43
47
|
private request;
|
|
44
48
|
}
|
|
45
49
|
export {};
|
package/dist/plugin-service.js
CHANGED
|
@@ -12,31 +12,37 @@ const REQUEST_TIMEOUT_MS = 30_000;
|
|
|
12
12
|
const EMAIL_MAX_BYTES = 254;
|
|
13
13
|
const FORM_MAX_BYTES = 2_048;
|
|
14
14
|
const SERVICE_JSON_MAX_BYTES = 1024 * 1024;
|
|
15
|
+
const ACTIVATION_CREDENTIAL = /^am_activation_[A-Za-z0-9_-]{32,256}$/;
|
|
15
16
|
const MISSING_ENTITLEMENT = {
|
|
16
17
|
plan: null,
|
|
17
18
|
state: "missing",
|
|
18
19
|
features: [],
|
|
19
20
|
capabilities: {},
|
|
20
|
-
reason: "Enter an email address to activate
|
|
21
|
-
};
|
|
22
|
-
const TEMPORARY_ENTITLEMENT = {
|
|
23
|
-
plan: "pro",
|
|
24
|
-
state: "active",
|
|
25
|
-
features: ["session-intelligence", "web-console"],
|
|
26
|
-
capabilities: Object.fromEntries([
|
|
27
|
-
"session-index",
|
|
28
|
-
"session-worker",
|
|
29
|
-
"learning",
|
|
30
|
-
"retrieval-evaluation",
|
|
31
|
-
"operational-metrics",
|
|
32
|
-
"web-console",
|
|
33
|
-
"memory-explorer",
|
|
34
|
-
].map((capability) => [capability, { enabled: true }])),
|
|
35
|
-
reason: "Temporary email activation grants unlimited local use",
|
|
21
|
+
reason: "Enter an email address to activate the free daily session allowance",
|
|
36
22
|
};
|
|
37
23
|
function cloneEntitlement(value) {
|
|
38
24
|
return structuredClone(value);
|
|
39
25
|
}
|
|
26
|
+
function freeEntitlement(dailySessionLimit) {
|
|
27
|
+
return {
|
|
28
|
+
plan: "free",
|
|
29
|
+
state: "active",
|
|
30
|
+
features: ["session-intelligence", "web-console"],
|
|
31
|
+
capabilities: {
|
|
32
|
+
"session-index": { enabled: true },
|
|
33
|
+
"session-worker": {
|
|
34
|
+
enabled: true,
|
|
35
|
+
quota: { limit: dailySessionLimit, window: "day", scope: "account" },
|
|
36
|
+
},
|
|
37
|
+
learning: { enabled: true },
|
|
38
|
+
"retrieval-evaluation": { enabled: true },
|
|
39
|
+
"operational-metrics": { enabled: true },
|
|
40
|
+
"web-console": { enabled: true },
|
|
41
|
+
"memory-explorer": { enabled: true },
|
|
42
|
+
},
|
|
43
|
+
reason: `${dailySessionLimit} free agent sessions per UTC day`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
40
46
|
function isEmail(value) {
|
|
41
47
|
return (Buffer.byteLength(value, "utf-8") <= EMAIL_MAX_BYTES &&
|
|
42
48
|
[...value].every((character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127) &&
|
|
@@ -51,12 +57,102 @@ function securityHeaders(contentType) {
|
|
|
51
57
|
"X-Content-Type-Options": "nosniff",
|
|
52
58
|
};
|
|
53
59
|
}
|
|
60
|
+
const ACTIVATION_PAGE_STYLES = `
|
|
61
|
+
:root{color-scheme:light;font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#172033;background:#f4f7fb;font-synthesis:none}
|
|
62
|
+
*{box-sizing:border-box}
|
|
63
|
+
body{min-height:100vh;margin:0;padding:2rem 1.25rem;display:grid;place-items:center;background:radial-gradient(circle at 12% 0%,#e8f0ff 0,transparent 38rem),radial-gradient(circle at 100% 100%,#eef2ff 0,transparent 32rem),#f7f9fc}
|
|
64
|
+
main{width:min(100%,34rem)}
|
|
65
|
+
.skip-link{position:fixed;z-index:1;top:1rem;left:1rem;padding:.65rem .8rem;border-radius:.55rem;background:#172033;color:#fff;font-weight:700;transform:translateY(-200%)}
|
|
66
|
+
.skip-link:focus{transform:translateY(0)}
|
|
67
|
+
.brand{display:flex;align-items:center;gap:.75rem;margin:0 0 1.25rem .25rem;color:#33415c;font-size:.875rem;font-weight:700;letter-spacing:.01em}
|
|
68
|
+
.mark{width:2.25rem;height:2.25rem;display:grid;place-items:center;border-radius:.7rem;background:linear-gradient(145deg,#172033,#3159b8);color:#fff;box-shadow:0 .5rem 1.25rem rgba(35,65,130,.2);font-size:1.05rem;letter-spacing:-.08em}
|
|
69
|
+
.card{padding:clamp(1.5rem,5vw,2.5rem);border:1px solid rgba(205,216,231,.9);border-radius:1.5rem;background:rgba(255,255,255,.94);box-shadow:0 1.5rem 4rem rgba(32,51,84,.12),0 .125rem .375rem rgba(32,51,84,.06);backdrop-filter:blur(1rem)}
|
|
70
|
+
.eyebrow{display:inline-flex;align-items:center;gap:.45rem;margin:0 0 1rem;padding:.42rem .7rem;border:1px solid #d9e4f7;border-radius:999px;background:#f3f7ff;color:#315798;font-size:.78rem;font-weight:750;letter-spacing:.03em;text-transform:uppercase}
|
|
71
|
+
.eyebrow::before{content:"";width:.45rem;height:.45rem;border-radius:50%;background:#2b67d1;box-shadow:0 0 0 .22rem #dbe8ff}
|
|
72
|
+
h1{margin:0;color:#141d2c;font-size:clamp(2rem,7vw,2.75rem);line-height:1.06;letter-spacing:-.045em;text-wrap:balance}
|
|
73
|
+
.intro{margin:.9rem 0 1.75rem;color:#526176;font-size:1.05rem;line-height:1.55}
|
|
74
|
+
form{display:grid;gap:.75rem}
|
|
75
|
+
label{color:#29364a;font-size:.9rem;font-weight:700}
|
|
76
|
+
input{width:100%;min-height:3.25rem;padding:.8rem 3.5rem .8rem 1rem;border:1px solid #bcc8d8;border-radius:.8rem;background:#fff;color:#172033;font:inherit;box-shadow:inset 0 1px 2px rgba(24,39,65,.04)}
|
|
77
|
+
input::placeholder{color:#8a96a7}
|
|
78
|
+
input:hover{border-color:#92a2b8}
|
|
79
|
+
input:focus-visible{outline:0;border-color:#2b67d1;box-shadow:0 0 0 .25rem rgba(43,103,209,.15)}
|
|
80
|
+
button{min-height:3.35rem;margin-top:.25rem;padding:.85rem 1rem;border:1px solid #172033;border-radius:.8rem;background:linear-gradient(180deg,#24334a,#172033);color:#fff;font:inherit;font-weight:750;cursor:pointer;box-shadow:0 .55rem 1.2rem rgba(23,32,51,.18);touch-action:manipulation;transition:transform .15s}
|
|
81
|
+
button:hover{background:linear-gradient(180deg,#2d405c,#1d2a40);box-shadow:0 .7rem 1.4rem rgba(23,32,51,.23);transform:translateY(-1px)}
|
|
82
|
+
button:active{transform:translateY(0)}
|
|
83
|
+
button:focus-visible,summary:focus-visible{outline:.2rem solid rgba(43,103,209,.32);outline-offset:.18rem}
|
|
84
|
+
.terminal-note{margin:.8rem 0 0;color:#6a7789;font-size:.85rem;text-align:center}
|
|
85
|
+
.error{margin:0 0 1rem;padding:.8rem 1rem;border:1px solid #efb8bd;border-radius:.75rem;background:#fff3f4;color:#982631;font-size:.9rem;line-height:1.45}
|
|
86
|
+
details{margin-top:1.5rem;padding-top:1.25rem;border-top:1px solid #e2e8f0;color:#5b687a;font-size:.85rem;line-height:1.55}
|
|
87
|
+
summary{color:#46566d;font-weight:700;cursor:pointer;list-style-position:outside;touch-action:manipulation}
|
|
88
|
+
details p{margin:.8rem 0 0}
|
|
89
|
+
.never-sent{padding:.75rem .85rem;border-radius:.65rem;background:#f5f7fa;color:#536176}
|
|
90
|
+
.success{display:grid;place-items:center;width:3.5rem;height:3.5rem;margin-bottom:1.4rem;border-radius:1rem;background:#eaf7ef;color:#197542;font-size:1.65rem;font-weight:800;box-shadow:inset 0 0 0 1px #c9ead6}
|
|
91
|
+
.completion .intro{margin-bottom:0}
|
|
92
|
+
@media (max-width:30rem){body{padding:1rem}.brand{margin-left:.1rem}.card{border-radius:1.15rem}h1{font-size:2rem}}
|
|
93
|
+
@media (prefers-reduced-motion:reduce){input,button{transition:none}}
|
|
94
|
+
`;
|
|
54
95
|
function activationPage(action, error) {
|
|
55
|
-
const errorHtml = error ? `<p class="error">${error}</p>` : "";
|
|
56
|
-
|
|
96
|
+
const errorHtml = error ? `<p class="error" id="activation-error" role="alert">${error}</p>` : "";
|
|
97
|
+
const describedBy = error ? "activation-error terminal-note" : "terminal-note";
|
|
98
|
+
const autofocus = error ? " autofocus" : "";
|
|
99
|
+
return `<!doctype html>
|
|
100
|
+
<html lang="en">
|
|
101
|
+
<head>
|
|
102
|
+
<meta charset="utf-8">
|
|
103
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
104
|
+
<meta name="theme-color" content="#f4f7fb">
|
|
105
|
+
<title>Activate AgentMemory</title>
|
|
106
|
+
<style>${ACTIVATION_PAGE_STYLES}</style>
|
|
107
|
+
</head>
|
|
108
|
+
<body>
|
|
109
|
+
<a class="skip-link" href="#activation">Skip to Activation</a>
|
|
110
|
+
<main id="activation">
|
|
111
|
+
<div class="brand" translate="no"><span class="mark" aria-hidden="true">AM</span><span>AgentMemory</span></div>
|
|
112
|
+
<section class="card" aria-labelledby="activation-title">
|
|
113
|
+
<p class="eyebrow">Free daily allowance</p>
|
|
114
|
+
<h1 id="activation-title">Activate This Device</h1>
|
|
115
|
+
<p class="intro">Use your email to enable AgentMemory Pro for your local agent sessions.</p>
|
|
116
|
+
<form method="post" action="${action}">
|
|
117
|
+
<label for="email">Email address</label>
|
|
118
|
+
${errorHtml}
|
|
119
|
+
<input id="email" type="email" name="email" autocomplete="email" inputmode="email" spellcheck="false" placeholder="you@example.com…" maxlength="254" required${autofocus} aria-describedby="${describedBy}">
|
|
120
|
+
<button type="submit">Activate AgentMemory</button>
|
|
121
|
+
</form>
|
|
122
|
+
<p class="terminal-note" id="terminal-note">Your terminal will finish setup after activation.</p>
|
|
123
|
+
<details>
|
|
124
|
+
<summary>What’s shared during activation</summary>
|
|
125
|
+
<p>Your email identifies your free daily allowance. The CLI also sends core and bundle versions, platform, architecture, and release channel. The service stores a daily count of opaque session-start operations. Activation records expire after 365 days without use.</p>
|
|
126
|
+
<p class="never-sent"><strong>Never sent:</strong> The request never includes memory, session content, queries, repository paths, raw agent session identifiers, IP addresses, or user-agent strings.</p>
|
|
127
|
+
</details>
|
|
128
|
+
</section>
|
|
129
|
+
</main>
|
|
130
|
+
</body>
|
|
131
|
+
</html>`;
|
|
57
132
|
}
|
|
58
133
|
function completionPage() {
|
|
59
|
-
return
|
|
134
|
+
return `<!doctype html>
|
|
135
|
+
<html lang="en">
|
|
136
|
+
<head>
|
|
137
|
+
<meta charset="utf-8">
|
|
138
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
139
|
+
<meta name="theme-color" content="#f4f7fb">
|
|
140
|
+
<title>AgentMemory activated</title>
|
|
141
|
+
<style>${ACTIVATION_PAGE_STYLES}</style>
|
|
142
|
+
</head>
|
|
143
|
+
<body>
|
|
144
|
+
<a class="skip-link" href="#completion">Skip to Activation Status</a>
|
|
145
|
+
<main id="completion">
|
|
146
|
+
<div class="brand" translate="no"><span class="mark" aria-hidden="true">AM</span><span>AgentMemory</span></div>
|
|
147
|
+
<section class="card completion" aria-labelledby="completion-title">
|
|
148
|
+
<div class="success" aria-hidden="true">✓</div>
|
|
149
|
+
<p class="eyebrow">Device activated</p>
|
|
150
|
+
<h1 id="completion-title">You’re All Set</h1>
|
|
151
|
+
<p class="intro">Return to your terminal to finish installing AgentMemory Pro. You can close this tab.</p>
|
|
152
|
+
</section>
|
|
153
|
+
</main>
|
|
154
|
+
</body>
|
|
155
|
+
</html>`;
|
|
60
156
|
}
|
|
61
157
|
function send(response, status, body, contentType = "text/html; charset=utf-8") {
|
|
62
158
|
response.writeHead(status, securityHeaders(contentType));
|
|
@@ -253,11 +349,13 @@ export class TemporaryPluginBackend {
|
|
|
253
349
|
this.activate = options.activate ?? (() => collectTemporaryActivation(this.openUrl));
|
|
254
350
|
}
|
|
255
351
|
async getLocalEntitlement() {
|
|
256
|
-
|
|
352
|
+
const activation = this.readActivation();
|
|
353
|
+
return activation ? freeEntitlement(activation.dailySessionLimit) : cloneEntitlement(MISSING_ENTITLEMENT);
|
|
257
354
|
}
|
|
258
355
|
async resolveAccess(request) {
|
|
259
|
-
|
|
260
|
-
|
|
356
|
+
const activation = this.readActivation();
|
|
357
|
+
let email = activation?.email;
|
|
358
|
+
if (!email) {
|
|
261
359
|
if (!request.allowAuthentication)
|
|
262
360
|
return {
|
|
263
361
|
kind: "auth_required",
|
|
@@ -268,33 +366,48 @@ export class TemporaryPluginBackend {
|
|
|
268
366
|
message: "Run plugin install in an interactive terminal to enter an email address",
|
|
269
367
|
},
|
|
270
368
|
};
|
|
271
|
-
|
|
272
|
-
this.writeActivation(email);
|
|
273
|
-
activation = this.readActivation();
|
|
369
|
+
email = await this.activate();
|
|
274
370
|
}
|
|
275
|
-
if (!activation)
|
|
276
|
-
throw new PluginBootstrapFailure("activation_failed", "The local activation record could not be loaded");
|
|
277
371
|
const response = await this.request(`${this.apiOrigin}/v1/plugin/access`, {
|
|
278
372
|
method: "POST",
|
|
279
373
|
headers: { "Content-Type": "application/json" },
|
|
280
374
|
body: JSON.stringify({
|
|
281
375
|
schemaVersion: 1,
|
|
282
|
-
email
|
|
376
|
+
email,
|
|
283
377
|
bundleId: request.bundleId,
|
|
284
378
|
installedVersion: request.installedVersion ?? null,
|
|
285
379
|
coreVersion: this.coreVersion,
|
|
286
380
|
channel: request.channel,
|
|
287
381
|
platform: process.platform,
|
|
288
382
|
architecture: process.arch,
|
|
289
|
-
consentVersion: "activation-
|
|
383
|
+
consentVersion: "activation-v2",
|
|
290
384
|
}),
|
|
291
385
|
});
|
|
292
386
|
const value = (await readJson(response));
|
|
293
387
|
validatePluginEntitlementStatusV1(value.entitlement);
|
|
294
388
|
if (typeof value.artifactGrant !== "string" || !value.artifactGrant)
|
|
295
389
|
throw new PluginBootstrapFailure("service_response_invalid", "The access response omitted its artifact grant");
|
|
390
|
+
if (typeof value.usageCredential !== "string" || !ACTIVATION_CREDENTIAL.test(value.usageCredential))
|
|
391
|
+
throw new PluginBootstrapFailure("service_response_invalid", "The access response omitted its usage credential");
|
|
392
|
+
const freeQuota = value.entitlement.capabilities["session-worker"]?.quota;
|
|
393
|
+
if (value.entitlement.plan !== "free" ||
|
|
394
|
+
value.entitlement.state !== "active" ||
|
|
395
|
+
!freeQuota ||
|
|
396
|
+
freeQuota.scope !== "account" ||
|
|
397
|
+
freeQuota.window !== "day")
|
|
398
|
+
throw new PluginBootstrapFailure("service_response_invalid", "The free session policy is invalid");
|
|
399
|
+
this.writeActivation(email, value.usageCredential, freeQuota.limit);
|
|
296
400
|
return { kind: "granted", entitlement: value.entitlement, artifactGrant: value.artifactGrant };
|
|
297
401
|
}
|
|
402
|
+
async reserveSession(operationId) {
|
|
403
|
+
return this.sessionUsage("reserve", operationId);
|
|
404
|
+
}
|
|
405
|
+
async commitSession(operationId) {
|
|
406
|
+
return this.sessionUsage("commit", operationId);
|
|
407
|
+
}
|
|
408
|
+
async releaseSession(operationId) {
|
|
409
|
+
return this.sessionUsage("release", operationId);
|
|
410
|
+
}
|
|
298
411
|
async listReleases(request) {
|
|
299
412
|
const response = await this.request(`${this.apiOrigin}/v1/plugin/releases`, {
|
|
300
413
|
headers: { Authorization: `Bearer ${request.artifactGrant}` },
|
|
@@ -338,17 +451,27 @@ export class TemporaryPluginBackend {
|
|
|
338
451
|
if (!stat.isFile() || stat.isSymbolicLink() || (process.platform !== "win32" && (stat.mode & 0o077) !== 0))
|
|
339
452
|
return null;
|
|
340
453
|
const value = JSON.parse(fs.readFileSync(activationPath, "utf-8"));
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
454
|
+
if (value.schemaVersion !== 2 ||
|
|
455
|
+
!isEmail(value.email) ||
|
|
456
|
+
!Number.isFinite(Date.parse(value.activatedAt)) ||
|
|
457
|
+
!ACTIVATION_CREDENTIAL.test(value.usageCredential) ||
|
|
458
|
+
!Number.isSafeInteger(value.dailySessionLimit) ||
|
|
459
|
+
value.dailySessionLimit <= 0 ||
|
|
460
|
+
value.dailySessionLimit > 10_000)
|
|
461
|
+
return null;
|
|
462
|
+
return value;
|
|
344
463
|
}
|
|
345
464
|
catch {
|
|
346
465
|
return null;
|
|
347
466
|
}
|
|
348
467
|
}
|
|
349
|
-
writeActivation(email) {
|
|
468
|
+
writeActivation(email, usageCredential, dailySessionLimit) {
|
|
350
469
|
if (!isEmail(email))
|
|
351
470
|
throw new PluginBootstrapFailure("email_invalid", "Enter a valid email address");
|
|
471
|
+
if (!ACTIVATION_CREDENTIAL.test(usageCredential))
|
|
472
|
+
throw new PluginBootstrapFailure("activation_failed", "The activation credential is invalid");
|
|
473
|
+
if (!Number.isSafeInteger(dailySessionLimit) || dailySessionLimit <= 0 || dailySessionLimit > 10_000)
|
|
474
|
+
throw new PluginBootstrapFailure("activation_failed", "The free session allowance is invalid");
|
|
352
475
|
const target = this.activationPath();
|
|
353
476
|
fs.mkdirSync(this.root, { recursive: true, mode: 0o700 });
|
|
354
477
|
const rootStat = fs.lstatSync(this.root);
|
|
@@ -361,9 +484,38 @@ export class TemporaryPluginBackend {
|
|
|
361
484
|
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink())
|
|
362
485
|
throw new PluginBootstrapFailure("activation_path_invalid", "The plugin activation directory is unsafe");
|
|
363
486
|
const temporary = `${target}.tmp-${process.pid}-${randomUUID()}`;
|
|
364
|
-
fs.writeFileSync(temporary, `${JSON.stringify({ schemaVersion:
|
|
487
|
+
fs.writeFileSync(temporary, `${JSON.stringify({ schemaVersion: 2, email, activatedAt: new Date().toISOString(), usageCredential, dailySessionLimit }, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
365
488
|
fs.renameSync(temporary, target);
|
|
366
489
|
}
|
|
490
|
+
async sessionUsage(action, operationId) {
|
|
491
|
+
const activation = this.readActivation();
|
|
492
|
+
if (!activation)
|
|
493
|
+
throw new PluginBootstrapFailure("auth_required", "Run plugin install to activate AgentMemory");
|
|
494
|
+
const response = await this.request(`${this.apiOrigin}/v1/plugin/sessions/${action}`, {
|
|
495
|
+
method: "POST",
|
|
496
|
+
headers: {
|
|
497
|
+
Authorization: `Bearer ${activation.usageCredential}`,
|
|
498
|
+
"Content-Type": "application/json",
|
|
499
|
+
},
|
|
500
|
+
body: JSON.stringify({ schemaVersion: 1, operationId }),
|
|
501
|
+
});
|
|
502
|
+
const value = (await readJson(response));
|
|
503
|
+
const decision = value.decision;
|
|
504
|
+
if (!decision ||
|
|
505
|
+
typeof decision.allowed !== "boolean" ||
|
|
506
|
+
!["reserved", "committed", "released", "exhausted", "missing"].includes(String(decision.state)) ||
|
|
507
|
+
!Number.isSafeInteger(decision.limit) ||
|
|
508
|
+
Number(decision.limit) <= 0 ||
|
|
509
|
+
!Number.isSafeInteger(decision.used) ||
|
|
510
|
+
Number(decision.used) < 0 ||
|
|
511
|
+
!Number.isSafeInteger(decision.remaining) ||
|
|
512
|
+
Number(decision.remaining) < 0 ||
|
|
513
|
+
typeof decision.resetAt !== "string" ||
|
|
514
|
+
!Number.isFinite(Date.parse(decision.resetAt)) ||
|
|
515
|
+
typeof decision.idempotent !== "boolean")
|
|
516
|
+
throw new PluginBootstrapFailure("service_response_invalid", "The session usage response is invalid");
|
|
517
|
+
return decision;
|
|
518
|
+
}
|
|
367
519
|
async request(url, init = {}) {
|
|
368
520
|
let response;
|
|
369
521
|
try {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Status
|
|
4
4
|
|
|
5
|
-
Accepted design on 2026-08-16. The public core
|
|
5
|
+
Accepted design on 2026-08-16 and revised on 2026-08-17. The public core implements host types, loopback email activation, live catalog and artifact retrieval, Ed25519 release verification, bounded package validation, transactional install, bundle health checks, paid-command dispatch, and SessionStart hook dispatch. The free plan grants a configurable number of agent sessions per normalized email and UTC day; durable account authentication, payment, renewal, and account management remain deferred.
|
|
6
6
|
|
|
7
7
|
The public `agentmemory` repository and `myagentmemory` npm package remain the free, MIT-licensed core. The public bootstrap client and host contracts are also MIT-licensed. Official commercial implementations and browser assets are built and distributed separately from the private `agent-memory-plugin` workspace under their own terms. Pricing, the billing provider, device limits, offline-grace duration, and Enterprise contract terms are intentionally not decided here. The temporary beta currently uses allowlisted `*.agentmemory.paperpilot.me` service origins; changing those origins is a public-client release change.
|
|
8
8
|
|
|
@@ -23,11 +23,11 @@ agent-memory plugin install
|
|
|
23
23
|
| Plugin absent | Active or grace | Install the compatible signed bundle |
|
|
24
24
|
| Plugin older than the selected release | Active or grace | Upgrade atomically |
|
|
25
25
|
| Plugin current | Active or grace | Report that it is current |
|
|
26
|
-
| Any | Missing | Start
|
|
26
|
+
| Any | Missing | Start loopback email activation in an interactive terminal |
|
|
27
27
|
| Any | Expired | Direct the user to renewal; leave core available |
|
|
28
28
|
| Incompatible bundle | Any | Leave the current version untouched and explain the required core version |
|
|
29
29
|
|
|
30
|
-
An absent plugin cannot activate itself. The public bootstrap
|
|
30
|
+
An absent plugin cannot activate itself. The public bootstrap collects an email locally, obtains a server-issued usage credential and short-lived artifact grant, and verifies the release and artifact. The credential is persisted only after the service accepts activation. After installation, the public host reconstructs the free account-metered capability policy in core code and checks required capabilities before commands and hooks. Signed long-lived entitlements can extend this credential when authentication and payment ship.
|
|
31
31
|
|
|
32
32
|
## Ownership boundary
|
|
33
33
|
|
|
@@ -81,7 +81,7 @@ agent-memory plugin manage [--no-browser]
|
|
|
81
81
|
- `status` is read-only. It reports the installed bundle, selected channel, compatibility, entitlement state, and update availability.
|
|
82
82
|
- `install` authenticates when necessary, then installs, upgrades, or reports current state.
|
|
83
83
|
- `update` requires an existing installation and never starts a new purchase implicitly.
|
|
84
|
-
- `uninstall` removes executable plugin material and the active receipt. It preserves core memory, plugin state, and the
|
|
84
|
+
- `uninstall` removes executable plugin material and the active receipt. It preserves core memory, plugin state, and the permission-restricted activation credential.
|
|
85
85
|
- `manage` remains unavailable until authenticated account and billing management exists.
|
|
86
86
|
|
|
87
87
|
Installed plugins contribute top-level commands such as `recall`, `learn`, `worker`, and `web`. Bootstrap command names are reserved by the core and cannot be replaced by a plugin.
|
|
@@ -133,16 +133,17 @@ Every bootstrap command supports `--json` and emits one JSON document with a ver
|
|
|
133
133
|
|
|
134
134
|
`result` is one of `not_installed`, `installed`, `upgraded`, `current`, `update_available`, `uninstalled`, `auth_required`, `renewal_required`, or `unavailable`. Failures use `ok: false` plus a stable `error.code` and redacted `error.message`. Output must never contain access tokens, download credentials, signed entitlement contents, local memory paths, or URLs containing bearer credentials.
|
|
135
135
|
|
|
136
|
-
##
|
|
136
|
+
## Free activation flow
|
|
137
137
|
|
|
138
138
|
1. `agent-memory plugin install` starts an HTTP server bound to `127.0.0.1` on an ephemeral port.
|
|
139
139
|
2. The CLI prints and opens a nonce-bearing local URL. The page accepts one email address with bounded input, an exact Host and nonce path, same-origin browser request validation, restrictive response headers, and a five-minute deadline.
|
|
140
|
-
3. Submission
|
|
141
|
-
4. The waiting CLI sends the email plus core, installed-bundle, platform, architecture, release-channel, and consent-version fields to the private control plane. Its activation database stores none of the user's memory,
|
|
142
|
-
5. The
|
|
143
|
-
6.
|
|
140
|
+
3. Submission returns a completion page but does not create a local credential yet.
|
|
141
|
+
4. The waiting CLI sends the email plus core, installed-bundle, platform, architecture, release-channel, and consent-version fields to the private control plane. Its activation database stores none of the user's memory, session content, queries, repository paths, raw agent session identifiers, IP address, or user-agent string.
|
|
142
|
+
5. The service normalizes the email, stores only a hash of a random usage credential, and returns the credential, a free account-metered entitlement, and a short-lived object-bound artifact grant. Only then does the CLI atomically write a mode-0600 activation record.
|
|
143
|
+
6. The CLI verifies the Ed25519-signed release plus package digest and limits, imports it for health checks, and atomically activates the receipt.
|
|
144
|
+
7. Each paid SessionStart hook reserves one opaque operation against the email's UTC-day allowance, commits after useful hook work, and releases on failure. Exhaustion skips paid hook work without affecting public-core context.
|
|
144
145
|
|
|
145
|
-
|
|
146
|
+
Email ownership is not verified in this free flow. Authentication, payment, renewal, account management, and signed paid entitlements are not implemented yet.
|
|
146
147
|
|
|
147
148
|
## Future authentication and purchase flow
|
|
148
149
|
|
|
@@ -160,13 +161,14 @@ An Enterprise administrator may pre-provision an organization entitlement or man
|
|
|
160
161
|
|
|
161
162
|
## Control-plane boundary
|
|
162
163
|
|
|
163
|
-
The
|
|
164
|
+
The service exposes:
|
|
164
165
|
|
|
165
|
-
- `POST /v1/plugin/access` for
|
|
166
|
+
- `POST /v1/plugin/access` for a free account-metered entitlement, a usage credential, and a short-lived artifact grant;
|
|
167
|
+
- `POST /v1/plugin/sessions/reserve|commit|release` for atomic daily allowance enforcement;
|
|
166
168
|
- `GET /v1/plugin/releases` for an Ed25519-signed release selected from the private R2 catalog;
|
|
167
169
|
- `GET|HEAD /v1/artifacts/download` for the exact content-addressed object authorized by the bearer grant.
|
|
168
170
|
|
|
169
|
-
The
|
|
171
|
+
The access request contains the submitted email plus the bounded core, bundle, platform, architecture, release-channel, and consent-version fields described above. Session metering sends only a random operation ID and bearer credential; D1 associates those values with a normalized email and UTC-day counter. Application payloads contain no memory content, search query, session content, path, repository name, raw agent session identifier, qmd data, IP address, or user-agent string. The activation database stores neither IP addresses nor user-agent strings. Future authenticated service responsibilities include:
|
|
170
172
|
|
|
171
173
|
- create and poll a device authorization;
|
|
172
174
|
- read the authenticated principal's effective entitlement;
|
|
@@ -181,9 +183,9 @@ The bootstrap may send only:
|
|
|
181
183
|
- core version, plugin-host API version, platform, and architecture;
|
|
182
184
|
- requested bundle ID, installed bundle version, and release channel;
|
|
183
185
|
- a pseudonymous license or organization identifier;
|
|
184
|
-
- protocol nonces and authentication material required for the request.
|
|
186
|
+
- protocol nonces, opaque quota operation IDs, and authentication material required for the request.
|
|
185
187
|
|
|
186
|
-
It must never send memory contents, search queries, session contents, working-directory names, repository names, filesystem paths, qmd data
|
|
188
|
+
It must never send memory contents, search queries, session contents, raw agent session identifiers, working-directory names, repository names, filesystem paths, or qmd data. The bounded allowance counter is authorization state, not general product telemetry.
|
|
187
189
|
|
|
188
190
|
Production builds use an allowlisted HTTPS origin. Development endpoint overrides must be explicit, must not silently affect production builds, and must never weaken TLS verification.
|
|
189
191
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myagentmemory",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.16",
|
|
4
4
|
"description": "agentmemory (agent-memory) is persistent memory for coding agents (Claude Code, OpenAI Codex, Cursor, Agent) with qmd-powered semantic search across daily logs, long-term memory, and scratchpad",
|
|
5
5
|
"main": "./dist/core.js",
|
|
6
6
|
"types": "./dist/core.d.ts",
|
package/src/cli.ts
CHANGED
|
@@ -244,7 +244,9 @@ function printPluginResult(result: PluginBootstrapResultV1, json: boolean, allow
|
|
|
244
244
|
console.log("Run: agent-memory plugin install");
|
|
245
245
|
break;
|
|
246
246
|
case "auth_required":
|
|
247
|
-
console.log(
|
|
247
|
+
console.log(
|
|
248
|
+
"Run this command in an interactive terminal to enter an email and activate free daily access.",
|
|
249
|
+
);
|
|
248
250
|
break;
|
|
249
251
|
case "renewal_required":
|
|
250
252
|
console.log("Renew AgentMemory Pro to continue using paid capabilities.");
|
|
@@ -1000,8 +1002,8 @@ Usage:
|
|
|
1000
1002
|
agent-memory plugin manage [--no-browser]
|
|
1001
1003
|
|
|
1002
1004
|
The public core remains fully usable without AgentMemory Pro. Interactive install
|
|
1003
|
-
opens a loopback website for
|
|
1004
|
-
|
|
1005
|
+
opens a loopback website for email activation and a configurable free daily
|
|
1006
|
+
agent-session allowance. Memory and session content stay on this device.`);
|
|
1005
1007
|
}
|
|
1006
1008
|
|
|
1007
1009
|
function pluginCommandFailure(command: string, error: unknown): PluginBootstrapResultV1 {
|
|
@@ -1227,6 +1229,17 @@ async function main() {
|
|
|
1227
1229
|
const agent = getFlag(flags, "agent");
|
|
1228
1230
|
if (!agent) exitError("hook session-start requires --agent", json);
|
|
1229
1231
|
await cmdContext({ "no-search": true });
|
|
1232
|
+
try {
|
|
1233
|
+
const decision = await new InstalledPluginRuntimeV1({ coreVersion: VERSION }).runSessionStart({
|
|
1234
|
+
host: agent,
|
|
1235
|
+
cwd: process.cwd(),
|
|
1236
|
+
signal: new AbortController().signal,
|
|
1237
|
+
});
|
|
1238
|
+
if (decision?.state === "exhausted")
|
|
1239
|
+
console.error(`AgentMemory free session allowance resets at ${decision.resetAt}`);
|
|
1240
|
+
} catch {
|
|
1241
|
+
// Paid SessionStart work must never make public-core context unavailable.
|
|
1242
|
+
}
|
|
1230
1243
|
break;
|
|
1231
1244
|
}
|
|
1232
1245
|
case "plugin":
|
package/src/plugin-bootstrap.ts
CHANGED
|
@@ -39,6 +39,16 @@ export interface PluginNextActionV1 {
|
|
|
39
39
|
message?: string;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export interface PluginSessionUsageDecisionV1 {
|
|
43
|
+
allowed: boolean;
|
|
44
|
+
state: "reserved" | "committed" | "released" | "exhausted" | "missing";
|
|
45
|
+
limit: number;
|
|
46
|
+
used: number;
|
|
47
|
+
remaining: number;
|
|
48
|
+
resetAt: string;
|
|
49
|
+
idempotent: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
export interface PluginInstallReceiptV1 {
|
|
43
53
|
schemaVersion: 1;
|
|
44
54
|
bundleId: string;
|
|
@@ -139,6 +149,9 @@ export interface PluginBootstrapBackendV1 {
|
|
|
139
149
|
artifactGrant: string;
|
|
140
150
|
}): Promise<SignedPluginReleaseV1[]>;
|
|
141
151
|
downloadArtifact(request: { release: SignedPluginReleaseV1; artifactGrant: string }): Promise<Uint8Array>;
|
|
152
|
+
reserveSession?(operationId: string): Promise<PluginSessionUsageDecisionV1>;
|
|
153
|
+
commitSession?(operationId: string): Promise<PluginSessionUsageDecisionV1>;
|
|
154
|
+
releaseSession?(operationId: string): Promise<PluginSessionUsageDecisionV1>;
|
|
142
155
|
getManagementAction(): Promise<PluginNextActionV1 | null>;
|
|
143
156
|
}
|
|
144
157
|
|
package/src/plugin-host.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type PluginPlanV1 = "free" | "trial" | "pro" | "team" | "enterprise";
|
|
|
17
17
|
export interface PluginCapabilityQuotaV1 {
|
|
18
18
|
limit: number;
|
|
19
19
|
window: "day";
|
|
20
|
-
scope: "device";
|
|
20
|
+
scope: "device" | "account";
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface PluginCapabilityGrantV1 {
|
|
@@ -218,7 +218,7 @@ export function validatePluginEntitlementStatusV1(
|
|
|
218
218
|
if (grant.quota) {
|
|
219
219
|
if (!Number.isInteger(grant.quota.limit) || grant.quota.limit <= 0)
|
|
220
220
|
throw new Error(`Capability ${capability} has an invalid quota limit`);
|
|
221
|
-
if (grant.quota.window !== "day" || grant.quota.scope
|
|
221
|
+
if (grant.quota.window !== "day" || !["device", "account"].includes(grant.quota.scope))
|
|
222
222
|
throw new Error(`Capability ${capability} has an invalid quota policy`);
|
|
223
223
|
}
|
|
224
224
|
}
|
package/src/plugin-runtime.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { pathToFileURL } from "node:url";
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
PluginBootstrapFailure,
|
|
11
12
|
type PluginInstallReceiptV1,
|
|
12
13
|
type PluginInstallStoreV1,
|
|
14
|
+
type PluginSessionUsageDecisionV1,
|
|
13
15
|
type SignedPluginReleaseV1,
|
|
14
16
|
} from "./plugin-bootstrap.js";
|
|
15
17
|
import {
|
|
@@ -172,6 +174,34 @@ export class InstalledPluginRuntimeV1 {
|
|
|
172
174
|
return registered.command.run(context);
|
|
173
175
|
}
|
|
174
176
|
|
|
177
|
+
async runSessionStart(
|
|
178
|
+
context: Parameters<PluginSessionStartHookV1["run"]>[0],
|
|
179
|
+
): Promise<PluginSessionUsageDecisionV1 | null> {
|
|
180
|
+
if (!(await this.load()) || this.hooks.length === 0) return null;
|
|
181
|
+
const entitlement = await this.refreshEntitlement();
|
|
182
|
+
const eligible = this.hooks.filter((hook) => isPluginCapabilityEnabled(entitlement, hook.requiredCapability));
|
|
183
|
+
if (eligible.length === 0) return null;
|
|
184
|
+
const metered = eligible.some(
|
|
185
|
+
(hook) => entitlement.capabilities[hook.requiredCapability]?.quota?.scope === "account",
|
|
186
|
+
);
|
|
187
|
+
if (!metered) {
|
|
188
|
+
for (const hook of eligible) await hook.run(context);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
if (!this.backend.reserveSession || !this.backend.commitSession || !this.backend.releaseSession)
|
|
192
|
+
throw new PluginBootstrapFailure("session_usage_unavailable", "Account session metering is unavailable");
|
|
193
|
+
const operationId = randomUUID();
|
|
194
|
+
const reservation = await this.backend.reserveSession(operationId);
|
|
195
|
+
if (!reservation.allowed) return reservation;
|
|
196
|
+
try {
|
|
197
|
+
for (const hook of eligible) await hook.run(context);
|
|
198
|
+
return await this.backend.commitSession(operationId);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
await this.backend.releaseSession(operationId);
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
175
205
|
private createHost(manifest: AgentMemoryPluginManifestV1): AgentMemoryPluginHostV1 {
|
|
176
206
|
const descriptors = new Map(manifest.commands.map((command) => [command.name, command]));
|
|
177
207
|
const stateRoot = path.join(this.store.root, "state");
|
package/src/plugin-service.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type PluginBootstrapBackendV1,
|
|
11
11
|
PluginBootstrapFailure,
|
|
12
12
|
type PluginNextActionV1,
|
|
13
|
+
type PluginSessionUsageDecisionV1,
|
|
13
14
|
type SignedPluginReleaseV1,
|
|
14
15
|
} from "./plugin-bootstrap.js";
|
|
15
16
|
import { type PluginEntitlementStatusV1, validatePluginEntitlementStatusV1 } from "./plugin-host.js";
|
|
@@ -21,37 +22,22 @@ const REQUEST_TIMEOUT_MS = 30_000;
|
|
|
21
22
|
const EMAIL_MAX_BYTES = 254;
|
|
22
23
|
const FORM_MAX_BYTES = 2_048;
|
|
23
24
|
const SERVICE_JSON_MAX_BYTES = 1024 * 1024;
|
|
25
|
+
const ACTIVATION_CREDENTIAL = /^am_activation_[A-Za-z0-9_-]{32,256}$/;
|
|
24
26
|
|
|
25
27
|
const MISSING_ENTITLEMENT: PluginEntitlementStatusV1 = {
|
|
26
28
|
plan: null,
|
|
27
29
|
state: "missing",
|
|
28
30
|
features: [],
|
|
29
31
|
capabilities: {},
|
|
30
|
-
reason: "Enter an email address to activate
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const TEMPORARY_ENTITLEMENT: PluginEntitlementStatusV1 = {
|
|
34
|
-
plan: "pro",
|
|
35
|
-
state: "active",
|
|
36
|
-
features: ["session-intelligence", "web-console"],
|
|
37
|
-
capabilities: Object.fromEntries(
|
|
38
|
-
[
|
|
39
|
-
"session-index",
|
|
40
|
-
"session-worker",
|
|
41
|
-
"learning",
|
|
42
|
-
"retrieval-evaluation",
|
|
43
|
-
"operational-metrics",
|
|
44
|
-
"web-console",
|
|
45
|
-
"memory-explorer",
|
|
46
|
-
].map((capability) => [capability, { enabled: true }]),
|
|
47
|
-
),
|
|
48
|
-
reason: "Temporary email activation grants unlimited local use",
|
|
32
|
+
reason: "Enter an email address to activate the free daily session allowance",
|
|
49
33
|
};
|
|
50
34
|
|
|
51
35
|
interface TemporaryActivationV1 {
|
|
52
|
-
schemaVersion:
|
|
36
|
+
schemaVersion: 2;
|
|
53
37
|
email: string;
|
|
54
38
|
activatedAt: string;
|
|
39
|
+
usageCredential: string;
|
|
40
|
+
dailySessionLimit: number;
|
|
55
41
|
}
|
|
56
42
|
|
|
57
43
|
interface TemporaryPluginBackendOptions {
|
|
@@ -68,6 +54,27 @@ function cloneEntitlement(value: PluginEntitlementStatusV1): PluginEntitlementSt
|
|
|
68
54
|
return structuredClone(value);
|
|
69
55
|
}
|
|
70
56
|
|
|
57
|
+
function freeEntitlement(dailySessionLimit: number): PluginEntitlementStatusV1 {
|
|
58
|
+
return {
|
|
59
|
+
plan: "free",
|
|
60
|
+
state: "active",
|
|
61
|
+
features: ["session-intelligence", "web-console"],
|
|
62
|
+
capabilities: {
|
|
63
|
+
"session-index": { enabled: true },
|
|
64
|
+
"session-worker": {
|
|
65
|
+
enabled: true,
|
|
66
|
+
quota: { limit: dailySessionLimit, window: "day", scope: "account" },
|
|
67
|
+
},
|
|
68
|
+
learning: { enabled: true },
|
|
69
|
+
"retrieval-evaluation": { enabled: true },
|
|
70
|
+
"operational-metrics": { enabled: true },
|
|
71
|
+
"web-console": { enabled: true },
|
|
72
|
+
"memory-explorer": { enabled: true },
|
|
73
|
+
},
|
|
74
|
+
reason: `${dailySessionLimit} free agent sessions per UTC day`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
71
78
|
function isEmail(value: string): boolean {
|
|
72
79
|
return (
|
|
73
80
|
Buffer.byteLength(value, "utf-8") <= EMAIL_MAX_BYTES &&
|
|
@@ -87,13 +94,104 @@ function securityHeaders(contentType: string): Record<string, string> {
|
|
|
87
94
|
};
|
|
88
95
|
}
|
|
89
96
|
|
|
97
|
+
const ACTIVATION_PAGE_STYLES = `
|
|
98
|
+
:root{color-scheme:light;font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#172033;background:#f4f7fb;font-synthesis:none}
|
|
99
|
+
*{box-sizing:border-box}
|
|
100
|
+
body{min-height:100vh;margin:0;padding:2rem 1.25rem;display:grid;place-items:center;background:radial-gradient(circle at 12% 0%,#e8f0ff 0,transparent 38rem),radial-gradient(circle at 100% 100%,#eef2ff 0,transparent 32rem),#f7f9fc}
|
|
101
|
+
main{width:min(100%,34rem)}
|
|
102
|
+
.skip-link{position:fixed;z-index:1;top:1rem;left:1rem;padding:.65rem .8rem;border-radius:.55rem;background:#172033;color:#fff;font-weight:700;transform:translateY(-200%)}
|
|
103
|
+
.skip-link:focus{transform:translateY(0)}
|
|
104
|
+
.brand{display:flex;align-items:center;gap:.75rem;margin:0 0 1.25rem .25rem;color:#33415c;font-size:.875rem;font-weight:700;letter-spacing:.01em}
|
|
105
|
+
.mark{width:2.25rem;height:2.25rem;display:grid;place-items:center;border-radius:.7rem;background:linear-gradient(145deg,#172033,#3159b8);color:#fff;box-shadow:0 .5rem 1.25rem rgba(35,65,130,.2);font-size:1.05rem;letter-spacing:-.08em}
|
|
106
|
+
.card{padding:clamp(1.5rem,5vw,2.5rem);border:1px solid rgba(205,216,231,.9);border-radius:1.5rem;background:rgba(255,255,255,.94);box-shadow:0 1.5rem 4rem rgba(32,51,84,.12),0 .125rem .375rem rgba(32,51,84,.06);backdrop-filter:blur(1rem)}
|
|
107
|
+
.eyebrow{display:inline-flex;align-items:center;gap:.45rem;margin:0 0 1rem;padding:.42rem .7rem;border:1px solid #d9e4f7;border-radius:999px;background:#f3f7ff;color:#315798;font-size:.78rem;font-weight:750;letter-spacing:.03em;text-transform:uppercase}
|
|
108
|
+
.eyebrow::before{content:"";width:.45rem;height:.45rem;border-radius:50%;background:#2b67d1;box-shadow:0 0 0 .22rem #dbe8ff}
|
|
109
|
+
h1{margin:0;color:#141d2c;font-size:clamp(2rem,7vw,2.75rem);line-height:1.06;letter-spacing:-.045em;text-wrap:balance}
|
|
110
|
+
.intro{margin:.9rem 0 1.75rem;color:#526176;font-size:1.05rem;line-height:1.55}
|
|
111
|
+
form{display:grid;gap:.75rem}
|
|
112
|
+
label{color:#29364a;font-size:.9rem;font-weight:700}
|
|
113
|
+
input{width:100%;min-height:3.25rem;padding:.8rem 3.5rem .8rem 1rem;border:1px solid #bcc8d8;border-radius:.8rem;background:#fff;color:#172033;font:inherit;box-shadow:inset 0 1px 2px rgba(24,39,65,.04)}
|
|
114
|
+
input::placeholder{color:#8a96a7}
|
|
115
|
+
input:hover{border-color:#92a2b8}
|
|
116
|
+
input:focus-visible{outline:0;border-color:#2b67d1;box-shadow:0 0 0 .25rem rgba(43,103,209,.15)}
|
|
117
|
+
button{min-height:3.35rem;margin-top:.25rem;padding:.85rem 1rem;border:1px solid #172033;border-radius:.8rem;background:linear-gradient(180deg,#24334a,#172033);color:#fff;font:inherit;font-weight:750;cursor:pointer;box-shadow:0 .55rem 1.2rem rgba(23,32,51,.18);touch-action:manipulation;transition:transform .15s}
|
|
118
|
+
button:hover{background:linear-gradient(180deg,#2d405c,#1d2a40);box-shadow:0 .7rem 1.4rem rgba(23,32,51,.23);transform:translateY(-1px)}
|
|
119
|
+
button:active{transform:translateY(0)}
|
|
120
|
+
button:focus-visible,summary:focus-visible{outline:.2rem solid rgba(43,103,209,.32);outline-offset:.18rem}
|
|
121
|
+
.terminal-note{margin:.8rem 0 0;color:#6a7789;font-size:.85rem;text-align:center}
|
|
122
|
+
.error{margin:0 0 1rem;padding:.8rem 1rem;border:1px solid #efb8bd;border-radius:.75rem;background:#fff3f4;color:#982631;font-size:.9rem;line-height:1.45}
|
|
123
|
+
details{margin-top:1.5rem;padding-top:1.25rem;border-top:1px solid #e2e8f0;color:#5b687a;font-size:.85rem;line-height:1.55}
|
|
124
|
+
summary{color:#46566d;font-weight:700;cursor:pointer;list-style-position:outside;touch-action:manipulation}
|
|
125
|
+
details p{margin:.8rem 0 0}
|
|
126
|
+
.never-sent{padding:.75rem .85rem;border-radius:.65rem;background:#f5f7fa;color:#536176}
|
|
127
|
+
.success{display:grid;place-items:center;width:3.5rem;height:3.5rem;margin-bottom:1.4rem;border-radius:1rem;background:#eaf7ef;color:#197542;font-size:1.65rem;font-weight:800;box-shadow:inset 0 0 0 1px #c9ead6}
|
|
128
|
+
.completion .intro{margin-bottom:0}
|
|
129
|
+
@media (max-width:30rem){body{padding:1rem}.brand{margin-left:.1rem}.card{border-radius:1.15rem}h1{font-size:2rem}}
|
|
130
|
+
@media (prefers-reduced-motion:reduce){input,button{transition:none}}
|
|
131
|
+
`;
|
|
132
|
+
|
|
90
133
|
function activationPage(action: string, error?: string): string {
|
|
91
|
-
const errorHtml = error ? `<p class="error">${error}</p>` : "";
|
|
92
|
-
|
|
134
|
+
const errorHtml = error ? `<p class="error" id="activation-error" role="alert">${error}</p>` : "";
|
|
135
|
+
const describedBy = error ? "activation-error terminal-note" : "terminal-note";
|
|
136
|
+
const autofocus = error ? " autofocus" : "";
|
|
137
|
+
return `<!doctype html>
|
|
138
|
+
<html lang="en">
|
|
139
|
+
<head>
|
|
140
|
+
<meta charset="utf-8">
|
|
141
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
142
|
+
<meta name="theme-color" content="#f4f7fb">
|
|
143
|
+
<title>Activate AgentMemory</title>
|
|
144
|
+
<style>${ACTIVATION_PAGE_STYLES}</style>
|
|
145
|
+
</head>
|
|
146
|
+
<body>
|
|
147
|
+
<a class="skip-link" href="#activation">Skip to Activation</a>
|
|
148
|
+
<main id="activation">
|
|
149
|
+
<div class="brand" translate="no"><span class="mark" aria-hidden="true">AM</span><span>AgentMemory</span></div>
|
|
150
|
+
<section class="card" aria-labelledby="activation-title">
|
|
151
|
+
<p class="eyebrow">Free daily allowance</p>
|
|
152
|
+
<h1 id="activation-title">Activate This Device</h1>
|
|
153
|
+
<p class="intro">Use your email to enable AgentMemory Pro for your local agent sessions.</p>
|
|
154
|
+
<form method="post" action="${action}">
|
|
155
|
+
<label for="email">Email address</label>
|
|
156
|
+
${errorHtml}
|
|
157
|
+
<input id="email" type="email" name="email" autocomplete="email" inputmode="email" spellcheck="false" placeholder="you@example.com…" maxlength="254" required${autofocus} aria-describedby="${describedBy}">
|
|
158
|
+
<button type="submit">Activate AgentMemory</button>
|
|
159
|
+
</form>
|
|
160
|
+
<p class="terminal-note" id="terminal-note">Your terminal will finish setup after activation.</p>
|
|
161
|
+
<details>
|
|
162
|
+
<summary>What’s shared during activation</summary>
|
|
163
|
+
<p>Your email identifies your free daily allowance. The CLI also sends core and bundle versions, platform, architecture, and release channel. The service stores a daily count of opaque session-start operations. Activation records expire after 365 days without use.</p>
|
|
164
|
+
<p class="never-sent"><strong>Never sent:</strong> The request never includes memory, session content, queries, repository paths, raw agent session identifiers, IP addresses, or user-agent strings.</p>
|
|
165
|
+
</details>
|
|
166
|
+
</section>
|
|
167
|
+
</main>
|
|
168
|
+
</body>
|
|
169
|
+
</html>`;
|
|
93
170
|
}
|
|
94
171
|
|
|
95
172
|
function completionPage(): string {
|
|
96
|
-
return
|
|
173
|
+
return `<!doctype html>
|
|
174
|
+
<html lang="en">
|
|
175
|
+
<head>
|
|
176
|
+
<meta charset="utf-8">
|
|
177
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
178
|
+
<meta name="theme-color" content="#f4f7fb">
|
|
179
|
+
<title>AgentMemory activated</title>
|
|
180
|
+
<style>${ACTIVATION_PAGE_STYLES}</style>
|
|
181
|
+
</head>
|
|
182
|
+
<body>
|
|
183
|
+
<a class="skip-link" href="#completion">Skip to Activation Status</a>
|
|
184
|
+
<main id="completion">
|
|
185
|
+
<div class="brand" translate="no"><span class="mark" aria-hidden="true">AM</span><span>AgentMemory</span></div>
|
|
186
|
+
<section class="card completion" aria-labelledby="completion-title">
|
|
187
|
+
<div class="success" aria-hidden="true">✓</div>
|
|
188
|
+
<p class="eyebrow">Device activated</p>
|
|
189
|
+
<h1 id="completion-title">You’re All Set</h1>
|
|
190
|
+
<p class="intro">Return to your terminal to finish installing AgentMemory Pro. You can close this tab.</p>
|
|
191
|
+
</section>
|
|
192
|
+
</main>
|
|
193
|
+
</body>
|
|
194
|
+
</html>`;
|
|
97
195
|
}
|
|
98
196
|
|
|
99
197
|
function send(response: ServerResponse, status: number, body: string, contentType = "text/html; charset=utf-8"): void {
|
|
@@ -294,7 +392,8 @@ export class TemporaryPluginBackend implements PluginBootstrapBackendV1 {
|
|
|
294
392
|
}
|
|
295
393
|
|
|
296
394
|
async getLocalEntitlement(): Promise<PluginEntitlementStatusV1> {
|
|
297
|
-
|
|
395
|
+
const activation = this.readActivation();
|
|
396
|
+
return activation ? freeEntitlement(activation.dailySessionLimit) : cloneEntitlement(MISSING_ENTITLEMENT);
|
|
298
397
|
}
|
|
299
398
|
|
|
300
399
|
async resolveAccess(request: {
|
|
@@ -303,8 +402,9 @@ export class TemporaryPluginBackend implements PluginBootstrapBackendV1 {
|
|
|
303
402
|
channel: string;
|
|
304
403
|
allowAuthentication: boolean;
|
|
305
404
|
}): Promise<PluginAccessDecisionV1> {
|
|
306
|
-
|
|
307
|
-
|
|
405
|
+
const activation = this.readActivation();
|
|
406
|
+
let email = activation?.email;
|
|
407
|
+
if (!email) {
|
|
308
408
|
if (!request.allowAuthentication)
|
|
309
409
|
return {
|
|
310
410
|
kind: "auth_required",
|
|
@@ -315,34 +415,61 @@ export class TemporaryPluginBackend implements PluginBootstrapBackendV1 {
|
|
|
315
415
|
message: "Run plugin install in an interactive terminal to enter an email address",
|
|
316
416
|
},
|
|
317
417
|
};
|
|
318
|
-
|
|
319
|
-
this.writeActivation(email);
|
|
320
|
-
activation = this.readActivation();
|
|
418
|
+
email = await this.activate();
|
|
321
419
|
}
|
|
322
|
-
if (!activation)
|
|
323
|
-
throw new PluginBootstrapFailure("activation_failed", "The local activation record could not be loaded");
|
|
324
420
|
const response = await this.request(`${this.apiOrigin}/v1/plugin/access`, {
|
|
325
421
|
method: "POST",
|
|
326
422
|
headers: { "Content-Type": "application/json" },
|
|
327
423
|
body: JSON.stringify({
|
|
328
424
|
schemaVersion: 1,
|
|
329
|
-
email
|
|
425
|
+
email,
|
|
330
426
|
bundleId: request.bundleId,
|
|
331
427
|
installedVersion: request.installedVersion ?? null,
|
|
332
428
|
coreVersion: this.coreVersion,
|
|
333
429
|
channel: request.channel,
|
|
334
430
|
platform: process.platform,
|
|
335
431
|
architecture: process.arch,
|
|
336
|
-
consentVersion: "activation-
|
|
432
|
+
consentVersion: "activation-v2",
|
|
337
433
|
}),
|
|
338
434
|
});
|
|
339
|
-
const value = (await readJson(response)) as {
|
|
435
|
+
const value = (await readJson(response)) as {
|
|
436
|
+
entitlement?: unknown;
|
|
437
|
+
artifactGrant?: unknown;
|
|
438
|
+
usageCredential?: unknown;
|
|
439
|
+
};
|
|
340
440
|
validatePluginEntitlementStatusV1(value.entitlement);
|
|
341
441
|
if (typeof value.artifactGrant !== "string" || !value.artifactGrant)
|
|
342
442
|
throw new PluginBootstrapFailure("service_response_invalid", "The access response omitted its artifact grant");
|
|
443
|
+
if (typeof value.usageCredential !== "string" || !ACTIVATION_CREDENTIAL.test(value.usageCredential))
|
|
444
|
+
throw new PluginBootstrapFailure(
|
|
445
|
+
"service_response_invalid",
|
|
446
|
+
"The access response omitted its usage credential",
|
|
447
|
+
);
|
|
448
|
+
const freeQuota = value.entitlement.capabilities["session-worker"]?.quota;
|
|
449
|
+
if (
|
|
450
|
+
value.entitlement.plan !== "free" ||
|
|
451
|
+
value.entitlement.state !== "active" ||
|
|
452
|
+
!freeQuota ||
|
|
453
|
+
freeQuota.scope !== "account" ||
|
|
454
|
+
freeQuota.window !== "day"
|
|
455
|
+
)
|
|
456
|
+
throw new PluginBootstrapFailure("service_response_invalid", "The free session policy is invalid");
|
|
457
|
+
this.writeActivation(email, value.usageCredential, freeQuota.limit);
|
|
343
458
|
return { kind: "granted", entitlement: value.entitlement, artifactGrant: value.artifactGrant };
|
|
344
459
|
}
|
|
345
460
|
|
|
461
|
+
async reserveSession(operationId: string): Promise<PluginSessionUsageDecisionV1> {
|
|
462
|
+
return this.sessionUsage("reserve", operationId);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async commitSession(operationId: string): Promise<PluginSessionUsageDecisionV1> {
|
|
466
|
+
return this.sessionUsage("commit", operationId);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async releaseSession(operationId: string): Promise<PluginSessionUsageDecisionV1> {
|
|
470
|
+
return this.sessionUsage("release", operationId);
|
|
471
|
+
}
|
|
472
|
+
|
|
346
473
|
async listReleases(request: {
|
|
347
474
|
bundleId: string;
|
|
348
475
|
channel: string;
|
|
@@ -395,16 +522,28 @@ export class TemporaryPluginBackend implements PluginBootstrapBackendV1 {
|
|
|
395
522
|
if (!stat.isFile() || stat.isSymbolicLink() || (process.platform !== "win32" && (stat.mode & 0o077) !== 0))
|
|
396
523
|
return null;
|
|
397
524
|
const value = JSON.parse(fs.readFileSync(activationPath, "utf-8")) as TemporaryActivationV1;
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
525
|
+
if (
|
|
526
|
+
value.schemaVersion !== 2 ||
|
|
527
|
+
!isEmail(value.email) ||
|
|
528
|
+
!Number.isFinite(Date.parse(value.activatedAt)) ||
|
|
529
|
+
!ACTIVATION_CREDENTIAL.test(value.usageCredential) ||
|
|
530
|
+
!Number.isSafeInteger(value.dailySessionLimit) ||
|
|
531
|
+
value.dailySessionLimit <= 0 ||
|
|
532
|
+
value.dailySessionLimit > 10_000
|
|
533
|
+
)
|
|
534
|
+
return null;
|
|
535
|
+
return value;
|
|
401
536
|
} catch {
|
|
402
537
|
return null;
|
|
403
538
|
}
|
|
404
539
|
}
|
|
405
540
|
|
|
406
|
-
private writeActivation(email: string): void {
|
|
541
|
+
private writeActivation(email: string, usageCredential: string, dailySessionLimit: number): void {
|
|
407
542
|
if (!isEmail(email)) throw new PluginBootstrapFailure("email_invalid", "Enter a valid email address");
|
|
543
|
+
if (!ACTIVATION_CREDENTIAL.test(usageCredential))
|
|
544
|
+
throw new PluginBootstrapFailure("activation_failed", "The activation credential is invalid");
|
|
545
|
+
if (!Number.isSafeInteger(dailySessionLimit) || dailySessionLimit <= 0 || dailySessionLimit > 10_000)
|
|
546
|
+
throw new PluginBootstrapFailure("activation_failed", "The free session allowance is invalid");
|
|
408
547
|
const target = this.activationPath();
|
|
409
548
|
fs.mkdirSync(this.root, { recursive: true, mode: 0o700 });
|
|
410
549
|
const rootStat = fs.lstatSync(this.root);
|
|
@@ -418,12 +557,50 @@ export class TemporaryPluginBackend implements PluginBootstrapBackendV1 {
|
|
|
418
557
|
const temporary = `${target}.tmp-${process.pid}-${randomUUID()}`;
|
|
419
558
|
fs.writeFileSync(
|
|
420
559
|
temporary,
|
|
421
|
-
`${JSON.stringify(
|
|
560
|
+
`${JSON.stringify(
|
|
561
|
+
{ schemaVersion: 2, email, activatedAt: new Date().toISOString(), usageCredential, dailySessionLimit },
|
|
562
|
+
null,
|
|
563
|
+
2,
|
|
564
|
+
)}\n`,
|
|
422
565
|
{ mode: 0o600, flag: "wx" },
|
|
423
566
|
);
|
|
424
567
|
fs.renameSync(temporary, target);
|
|
425
568
|
}
|
|
426
569
|
|
|
570
|
+
private async sessionUsage(
|
|
571
|
+
action: "reserve" | "commit" | "release",
|
|
572
|
+
operationId: string,
|
|
573
|
+
): Promise<PluginSessionUsageDecisionV1> {
|
|
574
|
+
const activation = this.readActivation();
|
|
575
|
+
if (!activation) throw new PluginBootstrapFailure("auth_required", "Run plugin install to activate AgentMemory");
|
|
576
|
+
const response = await this.request(`${this.apiOrigin}/v1/plugin/sessions/${action}`, {
|
|
577
|
+
method: "POST",
|
|
578
|
+
headers: {
|
|
579
|
+
Authorization: `Bearer ${activation.usageCredential}`,
|
|
580
|
+
"Content-Type": "application/json",
|
|
581
|
+
},
|
|
582
|
+
body: JSON.stringify({ schemaVersion: 1, operationId }),
|
|
583
|
+
});
|
|
584
|
+
const value = (await readJson(response)) as { decision?: Partial<PluginSessionUsageDecisionV1> };
|
|
585
|
+
const decision = value.decision;
|
|
586
|
+
if (
|
|
587
|
+
!decision ||
|
|
588
|
+
typeof decision.allowed !== "boolean" ||
|
|
589
|
+
!["reserved", "committed", "released", "exhausted", "missing"].includes(String(decision.state)) ||
|
|
590
|
+
!Number.isSafeInteger(decision.limit) ||
|
|
591
|
+
Number(decision.limit) <= 0 ||
|
|
592
|
+
!Number.isSafeInteger(decision.used) ||
|
|
593
|
+
Number(decision.used) < 0 ||
|
|
594
|
+
!Number.isSafeInteger(decision.remaining) ||
|
|
595
|
+
Number(decision.remaining) < 0 ||
|
|
596
|
+
typeof decision.resetAt !== "string" ||
|
|
597
|
+
!Number.isFinite(Date.parse(decision.resetAt)) ||
|
|
598
|
+
typeof decision.idempotent !== "boolean"
|
|
599
|
+
)
|
|
600
|
+
throw new PluginBootstrapFailure("service_response_invalid", "The session usage response is invalid");
|
|
601
|
+
return decision as PluginSessionUsageDecisionV1;
|
|
602
|
+
}
|
|
603
|
+
|
|
427
604
|
private async request(url: string, init: RequestInit = {}): Promise<Response> {
|
|
428
605
|
let response: Response;
|
|
429
606
|
try {
|