@verax-ai/proxy 0.1.2 → 0.1.4
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/dist/in-flight-log.d.ts +12 -0
- package/dist/in-flight-log.js +65 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/proxy.js +52 -4
- package/dist/verify-ledger.d.ts +25 -0
- package/dist/verify-ledger.js +171 -0
- package/package.json +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Called before `inner` runs. A failure here must not block the call.
|
|
3
|
+
*
|
|
4
|
+
* The stamp is wall-clock time and not the proxy's injected `now`: a mark is
|
|
5
|
+
* not a ledger row, and taking a tick from a deterministic clock would shift
|
|
6
|
+
* every timestamp recorded after it — the golden ledger caught exactly that.
|
|
7
|
+
*/
|
|
8
|
+
export declare function markStarted(stateDir: string, ref: string, subject: string): void;
|
|
9
|
+
/** Called once the effect row is written, on success and on failure alike. */
|
|
10
|
+
export declare function markEnded(stateDir: string, ref: string): void;
|
|
11
|
+
/** True when a run started and no end was recorded: the outcome is unknown. */
|
|
12
|
+
export declare function startedWithoutEnd(stateDir: string, ref: string): boolean;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable in-flight marks: "this ref started running".
|
|
3
|
+
*
|
|
4
|
+
* The in-memory registry that stops a second run dies with the process. On the
|
|
5
|
+
* approved path an `allow` does not mean the tool ran — it means the operator
|
|
6
|
+
* said yes — so a restarted body cannot tell "approved, never started" from
|
|
7
|
+
* "approved, started, crashed before the effect". Without that distinction it
|
|
8
|
+
* has to either run everything twice or run nothing at all.
|
|
9
|
+
*
|
|
10
|
+
* A mark is one empty file per ref, created before `inner` and removed after
|
|
11
|
+
* the effect is written. One file per ref rather than a shared log because two
|
|
12
|
+
* refs may start and finish at the same moment: file create and unlink need no
|
|
13
|
+
* lock, a shared append-only log would need a compaction pass and would race.
|
|
14
|
+
*
|
|
15
|
+
* A mark left behind by a crash is exactly the signal wanted: it outlives the
|
|
16
|
+
* process, and the retry that finds it answers `outcome-unknown` instead of
|
|
17
|
+
* running the tool a second time.
|
|
18
|
+
*/
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
const KLASOR = "in-flight";
|
|
23
|
+
/** Ref may carry a tenant prefix and any path character; the name never does. */
|
|
24
|
+
function markPath(stateDir, ref) {
|
|
25
|
+
const ad = createHash("sha256").update(ref, "utf8").digest("hex").slice(0, 32);
|
|
26
|
+
return join(stateDir, KLASOR, `${ad}.start`);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Called before `inner` runs. A failure here must not block the call.
|
|
30
|
+
*
|
|
31
|
+
* The stamp is wall-clock time and not the proxy's injected `now`: a mark is
|
|
32
|
+
* not a ledger row, and taking a tick from a deterministic clock would shift
|
|
33
|
+
* every timestamp recorded after it — the golden ledger caught exactly that.
|
|
34
|
+
*/
|
|
35
|
+
export function markStarted(stateDir, ref, subject) {
|
|
36
|
+
try {
|
|
37
|
+
mkdirSync(join(stateDir, KLASOR), { recursive: true, mode: 0o700 });
|
|
38
|
+
// The body is for a human reading the directory after a crash; nothing parses it.
|
|
39
|
+
writeFileSync(markPath(stateDir, ref), `${JSON.stringify({ ref, subject, atMs: Date.now() })}\n`, {
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
mode: 0o600,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// A missing mark degrades to today's behaviour, it does not deny the call.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Called once the effect row is written, on success and on failure alike. */
|
|
49
|
+
export function markEnded(stateDir, ref) {
|
|
50
|
+
try {
|
|
51
|
+
rmSync(markPath(stateDir, ref), { force: true });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Leaving a stale mark makes the next retry cautious, never wrong.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** True when a run started and no end was recorded: the outcome is unknown. */
|
|
58
|
+
export function startedWithoutEnd(stateDir, ref) {
|
|
59
|
+
try {
|
|
60
|
+
return existsSync(markPath(stateDir, ref));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,3 +14,5 @@ export type { ApprovalRow, ApproveResult } from "./approvals.ts";
|
|
|
14
14
|
export type { CardCsvOpts, ChannelRow, ReconcileReport } from "./reconcile.ts";
|
|
15
15
|
export type { FileLedgerOpts, LedgerCounts } from "./ledger.ts";
|
|
16
16
|
export type { EffectSigner, ExplainChain, ExplainPair, ExplainFinding, ExplainOpts, ExplainResult, ExplainTrustRoot, ExplainWarning, ExtractWindow, Ledger, LedgerEffect, Policy, PolicyDecision, Principal, ProxyDeps, RecordSigner, ToolCall, ToolResult, WitnessClass, } from "./types.ts";
|
|
17
|
+
export { verifyLedger } from "./verify-ledger.ts";
|
|
18
|
+
export type { VerifyResult, VerifyOptions, VerifyTrust } from "./verify-ledger.ts";
|
package/dist/index.js
CHANGED
package/dist/proxy.js
CHANGED
|
@@ -4,6 +4,7 @@ import { signDecisionRecord } from "@cedulon/core";
|
|
|
4
4
|
import { approvePending, approvalsLogFor, createApprovalBudgetGuard, drainApprovalCommands, spentTodayMinorOf, } from "./approvals.js";
|
|
5
5
|
import { SerialQueue } from "./serial-queue.js";
|
|
6
6
|
import { diskProbe } from "./disk.js";
|
|
7
|
+
import { markEnded, markStarted, startedWithoutEnd } from "./in-flight-log.js";
|
|
7
8
|
import { effectDescriptor, sha256Canonical } from "./hash.js";
|
|
8
9
|
import { inputsLogFor } from "./inputs.js";
|
|
9
10
|
import { countedWork, hasPrimaryEffect, lookupDecisionByRef, lookupReauthByHash, lookupResolvedBy, noteResolution, noteTenantRef, } from "./ledger.js";
|
|
@@ -64,6 +65,21 @@ function allowedReplay(ref) {
|
|
|
64
65
|
isError: false,
|
|
65
66
|
};
|
|
66
67
|
}
|
|
68
|
+
function threwReplay(ref) {
|
|
69
|
+
// The tool ran and exploded. That is not a deny — the gate allowed it.
|
|
70
|
+
// `allowed:` would teach a retry library the opposite of the ledger.
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: "text", text: `threw:${ref}` }],
|
|
73
|
+
isError: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async function replayAfterEffect(ledger, ref) {
|
|
77
|
+
const primary = (await ledger.effects()).find((e) => e.row.ref === ref && e.row.effectClass !== "duplicate-effect");
|
|
78
|
+
if (primary?.row.effectClass.endsWith(":threw")) {
|
|
79
|
+
return threwReplay(ref);
|
|
80
|
+
}
|
|
81
|
+
return allowedReplay(ref);
|
|
82
|
+
}
|
|
67
83
|
function deepFreeze(value) {
|
|
68
84
|
if (value === null || typeof value !== "object")
|
|
69
85
|
return value;
|
|
@@ -294,6 +310,11 @@ export function createProxy(deps) {
|
|
|
294
310
|
};
|
|
295
311
|
}
|
|
296
312
|
function launchInner(dispatchedCall, principal, allowRef, flightKey) {
|
|
313
|
+
// Written before the tool runs, removed after the effect is recorded. A
|
|
314
|
+
// mark that survives the process is what tells a restarted body that this
|
|
315
|
+
// ref was already started (in-flight-log.ts).
|
|
316
|
+
if (stateDir !== null)
|
|
317
|
+
markStarted(stateDir, flightKey, dispatchedCall.name);
|
|
297
318
|
const work = runInner(dispatchedCall, principal, allowRef);
|
|
298
319
|
inFlight.set(flightKey, work);
|
|
299
320
|
return { kind: "run", work, key: flightKey, replayRef: allowRef };
|
|
@@ -455,7 +476,7 @@ export function createProxy(deps) {
|
|
|
455
476
|
await approvals.updateStatus(scopedRef, "approved", { allowRef: allow.ref });
|
|
456
477
|
}
|
|
457
478
|
if (await hasPrimaryEffect(deps.ledger, allow.ref)) {
|
|
458
|
-
return { kind: "done", result:
|
|
479
|
+
return { kind: "done", result: await replayAfterEffect(deps.ledger, allow.ref) };
|
|
459
480
|
}
|
|
460
481
|
if (allow.subject === "spend") {
|
|
461
482
|
return {
|
|
@@ -469,6 +490,27 @@ export function createProxy(deps) {
|
|
|
469
490
|
const flying = inFlight.get(scopedRef);
|
|
470
491
|
if (flying)
|
|
471
492
|
return { kind: "wait", work: flying, replayRef: allow.ref };
|
|
493
|
+
// An approved allow means "the operator said yes", not "it ran",
|
|
494
|
+
// so the first retry after an approval is the first run. Only a
|
|
495
|
+
// mark left on disk says a run was already started and never
|
|
496
|
+
// finished — that one cannot be run again.
|
|
497
|
+
if (stateDir !== null && startedWithoutEnd(stateDir, scopedRef)) {
|
|
498
|
+
const unknownRef = deps.nonce();
|
|
499
|
+
await writeRecord({
|
|
500
|
+
decision: "deny",
|
|
501
|
+
reasonCode: "outcome-unknown",
|
|
502
|
+
ref: unknownRef,
|
|
503
|
+
requestHash,
|
|
504
|
+
inputs: {
|
|
505
|
+
...resolved.inputs,
|
|
506
|
+
approver: { id: "verax-proxy", via: "proxy", resolves: scopedRef },
|
|
507
|
+
},
|
|
508
|
+
effectHash: null,
|
|
509
|
+
subject: call.name,
|
|
510
|
+
timestampMs,
|
|
511
|
+
});
|
|
512
|
+
return { kind: "done", result: denied("outcome-unknown", unknownRef) };
|
|
513
|
+
}
|
|
472
514
|
return launchInner(dispatched, principal, allow.ref, scopedRef);
|
|
473
515
|
}
|
|
474
516
|
return { kind: "done", result: deferred(given) };
|
|
@@ -478,7 +520,7 @@ export function createProxy(deps) {
|
|
|
478
520
|
}
|
|
479
521
|
if (existing.decision === "allow" && existing.ref) {
|
|
480
522
|
if (await hasPrimaryEffect(deps.ledger, existing.ref)) {
|
|
481
|
-
return { kind: "done", result:
|
|
523
|
+
return { kind: "done", result: await replayAfterEffect(deps.ledger, existing.ref) };
|
|
482
524
|
}
|
|
483
525
|
if (existing.subject === "spend") {
|
|
484
526
|
return {
|
|
@@ -585,17 +627,23 @@ export function createProxy(deps) {
|
|
|
585
627
|
if (plan.kind === "wait") {
|
|
586
628
|
try {
|
|
587
629
|
await plan.work;
|
|
630
|
+
return allowedReplay(plan.replayRef);
|
|
588
631
|
}
|
|
589
632
|
catch {
|
|
590
|
-
// The first call recorded the throw on its effect row.
|
|
633
|
+
// The first call recorded the throw on its effect row. The
|
|
634
|
+
// waiter must not call the tool again, and must not say allowed.
|
|
635
|
+
return threwReplay(plan.replayRef);
|
|
591
636
|
}
|
|
592
|
-
return allowedReplay(plan.replayRef);
|
|
593
637
|
}
|
|
594
638
|
try {
|
|
595
639
|
return await plan.work;
|
|
596
640
|
}
|
|
597
641
|
finally {
|
|
598
642
|
inFlight.delete(plan.key);
|
|
643
|
+
// runInner has written its effect row by now — on the throw path too,
|
|
644
|
+
// as `<name>:threw`. The outcome is on the ledger, so the mark goes.
|
|
645
|
+
if (stateDir !== null)
|
|
646
|
+
markEnded(stateDir, plan.key);
|
|
599
647
|
}
|
|
600
648
|
},
|
|
601
649
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type VerifyTrust = {
|
|
2
|
+
/** `pinned`: the caller supplied the key. `in-ledger`: read from the records. */
|
|
3
|
+
source: "pinned" | "in-ledger" | "none";
|
|
4
|
+
publicKeyPem: string | null;
|
|
5
|
+
note: string;
|
|
6
|
+
};
|
|
7
|
+
export type VerifyResult = {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
directory: string;
|
|
10
|
+
decisions: number;
|
|
11
|
+
effects: number;
|
|
12
|
+
signaturesValid: number;
|
|
13
|
+
signaturesInvalid: number;
|
|
14
|
+
/** Index of the first record whose `prevRecordHash` does not match, else null. */
|
|
15
|
+
chainBreakAt: number | null;
|
|
16
|
+
effectsBound: number;
|
|
17
|
+
effectsOrphaned: number;
|
|
18
|
+
trust: VerifyTrust;
|
|
19
|
+
problems: string[];
|
|
20
|
+
};
|
|
21
|
+
export type VerifyOptions = {
|
|
22
|
+
/** Verify against this key instead of the one the records carry. */
|
|
23
|
+
publicKeyPem?: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function verifyLedger(dir: string, opts?: VerifyOptions): Promise<VerifyResult>;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads a ledger directory and says whether it still holds together, with no
|
|
3
|
+
* body running and nothing on the network.
|
|
4
|
+
*
|
|
5
|
+
* This is the answer to a question a customer is right to ask: if we stopped
|
|
6
|
+
* using Verax tomorrow, would these files still mean anything? Evidence that
|
|
7
|
+
* can only be read by the vendor who sold it is a weak kind of evidence.
|
|
8
|
+
*
|
|
9
|
+
* Three things fail separately, so three things are stated separately:
|
|
10
|
+
*
|
|
11
|
+
* signatures each record verifies against a public key
|
|
12
|
+
* chain each record names the hash of the one before it
|
|
13
|
+
* effects each effect row is bound to a decision by `effectHash`
|
|
14
|
+
*
|
|
15
|
+
* And a fourth, which matters most and is the easiest to fudge: **which key**.
|
|
16
|
+
* A ledger checked against the key sitting next to it is internally
|
|
17
|
+
* consistent and nothing more — whatever could write the file could write
|
|
18
|
+
* that key too (the downstream audit showed exactly this: a stdio child runs
|
|
19
|
+
* as the same user and can read `keys/*.pem`). So the trust source is part of
|
|
20
|
+
* the answer rather than a footnote, and `publicKeyPem` lets a reader pin a
|
|
21
|
+
* copy they hold themselves.
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { findDecisionRecordChainBreak, verifyDecisionRecord } from "@cedulon/core";
|
|
26
|
+
import { readLedgerManifest } from "./ledger-manifest.js";
|
|
27
|
+
/** Every decisions file this directory holds, oldest piece first. */
|
|
28
|
+
function decisionFiles(dir) {
|
|
29
|
+
const manifest = readLedgerManifest(dir);
|
|
30
|
+
if (manifest && Array.isArray(manifest.pieces) && manifest.pieces.length > 0) {
|
|
31
|
+
return manifest.pieces.map((p) => join(dir, p.decisions)).filter((p) => existsSync(p));
|
|
32
|
+
}
|
|
33
|
+
const tek = join(dir, "decisions.jsonl");
|
|
34
|
+
return existsSync(tek) ? [tek] : [];
|
|
35
|
+
}
|
|
36
|
+
function effectFiles(dir) {
|
|
37
|
+
const manifest = readLedgerManifest(dir);
|
|
38
|
+
if (manifest && Array.isArray(manifest.pieces) && manifest.pieces.length > 0) {
|
|
39
|
+
return manifest.pieces.map((p) => join(dir, p.effects)).filter((p) => existsSync(p));
|
|
40
|
+
}
|
|
41
|
+
const tek = join(dir, "effects.jsonl");
|
|
42
|
+
return existsSync(tek) ? [tek] : [];
|
|
43
|
+
}
|
|
44
|
+
/** Parses JSONL, reporting the line a bad row sits on rather than throwing. */
|
|
45
|
+
function readJsonl(path, problems) {
|
|
46
|
+
const out = [];
|
|
47
|
+
const text = readFileSync(path, "utf8");
|
|
48
|
+
const lines = text.split("\n");
|
|
49
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
50
|
+
const line = lines[i].trim();
|
|
51
|
+
if (line === "")
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
out.push(JSON.parse(line));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
problems.push(`unreadable row: ${path}:${i + 1} is not JSON`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
export async function verifyLedger(dir, opts = {}) {
|
|
63
|
+
const problems = [];
|
|
64
|
+
const kararDosyalari = decisionFiles(dir);
|
|
65
|
+
const records = [];
|
|
66
|
+
for (const path of kararDosyalari) {
|
|
67
|
+
for (const row of readJsonl(path, problems)) {
|
|
68
|
+
records.push(row);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// An empty directory is not a clean ledger. Saying "valid" over no records
|
|
72
|
+
// would let a deleted ledger pass as a verified one.
|
|
73
|
+
if (records.length === 0) {
|
|
74
|
+
problems.push("no decisions found: this directory holds no ledger to verify");
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
directory: dir,
|
|
78
|
+
decisions: 0,
|
|
79
|
+
effects: 0,
|
|
80
|
+
signaturesValid: 0,
|
|
81
|
+
signaturesInvalid: 0,
|
|
82
|
+
chainBreakAt: null,
|
|
83
|
+
effectsBound: 0,
|
|
84
|
+
effectsOrphaned: 0,
|
|
85
|
+
trust: { source: "none", publicKeyPem: null, note: "no records, so no key was used" },
|
|
86
|
+
problems,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const pinned = opts.publicKeyPem?.trim();
|
|
90
|
+
const icerden = typeof records[0]?.publicKeyPem === "string" ? records[0].publicKeyPem : null;
|
|
91
|
+
const anahtar = pinned && pinned !== "" ? pinned : icerden;
|
|
92
|
+
const trust = pinned && pinned !== ""
|
|
93
|
+
? {
|
|
94
|
+
source: "pinned",
|
|
95
|
+
publicKeyPem: pinned,
|
|
96
|
+
note: "verified against a key the reader supplied, not one taken from these files",
|
|
97
|
+
}
|
|
98
|
+
: {
|
|
99
|
+
source: "in-ledger",
|
|
100
|
+
publicKeyPem: icerden,
|
|
101
|
+
note: "verified against the key carried in the records themselves: this shows the files are " +
|
|
102
|
+
"internally consistent, not that the key was ever trusted. Pin a key you hold to check that.",
|
|
103
|
+
};
|
|
104
|
+
let signaturesValid = 0;
|
|
105
|
+
let signaturesInvalid = 0;
|
|
106
|
+
for (let i = 0; i < records.length; i += 1) {
|
|
107
|
+
const rec = records[i];
|
|
108
|
+
let gecerli = false;
|
|
109
|
+
try {
|
|
110
|
+
gecerli = verifyDecisionRecord(rec, anahtar ?? undefined);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
gecerli = false;
|
|
114
|
+
}
|
|
115
|
+
if (gecerli) {
|
|
116
|
+
signaturesValid += 1;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
signaturesInvalid += 1;
|
|
120
|
+
const ref = typeof rec.claims?.ref === "string" ? rec.claims.ref : "?";
|
|
121
|
+
problems.push(`signature does not verify: record ${i} (ref ${ref})`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Returns `{ index, reason }` or null. The reason is carried through: a
|
|
125
|
+
// broken link and a bad signature are different accidents, and a reader
|
|
126
|
+
// chasing one should not be told the other.
|
|
127
|
+
const brk = findDecisionRecordChainBreak(records, anahtar ? [anahtar] : undefined);
|
|
128
|
+
const chainBreakAt = brk ? brk.index : null;
|
|
129
|
+
if (brk) {
|
|
130
|
+
problems.push(`chain breaks at record ${brk.index} (${brk.reason}): a row was changed, removed or inserted`);
|
|
131
|
+
}
|
|
132
|
+
// Each effect names the decision it belongs to. An effect whose ref is on no
|
|
133
|
+
// record is an action with no decision behind it — the loudest thing this
|
|
134
|
+
// file can find, so it is counted rather than summarised away.
|
|
135
|
+
const refler = new Set();
|
|
136
|
+
for (const r of records) {
|
|
137
|
+
if (typeof r.claims?.ref === "string")
|
|
138
|
+
refler.add(r.claims.ref);
|
|
139
|
+
}
|
|
140
|
+
let effects = 0;
|
|
141
|
+
let effectsBound = 0;
|
|
142
|
+
let effectsOrphaned = 0;
|
|
143
|
+
for (const path of effectFiles(dir)) {
|
|
144
|
+
for (const row of readJsonl(path, problems)) {
|
|
145
|
+
effects += 1;
|
|
146
|
+
const e = row;
|
|
147
|
+
const ref = typeof e.row?.ref === "string" ? e.row.ref : null;
|
|
148
|
+
if (ref !== null && refler.has(ref)) {
|
|
149
|
+
effectsBound += 1;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
effectsOrphaned += 1;
|
|
153
|
+
problems.push(`effect with no decision: ref ${ref ?? "(missing)"}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const ok = problems.length === 0 && signaturesInvalid === 0 && chainBreakAt === null && effectsOrphaned === 0;
|
|
158
|
+
return {
|
|
159
|
+
ok,
|
|
160
|
+
directory: dir,
|
|
161
|
+
decisions: records.length,
|
|
162
|
+
effects,
|
|
163
|
+
signaturesValid,
|
|
164
|
+
signaturesInvalid,
|
|
165
|
+
chainBreakAt,
|
|
166
|
+
effectsBound,
|
|
167
|
+
effectsOrphaned,
|
|
168
|
+
trust,
|
|
169
|
+
problems,
|
|
170
|
+
};
|
|
171
|
+
}
|