@nullsquare/agent-authority 0.4.0 → 0.4.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 +15 -3
- package/ROADMAP.md +19 -11
- package/docs/announcement-draft.md +7 -1
- package/docs/assets/agent-authority-cover-editorial-v2.png +0 -0
- package/docs/evidence.md +154 -2
- package/docs/live-google-validation.md +100 -0
- package/docs/npm-release.md +13 -1
- package/docs/task-leases.md +95 -28
- package/examples/live-google-cross-provider.js +261 -0
- package/package.json +6 -3
- package/src/authority-evidence.js +146 -0
- package/src/guard.js +8 -1
- package/src/providers/google.js +238 -0
- package/src/task-lease.js +77 -0
- package/docs/assets/agent-authority-cover.svg +0 -41
package/src/guard.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createExecutionEvidence } from './authority-evidence.js';
|
|
2
|
+
|
|
1
3
|
export class AuthorityDeniedError extends Error {
|
|
2
4
|
constructor({ result, receipt }) {
|
|
3
5
|
super(result?.reason || 'action denied by Agent Authority');
|
|
@@ -25,6 +27,10 @@ export class AuthorityApprovalRequiredError extends Error {
|
|
|
25
27
|
* authority boundary returns ALLOW. A guard can use either a static mission or
|
|
26
28
|
* a TaskLease. Task leases add provenance-bound restrictions without changing
|
|
27
29
|
* the host application's credential ownership.
|
|
30
|
+
*
|
|
31
|
+
* Successful effects also return execution evidence binding the exact output
|
|
32
|
+
* hash to the ALLOW receipt. TaskLease.deriveFromEvidence() can use that record
|
|
33
|
+
* with a trusted adapter extractor so callers do not provide derived values.
|
|
28
34
|
*/
|
|
29
35
|
export class AuthorityGuard {
|
|
30
36
|
constructor({ mission, lease, runtime, onDecision } = {}) {
|
|
@@ -66,7 +72,8 @@ export class AuthorityGuard {
|
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
const output = await effect();
|
|
69
|
-
|
|
75
|
+
const evidence = createExecutionEvidence({ receipt: evaluation.receipt, output });
|
|
76
|
+
return { output, result: evaluation.result, receipt: evaluation.receipt, evidence };
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
79
|
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { brokeredProviderAdapter } from '../connections.js';
|
|
2
|
+
|
|
3
|
+
const MUTATING_ACTIONS = new Set(['event.create', 'event.delete']);
|
|
4
|
+
const SEND_UPDATES = new Set(['all', 'externalOnly', 'none']);
|
|
5
|
+
|
|
6
|
+
function required(value, name) {
|
|
7
|
+
if (value === undefined || value === null || value === '') throw new Error(`${name} is required`);
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function providerError(code, message) {
|
|
12
|
+
const error = new Error(message);
|
|
13
|
+
error.code = code;
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fromHeader(message) {
|
|
18
|
+
const headers = message?.payload?.headers;
|
|
19
|
+
if (!Array.isArray(headers)) return null;
|
|
20
|
+
const header = headers.find((item) => String(item?.name || '').toLowerCase() === 'from');
|
|
21
|
+
return typeof header?.value === 'string' ? header.value : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function extractEmailAddress(value) {
|
|
25
|
+
if (typeof value !== 'string') return null;
|
|
26
|
+
const match = value.match(/([A-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Z0-9.-]+\.[A-Z]{2,})/i);
|
|
27
|
+
return match ? match[1].trim().toLowerCase() : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function gmailThreadSenderEmail(thread) {
|
|
31
|
+
const messages = thread?.messages;
|
|
32
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
33
|
+
throw providerError('gmail_thread_empty', 'Gmail thread does not contain any messages');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const message of messages) {
|
|
37
|
+
const raw = fromHeader(message);
|
|
38
|
+
const email = extractEmailAddress(raw);
|
|
39
|
+
if (email) return { email, raw, message_id: message.id || null };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
throw providerError('gmail_sender_missing', 'Gmail thread does not contain a usable From header');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Trusted authority-extraction contract for the normalized Gmail thread output.
|
|
47
|
+
*
|
|
48
|
+
* The extractor chooses the authority-relevant selector only. It never returns
|
|
49
|
+
* the value itself; TaskLease resolves output.sender_email after verifying the
|
|
50
|
+
* guard's execution evidence. This prevents host code from substituting another
|
|
51
|
+
* email while retaining the original Gmail receipt/evidence chain.
|
|
52
|
+
*/
|
|
53
|
+
export function gmailThreadSenderAuthorityExtractor({ receipt, output } = {}) {
|
|
54
|
+
if (receipt?.service !== 'gmail' || receipt?.action !== 'thread.read') {
|
|
55
|
+
throw providerError(
|
|
56
|
+
'trusted_extractor_operation_mismatch',
|
|
57
|
+
'Gmail sender authority extractor only accepts gmail:thread.read receipts'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const raw = typeof output?.sender_email === 'string' ? output.sender_email.trim() : '';
|
|
62
|
+
const normalized = extractEmailAddress(raw);
|
|
63
|
+
if (!normalized || normalized !== raw) {
|
|
64
|
+
throw providerError(
|
|
65
|
+
'trusted_extractor_output_invalid',
|
|
66
|
+
'normalized Gmail output does not contain a canonical sender_email'
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
extractor_id: 'google.gmail.thread.sender-email.v1',
|
|
72
|
+
selector: 'output.sender_email'
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validateSendUpdates(value) {
|
|
77
|
+
const normalized = value || 'none';
|
|
78
|
+
if (!SEND_UPDATES.has(normalized)) {
|
|
79
|
+
throw providerError('invalid_send_updates', 'context.send_updates must be all, externalOnly, or none');
|
|
80
|
+
}
|
|
81
|
+
return normalized;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildOperation(request, {
|
|
85
|
+
gmailBaseUrl = 'https://gmail.googleapis.com',
|
|
86
|
+
calendarBaseUrl = 'https://www.googleapis.com/calendar/v3'
|
|
87
|
+
} = {}) {
|
|
88
|
+
const context = request?.context || {};
|
|
89
|
+
|
|
90
|
+
if (request?.service === 'gmail' && request?.action === 'thread.read') {
|
|
91
|
+
const threadId = required(context.thread_id, 'context.thread_id');
|
|
92
|
+
const query = new URLSearchParams({ format: 'metadata' });
|
|
93
|
+
query.append('metadataHeaders', 'From');
|
|
94
|
+
return {
|
|
95
|
+
method: 'GET',
|
|
96
|
+
url: `${gmailBaseUrl}/gmail/v1/users/me/threads/${encodeURIComponent(threadId)}?${query}`
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (request?.service === 'calendar' && request?.action === 'event.create') {
|
|
101
|
+
const calendarId = context.calendar_id || 'primary';
|
|
102
|
+
const attendeeEmail = required(context.attendee_email, 'context.attendee_email');
|
|
103
|
+
const startTime = required(context.start_time, 'context.start_time');
|
|
104
|
+
const endTime = required(context.end_time, 'context.end_time');
|
|
105
|
+
const sendUpdates = validateSendUpdates(context.send_updates);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
url: `${calendarBaseUrl}/calendars/${encodeURIComponent(calendarId)}/events?sendUpdates=${encodeURIComponent(sendUpdates)}`,
|
|
110
|
+
body: {
|
|
111
|
+
summary: context.summary || 'Agent Authority task event',
|
|
112
|
+
description: context.description || undefined,
|
|
113
|
+
start: { dateTime: startTime },
|
|
114
|
+
end: { dateTime: endTime },
|
|
115
|
+
attendees: [{ email: attendeeEmail }],
|
|
116
|
+
transparency: context.transparency || undefined,
|
|
117
|
+
visibility: context.visibility || undefined
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (request?.service === 'calendar' && request?.action === 'event.delete') {
|
|
123
|
+
const calendarId = context.calendar_id || 'primary';
|
|
124
|
+
const eventId = required(context.event_id, 'context.event_id');
|
|
125
|
+
const sendUpdates = validateSendUpdates(context.send_updates);
|
|
126
|
+
return {
|
|
127
|
+
method: 'DELETE',
|
|
128
|
+
url: `${calendarBaseUrl}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(eventId)}?sendUpdates=${encodeURIComponent(sendUpdates)}`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
throw providerError(
|
|
133
|
+
'unsupported_action',
|
|
134
|
+
`Google action ${request?.service || 'unknown'}:${request?.action || 'unknown'} has no provider operation mapping`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function accessToken(credential) {
|
|
139
|
+
const token = typeof credential === 'string' ? credential : credential?.access_token;
|
|
140
|
+
if (!token) throw providerError('credential_invalid', 'Google credential does not contain an access token');
|
|
141
|
+
return token;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function responseBody(response) {
|
|
145
|
+
const text = await response.text();
|
|
146
|
+
if (!text) return null;
|
|
147
|
+
try { return JSON.parse(text); } catch { return text; }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizedOutput(request, response, body) {
|
|
151
|
+
const common = {
|
|
152
|
+
provider: request.service,
|
|
153
|
+
status: response.status,
|
|
154
|
+
ok: response.ok,
|
|
155
|
+
request_id: response.headers?.get?.('x-request-id') || null
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
if (request.service === 'gmail' && request.action === 'thread.read') {
|
|
159
|
+
const sender = gmailThreadSenderEmail(body);
|
|
160
|
+
return {
|
|
161
|
+
...common,
|
|
162
|
+
thread_id: body?.id || request.context?.thread_id || null,
|
|
163
|
+
message_count: Array.isArray(body?.messages) ? body.messages.length : 0,
|
|
164
|
+
sender_email: sender.email,
|
|
165
|
+
sender_raw: sender.raw,
|
|
166
|
+
sender_message_id: sender.message_id
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (request.service === 'calendar' && request.action === 'event.create') {
|
|
171
|
+
return {
|
|
172
|
+
...common,
|
|
173
|
+
event_id: body?.id || null,
|
|
174
|
+
html_link: body?.htmlLink || null,
|
|
175
|
+
attendees: Array.isArray(body?.attendees)
|
|
176
|
+
? body.attendees.map((item) => item?.email).filter(Boolean)
|
|
177
|
+
: []
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (request.service === 'calendar' && request.action === 'event.delete') {
|
|
182
|
+
return { ...common, deleted: response.ok };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { ...common, body };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function createGoogleProviderAdapter({
|
|
189
|
+
broker,
|
|
190
|
+
fetchImpl = globalThis.fetch,
|
|
191
|
+
gmailBaseUrl = 'https://gmail.googleapis.com',
|
|
192
|
+
calendarBaseUrl = 'https://www.googleapis.com/calendar/v3'
|
|
193
|
+
} = {}) {
|
|
194
|
+
if (!broker) throw new Error('credential broker is required');
|
|
195
|
+
if (typeof fetchImpl !== 'function') throw new Error('fetch implementation is required');
|
|
196
|
+
|
|
197
|
+
const operationFor = (request) => buildOperation(request, { gmailBaseUrl, calendarBaseUrl });
|
|
198
|
+
|
|
199
|
+
const adapter = brokeredProviderAdapter({
|
|
200
|
+
kind: 'google-rest',
|
|
201
|
+
services: ['gmail', 'calendar'],
|
|
202
|
+
broker,
|
|
203
|
+
async execute({ request, credential }) {
|
|
204
|
+
const operation = operationFor(request);
|
|
205
|
+
const response = await fetchImpl(operation.url, {
|
|
206
|
+
method: operation.method,
|
|
207
|
+
headers: {
|
|
208
|
+
accept: 'application/json',
|
|
209
|
+
authorization: `Bearer ${accessToken(credential)}`,
|
|
210
|
+
'content-type': 'application/json',
|
|
211
|
+
'user-agent': 'nullsquare-agent-authority/0.4'
|
|
212
|
+
},
|
|
213
|
+
body: operation.body ? JSON.stringify(operation.body) : undefined
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const body = await responseBody(response);
|
|
217
|
+
const output = normalizedOutput(request, response, body);
|
|
218
|
+
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
const error = providerError('provider_error', `${request.service} API ${response.status}`);
|
|
221
|
+
error.provider_output = output;
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return output;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
adapter.validateRequest = (request) => operationFor(request);
|
|
230
|
+
adapter.isMutation = (request) => MUTATING_ACTIONS.has(request?.action);
|
|
231
|
+
adapter.authorityExtractor = (request, kind = 'email.address') => {
|
|
232
|
+
if (request?.service === 'gmail' && request?.action === 'thread.read' && kind === 'email.address') {
|
|
233
|
+
return gmailThreadSenderAuthorityExtractor;
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
};
|
|
237
|
+
return adapter;
|
|
238
|
+
}
|
package/src/task-lease.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { assertMission, createReceipt, hashObject, matchPattern } from './index.js';
|
|
3
|
+
import {
|
|
4
|
+
resolveEvidenceSelector,
|
|
5
|
+
runAuthorityExtractor,
|
|
6
|
+
verifyExecutionEvidence
|
|
7
|
+
} from './authority-evidence.js';
|
|
3
8
|
|
|
4
9
|
function authorityError(code, message) {
|
|
5
10
|
const error = new Error(message);
|
|
@@ -95,6 +100,12 @@ export class TaskLease {
|
|
|
95
100
|
return structuredClone(fact);
|
|
96
101
|
}
|
|
97
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Legacy host-trusted derivation path.
|
|
105
|
+
*
|
|
106
|
+
* The caller supplies both value and selector. Keep this API for compatibility,
|
|
107
|
+
* but prefer deriveFromEvidence() for authority-relevant provider outputs.
|
|
108
|
+
*/
|
|
98
109
|
derive({ fact_id, kind = 'opaque', value, from = [], receipt, selector } = {}) {
|
|
99
110
|
if (!fact_id) throw new Error('derived fact_id is required');
|
|
100
111
|
if (value === undefined) throw new Error('derived value is required');
|
|
@@ -125,6 +136,7 @@ export class TaskLease {
|
|
|
125
136
|
value,
|
|
126
137
|
provenance: {
|
|
127
138
|
type: 'derived',
|
|
139
|
+
derivation_mode: 'host-trusted',
|
|
128
140
|
from: parents,
|
|
129
141
|
task_lease_id: this.lease_id,
|
|
130
142
|
receipt_id: receipt.receipt_id,
|
|
@@ -140,6 +152,71 @@ export class TaskLease {
|
|
|
140
152
|
return structuredClone(fact);
|
|
141
153
|
}
|
|
142
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Strict derivation path for provider data.
|
|
157
|
+
*
|
|
158
|
+
* The caller cannot supply the authority value. A trusted adapter extractor
|
|
159
|
+
* identifies one normalized output selector, TaskLease resolves that selector
|
|
160
|
+
* itself, and execution evidence proves the output still matches the exact
|
|
161
|
+
* ALLOW receipt returned by guard.run().
|
|
162
|
+
*/
|
|
163
|
+
deriveFromEvidence({
|
|
164
|
+
fact_id,
|
|
165
|
+
kind = 'opaque',
|
|
166
|
+
from = [],
|
|
167
|
+
receipt,
|
|
168
|
+
evidence,
|
|
169
|
+
output,
|
|
170
|
+
extractor
|
|
171
|
+
} = {}) {
|
|
172
|
+
if (!fact_id) throw new Error('derived fact_id is required');
|
|
173
|
+
if (this.facts.has(fact_id)) throw authorityError('fact_exists', `authority fact ${fact_id} already exists`);
|
|
174
|
+
if (!receipt) throw authorityError('receipt_required', 'derived authority requires an authorized source receipt');
|
|
175
|
+
if (receipt.decision !== 'allow') throw authorityError('receipt_not_authorized', 'derived authority requires an ALLOW receipt');
|
|
176
|
+
if (receipt.mission_id !== this.mission.mission_id) {
|
|
177
|
+
throw authorityError('receipt_mission_mismatch', 'source receipt belongs to another mission');
|
|
178
|
+
}
|
|
179
|
+
if (receipt.task_lease_id !== this.lease_id) {
|
|
180
|
+
throw authorityError('receipt_lease_mismatch', 'source receipt belongs to another task lease');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const parents = [...new Set(from)];
|
|
184
|
+
if (parents.length === 0) {
|
|
185
|
+
throw authorityError('parent_fact_required', 'derived authority must descend from at least one existing task authority fact');
|
|
186
|
+
}
|
|
187
|
+
for (const parentId of parents) {
|
|
188
|
+
if (!this.facts.has(parentId)) throw authorityError('parent_fact_missing', `authority fact ${parentId} does not exist`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
verifyExecutionEvidence({ receipt, output, evidence });
|
|
192
|
+
const extraction = runAuthorityExtractor({ extractor, receipt, output });
|
|
193
|
+
const value = resolveEvidenceSelector(output, extraction.selector);
|
|
194
|
+
|
|
195
|
+
const fact = {
|
|
196
|
+
fact_id,
|
|
197
|
+
kind,
|
|
198
|
+
value,
|
|
199
|
+
provenance: {
|
|
200
|
+
type: 'derived',
|
|
201
|
+
derivation_mode: 'execution-evidence-v1',
|
|
202
|
+
from: parents,
|
|
203
|
+
task_lease_id: this.lease_id,
|
|
204
|
+
receipt_id: receipt.receipt_id,
|
|
205
|
+
receipt_hash: receipt.receipt_hash,
|
|
206
|
+
source_service: receipt.service,
|
|
207
|
+
source_action: receipt.action,
|
|
208
|
+
source_request_hash: receipt.request_hash,
|
|
209
|
+
selector: extraction.selector,
|
|
210
|
+
extractor_id: extraction.extractor_id,
|
|
211
|
+
source_output_hash: evidence.output_hash,
|
|
212
|
+
execution_evidence_hash: evidence.evidence_hash
|
|
213
|
+
},
|
|
214
|
+
created_at: new Date().toISOString()
|
|
215
|
+
};
|
|
216
|
+
this.facts.set(fact_id, fact);
|
|
217
|
+
return structuredClone(fact);
|
|
218
|
+
}
|
|
219
|
+
|
|
143
220
|
bind(binding) {
|
|
144
221
|
const normalized = validateBinding(binding);
|
|
145
222
|
this.bindings.push(normalized);
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="640" viewBox="0 0 1600 640" role="img" aria-label="Agent Authority — mission scoped authorization for AI agents">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
-
<stop offset="0" stop-color="#07111f"/><stop offset="1" stop-color="#121a36"/>
|
|
5
|
-
</linearGradient>
|
|
6
|
-
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="0">
|
|
7
|
-
<stop offset="0" stop-color="#7c5cff"/><stop offset="1" stop-color="#4ecbff"/>
|
|
8
|
-
</linearGradient>
|
|
9
|
-
</defs>
|
|
10
|
-
<rect width="1600" height="640" rx="36" fill="url(#bg)"/>
|
|
11
|
-
<g font-family="Arial, Helvetica, sans-serif" fill="#fff">
|
|
12
|
-
<text x="100" y="120" font-size="28" font-weight="700" letter-spacing="3">NULLSQUARE / OPEN SOURCE</text>
|
|
13
|
-
<text x="100" y="220" font-size="78" font-weight="800">Agent Authority</text>
|
|
14
|
-
<text x="100" y="285" font-size="34" fill="#b9c7e6">Mission-scoped authorization for AI agents</text>
|
|
15
|
-
<text x="100" y="355" font-size="27" fill="#8fe1ff">One mission · Any agent · Any auth system</text>
|
|
16
|
-
</g>
|
|
17
|
-
<g transform="translate(930 95)" font-family="Arial, Helvetica, sans-serif">
|
|
18
|
-
<rect x="120" y="0" width="330" height="72" rx="16" fill="#111c34" stroke="#6d85ff"/>
|
|
19
|
-
<text x="285" y="44" text-anchor="middle" font-size="24" fill="#fff">Human-approved mission</text>
|
|
20
|
-
<path d="M285 72 V120" stroke="#7786a8" stroke-width="3"/>
|
|
21
|
-
<rect x="80" y="120" width="410" height="90" rx="18" fill="#171842" stroke="#8b67ff" stroke-width="2"/>
|
|
22
|
-
<text x="285" y="158" text-anchor="middle" font-size="25" font-weight="700" fill="#fff">AUTHORITY RUNTIME</text>
|
|
23
|
-
<text x="285" y="190" text-anchor="middle" font-size="20" fill="#b9c7e6">allow · deny · approve · revoke</text>
|
|
24
|
-
<path d="M285 210 V260" stroke="#7786a8" stroke-width="3"/>
|
|
25
|
-
<rect x="80" y="260" width="410" height="72" rx="16" fill="#101b30" stroke="#4ecbff"/>
|
|
26
|
-
<text x="285" y="304" text-anchor="middle" font-size="23" fill="#fff">Adapters / connectors</text>
|
|
27
|
-
<path d="M285 332 V382" stroke="#7786a8" stroke-width="3"/>
|
|
28
|
-
<g fill="#101b30" stroke="#4b628c">
|
|
29
|
-
<rect x="0" y="382" width="135" height="64" rx="14"/><rect x="145" y="382" width="135" height="64" rx="14"/>
|
|
30
|
-
<rect x="290" y="382" width="135" height="64" rx="14"/><rect x="435" y="382" width="135" height="64" rx="14"/>
|
|
31
|
-
</g>
|
|
32
|
-
<g font-size="20" fill="#fff" text-anchor="middle">
|
|
33
|
-
<text x="67" y="422">OAuth</text><text x="212" y="422">MCP</text><text x="357" y="422">API / CLI</text><text x="502" y="422">Legacy</text>
|
|
34
|
-
</g>
|
|
35
|
-
</g>
|
|
36
|
-
<rect x="100" y="430" width="690" height="2" fill="url(#accent)"/>
|
|
37
|
-
<g font-family="Arial, Helvetica, sans-serif" font-size="22" fill="#b9c7e6">
|
|
38
|
-
<text x="100" y="485">Delegation attenuation</text><text x="330" y="485">Human approvals</text><text x="555" y="485">Action receipts</text>
|
|
39
|
-
<text x="100" y="530">Credential isolation</text><text x="330" y="530">Legacy compatibility</text><text x="555" y="530">Protocol neutral</text>
|
|
40
|
-
</g>
|
|
41
|
-
</svg>
|