omp-onlyne 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.sample.md +27 -0
- package/LICENSE +21 -0
- package/README.md +140 -2
- package/index.ts +55 -0
- package/package.json +62 -5
- package/src/acquire.ts +182 -0
- package/src/binding.ts +132 -0
- package/src/bridge-text.ts +145 -0
- package/src/config.ts +332 -0
- package/src/decider-text.ts +438 -0
- package/src/decider.ts +2233 -0
- package/src/drafts.ts +57 -0
- package/src/handshake.ts +291 -0
- package/src/host-text.ts +92 -0
- package/src/messages.ts +427 -0
- package/src/node.ts +139 -0
- package/src/resources.ts +36 -0
- package/src/runtime.ts +676 -0
- package/src/store.ts +189 -0
- package/src/tern.ts +66 -0
- package/src/todo-native.ts +230 -0
- package/src/todo.ts +896 -0
- package/src/tool-text.ts +167 -0
- package/src/tool.ts +342 -0
- package/src/types.ts +150 -0
- package/src/ui/bootstrap-text.ts +125 -0
- package/src/ui/bootstrap.ts +363 -0
- package/src/ui/command-text.ts +38 -0
- package/src/ui/format.ts +56 -0
- package/src/ui/index.ts +156 -0
- package/src/ui/mail-message.ts +263 -0
- package/src/ui/status.ts +213 -0
- package/src/ui/viewer-text.ts +309 -0
- package/src/ui/viewer.ts +1403 -0
- package/src/urls-text.ts +304 -0
- package/src/urls.ts +1488 -0
package/src/drafts.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { OnlyneError, type Json, type MessageRef, type MsgInput, type PreparedSend } from "@onlyne/lib";
|
|
2
|
+
import type { ExtensionContext } from "@oh-my-pi/pi-coding-agent";
|
|
3
|
+
import { runDecision } from "./decider.ts";
|
|
4
|
+
import type { PluginRuntime } from "./runtime.ts";
|
|
5
|
+
import { bridgeText as t } from "./bridge-text.ts";
|
|
6
|
+
|
|
7
|
+
interface Draft {
|
|
8
|
+
id: string;
|
|
9
|
+
created_by: string;
|
|
10
|
+
created_at: string;
|
|
11
|
+
prepared: PreparedSend;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function createDraft(runtime: PluginRuntime, ctx: ExtensionContext, input: MsgInput, signal?: AbortSignal): Promise<unknown> {
|
|
15
|
+
const meta = { ...input.meta };
|
|
16
|
+
delete meta.op;
|
|
17
|
+
if (meta.relation !== "reply" && meta.relation !== "followup") throw new OnlyneError("invalid_request", t("draftNeedsRelation"));
|
|
18
|
+
const original = await resolveSource(runtime, meta.in_reply_to);
|
|
19
|
+
if (ctx.agent.kind === "sub") {
|
|
20
|
+
const authorized = (await runtime.contexts.list()).some(record => record.agent_id === ctx.agent.id
|
|
21
|
+
&& record.source?.sender === original.sender && record.source.id === original.id);
|
|
22
|
+
if (!authorized) throw new OnlyneError("forbidden", t("draftChildScope"));
|
|
23
|
+
}
|
|
24
|
+
meta.in_reply_to = { sender: original.sender, id: original.id };
|
|
25
|
+
const normalized = { ...input, meta };
|
|
26
|
+
const prepared = await runDecision({ ctx, input: normalized, signal, readOnly: false }, () => runtime.node.client.prepare(normalized));
|
|
27
|
+
const draft: Draft = { id: crypto.randomUUID(), created_by: ctx.agent.id, created_at: new Date().toISOString(), prepared };
|
|
28
|
+
await runtime.storage.transaction(tx => tx.put(`draft/${draft.id}`, draft));
|
|
29
|
+
return { draft_id: draft.id, to: prepared.to, relation: prepared.message.meta?.relation,
|
|
30
|
+
in_reply_to: original, body: prepared.message.msg, state: "draft", send: { meta: { draft_id: draft.id } } };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function sendDraft(runtime: PluginRuntime, ctx: ExtensionContext, id: string, signal?: AbortSignal): Promise<unknown> {
|
|
34
|
+
if (ctx.agent.kind === "sub") throw new OnlyneError("forbidden", t("draftChildCannotSend"));
|
|
35
|
+
const draft = await runtime.storage.transaction(tx => tx.get<Draft>(`draft/${id}`));
|
|
36
|
+
if (draft === undefined) throw new OnlyneError("not_found", t("draftNotFound", { id }));
|
|
37
|
+
const { to, message, plan } = draft.prepared;
|
|
38
|
+
return runtime.node.client.send(to, message, plan, { context: ctx.agent.id, signal });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function resolveSource(runtime: PluginRuntime, value: Json | undefined): Promise<MessageRef> {
|
|
42
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new OnlyneError("invalid_request", t("draftNeedsReference"));
|
|
43
|
+
if (typeof value.sender === "string" && typeof value.id === "string") return { sender: value.sender, id: value.id };
|
|
44
|
+
if (typeof value.thread === "string" && (typeof value.seq === "string" || typeof value.seq === "number")) {
|
|
45
|
+
const slash = value.thread.lastIndexOf("/");
|
|
46
|
+
const thread = slash < 0 ? { owner: runtime.config.node.id, id: value.thread }
|
|
47
|
+
: { owner: value.thread.slice(0, slash), id: value.thread.slice(slash + 1) };
|
|
48
|
+
let cursor: string | undefined;
|
|
49
|
+
do {
|
|
50
|
+
const result = await runtime.node.messages.list(runtime.node.auth, thread, { attention: "thread", page_size: 100, cursor });
|
|
51
|
+
const found = result.items.find(row => row.seq === String(value.seq));
|
|
52
|
+
if (found !== undefined) return { sender: found.message.sender, id: found.message.id };
|
|
53
|
+
cursor = result.next_cursor ?? undefined;
|
|
54
|
+
} while (cursor !== undefined);
|
|
55
|
+
}
|
|
56
|
+
throw new OnlyneError("not_found", t("draftSourceUnavailable"));
|
|
57
|
+
}
|
package/src/handshake.ts
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { OnlyneError, type Json, type KeyExchange, type Message, type NodeId, type Storage, type ThreadRef, type TrustEntry, type TrustPhase } from "@onlyne/lib";
|
|
2
|
+
import { evaluateAcquisition, type AcquireInput, type AcquireVerdict, type AcquisitionPolicy } from "./acquire.ts";
|
|
3
|
+
import { bridgeText as t } from "./bridge-text.ts";
|
|
4
|
+
import { hostText } from "./host-text.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* RFC 0018 §3.2: the handshake entries live in one thread this node owns alone, so nothing in
|
|
8
|
+
* them ever leaves the node, and the business thread tree stays a record of conversations
|
|
9
|
+
* rather than a log of system events.
|
|
10
|
+
*/
|
|
11
|
+
export const TRUST_THREAD = "trust";
|
|
12
|
+
|
|
13
|
+
export function trustThread(nodeId: NodeId): ThreadRef {
|
|
14
|
+
return { owner: nodeId, id: TRUST_THREAD };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isTrustThread(nodeId: NodeId, ref: ThreadRef | undefined): boolean {
|
|
18
|
+
return ref !== undefined && ref.owner === nodeId && ref.id === TRUST_THREAD;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** RFC 0018 §2.1: where one peer statement's connection arrived from. */
|
|
22
|
+
export interface StatementObservation {
|
|
23
|
+
/** The address the connection came from; absent when this host could not observe one. */
|
|
24
|
+
observed?: string;
|
|
25
|
+
at: string;
|
|
26
|
+
cause: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const OBSERVED = "acquire/observed/";
|
|
30
|
+
const DELIVERED = "acquire/delivered/";
|
|
31
|
+
|
|
32
|
+
function observedKey(nodeId: NodeId, fingerprint: string): string {
|
|
33
|
+
return `${OBSERVED}${encodeURIComponent(nodeId)}/${encodeURIComponent(fingerprint)}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function deliveredKey(nodeId: NodeId, fingerprint: string, phase: TrustPhase): string {
|
|
37
|
+
return `${DELIVERED}${encodeURIComponent(nodeId)}/${encodeURIComponent(fingerprint)}/${phase}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* RFC 0018 §2.1/§3: the two durable records this host keeps about a handshake. The observed
|
|
42
|
+
* address is written for every statement, before any verdict, so an approval decided later
|
|
43
|
+
* reads the address that statement itself arrived from and never the address of the call that
|
|
44
|
+
* asks (§2.1). The delivered records are one per `(node_id, fingerprint, phase)`, so a
|
|
45
|
+
* replayed event appends no second entry.
|
|
46
|
+
*/
|
|
47
|
+
export class AcquisitionLog {
|
|
48
|
+
constructor(private readonly storage: Storage) {}
|
|
49
|
+
|
|
50
|
+
async observe(nodeId: NodeId, fingerprint: string, observation: StatementObservation): Promise<void> {
|
|
51
|
+
await this.storage.transaction(tx => tx.put(observedKey(nodeId, fingerprint), observation));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A local transition is still an observed trust event, but it must not replace the address
|
|
56
|
+
* of the peer statement. Keep its timestamp and cause beside the original address instead.
|
|
57
|
+
*/
|
|
58
|
+
async transition(nodeId: NodeId, fingerprint: string, at: string, cause: string): Promise<void> {
|
|
59
|
+
await this.storage.transaction(async tx => {
|
|
60
|
+
const key = observedKey(nodeId, fingerprint);
|
|
61
|
+
const current = await tx.get<StatementObservation>(key);
|
|
62
|
+
await tx.put(key, { ...(current?.observed === undefined ? {} : { observed: current.observed }), at, cause });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async observation(nodeId: NodeId, fingerprint: string): Promise<StatementObservation | undefined> {
|
|
67
|
+
return this.storage.transaction(tx => tx.get<StatementObservation>(observedKey(nodeId, fingerprint)));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async delivered(nodeId: NodeId, fingerprint: string, phase: TrustPhase): Promise<boolean> {
|
|
71
|
+
return (await this.storage.transaction(tx => tx.get(deliveredKey(nodeId, fingerprint, phase)))) !== undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async markDelivered(nodeId: NodeId, fingerprint: string, phase: TrustPhase, at: string): Promise<void> {
|
|
75
|
+
await this.storage.transaction(tx => tx.put(deliveredKey(nodeId, fingerprint, phase), { node_id: nodeId, fingerprint, phase, at }));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* RFC 0018 §3: the reports an agent already acted on are settled. A revocation names no
|
|
80
|
+
* fingerprint, so it settles every report held for the node; a later statement opens its own.
|
|
81
|
+
*/
|
|
82
|
+
async settle(nodeId: NodeId, fingerprint?: string): Promise<void> {
|
|
83
|
+
const prefix = `${DELIVERED}${encodeURIComponent(nodeId)}/${fingerprint === undefined ? "" : `${encodeURIComponent(fingerprint)}/`}`;
|
|
84
|
+
await this.storage.transaction(async tx => {
|
|
85
|
+
for (const row of await tx.scan(prefix)) await tx.delete(row.key);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The machine facts a handshake entry carries beside its body (RFC 0018 §3). */
|
|
91
|
+
export interface HandshakeFacts {
|
|
92
|
+
node_id: NodeId;
|
|
93
|
+
fingerprint: string;
|
|
94
|
+
phase: TrustPhase;
|
|
95
|
+
/** Whether this entry owes an agent decision; a status update owes none. */
|
|
96
|
+
pending: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The facts one handshake entry carries, or undefined for an entry that is not one. */
|
|
100
|
+
export function handshakeFacts(meta: Message["meta"] | undefined): HandshakeFacts | undefined {
|
|
101
|
+
const value = meta?.data?.trust;
|
|
102
|
+
if (value === null || value === undefined || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
103
|
+
const row = value as Record<string, Json>;
|
|
104
|
+
const { node_id: node, fingerprint, phase, pending } = row;
|
|
105
|
+
if (typeof node !== "string" || typeof fingerprint !== "string" || typeof pending !== "boolean") return undefined;
|
|
106
|
+
if (phase !== "invited" && phase !== "requested" && phase !== "exchanged" && phase !== "revoked") return undefined;
|
|
107
|
+
return { node_id: node, fingerprint, phase, pending };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface HandshakeInput {
|
|
111
|
+
node_id: NodeId;
|
|
112
|
+
fingerprint: string;
|
|
113
|
+
phase: TrustPhase;
|
|
114
|
+
/** The address the peer wrote into its own statement. */
|
|
115
|
+
declared: string;
|
|
116
|
+
/** The address that statement arrived from; absent when this host observed none. */
|
|
117
|
+
observed?: string;
|
|
118
|
+
policy: AcquisitionPolicy;
|
|
119
|
+
/** The judgement made for this event, when one was made. */
|
|
120
|
+
judgement?: ApprovalJudgement;
|
|
121
|
+
/** Whether an approval was committed without an agent deciding first. */
|
|
122
|
+
approved: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* RFC 0018 §3: one handshake entry. The body carries what the judgement read and concluded;
|
|
127
|
+
* the action identifiers appear only when a decision is actually owed, and a status update
|
|
128
|
+
* says so instead. `pending` is the same fact for the host's todo hook.
|
|
129
|
+
*/
|
|
130
|
+
export function handshakeEntry(input: HandshakeInput): { msg: string; trust: { [key: string]: Json } } {
|
|
131
|
+
// RFC 0018 §3: only an invitation this host did not approve itself is work. A pending local
|
|
132
|
+
// approval waits on the peer, so there is nothing to do and nothing to wake a turn for.
|
|
133
|
+
const pending = input.phase === "invited" && !input.approved;
|
|
134
|
+
const lines = [
|
|
135
|
+
t("trustHeader", { phase: input.phase, node: input.node_id }),
|
|
136
|
+
t("trustFingerprint", { fingerprint: input.fingerprint }),
|
|
137
|
+
t("trustDeclared", { declared: input.declared }),
|
|
138
|
+
t("trustObserved", { observed: input.observed ?? t("trustUnobserved") }),
|
|
139
|
+
t("trustPolicy", { policy: input.policy.policy }),
|
|
140
|
+
];
|
|
141
|
+
if (input.judgement !== undefined) {
|
|
142
|
+
lines.push(t("trustJudgement", {
|
|
143
|
+
reason: input.judgement.verdict.reason,
|
|
144
|
+
conclusion: input.judgement.verdict.allowed ? t("trustAllowed") : t("trustRefused"),
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
if (input.phase === "invited") lines.push(input.approved ? t("trustApproved") : t("trustInvited"));
|
|
148
|
+
else if (input.phase === "requested") lines.push(t("trustRequested"));
|
|
149
|
+
else if (input.phase === "exchanged") lines.push(t("trustExchanged"));
|
|
150
|
+
else lines.push(t("trustRevoked"));
|
|
151
|
+
if (input.phase === "invited" && !input.approved) {
|
|
152
|
+
// RFC 0018 §3: under `none` the agent may only look. Offering an action the policy
|
|
153
|
+
// refuses would ask for a decision that can never be made.
|
|
154
|
+
lines.push(input.policy.policy === "none"
|
|
155
|
+
? t("trustPanelOnly")
|
|
156
|
+
: t("trustAcceptAction", { node: input.node_id, fingerprint: input.fingerprint }));
|
|
157
|
+
}
|
|
158
|
+
if (pending) lines.push(t("trustRevokeAction", { node: input.node_id }));
|
|
159
|
+
return { msg: lines.join("\n"), trust: { node_id: input.node_id, fingerprint: input.fingerprint, phase: input.phase, pending } };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** RFC 0018 §2: one approval judgement and every input it read. */
|
|
163
|
+
export interface ApprovalJudgement {
|
|
164
|
+
at: string;
|
|
165
|
+
node_id: NodeId;
|
|
166
|
+
/** Absent when no fingerprint has been proved yet: an initiation reads the peer's key after the gate. */
|
|
167
|
+
fingerprint?: string;
|
|
168
|
+
policy: AcquisitionPolicy;
|
|
169
|
+
input: AcquireInput;
|
|
170
|
+
verdict: AcquireVerdict;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The judgement as the audit line records it: the time, the peer, and every input the verdict read. */
|
|
174
|
+
export function judgementFacts(judgement: ApprovalJudgement): Record<string, unknown> {
|
|
175
|
+
const { input, verdict } = judgement;
|
|
176
|
+
return {
|
|
177
|
+
at: judgement.at,
|
|
178
|
+
node: judgement.node_id,
|
|
179
|
+
fingerprint: judgement.fingerprint ?? null,
|
|
180
|
+
policy: judgement.policy.policy,
|
|
181
|
+
allow: [...judgement.policy.allow],
|
|
182
|
+
observed: input.observed ?? null,
|
|
183
|
+
declared: input.declared ?? null,
|
|
184
|
+
recorded: [...(input.recorded ?? [])],
|
|
185
|
+
allowed: verdict.allowed,
|
|
186
|
+
reason: verdict.reason,
|
|
187
|
+
...(verdict.matched === undefined ? {} : { matched: verdict.matched }),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Everything the judgement reads from the host, and the two calls it may then commit. */
|
|
192
|
+
export interface ApprovalDeps {
|
|
193
|
+
policy: AcquisitionPolicy;
|
|
194
|
+
log: AcquisitionLog;
|
|
195
|
+
/** RFC 0018 §2.1: the address the peer's last statement arrived from, as this host recorded it. */
|
|
196
|
+
observed(nodeId: NodeId, fingerprint: string): Promise<string | undefined>;
|
|
197
|
+
/** The address the peer wrote into its own statement, never the address of this call. */
|
|
198
|
+
declared(nodeId: NodeId): Promise<string | undefined>;
|
|
199
|
+
/** Every discovery address recorded for this node id; each is an outbound candidate (§2.2). */
|
|
200
|
+
recorded(nodeId: NodeId): Promise<string[]>;
|
|
201
|
+
/** The audit line, written whether or not the verdict allows the approval (§4.1). */
|
|
202
|
+
audit(judgement: ApprovalJudgement): void;
|
|
203
|
+
accept(nodeId: NodeId, fingerprint: string): Promise<TrustEntry>;
|
|
204
|
+
revoke(nodeId: NodeId): Promise<void>;
|
|
205
|
+
now(): string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface ApprovalRequest {
|
|
209
|
+
node_id: NodeId;
|
|
210
|
+
/** The fingerprint the peer proved. Omitted by an initiation, whose key read comes after the gate. */
|
|
211
|
+
fingerprint?: string;
|
|
212
|
+
/** The statement's own connection address, when the caller is holding it. */
|
|
213
|
+
observed?: string;
|
|
214
|
+
/**
|
|
215
|
+
* The discovery addresses as they stood when the statement arrived (RFC 0018 §2.2). A record
|
|
216
|
+
* this host derives from the statement itself is not part of the judgement.
|
|
217
|
+
*/
|
|
218
|
+
recorded?: string[];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* RFC 0018 §2: the one judgement, read by both the agent path and the auto-approval path.
|
|
223
|
+
* `observed` is the address a recorded statement arrived from — the caller's live connection
|
|
224
|
+
* address only when it is judging the statement it just received — and the declared address
|
|
225
|
+
* and every recorded address are checked with it (§2.1–2.2).
|
|
226
|
+
*/
|
|
227
|
+
export async function judgeApproval(deps: ApprovalDeps, request: ApprovalRequest): Promise<ApprovalJudgement> {
|
|
228
|
+
const observed = request.observed
|
|
229
|
+
?? (request.fingerprint === undefined ? undefined : await deps.observed(request.node_id, request.fingerprint));
|
|
230
|
+
const declared = await deps.declared(request.node_id);
|
|
231
|
+
const input: AcquireInput = {
|
|
232
|
+
...(observed === undefined || observed === "" ? {} : { observed }),
|
|
233
|
+
...(declared === undefined || declared === "" ? {} : { declared }),
|
|
234
|
+
recorded: request.recorded ?? await deps.recorded(request.node_id),
|
|
235
|
+
};
|
|
236
|
+
const judgement: ApprovalJudgement = {
|
|
237
|
+
at: deps.now(), node_id: request.node_id, fingerprint: request.fingerprint,
|
|
238
|
+
policy: deps.policy, input, verdict: await evaluateAcquisition(deps.policy, input),
|
|
239
|
+
};
|
|
240
|
+
deps.audit(judgement);
|
|
241
|
+
return judgement;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** The refusal an agent-initiated approval gets. A refused approval is not a revocation: the peer's row is left as it is. */
|
|
245
|
+
export function approvalRefused(judgement: ApprovalJudgement): OnlyneError {
|
|
246
|
+
const { policy, verdict } = judgement;
|
|
247
|
+
return new OnlyneError("forbidden", hostText("acquireDenied", { policy: policy.policy, reason: verdict.reason }), {
|
|
248
|
+
policy_denied: verdict.reason,
|
|
249
|
+
policy: policy.policy,
|
|
250
|
+
...(judgement.input.observed === undefined ? {} : { observed: judgement.input.observed }),
|
|
251
|
+
...(judgement.input.declared === undefined ? {} : { declared: judgement.input.declared }),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** RFC 0018 §2/§3.1: the agent's `trust_accept`, judged before anything is committed. */
|
|
256
|
+
export async function approvePeer(deps: ApprovalDeps, nodeId: NodeId, fingerprint: string): Promise<TrustEntry> {
|
|
257
|
+
const judgement = await judgeApproval(deps, { node_id: nodeId, fingerprint });
|
|
258
|
+
if (!judgement.verdict.allowed) throw approvalRefused(judgement);
|
|
259
|
+
const entry = await deps.accept(nodeId, fingerprint);
|
|
260
|
+
await deps.log.settle(nodeId, fingerprint);
|
|
261
|
+
return entry;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* RFC 0018 §2/§3.1: the agent's `trust_begin`. The judgement reads stored facts only and runs
|
|
266
|
+
* before `exchange`, which is this path's first outbound request — the key read that starts the
|
|
267
|
+
* exchange — so a refusal sends nothing and leaves the peer's row untouched. On an allow the
|
|
268
|
+
* library commits the local approval and mails the statement, and the entry it returns carries
|
|
269
|
+
* the phase that commit produced.
|
|
270
|
+
*/
|
|
271
|
+
export async function beginPeer(
|
|
272
|
+
deps: ApprovalDeps,
|
|
273
|
+
exchange: (nodeId: NodeId) => Promise<KeyExchange>,
|
|
274
|
+
request: ApprovalRequest,
|
|
275
|
+
): Promise<{ session: KeyExchange; entry: TrustEntry }> {
|
|
276
|
+
const judgement = await judgeApproval(deps, request);
|
|
277
|
+
if (!judgement.verdict.allowed) throw approvalRefused(judgement);
|
|
278
|
+
const session = await exchange(request.node_id);
|
|
279
|
+
const entry = await session.confirm(session.fingerprint);
|
|
280
|
+
await deps.log.settle(request.node_id, entry.fingerprint);
|
|
281
|
+
return { session, entry };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* RFC 0018 §2: the agent's `trust_revoke`, never judged. The policy decides who may come in;
|
|
286
|
+
* it has no say over who may be turned away, which is also why this path reads no address.
|
|
287
|
+
*/
|
|
288
|
+
export async function revokePeer(deps: ApprovalDeps, nodeId: NodeId): Promise<void> {
|
|
289
|
+
await deps.revoke(nodeId);
|
|
290
|
+
await deps.log.settle(nodeId);
|
|
291
|
+
}
|
package/src/host-text.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createTranslator } from "@onlyne/node-host/i18n";
|
|
2
|
+
const en = {
|
|
3
|
+
regionEmpty: "region must not be empty.",
|
|
4
|
+
nodeIdEmpty: "node.id must not be empty.",
|
|
5
|
+
addressEmpty: "A node address must not be empty.",
|
|
6
|
+
addressUrl: "A node address must be an absolute URL, got {value}.",
|
|
7
|
+
addressScheme: "A node address must use http or https, got {value}.",
|
|
8
|
+
addressShape: "A node address is an origin (http://host:7801) or a service URL (http://host:7801{service}), got {value}.",
|
|
9
|
+
configMissing: "No Onlyne configuration at {path}. Run /onlyne init to create it.",
|
|
10
|
+
configParse: "{path}: {error}",
|
|
11
|
+
languageInvalid: "language must be en or zh-CN.",
|
|
12
|
+
keyScopeInvalid: "node.key_scope must be project, global or none, got {value}.",
|
|
13
|
+
publicUrlEmpty: "server.public_url must not be empty.",
|
|
14
|
+
bindEmpty: "server.bind must not be empty.",
|
|
15
|
+
bindShape: "server.bind must be a hostname or IP address, got {value}.",
|
|
16
|
+
pageSizeRange: "threads.page_size must be an integer from 1 to 100, got {value}.",
|
|
17
|
+
retiredEndpoint: "node.endpoint is not supported; the endpoint comes from server.bind and the listening port.",
|
|
18
|
+
portRange: "server.port must be an integer from 0 to 65535, got {value}.",
|
|
19
|
+
retiredPeers: "peers is not supported; peers are discovered over the network and kept in the project SQLite.",
|
|
20
|
+
retiredOmp: "omp is not supported; delivery and spawning are decided by the OMP judge.",
|
|
21
|
+
retiredAttention: "threads.default_attention is not supported; reads default to the attention list.",
|
|
22
|
+
tableExpected: "{path}: {key} must be a table.",
|
|
23
|
+
stringExpected: "{field} must be a string.",
|
|
24
|
+
integerExpected: "{field} must be an integer.",
|
|
25
|
+
stringArrayExpected: "{field} must be an array of non-empty strings.",
|
|
26
|
+
trustArray: "{path}: trust must be an array of tables.",
|
|
27
|
+
trustEntry: "{path}: a trust entry needs node_id and a 16-character fingerprint.",
|
|
28
|
+
acquirePolicy: "acquisition.policy must be all, segment or none, got {value}.",
|
|
29
|
+
acquireAllow: "acquisition.allow entries must be network/prefix blocks, got {value}.",
|
|
30
|
+
acquireDenied: "The acquisition policy {policy} refused this approval: {reason}.",
|
|
31
|
+
addressUnavailable: "No concrete network address is available for this bind host.",
|
|
32
|
+
keyProvider: "External credential references need a host key provider.",
|
|
33
|
+
loopbackBind: "The unauthenticated development binding requires a loopback bind host. Create a node key to serve a remote address.",
|
|
34
|
+
managementIdentity: "HTTP callers cannot claim this node's management identity.",
|
|
35
|
+
mentionNotFound: "No unique discovered node is named {name}.",
|
|
36
|
+
guideContacts: "Contacts",
|
|
37
|
+
guidePending: "Pending",
|
|
38
|
+
guideAccept: "Accept",
|
|
39
|
+
guideDiscover: "Discover",
|
|
40
|
+
guideRequestExchange: "Request key exchange",
|
|
41
|
+
resourcePageSize: "page_size must be from 1 to 100.",
|
|
42
|
+
resourceOwner: "Resource owner must be a non-empty node id.",
|
|
43
|
+
resourceRef: "Resource reads need meta.resource {owner,id}.",
|
|
44
|
+
resourceSince: "since must be a decimal event id.",
|
|
45
|
+
resourceOp: "Resource reads use list, describe, log, event with event_id, operations, or status with operation_id.",
|
|
46
|
+
};
|
|
47
|
+
const zh: Record<keyof typeof en, string> = {
|
|
48
|
+
regionEmpty: "region 不能为空。",
|
|
49
|
+
nodeIdEmpty: "node.id 不能为空。",
|
|
50
|
+
addressEmpty: "节点地址不能为空。",
|
|
51
|
+
addressUrl: "节点地址必须是绝对 URL,收到的是 {value}。",
|
|
52
|
+
addressScheme: "节点地址必须使用 http 或 https,收到的是 {value}。",
|
|
53
|
+
addressShape: "节点地址应写成源站(http://host:7801)或服务 URL(http://host:7801{service}),收到的是 {value}。",
|
|
54
|
+
configMissing: "在 {path} 没有 Onlyne 配置。运行 /onlyne init 创建。",
|
|
55
|
+
configParse: "{path}: {error}",
|
|
56
|
+
languageInvalid: "language 必须是 en 或 zh-CN。",
|
|
57
|
+
keyScopeInvalid: "node.key_scope 必须是 project、global 或 none,收到的是 {value}。",
|
|
58
|
+
publicUrlEmpty: "server.public_url 不能为空。",
|
|
59
|
+
bindEmpty: "server.bind 不能为空。",
|
|
60
|
+
bindShape: "server.bind 必须是主机名或 IP 地址,收到的是 {value}。",
|
|
61
|
+
pageSizeRange: "threads.page_size 必须是 1 到 100 之间的整数,收到的是 {value}。",
|
|
62
|
+
retiredEndpoint: "不支持 node.endpoint;端点由 server.bind 和监听端口决定。",
|
|
63
|
+
portRange: "server.port 必须是 0 到 65535 之间的整数,收到的是 {value}。",
|
|
64
|
+
retiredPeers: "不支持 peers;对端通过网络发现,并保存在项目 SQLite 中。",
|
|
65
|
+
retiredOmp: "不支持 omp;投送与派生由 OMP judge 决定。",
|
|
66
|
+
retiredAttention: "不支持 threads.default_attention;读取默认使用关注列表。",
|
|
67
|
+
tableExpected: "{path}: {key} 必须是表。",
|
|
68
|
+
stringExpected: "{field} 必须是字符串。",
|
|
69
|
+
integerExpected: "{field} 必须是整数。",
|
|
70
|
+
stringArrayExpected: "{field} 必须是非空字符串数组。",
|
|
71
|
+
trustArray: "{path}: trust 必须是表数组。",
|
|
72
|
+
trustEntry: "{path}: 每条 trust 记录需要 node_id 和 16 个字符的指纹。",
|
|
73
|
+
acquirePolicy: "acquisition.policy 必须是 all、segment 或 none,收到的是 {value}。",
|
|
74
|
+
acquireAllow: "acquisition.allow 的条目必须是 network/prefix 形式,收到的是 {value}。",
|
|
75
|
+
acquireDenied: "获取策略 {policy} 拒绝了这次批准:{reason}。",
|
|
76
|
+
addressUnavailable: "这个绑定主机没有可用的具体网络地址。",
|
|
77
|
+
keyProvider: "外部凭据引用需要主机密钥提供者。",
|
|
78
|
+
loopbackBind: "未认证的开发绑定需要回环绑定主机。要对外提供服务,请创建节点密钥。",
|
|
79
|
+
managementIdentity: "HTTP 调用方不能冒用本节点的管理身份。",
|
|
80
|
+
mentionNotFound: "没有唯一发现到的节点名为 {name}。",
|
|
81
|
+
guideContacts: "联系人",
|
|
82
|
+
guidePending: "待确认",
|
|
83
|
+
guideAccept: "同意",
|
|
84
|
+
guideDiscover: "发现",
|
|
85
|
+
guideRequestExchange: "请求交换密钥",
|
|
86
|
+
resourcePageSize: "page_size 必须是 1 到 100。",
|
|
87
|
+
resourceOwner: "资源 owner 必须是非空节点 id。",
|
|
88
|
+
resourceRef: "读取资源需要 meta.resource {owner,id}。",
|
|
89
|
+
resourceSince: "since 必须是十进制事件 id。",
|
|
90
|
+
resourceOp: "读取资源可用 list、describe、log、带 event_id 的 event、operations,或带 operation_id 的 status。",
|
|
91
|
+
};
|
|
92
|
+
export const hostText = createTranslator({ en, "zh-CN": zh });
|