@idleware/ringi 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +83 -2
- package/package.json +53 -4
- package/src/main/resources/profiles/a2a-1.json +43 -0
- package/src/main/resources/skill/ringi/SKILL.md +72 -0
- package/target/dts/app/agent.d.ts +104 -0
- package/target/dts/app/budget.d.ts +36 -0
- package/target/dts/app/context.d.ts +19 -0
- package/target/dts/app/hubs.d.ts +38 -0
- package/target/dts/app/overview.d.ts +25 -0
- package/target/dts/app/room.d.ts +76 -0
- package/target/dts/app/rooms.d.ts +26 -0
- package/target/dts/app/session.d.ts +60 -0
- package/target/dts/app/view.d.ts +64 -0
- package/target/dts/cli/agent.d.ts +10 -0
- package/target/dts/cli/args.d.ts +13 -0
- package/target/dts/cli/context.d.ts +6 -0
- package/target/dts/cli/index.d.ts +5 -0
- package/target/dts/cli/operator.d.ts +8 -0
- package/target/dts/cli/session.d.ts +7 -0
- package/target/dts/cli/shared.d.ts +20 -0
- package/target/dts/cli/skill.d.ts +4 -0
- package/target/dts/cli.d.ts +3 -0
- package/target/dts/domain/decisions.d.ts +65 -0
- package/target/dts/domain/index.d.ts +2 -0
- package/target/dts/domain/profiles.d.ts +39 -0
- package/target/dts/domain/signals.d.ts +37 -0
- package/target/dts/home/index.d.ts +21 -0
- package/target/dts/hub/index.d.ts +66 -0
- package/target/dts/hub/signals.d.ts +4 -0
- package/target/dts/index.d.ts +8 -0
- package/target/dts/lib/index.d.ts +18 -0
- package/target/dts/protocol/files.d.ts +9 -0
- package/target/dts/protocol/index.d.ts +72 -0
- package/target/dts/runtime/reader.d.ts +34 -0
- package/target/dts/runtime/session.d.ts +42 -0
- package/target/dts/runtime/watch.d.ts +17 -0
- package/target/dts/store/git/index.d.ts +64 -0
- package/target/dts/store/git/remote.d.ts +70 -0
- package/target/dts/store/index.d.ts +102 -0
- package/target/dts/store/kafka/index.d.ts +2 -0
- package/target/dts/store/temporal/index.d.ts +2 -0
- package/target/esm/app/agent.js +84 -0
- package/target/esm/app/budget.js +95 -0
- package/target/esm/app/context.js +16 -0
- package/target/esm/app/hubs.js +76 -0
- package/target/esm/app/overview.js +39 -0
- package/target/esm/app/room.js +127 -0
- package/target/esm/app/rooms.js +120 -0
- package/target/esm/app/session.js +97 -0
- package/target/esm/app/view.js +52 -0
- package/target/esm/cli/agent.js +40 -0
- package/target/esm/cli/args.js +61 -0
- package/target/esm/cli/context.js +8 -0
- package/target/esm/cli/index.js +125 -0
- package/target/esm/cli/operator.js +147 -0
- package/target/esm/cli/session.js +39 -0
- package/target/esm/cli/shared.js +143 -0
- package/target/esm/cli/skill.js +54 -0
- package/target/esm/cli.js +14 -0
- package/target/esm/domain/decisions.js +178 -0
- package/target/esm/domain/index.js +16 -0
- package/target/esm/domain/profiles.js +96 -0
- package/target/esm/domain/signals.js +129 -0
- package/target/esm/home/index.js +88 -0
- package/target/esm/hub/index.js +212 -0
- package/target/esm/hub/signals.js +27 -0
- package/target/esm/index.js +9 -0
- package/target/esm/lib/index.js +202 -0
- package/target/esm/protocol/files.js +19 -0
- package/target/esm/protocol/index.js +217 -0
- package/target/esm/runtime/reader.js +64 -0
- package/target/esm/runtime/session.js +112 -0
- package/target/esm/runtime/watch.js +304 -0
- package/target/esm/store/git/index.js +346 -0
- package/target/esm/store/git/remote.js +700 -0
- package/target/esm/store/index.js +15 -0
- package/target/esm/store/kafka/index.js +3 -0
- package/target/esm/store/temporal/index.js +3 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as Remote from './remote.ts';
|
|
2
|
+
import type { Descriptor, JoinKey, Roster, SignalRecord, Store, StoredMessage, StoredRoom } from '../index.ts';
|
|
3
|
+
import type { Entry } from '../../protocol/files.ts';
|
|
4
|
+
type GitOpt = {
|
|
5
|
+
input?: string | Buffer;
|
|
6
|
+
env?: Record<string, string>;
|
|
7
|
+
where?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const runGit: (args: string[], o?: GitOpt) => string;
|
|
10
|
+
export declare class GitStore implements Store {
|
|
11
|
+
readonly type = "git";
|
|
12
|
+
readonly trust: 'asserted' | 'signed';
|
|
13
|
+
readonly dir: string;
|
|
14
|
+
readonly d: Descriptor;
|
|
15
|
+
readonly home: string;
|
|
16
|
+
readonly h: Remote.Handle;
|
|
17
|
+
constructor(hubDir: string, d: Descriptor, home: string);
|
|
18
|
+
private args;
|
|
19
|
+
private git;
|
|
20
|
+
private gitOk;
|
|
21
|
+
rev(ref: string): string | null;
|
|
22
|
+
isAncestor(a: string, b: string): boolean;
|
|
23
|
+
isRef(s: string): boolean;
|
|
24
|
+
extends(older: string, newer: string): boolean;
|
|
25
|
+
private emptyTree;
|
|
26
|
+
private tree;
|
|
27
|
+
private commit;
|
|
28
|
+
private advance;
|
|
29
|
+
static create(hubDir: string, d: Descriptor, home: string, contract: Record<string, string>, text: string): GitStore;
|
|
30
|
+
static rootFor(hubDir: string, d: Descriptor, home: string): string;
|
|
31
|
+
sync(): void;
|
|
32
|
+
contractHead(): string;
|
|
33
|
+
contractFile(rev: string, name: string): string | null;
|
|
34
|
+
contractFiles(rev: string): string[];
|
|
35
|
+
amendContract(who: string, text: string, files: Record<string, string | Buffer>): string;
|
|
36
|
+
private base;
|
|
37
|
+
roomNames(archived: boolean): string[];
|
|
38
|
+
room(name: string): StoredRoom | null;
|
|
39
|
+
controlFile(room: StoredRoom, name: string, rev?: string): string | null;
|
|
40
|
+
createRoom(name: string, who: string, text: string, files: Record<string, string>): string;
|
|
41
|
+
updateControl(room: StoredRoom, who: string, text: string, files: Record<string, string>): string;
|
|
42
|
+
private refsUnder;
|
|
43
|
+
archiveRoom(room: StoredRoom, as: string): void;
|
|
44
|
+
deleteRoom(room: StoredRoom): void;
|
|
45
|
+
private legacy;
|
|
46
|
+
roster(room: StoredRoom): Roster | null;
|
|
47
|
+
logIds(room: StoredRoom): string[];
|
|
48
|
+
logHead(room: StoredRoom, id: string): string | null;
|
|
49
|
+
messages(room: StoredRoom, id: string, head: string): StoredMessage[];
|
|
50
|
+
append(room: StoredRoom, who: string, text: string, entries: Entry[]): string;
|
|
51
|
+
verifyLog(room: StoredRoom, id: string, head: string): string | null;
|
|
52
|
+
pin(room: StoredRoom, reader: string, id: string, rev: string): void;
|
|
53
|
+
unpin(room: StoredRoom, reader: string, id: string): void;
|
|
54
|
+
file(rev: string, p: string): Buffer;
|
|
55
|
+
sizes(rev: string, paths: string[]): Map<string, number>;
|
|
56
|
+
signal(room: StoredRoom, who: string, target: string, name: string, text: string): void;
|
|
57
|
+
signals(room: StoredRoom): SignalRecord[];
|
|
58
|
+
admit(room: StoredRoom, who: string, key: string): string;
|
|
59
|
+
joinKey(room: StoredRoom, who: string): JoinKey;
|
|
60
|
+
forgetLog(room: StoredRoom, id: string): void;
|
|
61
|
+
rootKey(): string;
|
|
62
|
+
rootRoster(): string;
|
|
63
|
+
}
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as C from '../../lib/index.ts';
|
|
2
|
+
import type { Descriptor, Member } from '../index.ts';
|
|
3
|
+
export type Handle = {
|
|
4
|
+
name: string;
|
|
5
|
+
dir: string;
|
|
6
|
+
d: Descriptor;
|
|
7
|
+
main?: string;
|
|
8
|
+
remoteAlarms?: Record<string, string>;
|
|
9
|
+
bootstrap?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type Room = {
|
|
12
|
+
name: string;
|
|
13
|
+
head: string;
|
|
14
|
+
genesis: string;
|
|
15
|
+
control: string;
|
|
16
|
+
log: (who: string) => string;
|
|
17
|
+
archived?: boolean;
|
|
18
|
+
members?: Record<string, Member>;
|
|
19
|
+
};
|
|
20
|
+
type Key = {
|
|
21
|
+
line: string;
|
|
22
|
+
fp: string;
|
|
23
|
+
};
|
|
24
|
+
type Signer = {
|
|
25
|
+
principal: string;
|
|
26
|
+
key: Key;
|
|
27
|
+
};
|
|
28
|
+
type VerifiedRoom = {
|
|
29
|
+
genesis: string;
|
|
30
|
+
entries: Signer[];
|
|
31
|
+
members: Record<string, Member>;
|
|
32
|
+
};
|
|
33
|
+
export declare const withVerification: <T>(H: Handle, fn: () => T) => T;
|
|
34
|
+
declare const strictJson: typeof C.strictJson;
|
|
35
|
+
export { strictJson };
|
|
36
|
+
export declare const gitEnv: (extra?: NodeJS.ProcessEnv) => NodeJS.ProcessEnv;
|
|
37
|
+
export declare const gitTimeout: () => number;
|
|
38
|
+
export declare const checkGitTimeout: (error?: NodeJS.ErrnoException) => void;
|
|
39
|
+
export declare const isRemote: (H: Handle) => boolean;
|
|
40
|
+
export declare const parseKey: (value: unknown) => Key;
|
|
41
|
+
export declare const setupSigned: (H: Handle, home: string, url?: string) => {
|
|
42
|
+
root: string;
|
|
43
|
+
allowedSigners: string;
|
|
44
|
+
};
|
|
45
|
+
export declare const sign: (H: Handle, who: string, home: string) => string[];
|
|
46
|
+
export declare const rootRoster: (H: Handle) => string;
|
|
47
|
+
export declare const verifyHub: (H: Handle) => {
|
|
48
|
+
head: string;
|
|
49
|
+
genesis: string;
|
|
50
|
+
commits: string[];
|
|
51
|
+
} | null;
|
|
52
|
+
export declare const verifyRoom: (H: Handle, R: Room) => VerifiedRoom;
|
|
53
|
+
export declare const verify: (H: Handle, R: Room, who: string, head: string) => string | null;
|
|
54
|
+
export declare const verifySignal: (H: Handle, R: Room, ref: string, sha: string) => string | null;
|
|
55
|
+
export declare const sync: (H: Handle) => Handle;
|
|
56
|
+
export declare const hubAdd: ({ home, name, url, root, genesis }: {
|
|
57
|
+
home: string;
|
|
58
|
+
name: string;
|
|
59
|
+
url: string;
|
|
60
|
+
root?: string;
|
|
61
|
+
genesis?: string;
|
|
62
|
+
}) => string;
|
|
63
|
+
export declare const join: (H: Handle, R: Room, who: string, home: string) => {
|
|
64
|
+
publicKey: string;
|
|
65
|
+
fingerprint: string;
|
|
66
|
+
admitted: boolean;
|
|
67
|
+
};
|
|
68
|
+
export declare const roomAdmit: (H: Handle, R: Room, who: string, publicKey: string, home: string) => string;
|
|
69
|
+
export declare const push: (H: Handle, ref: string) => void;
|
|
70
|
+
export declare const forget: (H: Handle, R: Room, who: string) => void;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Entry } from '../protocol/files.ts';
|
|
2
|
+
export type Rev = string;
|
|
3
|
+
export type Member = {
|
|
4
|
+
role?: string;
|
|
5
|
+
since?: string;
|
|
6
|
+
};
|
|
7
|
+
export type StoredMessage = {
|
|
8
|
+
id: Rev;
|
|
9
|
+
parent: Rev | null;
|
|
10
|
+
time: number;
|
|
11
|
+
text: Buffer;
|
|
12
|
+
files: {
|
|
13
|
+
path: string;
|
|
14
|
+
mode: string;
|
|
15
|
+
}[];
|
|
16
|
+
refused: {
|
|
17
|
+
path: string;
|
|
18
|
+
mode: string;
|
|
19
|
+
why?: string;
|
|
20
|
+
}[];
|
|
21
|
+
};
|
|
22
|
+
export type ControlRecord = {
|
|
23
|
+
rev: Rev;
|
|
24
|
+
text: string;
|
|
25
|
+
};
|
|
26
|
+
export type StoredRoom = {
|
|
27
|
+
name: string;
|
|
28
|
+
archived: boolean;
|
|
29
|
+
head: Rev;
|
|
30
|
+
genesis: Rev;
|
|
31
|
+
base: Rev;
|
|
32
|
+
control: ControlRecord[];
|
|
33
|
+
};
|
|
34
|
+
export type SignalRecord = {
|
|
35
|
+
ref: string;
|
|
36
|
+
object: Rev;
|
|
37
|
+
name: string;
|
|
38
|
+
actor: string;
|
|
39
|
+
target: Rev;
|
|
40
|
+
tagger: string;
|
|
41
|
+
text: string;
|
|
42
|
+
};
|
|
43
|
+
export type Roster = {
|
|
44
|
+
members: Record<string, Member>;
|
|
45
|
+
signers: Array<{
|
|
46
|
+
principal: string;
|
|
47
|
+
}>;
|
|
48
|
+
};
|
|
49
|
+
export type JoinKey = {
|
|
50
|
+
publicKey: string;
|
|
51
|
+
fingerprint: string;
|
|
52
|
+
admitted: boolean;
|
|
53
|
+
};
|
|
54
|
+
export interface Store {
|
|
55
|
+
readonly type: string;
|
|
56
|
+
readonly trust: 'asserted' | 'signed';
|
|
57
|
+
isRef(s: string): boolean;
|
|
58
|
+
sync(): void;
|
|
59
|
+
contractHead(): Rev;
|
|
60
|
+
contractFile(rev: Rev, name: string): string | null;
|
|
61
|
+
contractFiles(rev: Rev): string[];
|
|
62
|
+
amendContract(who: string, text: string, files: Record<string, string | Buffer>): Rev;
|
|
63
|
+
roomNames(archived: boolean): string[];
|
|
64
|
+
room(name: string): StoredRoom | null;
|
|
65
|
+
controlFile(room: StoredRoom, name: string, rev?: Rev): string | null;
|
|
66
|
+
createRoom(name: string, who: string, text: string, files: Record<string, string>): Rev;
|
|
67
|
+
updateControl(room: StoredRoom, who: string, text: string, files: Record<string, string>): Rev;
|
|
68
|
+
archiveRoom(room: StoredRoom, as: string): void;
|
|
69
|
+
deleteRoom(room: StoredRoom): void;
|
|
70
|
+
roster(room: StoredRoom): Roster | null;
|
|
71
|
+
logIds(room: StoredRoom): string[];
|
|
72
|
+
logHead(room: StoredRoom, id: string): Rev | null;
|
|
73
|
+
extends(older: Rev, newer: Rev): boolean;
|
|
74
|
+
messages(room: StoredRoom, id: string, head: Rev): StoredMessage[];
|
|
75
|
+
append(room: StoredRoom, who: string, text: string, entries: Entry[]): Rev;
|
|
76
|
+
verifyLog(room: StoredRoom, id: string, head: Rev): string | null;
|
|
77
|
+
pin(room: StoredRoom, reader: string, id: string, rev: Rev): void;
|
|
78
|
+
unpin(room: StoredRoom, reader: string, id: string): void;
|
|
79
|
+
file(rev: Rev, path: string): Buffer;
|
|
80
|
+
sizes(rev: Rev, paths: string[]): Map<string, number>;
|
|
81
|
+
signal(room: StoredRoom, who: string, target: Rev, name: string, text: string): void;
|
|
82
|
+
signals(room: StoredRoom): SignalRecord[];
|
|
83
|
+
admit?(room: StoredRoom, who: string, key: string): Rev;
|
|
84
|
+
joinKey?(room: StoredRoom, who: string): JoinKey;
|
|
85
|
+
forgetLog?(room: StoredRoom, id: string): void;
|
|
86
|
+
rootKey?(): string;
|
|
87
|
+
rootRoster?(): string;
|
|
88
|
+
}
|
|
89
|
+
export type Descriptor = {
|
|
90
|
+
schema: 'ringi-hub/2';
|
|
91
|
+
name: string;
|
|
92
|
+
trust: 'asserted' | 'signed';
|
|
93
|
+
root?: string;
|
|
94
|
+
genesis?: string;
|
|
95
|
+
store: {
|
|
96
|
+
type: string;
|
|
97
|
+
path: string;
|
|
98
|
+
remote?: string;
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
export type StoreFactory = (dir: string, d: Descriptor, home: string) => Store;
|
|
102
|
+
export declare const STORES: Record<string, StoreFactory>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Agent operations return data; the runtime owns readers, seats and headless turns.
|
|
2
|
+
import { HIDDEN, append, pongText, withSizes } from '../hub/index.js';
|
|
3
|
+
import * as Signals from '../hub/signals.js';
|
|
4
|
+
import { fail, sleep } from '../lib/index.js';
|
|
5
|
+
import { ID_RE, checkOutgoing, compose, link, lookup, unread } from '../protocol/index.js';
|
|
6
|
+
import { linked, loadRoom, loadRoomUnlocked, loadState, readerLock, saveState } from '../runtime/reader.js';
|
|
7
|
+
import { bound } from '../runtime/session.js';
|
|
8
|
+
import { isRef } from './room.js';
|
|
9
|
+
export { bound } from '../runtime/session.js';
|
|
10
|
+
export { watch } from '../runtime/watch.js';
|
|
11
|
+
export const inbox = ({ R, me }, { peek = false, limit = 20 } = {}) => {
|
|
12
|
+
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
13
|
+
fail('inbox limit must be a positive integer');
|
|
14
|
+
const res = readerLock(R, me, () => {
|
|
15
|
+
const { logs, st } = loadRoomUnlocked(R, me);
|
|
16
|
+
const r = unread(logs, me, st.seen ?? {}, limit, HIDDEN);
|
|
17
|
+
if (!peek)
|
|
18
|
+
saveState(R, me, { ...loadState(R, me), seen: r.cursors });
|
|
19
|
+
return { all: link(logs), ...r };
|
|
20
|
+
});
|
|
21
|
+
if (!peek)
|
|
22
|
+
for (const m of res.msgs)
|
|
23
|
+
Signals.mark(R, me, m, 'read');
|
|
24
|
+
return { ...res, msgs: res.msgs.map(m => ({ ...m, files: withSizes(R, m) })) };
|
|
25
|
+
};
|
|
26
|
+
export const take = ({ R, me }, ref) => {
|
|
27
|
+
const m = lookup(linked(R, me).all, ref);
|
|
28
|
+
return { m, made: Signals.mark(R, me, m, 'taken') };
|
|
29
|
+
};
|
|
30
|
+
export const whoami = ({ s, R }) => ({
|
|
31
|
+
session: s ?? fail('agent identity needs a joined session'), profile: R.profile.name, members: Object.keys(R.members),
|
|
32
|
+
});
|
|
33
|
+
export const card = ({ R, me }, text) => {
|
|
34
|
+
const message = compose(text, [['To', '*'], ['Kind', 'card']]);
|
|
35
|
+
checkOutgoing(message, isRef(R));
|
|
36
|
+
return append(R, me, message);
|
|
37
|
+
};
|
|
38
|
+
export const ping = ({ R, me }, { to, text = 'ping: still working', next = 600, re }) => {
|
|
39
|
+
if (!ID_RE.test(to))
|
|
40
|
+
fail(`bad recipient ${to}`);
|
|
41
|
+
if (!Number.isFinite(next) || next <= 0)
|
|
42
|
+
fail('ping next must be positive seconds');
|
|
43
|
+
const { all } = linked(R, me);
|
|
44
|
+
const mine = [...all.values()].filter(m => m.from === me && m.kind === 'ping' && m.to.includes(to)).at(-1);
|
|
45
|
+
const pong = mine && [...all.values()].find(m => m.kind === 'pong' && m.from === to && m.reId === mine.id);
|
|
46
|
+
const parent = re ? lookup(all, re) : null;
|
|
47
|
+
const message = compose(text, [['To', to], ['Re', parent?.id], ['Kind', 'ping'], ['Next', String(next)], ['Hop', parent && String(parent.hopEff + 1)]]);
|
|
48
|
+
checkOutgoing(message, isRef(R));
|
|
49
|
+
return { id: append(R, me, message), to, previous: mine ? { id: mine.id, answeredAt: pong?.time ?? null } : null };
|
|
50
|
+
};
|
|
51
|
+
export const wait = async (a, { max = 120, ...options } = {}) => {
|
|
52
|
+
if (!Number.isFinite(max) || max < 0)
|
|
53
|
+
fail('wait max must be nonnegative seconds');
|
|
54
|
+
const start = Date.now(), ponged = new Set();
|
|
55
|
+
let last = start, silence = Math.min(max, 110);
|
|
56
|
+
for (;;) {
|
|
57
|
+
// Each poll renews the lease and checks stop, takeover, membership and the room's identity.
|
|
58
|
+
const current = bound();
|
|
59
|
+
if (current.R.genesis !== a.R.genesis || current.me !== a.me)
|
|
60
|
+
fail('the waiting session changed its binding');
|
|
61
|
+
const { R, me } = current;
|
|
62
|
+
const { logs, st } = loadRoom(R, me);
|
|
63
|
+
if (unread(logs, me, st.seen ?? {}, Infinity, HIDDEN).msgs.length)
|
|
64
|
+
return { inbox: inbox(current, options), seconds: (Date.now() - start) / 1000 };
|
|
65
|
+
link(logs);
|
|
66
|
+
for (const m of logs.get(me) ?? [])
|
|
67
|
+
if (m.kind === 'pong' && m.reId)
|
|
68
|
+
ponged.add(m.reId);
|
|
69
|
+
for (const msgs of logs.values())
|
|
70
|
+
for (const m of msgs) {
|
|
71
|
+
if (m.kind !== 'ping' || m.bad || !m.to.includes(me) || ponged.has(m.id))
|
|
72
|
+
continue;
|
|
73
|
+
ponged.add(m.id);
|
|
74
|
+
last = Date.now();
|
|
75
|
+
silence = Math.max(silence, Number(m.trailers.next?.[0]) || 600);
|
|
76
|
+
append(R, me, pongText(me, m, 'is waiting'));
|
|
77
|
+
}
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
if (now - last >= silence * 1000 || now - start >= max * 1000)
|
|
80
|
+
return { inbox: null, seconds: (now - start) / 1000 };
|
|
81
|
+
await sleep(Math.min(1000, max * 1000 - (now - start)));
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=agent.js.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// budget: proposals, reviewers' stamps, the decider's decision, and the grants it makes (decisions/1). Each operation
|
|
2
|
+
// takes who acts and typed input, and returns data; interfaces print it.
|
|
3
|
+
import * as D from '../domain/decisions.js';
|
|
4
|
+
import { OPERATOR, locked } from '../home/index.js';
|
|
5
|
+
import { append } from '../hub/index.js';
|
|
6
|
+
import { fail, note, short } from '../lib/index.js';
|
|
7
|
+
import { ID_RE, checkOutgoing, compose, lookup } from '../protocol/index.js';
|
|
8
|
+
import { linked } from '../runtime/reader.js';
|
|
9
|
+
import { isRef } from './room.js';
|
|
10
|
+
import { grantView, proposalView } from './view.js';
|
|
11
|
+
export const propose = ({ R, me }, x) => {
|
|
12
|
+
const { all } = linked(R, me);
|
|
13
|
+
if (!ID_RE.test(x.decider))
|
|
14
|
+
fail(`bad decider ${x.decider}`);
|
|
15
|
+
const reviewers = [...new Set(x.reviewers ?? [])].filter(r => r !== me);
|
|
16
|
+
for (const r of reviewers)
|
|
17
|
+
if (!ID_RE.test(r))
|
|
18
|
+
fail(`bad reviewer ${r}`);
|
|
19
|
+
const budget = x.budget ? D.parseBudget(x.budget) ?? fail('--budget takes runs=<n>[, run-seconds=<n>][, concurrency=<n>]') : null;
|
|
20
|
+
const sup = x.supersedes ? lookup(all, x.supersedes) : null;
|
|
21
|
+
if (sup && (sup.kind !== 'proposal' || (sup.from !== me && me !== OPERATOR)))
|
|
22
|
+
fail(`${short(sup.id)} is not your proposal`);
|
|
23
|
+
if (budget && !D.mayDecide(R.contract.authority, x.decider, me, budget))
|
|
24
|
+
note(`${x.decider} has no authority for this budget in hub ${R.hub.name}; only operator could approve it`);
|
|
25
|
+
const to = [...new Set([...reviewers, x.decider])].filter(t => t !== me);
|
|
26
|
+
const text = compose(x.text, [['To', to.join(', ') || '*'], ['Kind', 'proposal'], ['Decider', x.decider], ['Reviewers', reviewers.join(', ')],
|
|
27
|
+
['Due', x.due && D.parseDue(x.due)], ['Budget', budget && D.budgetText(budget)], ['Supersedes', sup?.id]]);
|
|
28
|
+
const bad = D.check(checkOutgoing(text, isRef(R)), isRef(R));
|
|
29
|
+
if (bad)
|
|
30
|
+
fail(`refusing to send: ${bad}`);
|
|
31
|
+
return { id: append(R, me, text, x.files ?? []), to, budget };
|
|
32
|
+
};
|
|
33
|
+
const proposalAt = (all, ref) => {
|
|
34
|
+
const p = lookup(all, ref);
|
|
35
|
+
if (p.kind !== 'proposal' || p.bad)
|
|
36
|
+
fail(`${short(p.id)} is not a proposal`);
|
|
37
|
+
return p;
|
|
38
|
+
};
|
|
39
|
+
export const stamp = ({ R, me }, ref, s, text) => {
|
|
40
|
+
const { all } = linked(R, me);
|
|
41
|
+
const p = proposalAt(all, ref);
|
|
42
|
+
if (!D.STAMPS.includes(s))
|
|
43
|
+
fail(`usage: stamp <proposal> ${D.STAMPS.join('|')} -m <reason>`);
|
|
44
|
+
const info = D.proposalOf(all, p, R.authorityAt);
|
|
45
|
+
if (!info.reviewers.includes(me))
|
|
46
|
+
fail(`you are not a named reviewer of ${short(p.id)} (reviewers: ${info.reviewers.join(', ') || 'none'})`);
|
|
47
|
+
if (info.decision)
|
|
48
|
+
fail(`${short(p.id)} is already ${info.decision.decision}`);
|
|
49
|
+
const out = compose(text, [['To', [...new Set([p.from, info.decider])].filter(x => x !== me).join(', ') || p.from], ['Re', p.id], ['Kind', 'stamp'], ['Stamp', s], ['Hop', String(p.hopEff + 1)]]);
|
|
50
|
+
checkOutgoing(out, isRef(R));
|
|
51
|
+
return { proposal: p, id: append(R, me, out) };
|
|
52
|
+
};
|
|
53
|
+
// The decision names the control entry, and so the authority, it was made under (decisions/1).
|
|
54
|
+
export const decide = ({ R, me }, ref, d, text) => locked(`grant-${R.hub.name}`, () => {
|
|
55
|
+
const { all } = linked(R, me);
|
|
56
|
+
const p = proposalAt(all, ref);
|
|
57
|
+
if (!D.DECISIONS.includes(d))
|
|
58
|
+
fail(`usage: decide <proposal> ${D.DECISIONS.join('|')} -m <reason>`);
|
|
59
|
+
const info = D.proposalOf(all, p, R.authorityAt);
|
|
60
|
+
if (d === 'revoked') {
|
|
61
|
+
if (info.state !== 'approved')
|
|
62
|
+
fail(`${short(p.id)} is ${info.state}; only an approval can be revoked`);
|
|
63
|
+
if (me !== info.decider && me !== OPERATOR)
|
|
64
|
+
fail(`only ${info.decider} or the operator revokes it`);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
if (me !== info.decider)
|
|
68
|
+
fail(`only the named decider, ${info.decider}, decides ${short(p.id)}`);
|
|
69
|
+
if (!D.mayDecide(R.contract.authority, me, p.from, info.budget))
|
|
70
|
+
fail(`${me} has no authority to decide this${info.budget ? ` budget (${D.budgetText(info.budget)})` : ''} in hub ${R.hub.name}, or it is your own proposal`);
|
|
71
|
+
if (info.decision)
|
|
72
|
+
fail(`${short(p.id)} is already ${info.decision.decision}`);
|
|
73
|
+
if (info.superseded)
|
|
74
|
+
fail(`${short(p.id)} was superseded by ${short(info.superseded.id)}; decide that revision`);
|
|
75
|
+
}
|
|
76
|
+
const to = [...new Set([p.from, ...info.reviewers])].filter(x => x !== me);
|
|
77
|
+
const out = compose(text, [['To', to.join(', ') || '*'], ['Re', p.id], ['Kind', 'decision'], ['Decision', d], ['Control', R.head], ['Hop', String(p.hopEff + 1)]]);
|
|
78
|
+
checkOutgoing(out, isRef(R));
|
|
79
|
+
const id = append(R, me, out);
|
|
80
|
+
if (d === 'approved' && info.missing.length)
|
|
81
|
+
note(`decided without stamps from ${info.missing.join(', ')}`);
|
|
82
|
+
return { proposal: p, id, budget: d === 'approved' ? info.budget : null };
|
|
83
|
+
});
|
|
84
|
+
// Open proposals (with `all`, every one).
|
|
85
|
+
export const proposals = ({ R, me }, { all: everything = false } = {}) => {
|
|
86
|
+
const { all } = linked(R, me);
|
|
87
|
+
const ps = D.proposals(all, R.authorityAt).filter(x => everything || ['open', 'overdue'].includes(x.state));
|
|
88
|
+
return { proposals: ps, views: ps.map(proposalView) };
|
|
89
|
+
};
|
|
90
|
+
export const grants = ({ R, me }) => {
|
|
91
|
+
const { all } = linked(R, me);
|
|
92
|
+
return D.proposals(all, R.authorityAt).filter(x => x.grant).map(x => grantView(x, D.usageOf(all, x.grant)));
|
|
93
|
+
};
|
|
94
|
+
export { DECISIONS, STAMPS, budgetText } from '../domain/decisions.js';
|
|
95
|
+
//# sourceMappingURL=budget.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// context: who acts and where, as an interface asks for it (the operator in its terminal context, an agent through its
|
|
2
|
+
// session's binding), and what this build is.
|
|
3
|
+
import { HOME, OPERATOR, RESOURCES, VERSION, config } from '../home/index.js';
|
|
4
|
+
import { CAPS, HUB_MAJOR, openHub, room as roomOf } from '../hub/index.js';
|
|
5
|
+
import { bound } from '../runtime/session.js';
|
|
6
|
+
// The operator, in the named hub and room, else in the terminal's current ones.
|
|
7
|
+
export const operator = ({ hub, room } = {}) => {
|
|
8
|
+
const H = openHub(hub ?? config().hub);
|
|
9
|
+
const name = room ?? (hub && hub !== config().hub ? null : config().room);
|
|
10
|
+
return { R: roomOf(H, name), me: OPERATOR };
|
|
11
|
+
};
|
|
12
|
+
// The agent this session is bound to; refuses when it is not joined, is stopped, or its room is gone.
|
|
13
|
+
export const agent = () => bound();
|
|
14
|
+
export const about = () => ({ version: VERSION, home: HOME, resources: RESOURCES, contract: `${HUB_MAJOR}.x`, capabilities: [...CAPS] });
|
|
15
|
+
export { config };
|
|
16
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// hubs: the operator's hubs on this machine: list, create, switch, add a signed replica, amend the shared contract.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { DEFAULT_PROFILE, PROFILES_CAPABILITY, parseProfile } from '../domain/profiles.js';
|
|
5
|
+
import { HOME, OPERATOR, config, ensureHome, setConfig } from '../home/index.js';
|
|
6
|
+
import { createHub, hubNames, openHub, roomNames } from '../hub/index.js';
|
|
7
|
+
import { fail, strictJson, words } from '../lib/index.js';
|
|
8
|
+
import { ID_RE, compose } from '../protocol/index.js';
|
|
9
|
+
import * as Remote from '../store/git/remote.js';
|
|
10
|
+
// The operator's terminal context picks the hub unless one is named.
|
|
11
|
+
export const hubOf = (name) => openHub(name ?? config().hub);
|
|
12
|
+
export const hubs = () => {
|
|
13
|
+
ensureHome();
|
|
14
|
+
if (!hubNames().length)
|
|
15
|
+
openHub('local');
|
|
16
|
+
const cur = config().hub;
|
|
17
|
+
return hubNames().map(n => {
|
|
18
|
+
const H = openHub(n);
|
|
19
|
+
return { name: H.name, current: H.name === cur, store: H.d.store.type, trust: H.contract.trust, contract: H.contract.version,
|
|
20
|
+
rooms: roomNames(H).length, remote: H.d.store.remote ?? null, dir: H.dir };
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
// A new hub; a signed one makes its root key here and returns what other machines need to pin it.
|
|
24
|
+
export const create = (name, { signed = false, url } = {}) => {
|
|
25
|
+
if (!ID_RE.test(name))
|
|
26
|
+
fail('usage: ringi hub create <name> [--signed [--url <git url>]]');
|
|
27
|
+
createHub(name, { signed, url });
|
|
28
|
+
const H = openHub(name);
|
|
29
|
+
return { H, rootKey: H.signed ? path.join(HOME, 'keys', name, OPERATOR) : null,
|
|
30
|
+
add: H.signed ? `ringi hub add ${name} ${H.d.store.remote ?? '<url>'} --root '${H.d.root}' --genesis ${H.d.genesis}` : null };
|
|
31
|
+
};
|
|
32
|
+
export const use = (name) => {
|
|
33
|
+
openHub(name);
|
|
34
|
+
setConfig({ hub: name, room: null });
|
|
35
|
+
};
|
|
36
|
+
// A replica of a signed hub. The root key comes from the operator, out of band: a key the hub advertises proves nothing.
|
|
37
|
+
export const add = (name, url, { root, genesis }) => {
|
|
38
|
+
if (!ID_RE.test(name) || !url)
|
|
39
|
+
fail('usage: ringi hub add <name> <git url> --root <public key|file> [--genesis <sha>]');
|
|
40
|
+
ensureHome();
|
|
41
|
+
Remote.hubAdd({ home: HOME, name, url, root, genesis });
|
|
42
|
+
return openHub(name);
|
|
43
|
+
};
|
|
44
|
+
// A new version of the shared contract on `main`. Rooms keep their base until moved (rooms.rebase).
|
|
45
|
+
export const amend = (H, x) => {
|
|
46
|
+
const name = H.store.contractFile(H.main, 'contract.json') !== null ? 'contract.json' : 'hub.json';
|
|
47
|
+
const contract = strictJson(H.store.contractFile(H.main, name), name);
|
|
48
|
+
for (const kv of x.set ?? []) {
|
|
49
|
+
const [k, v] = kv.split(/=(.*)/s);
|
|
50
|
+
if (!['version', 'capabilities', 'protocol', 'profile'].includes(k))
|
|
51
|
+
fail(`contract keys that can be set: version, capabilities, protocol, profile (not ${k})`);
|
|
52
|
+
contract[k] = k === 'capabilities' ? words(v) : k === 'protocol' ? Number(v) : v;
|
|
53
|
+
}
|
|
54
|
+
const files = { [name]: JSON.stringify(contract, null, 2) + '\n' };
|
|
55
|
+
if (x.authority)
|
|
56
|
+
files['authority.json'] = JSON.stringify(strictJson(fs.readFileSync(x.authority, 'utf8'), x.authority), null, 2) + '\n';
|
|
57
|
+
for (const f of x.files ?? []) {
|
|
58
|
+
// A profile is checked before it reaches the contract.
|
|
59
|
+
if (/^profiles\/[^/]+\.json$/.test(f.path))
|
|
60
|
+
parseProfile(f.content.toString('utf8'), f.path);
|
|
61
|
+
files[f.path] = f.content;
|
|
62
|
+
}
|
|
63
|
+
if (typeof contract.profile === 'string' && contract.profile !== DEFAULT_PROFILE && !files[`profiles/${contract.profile}.json`] && H.store.contractFile(H.main, `profiles/${contract.profile}.json`) === null) {
|
|
64
|
+
fail(`the default profile ${contract.profile} is not in the hub; attach it: --attach <file>:profiles/${contract.profile}.json`);
|
|
65
|
+
}
|
|
66
|
+
// Profiles other than the built-in A2A rules need clients that read them: the contract says so.
|
|
67
|
+
if (Object.keys(files).some(f => f.startsWith('profiles/')) || (x.set ?? []).some(kv => kv.startsWith('profile='))) {
|
|
68
|
+
const caps = Array.isArray(contract.capabilities) ? contract.capabilities : [];
|
|
69
|
+
if (!caps.includes(PROFILES_CAPABILITY))
|
|
70
|
+
contract.capabilities = [...caps, PROFILES_CAPABILITY];
|
|
71
|
+
files[name] = JSON.stringify(contract, null, 2) + '\n';
|
|
72
|
+
}
|
|
73
|
+
H.store.amendContract(OPERATOR, compose(x.text, [['To', '*'], ['Kind', 'hub']]), files);
|
|
74
|
+
return { version: String(contract.version) };
|
|
75
|
+
};
|
|
76
|
+
//# sourceMappingURL=hubs.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// overview: where things stand for the operator: the hub, the room, who is in, open work and proposals, the latest
|
|
2
|
+
// messages, and new ones as they land; and the sessions of this machine.
|
|
3
|
+
import * as D from '../domain/decisions.js';
|
|
4
|
+
import { OPERATOR, ensureHome, locked } from '../home/index.js';
|
|
5
|
+
import { HIDDEN, openHub, room as roomOf } from '../hub/index.js';
|
|
6
|
+
import { fail } from '../lib/index.js';
|
|
7
|
+
import { ordered } from '../protocol/index.js';
|
|
8
|
+
import { linked } from '../runtime/reader.js';
|
|
9
|
+
import { allSessions, dropSession, liveness, readSession } from '../runtime/session.js';
|
|
10
|
+
import { tasks } from './room.js';
|
|
11
|
+
const sessionView = (s) => ({ ...s, liveness: liveness(s) });
|
|
12
|
+
export const where = (H, name) => {
|
|
13
|
+
if (!name)
|
|
14
|
+
return { hub: H, room: null, sessions: [], work: [], proposals: [], all: new Map(), messages: [] };
|
|
15
|
+
const R = roomOf(H, name);
|
|
16
|
+
const { all } = linked(R, OPERATOR);
|
|
17
|
+
return { hub: H, room: R, sessions: allSessions().filter(s => s.hub === H.name && s.genesis === R.genesis).map(sessionView),
|
|
18
|
+
work: tasks({ R, me: OPERATOR }).views, proposals: D.proposals(all, R.authorityAt).filter(x => ['open', 'overdue'].includes(x.state)),
|
|
19
|
+
all, messages: ordered([...all.values()].filter(m => !(m.kind && HIDDEN.has(m.kind)))) };
|
|
20
|
+
};
|
|
21
|
+
// The visible messages that landed since `seen` (which it extends), read afresh from the store.
|
|
22
|
+
export const arrived = (H, R, seen) => {
|
|
23
|
+
const { all } = linked(roomOf(openHub(H.name, { refresh: true }), R.name), OPERATOR);
|
|
24
|
+
const messages = ordered([...all.values()].filter(m => !seen.has(m.id) && !(m.kind && HIDDEN.has(m.kind))));
|
|
25
|
+
for (const m of messages)
|
|
26
|
+
seen.add(m.id);
|
|
27
|
+
return { all, messages };
|
|
28
|
+
};
|
|
29
|
+
export const sessions = () => {
|
|
30
|
+
ensureHome();
|
|
31
|
+
return allSessions().map(sessionView);
|
|
32
|
+
};
|
|
33
|
+
// The seat a session held becomes free.
|
|
34
|
+
export const forgetSession = (handle) => locked('sessions', () => {
|
|
35
|
+
if (!readSession(handle))
|
|
36
|
+
fail(`no session ${handle}`);
|
|
37
|
+
dropSession(handle);
|
|
38
|
+
});
|
|
39
|
+
//# sourceMappingURL=overview.js.map
|